Upstream: https://github.com/pollen-robotics/microduck_rl Upstream-Commit: d424a0c899f6b33cbd3daeb279913134349c0b63 Upstream-Branch: develop
196 lines
12 KiB
Markdown
196 lines
12 KiB
Markdown
# Policy `roller_standup` — se relever sur rollers
|
||
|
||
**But** : le microduck (sur rollers) part du sol — à plat ventre ou à plat dos — et se remet **debout sur ses roues**, puis **tient** la station.
|
||
|
||
- **Tâche** : `Mjlab-RollerStandUp-Flat-MicroDuck`
|
||
- **Fichier** : `src/mjlab_microduck/tasks/microduck_roller_standup_env_cfg.py`
|
||
- **Base** : dérivée de l'env roller (`velocity_rollers`) → même robot, même physique/DR, **même observation 61D** (interchangeable au runtime, chargeable via `--new-cmd-obs`).
|
||
- **Spec** : `docs/superpowers/specs/2026-08-04-roller-standup-design.md`
|
||
- **Politique aveugle** : pas de scan de terrain ; proprioception + `projected_gravity`.
|
||
|
||
## Hauteurs (mesurées, pas devinées)
|
||
|
||
| pose | modèle pieds | modèle rollers |
|
||
|---|---|---|
|
||
| debout | 0.1172 → `STAND_Z=0.115` sous charge | 0.1407 → **`ROLLER_STAND_Z=0.138`** |
|
||
| à plat ventre (repos) | 0.075 | 0.075 |
|
||
| à plat dos (repos) | 0.048 | 0.048 |
|
||
|
||
Les hauteurs de repos au sol sont identiques aux deux modèles : c'est la coque du tronc qui touche, pas les pieds.
|
||
|
||
## ⚠️ Indices de joints — les roues sont INTERCALÉES
|
||
|
||
```
|
||
0-4 jambe gauche 5-6 roues gauches
|
||
7-10 cou / tête 11-15 jambe droite 16-17 roues droites
|
||
```
|
||
`_LEG_JOINTS = [0-4, 11-15]`. Les indices du `standup` (`[0-4, 9-13]`) valent pour le modèle **sans** roues et pointeraient sur des roues ici. Verrouillé par `tests/test_roller_standup_cfg.py::test_joint_indices_match_actual_roller_model`.
|
||
|
||
## Reset — départ au sol
|
||
|
||
`set_random_ground_state` : ventre (`prone_z` 0.076–0.09, plancher relevé car le ventre ne décolle du sol qu'à 0.0752) / dos / **déjà debout** (`standing_z` 0.134–0.144), ± 10° de bruit en pitch/roll. Pas de bucket « assis ». Le bucket « debout » est nécessaire : sans lui la policy monte mais ne tient pas.
|
||
|
||
**Curriculum `ground_state_mix`** (easy → hard, le dos en dernier) :
|
||
|
||
| iter | debout | ventre | dos |
|
||
|---|---|---|---|
|
||
| 0 | 0.50 | 0.50 | 0.00 |
|
||
| 600 | 0.35 | 0.45 | 0.20 |
|
||
| 1500 | 0.25 | 0.40 | 0.35 |
|
||
| 2500 | 0.20 | 0.40 | 0.40 |
|
||
|
||
## Récompenses
|
||
|
||
Dix termes repris du `standup` avec leurs poids déjà réglés : `pose_stand_legs` (+8), `pose_stand_l1` (+5), `height_stand` (+4, std 0.04), `height_stand_sharp` (+4, std 0.015), `height_stand_l1` (+30), `com_upward_velocity` (+3), `gentle_rise` (−0.02), `upright_linear` (+6), `upright_sharp` (+6), `standing_composite` (+15). Plus `joint_torque_rate_l2` (−2e-3), l'anti-jitter qui n'empêche pas le retournement.
|
||
|
||
Régularisateurs hérités : `body_ang_vel` **−0.05** (bloqueur de mouvement, à garder LÉGER), `angular_momentum` −0.02, `action_rate_l2` (rampe −0.4 → −1.0, **pas** le −2.0 du roller), `neck_action_rate_l2` −0.5, `neck_joint_pos_l2` −0.5 (tête droite), `joint_torques_l2` −1e-3, `action_over_limit` −0.5, `self_collisions` −1.0.
|
||
|
||
Retirées : toutes les récompenses de patinage, plus `feet_flat` (les lames ne sont pas à plat pendant la montée) et `hip_roll_neutral` (se relever demande d'écarter les jambes).
|
||
|
||
## ⚠️ Le point dur : les roues roulent
|
||
|
||
Aucune adhérence longitudinale pour pousser sur le sol. Le **curriculum de friction de roulement est INVERSÉ** (l'env roller la fait monter, ici elle descend) :
|
||
|
||
| iter | frictionloss | |
|
||
|---|---|---|
|
||
| 0 | 0.05 | roues quasi bloquées → se relève comme avec des pieds |
|
||
| 1000 | 0.02 | |
|
||
| 2000 | 0.008 | |
|
||
| 3000 | 0.003 | |
|
||
| 4000 | 0.0015 | la vraie valeur du roulement |
|
||
|
||
**Surveiller `Episode_Reward/standing_composite` aux paliers.** S'il s'écroule, le geste « pieds adhérents » ne transfère pas aux roues libres → il faudra guider une technique de patineur (appui genou intermédiaire, un patin à la fois). C'est un résultat, pas un échec.
|
||
|
||
**Surveiller AUSSI la dérive horizontale du robot en play**, à chaque palier de friction. `standing_composite` ne voit ni `root_link_pos_w[:2]` ni la vitesse horizontale : une policy qui se relève en glissant loin de son point de départ collecte exactement le même score qu'une qui se relève et s'arrête. Tant que cette dérive n'a pas été mesurée visuellement, le résultat du curriculum de friction (la question même que cet env existe pour trancher) n'est pas fiable.
|
||
|
||
**Sim2real** : seuls les checkpoints d'après iter 4000 sont candidats au déploiement. Avant, la policy s'appuie sur une friction qui n'existe pas sur le vrai robot.
|
||
|
||
## Commande
|
||
|
||
Slot `twist` neutralisé : `lin_vel_x`/`lin_vel_y` ± 0.01, `ang_vel_z` **± 0.05** (5× plus large — même
|
||
choix que le `standup`). Slots `head_pose` / `body_pose` **zero-paddés** (convention roller). Déploiement visé : en `--standing` face à la policy roller en `--walking`, avec la bascule automatique sur la magnitude de la commande (`infer_policy.py:262`, seuil 0.05) ; le slot twist y est laissé à zéro (`infer_policy.py:239`).
|
||
|
||
**Réserve** : `infer_policy.py` est le script de sim/clavier local. Le runtime robot est le binaire Rust `microduck_runtime`, absent du repo — il n'est pas vérifié qu'il expose un équivalent `--standing`. Le doc de passation du crouch ne liste que `--model`, `--ground-pick`, `--fold-policy`. À confirmer.
|
||
|
||
## Terminaisons
|
||
|
||
`fell_over` **supprimée** (le robot démarre tombé). `nan_state` héritée. `nan_policy="sanitize"` sur les obs actor/critic.
|
||
|
||
## Réseau / PPO
|
||
|
||
Actor et critic `(512, 256, 128)` elu, `obs_normalization=True`. PPO `lr=1e-3` adaptive, `desired_kl=0.01`, `gamma=0.99`, `lam=0.95`, `num_steps_per_env=24`, épisode 6 s, `max_iterations=15000`. **Symétrie OFF** (`SYMMETRY_CFG` est câblé pour le layout 51D).
|
||
|
||
## Commandes
|
||
|
||
```bash
|
||
uv run train Mjlab-RollerStandUp-Flat-MicroDuck --env.scene.num-envs 4096 --agent.max_iterations 15000
|
||
uv run scripts/play_latest.py # alias md-play
|
||
uv run scripts/export_latest.py # alias md-export
|
||
uv run --with pytest pytest tests/test_roller_standup_cfg.py -q
|
||
```
|
||
|
||
### ⚠️ Voir les départs sur le dos au play
|
||
|
||
Un play ne montre **jamais** de départ sur le dos par défaut : l'env de play est
|
||
reconstruit à neuf, donc `common_step_counter` repart à 0 et le curriculum applique son
|
||
palier 0, où `face_up_prob = 0`. On ne voit que 50 % ventre / 50 % debout, quelle que soit
|
||
la maturité du checkpoint chargé. Or le dos est le cas le plus dur, celui qu'on veut
|
||
justement inspecter.
|
||
|
||
`STANDUP_PLAY_FACE_UP` force le mélange (même motif que `SLOPE_PLAY_DIFFICULTY` dans
|
||
`roller_slope`), **uniquement sur le chemin `play=True`** — l'entraînement et son
|
||
curriculum easy → hard sont intouchés :
|
||
|
||
```bash
|
||
STANDUP_PLAY_FACE_UP=1.0 md-play # 100 % de départs sur le dos
|
||
STANDUP_PLAY_FACE_UP=0.4 md-play # le mélange du dernier palier du curriculum
|
||
STANDUP_PLAY_FACE_UP=none md-play # défaut (palier 0, pas de dos)
|
||
```
|
||
|
||
Le reste (`1 - face_up`) est réparti ventre:debout dans le rapport 2:1 du dernier palier,
|
||
si bien que `0.4` reproduit exactement le mélange de fin d'entraînement (0.40 / 0.20 / 0.40).
|
||
|
||
## 🔧 Correction anti-violence (après premier test robot)
|
||
|
||
**Symptômes** sur un checkpoint 4000+ : mouvements très brusques, la tête tape le sol,
|
||
échec du relevé depuis le dos sur le robot. **Présents en simu aussi** → ce n'était donc
|
||
ni du sim2real, ni un checkpoint trop jeune, mais la conception des récompenses.
|
||
|
||
**Root cause : `gentle_rise` récompensait la violence.** `trunk_vertical_accel_penalty`
|
||
renvoie déjà `-|a_z|` (`mdp.py:2171`) ; multiplié par le poids **−0.02** hérité du
|
||
`standup`, ça faisait un double négatif, donc `+0.02·|a_z|` — **plus le tronc accélérait
|
||
brutalement, plus la policy était payée**. Confirmé par le log : `Episode_Reward/gentle_rise
|
||
= +0.0118` sur le run `vweolw91`, seul terme de pénalité loggé positif.
|
||
|
||
`mdp.py` mélange deux conventions de signe, et c'est le piège :
|
||
|
||
| terme | la fonction renvoie | poids correct |
|
||
|---|---|---|
|
||
| `height_stand_l1`, `pose_stand_l1`, `gentle_rise` | `-abs(...)`, déjà négatif | **positif** |
|
||
| `joint_torques_l2`, `joint_torque_rate_l2`, `action_rate_l2`, `body_impact_cost` | magnitude positive | **négatif** |
|
||
|
||
Verrouillé par `test_already_negative_penalties_use_positive_weights`.
|
||
|
||
⚠️ **Le `standup` du marcheur a exactement le même bug** (même fonction, même poids −0.02).
|
||
Ça explique la série de tentatives d'amortissement infructueuses documentées dans ses
|
||
commentaires (« *violent / shaky / overshoot-tip-repeat on the real robot* ») : elles
|
||
combattaient un terme qui poussait activement dans l'autre sens. **Non corrigé ici** — c'est
|
||
un autre env, à trancher séparément.
|
||
|
||
**Problème structurel associé.** À convergence les récompenses de tâche totalisaient **≈ +41.6**
|
||
saturées à 95–99 %, contre **≈ −1.2** pour tous les amortisseurs réunis — dont
|
||
`joint_torque_rate_l2` à **−0.0002/pas** et `joint_torques_l2` à **−0.0001/pas**, soit rien.
|
||
Rapport ~35:1 : aucune raison d'être doux.
|
||
|
||
**État actuel des corrections :**
|
||
|
||
| | avant | maintenant | pourquoi |
|
||
|---|---|---|---|
|
||
| `gentle_rise` | −0.02 (récompense) | **+0.02** (pénalité) | signe corrigé ; magnitude gardée PETITE exprès — `\|a_z\|` est forcément élevé pendant un retournement, un gros poids serait un bloqueur de mouvement |
|
||
| `joint_torque_rate_l2` | −2e-3 | **−0.2** | le levier SÛR : pénalise la variation de couple, pas le mouvement |
|
||
| `head_impact_penalty` | absent | **toujours absent** | essayé à −1.0, a gelé la policy — voir ci-dessous |
|
||
|
||
### ⚠️ La pénalité d'impact tête a gelé la policy — ne pas la remettre telle quelle
|
||
|
||
Tentative avec les valeurs de `velstand` (`body_impact_cost`, sous-arbre `neck`, −1.0,
|
||
seuil 2.0) : **la policy a convergé vers rester couchée, inerte.** Mesuré (run `d8rnko6p`) :
|
||
|
||
| terme | avant (violent) | avec head_impact (gelé) |
|
||
|---|---|---|
|
||
| `standing_composite` | +14.32 | **+3.26** |
|
||
| `upright_sharp` | +5.76 | +1.06 |
|
||
| `head_impact_penalty` | — | **−1.01** ← plus gros terme négatif |
|
||
| `joint_torque_rate_l2` | −0.0002 | −0.255 (donc **pas** le coupable) |
|
||
|
||
L'erreur de raisonnement : croire qu'une pénalité « ciblée » ne bride pas le mouvement.
|
||
**Faux ici — pour se relever du dos, ce robot pivote sur sa tête et ses épaules.** La tête
|
||
est le point d'appui du retournement, pas un dégât collatéral ; la pénaliser bloque le seul
|
||
mécanisme disponible, et le dos était déjà le cas qui échouait.
|
||
|
||
**L'optimum paresseux qui rend ce gel possible** : `pose_stand_legs` restait à **+7.72 sur 8**
|
||
alors que le robot était allongé — les jambes sont à HOME en position couchée, donc cette
|
||
récompense est encaissée quasi gratuitement. C'est `height_stand_l1` (poids +30) qui doit
|
||
rendre « rester au sol » net négatif ; il ne faut pas l'affaiblir.
|
||
|
||
**Hypothèse en cours de test** : taper la tête était un *symptôme* de la violence (le bug de
|
||
signe payait la brutalité, et une montée brutale finit sur la tête), pas un défaut séparé.
|
||
Si le slam revient maintenant que le signe est corrigé, la reprise doit être une pénalité
|
||
**gatée en hauteur** (comme `upright_sharp` l'est), qui épargne la phase de retournement au sol.
|
||
|
||
**Leçon de méthode** : les trois corrections ont été appliquées d'un coup, donc le gel n'a pas
|
||
pu être attribué avec certitude — seul le suspect le plus probable a pu être désigné. Une
|
||
correction à la fois, à l'avenir.
|
||
|
||
**Recalibrage si c'est encore violent** : `|Δτ|²` vaut ~0.1 à convergence, donc la
|
||
contribution de `joint_torque_rate_l2` ≈ `0.1 × |poids|`. Monter **ce** terme, **pas**
|
||
`body_ang_vel` (−0.05) ni `action_rate_l2` (rampe → −1.0) : ceux-là sont des bloqueurs de
|
||
mouvement et le `standup` documente qu'à −0.15 et −1.2 respectivement, ils **gelaient** le
|
||
relevé depuis le dos. Si au contraire le dos cesse de fonctionner, **baisser**
|
||
`joint_torque_rate_l2` en premier.
|
||
|
||
## Hors périmètre
|
||
|
||
Intégrer le relevé dans la policy de roulage (recette `velstand`) ; buckets de départ sur le côté ; variante rough ; pénalités d'impact tronc/tête.
|
||
|
||
Aucune récompense ne pénalise la vitesse horizontale du tronc (`root_link_lin_vel_w[:, :2]`) : « se relever en roulant loin » est un résultat non pénalisé et qui score à plein. Décision volontaire (pas un oubli) : une récompense d'immobilité qui ne serait pas gatée en hauteur pénaliserait aussi la translation que le relevé depuis le sol exige physiquement — le mode d'échec « bloqueur de mouvement » que le `standup` documente. Candidat si le problème se confirme : une immobilité gatée en hauteur (proche de `ROLLER_STAND_Z` seulement).
|