microduck_rl/docs/superpowers/specs/2026-07-17-roller-crouch-glide-design.md
Upstream Snapshot 47372443ff Import upstream snapshot d424a0c899f6b33cbd3daeb279913134349c0b63
Upstream: https://github.com/pollen-robotics/microduck_rl
Upstream-Commit: d424a0c899f6b33cbd3daeb279913134349c0b63
Upstream-Branch: develop
2026-08-28 15:41:56 +08:00

159 lines
8.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Design — Roller Crouch-Glide (« s'accroupir en glissant » au bouton)
**Date :** 2026-07-17
**Statut :** conception validée, prêt pour le plan d'implémentation
## Contexte
Le robot microduck sait patiner (policy roller, tâche `Mjlab-Velocity-Flat-MicroDuck-Rollers`).
On veut un nouveau geste : sur un appui bouton, il **s'accroupit et continue de glisser
sur son élan** (comme un patineur en position basse), maintient ~1 s, puis **se relève**
tout seul et reprend le patinage.
Contrainte forte de l'utilisatrice : **ne pas modifier le runtime Rust**
(`apirrone/microduck_runtime`, installé en binaire). Le geste doit donc réutiliser un
mécanisme déjà présent dans le runtime.
**Découverte clé :** le runtime a déjà un slot « comportement one-shot déclenché au
bouton » : `--ground-pick`. Il est déclenché par le **bouton A** (front montant),
joue une policy ONNX pilotée par une **phase** pendant une durée fixe, puis revient
automatiquement à la policy principale. Surtout, il utilise **exactement le même
layout d'observation 61D** que la policy roller — les deux sont interchangeables au
runtime. C'est le véhicule idéal, sans une ligne de Rust.
Compromis accepté : le geste est **one-shot** (durée fixe, pas de « bascule maintenue »).
La durée de l'accroupi est fixée par la période du slot.
## Approche retenue (approche B)
Créer une **nouvelle tâche mjlab** entraînée sur le robot rollers, qui joue
descente → glisse accroupi → remontée, piloté par la phase du slot ground-pick.
L'exporter en ONNX et la charger via `--ground-pick`. Aucune modif Rust.
### Fichiers concernés
| Fichier | Action |
|---|---|
| `src/mjlab_microduck/tasks/microduck_roller_crouch_env_cfg.py` | **Nouveau.** L'env, hybride roller + ground-pick. |
| `src/mjlab_microduck/tasks/mdp.py` | **Ajout** de la reward `crouch_glide_height_by_phase`. |
| `src/mjlab_microduck/tasks/__init__.py` | **Ajout** : enregistrer `Mjlab-RollerCrouch-Flat-MicroDuck`. |
### Réutilisation (ne rien réinventer)
- **Physique / robot roller** ← `microduck_velocity_rollers_env_cfg.py` :
`MICRODUCK_WALK_ROLLERS_ROBOT_CFG` (14 joints actifs + 4 roues passives),
capteur de contact sur les `roller_blade`, DR friction des roulements
(`randomize_wheel_friction` + curriculum), obs 14-dim (roues exclues via
`SceneEntityCfg("robot", joint_names=(r"^(?!passive_).*",))`), `action.scale=1.0`,
`kp_fw=200`.
- **Machinerie phase / one-shot** ← `microduck_ground_pick_env_cfg.py` :
commande `microduck_mdp.GroundPickPhaseCommand` **réutilisée telle quelle**
(produit le `[cos(2πφ), sin(2πφ), 0]` que le runtime enverra dans le slot twist),
padding head/body à zéro (`zero_command_padding`), terminaison `robot_state_is_nan`,
`reset_action_history`.
- **DR sim2real** ← repris du roller env sans changement (IMU misalignment obs-level,
encoder bias, masse/inertie, friction BAM, armature, pushes doux ±0.2).
## Le cœur : cible de hauteur « en trapèze » pilotée par la phase
Seule vraie nouveauté. Au lieu de descendre la bouche (ground-pick), on pilote la
**hauteur du tronc** (`com_height` du `trunk_base`) selon la phase, avec un palier bas :
```
hauteur
haute ┐ ┌── debout (rend la main à la policy roller)
│ \ /
basse│ \_______________/ accroupi + glisse (palier 1 s)
└───────────────────────► phase
0 0.375 0.625 1
```
- φ ∈ [0, 0.375] : descente vers la hauteur accroupie
- φ ∈ [0.375, 0.625] : **maintien accroupi** (= 1 s sur une période de 4 s) → glisse
- φ ∈ [0.625, 1.0] : remontée vers la pose roller debout
**Nouvelle reward `crouch_glide_height_by_phase(env, command_name, height_low,
height_high, hold_lo=0.375, hold_hi=0.625, std=...)`** dans `mdp.py` :
lit la phase depuis la commande, calcule la hauteur-cible (interpolée haut→bas→haut,
plate sur le palier), récompense `exp(-((h_mesurée - h_cible)/std)²)`.
S'inspirer des fonctions `com_height_target` (mdp.py:694) et des
`interpolated/multistage height target` déjà présentes.
Valeurs de départ : `height_high ≈ 0.11` m (hauteur roller debout, cf. bande
`com_height_target` roller 0.09350.1235), `height_low ≈ 0.075` m (accroupi ;
à affiner en play). La phase est reconstruite depuis `atan2(sin, cos)` de la commande.
## Récompenses
| Reward | Rôle | Origine |
|---|---|---|
| `crouch_glide_height_by_phase` | Cible principale (haut→bas→haut) | **nouveau** |
| `wheel_speed` (poids réduit ~23) | Garder l'élan, ne pas freiner pendant l'accroupi | roller env (`wheel_speed_reward`) |
| `upright` (≈2), `body_ang_vel` (0.05), `angular_momentum` (0.02) | Équilibre / stabilité | roller env |
| `return_pose` (fin de phase) | Converger vers la pose roller debout pour rendre la main proprement | adapté de `ground_pick_return_pose` |
| `feet_flat` (2) | Lames à plat → glisse stable | roller env |
| `action_rate_l2`, `neck_action_rate_l2`, `joint_torques_l2`, `self_collisions` | Lissage / transfert sim2real | les deux envs |
**Explicitement PAS inclus :** `braking` (on ne veut pas s'arrêter), `mouth_ground_proximity`
/ `mouth_perpendicular_to_ground` (on ne touche pas le sol), `skating_air_time` /
`single_support` / `glide` (pas de stride pendant le trick — on glisse passivement).
## Entraînement
- `MicroduckRollerCrouchRlCfg` = copie de `MicroduckRollersRlCfg`
(MLP 512/256/128, ELU, obs_normalization, PPO, `experiment_name="roller_crouch"`).
- Enregistrer dans `tasks/__init__.py` :
`register_mjlab_task(task_id="Mjlab-RollerCrouch-Flat-MicroDuck", ...)`.
- Lancer :
```bash
uv run train Mjlab-RollerCrouch-Flat-MicroDuck \
--env.scene.num-envs 4096 --agent.max_iterations 8000
```
- Épisodes démarrés avec une **vitesse d'entrée réaliste** (le robot arrive en roulant),
sinon il n'aura pas d'élan à conserver pendant l'accroupi. À câbler via un event de
reset (vitesse initiale non nulle) ou un push au début d'épisode.
## Export + déploiement (flags runtime exacts)
Export ONNX (le normaliseur est baké par `export.py`), puis :
```bash
microduck_runtime --variant pre-alpha --new-cmd-obs --roller \
--model output.onnx \
--new-dxl-imu --kp 200 --action-scale 0.8 \
--max-linear-vel 0.6 --max-linear-vel-backward 0.5 --max-angular-vel 0.0 \
--ground-pick roller_crouch.onnx \
--ground-pick-period 5.0 \
--ground-pick-kp-ratio 1.0 \
--ground-pick-action-scale 0.8
```
Bouton **A** → crouch-glide, puis retour auto à la policy roller.
**Pièges de parité entraînement/déploiement (importants pour le sim2real) :**
- `--ground-pick-kp-ratio 1.0` : le défaut est **0.6** (baisse kp à 120 pendant le trick).
On entraîne à kp=200 → il faut forcer **1.0** pour que ça corresponde.
- `--ground-pick-action-scale` doit matcher l'`action_scale` d'entraînement (0.8 ci-dessus).
- `--ground-pick-period 5.0` doit matcher la période/longueur de mouvement entraînée
(défaut 4.0, on le garde).
## Risques et vérification
- **One-shot, durée fixe :** l'accroupi dure `ground-pick-period` puis remonte tout seul.
Pas de maintien libre — limite acceptée de l'approche B.
- **Élan pendant le trick :** la phase remplace la commande de vitesse → **pas de poussée
active** pendant l'accroupi. Si l'élan d'entrée est trop faible, il ralentit. D'où
l'entraînement avec vitesse d'entrée réaliste.
- **Vérification :**
1. En sim (`play`) : il descend, garde les roues qui tournent pendant le palier,
se relève sans tomber, et la pose finale rejoint proprement la pose roller debout.
2. Sur le vrai robot : lancer à petite vitesse, appuyer sur A, observer.
3. Confirmer que la policy roller reprend la main proprement après le retour.
## Questions ouvertes / à confirmer pendant l'implémentation
- Valeur exacte de `height_low` (accroupi) — à régler en play.
- Meilleure façon d'injecter la vitesse d'entrée à l'épisode (event reset vs push initial).
- Poids relatif `wheel_speed` vs `crouch_glide_height_by_phase` (garder l'élan sans
empêcher de s'accroupir).