Upstream: https://github.com/pollen-robotics/microduck_rl Upstream-Commit: d424a0c899f6b33cbd3daeb279913134349c0b63 Upstream-Branch: develop
7.8 KiB
Ground-pick par suivi de pose interpolée par la phase
Date : 2026-07-24
Branche : new_pre_alpha_ground_pick
Fichier cible : src/mjlab_microduck/tasks/microduck_ground_pick_env_cfg.py (réécriture en place)
Task id : Mjlab-GroundPick-Flat-MicroDuck (inchangé)
1. Objectif
Remplacer l'objectif espace-tâche du ground_pick actuel (récompense la bouche qui descend au sol, puis récompense séparément le retour debout) par un objectif directif de suivi de pose : on définit deux poses articulaires cibles — STAND et DOWN — et on récompense le suivi de la pose interpolée par la phase (STAND→DOWN→STAND).
Motivation (reprise de l'approche roller_crouch, validée) : l'objectif par pose
interpolée est symétrique par construction — le « se relever » (cible → STAND)
est récompensé exactement comme le « se baisser » (cible → DOWN), ce qui règle le
problème d'optimum paresseux où la policy descend mais remonte mal. Le signal est
dense à chaque phase (cible qui bouge en continu), contrairement à une cible
fixe pondérée par sin qui ne donne aucun signal aux transitions.
Le geste reste déclenché au bouton A via le slot --ground-pick du runtime
(one-shot, retour auto à la policy principale). Obs 61D unifié inchangé →
policy interchangeable dans le slot.
2. Poses cibles
Résolution des joints PAR NOM (asset.find_joints([name])) — robuste, cohérent
avec l'approche roller. 14 joints (mouth exclu).
-
STAND_POSE = HOME (
default_joint_posdu modèle). Source du blend ; ne pas la redéfinir en dur — utiliser le défaut du modèle comme source (blend=0). Au déploiement, la policy principale reprend depuis HOME → retour propre. -
DOWN_POSE = valeurs initiales issues du keyframe FOLD de
scene_walk.xml(pli avant profond, tête baissée → bouche vers le sol). Dict par nom en tête de fichier, commenté comme remplaçable par une lectureread_pose.pydu vrai robot posé bouche-au-sol. Valeurs de départ :DOWN_POSE = { "left_hip_yaw": 0.0, "left_hip_roll": 0.0, "left_hip_pitch": 1.57, "left_knee": 1.57, "left_ankle": 0.0, "neck_pitch": 1.0, "head_pitch": 1.0, "head_yaw": 0.0, "head_roll": 0.0, "right_hip_yaw": 0.0, "right_hip_roll": 0.0, "right_hip_pitch": -1.57, "right_knee": -1.57, "right_ankle": 0.0, }
3. Profil de phase (4 segments)
Commande GroundPickPhaseCommand : [cos(2πφ), sin(2πφ), 0], période 4.0 s
(défaut du slot runtime → pas de flag période à changer au déploiement).
DESCENT_END=0.15 HOLD_END=0.50 RISE_END=0.65 (période 4 s)
[0, 0.15) descente STAND->DOWN ~0.6 s blend 0->1
[0.15, 0.50) bas DOWN ~1.4 s blend 1
[0.50, 0.65) remontée DOWN->STAND ~0.6 s blend 1->0
[0.65, 1.0) haut STAND (repos) ~1.4 s blend 0
blend ∈ [0,1] : 0 = STAND (HOME), 1 = DOWN. Cible = stand + blend·(down - stand).
Bornes tunables (constantes en tête de fichier).
randomize_phase=False : chaque épisode démarre à φ=0 (= debout), comme le
déclenchement bouton A au déploiement. Les épisodes se réinitialisant à des
instants échelonnés, les envs se décorrèlent naturellement en phase (pas besoin de
randomiser). Nécessite d'ajouter un flag randomize_phase à
GroundPickPhaseCommandCfg (défaut True → autres tâches sit/stand inchangées),
honoré dans reset().
4. Nouvelles fonctions mdp (portées de roller, adaptées, par nom)
Dans src/mjlab_microduck/tasks/mdp.py. Noms distincts du phase_pose_match
existant (qui est la variante cible-fixe-pondérée-sin) pour éviter la confusion.
phase_pose_blend(phase, descent_end, hold_end, rise_end) -> Tensor— pur, blend 4 segments 0..1 (testable en isolation)._phase_pose_error(env, asset_cfg, command_name, target_pose, descent_end, hold_end, rise_end, source_pose=None) -> (cur, target)— résout les joints par nom ;source_pose= HOME (default_joint_pos) siNone; calculephase = atan2(sin,cos)/2π % 1,blend, puistarget = source + blend·(target_pose - source).phase_pose_track(env, command_name, target_pose, source_pose=None, std=0.3, descent_end, hold_end, rise_end, asset_cfg) -> Tensor— gaussienneexp(-((cur-target)/std)²).mean(-1).phase_pose_track_l1(env, ...même args sans std...) -> Tensor— bootstrap-(cur-target).abs().mean(-1)(gradient constant quand la gaussienne sature).
target_pose = DOWN_POSE (dict par nom). source_pose=None → HOME.
5. Rewards
Réécriture minimale par rapport à l'actuel — on remplace la mécanique de retour de pose, on garde la stabilité/régul/sim2real.
| Reward | Poids | Statut | Rôle |
|---|---|---|---|
phase_pose_track (std 0.3) |
6.0 | NOUVEAU | suivi pose interpolée STAND↔DOWN |
phase_pose_track_l1 |
2.0 | NOUVEAU | bootstrap L1 |
mouth_ground_proximity (std 0.10) |
1.0 | retune (était 2.0) | filet : garantit la bouche au sol si DOWN imparfaite ; gaté approche (+sin) |
upright |
0.2 | gardé | tronc ~vertical (faible, le robot penche) |
feet_grounded |
3.0 | gardé | 2 pieds au sol pendant tout le geste |
self_collisions |
-1.0 | gardé | |
head_impact_penalty (seuil 2 N) |
-0.5 | gardé | pas de slam tête (DOWN amène la tête bas) |
action_rate_l2 |
-0.8→-2.0 (curric) | gardé | lissage |
neck_action_rate_l2 |
-1.0 | gardé | |
joint_torques_l2 |
-5e-3 | gardé | |
body_ang_vel |
-0.05 | gardé | |
angular_momentum |
-0.02 | gardé | |
soft_landing |
-1e-5 | gardé |
Retirées : mouth_perpendicular_to_ground, ground_pick_return_pose_legs,
ground_pick_return_pose_neck (remplacées par le suivi de pose).
Tout le reste inchangé : bloc DR (CoM/head-CoM/mass-inertia/friction/armature/
IMU-misalign/encoder-bias/pushes), obs 61D + padding head/body zéro, terminaisons
(nan_state), curricula (action_rate_weight, com_range, head_com_range),
RlCfg (experiment_name="ground_pick").
6. Déploiement (parité sim2real)
microduck_runtime ... \
--ground-pick ground_pick.onnx \
--ground-pick-period 4.0 \ # = période env (défaut, rien à changer)
--ground-pick-kp-ratio 1.0 \ # entraîné kp 200 → forcer 1.0 (défaut 0.6 baisse à 120)
--ground-pick-action-scale 1.0 # = action.scale env
7. Tests
tests/ (lancer uv run --with pytest pytest tests/ -q) :
- Fonctions pures :
phase_pose_blendaux points clés (φ=0→0, φ=0.075→0.5, φ=0.3→1, φ=0.575→0.5, φ=0.8→0, monotone par segment) ;phase_pose_track/_l1: valeur max (cur==target) et signe. - Construction de l'env :
make_microduck_ground_pick_env_cfg()construit ; commande =GroundPickPhaseCommandavecrandomize_phase=False,period=4.0; rewardsphase_pose_track/phase_pose_track_l1présents ;mouth_perpendicular_to_ground/ground_pick_return_pose_*absents ;mouth_ground_proximityprésent poids 1.0.
8. Entraînement / play / export
uv run train Mjlab-GroundPick-Flat-MicroDuck --env.scene.num-envs 4096 --agent.max_iterations 20000
uv run scripts/play_latest.py # md-play
uv run scripts/export_latest.py # normaliseur baké dans l'ONNX
Surveiller Episode_Reward/phase_pose_track (doit monter).
9. Hors scope / notes
- Doublon
pose_target_match(mdp.py 1577 et 1914) : latent, non traité ici. - Ajustement DOWN_POSE : si la bouche ne touche pas assez le sol avec les
valeurs FOLD, ajuster le dict (idéalement lecture
read_pose.pydu vrai robot posé bouche-au-sol) plutôt que de gonflermouth_ground_proximity. - Transition au déploiement : STAND=HOME = neutre de la policy principale → pas d'à-coup au retour (contrairement au souci noté sur roller où STAND≠HOME).