Microduck RL: exact DuckEMW recipe, cap 2.0, model_58000 (1.65 m/s @ cmd 2.0)
Browse files- README.md +48 -0
- checkpoints/model_58000.pt +3 -0
- microduck_running_env_cfg.py +443 -0
- scripts/eval_sprint_speed.py +108 -0
- src/mjlab_microduck/tasks/mdp.py +0 -0
- src/mjlab_microduck/tasks/microduck_running_env_cfg.py +443 -0
README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
tags:
|
| 3 |
+
- reinforcement-learning
|
| 4 |
+
- locomotion
|
| 5 |
+
- bipedal
|
| 6 |
+
- microduck
|
| 7 |
+
- pollen-robotics
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# Microduck RL — 2.0 m/s running push
|
| 11 |
+
|
| 12 |
+
Fork of [pollen-robotics/microduck_rl](https://github.com/pollen-robotics/microduck_rl) training
|
| 13 |
+
Pollin Robotics **Microduck** (25 cm open-source biped, XL330 servos) to run at a
|
| 14 |
+
**measured mean 2.0 m/s** forward speed via PPO (rsl_rl + MuJoCo Warp / BAM).
|
| 15 |
+
|
| 16 |
+
## Honest status (2026-09-12)
|
| 17 |
+
|
| 18 |
+
- Recipe: exact DuckEMW running config, **only deviation = speed cap raised to 2.0**
|
| 19 |
+
(`MICRODUCK_RUNNING_SPEED_CAP=2.0`, `MICRODUCK_RUNNING_TARGET_MAX_SPEED=2.0`).
|
| 20 |
+
No flight reward, no Su-style rewards, no achievement gate — reverted to the proven base.
|
| 21 |
+
- Measured mean forward speed (world-displacement eval, `scripts/eval_sprint_speed.py`):
|
| 22 |
+
- cmd 0.8 → 0.90 m/s
|
| 23 |
+
- cmd 1.2 → 1.32 m/s
|
| 24 |
+
- cmd 1.6 → 1.54 m/s
|
| 25 |
+
- **cmd 2.0 → 1.65 m/s** (p10 1.45, p90 1.78, max 1.89, ~0 resets)
|
| 26 |
+
- Goal (mean 2.0 @ cmd 2.0) **NOT yet reached** — training still climbing (~+0.3 m/s per 40k iters).
|
| 27 |
+
|
| 28 |
+
## How speed is measured (no lies)
|
| 29 |
+
|
| 30 |
+
`scripts/eval_sprint_speed.py` measures true world-frame displacement
|
| 31 |
+
(`root_link_pos_w` delta / time) across 64 envs, reporting mean / p10 / p90 / max / resets
|
| 32 |
+
per command. Reward/curriculum metrics are NEVER reported as speed.
|
| 33 |
+
|
| 34 |
+
## Run it
|
| 35 |
+
|
| 36 |
+
```bash
|
| 37 |
+
export MICRODUCK_RUNNING_SPEED_CAP=2.0
|
| 38 |
+
export MICRODUCK_RUNNING_TARGET_MAX_SPEED=2.0
|
| 39 |
+
.venv/bin/train Mjlab-Running-Flat-MicroDuck \
|
| 40 |
+
--env.scene.num-envs 4096 --agent.max-iterations 80000
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
## Files
|
| 44 |
+
|
| 45 |
+
- `src/mjlab_microduck/tasks/microduck_running_env_cfg.py` — running task cfg + knobs
|
| 46 |
+
- `src/mjlab_microduck/tasks/mdp.py` — rewards + speed curriculum
|
| 47 |
+
- `scripts/eval_sprint_speed.py` — lie-proof world-displacement eval
|
| 48 |
+
- `model_58000.pt` — latest checkpoint (measured 1.65 m/s @ cmd 2.0)
|
checkpoints/model_58000.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b06b59ca7c1beaa7c004555cb81124a9640baef30e23890acc49860913ef4bbd
|
| 3 |
+
size 4843527
|
microduck_running_env_cfg.py
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Forward-running task for Microduck.
|
| 2 |
+
|
| 3 |
+
This deliberately starts from the proven velocity environment so the 61D
|
| 4 |
+
observation contract, BAM actuator model, delays, noise, and sim2real domain
|
| 5 |
+
randomization remain identical to the walking policy family. The task is
|
| 6 |
+
forward-only and rewards measured progress beyond ordinary walking speed.
|
| 7 |
+
|
| 8 |
+
Two registered recipes use this file:
|
| 9 |
+
|
| 10 |
+
* ``Running`` optimizes forward progress without prescribing a gait.
|
| 11 |
+
* ``RunningFlight`` adds a small reward only when a controlled flight phase
|
| 12 |
+
begins. It does not pay for airtime, so a long ballistic fall is not useful.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import math
|
| 16 |
+
import os
|
| 17 |
+
from copy import deepcopy
|
| 18 |
+
|
| 19 |
+
from mjlab.managers import (
|
| 20 |
+
CurriculumTermCfg,
|
| 21 |
+
EventTermCfg,
|
| 22 |
+
RewardTermCfg,
|
| 23 |
+
SceneEntityCfg,
|
| 24 |
+
)
|
| 25 |
+
from mjlab.tasks.velocity import mdp as velocity_mdp
|
| 26 |
+
|
| 27 |
+
from mjlab_microduck.tasks import mdp as microduck_mdp
|
| 28 |
+
from mjlab_microduck.tasks.microduck_velocity_env_cfg import (
|
| 29 |
+
NUM_STEPS_PER_ENV,
|
| 30 |
+
VELOCITY_PUSH_INTERVAL_S,
|
| 31 |
+
MicroduckRlCfg,
|
| 32 |
+
make_microduck_velocity_env_cfg,
|
| 33 |
+
)
|
| 34 |
+
from mjlab_microduck.tasks.symmetry import SYMMETRY_CFG
|
| 35 |
+
|
| 36 |
+
RUNNING_PLAY_SPEED = 1.0
|
| 37 |
+
RUNNING_SPEED_CAP = float(os.environ.get("MICRODUCK_RUNNING_SPEED_CAP", "1.4"))
|
| 38 |
+
RUNNING_TARGET_MAX_SPEED = float(
|
| 39 |
+
os.environ.get("MICRODUCK_RUNNING_TARGET_MAX_SPEED", "1.2")
|
| 40 |
+
)
|
| 41 |
+
RUNNING_FINAL_ACTION_RATE_WEIGHT = float(
|
| 42 |
+
os.environ.get("MICRODUCK_RUNNING_ACTION_RATE_WEIGHT", "-0.10")
|
| 43 |
+
)
|
| 44 |
+
RUNNING_FORWARD_PROGRESS_WEIGHT = float(
|
| 45 |
+
os.environ.get("MICRODUCK_RUNNING_FORWARD_PROGRESS_WEIGHT", "5.0")
|
| 46 |
+
)
|
| 47 |
+
RUNNING_HIGH_SPEED_STAGE_INTERVAL = int(
|
| 48 |
+
os.environ.get("MICRODUCK_RUNNING_HIGH_SPEED_STAGE_INTERVAL", "500")
|
| 49 |
+
)
|
| 50 |
+
RUNNING_HIGH_SPEED_STEP = float(
|
| 51 |
+
os.environ.get("MICRODUCK_RUNNING_HIGH_SPEED_STEP", "0.15")
|
| 52 |
+
)
|
| 53 |
+
RUNNING_FLIGHT_REWARD_WEIGHT = float(
|
| 54 |
+
os.environ.get("MICRODUCK_RUNNING_FLIGHT_WEIGHT", "0.0")
|
| 55 |
+
)
|
| 56 |
+
RUNNING_GATE_PERFORMANCE = os.environ.get("MICRODUCK_RUNNING_GATE_PERFORMANCE") == "1"
|
| 57 |
+
RUNNING_ENABLE_SYMMETRY = os.environ.get("MICRODUCK_RUNNING_ENABLE_SYMMETRY") == "1"
|
| 58 |
+
RUNNING_ENABLE_HEADING_FEEDBACK = (
|
| 59 |
+
os.environ.get("MICRODUCK_RUNNING_ENABLE_HEADING_FEEDBACK") == "1"
|
| 60 |
+
)
|
| 61 |
+
RUNNING_CADENCE_WEIGHT = float(os.environ.get("MICRODUCK_RUNNING_CADENCE_WEIGHT", "0.0"))
|
| 62 |
+
RUNNING_FAST_TOUCH_WEIGHT = float(os.environ.get("MICRODUCK_RUNNING_FAST_TOUCH_WEIGHT", "0.0"))
|
| 63 |
+
RUNNING_EXPLOSIVE_ACCEL_WEIGHT = float(os.environ.get("MICRODUCK_RUNNING_EXPLOSIVE_ACCEL_WEIGHT", "0.0"))
|
| 64 |
+
RUNNING_STANDING_FRACTION = 0.03
|
| 65 |
+
RUNNING_CURRICULUM_START_ITERATION = int(
|
| 66 |
+
os.environ.get("MICRODUCK_RUNNING_CURRICULUM_START_ITERATION", "0")
|
| 67 |
+
)
|
| 68 |
+
RUNNING_ROBUST_PUSH_MPS = float(
|
| 69 |
+
os.environ.get("MICRODUCK_RUNNING_ROBUST_PUSH_MPS", "0")
|
| 70 |
+
)
|
| 71 |
+
RUNNING_ROBUST_TRUNK_COM_M = float(
|
| 72 |
+
os.environ.get("MICRODUCK_RUNNING_ROBUST_TRUNK_COM_M", "0")
|
| 73 |
+
)
|
| 74 |
+
RUNNING_ROBUST_HEAD_COM_M = float(
|
| 75 |
+
os.environ.get("MICRODUCK_RUNNING_ROBUST_HEAD_COM_M", "0")
|
| 76 |
+
)
|
| 77 |
+
RUNNING_ROBUST_INITIAL_TILT_DEG = float(
|
| 78 |
+
os.environ.get("MICRODUCK_RUNNING_ROBUST_INITIAL_TILT_DEG", "0")
|
| 79 |
+
)
|
| 80 |
+
RUNNING_ROBUST_FRICTION_MIN = float(
|
| 81 |
+
os.environ.get("MICRODUCK_RUNNING_ROBUST_FRICTION_MIN", "0.7")
|
| 82 |
+
)
|
| 83 |
+
RUNNING_ROBUST_FRICTION_MAX = float(
|
| 84 |
+
os.environ.get("MICRODUCK_RUNNING_ROBUST_FRICTION_MAX", "1.3")
|
| 85 |
+
)
|
| 86 |
+
RUNNING_ROBUST_PUSH_INTERVAL_MIN_S = float(
|
| 87 |
+
os.environ.get("MICRODUCK_RUNNING_ROBUST_PUSH_INTERVAL_MIN_S", "3.0")
|
| 88 |
+
)
|
| 89 |
+
RUNNING_ROBUST_PUSH_INTERVAL_MAX_S = float(
|
| 90 |
+
os.environ.get("MICRODUCK_RUNNING_ROBUST_PUSH_INTERVAL_MAX_S", "6.0")
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
# The lower edge rises too: zero-command behavior comes from the explicit
|
| 94 |
+
# standing bucket, rather than consuming most running samples near zero.
|
| 95 |
+
_RUNNING_BASE_SPEED_STAGES = (
|
| 96 |
+
{"step": 0 * NUM_STEPS_PER_ENV, "min_speed": 0.20, "max_speed": 0.45},
|
| 97 |
+
{"step": 1000 * NUM_STEPS_PER_ENV, "min_speed": 0.30, "max_speed": 0.55},
|
| 98 |
+
{"step": 2000 * NUM_STEPS_PER_ENV, "min_speed": 0.40, "max_speed": 0.65},
|
| 99 |
+
{"step": 3000 * NUM_STEPS_PER_ENV, "min_speed": 0.50, "max_speed": 0.75},
|
| 100 |
+
{"step": 4000 * NUM_STEPS_PER_ENV, "min_speed": 0.60, "max_speed": 0.85},
|
| 101 |
+
{"step": 5000 * NUM_STEPS_PER_ENV, "min_speed": 0.70, "max_speed": 0.95},
|
| 102 |
+
{"step": 6000 * NUM_STEPS_PER_ENV, "min_speed": 0.90, "max_speed": 1.20},
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def _running_speed_stages(
|
| 107 |
+
target_max_speed: float, high_speed_stage_interval: int = 500, high_speed_step: float = 0.15
|
| 108 |
+
) -> tuple[dict, ...]:
|
| 109 |
+
"""Return the proven base curriculum plus an optional continuation ramp."""
|
| 110 |
+
if target_max_speed < 1.2:
|
| 111 |
+
raise ValueError("running target max speed must be at least 1.2 m/s")
|
| 112 |
+
if high_speed_stage_interval <= 0:
|
| 113 |
+
raise ValueError("running high-speed stage interval must be positive")
|
| 114 |
+
if target_max_speed == 1.2:
|
| 115 |
+
return _RUNNING_BASE_SPEED_STAGES
|
| 116 |
+
intermediate_1 = min(1.35, target_max_speed)
|
| 117 |
+
intermediate_2 = min(1.50, target_max_speed)
|
| 118 |
+
intermediate_3 = min(1.65, target_max_speed)
|
| 119 |
+
stages = _RUNNING_BASE_SPEED_STAGES + (
|
| 120 |
+
{
|
| 121 |
+
"step": 6750 * NUM_STEPS_PER_ENV,
|
| 122 |
+
"min_speed": 0.95,
|
| 123 |
+
"max_speed": intermediate_1,
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
"step": 7250 * NUM_STEPS_PER_ENV,
|
| 127 |
+
"min_speed": 1.05,
|
| 128 |
+
"max_speed": intermediate_2,
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
"step": 7750 * NUM_STEPS_PER_ENV,
|
| 132 |
+
"min_speed": 1.15,
|
| 133 |
+
"max_speed": intermediate_3,
|
| 134 |
+
},
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
# Checkpoint 8,749 has consolidated the 1.65 m/s slice. Targets above
|
| 138 |
+
# that must arrive as new post-checkpoint stages; replacing the 7,750
|
| 139 |
+
# target would make a resumed run jump immediately to its final command.
|
| 140 |
+
if target_max_speed <= 1.65:
|
| 141 |
+
return stages
|
| 142 |
+
|
| 143 |
+
high_speed_targets: list[float] = []
|
| 144 |
+
next_target = 1.80
|
| 145 |
+
while next_target < target_max_speed - 1e-9:
|
| 146 |
+
high_speed_targets.append(round(next_target, 2))
|
| 147 |
+
next_target += high_speed_step
|
| 148 |
+
high_speed_targets.append(target_max_speed)
|
| 149 |
+
|
| 150 |
+
return stages + tuple(
|
| 151 |
+
{
|
| 152 |
+
"step": (8750 + index * high_speed_stage_interval)
|
| 153 |
+
* NUM_STEPS_PER_ENV,
|
| 154 |
+
"min_speed": round(max(1.25, max_speed - 0.55), 2),
|
| 155 |
+
"max_speed": max_speed,
|
| 156 |
+
}
|
| 157 |
+
for index, max_speed in enumerate(high_speed_targets)
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
RUNNING_SPEED_STAGES = _running_speed_stages(
|
| 162 |
+
RUNNING_TARGET_MAX_SPEED, RUNNING_HIGH_SPEED_STAGE_INTERVAL, RUNNING_HIGH_SPEED_STEP
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def _apply_running_robustness(
|
| 167 |
+
cfg,
|
| 168 |
+
*,
|
| 169 |
+
push_mps: float,
|
| 170 |
+
trunk_com_m: float,
|
| 171 |
+
head_com_m: float,
|
| 172 |
+
initial_tilt_deg: float,
|
| 173 |
+
friction_range: tuple[float, float],
|
| 174 |
+
push_interval_s: tuple[float, float] = VELOCITY_PUSH_INTERVAL_S,
|
| 175 |
+
) -> None:
|
| 176 |
+
"""Apply one fixed continuation stage without resume-step ambiguity."""
|
| 177 |
+
values = (push_mps, trunk_com_m, head_com_m, initial_tilt_deg)
|
| 178 |
+
if any(value < 0.0 for value in values):
|
| 179 |
+
raise ValueError("running robustness magnitudes must be non-negative")
|
| 180 |
+
if not 0.0 < friction_range[0] <= friction_range[1]:
|
| 181 |
+
raise ValueError("running robustness friction range must be positive and ordered")
|
| 182 |
+
if not 0.0 < push_interval_s[0] <= push_interval_s[1]:
|
| 183 |
+
raise ValueError("running robustness push interval must be positive and ordered")
|
| 184 |
+
|
| 185 |
+
cfg.events["foot_friction"].params["ranges"] = friction_range
|
| 186 |
+
if push_mps > 0.0:
|
| 187 |
+
velocity_range = {
|
| 188 |
+
"x": (-push_mps, push_mps),
|
| 189 |
+
"y": (-push_mps, push_mps),
|
| 190 |
+
}
|
| 191 |
+
if "push_robot" in cfg.events:
|
| 192 |
+
cfg.events["push_robot"].params["velocity_range"] = velocity_range
|
| 193 |
+
cfg.events["push_robot"].interval_range_s = push_interval_s
|
| 194 |
+
else:
|
| 195 |
+
cfg.events["push_robot"] = EventTermCfg(
|
| 196 |
+
func=velocity_mdp.push_by_setting_velocity,
|
| 197 |
+
mode="interval",
|
| 198 |
+
interval_range_s=push_interval_s,
|
| 199 |
+
params={
|
| 200 |
+
"velocity_range": velocity_range,
|
| 201 |
+
"asset_cfg": SceneEntityCfg("robot"),
|
| 202 |
+
},
|
| 203 |
+
)
|
| 204 |
+
else:
|
| 205 |
+
cfg.events.pop("push_robot", None)
|
| 206 |
+
|
| 207 |
+
if trunk_com_m > 0.0:
|
| 208 |
+
cfg.events["randomize_com"].params["ranges"] = (
|
| 209 |
+
-trunk_com_m,
|
| 210 |
+
trunk_com_m,
|
| 211 |
+
)
|
| 212 |
+
if head_com_m > 0.0:
|
| 213 |
+
cfg.events["randomize_head_com"].params["ranges"] = (
|
| 214 |
+
-head_com_m,
|
| 215 |
+
head_com_m,
|
| 216 |
+
)
|
| 217 |
+
if initial_tilt_deg > 0.0:
|
| 218 |
+
cfg.events["randomize_base_orientation"] = EventTermCfg(
|
| 219 |
+
func=microduck_mdp.randomize_base_orientation,
|
| 220 |
+
mode="reset",
|
| 221 |
+
params={
|
| 222 |
+
"asset_cfg": SceneEntityCfg("robot"),
|
| 223 |
+
"max_pitch_deg": initial_tilt_deg,
|
| 224 |
+
"max_roll_deg": initial_tilt_deg,
|
| 225 |
+
},
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def make_microduck_running_env_cfg(
|
| 230 |
+
play: bool = False, flight_reward_weight: float = RUNNING_FLIGHT_REWARD_WEIGHT
|
| 231 |
+
):
|
| 232 |
+
"""Build the flat-ground, forward-only running environment."""
|
| 233 |
+
cfg = make_microduck_velocity_env_cfg(play=play, rough=False)
|
| 234 |
+
cfg.episode_length_s = 12.0
|
| 235 |
+
# Microduck is only ~25 cm tall; the velocity recipe's 3 m follow camera
|
| 236 |
+
# hides gait and contact details in rollout videos.
|
| 237 |
+
cfg.viewer.distance = 0.55
|
| 238 |
+
cfg.viewer.max_extra_envs = 0
|
| 239 |
+
|
| 240 |
+
command = cfg.commands["twist"]
|
| 241 |
+
command.rel_standing_envs = 0.0 if play else RUNNING_STANDING_FRACTION
|
| 242 |
+
command.rel_heading_envs = 0.0
|
| 243 |
+
command.rel_turn_in_place_envs = 0.0
|
| 244 |
+
command.resampling_time_range = (12.0, 12.0)
|
| 245 |
+
if play:
|
| 246 |
+
command.ranges.lin_vel_x = (RUNNING_PLAY_SPEED, RUNNING_PLAY_SPEED)
|
| 247 |
+
command.ranges.lin_vel_y = (0.0, 0.0)
|
| 248 |
+
command.ranges.ang_vel_z = (0.0, 0.0)
|
| 249 |
+
else:
|
| 250 |
+
command.ranges.lin_vel_x = (
|
| 251 |
+
RUNNING_SPEED_STAGES[0]["min_speed"],
|
| 252 |
+
RUNNING_SPEED_STAGES[0]["max_speed"],
|
| 253 |
+
)
|
| 254 |
+
# Tiny non-zero lateral/yaw ranges keep those command neurons alive,
|
| 255 |
+
# while still making essentially every active sample forward-running.
|
| 256 |
+
command.ranges.lin_vel_y = (-0.02, 0.02)
|
| 257 |
+
command.ranges.ang_vel_z = (-0.05, 0.05)
|
| 258 |
+
|
| 259 |
+
if RUNNING_ENABLE_HEADING_FEEDBACK:
|
| 260 |
+
# Slot 2 becomes signed spawn-heading error, recomputed every step. It
|
| 261 |
+
# remains in the same position and mirrors with the same sign rule as a
|
| 262 |
+
# yaw-rate command, so the deployment observation contract is unchanged.
|
| 263 |
+
command.heading_command = False
|
| 264 |
+
command.rel_heading_envs = 0.0
|
| 265 |
+
command.ranges.heading = None
|
| 266 |
+
command.ranges.ang_vel_z = (0.0, 0.0)
|
| 267 |
+
command = microduck_mdp.SpawnHeadingVelocityCommandCfg(
|
| 268 |
+
**vars(command), heading_error_clip=1.0
|
| 269 |
+
)
|
| 270 |
+
cfg.commands["twist"] = command
|
| 271 |
+
|
| 272 |
+
# Max-speed objective. Velocity tracking remains useful as a curriculum
|
| 273 |
+
# guide, but forward progress is strong enough that exceeding the command is
|
| 274 |
+
# profitable. This is what makes the optimum "as fast as possible".
|
| 275 |
+
cfg.rewards["forward_progress"] = RewardTermCfg(
|
| 276 |
+
func=microduck_mdp.running_forward_progress,
|
| 277 |
+
weight=RUNNING_FORWARD_PROGRESS_WEIGHT,
|
| 278 |
+
params={"speed_cap": RUNNING_SPEED_CAP},
|
| 279 |
+
)
|
| 280 |
+
cfg.rewards["track_linear_velocity"].weight = 2.0
|
| 281 |
+
cfg.rewards["track_linear_velocity"].params["std"] = math.sqrt(0.15)
|
| 282 |
+
cfg.rewards["track_angular_velocity"].weight = 0.5
|
| 283 |
+
cfg.rewards["track_angular_velocity"].params["std"] = math.sqrt(0.5)
|
| 284 |
+
cfg.rewards["planar_drift"] = RewardTermCfg(
|
| 285 |
+
func=microduck_mdp.running_planar_drift_cost,
|
| 286 |
+
weight=-0.05,
|
| 287 |
+
params={"command_name": "twist", "lateral_weight": 4.0},
|
| 288 |
+
)
|
| 289 |
+
# Rate tracking alone can only say "stop turning"; once a heading error
|
| 290 |
+
# exists it provides no signal for which way to steer back. Anchor yaw to
|
| 291 |
+
# each randomized spawn heading so straight running is the actual optimum.
|
| 292 |
+
cfg.rewards["heading_hold"] = RewardTermCfg(
|
| 293 |
+
func=microduck_mdp.heading_hold_reward,
|
| 294 |
+
weight=1.5,
|
| 295 |
+
params={"std": 0.4, "asset_cfg": SceneEntityCfg("robot")},
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
# Permit the forward lean and fast leg cycling a sprint needs. These stay
|
| 299 |
+
# non-zero only to rule out tumbling and unbounded thrash as cheap optima.
|
| 300 |
+
cfg.rewards["pose"].weight = 0.15
|
| 301 |
+
cfg.rewards["upright"].weight = 0.75
|
| 302 |
+
cfg.rewards["upright"].params["std"] = math.sqrt(0.15)
|
| 303 |
+
cfg.rewards["body_ang_vel"].weight = -0.01
|
| 304 |
+
cfg.rewards["angular_momentum"].weight = -0.005
|
| 305 |
+
cfg.rewards["action_rate_l2"].weight = -0.02
|
| 306 |
+
cfg.rewards["foot_slip"].weight = -0.05
|
| 307 |
+
cfg.rewards["air_time"].weight = 0.0
|
| 308 |
+
cfg.rewards["foot_clearance"].params["target_height"] = 0.015
|
| 309 |
+
cfg.rewards["foot_swing_height"].params["target_height"] = 0.015
|
| 310 |
+
|
| 311 |
+
# Head/body slots remain present and sample tiny non-zero ranges, preserving
|
| 312 |
+
# the 61D hot-swap contract, but posture precision must not block discovery.
|
| 313 |
+
cfg.rewards["head_pose_tracking"].weight = 0.0
|
| 314 |
+
cfg.rewards["head_pose_bias"].weight = 0.0
|
| 315 |
+
cfg.rewards["body_pose_tracking"].weight = 0.0
|
| 316 |
+
|
| 317 |
+
if flight_reward_weight > 0.0:
|
| 318 |
+
cfg.rewards["flight_event"] = RewardTermCfg(
|
| 319 |
+
func=microduck_mdp.running_flight_event,
|
| 320 |
+
weight=flight_reward_weight,
|
| 321 |
+
params={
|
| 322 |
+
"sensor_name": "feet_ground_contact",
|
| 323 |
+
"min_forward_speed": 0.30,
|
| 324 |
+
"max_tilt_deg": 50.0,
|
| 325 |
+
"min_airborne_steps": 3,
|
| 326 |
+
},
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
# Su-Bingtian-style sprint levers for a small biped (cadence + stiff/fast
|
| 330 |
+
# ground touch + explosive low-speed acceleration). All off by default;
|
| 331 |
+
# enable via env knobs so they can be tuned without code edits.
|
| 332 |
+
if RUNNING_CADENCE_WEIGHT != 0.0:
|
| 333 |
+
cfg.rewards["running_cadence"] = RewardTermCfg(
|
| 334 |
+
func=microduck_mdp.running_cadence,
|
| 335 |
+
weight=RUNNING_CADENCE_WEIGHT,
|
| 336 |
+
params={"sensor_name": "feet_ground_contact"},
|
| 337 |
+
)
|
| 338 |
+
if RUNNING_FAST_TOUCH_WEIGHT != 0.0:
|
| 339 |
+
cfg.rewards["running_fast_ground_touch"] = RewardTermCfg(
|
| 340 |
+
func=microduck_mdp.running_fast_ground_touch,
|
| 341 |
+
weight=RUNNING_FAST_TOUCH_WEIGHT,
|
| 342 |
+
params={"sensor_name": "feet_ground_contact"},
|
| 343 |
+
)
|
| 344 |
+
if RUNNING_EXPLOSIVE_ACCEL_WEIGHT != 0.0:
|
| 345 |
+
cfg.rewards["running_explosive_accel"] = RewardTermCfg(
|
| 346 |
+
func=microduck_mdp.running_explosive_accel,
|
| 347 |
+
weight=RUNNING_EXPLOSIVE_ACCEL_WEIGHT,
|
| 348 |
+
params={"command_name": "twist", "speed_cap": RUNNING_SPEED_CAP},
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
# A clean speed-discovery phase: no random velocity kicks and no scheduled
|
| 352 |
+
# widening of CoM/head commands or precision taxes. Fixed initial DR stays
|
| 353 |
+
# active, so the result is not a deterministic-sim-only policy.
|
| 354 |
+
_apply_running_robustness(
|
| 355 |
+
cfg,
|
| 356 |
+
push_mps=RUNNING_ROBUST_PUSH_MPS,
|
| 357 |
+
trunk_com_m=RUNNING_ROBUST_TRUNK_COM_M,
|
| 358 |
+
head_com_m=RUNNING_ROBUST_HEAD_COM_M,
|
| 359 |
+
initial_tilt_deg=RUNNING_ROBUST_INITIAL_TILT_DEG,
|
| 360 |
+
friction_range=(
|
| 361 |
+
RUNNING_ROBUST_FRICTION_MIN,
|
| 362 |
+
RUNNING_ROBUST_FRICTION_MAX,
|
| 363 |
+
),
|
| 364 |
+
push_interval_s=(
|
| 365 |
+
RUNNING_ROBUST_PUSH_INTERVAL_MIN_S,
|
| 366 |
+
RUNNING_ROBUST_PUSH_INTERVAL_MAX_S,
|
| 367 |
+
),
|
| 368 |
+
)
|
| 369 |
+
for name in (
|
| 370 |
+
"standing_envs",
|
| 371 |
+
"head_pose_range",
|
| 372 |
+
"body_pose_range",
|
| 373 |
+
"com_range",
|
| 374 |
+
"head_com_range",
|
| 375 |
+
"head_pose_bias_weight",
|
| 376 |
+
):
|
| 377 |
+
cfg.curriculum.pop(name, None)
|
| 378 |
+
|
| 379 |
+
if not play:
|
| 380 |
+
cfg.curriculum["running_speed_range"] = CurriculumTermCfg(
|
| 381 |
+
func=microduck_mdp.running_command_ranges_curriculum,
|
| 382 |
+
params={
|
| 383 |
+
"command_name": "twist",
|
| 384 |
+
"gate_on_performance": RUNNING_GATE_PERFORMANCE,
|
| 385 |
+
"gate_margin": 0.02,
|
| 386 |
+
"gate_min_steps": 500,
|
| 387 |
+
"speed_stages": [
|
| 388 |
+
{
|
| 389 |
+
**stage,
|
| 390 |
+
"step": stage["step"]
|
| 391 |
+
- RUNNING_CURRICULUM_START_ITERATION * NUM_STEPS_PER_ENV,
|
| 392 |
+
}
|
| 393 |
+
for stage in RUNNING_SPEED_STAGES
|
| 394 |
+
],
|
| 395 |
+
},
|
| 396 |
+
)
|
| 397 |
+
# Smoothness is introduced only after a fast gait exists.
|
| 398 |
+
cfg.curriculum["action_rate_weight"] = CurriculumTermCfg(
|
| 399 |
+
func=microduck_mdp.reward_weight,
|
| 400 |
+
params={
|
| 401 |
+
"reward_name": "action_rate_l2",
|
| 402 |
+
"weight_stages": [
|
| 403 |
+
{
|
| 404 |
+
"step": (0 - RUNNING_CURRICULUM_START_ITERATION)
|
| 405 |
+
* NUM_STEPS_PER_ENV,
|
| 406 |
+
"weight": -0.02,
|
| 407 |
+
},
|
| 408 |
+
{
|
| 409 |
+
"step": (2500 - RUNNING_CURRICULUM_START_ITERATION)
|
| 410 |
+
* NUM_STEPS_PER_ENV,
|
| 411 |
+
"weight": -0.05,
|
| 412 |
+
},
|
| 413 |
+
{
|
| 414 |
+
"step": (4000 - RUNNING_CURRICULUM_START_ITERATION)
|
| 415 |
+
* NUM_STEPS_PER_ENV,
|
| 416 |
+
"weight": RUNNING_FINAL_ACTION_RATE_WEIGHT,
|
| 417 |
+
},
|
| 418 |
+
{
|
| 419 |
+
"step": (5500 - RUNNING_CURRICULUM_START_ITERATION)
|
| 420 |
+
* NUM_STEPS_PER_ENV,
|
| 421 |
+
"weight": RUNNING_FINAL_ACTION_RATE_WEIGHT,
|
| 422 |
+
},
|
| 423 |
+
],
|
| 424 |
+
},
|
| 425 |
+
)
|
| 426 |
+
else:
|
| 427 |
+
cfg.curriculum.pop("action_rate_weight", None)
|
| 428 |
+
|
| 429 |
+
return cfg
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
MicroduckRunningRlCfg = deepcopy(MicroduckRlCfg)
|
| 433 |
+
MicroduckRunningRlCfg.experiment_name = "running"
|
| 434 |
+
MicroduckRunningRlCfg.run_name = "running-max-speed"
|
| 435 |
+
MicroduckRunningRlCfg.algorithm.entropy_coef = 0.02
|
| 436 |
+
MicroduckRunningRlCfg.algorithm.symmetry_cfg = (
|
| 437 |
+
deepcopy(SYMMETRY_CFG) if RUNNING_ENABLE_SYMMETRY else None
|
| 438 |
+
)
|
| 439 |
+
MicroduckRunningRlCfg.max_iterations = 7_500
|
| 440 |
+
|
| 441 |
+
MicroduckRunningFlightRlCfg = deepcopy(MicroduckRunningRlCfg)
|
| 442 |
+
MicroduckRunningFlightRlCfg.experiment_name = "running_flight"
|
| 443 |
+
MicroduckRunningFlightRlCfg.run_name = "running-flight-event"
|
scripts/eval_sprint_speed.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""eval_sprint_speed.py — 在 warp 训练环境内直接测 checkpoint 的真实速度。
|
| 2 |
+
|
| 3 |
+
绕开本地 CPU harness 的所有差异项(BAM/延迟/噪声/归一化烘焙),用训练同款
|
| 4 |
+
环境 + rsl_rl 推理策略(含 obs normalizer)回答:策略在原生环境里跑多快?
|
| 5 |
+
|
| 6 |
+
用法(实例上,工作目录 = repo 根):
|
| 7 |
+
uv run --no-sync python scripts/eval_sprint_speed.py \
|
| 8 |
+
--checkpoint logs/rsl_rl/sprint/<run>/model_1999.pt --vx 0.4 1.2 2.0
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import argparse
|
| 14 |
+
from dataclasses import asdict
|
| 15 |
+
|
| 16 |
+
import numpy as np
|
| 17 |
+
import torch
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def main():
|
| 21 |
+
ap = argparse.ArgumentParser()
|
| 22 |
+
ap.add_argument("--task", default="Mjlab-Sprint-Flat-MicroDuck")
|
| 23 |
+
ap.add_argument("--checkpoint", default=None)
|
| 24 |
+
ap.add_argument("--onnx", default=None, help="测 ONNX(归一化已烘焙)而非 .pt checkpoint")
|
| 25 |
+
ap.add_argument("--vx", type=float, nargs="+", default=[0.4, 0.8, 1.2, 1.6, 2.0])
|
| 26 |
+
ap.add_argument("--num-envs", type=int, default=64)
|
| 27 |
+
ap.add_argument("--seconds", type=float, default=8.0)
|
| 28 |
+
ap.add_argument("--device", default="cuda:0")
|
| 29 |
+
args = ap.parse_args()
|
| 30 |
+
|
| 31 |
+
import mjlab.tasks # noqa: F401 — populate the registry
|
| 32 |
+
import mjlab_microduck.tasks # noqa: F401 — microduck registrations
|
| 33 |
+
from mjlab.tasks.registry import load_env_cfg, load_rl_cfg, load_runner_cls
|
| 34 |
+
from mjlab.envs import ManagerBasedRlEnv
|
| 35 |
+
from mjlab.rl import RslRlVecEnvWrapper, MjlabOnPolicyRunner
|
| 36 |
+
|
| 37 |
+
env_cfg = load_env_cfg(args.task)
|
| 38 |
+
agent_cfg = load_rl_cfg(args.task)
|
| 39 |
+
env_cfg.scene.num_envs = args.num_envs
|
| 40 |
+
|
| 41 |
+
env = ManagerBasedRlEnv(cfg=env_cfg, device=args.device)
|
| 42 |
+
env = RslRlVecEnvWrapper(env, clip_actions=agent_cfg.clip_actions)
|
| 43 |
+
|
| 44 |
+
if args.onnx:
|
| 45 |
+
import onnxruntime as ort
|
| 46 |
+
session = ort.InferenceSession(args.onnx)
|
| 47 |
+
|
| 48 |
+
def policy(obs):
|
| 49 |
+
vec = obs["actor"].detach().cpu().numpy().astype(np.float32)
|
| 50 |
+
# 导出的 ONNX batch 维固定为 1,逐 env 推理
|
| 51 |
+
acts = [session.run(None, {session.get_inputs()[0].name: vec[i : i + 1]})[0]
|
| 52 |
+
for i in range(vec.shape[0])]
|
| 53 |
+
return torch.tensor(np.concatenate(acts, axis=0),
|
| 54 |
+
device=args.device, dtype=torch.float32)
|
| 55 |
+
else:
|
| 56 |
+
assert args.checkpoint, "--checkpoint or --onnx required"
|
| 57 |
+
runner_cls = load_runner_cls(args.task) or MjlabOnPolicyRunner
|
| 58 |
+
runner = runner_cls(env, asdict(agent_cfg), device=args.device)
|
| 59 |
+
runner.load(args.checkpoint, load_cfg={"actor": True}, strict=True,
|
| 60 |
+
map_location=args.device)
|
| 61 |
+
policy = runner.get_inference_policy(device=args.device)
|
| 62 |
+
|
| 63 |
+
uw = env.unwrapped
|
| 64 |
+
robot = uw.scene["robot"]
|
| 65 |
+
cmd_term = uw.command_manager.get_term("twist")
|
| 66 |
+
dt = uw.step_dt
|
| 67 |
+
|
| 68 |
+
print(f"policy: {args.onnx or args.checkpoint}")
|
| 69 |
+
print(f"control dt: {dt:.4f}s envs: {args.num_envs} task: {args.task}")
|
| 70 |
+
|
| 71 |
+
def force_cmd(vx):
|
| 72 |
+
cmd_term.vel_command_b[:, 0] = vx
|
| 73 |
+
cmd_term.vel_command_b[:, 1] = 0.0
|
| 74 |
+
cmd_term.vel_command_b[:, 2] = 0.0
|
| 75 |
+
cmd_term.is_standing_env[:] = False
|
| 76 |
+
|
| 77 |
+
obs, _ = env.reset()
|
| 78 |
+
for vx in args.vx:
|
| 79 |
+
obs, _ = env.reset()
|
| 80 |
+
force_cmd(vx)
|
| 81 |
+
# 2s 加速/稳定段
|
| 82 |
+
for _ in range(int(2.0 / dt)):
|
| 83 |
+
force_cmd(vx)
|
| 84 |
+
actions = policy(obs)
|
| 85 |
+
obs, _, _, _ = env.step(actions)
|
| 86 |
+
x0 = robot.data.root_link_pos_w[:, 0].clone()
|
| 87 |
+
y0 = robot.data.root_link_pos_w[:, 1].clone()
|
| 88 |
+
n_meas = int(args.seconds / dt)
|
| 89 |
+
dones_total = 0
|
| 90 |
+
for _ in range(n_meas):
|
| 91 |
+
force_cmd(vx)
|
| 92 |
+
actions = policy(obs)
|
| 93 |
+
obs, _, dones, _ = env.step(actions)
|
| 94 |
+
dones_total += int(dones.sum())
|
| 95 |
+
x1 = robot.data.root_link_pos_w[:, 0]
|
| 96 |
+
y1 = robot.data.root_link_pos_w[:, 1]
|
| 97 |
+
dist = torch.hypot(x1 - x0, y1 - y0)
|
| 98 |
+
speed = dist / args.seconds
|
| 99 |
+
alive = (dones_total == 0)
|
| 100 |
+
print(f"cmd={vx:4.1f} 实测 speed mean={speed.mean():.3f} "
|
| 101 |
+
f"p10={speed.quantile(0.1):.3f} p90={speed.quantile(0.9):.3f} "
|
| 102 |
+
f"max={speed.max():.3f} m/s resets={dones_total}")
|
| 103 |
+
|
| 104 |
+
env.close()
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
if __name__ == "__main__":
|
| 108 |
+
main()
|
src/mjlab_microduck/tasks/mdp.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/mjlab_microduck/tasks/microduck_running_env_cfg.py
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Forward-running task for Microduck.
|
| 2 |
+
|
| 3 |
+
This deliberately starts from the proven velocity environment so the 61D
|
| 4 |
+
observation contract, BAM actuator model, delays, noise, and sim2real domain
|
| 5 |
+
randomization remain identical to the walking policy family. The task is
|
| 6 |
+
forward-only and rewards measured progress beyond ordinary walking speed.
|
| 7 |
+
|
| 8 |
+
Two registered recipes use this file:
|
| 9 |
+
|
| 10 |
+
* ``Running`` optimizes forward progress without prescribing a gait.
|
| 11 |
+
* ``RunningFlight`` adds a small reward only when a controlled flight phase
|
| 12 |
+
begins. It does not pay for airtime, so a long ballistic fall is not useful.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import math
|
| 16 |
+
import os
|
| 17 |
+
from copy import deepcopy
|
| 18 |
+
|
| 19 |
+
from mjlab.managers import (
|
| 20 |
+
CurriculumTermCfg,
|
| 21 |
+
EventTermCfg,
|
| 22 |
+
RewardTermCfg,
|
| 23 |
+
SceneEntityCfg,
|
| 24 |
+
)
|
| 25 |
+
from mjlab.tasks.velocity import mdp as velocity_mdp
|
| 26 |
+
|
| 27 |
+
from mjlab_microduck.tasks import mdp as microduck_mdp
|
| 28 |
+
from mjlab_microduck.tasks.microduck_velocity_env_cfg import (
|
| 29 |
+
NUM_STEPS_PER_ENV,
|
| 30 |
+
VELOCITY_PUSH_INTERVAL_S,
|
| 31 |
+
MicroduckRlCfg,
|
| 32 |
+
make_microduck_velocity_env_cfg,
|
| 33 |
+
)
|
| 34 |
+
from mjlab_microduck.tasks.symmetry import SYMMETRY_CFG
|
| 35 |
+
|
| 36 |
+
RUNNING_PLAY_SPEED = 1.0
|
| 37 |
+
RUNNING_SPEED_CAP = float(os.environ.get("MICRODUCK_RUNNING_SPEED_CAP", "1.4"))
|
| 38 |
+
RUNNING_TARGET_MAX_SPEED = float(
|
| 39 |
+
os.environ.get("MICRODUCK_RUNNING_TARGET_MAX_SPEED", "1.2")
|
| 40 |
+
)
|
| 41 |
+
RUNNING_FINAL_ACTION_RATE_WEIGHT = float(
|
| 42 |
+
os.environ.get("MICRODUCK_RUNNING_ACTION_RATE_WEIGHT", "-0.10")
|
| 43 |
+
)
|
| 44 |
+
RUNNING_FORWARD_PROGRESS_WEIGHT = float(
|
| 45 |
+
os.environ.get("MICRODUCK_RUNNING_FORWARD_PROGRESS_WEIGHT", "5.0")
|
| 46 |
+
)
|
| 47 |
+
RUNNING_HIGH_SPEED_STAGE_INTERVAL = int(
|
| 48 |
+
os.environ.get("MICRODUCK_RUNNING_HIGH_SPEED_STAGE_INTERVAL", "500")
|
| 49 |
+
)
|
| 50 |
+
RUNNING_HIGH_SPEED_STEP = float(
|
| 51 |
+
os.environ.get("MICRODUCK_RUNNING_HIGH_SPEED_STEP", "0.15")
|
| 52 |
+
)
|
| 53 |
+
RUNNING_FLIGHT_REWARD_WEIGHT = float(
|
| 54 |
+
os.environ.get("MICRODUCK_RUNNING_FLIGHT_WEIGHT", "0.0")
|
| 55 |
+
)
|
| 56 |
+
RUNNING_GATE_PERFORMANCE = os.environ.get("MICRODUCK_RUNNING_GATE_PERFORMANCE") == "1"
|
| 57 |
+
RUNNING_ENABLE_SYMMETRY = os.environ.get("MICRODUCK_RUNNING_ENABLE_SYMMETRY") == "1"
|
| 58 |
+
RUNNING_ENABLE_HEADING_FEEDBACK = (
|
| 59 |
+
os.environ.get("MICRODUCK_RUNNING_ENABLE_HEADING_FEEDBACK") == "1"
|
| 60 |
+
)
|
| 61 |
+
RUNNING_CADENCE_WEIGHT = float(os.environ.get("MICRODUCK_RUNNING_CADENCE_WEIGHT", "0.0"))
|
| 62 |
+
RUNNING_FAST_TOUCH_WEIGHT = float(os.environ.get("MICRODUCK_RUNNING_FAST_TOUCH_WEIGHT", "0.0"))
|
| 63 |
+
RUNNING_EXPLOSIVE_ACCEL_WEIGHT = float(os.environ.get("MICRODUCK_RUNNING_EXPLOSIVE_ACCEL_WEIGHT", "0.0"))
|
| 64 |
+
RUNNING_STANDING_FRACTION = 0.03
|
| 65 |
+
RUNNING_CURRICULUM_START_ITERATION = int(
|
| 66 |
+
os.environ.get("MICRODUCK_RUNNING_CURRICULUM_START_ITERATION", "0")
|
| 67 |
+
)
|
| 68 |
+
RUNNING_ROBUST_PUSH_MPS = float(
|
| 69 |
+
os.environ.get("MICRODUCK_RUNNING_ROBUST_PUSH_MPS", "0")
|
| 70 |
+
)
|
| 71 |
+
RUNNING_ROBUST_TRUNK_COM_M = float(
|
| 72 |
+
os.environ.get("MICRODUCK_RUNNING_ROBUST_TRUNK_COM_M", "0")
|
| 73 |
+
)
|
| 74 |
+
RUNNING_ROBUST_HEAD_COM_M = float(
|
| 75 |
+
os.environ.get("MICRODUCK_RUNNING_ROBUST_HEAD_COM_M", "0")
|
| 76 |
+
)
|
| 77 |
+
RUNNING_ROBUST_INITIAL_TILT_DEG = float(
|
| 78 |
+
os.environ.get("MICRODUCK_RUNNING_ROBUST_INITIAL_TILT_DEG", "0")
|
| 79 |
+
)
|
| 80 |
+
RUNNING_ROBUST_FRICTION_MIN = float(
|
| 81 |
+
os.environ.get("MICRODUCK_RUNNING_ROBUST_FRICTION_MIN", "0.7")
|
| 82 |
+
)
|
| 83 |
+
RUNNING_ROBUST_FRICTION_MAX = float(
|
| 84 |
+
os.environ.get("MICRODUCK_RUNNING_ROBUST_FRICTION_MAX", "1.3")
|
| 85 |
+
)
|
| 86 |
+
RUNNING_ROBUST_PUSH_INTERVAL_MIN_S = float(
|
| 87 |
+
os.environ.get("MICRODUCK_RUNNING_ROBUST_PUSH_INTERVAL_MIN_S", "3.0")
|
| 88 |
+
)
|
| 89 |
+
RUNNING_ROBUST_PUSH_INTERVAL_MAX_S = float(
|
| 90 |
+
os.environ.get("MICRODUCK_RUNNING_ROBUST_PUSH_INTERVAL_MAX_S", "6.0")
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
# The lower edge rises too: zero-command behavior comes from the explicit
|
| 94 |
+
# standing bucket, rather than consuming most running samples near zero.
|
| 95 |
+
_RUNNING_BASE_SPEED_STAGES = (
|
| 96 |
+
{"step": 0 * NUM_STEPS_PER_ENV, "min_speed": 0.20, "max_speed": 0.45},
|
| 97 |
+
{"step": 1000 * NUM_STEPS_PER_ENV, "min_speed": 0.30, "max_speed": 0.55},
|
| 98 |
+
{"step": 2000 * NUM_STEPS_PER_ENV, "min_speed": 0.40, "max_speed": 0.65},
|
| 99 |
+
{"step": 3000 * NUM_STEPS_PER_ENV, "min_speed": 0.50, "max_speed": 0.75},
|
| 100 |
+
{"step": 4000 * NUM_STEPS_PER_ENV, "min_speed": 0.60, "max_speed": 0.85},
|
| 101 |
+
{"step": 5000 * NUM_STEPS_PER_ENV, "min_speed": 0.70, "max_speed": 0.95},
|
| 102 |
+
{"step": 6000 * NUM_STEPS_PER_ENV, "min_speed": 0.90, "max_speed": 1.20},
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def _running_speed_stages(
|
| 107 |
+
target_max_speed: float, high_speed_stage_interval: int = 500, high_speed_step: float = 0.15
|
| 108 |
+
) -> tuple[dict, ...]:
|
| 109 |
+
"""Return the proven base curriculum plus an optional continuation ramp."""
|
| 110 |
+
if target_max_speed < 1.2:
|
| 111 |
+
raise ValueError("running target max speed must be at least 1.2 m/s")
|
| 112 |
+
if high_speed_stage_interval <= 0:
|
| 113 |
+
raise ValueError("running high-speed stage interval must be positive")
|
| 114 |
+
if target_max_speed == 1.2:
|
| 115 |
+
return _RUNNING_BASE_SPEED_STAGES
|
| 116 |
+
intermediate_1 = min(1.35, target_max_speed)
|
| 117 |
+
intermediate_2 = min(1.50, target_max_speed)
|
| 118 |
+
intermediate_3 = min(1.65, target_max_speed)
|
| 119 |
+
stages = _RUNNING_BASE_SPEED_STAGES + (
|
| 120 |
+
{
|
| 121 |
+
"step": 6750 * NUM_STEPS_PER_ENV,
|
| 122 |
+
"min_speed": 0.95,
|
| 123 |
+
"max_speed": intermediate_1,
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
"step": 7250 * NUM_STEPS_PER_ENV,
|
| 127 |
+
"min_speed": 1.05,
|
| 128 |
+
"max_speed": intermediate_2,
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
"step": 7750 * NUM_STEPS_PER_ENV,
|
| 132 |
+
"min_speed": 1.15,
|
| 133 |
+
"max_speed": intermediate_3,
|
| 134 |
+
},
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
# Checkpoint 8,749 has consolidated the 1.65 m/s slice. Targets above
|
| 138 |
+
# that must arrive as new post-checkpoint stages; replacing the 7,750
|
| 139 |
+
# target would make a resumed run jump immediately to its final command.
|
| 140 |
+
if target_max_speed <= 1.65:
|
| 141 |
+
return stages
|
| 142 |
+
|
| 143 |
+
high_speed_targets: list[float] = []
|
| 144 |
+
next_target = 1.80
|
| 145 |
+
while next_target < target_max_speed - 1e-9:
|
| 146 |
+
high_speed_targets.append(round(next_target, 2))
|
| 147 |
+
next_target += high_speed_step
|
| 148 |
+
high_speed_targets.append(target_max_speed)
|
| 149 |
+
|
| 150 |
+
return stages + tuple(
|
| 151 |
+
{
|
| 152 |
+
"step": (8750 + index * high_speed_stage_interval)
|
| 153 |
+
* NUM_STEPS_PER_ENV,
|
| 154 |
+
"min_speed": round(max(1.25, max_speed - 0.55), 2),
|
| 155 |
+
"max_speed": max_speed,
|
| 156 |
+
}
|
| 157 |
+
for index, max_speed in enumerate(high_speed_targets)
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
RUNNING_SPEED_STAGES = _running_speed_stages(
|
| 162 |
+
RUNNING_TARGET_MAX_SPEED, RUNNING_HIGH_SPEED_STAGE_INTERVAL, RUNNING_HIGH_SPEED_STEP
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def _apply_running_robustness(
|
| 167 |
+
cfg,
|
| 168 |
+
*,
|
| 169 |
+
push_mps: float,
|
| 170 |
+
trunk_com_m: float,
|
| 171 |
+
head_com_m: float,
|
| 172 |
+
initial_tilt_deg: float,
|
| 173 |
+
friction_range: tuple[float, float],
|
| 174 |
+
push_interval_s: tuple[float, float] = VELOCITY_PUSH_INTERVAL_S,
|
| 175 |
+
) -> None:
|
| 176 |
+
"""Apply one fixed continuation stage without resume-step ambiguity."""
|
| 177 |
+
values = (push_mps, trunk_com_m, head_com_m, initial_tilt_deg)
|
| 178 |
+
if any(value < 0.0 for value in values):
|
| 179 |
+
raise ValueError("running robustness magnitudes must be non-negative")
|
| 180 |
+
if not 0.0 < friction_range[0] <= friction_range[1]:
|
| 181 |
+
raise ValueError("running robustness friction range must be positive and ordered")
|
| 182 |
+
if not 0.0 < push_interval_s[0] <= push_interval_s[1]:
|
| 183 |
+
raise ValueError("running robustness push interval must be positive and ordered")
|
| 184 |
+
|
| 185 |
+
cfg.events["foot_friction"].params["ranges"] = friction_range
|
| 186 |
+
if push_mps > 0.0:
|
| 187 |
+
velocity_range = {
|
| 188 |
+
"x": (-push_mps, push_mps),
|
| 189 |
+
"y": (-push_mps, push_mps),
|
| 190 |
+
}
|
| 191 |
+
if "push_robot" in cfg.events:
|
| 192 |
+
cfg.events["push_robot"].params["velocity_range"] = velocity_range
|
| 193 |
+
cfg.events["push_robot"].interval_range_s = push_interval_s
|
| 194 |
+
else:
|
| 195 |
+
cfg.events["push_robot"] = EventTermCfg(
|
| 196 |
+
func=velocity_mdp.push_by_setting_velocity,
|
| 197 |
+
mode="interval",
|
| 198 |
+
interval_range_s=push_interval_s,
|
| 199 |
+
params={
|
| 200 |
+
"velocity_range": velocity_range,
|
| 201 |
+
"asset_cfg": SceneEntityCfg("robot"),
|
| 202 |
+
},
|
| 203 |
+
)
|
| 204 |
+
else:
|
| 205 |
+
cfg.events.pop("push_robot", None)
|
| 206 |
+
|
| 207 |
+
if trunk_com_m > 0.0:
|
| 208 |
+
cfg.events["randomize_com"].params["ranges"] = (
|
| 209 |
+
-trunk_com_m,
|
| 210 |
+
trunk_com_m,
|
| 211 |
+
)
|
| 212 |
+
if head_com_m > 0.0:
|
| 213 |
+
cfg.events["randomize_head_com"].params["ranges"] = (
|
| 214 |
+
-head_com_m,
|
| 215 |
+
head_com_m,
|
| 216 |
+
)
|
| 217 |
+
if initial_tilt_deg > 0.0:
|
| 218 |
+
cfg.events["randomize_base_orientation"] = EventTermCfg(
|
| 219 |
+
func=microduck_mdp.randomize_base_orientation,
|
| 220 |
+
mode="reset",
|
| 221 |
+
params={
|
| 222 |
+
"asset_cfg": SceneEntityCfg("robot"),
|
| 223 |
+
"max_pitch_deg": initial_tilt_deg,
|
| 224 |
+
"max_roll_deg": initial_tilt_deg,
|
| 225 |
+
},
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def make_microduck_running_env_cfg(
|
| 230 |
+
play: bool = False, flight_reward_weight: float = RUNNING_FLIGHT_REWARD_WEIGHT
|
| 231 |
+
):
|
| 232 |
+
"""Build the flat-ground, forward-only running environment."""
|
| 233 |
+
cfg = make_microduck_velocity_env_cfg(play=play, rough=False)
|
| 234 |
+
cfg.episode_length_s = 12.0
|
| 235 |
+
# Microduck is only ~25 cm tall; the velocity recipe's 3 m follow camera
|
| 236 |
+
# hides gait and contact details in rollout videos.
|
| 237 |
+
cfg.viewer.distance = 0.55
|
| 238 |
+
cfg.viewer.max_extra_envs = 0
|
| 239 |
+
|
| 240 |
+
command = cfg.commands["twist"]
|
| 241 |
+
command.rel_standing_envs = 0.0 if play else RUNNING_STANDING_FRACTION
|
| 242 |
+
command.rel_heading_envs = 0.0
|
| 243 |
+
command.rel_turn_in_place_envs = 0.0
|
| 244 |
+
command.resampling_time_range = (12.0, 12.0)
|
| 245 |
+
if play:
|
| 246 |
+
command.ranges.lin_vel_x = (RUNNING_PLAY_SPEED, RUNNING_PLAY_SPEED)
|
| 247 |
+
command.ranges.lin_vel_y = (0.0, 0.0)
|
| 248 |
+
command.ranges.ang_vel_z = (0.0, 0.0)
|
| 249 |
+
else:
|
| 250 |
+
command.ranges.lin_vel_x = (
|
| 251 |
+
RUNNING_SPEED_STAGES[0]["min_speed"],
|
| 252 |
+
RUNNING_SPEED_STAGES[0]["max_speed"],
|
| 253 |
+
)
|
| 254 |
+
# Tiny non-zero lateral/yaw ranges keep those command neurons alive,
|
| 255 |
+
# while still making essentially every active sample forward-running.
|
| 256 |
+
command.ranges.lin_vel_y = (-0.02, 0.02)
|
| 257 |
+
command.ranges.ang_vel_z = (-0.05, 0.05)
|
| 258 |
+
|
| 259 |
+
if RUNNING_ENABLE_HEADING_FEEDBACK:
|
| 260 |
+
# Slot 2 becomes signed spawn-heading error, recomputed every step. It
|
| 261 |
+
# remains in the same position and mirrors with the same sign rule as a
|
| 262 |
+
# yaw-rate command, so the deployment observation contract is unchanged.
|
| 263 |
+
command.heading_command = False
|
| 264 |
+
command.rel_heading_envs = 0.0
|
| 265 |
+
command.ranges.heading = None
|
| 266 |
+
command.ranges.ang_vel_z = (0.0, 0.0)
|
| 267 |
+
command = microduck_mdp.SpawnHeadingVelocityCommandCfg(
|
| 268 |
+
**vars(command), heading_error_clip=1.0
|
| 269 |
+
)
|
| 270 |
+
cfg.commands["twist"] = command
|
| 271 |
+
|
| 272 |
+
# Max-speed objective. Velocity tracking remains useful as a curriculum
|
| 273 |
+
# guide, but forward progress is strong enough that exceeding the command is
|
| 274 |
+
# profitable. This is what makes the optimum "as fast as possible".
|
| 275 |
+
cfg.rewards["forward_progress"] = RewardTermCfg(
|
| 276 |
+
func=microduck_mdp.running_forward_progress,
|
| 277 |
+
weight=RUNNING_FORWARD_PROGRESS_WEIGHT,
|
| 278 |
+
params={"speed_cap": RUNNING_SPEED_CAP},
|
| 279 |
+
)
|
| 280 |
+
cfg.rewards["track_linear_velocity"].weight = 2.0
|
| 281 |
+
cfg.rewards["track_linear_velocity"].params["std"] = math.sqrt(0.15)
|
| 282 |
+
cfg.rewards["track_angular_velocity"].weight = 0.5
|
| 283 |
+
cfg.rewards["track_angular_velocity"].params["std"] = math.sqrt(0.5)
|
| 284 |
+
cfg.rewards["planar_drift"] = RewardTermCfg(
|
| 285 |
+
func=microduck_mdp.running_planar_drift_cost,
|
| 286 |
+
weight=-0.05,
|
| 287 |
+
params={"command_name": "twist", "lateral_weight": 4.0},
|
| 288 |
+
)
|
| 289 |
+
# Rate tracking alone can only say "stop turning"; once a heading error
|
| 290 |
+
# exists it provides no signal for which way to steer back. Anchor yaw to
|
| 291 |
+
# each randomized spawn heading so straight running is the actual optimum.
|
| 292 |
+
cfg.rewards["heading_hold"] = RewardTermCfg(
|
| 293 |
+
func=microduck_mdp.heading_hold_reward,
|
| 294 |
+
weight=1.5,
|
| 295 |
+
params={"std": 0.4, "asset_cfg": SceneEntityCfg("robot")},
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
# Permit the forward lean and fast leg cycling a sprint needs. These stay
|
| 299 |
+
# non-zero only to rule out tumbling and unbounded thrash as cheap optima.
|
| 300 |
+
cfg.rewards["pose"].weight = 0.15
|
| 301 |
+
cfg.rewards["upright"].weight = 0.75
|
| 302 |
+
cfg.rewards["upright"].params["std"] = math.sqrt(0.15)
|
| 303 |
+
cfg.rewards["body_ang_vel"].weight = -0.01
|
| 304 |
+
cfg.rewards["angular_momentum"].weight = -0.005
|
| 305 |
+
cfg.rewards["action_rate_l2"].weight = -0.02
|
| 306 |
+
cfg.rewards["foot_slip"].weight = -0.05
|
| 307 |
+
cfg.rewards["air_time"].weight = 0.0
|
| 308 |
+
cfg.rewards["foot_clearance"].params["target_height"] = 0.015
|
| 309 |
+
cfg.rewards["foot_swing_height"].params["target_height"] = 0.015
|
| 310 |
+
|
| 311 |
+
# Head/body slots remain present and sample tiny non-zero ranges, preserving
|
| 312 |
+
# the 61D hot-swap contract, but posture precision must not block discovery.
|
| 313 |
+
cfg.rewards["head_pose_tracking"].weight = 0.0
|
| 314 |
+
cfg.rewards["head_pose_bias"].weight = 0.0
|
| 315 |
+
cfg.rewards["body_pose_tracking"].weight = 0.0
|
| 316 |
+
|
| 317 |
+
if flight_reward_weight > 0.0:
|
| 318 |
+
cfg.rewards["flight_event"] = RewardTermCfg(
|
| 319 |
+
func=microduck_mdp.running_flight_event,
|
| 320 |
+
weight=flight_reward_weight,
|
| 321 |
+
params={
|
| 322 |
+
"sensor_name": "feet_ground_contact",
|
| 323 |
+
"min_forward_speed": 0.30,
|
| 324 |
+
"max_tilt_deg": 50.0,
|
| 325 |
+
"min_airborne_steps": 3,
|
| 326 |
+
},
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
# Su-Bingtian-style sprint levers for a small biped (cadence + stiff/fast
|
| 330 |
+
# ground touch + explosive low-speed acceleration). All off by default;
|
| 331 |
+
# enable via env knobs so they can be tuned without code edits.
|
| 332 |
+
if RUNNING_CADENCE_WEIGHT != 0.0:
|
| 333 |
+
cfg.rewards["running_cadence"] = RewardTermCfg(
|
| 334 |
+
func=microduck_mdp.running_cadence,
|
| 335 |
+
weight=RUNNING_CADENCE_WEIGHT,
|
| 336 |
+
params={"sensor_name": "feet_ground_contact"},
|
| 337 |
+
)
|
| 338 |
+
if RUNNING_FAST_TOUCH_WEIGHT != 0.0:
|
| 339 |
+
cfg.rewards["running_fast_ground_touch"] = RewardTermCfg(
|
| 340 |
+
func=microduck_mdp.running_fast_ground_touch,
|
| 341 |
+
weight=RUNNING_FAST_TOUCH_WEIGHT,
|
| 342 |
+
params={"sensor_name": "feet_ground_contact"},
|
| 343 |
+
)
|
| 344 |
+
if RUNNING_EXPLOSIVE_ACCEL_WEIGHT != 0.0:
|
| 345 |
+
cfg.rewards["running_explosive_accel"] = RewardTermCfg(
|
| 346 |
+
func=microduck_mdp.running_explosive_accel,
|
| 347 |
+
weight=RUNNING_EXPLOSIVE_ACCEL_WEIGHT,
|
| 348 |
+
params={"command_name": "twist", "speed_cap": RUNNING_SPEED_CAP},
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
# A clean speed-discovery phase: no random velocity kicks and no scheduled
|
| 352 |
+
# widening of CoM/head commands or precision taxes. Fixed initial DR stays
|
| 353 |
+
# active, so the result is not a deterministic-sim-only policy.
|
| 354 |
+
_apply_running_robustness(
|
| 355 |
+
cfg,
|
| 356 |
+
push_mps=RUNNING_ROBUST_PUSH_MPS,
|
| 357 |
+
trunk_com_m=RUNNING_ROBUST_TRUNK_COM_M,
|
| 358 |
+
head_com_m=RUNNING_ROBUST_HEAD_COM_M,
|
| 359 |
+
initial_tilt_deg=RUNNING_ROBUST_INITIAL_TILT_DEG,
|
| 360 |
+
friction_range=(
|
| 361 |
+
RUNNING_ROBUST_FRICTION_MIN,
|
| 362 |
+
RUNNING_ROBUST_FRICTION_MAX,
|
| 363 |
+
),
|
| 364 |
+
push_interval_s=(
|
| 365 |
+
RUNNING_ROBUST_PUSH_INTERVAL_MIN_S,
|
| 366 |
+
RUNNING_ROBUST_PUSH_INTERVAL_MAX_S,
|
| 367 |
+
),
|
| 368 |
+
)
|
| 369 |
+
for name in (
|
| 370 |
+
"standing_envs",
|
| 371 |
+
"head_pose_range",
|
| 372 |
+
"body_pose_range",
|
| 373 |
+
"com_range",
|
| 374 |
+
"head_com_range",
|
| 375 |
+
"head_pose_bias_weight",
|
| 376 |
+
):
|
| 377 |
+
cfg.curriculum.pop(name, None)
|
| 378 |
+
|
| 379 |
+
if not play:
|
| 380 |
+
cfg.curriculum["running_speed_range"] = CurriculumTermCfg(
|
| 381 |
+
func=microduck_mdp.running_command_ranges_curriculum,
|
| 382 |
+
params={
|
| 383 |
+
"command_name": "twist",
|
| 384 |
+
"gate_on_performance": RUNNING_GATE_PERFORMANCE,
|
| 385 |
+
"gate_margin": 0.02,
|
| 386 |
+
"gate_min_steps": 500,
|
| 387 |
+
"speed_stages": [
|
| 388 |
+
{
|
| 389 |
+
**stage,
|
| 390 |
+
"step": stage["step"]
|
| 391 |
+
- RUNNING_CURRICULUM_START_ITERATION * NUM_STEPS_PER_ENV,
|
| 392 |
+
}
|
| 393 |
+
for stage in RUNNING_SPEED_STAGES
|
| 394 |
+
],
|
| 395 |
+
},
|
| 396 |
+
)
|
| 397 |
+
# Smoothness is introduced only after a fast gait exists.
|
| 398 |
+
cfg.curriculum["action_rate_weight"] = CurriculumTermCfg(
|
| 399 |
+
func=microduck_mdp.reward_weight,
|
| 400 |
+
params={
|
| 401 |
+
"reward_name": "action_rate_l2",
|
| 402 |
+
"weight_stages": [
|
| 403 |
+
{
|
| 404 |
+
"step": (0 - RUNNING_CURRICULUM_START_ITERATION)
|
| 405 |
+
* NUM_STEPS_PER_ENV,
|
| 406 |
+
"weight": -0.02,
|
| 407 |
+
},
|
| 408 |
+
{
|
| 409 |
+
"step": (2500 - RUNNING_CURRICULUM_START_ITERATION)
|
| 410 |
+
* NUM_STEPS_PER_ENV,
|
| 411 |
+
"weight": -0.05,
|
| 412 |
+
},
|
| 413 |
+
{
|
| 414 |
+
"step": (4000 - RUNNING_CURRICULUM_START_ITERATION)
|
| 415 |
+
* NUM_STEPS_PER_ENV,
|
| 416 |
+
"weight": RUNNING_FINAL_ACTION_RATE_WEIGHT,
|
| 417 |
+
},
|
| 418 |
+
{
|
| 419 |
+
"step": (5500 - RUNNING_CURRICULUM_START_ITERATION)
|
| 420 |
+
* NUM_STEPS_PER_ENV,
|
| 421 |
+
"weight": RUNNING_FINAL_ACTION_RATE_WEIGHT,
|
| 422 |
+
},
|
| 423 |
+
],
|
| 424 |
+
},
|
| 425 |
+
)
|
| 426 |
+
else:
|
| 427 |
+
cfg.curriculum.pop("action_rate_weight", None)
|
| 428 |
+
|
| 429 |
+
return cfg
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
MicroduckRunningRlCfg = deepcopy(MicroduckRlCfg)
|
| 433 |
+
MicroduckRunningRlCfg.experiment_name = "running"
|
| 434 |
+
MicroduckRunningRlCfg.run_name = "running-max-speed"
|
| 435 |
+
MicroduckRunningRlCfg.algorithm.entropy_coef = 0.02
|
| 436 |
+
MicroduckRunningRlCfg.algorithm.symmetry_cfg = (
|
| 437 |
+
deepcopy(SYMMETRY_CFG) if RUNNING_ENABLE_SYMMETRY else None
|
| 438 |
+
)
|
| 439 |
+
MicroduckRunningRlCfg.max_iterations = 7_500
|
| 440 |
+
|
| 441 |
+
MicroduckRunningFlightRlCfg = deepcopy(MicroduckRunningRlCfg)
|
| 442 |
+
MicroduckRunningFlightRlCfg.experiment_name = "running_flight"
|
| 443 |
+
MicroduckRunningFlightRlCfg.run_name = "running-flight-event"
|