Upstream: https://github.com/pollen-robotics/microduck_rl Upstream-Commit: d424a0c899f6b33cbd3daeb279913134349c0b63 Upstream-Branch: develop
11 KiB
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.pymake_microduck_shoot_env_cfg(play: bool = False, rough: bool = False) -> ManagerBasedRlEnvCfgMicroduckShootRlCfg(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_cfgcomme 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 viacfg.period). - Nouveau flag
randomize_phasesurGroundPickPhaseCommandCfg/GroundPickPhaseCommand:- Défaut
True(non-cassant :ground_pickgarde le comportement actuel). - Shoot le met à
False→reset()remet φ=0 au lieu derand(). - 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_POSEDOIT é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é partest_stand_pose_matches_home_standing_pose. Les placeholders initialement à zéro cassaient cet invariant (corrigé après revue finale).
- Défaut
Reset (hauteur debout, pas d'élan)
reset_base.pose_range.z = (0.12, 0.13)— hauteur debout absolue (leposracine par défaut deInitialStateCfgest (0,0,0), donc z de reset = 0.12–0.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 unstep()(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é parmdp.kick_engagement(0 au repos STAND, 1 pendant la frappe). Poids 3.0.- suivi de pose scindé (param
joint_namessurkick_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, etkick_pose_track/kick_pose_l1ne 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 :
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. :
Bouton → shoot → retour auto à la policy principale.--ground-pick shoot.onnx --ground-pick-period 2.5 \ --ground-pick-kp-ratio 1.0 --ground-pick-action-scale <match>
Tests
tests/test_shoot.py— fonctions pures :kick_pose_targetaux 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_l1sur 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
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).