microduck_rl/docs/superpowers/specs/2026-07-24-shoot-pose-following-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

203 lines
11 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.

# Spec — Tâche RL « shoot dans une balle » par suivi de poses
**Date** : 2026-07-24
**Branche** : `new_pre_alpha_ground_pick`
**Task id** : `Mjlab-Shoot-Flat-MicroDuck`
## Objectif
Apprendre un geste de **shoot one-shot** (frappe dans une balle) par **suivi d'une
trajectoire de poses articulaires à 4 keyframes** interpolée par la phase :
```
STAND → PIED_ARRIÈRE (armement) → PIED_AVANT (frappe) → STAND (repos)
```
- **Jambe droite** frappe, **jambe gauche** en appui.
- **Aucune balle simulée** : on apprend le *geste* par suivi de poses (comme
`ground_pick` / crouch). Si une vraie balle est devant le robot au déploiement,
elle se fait frapper.
- Obs **61D unifiée** identique aux autres policies microduck → l'ONNX exporté se
déploie tel quel dans un **slot bouton** du runtime (one-shot : joue le geste
puis rend la main à la policy principale).
Même moule que la tâche `ground_pick` de cette branche (phase encodée `[cos, sin, 0]`
dans le slot twist, suivi de pose par phase, obs 61D, DR sim2real héritée de velocity).
## Non-objectifs (YAGNI)
- Pas de balle physique, pas de reward de contact/vitesse de balle.
- Pas de côté configurable (droite uniquement ; gauche = symétrisable plus tard si besoin).
- Pas de marche / récupération de chute : tous les termes de locomotion sont retirés.
## Architecture
### Fichier & enregistrement
- `src/mjlab_microduck/tasks/microduck_shoot_env_cfg.py`
- `make_microduck_shoot_env_cfg(play: bool = False, rough: bool = False) -> ManagerBasedRlEnvCfg`
- `MicroduckShootRlCfg` (RslRlOnPolicyRunnerCfg, `experiment_name="shoot"`)
- Enregistrement dans `src/mjlab_microduck/tasks/__init__.py` :
`Mjlab-Shoot-Flat-MicroDuck` (variante `-Rough-` optionnelle).
- Base : hérite de l'env velocity (via `make_velocity_env_cfg` comme ground_pick),
puis strip agressif de tout ce qui est locomotion.
- Robot : `MICRODUCK_WALK_ROBOT_CFG` (marche standard, 14 joints, pas de rollers).
- `action.scale = 1.0`.
### Poses (placeholders → lues sur le vrai robot via `read_pose.py`)
Dicts `{nom_joint: rad}`, **14 joints** (mouth exclu). Sommet du fichier env.
- `STAND_POSE` : station neutre (~HOME du sim).
- `KICK_BACK_POSE` : hanche droite en **extension arrière** + genou droit fléchi
(armement) ; jambe gauche + cou ≈ HOME.
- `KICK_FWD_POSE` : hanche droite **fléchie avant** + genou droit tendu (frappe) ;
jambe gauche + cou ≈ HOME.
Placeholders plausibles au départ (ajustables), à remplacer par les lectures réelles.
### Commande & phase
- Réutilise `GroundPickPhaseCommand` : `command = [cos(2π·φ), sin(2π·φ), 0]` dans le
slot twist.
- **Période** : `SHOOT_PERIOD ≈ 2.5 s` (configurable via `cfg.period`).
- **Nouveau flag `randomize_phase`** sur `GroundPickPhaseCommandCfg` /
`GroundPickPhaseCommand` :
- Défaut `True` (non-cassant : `ground_pick` garde le comportement actuel).
- Shoot le met à `False``reset()` remet φ=0 au lieu de `rand()`.
- Raison : chaque épisode démarre au STAND (état du robot = `default_joint_pos`)
avec φ=0 = cible STAND → cohérence état/cible au reset (sinon la policy est
sommée d'être instantanément en pose « frappe » depuis une station immobile).
- **Invariant de cohérence** : `STAND_POSE` DOIT égaler la pose articulaire de
reset du sim (`HOME_FRAME` / `default_joint_pos`, non nulle : hip_pitch ±0.4579,
ankle ±0.4530, hip_roll ±0.0873, neck/head_pitch 0.3491). Vérifié par
`test_stand_pose_matches_home_standing_pose`. Les placeholders initialement à
zéro cassaient cet invariant (corrigé après revue finale).
### Reset (hauteur debout, pas d'élan)
- `reset_base.pose_range.z = (0.12, 0.13)`**hauteur debout absolue** (le `pos`
racine par défaut de `InitialStateCfg` est (0,0,0), donc z de reset = 0.120.13 m,
pas un offset additif ; valeur identique à l'env velocity qui marche). Pas de chute.
- **Pas d'injection de vitesse d'entrée** (shoot debout, contrairement au crouch-glide).
### Rewards hérités non listés
La table ci-dessus n'est pas exhaustive : l'env hérite de velocity quelques
régularisateurs génériques de faible poids non spécifiques au shoot —
`angular_momentum` (-0.02), `dof_pos_limits` — conservés (stabilité, négligeables).
⚠️ `soft_landing` (reward de marche) est **retiré** : il lit le capteur 2-pieds
`feet_ground_contact` supprimé au profit du capteur pied gauche → KeyError au 1er step
sinon, et il est inerte pour un shoot debout.
### Gotcha renommage capteur (⚠️)
Renommer le capteur pied (`feet_ground_contact``left_foot_ground_contact`) casse
tout ce que l'héritage velocity/ground_pick référence par ce nom. À traiter :
- **obs critic** `foot_air_time`/`foot_contact`/`foot_contact_forces` → repointés vers
le capteur pied gauche (le critic garde l'info d'appui ; sinon KeyError à la
construction de l'env).
- **reward** `soft_landing` → retiré (voir ci-dessus ; sinon KeyError au 1er step).
Toujours valider par une construction live + **au moins un `step()`** (le reward
manager ne tourne qu'au step), pas seulement le build de cfg ni les tests unitaires.
### ⚠️ Transfert de poids appris (révision post-1er entraînement)
Constat : les poses BACK/FWD relevées **robot tenu à la main (appui bipède)** gardent le
CoM **centré entre les deux pieds** (~4-5 cm à l'intérieur du pied gauche) à toutes les
phases. Avec `upright` imposé, dès que le pied droit se lève le robot bascule → aucune
policy ne peut tenir (géométrique, pas du tuning). Vérifié en sim (CoM vs sites pieds).
Fix retenu (RL apprend l'équilibre) :
- `mdp.com_over_support_foot` : reward gaussien (std 4 cm) tirant la projection du CoM
(`root_com_pos_w`) vers le pied d'appui, **gaté** par `mdp.kick_engagement` (0 au repos
STAND, 1 pendant la frappe). Poids 3.0.
- **suivi de pose scindé** (param `joint_names` sur `kick_pose_track`/`_l1`) :
GESTE = jambe droite + cou/tête (std 0.35, serré) ; APPUI = jambe gauche (std 0.9,
poids 1.0, **lâche**) → la policy peut adducter/décaler le bassin pour transférer le
poids sans que le suivi fige le bassin centré.
La table « Équilibre / appui » ci-dessus est donc étendue : `support_leg_pose` (1.0),
`com_over_support` (3.0) s'ajoutent, et `kick_pose_track`/`kick_pose_l1` ne portent plus
que sur les 9 joints du geste (droite+cou).
### Objectif : suivi de la pose interpolée par la phase
Nouvelle fonction **pure** dans `mdp.py` :
```python
kick_pose_target(phase, stand, back, forward, windup_end, kick_end, return_end) -> Tensor
```
Interpole entre les vecteurs de pose selon 4 segments (période normalisée [0,1)) :
```
[0, windup_end) STAND → BACK (armement, défaut 0.35)
[windup_end, kick_end) BACK → FORWARD (frappe sèche, défaut 0.10 = "snap")
[kick_end, return_end) FORWARD → STAND (retour, défaut 0.30)
[return_end, 1.0) STAND (repos)
```
Le « snap » vient du segment frappe court : la cible articulaire bouge vite → swing
rapide du pied. Les 3 bornes de timing sont paramétrables.
Résolution des joints **par nom** (`asset.find_joints([name])`) — robuste à l'ordre.
Rewards de suivi (toujours actifs, symétriques comme crouch) :
| Reward | Poids | Rôle |
|---|---|---|
| `kick_pose_tracking` | 6.0 | suivi gaussien `exp(-((q-cible)/std)²).mean`, std=0.4 |
| `kick_pose_l1` | 2.0 | bootstrap L1 (gradient constant tôt) |
### Équilibre / appui (jambe unique = risque de bascule)
| Reward | Poids | Rôle |
|---|---|---|
| `upright` | 2.0 | tronc vertical |
| `support_foot_grounded` (pied gauche) | 6.0 | garder le pied d'appui planté (capteur mono-pied → `found∈{0,1}` → reward∈{0,0.5} après `/2`, donc poids 6.0 ≈ contribution max 3.0) |
| `feet_flat` (gauche) | -1.0 | lame gauche à plat |
| `self_collisions` | -1.0 | |
| `body_ang_vel` | -0.05 | |
`support_foot_grounded` : réutiliser le mécanisme `feet_grounded_reward` du
ground_pick mais restreint au **pied gauche** (capteur de contact sur
`left_foot_collision`).
### Régularisation (allégée vs ground_pick — laisser passer le snap)
| Reward | Poids | Rôle |
|---|---|---|
| `action_rate_l2` | -0.5 | léger : un poids lourd tuerait la frappe rapide |
| `neck_action_rate_l2` | -0.5 | tête stable |
| `joint_torques_l2` | -1e-3 | |
**Retirés** (termes de marche) : `track_linear_velocity`, `track_angular_velocity`,
`air_time`, `foot_clearance`, `foot_swing_height`, `foot_slip`, `pose`.
### Observations / déploiement (parité)
- Obs **61D identique** à ground_pick/roller : `[gyro(3), projected_gravity(3),
joint_pos(14), joint_vel(14), last_action(14), command(13)]` avec les slots
head(4)+body(6) **zero-paddés** (`zero_command_padding`).
- Même DR sim2real héritée de velocity (CoM, mass/inertia, friction BAM, armature,
IMU misalignment obs-level, encoder-bias, pushes ±0.3), termine par NaN guard.
- Export ONNX (normaliseur baké) via le script d'export existant.
- Déploiement dans un slot phase du runtime, p.ex. :
```
--ground-pick shoot.onnx --ground-pick-period 2.5 \
--ground-pick-kp-ratio 1.0 --ground-pick-action-scale <match>
```
Bouton → shoot → retour auto à la policy principale.
## Tests
- `tests/test_shoot.py` — fonctions pures :
- `kick_pose_target` aux keypoints : STAND à φ=0, BACK à `windup_end`,
FORWARD à `kick_end`, STAND dans le segment repos ; interpolation à mi-segment ;
bornes (chaque composante entre min/max des poses).
- Valeurs des rewards `kick_pose_tracking` / `kick_pose_l1` sur cas simples.
- `tests/test_shoot_cfg.py` — l'env se construit avec la bonne commande
(`GroundPickPhaseCommand`, `randomize_phase=False`, période) et les rewards
attendus présents / termes de marche absents.
- Lancer : `uv run --with pytest pytest tests/ -q`.
## Entraînement
```bash
uv run train Mjlab-Shoot-Flat-MicroDuck --env.scene.num-envs 4096 --agent.max_iterations <N>
```
Surveiller `Episode_Reward/kick_pose_tracking` (doit monter). Play : script play_latest.
## Points ouverts / à régler à l'entraînement
- **Timings** (windup/kick/return) et **période** : défauts snap raisonnables,
à ajuster selon la vitesse de pied obtenue et la stabilité.
- **Poids `action_rate`** : tension snap vs lissage sim2real ; démarrer léger (-0.5).
- **Enrichissement optionnel (non retenu pour v1)** : petit reward de « vitesse du
pied droit vers l'avant » gaté sur le segment frappe, pour pousser la puissance
sans balle simulée. À ajouter seulement si le suivi de pose seul manque de punch.
- **Transitions au déploiement** : si `STAND_POSE` ≠ neutre de la policy principale,
léger à-coup au déclenchement/retour (comme noté pour crouch).