microduck_rl/tests/test_spin_cfg.py
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

111 lines
4.4 KiB
Python

from mjlab_microduck.tasks import mdp as microduck_mdp
from mjlab_microduck.tasks.microduck_spin_env_cfg import (
make_microduck_spin_env_cfg,
MicroduckSpinRlCfg,
)
def test_cfg_uses_phase_command_with_runtime_default_period():
cfg = make_microduck_spin_env_cfg()
cmd = cfg.commands["twist"]
assert isinstance(cmd, microduck_mdp.GroundPickPhaseCommandCfg)
# 4.0 s = le défaut de --ground-pick-period : rien à passer au runtime
assert cmd.period == 4.0
# chaque épisode démarre à phase 0 (debout), comme le bouton au déploiement
assert cmd.randomize_phase is False
def test_cfg_has_the_spin_rewards():
cfg = make_microduck_spin_env_cfg()
for name in (
"spin_rate_track",
"spin_rate_l1",
"spin_stay_in_place",
"spin_wheel_differential",
"spin_grounded",
"leg_antisymmetry",
):
assert name in cfg.rewards, name
# objectif principal avec un poids dominant
assert cfg.rewards["spin_rate_track"].weight == 6.0
# sur-place est un COÛT
assert cfg.rewards["spin_stay_in_place"].weight < 0.0
def test_stay_in_place_is_attenuated_during_the_launch_ramp():
# Renforcé à -3.0, ce terme s'opposerait à l'injection de moment angulaire s'il
# était plein tarif pendant la rampe de lancement : il doit y être atténué.
cfg = make_microduck_spin_env_cfg()
params = cfg.rewards["spin_stay_in_place"].params
assert 0.0 < params["launch_scale"] < 1.0
assert params["accel_end"] == microduck_mdp.SPIN_ACCEL_END
# cible positive = anti-horaire (le sens est porté par l'enveloppe)
assert microduck_mdp.SPIN_RATE_MAX > 0.0
def test_angular_momentum_reward_is_removed():
# Régression : angular_momentum_penalty pénalise la NORME 3D du moment
# angulaire, elle combattrait directement le spin. Elle doit être absente.
cfg = make_microduck_spin_env_cfg()
assert "angular_momentum" not in cfg.rewards
# body_ang_vel ne pénalise que x/y -> elle reste, elle mate le ballant
assert "body_ang_vel" in cfg.rewards
def test_head_yaw_is_free_to_act_as_a_flywheel():
cfg = make_microduck_spin_env_cfg()
pattern = cfg.rewards["neck_joint_pos_l2"].params["pattern"]
assert "head_yaw" not in pattern
def test_entry_velocity_allows_standstill_and_slow_roll():
cfg = make_microduck_spin_env_cfg()
# jamais via un push en mode reset (régression NaN du crouch)
assert "entry_velocity" not in cfg.events
lo, hi = cfg.events["reset_base"].params["velocity_range"]["x"]
assert lo == 0.0 and hi > 0.0
def test_symmetry_augmentation_is_disabled():
# la symétrie G/D transformerait un spin à gauche en spin à droite
assert MicroduckSpinRlCfg.algorithm.symmetry_cfg is None
def test_leg_antisymmetry_shaping_decays():
cfg = make_microduck_spin_env_cfg()
stages = cfg.curriculum["leg_antisym_weight"].params["weight_stages"]
weights = [s["weight"] for s in stages]
assert weights[0] == cfg.rewards["leg_antisymmetry"].weight
assert weights == sorted(weights, reverse=True)
assert weights[-1] < weights[0]
def test_actor_observation_keeps_the_61d_slot_layout():
# condition pour que l'ONNX charge dans le slot du runtime. L'égalité exacte
# des dimensions avec le crouch est vérifiée par test_obs_parity_with_roller_crouch
# ci-dessous ; ici on vérifie la structure.
cfg = make_microduck_spin_env_cfg()
terms = cfg.observations["actor"].terms
assert "base_lin_vel" not in terms
assert "height_scan" not in terms
for padded in ("head_command", "body_command"):
assert padded in terms
assert terms["head_command"].params["dim"] == 4
assert terms["body_command"].params["dim"] == 6
def test_obs_parity_with_roller_crouch():
# Parité de layout obligatoire : sinon l'ONNX exporté ne charge pas dans le
# slot du runtime. Contrairement au test de structure ci-dessus, celui-ci
# compare l'ordre EXACT des termes, groupe par groupe.
from mjlab_microduck.tasks.microduck_roller_crouch_env_cfg import (
make_microduck_roller_crouch_env_cfg,
)
spin = make_microduck_spin_env_cfg()
crouch = make_microduck_roller_crouch_env_cfg()
for grp in ("actor", "critic"):
assert list(spin.observations[grp].terms.keys()) == list(
crouch.observations[grp].terms.keys()
), f"layout d'observation divergent sur le groupe {grp}"