Upload ninja_env.py with huggingface_hub
Browse files- ninja_env.py +1801 -0
ninja_env.py
ADDED
|
@@ -0,0 +1,1801 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
embodied_pcei/src/ninja_env.py
|
| 3 |
+
|
| 4 |
+
High-performance physical simulation environment for the American Ninja Warrior course.
|
| 5 |
+
Built on MuJoCo with native Apple Silicon performance.
|
| 6 |
+
|
| 7 |
+
Features:
|
| 8 |
+
- Articulated bipedal humanoid with 21 joint torque actuators.
|
| 9 |
+
- Staged American Ninja Warrior course:
|
| 10 |
+
* Stage 0: Flat sprint runway (0 to 8m)
|
| 11 |
+
* Stage 1: Quad steps / angled stepping stones (8 to 18m)
|
| 12 |
+
* Stage 2: Elevated balance beam (18 to 28m)
|
| 13 |
+
* Stage 3: Hurdles & stepping barriers (28 to 38m)
|
| 14 |
+
* Stage 4: Warped wall incline ramp & summit buzzer (38 to 48m)
|
| 15 |
+
- Raycast Pseudo-Vision: forward height probes measuring terrain/obstacle topology.
|
| 16 |
+
- Metabolic / Homeostatic Telemetry: lungs/stamina, stomach/hunger, heart/health.
|
| 17 |
+
- Built-in offscreen renderer for live web dashboard streaming.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import numpy as np
|
| 21 |
+
import mujoco
|
| 22 |
+
from pathlib import Path
|
| 23 |
+
from typing import Dict, Tuple, Optional
|
| 24 |
+
|
| 25 |
+
XML_PATH = Path(__file__).resolve().parent.parent / "models" / "ninja_course.xml"
|
| 26 |
+
FLAT_ARENA_XML = Path(__file__).resolve().parent.parent / "models" / "flat_arena.xml"
|
| 27 |
+
|
| 28 |
+
# Obstacle Stage Milestones along +X
|
| 29 |
+
STAGE_MILESTONES = [
|
| 30 |
+
(8.0, "Stage 1: Quad Steps"),
|
| 31 |
+
(18.0, "Stage 2: Balance Beam"),
|
| 32 |
+
(28.0, "Stage 3: Hurdles"),
|
| 33 |
+
(38.0, "Stage 4: Warped Wall"),
|
| 34 |
+
(46.0, "Course Finish: Summit Buzzer"),
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
# 18 Progressive Navigation Waypoints across the 48m Ninja Warrior Course
|
| 38 |
+
# Format: (Target 3D Coordinate [X, Y, Z], Waypoint Label, Arrival Radius in meters)
|
| 39 |
+
COURSE_WAYPOINTS = [
|
| 40 |
+
(np.array([3.0, 0.00, 1.33], dtype=np.float32), "Runway Accelerate 1", 1.2),
|
| 41 |
+
(np.array([6.0, 0.00, 1.33], dtype=np.float32), "Runway Accelerate 2", 1.2),
|
| 42 |
+
(np.array([8.0, 0.00, 1.33], dtype=np.float32), "Runway Exit Threshold", 1.0),
|
| 43 |
+
(np.array([10.0, -0.60, 0.20], dtype=np.float32), "Stage 1: Quad Step 1 (Right)", 0.8),
|
| 44 |
+
(np.array([12.2, 0.60, 0.30], dtype=np.float32), "Stage 1: Quad Step 2 (Left)", 0.8),
|
| 45 |
+
(np.array([14.4, -0.50, 0.35], dtype=np.float32), "Stage 1: Quad Step 3 (Right)", 0.8),
|
| 46 |
+
(np.array([16.6, 0.50, 0.40], dtype=np.float32), "Stage 1: Quad Step 4 (Left)", 0.8),
|
| 47 |
+
(np.array([18.5, 0.00, 0.45], dtype=np.float32), "Platform Mid 1", 1.0),
|
| 48 |
+
(np.array([20.5, 0.00, 0.50], dtype=np.float32), "Stage 2: Balance Beam Entry", 0.6),
|
| 49 |
+
(np.array([23.5, 0.00, 0.50], dtype=np.float32), "Stage 2: Balance Beam Mid", 0.6),
|
| 50 |
+
(np.array([26.5, 0.00, 0.50], dtype=np.float32), "Stage 2: Balance Beam Exit", 0.6),
|
| 51 |
+
(np.array([28.5, 0.00, 0.50], dtype=np.float32), "Platform Mid 2", 1.0),
|
| 52 |
+
(np.array([31.5, 0.00, 0.20], dtype=np.float32), "Stage 3: Hurdle 1", 0.9),
|
| 53 |
+
(np.array([34.5, 0.00, 0.28], dtype=np.float32), "Stage 3: Hurdle 2", 0.9),
|
| 54 |
+
(np.array([37.5, 0.00, 0.22], dtype=np.float32), "Stage 3: Hurdle 3", 0.9),
|
| 55 |
+
(np.array([40.0, 0.00, 0.30], dtype=np.float32), "Stage 4: Warped Wall Approach",1.2),
|
| 56 |
+
(np.array([42.5, 0.00, 0.70], dtype=np.float32), "Stage 4: Warped Wall Incline", 1.2),
|
| 57 |
+
(np.array([47.5, 0.00, 2.45], dtype=np.float32), "Course Finish: Summit Buzzer", 0.6),
|
| 58 |
+
]
|
| 59 |
+
|
| 60 |
+
# Translucent Neon Color Palette for Multi-Agent Ghost Swarm Rendering
|
| 61 |
+
GHOST_PALETTE = [
|
| 62 |
+
np.array([0.2, 0.7, 1.0, 0.16], dtype=np.float32), # Neon Cyan
|
| 63 |
+
np.array([0.7, 0.3, 1.0, 0.16], dtype=np.float32), # Electric Purple
|
| 64 |
+
np.array([0.1, 0.9, 0.6, 0.16], dtype=np.float32), # Neon Emerald
|
| 65 |
+
np.array([1.0, 0.55, 0.1, 0.16], dtype=np.float32), # Sunset Orange
|
| 66 |
+
np.array([1.0, 0.2, 0.6, 0.16], dtype=np.float32), # Neon Pink
|
| 67 |
+
np.array([0.35, 0.8, 1.0, 0.16], dtype=np.float32), # Sky Blue
|
| 68 |
+
np.array([0.85, 0.95, 0.2, 0.16], dtype=np.float32),# Lime Volt
|
| 69 |
+
]
|
| 70 |
+
|
| 71 |
+
# Canonical Reference Quiet Standing Equilibrium Pose (21 Actuated Joints)
|
| 72 |
+
# Torso at Z=1.168m, spine vertical, knees naturally flexed at 0.05 rad (~3 deg), arms down at sides
|
| 73 |
+
STAND_REF_QPOS = np.array([
|
| 74 |
+
0.0, 0.0, 0.0, # abdomen_z, abdomen_y, abdomen_x
|
| 75 |
+
0.0, 0.0, -0.05, 0.05, -0.025, 0.0, # right_hip_x, z, y, knee, ankle_y, ankle_x
|
| 76 |
+
0.0, 0.0, -0.05, 0.05, -0.025, 0.0, # left_hip_x, z, y, knee, ankle_y, ankle_x
|
| 77 |
+
0.65, 0.0, 0.0, # right_shoulder_x, y, elbow (down at side)
|
| 78 |
+
-0.65, 0.0, 0.0, # left_shoulder_x, y, elbow (down at side)
|
| 79 |
+
], dtype=np.float32)
|
| 80 |
+
|
| 81 |
+
STAND_JOINT_WEIGHTS = np.array([
|
| 82 |
+
1.5, 1.5, 1.5, # abdomen_z, abdomen_y, abdomen_x
|
| 83 |
+
1.5, 1.5, 1.5, 1.5, 1.5, 1.5, # right leg (hip_x, hip_z, hip_y, knee, ankle_y, ankle_x)
|
| 84 |
+
1.5, 1.5, 1.5, 1.5, 1.5, 1.5, # left leg (hip_x, hip_z, hip_y, knee, ankle_y, ankle_x)
|
| 85 |
+
0.5, 0.5, 0.5, # right arm (shoulder_x, shoulder_y, elbow)
|
| 86 |
+
0.5, 0.5, 0.5 # left arm (shoulder_x, shoulder_y, elbow)
|
| 87 |
+
], dtype=np.float32)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class NinjaEnv:
|
| 91 |
+
def __init__(self, xml_path: Optional[str] = None, frame_skip: int = 5, arena_mode: bool = True, stream_mode: str = "cruise"):
|
| 92 |
+
self.arena_mode = arena_mode
|
| 93 |
+
self.stream_mode = stream_mode # "cruise", "stand", "getup", "reach"
|
| 94 |
+
if xml_path is None:
|
| 95 |
+
xml_path = str(FLAT_ARENA_XML if arena_mode else XML_PATH)
|
| 96 |
+
self.model = mujoco.MjModel.from_xml_path(xml_path)
|
| 97 |
+
self.data = mujoco.MjData(self.model)
|
| 98 |
+
self.frame_skip = frame_skip
|
| 99 |
+
|
| 100 |
+
self.dt = self.model.opt.timestep * frame_skip # 0.005 * 5 = 0.025s (40 Hz)
|
| 101 |
+
|
| 102 |
+
self.n_actuators = self.model.nu # 21
|
| 103 |
+
self.n_qpos = self.model.nq # 28 (7 root + 21 joints)
|
| 104 |
+
self.n_qvel = self.model.nv # 27 (6 root + 21 joints)
|
| 105 |
+
|
| 106 |
+
# Solid-State Angular LiDAR Sensor Configuration
|
| 107 |
+
# 16 vertical beams (pitch: -35 deg to +15 deg) x 32 horizontal beams (yaw: -45 deg to +45 deg) = 512 rays
|
| 108 |
+
self.num_lidar_v = 16
|
| 109 |
+
self.num_lidar_h = 32
|
| 110 |
+
self.num_lidar_rays = self.num_lidar_v * self.num_lidar_h # 512
|
| 111 |
+
self.num_rays = self.num_lidar_rays # backwards-compatibility alias
|
| 112 |
+
self.lidar_max_range = 10.0 # meters
|
| 113 |
+
self.lidar_v_angles = np.radians(np.linspace(-35, 15, self.num_lidar_v))
|
| 114 |
+
self.lidar_h_angles = np.radians(np.linspace(-45, 45, self.num_lidar_h))
|
| 115 |
+
|
| 116 |
+
# Precompute unit direction vectors in head local coordinate frame
|
| 117 |
+
# Forward = +X, Left = +Y, Up = +Z
|
| 118 |
+
local_vectors = []
|
| 119 |
+
for pitch in self.lidar_v_angles:
|
| 120 |
+
for yaw in self.lidar_h_angles:
|
| 121 |
+
vx = np.cos(pitch) * np.cos(yaw)
|
| 122 |
+
vy = np.cos(pitch) * np.sin(yaw)
|
| 123 |
+
vz = np.sin(pitch)
|
| 124 |
+
v = np.array([vx, vy, vz], dtype=np.float64)
|
| 125 |
+
v /= np.linalg.norm(v)
|
| 126 |
+
local_vectors.append(v)
|
| 127 |
+
self.lidar_local_vectors = np.array(local_vectors, dtype=np.float64) # (128, 3)
|
| 128 |
+
self.current_lidar_grid = np.zeros((self.num_lidar_v, self.num_lidar_h), dtype=np.float32)
|
| 129 |
+
self._geomid_buf = np.zeros(1, dtype=np.int32)
|
| 130 |
+
self._visor_offset = np.array([0.15, 0.0, 0.0], dtype=np.float64)
|
| 131 |
+
|
| 132 |
+
# Metabolic internal states
|
| 133 |
+
self.stamina = 1.0 # 0.0 to 1.0
|
| 134 |
+
self.health = 1.0 # 0.0 to 1.0
|
| 135 |
+
self.hunger = 0.0 # 0.0 to 1.0
|
| 136 |
+
|
| 137 |
+
# Milestone tracking & Course Waypoints
|
| 138 |
+
self.milestones_reached = set()
|
| 139 |
+
self.waypoints_reached = set()
|
| 140 |
+
self.current_wp_idx = 0
|
| 141 |
+
self.furthest_x = 0.0
|
| 142 |
+
self.max_steps = 1000
|
| 143 |
+
|
| 144 |
+
# Flat Arena & Dynamic Random Goal Navigation State (Stream 4: Cruise)
|
| 145 |
+
self.goal_pos = np.array([5.0, 0.0, 1.25], dtype=np.float32)
|
| 146 |
+
self.goals_completed = 0
|
| 147 |
+
self.goal_arrival_radius = 2.0 # Wide capture radius (2.0m): easily clears by walking near target
|
| 148 |
+
self.min_dist_to_goal = 5.0
|
| 149 |
+
self.prev_dist_to_goal = 5.0
|
| 150 |
+
self.steps_not_approaching = 0
|
| 151 |
+
self.transition_leeway_window = 90 # ~2.25s leeway (90 steps at 40Hz) for turning & realignment
|
| 152 |
+
self.goal_transition_steps = 0 # No leeway on initial spawn (must advance immediately)
|
| 153 |
+
|
| 154 |
+
# Stream 1: Quiet Standing Stillness State
|
| 155 |
+
self.stand_still_steps = 0
|
| 156 |
+
self.stand_drift_steps = 0
|
| 157 |
+
self.stand_one_leg_steps = 0
|
| 158 |
+
|
| 159 |
+
# Stream 2: Fall Recovery State (Ragdoll & Curated Library)
|
| 160 |
+
self.fall_state_type = "random_ragdoll"
|
| 161 |
+
self.consecutive_upright_steps = 0
|
| 162 |
+
self.prev_z = 0.20
|
| 163 |
+
self.prev_head_z = 0.20
|
| 164 |
+
self.getup_stagnant_steps = 0
|
| 165 |
+
self.max_elevation_achieved = 0.20
|
| 166 |
+
self.max_head_elevation = 0.20
|
| 167 |
+
self.getup_goal_pos = np.array([0.0, 0.0, 1.168], dtype=np.float32)
|
| 168 |
+
self.getup_success_count = 0
|
| 169 |
+
|
| 170 |
+
# Stream 3: Bilateral Hand Reaching State (Bolted Torso)
|
| 171 |
+
self.active_reach_hand = "right"
|
| 172 |
+
self.reach_target_pos = np.array([0.35, -0.25, 1.35], dtype=np.float32)
|
| 173 |
+
self.reach_touches = 0
|
| 174 |
+
self.prev_reach_dist = 0.40
|
| 175 |
+
self.min_reach_dist = 0.40
|
| 176 |
+
self.steps_on_reach_target = 0
|
| 177 |
+
self.steps_receding_reach = 0
|
| 178 |
+
|
| 179 |
+
# Bilateral gait stance duty-cycle counters
|
| 180 |
+
self.r_foot_contact_steps = 0
|
| 181 |
+
self.l_foot_contact_steps = 0
|
| 182 |
+
|
| 183 |
+
# Offscreen renderer instance (lazy-loaded)
|
| 184 |
+
self._renderer = None
|
| 185 |
+
|
| 186 |
+
# Cache body and geom IDs for fast lookups
|
| 187 |
+
self.torso_body_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_BODY, "torso")
|
| 188 |
+
self.head_body_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_BODY, "head")
|
| 189 |
+
self.r_foot_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_BODY, "right_foot")
|
| 190 |
+
self.l_foot_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_BODY, "left_foot")
|
| 191 |
+
self.r_foot_geom_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_GEOM, "right_foot_geom")
|
| 192 |
+
self.l_foot_geom_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_GEOM, "left_foot_geom")
|
| 193 |
+
self.r_hand_geom_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_GEOM, "right_hand")
|
| 194 |
+
self.l_hand_geom_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_GEOM, "left_hand")
|
| 195 |
+
self.r_arm_body_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_BODY, "right_lower_arm")
|
| 196 |
+
self.l_arm_body_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_BODY, "left_lower_arm")
|
| 197 |
+
|
| 198 |
+
# Cache leg joint qpos indices for athletic stance and gait monitoring
|
| 199 |
+
self.r_knee_qpos = self.model.jnt_qposadr[mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "right_knee")]
|
| 200 |
+
self.l_knee_qpos = self.model.jnt_qposadr[mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "left_knee")]
|
| 201 |
+
self.r_hip_qpos = self.model.jnt_qposadr[mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "right_hip_y")]
|
| 202 |
+
self.l_hip_qpos = self.model.jnt_qposadr[mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "left_hip_y")]
|
| 203 |
+
self.r_ankle_qpos = self.model.jnt_qposadr[mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "right_ankle_y")]
|
| 204 |
+
self.l_ankle_qpos = self.model.jnt_qposadr[mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "left_ankle_y")]
|
| 205 |
+
# Cache abdomen joint qpos indices for core posture alignment
|
| 206 |
+
self.abdomen_x_qpos = self.model.jnt_qposadr[mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "abdomen_x")]
|
| 207 |
+
self.abdomen_y_qpos = self.model.jnt_qposadr[mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "abdomen_y")]
|
| 208 |
+
self.abdomen_z_qpos = self.model.jnt_qposadr[mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "abdomen_z")]
|
| 209 |
+
|
| 210 |
+
# Cache arm joint qpos and dof velocity indices for bilateral reach
|
| 211 |
+
self.arm_qpos_indices = [
|
| 212 |
+
self.model.jnt_qposadr[mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "right_shoulder_x")],
|
| 213 |
+
self.model.jnt_qposadr[mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "right_shoulder_y")],
|
| 214 |
+
self.model.jnt_qposadr[mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "right_elbow")],
|
| 215 |
+
self.model.jnt_qposadr[mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "left_shoulder_x")],
|
| 216 |
+
self.model.jnt_qposadr[mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "left_shoulder_y")],
|
| 217 |
+
self.model.jnt_qposadr[mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "left_elbow")],
|
| 218 |
+
]
|
| 219 |
+
self.arm_dof_indices = [
|
| 220 |
+
self.model.jnt_dofadr[mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "right_shoulder_x")],
|
| 221 |
+
self.model.jnt_dofadr[mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "right_shoulder_y")],
|
| 222 |
+
self.model.jnt_dofadr[mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "right_elbow")],
|
| 223 |
+
self.model.jnt_dofadr[mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "left_shoulder_x")],
|
| 224 |
+
self.model.jnt_dofadr[mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "left_shoulder_y")],
|
| 225 |
+
self.model.jnt_dofadr[mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "left_elbow")],
|
| 226 |
+
]
|
| 227 |
+
|
| 228 |
+
# Cache collision geoms for physical impact detection (head, visor, chest, torso)
|
| 229 |
+
# All geoms belonging to world body (body 0) are environmental surfaces: floor + all obstacles
|
| 230 |
+
self.world_obstacle_geoms = {i for i in range(self.model.ngeom) if self.model.geom_bodyid[i] == 0}
|
| 231 |
+
self.body_impact_geoms = {
|
| 232 |
+
mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_GEOM, "torso_geom"),
|
| 233 |
+
mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_GEOM, "chest_geom"),
|
| 234 |
+
mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_GEOM, "head_geom"),
|
| 235 |
+
mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_GEOM, "visor"),
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
# Cache humanoid geom indices (all geoms belonging to non-world bodies) for ghost rendering
|
| 239 |
+
self.humanoid_geoms = [i for i in range(self.model.ngeom) if self.model.geom_bodyid[i] != 0]
|
| 240 |
+
|
| 241 |
+
# Cache limb geoms for Get-Up multi-phase floor contact righting
|
| 242 |
+
self.r_shin_geom_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_GEOM, "right_shin_geom")
|
| 243 |
+
self.l_shin_geom_id = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_GEOM, "left_shin_geom")
|
| 244 |
+
self.support_hand_geoms = {
|
| 245 |
+
self.r_hand_geom_id,
|
| 246 |
+
self.l_hand_geom_id,
|
| 247 |
+
mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_GEOM, "right_larm_geom"),
|
| 248 |
+
mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_GEOM, "left_larm_geom"),
|
| 249 |
+
}
|
| 250 |
+
self.support_knee_geoms = {self.r_shin_geom_id, self.l_shin_geom_id}
|
| 251 |
+
|
| 252 |
+
def _lock_torso_for_reach(self):
|
| 253 |
+
"""Bolts root torso, core spine, and legs in neutral athletic stance (Stream 3: Isolated Arm Motor Cortex)."""
|
| 254 |
+
self.data.qpos[0] = 0.0
|
| 255 |
+
self.data.qpos[1] = 0.0
|
| 256 |
+
self.data.qpos[2] = 1.35
|
| 257 |
+
self.data.qpos[3:7] = np.array([1.0, 0.0, 0.0, 0.0]) # Upright quaternion
|
| 258 |
+
self.data.qvel[0:6] = 0.0 # Zero base linear & angular velocity
|
| 259 |
+
# Lock core spine neutral
|
| 260 |
+
self.data.qpos[7:10] = 0.0 # abdomen_z, abdomen_y, abdomen_x
|
| 261 |
+
# Lock legs in neutral supportive stance
|
| 262 |
+
self.data.qpos[10:22] = 0.0 # zero all hip, knee, ankle
|
| 263 |
+
self.data.qpos[self.r_knee_qpos] = 0.30
|
| 264 |
+
self.data.qpos[self.l_knee_qpos] = 0.30
|
| 265 |
+
self.data.qpos[self.r_ankle_qpos] = -0.10
|
| 266 |
+
self.data.qpos[self.l_ankle_qpos] = -0.10
|
| 267 |
+
self.data.qvel[6:21] = 0.0 # Zero core & lower body velocities (DOFs 6 to 20, preserving arm DOFs 21:27)
|
| 268 |
+
|
| 269 |
+
def _sample_reach_target(self):
|
| 270 |
+
"""
|
| 271 |
+
Samples a randomized 3D reach goal via Forward Kinematics (FK) from the active arm's
|
| 272 |
+
reachable joint space, mathematically guaranteeing 100.0% of targets are physically reachable (Stream 3).
|
| 273 |
+
"""
|
| 274 |
+
self.active_reach_hand = np.random.choice(["right", "left"])
|
| 275 |
+
|
| 276 |
+
# Save current posture
|
| 277 |
+
saved_qpos = self.data.qpos.copy()
|
| 278 |
+
target_pos = None
|
| 279 |
+
|
| 280 |
+
# Sample joint angles from the arm's valid forward/lateral reachable envelope
|
| 281 |
+
for _ in range(25):
|
| 282 |
+
if self.active_reach_hand == "right":
|
| 283 |
+
# Right arm envelope: shoulder roll/pitch and elbow flexion
|
| 284 |
+
target_sx = float(np.random.uniform(-0.75, 0.40))
|
| 285 |
+
target_sy = float(np.random.uniform(-0.60, 0.85))
|
| 286 |
+
target_el = float(np.random.uniform(-1.10, 0.45))
|
| 287 |
+
self.data.qpos[self.arm_qpos_indices[0]] = target_sx # right_shoulder_x
|
| 288 |
+
self.data.qpos[self.arm_qpos_indices[1]] = target_sy # right_shoulder_y
|
| 289 |
+
self.data.qpos[self.arm_qpos_indices[2]] = target_el # right_elbow
|
| 290 |
+
mujoco.mj_forward(self.model, self.data)
|
| 291 |
+
cand_pos = self.data.geom_xpos[self.r_hand_geom_id].copy()
|
| 292 |
+
else:
|
| 293 |
+
# Left arm envelope: shoulder roll/pitch and elbow flexion
|
| 294 |
+
target_sx = float(np.random.uniform(-0.40, 0.75))
|
| 295 |
+
target_sy = float(np.random.uniform(-0.60, 0.85))
|
| 296 |
+
target_el = float(np.random.uniform(-0.45, 1.10))
|
| 297 |
+
self.data.qpos[self.arm_qpos_indices[3]] = target_sx # left_shoulder_x
|
| 298 |
+
self.data.qpos[self.arm_qpos_indices[4]] = target_sy # left_shoulder_y
|
| 299 |
+
self.data.qpos[self.arm_qpos_indices[5]] = target_el # left_elbow
|
| 300 |
+
mujoco.mj_forward(self.model, self.data)
|
| 301 |
+
cand_pos = self.data.geom_xpos[self.l_hand_geom_id].copy()
|
| 302 |
+
|
| 303 |
+
# Restore original pose to measure distance
|
| 304 |
+
self.data.qpos[:] = saved_qpos
|
| 305 |
+
mujoco.mj_forward(self.model, self.data)
|
| 306 |
+
curr_h = self.data.geom_xpos[self.r_hand_geom_id] if self.active_reach_hand == "right" else self.data.geom_xpos[self.l_hand_geom_id]
|
| 307 |
+
dist_to_cand = float(np.linalg.norm(curr_h - cand_pos))
|
| 308 |
+
target_pos = cand_pos
|
| 309 |
+
# Ensure target is at least 0.16m away so the agent must actively articulate its arm
|
| 310 |
+
if dist_to_cand >= 0.16:
|
| 311 |
+
break
|
| 312 |
+
|
| 313 |
+
# Always restore state cleanly
|
| 314 |
+
self.data.qpos[:] = saved_qpos
|
| 315 |
+
mujoco.mj_forward(self.model, self.data)
|
| 316 |
+
|
| 317 |
+
self.reach_target_pos = target_pos.astype(np.float32)
|
| 318 |
+
self.goal_pos = self.reach_target_pos.copy()
|
| 319 |
+
|
| 320 |
+
def _apply_fall_state(self, pose_name: Optional[str] = None):
|
| 321 |
+
"""
|
| 322 |
+
Initializes agent in a randomized fall state with 360° randomized yaw,
|
| 323 |
+
perturbed joints, physics contact settling, and a randomized standing destination goal (Stream 2).
|
| 324 |
+
"""
|
| 325 |
+
if pose_name is None:
|
| 326 |
+
poses = ["prone", "supine", "lateral_left", "lateral_right", "kneeling_crumpled", "seated_slumped", "random_ragdoll"]
|
| 327 |
+
pose_name = np.random.choice(poses)
|
| 328 |
+
self.fall_state_type = pose_name
|
| 329 |
+
|
| 330 |
+
self.data.qvel[:] = 0.0
|
| 331 |
+
# Random initial floor translation offset
|
| 332 |
+
spawn_x = float(np.random.uniform(-0.4, 0.4))
|
| 333 |
+
spawn_y = float(np.random.uniform(-0.4, 0.4))
|
| 334 |
+
|
| 335 |
+
# Base orientation quaternion [w, x, y, z] per canonical pose
|
| 336 |
+
if pose_name == "prone":
|
| 337 |
+
# Lying on stomach: pitch 90 deg forward
|
| 338 |
+
self.data.qpos[0:3] = np.array([spawn_x, spawn_y, 0.22])
|
| 339 |
+
q_base = np.array([0.7071, 0.0, 0.7071, 0.0])
|
| 340 |
+
self.data.qpos[self.r_knee_qpos] = 0.2
|
| 341 |
+
self.data.qpos[self.l_knee_qpos] = 0.2
|
| 342 |
+
elif pose_name == "supine":
|
| 343 |
+
# Lying on back: pitch 90 deg backward
|
| 344 |
+
self.data.qpos[0:3] = np.array([spawn_x, spawn_y, 0.22])
|
| 345 |
+
q_base = np.array([0.7071, 0.0, -0.7071, 0.0])
|
| 346 |
+
self.data.qpos[self.r_knee_qpos] = 0.3
|
| 347 |
+
self.data.qpos[self.l_knee_qpos] = 0.3
|
| 348 |
+
elif pose_name == "lateral_left":
|
| 349 |
+
# Lying on left flank: roll 90 deg left
|
| 350 |
+
self.data.qpos[0:3] = np.array([spawn_x, spawn_y, 0.22])
|
| 351 |
+
q_base = np.array([0.7071, -0.7071, 0.0, 0.0])
|
| 352 |
+
elif pose_name == "lateral_right":
|
| 353 |
+
# Lying on right flank: roll 90 deg right
|
| 354 |
+
self.data.qpos[0:3] = np.array([spawn_x, spawn_y, 0.22])
|
| 355 |
+
q_base = np.array([0.7071, 0.7071, 0.0, 0.0])
|
| 356 |
+
elif pose_name == "kneeling_crumpled":
|
| 357 |
+
# Crumpled on knees, torso pitched forward
|
| 358 |
+
self.data.qpos[0:3] = np.array([spawn_x, spawn_y, 0.48])
|
| 359 |
+
q_base = np.array([0.9238, 0.0, 0.3826, 0.0])
|
| 360 |
+
self.data.qpos[self.r_knee_qpos] = 1.7
|
| 361 |
+
self.data.qpos[self.l_knee_qpos] = 1.7
|
| 362 |
+
self.data.qpos[self.r_hip_qpos] = -1.0
|
| 363 |
+
self.data.qpos[self.l_hip_qpos] = -1.0
|
| 364 |
+
elif pose_name == "seated_slumped":
|
| 365 |
+
# Slumped seated on buttocks, legs forward
|
| 366 |
+
self.data.qpos[0:3] = np.array([spawn_x, spawn_y, 0.35])
|
| 367 |
+
q_base = np.array([0.9238, 0.0, -0.3826, 0.0])
|
| 368 |
+
self.data.qpos[self.r_hip_qpos] = -1.4
|
| 369 |
+
self.data.qpos[self.l_hip_qpos] = -1.4
|
| 370 |
+
self.data.qpos[self.r_knee_qpos] = 0.2
|
| 371 |
+
self.data.qpos[self.l_knee_qpos] = 0.2
|
| 372 |
+
else:
|
| 373 |
+
# Random ragdoll drop: drop from 0.45m with completely randomized 3D orientation
|
| 374 |
+
self.data.qpos[0:3] = np.array([spawn_x, spawn_y, float(np.random.uniform(0.40, 0.60))])
|
| 375 |
+
rand_quat = np.random.uniform(-1.0, 1.0, size=4)
|
| 376 |
+
q_base = rand_quat / np.linalg.norm(rand_quat)
|
| 377 |
+
|
| 378 |
+
# Apply randomized 360° yaw rotation to base pose quaternion
|
| 379 |
+
yaw = float(np.random.uniform(-np.pi, np.pi))
|
| 380 |
+
q_yaw = np.array([np.cos(yaw / 2.0), 0.0, 0.0, np.sin(yaw / 2.0)], dtype=np.float64)
|
| 381 |
+
# Quaternion multiplication: q_yaw * q_base
|
| 382 |
+
w1, x1, y1, z1 = q_yaw
|
| 383 |
+
w2, x2, y2, z2 = q_base
|
| 384 |
+
q_rot = np.array([
|
| 385 |
+
w1*w2 - x1*x2 - y1*y2 - z1*z2,
|
| 386 |
+
w1*x2 + x1*w2 + y1*z2 - z1*y2,
|
| 387 |
+
w1*y2 - x1*z2 + y1*w2 + z1*x2,
|
| 388 |
+
w1*z2 + x1*y2 - y1*x2 + z1*w2,
|
| 389 |
+
], dtype=np.float64)
|
| 390 |
+
self.data.qpos[3:7] = q_rot / np.linalg.norm(q_rot)
|
| 391 |
+
|
| 392 |
+
# Stochastic joint perturbation across all 21 joint degrees of freedom
|
| 393 |
+
self.data.qpos[7:] += np.random.uniform(-0.20, 0.20, size=self.n_actuators)
|
| 394 |
+
|
| 395 |
+
# Settle contact forces smoothly (12 passive sub-steps with zero actuation)
|
| 396 |
+
mujoco.mj_forward(self.model, self.data)
|
| 397 |
+
for _ in range(12):
|
| 398 |
+
self.data.ctrl[:] = 0.0
|
| 399 |
+
mujoco.mj_step(self.model, self.data)
|
| 400 |
+
self.data.qvel[:] = 0.0
|
| 401 |
+
mujoco.mj_forward(self.model, self.data)
|
| 402 |
+
|
| 403 |
+
# Sample a randomized standing destination goal beacon (0.6m - 1.8m away at random heading)
|
| 404 |
+
stand_dist = float(np.random.uniform(0.6, 1.8))
|
| 405 |
+
stand_heading = float(np.random.uniform(-np.pi, np.pi))
|
| 406 |
+
cur_torso = self.data.xpos[self.torso_body_id]
|
| 407 |
+
gx = float(cur_torso[0] + stand_dist * np.cos(stand_heading))
|
| 408 |
+
gy = float(cur_torso[1] + stand_dist * np.sin(stand_heading))
|
| 409 |
+
gz = 1.168 # Grounded standing torso height equilibrium
|
| 410 |
+
self.getup_goal_pos = np.array([gx, gy, gz], dtype=np.float32)
|
| 411 |
+
self.goal_pos = self.getup_goal_pos.copy()
|
| 412 |
+
|
| 413 |
+
def _sample_new_goal(self, is_initial: bool = False):
|
| 414 |
+
"""Samples a new random navigation target within the 50m x 50m flat arena."""
|
| 415 |
+
torso_pos = self.data.xpos[self.torso_body_id]
|
| 416 |
+
if is_initial:
|
| 417 |
+
# Initial target: distance 5.0m - 7.5m in forward cone (-60 deg to +60 deg)
|
| 418 |
+
dist = float(np.random.uniform(5.0, 7.5))
|
| 419 |
+
angle = float(np.random.uniform(-np.pi / 3, np.pi / 3))
|
| 420 |
+
self.goal_transition_steps = 0 # Spawn directly facing target: immediate approach required
|
| 421 |
+
else:
|
| 422 |
+
# Subsequent target: distance 5.5m - 9.0m in full 360 deg omnidirectional bearing
|
| 423 |
+
dist = float(np.random.uniform(5.5, 9.0))
|
| 424 |
+
angle = float(np.random.uniform(-np.pi, np.pi))
|
| 425 |
+
self.goal_transition_steps = self.transition_leeway_window # ~1.85s direction change leeway
|
| 426 |
+
|
| 427 |
+
gx = float(np.clip(torso_pos[0] + dist * np.cos(angle), -22.0, 22.0))
|
| 428 |
+
gy = float(np.clip(torso_pos[1] + dist * np.sin(angle), -22.0, 22.0))
|
| 429 |
+
|
| 430 |
+
# Ensure newly sampled target is at least (arrival_radius + 1.5m = 3.5m) away from current torso
|
| 431 |
+
for _ in range(20):
|
| 432 |
+
if np.linalg.norm(np.array([gx - torso_pos[0], gy - torso_pos[1]])) >= (self.goal_arrival_radius + 1.5):
|
| 433 |
+
break
|
| 434 |
+
center_angle = float(np.arctan2(-torso_pos[1], -torso_pos[0]))
|
| 435 |
+
angle = float(center_angle + np.random.uniform(-np.pi / 4, np.pi / 4))
|
| 436 |
+
dist = float(np.random.uniform(5.5, 8.5))
|
| 437 |
+
gx = float(np.clip(torso_pos[0] + dist * np.cos(angle), -22.0, 22.0))
|
| 438 |
+
gy = float(np.clip(torso_pos[1] + dist * np.sin(angle), -22.0, 22.0))
|
| 439 |
+
|
| 440 |
+
gz = 1.25 # Torso/chest level
|
| 441 |
+
self.goal_pos = np.array([gx, gy, gz], dtype=np.float32)
|
| 442 |
+
self.min_dist_to_goal = float(np.linalg.norm(self.goal_pos - torso_pos))
|
| 443 |
+
self.prev_dist_to_goal = float(self.min_dist_to_goal)
|
| 444 |
+
self.steps_since_progress = 0
|
| 445 |
+
self.steps_not_approaching = 0
|
| 446 |
+
self.negative_vx_steps = 0
|
| 447 |
+
|
| 448 |
+
@property
|
| 449 |
+
def observation_dim(self) -> int:
|
| 450 |
+
"""
|
| 451 |
+
Total observation vector length:
|
| 452 |
+
- Torso height z: 1
|
| 453 |
+
- Torso orientation (forward & up vectors): 6
|
| 454 |
+
- Torso linear velocity (local/global): 3
|
| 455 |
+
- Torso angular velocity: 3
|
| 456 |
+
- 21 joint angles: 21
|
| 457 |
+
- 21 joint velocities: 21
|
| 458 |
+
- 2 foot contact forces: 2
|
| 459 |
+
- 512 solid-state LiDAR range rays (16x32 grid): 512
|
| 460 |
+
- 3 ego-centric relative waypoint vector (Δx, Δy, Δz local): 3
|
| 461 |
+
- 3 metabolic stats (stamina, health, hunger): 3
|
| 462 |
+
Total: 1 + 6 + 3 + 3 + 21 + 21 + 2 + 512 + 3 + 3 = 575
|
| 463 |
+
"""
|
| 464 |
+
return 575
|
| 465 |
+
|
| 466 |
+
@property
|
| 467 |
+
def action_dim(self) -> int:
|
| 468 |
+
return self.n_actuators # 21
|
| 469 |
+
|
| 470 |
+
def reset(self, seed: Optional[int] = None, spawn_x: float = 0.0, initial_vx: float = 0.0) -> np.ndarray:
|
| 471 |
+
if seed is not None:
|
| 472 |
+
np.random.seed(seed)
|
| 473 |
+
|
| 474 |
+
mujoco.mj_resetData(self.model, self.data)
|
| 475 |
+
|
| 476 |
+
# Initial root pose: at X=spawn_x, Y=0, Z=1.33 (athletic bent-knee stance)
|
| 477 |
+
self.data.qpos[0] = spawn_x # X
|
| 478 |
+
self.data.qpos[1] = 0.0 # Y
|
| 479 |
+
self.data.qpos[2] = 1.33 # Z
|
| 480 |
+
self.data.qpos[3:7] = np.array([1.0, 0.0, 0.0, 0.0]) # Quaternion (unit)
|
| 481 |
+
|
| 482 |
+
# Staggered lead leg initialization: alternates 50% left lead, 50% right lead
|
| 483 |
+
# This breaks single-leg asymmetric gait traps and forces both thighs to cycle
|
| 484 |
+
lead_leg = np.random.choice(["right", "left"])
|
| 485 |
+
if lead_leg == "right":
|
| 486 |
+
self.data.qpos[self.r_hip_qpos] = -0.35 # Right thigh poised forward
|
| 487 |
+
self.data.qpos[self.r_knee_qpos] = 0.45
|
| 488 |
+
self.data.qpos[self.r_ankle_qpos] = -0.15
|
| 489 |
+
self.data.qpos[self.l_hip_qpos] = 0.05 # Left leg supporting in stance
|
| 490 |
+
self.data.qpos[self.l_knee_qpos] = 0.25
|
| 491 |
+
self.data.qpos[self.l_ankle_qpos] = -0.05
|
| 492 |
+
else:
|
| 493 |
+
self.data.qpos[self.l_hip_qpos] = -0.35 # Left thigh poised forward
|
| 494 |
+
self.data.qpos[self.l_knee_qpos] = 0.45
|
| 495 |
+
self.data.qpos[self.l_ankle_qpos] = -0.15
|
| 496 |
+
self.data.qpos[self.r_hip_qpos] = 0.05 # Right leg supporting in stance
|
| 497 |
+
self.data.qpos[self.r_knee_qpos] = 0.25
|
| 498 |
+
self.data.qpos[self.r_ankle_qpos] = -0.05
|
| 499 |
+
|
| 500 |
+
# Add slight stochastic noise to joint angles for exploration diversity
|
| 501 |
+
joint_noise = np.random.uniform(-0.03, 0.03, size=self.n_actuators)
|
| 502 |
+
self.data.qpos[7:] += joint_noise
|
| 503 |
+
|
| 504 |
+
# Zero velocities with tiny noise (or inject forward running momentum if pioneer spawn)
|
| 505 |
+
self.data.qvel[:] = np.random.uniform(-0.02, 0.02, size=self.n_qvel)
|
| 506 |
+
if initial_vx > 0.0:
|
| 507 |
+
self.data.qvel[0] = initial_vx + np.random.uniform(-0.1, 0.1)
|
| 508 |
+
|
| 509 |
+
# Reset internal stats
|
| 510 |
+
self.stamina = 1.0
|
| 511 |
+
self.health = 1.0
|
| 512 |
+
self.hunger = 0.0
|
| 513 |
+
|
| 514 |
+
self.milestones_reached = set()
|
| 515 |
+
self.waypoints_reached = set()
|
| 516 |
+
self.current_wp_idx = 0
|
| 517 |
+
|
| 518 |
+
if self.stream_mode == "stand":
|
| 519 |
+
# Stream 1: Quiet Standing Balance (Canonical Reference Equilibrium Stance)
|
| 520 |
+
self.data.qpos[0] = 0.0
|
| 521 |
+
self.data.qpos[1] = 0.0
|
| 522 |
+
self.data.qpos[2] = 1.168
|
| 523 |
+
self.data.qpos[3:7] = np.array([1.0, 0.0, 0.0, 0.0])
|
| 524 |
+
self.data.qvel[:] = 0.0
|
| 525 |
+
self.data.qpos[7:7 + self.n_actuators] = STAND_REF_QPOS.copy()
|
| 526 |
+
# Add slight stochastic exploration noise (+/- 0.015 rad ~ 0.8 deg) so agent learns to balance
|
| 527 |
+
self.data.qpos[7:7 + self.n_actuators] += np.random.uniform(-0.015, 0.015, size=self.n_actuators)
|
| 528 |
+
# Settle foot ground contacts smoothly (10 passive sub-steps with zero actuation)
|
| 529 |
+
mujoco.mj_forward(self.model, self.data)
|
| 530 |
+
for _ in range(10):
|
| 531 |
+
self.data.ctrl[:] = 0.0
|
| 532 |
+
mujoco.mj_step(self.model, self.data)
|
| 533 |
+
self.data.qvel[:] = 0.0
|
| 534 |
+
mujoco.mj_forward(self.model, self.data)
|
| 535 |
+
self.goal_pos = np.array([0.0, 0.0, 1.168], dtype=np.float32)
|
| 536 |
+
self.stand_still_steps = 0
|
| 537 |
+
self.stand_drift_steps = 0
|
| 538 |
+
self.stand_one_leg_steps = 0
|
| 539 |
+
self.goals_completed = 0
|
| 540 |
+
elif self.stream_mode == "getup":
|
| 541 |
+
# Stream 2: Fall Recovery (Randomized 360° poses & standing destination goal)
|
| 542 |
+
self._apply_fall_state()
|
| 543 |
+
self.prev_z = float(self.data.xpos[self.torso_body_id][2])
|
| 544 |
+
self.prev_head_z = float(self.data.xpos[self.head_body_id][2])
|
| 545 |
+
self.consecutive_upright_steps = 0
|
| 546 |
+
self.getup_stagnant_steps = 0
|
| 547 |
+
self.max_elevation_achieved = self.prev_z
|
| 548 |
+
self.max_head_elevation = self.prev_head_z
|
| 549 |
+
self.getup_success_count = 0
|
| 550 |
+
self.goals_completed = 0
|
| 551 |
+
elif self.stream_mode == "reach":
|
| 552 |
+
# Stream 2: Bilateral Hand Reaching (Bolted Torso with neutral resting arm configuration)
|
| 553 |
+
self._lock_torso_for_reach()
|
| 554 |
+
# Initialize both arms down at sides (natural resting posture at hip)
|
| 555 |
+
self.data.qpos[self.arm_qpos_indices[0]] = 0.65 + float(np.random.uniform(-0.03, 0.03)) # right_shoulder_x (down at side)
|
| 556 |
+
self.data.qpos[self.arm_qpos_indices[1]] = float(np.random.uniform(-0.03, 0.03)) # right_shoulder_y
|
| 557 |
+
self.data.qpos[self.arm_qpos_indices[2]] = float(np.random.uniform(-0.03, 0.03)) # right_elbow
|
| 558 |
+
self.data.qpos[self.arm_qpos_indices[3]] = -0.65 + float(np.random.uniform(-0.03, 0.03)) # left_shoulder_x (down at side)
|
| 559 |
+
self.data.qpos[self.arm_qpos_indices[4]] = float(np.random.uniform(-0.03, 0.03)) # left_shoulder_y
|
| 560 |
+
self.data.qpos[self.arm_qpos_indices[5]] = float(np.random.uniform(-0.03, 0.03)) # left_elbow
|
| 561 |
+
self.data.qvel[self.arm_dof_indices] = 0.0
|
| 562 |
+
mujoco.mj_forward(self.model, self.data)
|
| 563 |
+
self._sample_reach_target()
|
| 564 |
+
is_right_target = (self.active_reach_hand == "right")
|
| 565 |
+
active_h = self.data.geom_xpos[self.r_hand_geom_id] if is_right_target else self.data.geom_xpos[self.l_hand_geom_id]
|
| 566 |
+
self.prev_reach_dist = float(np.linalg.norm(active_h - self.reach_target_pos))
|
| 567 |
+
self.min_reach_dist = self.prev_reach_dist
|
| 568 |
+
self.reach_touches = 0
|
| 569 |
+
self.steps_on_reach_target = 0
|
| 570 |
+
self.steps_receding_reach = 0
|
| 571 |
+
self.goals_completed = 0
|
| 572 |
+
elif self.arena_mode:
|
| 573 |
+
self.goals_completed = 0
|
| 574 |
+
self._sample_new_goal(is_initial=True)
|
| 575 |
+
else:
|
| 576 |
+
# Fast-forward waypoint index if spawning downfield
|
| 577 |
+
for idx, (wp_pos, _, _) in enumerate(COURSE_WAYPOINTS):
|
| 578 |
+
if wp_pos[0] > spawn_x + 0.3:
|
| 579 |
+
self.current_wp_idx = idx
|
| 580 |
+
break
|
| 581 |
+
|
| 582 |
+
self.spawn_x = float(spawn_x)
|
| 583 |
+
self.furthest_x = float(spawn_x)
|
| 584 |
+
self.current_step = 0
|
| 585 |
+
self.steps_since_progress = 0
|
| 586 |
+
self.last_progress_x = float(spawn_x)
|
| 587 |
+
self.negative_vx_steps = 0
|
| 588 |
+
self.steps_not_approaching = 0
|
| 589 |
+
self.r_foot_contact_steps = 0
|
| 590 |
+
self.l_foot_contact_steps = 0
|
| 591 |
+
|
| 592 |
+
mujoco.mj_forward(self.model, self.data)
|
| 593 |
+
return self._get_obs()
|
| 594 |
+
|
| 595 |
+
def _get_obs(self) -> np.ndarray:
|
| 596 |
+
# 1. Torso state
|
| 597 |
+
torso_xpos = self.data.xpos[self.torso_body_id] # (3,)
|
| 598 |
+
torso_xmat = self.data.xmat[self.torso_body_id].reshape(3, 3) # (3, 3)
|
| 599 |
+
forward_vec = torso_xmat[:, 0] # Local X axis in world frame
|
| 600 |
+
up_vec = torso_xmat[:, 2] # Local Z axis in world frame
|
| 601 |
+
|
| 602 |
+
torso_cvel = self.data.cvel[self.torso_body_id] # 6D: [ang_vel(3), lin_vel(3)]
|
| 603 |
+
ang_vel = torso_cvel[:3]
|
| 604 |
+
lin_vel = torso_cvel[3:]
|
| 605 |
+
|
| 606 |
+
# 2. Joint states (21 angles, 21 velocities)
|
| 607 |
+
joint_qpos = self.data.qpos[7:7 + self.n_actuators]
|
| 608 |
+
joint_qvel = self.data.qvel[6:6 + self.n_actuators]
|
| 609 |
+
|
| 610 |
+
# 3. Touch sensors & Task-Specific Contact Indicators
|
| 611 |
+
touch_forces = np.zeros(2, dtype=np.float32)
|
| 612 |
+
if self.stream_mode == "reach":
|
| 613 |
+
# For isolated Reach, legs are bolted stationary. Repurpose touch_forces as explicit Active Hand Indicator:
|
| 614 |
+
# [1.0, 0.0] -> Right Hand Active | [0.0, 1.0] -> Left Hand Active
|
| 615 |
+
if self.active_reach_hand == "right":
|
| 616 |
+
touch_forces[0] = 1.0
|
| 617 |
+
touch_forces[1] = 0.0
|
| 618 |
+
else:
|
| 619 |
+
touch_forces[0] = 0.0
|
| 620 |
+
touch_forces[1] = 1.0
|
| 621 |
+
else:
|
| 622 |
+
if self.model.nsensor >= 2:
|
| 623 |
+
touch_forces[0] = np.clip(self.data.sensordata[0] / 100.0, 0.0, 5.0) # Right foot
|
| 624 |
+
touch_forces[1] = np.clip(self.data.sensordata[1] / 100.0, 0.0, 5.0) # Left foot
|
| 625 |
+
|
| 626 |
+
# 4. True Solid-State Angular LiDAR (512 rays from visor/eyes)
|
| 627 |
+
lidar_obs = self._compute_lidar_vision()
|
| 628 |
+
|
| 629 |
+
# 5. Dynamic Goal / Progressive Waypoint (Ego-centric local relative vector)
|
| 630 |
+
if self.stream_mode == "stand":
|
| 631 |
+
rel_world = self.goal_pos - torso_xpos
|
| 632 |
+
rel_local = torso_xmat.T @ rel_world
|
| 633 |
+
waypoint_obs = np.array([
|
| 634 |
+
np.clip(rel_local[0] / 10.0, -1.0, 1.0),
|
| 635 |
+
np.clip(rel_local[1] / 5.0, -1.0, 1.0),
|
| 636 |
+
np.clip(rel_local[2] / 2.0, -1.0, 1.0),
|
| 637 |
+
], dtype=np.float32)
|
| 638 |
+
elif self.stream_mode == "getup":
|
| 639 |
+
rel_world = self.getup_goal_pos - torso_xpos
|
| 640 |
+
rel_local = torso_xmat.T @ rel_world
|
| 641 |
+
waypoint_obs = np.array([
|
| 642 |
+
np.clip(rel_local[0] / 5.0, -1.0, 1.0),
|
| 643 |
+
np.clip(rel_local[1] / 5.0, -1.0, 1.0),
|
| 644 |
+
np.clip(rel_local[2] / 2.0, -1.0, 1.0),
|
| 645 |
+
], dtype=np.float32)
|
| 646 |
+
elif self.stream_mode == "reach":
|
| 647 |
+
# Direct closed-loop motor servoing: vector from ACTIVE HAND directly to the reach target orb!
|
| 648 |
+
active_h = self.data.geom_xpos[self.r_hand_geom_id] if (self.active_reach_hand == "right") else self.data.geom_xpos[self.l_hand_geom_id]
|
| 649 |
+
rel_hand_world = self.reach_target_pos - active_h
|
| 650 |
+
rel_hand_local = torso_xmat.T @ rel_hand_world
|
| 651 |
+
waypoint_obs = np.array([
|
| 652 |
+
np.clip(rel_hand_local[0] / 0.6, -1.0, 1.0),
|
| 653 |
+
np.clip(rel_hand_local[1] / 0.6, -1.0, 1.0),
|
| 654 |
+
np.clip(rel_hand_local[2] / 0.6, -1.0, 1.0),
|
| 655 |
+
], dtype=np.float32)
|
| 656 |
+
elif self.arena_mode:
|
| 657 |
+
target_pos = self.goal_pos
|
| 658 |
+
rel_world = target_pos - torso_xpos
|
| 659 |
+
rel_local = torso_xmat.T @ rel_world
|
| 660 |
+
waypoint_obs = np.array([
|
| 661 |
+
np.clip(rel_local[0] / 10.0, -1.0, 1.0),
|
| 662 |
+
np.clip(rel_local[1] / 5.0, -1.0, 1.0),
|
| 663 |
+
np.clip(rel_local[2] / 2.0, -1.0, 1.0),
|
| 664 |
+
], dtype=np.float32)
|
| 665 |
+
else:
|
| 666 |
+
target_pos = COURSE_WAYPOINTS[self.current_wp_idx][0]
|
| 667 |
+
rel_world = target_pos - torso_xpos
|
| 668 |
+
rel_local = torso_xmat.T @ rel_world
|
| 669 |
+
waypoint_obs = np.array([
|
| 670 |
+
np.clip(rel_local[0] / 10.0, -1.0, 1.0),
|
| 671 |
+
np.clip(rel_local[1] / 5.0, -1.0, 1.0),
|
| 672 |
+
np.clip(rel_local[2] / 2.0, -1.0, 1.0),
|
| 673 |
+
], dtype=np.float32)
|
| 674 |
+
|
| 675 |
+
# 6. Metabolic state (stamina, health, hunger/task-channel)
|
| 676 |
+
if self.stream_mode == "reach":
|
| 677 |
+
hand_indicator = 1.0 if (self.active_reach_hand == "right") else -1.0
|
| 678 |
+
metabolic_obs = np.array([self.stamina, self.health, hand_indicator], dtype=np.float32)
|
| 679 |
+
else:
|
| 680 |
+
metabolic_obs = np.array([self.stamina, self.health, self.hunger], dtype=np.float32)
|
| 681 |
+
|
| 682 |
+
obs = np.concatenate([
|
| 683 |
+
np.array([torso_xpos[2]], dtype=np.float32), # Torso Z (1)
|
| 684 |
+
forward_vec.astype(np.float32), # 3
|
| 685 |
+
up_vec.astype(np.float32), # 3
|
| 686 |
+
lin_vel.astype(np.float32), # 3
|
| 687 |
+
ang_vel.astype(np.float32), # 3
|
| 688 |
+
joint_qpos.astype(np.float32), # 21
|
| 689 |
+
joint_qvel.astype(np.float32), # 21
|
| 690 |
+
touch_forces, # 2
|
| 691 |
+
lidar_obs, # 512
|
| 692 |
+
waypoint_obs, # 3
|
| 693 |
+
metabolic_obs, # 3
|
| 694 |
+
])
|
| 695 |
+
|
| 696 |
+
return np.nan_to_num(obs, nan=0.0, posinf=50.0, neginf=-50.0)
|
| 697 |
+
|
| 698 |
+
def _compute_lidar_vision(self) -> np.ndarray:
|
| 699 |
+
"""
|
| 700 |
+
True Solid-State Angular LiDAR scan from the robot's visor/eyes.
|
| 701 |
+
Casts 128 rays (8 vertical elevation x 16 horizontal azimuth) using MuJoCo's native C raycaster.
|
| 702 |
+
Returns normalized range vector (128,) in [0.0, 1.0].
|
| 703 |
+
"""
|
| 704 |
+
head_pos = self.data.xpos[self.head_body_id]
|
| 705 |
+
head_mat = self.data.xmat[self.head_body_id].reshape(3, 3)
|
| 706 |
+
|
| 707 |
+
# Ray origin: 0.15m forward from head center through visor
|
| 708 |
+
ray_origin = np.ascontiguousarray(head_pos + head_mat @ self._visor_offset, dtype=np.float64)
|
| 709 |
+
|
| 710 |
+
# Transform all 128 precomputed local vectors to world coordinates
|
| 711 |
+
# (128, 3) @ (3, 3).T -> (128, 3)
|
| 712 |
+
world_vectors = self.lidar_local_vectors @ head_mat.T
|
| 713 |
+
|
| 714 |
+
normalized_ranges = np.zeros(self.num_lidar_rays, dtype=np.float32)
|
| 715 |
+
raw_grid = np.zeros((self.num_lidar_v, self.num_lidar_h), dtype=np.float32)
|
| 716 |
+
|
| 717 |
+
ray_idx = 0
|
| 718 |
+
for row in range(self.num_lidar_v):
|
| 719 |
+
for col in range(self.num_lidar_h):
|
| 720 |
+
vec = np.ascontiguousarray(world_vectors[ray_idx], dtype=np.float64)
|
| 721 |
+
dist = mujoco.mj_ray(self.model, self.data, ray_origin, vec, None, 1, -1, self._geomid_buf)
|
| 722 |
+
|
| 723 |
+
if dist >= 0:
|
| 724 |
+
gid = int(self._geomid_buf[0])
|
| 725 |
+
if gid in self.humanoid_geoms:
|
| 726 |
+
actual_dist = self.lidar_max_range
|
| 727 |
+
else:
|
| 728 |
+
actual_dist = min(dist, self.lidar_max_range)
|
| 729 |
+
else:
|
| 730 |
+
actual_dist = self.lidar_max_range
|
| 731 |
+
|
| 732 |
+
raw_grid[row, col] = actual_dist
|
| 733 |
+
# Zero-centered range: -0.5 (impact / 0m) to +0.5 (clear / 10m), eliminates DC bias
|
| 734 |
+
normalized_ranges[ray_idx] = (actual_dist / self.lidar_max_range) - 0.5
|
| 735 |
+
ray_idx += 1
|
| 736 |
+
|
| 737 |
+
self.current_lidar_grid = raw_grid
|
| 738 |
+
return normalized_ranges
|
| 739 |
+
|
| 740 |
+
def _compute_pseudo_vision(self, agent_x: float, agent_y: float, agent_z: float) -> np.ndarray:
|
| 741 |
+
"""Backwards compatibility fallback for 1D pseudo-vision."""
|
| 742 |
+
return self._compute_lidar_vision()[:16]
|
| 743 |
+
|
| 744 |
+
def step(self, action: np.ndarray) -> Tuple[np.ndarray, float, bool, bool, Dict]:
|
| 745 |
+
self.current_step += 1
|
| 746 |
+
|
| 747 |
+
# Clip action to [-1.0, 1.0]
|
| 748 |
+
action = np.clip(action, -1.0, 1.0)
|
| 749 |
+
self.data.ctrl[:] = action
|
| 750 |
+
if self.stream_mode == "reach":
|
| 751 |
+
# For isolated arm reaching, freeze motor commands to core and legs
|
| 752 |
+
self.data.ctrl[0:15] = 0.0
|
| 753 |
+
|
| 754 |
+
# Substep physics
|
| 755 |
+
for _ in range(self.frame_skip):
|
| 756 |
+
mujoco.mj_step(self.model, self.data)
|
| 757 |
+
if self.stream_mode == "reach":
|
| 758 |
+
self._lock_torso_for_reach()
|
| 759 |
+
|
| 760 |
+
# Extract state measurements
|
| 761 |
+
torso_xpos = self.data.xpos[self.torso_body_id]
|
| 762 |
+
x, y, z = torso_xpos[0], torso_xpos[1], torso_xpos[2]
|
| 763 |
+
|
| 764 |
+
torso_cvel = self.data.cvel[self.torso_body_id]
|
| 765 |
+
vx = torso_cvel[3] # Forward linear velocity
|
| 766 |
+
vy = torso_cvel[4]
|
| 767 |
+
|
| 768 |
+
torso_xmat = self.data.xmat[self.torso_body_id].reshape(3, 3)
|
| 769 |
+
up_z = torso_xmat[2, 2] # Cosine of torso vertical tilt
|
| 770 |
+
|
| 771 |
+
# Update metabolic stats
|
| 772 |
+
torque_cost = np.sum(np.square(action))
|
| 773 |
+
self.stamina = np.clip(self.stamina - (torque_cost * 0.0003) + 0.0005, 0.0, 1.0)
|
| 774 |
+
self.hunger = np.clip(self.hunger + 0.0001, 0.0, 1.0)
|
| 775 |
+
|
| 776 |
+
# Multi-limb ground contact detection for floor righting and locomotion
|
| 777 |
+
contact_r = False
|
| 778 |
+
contact_l = False
|
| 779 |
+
contact_hand = False
|
| 780 |
+
contact_knee = False
|
| 781 |
+
for c_idx in range(self.data.ncon):
|
| 782 |
+
c = self.data.contact[c_idx]
|
| 783 |
+
g1, g2 = c.geom1, c.geom2
|
| 784 |
+
if (g1 == self.r_foot_geom_id and g2 in self.world_obstacle_geoms) or (g2 == self.r_foot_geom_id and g1 in self.world_obstacle_geoms):
|
| 785 |
+
contact_r = True
|
| 786 |
+
if (g1 == self.l_foot_geom_id and g2 in self.world_obstacle_geoms) or (g2 == self.l_foot_geom_id and g1 in self.world_obstacle_geoms):
|
| 787 |
+
contact_l = True
|
| 788 |
+
if (g1 in self.support_hand_geoms and g2 in self.world_obstacle_geoms) or (g2 in self.support_hand_geoms and g1 in self.world_obstacle_geoms):
|
| 789 |
+
contact_hand = True
|
| 790 |
+
if (g1 in self.support_knee_geoms and g2 in self.world_obstacle_geoms) or (g2 in self.support_knee_geoms and g1 in self.world_obstacle_geoms):
|
| 791 |
+
contact_knee = True
|
| 792 |
+
|
| 793 |
+
touch_r = contact_r or (bool(self.data.sensordata[0] > 5.0) if self.model.nsensor >= 1 else False)
|
| 794 |
+
touch_l = contact_l or (bool(self.data.sensordata[1] > 5.0) if self.model.nsensor >= 2 else False)
|
| 795 |
+
if touch_r:
|
| 796 |
+
self.r_foot_contact_steps += 1
|
| 797 |
+
if touch_l:
|
| 798 |
+
self.l_foot_contact_steps += 1
|
| 799 |
+
|
| 800 |
+
upright_scale = float(np.clip((z - 0.45) / 0.40, 0.0, 1.0)) # 0 at z<=0.45m, 1.0 at z>=0.85m
|
| 801 |
+
in_leeway = False
|
| 802 |
+
torso_speed = float(np.linalg.norm(torso_cvel[3:]))
|
| 803 |
+
|
| 804 |
+
# ── STREAM 1: QUIET STANDING STILLNESS (REFERENCE POSE SIMILARITY) ────────
|
| 805 |
+
if self.stream_mode == "stand":
|
| 806 |
+
head_z = float(self.data.xpos[self.head_body_id][2])
|
| 807 |
+
dist_from_origin = float(np.linalg.norm([x, y]))
|
| 808 |
+
|
| 809 |
+
# 1. Joint Configuration Similarity:
|
| 810 |
+
# Gaussian kernel comparing 21 actuated joints against canonical STAND_REF_QPOS
|
| 811 |
+
current_qpos = self.data.qpos[7:7 + self.n_actuators]
|
| 812 |
+
diff_q = current_qpos - STAND_REF_QPOS
|
| 813 |
+
weighted_q_mse = float(np.sum(STAND_JOINT_WEIGHTS * (diff_q ** 2)) / np.sum(STAND_JOINT_WEIGHTS))
|
| 814 |
+
r_pose = float(np.exp(-3.0 * weighted_q_mse))
|
| 815 |
+
|
| 816 |
+
# 2. Torso Center-of-Mass Height Similarity (centered at z* = 1.168m):
|
| 817 |
+
r_height = float(np.exp(-30.0 * ((z - 1.168) ** 2)))
|
| 818 |
+
|
| 819 |
+
# 3. Torso Upright Orientation Similarity:
|
| 820 |
+
r_upright = float(np.exp(-12.0 * (max(0.0, 1.0 - up_z) ** 2)))
|
| 821 |
+
|
| 822 |
+
# 4. Multi-Rigid-Body Stillness Similarity (root linear, root angular, joint vel):
|
| 823 |
+
lin_vel_sq = float(np.sum(torso_cvel[3:] ** 2))
|
| 824 |
+
ang_vel_sq = float(np.sum(torso_cvel[:3] ** 2))
|
| 825 |
+
joint_vel_sq = float(np.mean(self.data.qvel[6:6 + self.n_actuators] ** 2))
|
| 826 |
+
r_stillness = float(np.exp(-4.0 * (lin_vel_sq + 0.5 * ang_vel_sq + 0.1 * joint_vel_sq)))
|
| 827 |
+
|
| 828 |
+
# 5. Foot Ground Contact Multiplier:
|
| 829 |
+
both_feet_down = bool(touch_r and touch_l)
|
| 830 |
+
one_foot_down = bool(touch_r or touch_l)
|
| 831 |
+
if both_feet_down:
|
| 832 |
+
m_feet = 1.0
|
| 833 |
+
elif one_foot_down:
|
| 834 |
+
m_feet = 0.45
|
| 835 |
+
else:
|
| 836 |
+
m_feet = 0.05
|
| 837 |
+
|
| 838 |
+
# 6. Action Control / Effort Penalty:
|
| 839 |
+
r_ctrl = -0.002 * float(np.sum(action ** 2))
|
| 840 |
+
|
| 841 |
+
# Bounded Gaussian Similarity Reward (Peak = 14.0/step at quiet standing equilibrium)
|
| 842 |
+
total_reward = m_feet * (4.0 * r_pose + 3.0 * r_height + 3.0 * r_upright + 4.0 * r_stillness) + r_ctrl
|
| 843 |
+
|
| 844 |
+
# Track stillness success criteria:
|
| 845 |
+
is_stand_still = bool(
|
| 846 |
+
r_pose > 0.85 and
|
| 847 |
+
r_height > 0.88 and
|
| 848 |
+
r_upright > 0.90 and
|
| 849 |
+
r_stillness > 0.80 and
|
| 850 |
+
both_feet_down
|
| 851 |
+
)
|
| 852 |
+
if is_stand_still:
|
| 853 |
+
self.stand_still_steps += 1
|
| 854 |
+
else:
|
| 855 |
+
self.stand_still_steps = max(0, self.stand_still_steps - 1)
|
| 856 |
+
|
| 857 |
+
# Check collision with environmental obstacles/ground using body geoms (head, chest, torso, visor)
|
| 858 |
+
is_obstacle_or_ground_impact = False
|
| 859 |
+
for c_idx in range(self.data.ncon):
|
| 860 |
+
c = self.data.contact[c_idx]
|
| 861 |
+
if (c.geom1 in self.body_impact_geoms and c.geom2 in self.world_obstacle_geoms) or \
|
| 862 |
+
(c.geom2 in self.body_impact_geoms and c.geom1 in self.world_obstacle_geoms):
|
| 863 |
+
is_obstacle_or_ground_impact = True
|
| 864 |
+
break
|
| 865 |
+
|
| 866 |
+
terminated = False
|
| 867 |
+
# Clean early termination only when genuinely fallen or severely displaced:
|
| 868 |
+
if z < 0.85 or up_z < 0.55 or is_obstacle_or_ground_impact:
|
| 869 |
+
terminated = True
|
| 870 |
+
total_reward -= 15.0
|
| 871 |
+
elif dist_from_origin > 1.20:
|
| 872 |
+
terminated = True
|
| 873 |
+
total_reward -= 10.0
|
| 874 |
+
|
| 875 |
+
# Mastery time limit: 384 steps (~9.6s)
|
| 876 |
+
truncated = (self.current_step >= 384)
|
| 877 |
+
if truncated and (not terminated) and z > 1.10 and both_feet_down:
|
| 878 |
+
total_reward += 50.0 # Mastery bonus for surviving 384 steps upright on both feet
|
| 879 |
+
|
| 880 |
+
# Detailed metrics for logging & dashboard
|
| 881 |
+
torso_roll = abs(float(torso_xmat[2, 1]))
|
| 882 |
+
torso_pitch = abs(float(torso_xmat[2, 0]))
|
| 883 |
+
spine_lateral = abs(float(self.data.qpos[self.abdomen_x_qpos]))
|
| 884 |
+
spine_sagittal = abs(float(self.data.qpos[self.abdomen_y_qpos]))
|
| 885 |
+
|
| 886 |
+
info = {
|
| 887 |
+
"x_pos": float(x), "y_pos": float(y), "z_pos": float(z),
|
| 888 |
+
"vx": float(vx), "torso_speed": float(torso_speed),
|
| 889 |
+
"furthest_x": float(self.furthest_x), "milestones": 0,
|
| 890 |
+
"waypoint_idx": 0, "waypoint_name": "Quiet Stand Stillness",
|
| 891 |
+
"dist_to_wp": float(dist_from_origin), "waypoints_completed": int(self.stand_still_steps > 150),
|
| 892 |
+
"goals_completed": int(self.stand_still_steps > 150),
|
| 893 |
+
"dist_to_goal": float(dist_from_origin),
|
| 894 |
+
"goal_arrival_radius": 1.20,
|
| 895 |
+
"goal_pos": [0.0, 0.0, 1.168],
|
| 896 |
+
"target_local_pos": [0.0, 0.0, 0.0],
|
| 897 |
+
"gaze_alignment": 1.0,
|
| 898 |
+
"stamina": float(self.stamina), "health": float(self.health), "hunger": float(self.hunger),
|
| 899 |
+
"step": self.current_step, "lidar_grid": self.current_lidar_grid.tolist(),
|
| 900 |
+
"in_leeway": False, "leeway_remaining": 0,
|
| 901 |
+
"stream_mode": "stand", "active_reach_hand": "none", "reach_touches": 0,
|
| 902 |
+
"getup_success": False,
|
| 903 |
+
"both_feet_down": bool(both_feet_down),
|
| 904 |
+
"one_foot_down": bool(one_foot_down),
|
| 905 |
+
"stand_still_score": float(is_stand_still),
|
| 906 |
+
"torso_roll": float(torso_roll),
|
| 907 |
+
"torso_pitch": float(torso_pitch),
|
| 908 |
+
"spine_lateral": float(spine_lateral),
|
| 909 |
+
"spine_sagittal": float(spine_sagittal),
|
| 910 |
+
"r_pose": float(r_pose),
|
| 911 |
+
"r_height": float(r_height),
|
| 912 |
+
"r_upright": float(r_upright),
|
| 913 |
+
"r_stillness": float(r_stillness),
|
| 914 |
+
}
|
| 915 |
+
return self._get_obs(), float(total_reward), terminated, truncated, info
|
| 916 |
+
|
| 917 |
+
# ── STREAM 2: FALL RECOVERY (GET-UP) ────────────────────────────────
|
| 918 |
+
elif self.stream_mode == "getup":
|
| 919 |
+
head_z = float(self.data.xpos[self.head_body_id][2])
|
| 920 |
+
delta_head_z = head_z - self.prev_head_z
|
| 921 |
+
self.prev_head_z = head_z
|
| 922 |
+
|
| 923 |
+
r_foot_z = float(self.data.xpos[self.r_foot_id][2])
|
| 924 |
+
l_foot_z = float(self.data.xpos[self.l_foot_id][2])
|
| 925 |
+
max_foot_z = max(r_foot_z, l_foot_z)
|
| 926 |
+
|
| 927 |
+
# 1. REWARD FOR HEAD & TORSO ELEVATION (Rises vertically, head leads above torso):
|
| 928 |
+
reward_head_elevation = 16.0 * delta_head_z
|
| 929 |
+
reward_torso_elevation = 12.0 * max(0.0, z - self.prev_z)
|
| 930 |
+
reward_head_height = 5.0 * max(0.0, head_z - 0.40)
|
| 931 |
+
reward_head_above_torso = 3.5 * max(0.0, head_z - z)
|
| 932 |
+
reward_upright = 3.0 * max(0.0, up_z - 0.30)
|
| 933 |
+
|
| 934 |
+
# 2. MULTI-PHASE GROUND CONTACT SUPPORT (Hands, Knees, Feet):
|
| 935 |
+
both_feet_down = touch_r and touch_l
|
| 936 |
+
one_foot_down = touch_r or touch_l
|
| 937 |
+
|
| 938 |
+
# Floor bracing support (head_z < 1.15m): reward hands pushing floor & knees tucked
|
| 939 |
+
reward_hand_brace = 4.0 if (contact_hand and head_z < 1.15) else 0.0
|
| 940 |
+
reward_knee_brace = 3.0 if (contact_knee and head_z < 1.15) else 0.0
|
| 941 |
+
|
| 942 |
+
# Standing stage (head_z >= 1.05m): reward planting feet
|
| 943 |
+
if both_feet_down:
|
| 944 |
+
reward_feet_ground = 6.0
|
| 945 |
+
elif one_foot_down:
|
| 946 |
+
reward_feet_ground = 2.0
|
| 947 |
+
elif head_z >= 1.20:
|
| 948 |
+
reward_feet_ground = -1.0 # Only penalize missing foot contact once upright
|
| 949 |
+
else:
|
| 950 |
+
reward_feet_ground = 0.0 # Zero penalty while floor-bound rolling or pushing up!
|
| 951 |
+
|
| 952 |
+
# Floor proximity for feet during ground phase
|
| 953 |
+
reward_feet_down = 2.0 * max(0.0, 0.30 - max_foot_z) if head_z < 1.15 else 0.0
|
| 954 |
+
|
| 955 |
+
# 3. MITIGATE LEG AIR-FLAILING:
|
| 956 |
+
# Only penalize feet higher than head IF head is actually on the ground and feet are violently kicked up
|
| 957 |
+
feet_above_head = max(0.0, max_foot_z - max(0.40, head_z))
|
| 958 |
+
penalty_shooting_legs = -4.0 * feet_above_head
|
| 959 |
+
|
| 960 |
+
# Air-flail penalty only when feet are high in air with zero contact
|
| 961 |
+
penalty_air_flail = -2.0 * max(0.0, max_foot_z - 0.50) if (head_z < 1.0 and not (contact_hand or contact_knee)) else 0.0
|
| 962 |
+
|
| 963 |
+
# 4. Energy cost & Arm Windmilling Suppression
|
| 964 |
+
arm_flail_penalty = -0.001 * float(np.sum(np.square(action[15:21])))
|
| 965 |
+
reward_ctrl = -0.001 * torque_cost + arm_flail_penalty
|
| 966 |
+
dist_to_stand_goal = float(np.linalg.norm(self.getup_goal_pos[:2] - np.array([x, y])))
|
| 967 |
+
reward_goal_heading = 2.0 * max(0.0, 1.0 - dist_to_stand_goal / 2.0) if head_z > 1.30 else 0.0
|
| 968 |
+
|
| 969 |
+
total_reward = (
|
| 970 |
+
reward_head_elevation +
|
| 971 |
+
reward_torso_elevation +
|
| 972 |
+
reward_head_height +
|
| 973 |
+
reward_head_above_torso +
|
| 974 |
+
reward_upright +
|
| 975 |
+
reward_hand_brace +
|
| 976 |
+
reward_knee_brace +
|
| 977 |
+
reward_feet_ground +
|
| 978 |
+
reward_feet_down +
|
| 979 |
+
penalty_shooting_legs +
|
| 980 |
+
penalty_air_flail +
|
| 981 |
+
reward_ctrl +
|
| 982 |
+
reward_goal_heading
|
| 983 |
+
)
|
| 984 |
+
self.prev_z = z
|
| 985 |
+
|
| 986 |
+
# Track peak head elevation for stagnation monitoring
|
| 987 |
+
if head_z > self.max_head_elevation:
|
| 988 |
+
self.max_head_elevation = head_z
|
| 989 |
+
self.getup_stagnant_steps = 0
|
| 990 |
+
else:
|
| 991 |
+
self.getup_stagnant_steps += 1
|
| 992 |
+
|
| 993 |
+
# Upright standing with feet planted verification:
|
| 994 |
+
# Requires head up (> 1.35m), torso upright (z > 1.05m, up_z > 0.70), and foot contact
|
| 995 |
+
is_upright = (head_z > 1.35) and (z > 1.05) and (up_z > 0.70) and (one_foot_down or (head_z > 1.45 and z > 1.10))
|
| 996 |
+
if is_upright:
|
| 997 |
+
self.consecutive_upright_steps += 1
|
| 998 |
+
total_reward += 3.0
|
| 999 |
+
if both_feet_down:
|
| 1000 |
+
total_reward += 3.0 # Extra jackpot bonus when BOTH feet are planted!
|
| 1001 |
+
else:
|
| 1002 |
+
self.consecutive_upright_steps = 0
|
| 1003 |
+
|
| 1004 |
+
# Lenient Ground Collapse Stagnation & Rise Stall Deaths:
|
| 1005 |
+
# Tolerates 140 steps (~3.5s) struggling on ground without head elevation
|
| 1006 |
+
# Tolerates 180 steps (~4.5s) coordinating knee-tuck and feet planting to reach upright stance
|
| 1007 |
+
is_ground_stuck = (head_z < 0.50 and self.getup_stagnant_steps > 140)
|
| 1008 |
+
is_stall_stuck = (head_z >= 0.50 and self.consecutive_upright_steps == 0 and self.getup_stagnant_steps > 180)
|
| 1009 |
+
|
| 1010 |
+
terminated = False
|
| 1011 |
+
if self.consecutive_upright_steps >= 12:
|
| 1012 |
+
total_reward += 100.0 # Upright Recovery Jackpot!
|
| 1013 |
+
self.getup_success_count += 1
|
| 1014 |
+
terminated = True
|
| 1015 |
+
elif is_ground_stuck or is_stall_stuck:
|
| 1016 |
+
terminated = True
|
| 1017 |
+
total_reward -= 10.0 # Timeout stagnation death
|
| 1018 |
+
|
| 1019 |
+
truncated = (self.current_step >= 280)
|
| 1020 |
+
target_pos = self.getup_goal_pos
|
| 1021 |
+
|
| 1022 |
+
info = {
|
| 1023 |
+
"x_pos": float(x), "y_pos": float(y), "z_pos": float(z),
|
| 1024 |
+
"vx": float(vx), "torso_speed": float(torso_speed),
|
| 1025 |
+
"furthest_x": float(self.furthest_x), "milestones": 0,
|
| 1026 |
+
"waypoint_idx": 0, "waypoint_name": f"Get-Up ({self.fall_state_type})",
|
| 1027 |
+
"dist_to_wp": float(max(0.0, 1.55 - head_z)),
|
| 1028 |
+
"waypoints_completed": int(self.consecutive_upright_steps >= 12),
|
| 1029 |
+
"goals_completed": int(self.consecutive_upright_steps >= 12),
|
| 1030 |
+
"dist_to_goal": float(dist_to_stand_goal),
|
| 1031 |
+
"goal_arrival_radius": 0.50,
|
| 1032 |
+
"goal_pos": self.getup_goal_pos.tolist(),
|
| 1033 |
+
"target_local_pos": (torso_xmat.T @ (target_pos - torso_xpos)).tolist(),
|
| 1034 |
+
"gaze_alignment": float(max(0.0, up_z)),
|
| 1035 |
+
"stamina": float(self.stamina), "health": float(self.health), "hunger": float(self.hunger),
|
| 1036 |
+
"step": self.current_step, "lidar_grid": self.current_lidar_grid.tolist(),
|
| 1037 |
+
"in_leeway": False, "leeway_remaining": 0,
|
| 1038 |
+
"stream_mode": "getup", "active_reach_hand": "none", "reach_touches": 0,
|
| 1039 |
+
"getup_success": bool(self.consecutive_upright_steps >= 12),
|
| 1040 |
+
"head_z": float(head_z),
|
| 1041 |
+
"both_feet_down": bool(both_feet_down),
|
| 1042 |
+
}
|
| 1043 |
+
return self._get_obs(), float(total_reward), terminated, truncated, info
|
| 1044 |
+
|
| 1045 |
+
# ── STREAM 2: BILATERAL HAND REACHING (BOLTED BODY) ────────────────
|
| 1046 |
+
elif self.stream_mode == "reach":
|
| 1047 |
+
# Torso, core, and legs are bolted in neutral athletic upright stance.
|
| 1048 |
+
# Control is isolated strictly to the 6 arm actuators reaching for 3D orbs.
|
| 1049 |
+
r_h = self.data.geom_xpos[self.r_hand_geom_id]
|
| 1050 |
+
l_h = self.data.geom_xpos[self.l_hand_geom_id]
|
| 1051 |
+
dist_r = float(np.linalg.norm(r_h - self.reach_target_pos))
|
| 1052 |
+
dist_l = float(np.linalg.norm(l_h - self.reach_target_pos))
|
| 1053 |
+
|
| 1054 |
+
# The designated active hand is the hand closer to the target ball
|
| 1055 |
+
is_right_target = (self.active_reach_hand == "right")
|
| 1056 |
+
|
| 1057 |
+
if is_right_target:
|
| 1058 |
+
active_h = r_h
|
| 1059 |
+
wrong_h = l_h
|
| 1060 |
+
dist_to_target = dist_r
|
| 1061 |
+
dist_inactive = dist_l
|
| 1062 |
+
active_body_id = self.r_arm_body_id
|
| 1063 |
+
wrong_body_id = self.l_arm_body_id
|
| 1064 |
+
active_act = action[15:18]
|
| 1065 |
+
wrong_act = action[18:21]
|
| 1066 |
+
active_qvel = self.data.qvel[self.arm_dof_indices[0:3]]
|
| 1067 |
+
wrong_qvel = self.data.qvel[self.arm_dof_indices[3:6]]
|
| 1068 |
+
wrong_qpos = self.data.qpos[self.arm_qpos_indices[3:6]]
|
| 1069 |
+
wrong_rest_qpos = np.array([-0.65, 0.0, 0.0], dtype=np.float32)
|
| 1070 |
+
else:
|
| 1071 |
+
active_h = l_h
|
| 1072 |
+
wrong_h = r_h
|
| 1073 |
+
dist_to_target = dist_l
|
| 1074 |
+
dist_inactive = dist_r
|
| 1075 |
+
active_body_id = self.l_arm_body_id
|
| 1076 |
+
wrong_body_id = self.r_arm_body_id
|
| 1077 |
+
active_act = action[18:21]
|
| 1078 |
+
wrong_act = action[15:18]
|
| 1079 |
+
active_qvel = self.data.qvel[self.arm_dof_indices[3:6]]
|
| 1080 |
+
wrong_qvel = self.data.qvel[self.arm_dof_indices[0:3]]
|
| 1081 |
+
wrong_qpos = self.data.qpos[self.arm_qpos_indices[0:3]]
|
| 1082 |
+
wrong_rest_qpos = np.array([0.65, 0.0, 0.0], dtype=np.float32)
|
| 1083 |
+
|
| 1084 |
+
# 1. Main reach distance progress and proximity reward
|
| 1085 |
+
dist_prog = self.prev_reach_dist - dist_to_target
|
| 1086 |
+
reward_reach_prog = 25.0 * dist_prog
|
| 1087 |
+
reward_proximity = 6.0 * np.exp(-4.0 * dist_to_target)
|
| 1088 |
+
|
| 1089 |
+
# 2. MASSIVE SPEED EFFICIENCY REWARDS:
|
| 1090 |
+
# a) Direct closing velocity vector along the unit ray connecting active hand to target orb
|
| 1091 |
+
delta_active_target = self.reach_target_pos - active_h
|
| 1092 |
+
dist_hand_target = max(1e-4, float(np.linalg.norm(delta_active_target)))
|
| 1093 |
+
u_reach_target = delta_active_target / dist_hand_target
|
| 1094 |
+
active_hand_vel = self.data.cvel[active_body_id][3:6]
|
| 1095 |
+
v_closing = float(np.dot(active_hand_vel, u_reach_target))
|
| 1096 |
+
# Speed efficiency bonus for thrusting active hand toward target
|
| 1097 |
+
reward_closing_speed = 4.0 * max(0.0, v_closing)
|
| 1098 |
+
|
| 1099 |
+
# b) Mild latency penalty (encourages prompt reaching)
|
| 1100 |
+
penalty_latency = -0.02 * (float(self.steps_on_reach_target) / 40.0)
|
| 1101 |
+
|
| 1102 |
+
# 3. Slight reward for moving the correct hand
|
| 1103 |
+
active_hand_speed = float(np.linalg.norm(active_hand_vel))
|
| 1104 |
+
active_qvel_mag = float(np.sum(np.abs(active_qvel)))
|
| 1105 |
+
reward_correct_motion = 0.15 * min(1.0, active_hand_speed / 0.25) + 0.10 * np.tanh(active_qvel_mag)
|
| 1106 |
+
|
| 1107 |
+
# 4. REGULARIZATION ON INACTIVE HAND (stay neutral and relaxed at side)
|
| 1108 |
+
wrong_hand_speed = float(np.linalg.norm(self.data.cvel[wrong_body_id][3:6]))
|
| 1109 |
+
penalty_wrong_act = -0.05 * float(np.sum(np.square(wrong_act)))
|
| 1110 |
+
penalty_wrong_qvel = -0.05 * float(np.sum(np.abs(wrong_qvel)))
|
| 1111 |
+
penalty_wrong_speed = -0.20 * min(1.0, wrong_hand_speed)
|
| 1112 |
+
penalty_wrong_raised = -0.50 * max(0.0, float(wrong_h[2]) - 1.28)
|
| 1113 |
+
penalty_wrong_posture = -0.20 * min(1.0, float(np.linalg.norm(wrong_qpos - wrong_rest_qpos)))
|
| 1114 |
+
penalty_wrong_lead = -0.50 * max(0.0, dist_to_target - dist_inactive)
|
| 1115 |
+
penalty_wrong_encroach = -0.50 * max(0.0, 0.35 - dist_inactive)
|
| 1116 |
+
penalty_wrong_touch = -2.0 if dist_inactive < 0.14 else 0.0
|
| 1117 |
+
|
| 1118 |
+
# General control penalty on active arm to encourage efficient articulation
|
| 1119 |
+
reward_ctrl = -0.001 * float(np.sum(np.square(active_act)))
|
| 1120 |
+
|
| 1121 |
+
self.steps_on_reach_target += 1
|
| 1122 |
+
if dist_to_target < self.min_reach_dist:
|
| 1123 |
+
self.min_reach_dist = dist_to_target
|
| 1124 |
+
self.steps_receding_reach = 0
|
| 1125 |
+
elif dist_to_target > self.min_reach_dist + 0.25:
|
| 1126 |
+
self.steps_receding_reach += 1
|
| 1127 |
+
|
| 1128 |
+
reward_touch = 0.0
|
| 1129 |
+
speed_efficiency_jackpot = 0.0
|
| 1130 |
+
if dist_to_target < 0.14:
|
| 1131 |
+
# 1. Base touch award
|
| 1132 |
+
base_touch = 40.0
|
| 1133 |
+
# 2. Speed efficiency jackpot: maximum when acquired in few steps
|
| 1134 |
+
speed_efficiency_jackpot = max(0.0, 110.0 - float(self.steps_on_reach_target)) * 0.40
|
| 1135 |
+
# 3. Consecutive hit streak multiplier
|
| 1136 |
+
touch_streak_bonus = min(30.0, float(self.reach_touches) * 6.0)
|
| 1137 |
+
reward_touch = base_touch + speed_efficiency_jackpot + touch_streak_bonus
|
| 1138 |
+
|
| 1139 |
+
self.reach_touches += 1
|
| 1140 |
+
self._sample_reach_target()
|
| 1141 |
+
is_right_target = (self.active_reach_hand == "right")
|
| 1142 |
+
active_h = self.data.geom_xpos[self.r_hand_geom_id] if is_right_target else self.data.geom_xpos[self.l_hand_geom_id]
|
| 1143 |
+
dist_to_target = float(np.linalg.norm(active_h - self.reach_target_pos))
|
| 1144 |
+
# Reset timers for next target!
|
| 1145 |
+
self.steps_on_reach_target = 0
|
| 1146 |
+
self.steps_receding_reach = 0
|
| 1147 |
+
self.min_reach_dist = dist_to_target
|
| 1148 |
+
|
| 1149 |
+
self.prev_reach_dist = dist_to_target
|
| 1150 |
+
total_reward = (
|
| 1151 |
+
reward_reach_prog +
|
| 1152 |
+
reward_proximity +
|
| 1153 |
+
reward_closing_speed +
|
| 1154 |
+
reward_correct_motion +
|
| 1155 |
+
reward_touch +
|
| 1156 |
+
penalty_latency +
|
| 1157 |
+
penalty_wrong_act +
|
| 1158 |
+
penalty_wrong_qvel +
|
| 1159 |
+
penalty_wrong_speed +
|
| 1160 |
+
penalty_wrong_raised +
|
| 1161 |
+
penalty_wrong_posture +
|
| 1162 |
+
penalty_wrong_lead +
|
| 1163 |
+
penalty_wrong_encroach +
|
| 1164 |
+
penalty_wrong_touch +
|
| 1165 |
+
reward_ctrl
|
| 1166 |
+
)
|
| 1167 |
+
|
| 1168 |
+
# Lenient Reaching Time Limits:
|
| 1169 |
+
# Tolerates 110 steps (~2.75s) per target orb, or 45 steps receding away
|
| 1170 |
+
is_reach_timeout = (self.steps_on_reach_target > 110)
|
| 1171 |
+
is_reach_receding = (self.steps_receding_reach > 45)
|
| 1172 |
+
|
| 1173 |
+
terminated = False
|
| 1174 |
+
if is_reach_timeout or is_reach_receding:
|
| 1175 |
+
terminated = True
|
| 1176 |
+
total_reward -= 2.0 # Mild timeout penalty
|
| 1177 |
+
|
| 1178 |
+
truncated = (self.current_step >= 384)
|
| 1179 |
+
target_pos = self.reach_target_pos
|
| 1180 |
+
|
| 1181 |
+
head_xmat = self.data.xmat[self.head_body_id].reshape(3, 3)
|
| 1182 |
+
head_fwd = head_xmat[:, 0]
|
| 1183 |
+
delta_head_target = target_pos - self.data.xpos[self.head_body_id]
|
| 1184 |
+
cos_gaze = float(np.dot(head_fwd, delta_head_target / max(1e-3, np.linalg.norm(delta_head_target))))
|
| 1185 |
+
|
| 1186 |
+
info = {
|
| 1187 |
+
"x_pos": float(x), "y_pos": float(y), "z_pos": float(z),
|
| 1188 |
+
"vx": float(vx), "torso_speed": float(torso_speed),
|
| 1189 |
+
"furthest_x": float(self.furthest_x), "milestones": 0,
|
| 1190 |
+
"waypoint_idx": 0, "waypoint_name": f"Reach ({self.active_reach_hand.upper()})",
|
| 1191 |
+
"dist_to_wp": float(dist_to_target), "waypoints_completed": int(self.reach_touches),
|
| 1192 |
+
"goals_completed": int(self.reach_touches),
|
| 1193 |
+
"dist_to_goal": float(dist_to_target),
|
| 1194 |
+
"goal_arrival_radius": 0.14,
|
| 1195 |
+
"goal_pos": self.reach_target_pos.tolist(),
|
| 1196 |
+
"target_local_pos": (torso_xmat.T @ (target_pos - torso_xpos)).tolist(),
|
| 1197 |
+
"gaze_alignment": float(cos_gaze),
|
| 1198 |
+
"stamina": float(self.stamina), "health": float(self.health), "hunger": float(self.hunger),
|
| 1199 |
+
"step": self.current_step, "lidar_grid": self.current_lidar_grid.tolist(),
|
| 1200 |
+
"in_leeway": False, "leeway_remaining": 0,
|
| 1201 |
+
"stream_mode": "reach", "active_reach_hand": str(self.active_reach_hand),
|
| 1202 |
+
"reach_touches": int(self.reach_touches), "getup_success": False, "stand_still_score": 0.0,
|
| 1203 |
+
"wrong_hand_dist": float(dist_inactive), "wrong_hand_touch": bool(dist_inactive < 0.14),
|
| 1204 |
+
"wrong_hand_speed": float(wrong_hand_speed),
|
| 1205 |
+
"wrong_arm_action_penalty": float(penalty_wrong_act),
|
| 1206 |
+
"wrong_arm_raised_penalty": float(penalty_wrong_raised),
|
| 1207 |
+
"v_closing": float(v_closing),
|
| 1208 |
+
"reward_closing_speed": float(reward_closing_speed),
|
| 1209 |
+
"speed_efficiency_jackpot": float(speed_efficiency_jackpot),
|
| 1210 |
+
}
|
| 1211 |
+
return self._get_obs(), float(total_reward), terminated, truncated, info
|
| 1212 |
+
# Target Navigation Vector & Forward Propulsion
|
| 1213 |
+
if self.arena_mode:
|
| 1214 |
+
target_pos = self.goal_pos
|
| 1215 |
+
delta_target = target_pos - torso_xpos
|
| 1216 |
+
dist_to_target = float(np.linalg.norm(delta_target))
|
| 1217 |
+
dist_xy = float(np.linalg.norm(delta_target[:2]))
|
| 1218 |
+
|
| 1219 |
+
# Check dynamic goal arrival (entering wide arrival radius)
|
| 1220 |
+
reward_arrival = 0.0
|
| 1221 |
+
if dist_xy < self.goal_arrival_radius:
|
| 1222 |
+
self.goals_completed += 1
|
| 1223 |
+
# Escalating jackpot for chained consecutive goals: 50 for 1st, 85 for 2nd, 120 for 3rd, 155 for 4th...
|
| 1224 |
+
reward_arrival = 50.0 + 35.0 * (self.goals_completed - 1)
|
| 1225 |
+
self._sample_new_goal(is_initial=False)
|
| 1226 |
+
target_pos = self.goal_pos
|
| 1227 |
+
delta_target = target_pos - torso_xpos
|
| 1228 |
+
dist_to_target = float(np.linalg.norm(delta_target))
|
| 1229 |
+
dist_xy = float(np.linalg.norm(delta_target[:2]))
|
| 1230 |
+
self.prev_dist_to_goal = dist_to_target
|
| 1231 |
+
dist_delta = 0.0
|
| 1232 |
+
else:
|
| 1233 |
+
dist_delta = self.prev_dist_to_goal - dist_to_target
|
| 1234 |
+
self.prev_dist_to_goal = dist_to_target
|
| 1235 |
+
|
| 1236 |
+
# Leeway period active after goal completion to allow decelerating & turning
|
| 1237 |
+
in_leeway = (self.goal_transition_steps > 0)
|
| 1238 |
+
if in_leeway:
|
| 1239 |
+
self.goal_transition_steps -= 1
|
| 1240 |
+
# While turning during leeway, continuously update closest distance baseline to current position
|
| 1241 |
+
self.min_dist_to_goal = dist_to_target
|
| 1242 |
+
|
| 1243 |
+
# 1. Distance progress potential (scaled with goal streak) & sharp penalty for moving away
|
| 1244 |
+
streak_mult = 1.0 + 0.35 * min(4, self.goals_completed)
|
| 1245 |
+
if dist_delta >= 0.0:
|
| 1246 |
+
reward_dist_progress = 3.0 * streak_mult * dist_delta
|
| 1247 |
+
else:
|
| 1248 |
+
# Double penalty for moving further away from target outside of turn leeway
|
| 1249 |
+
reward_dist_progress = 0.0 if in_leeway else (6.0 * dist_delta)
|
| 1250 |
+
|
| 1251 |
+
# 2. Penalty for drifting/retreating beyond the closest distance achieved so far (muted in leeway)
|
| 1252 |
+
retreat_distance = max(0.0, dist_to_target - self.min_dist_to_goal)
|
| 1253 |
+
penalty_retreat = 0.0 if in_leeway else (-3.0 * retreat_distance if retreat_distance > 0.10 else 0.0)
|
| 1254 |
+
|
| 1255 |
+
unit_target_dir = delta_target / max(0.1, dist_to_target)
|
| 1256 |
+
vel_towards_target = float(np.dot(torso_cvel[3:], unit_target_dir))
|
| 1257 |
+
torso_speed = float(np.linalg.norm(torso_cvel[3:]))
|
| 1258 |
+
|
| 1259 |
+
# 3. Controlled Forward Movement vs Severe Retrograde Penalty
|
| 1260 |
+
# Cap forward reward at 1.8 m/s so Olympic sprinting gives no extra reward
|
| 1261 |
+
v_eff = min(max(0.0, vel_towards_target), 1.8)
|
| 1262 |
+
reward_forward = 3.0 * v_eff * upright_scale
|
| 1263 |
+
penalty_retrograde = 0.0 if (vel_towards_target >= 0.0 or in_leeway) else (5.0 * vel_towards_target)
|
| 1264 |
+
|
| 1265 |
+
# 4. Heading guidance velocity (steep negative slope when heading backwards outside leeway)
|
| 1266 |
+
if vel_towards_target >= 0.0:
|
| 1267 |
+
reward_heading = 1.5 * v_eff * upright_scale
|
| 1268 |
+
else:
|
| 1269 |
+
reward_heading = 0.0 if in_leeway else (3.0 * vel_towards_target)
|
| 1270 |
+
|
| 1271 |
+
# 5. Speed Regulation Penalty: Penalize reckless overspeeding (> 2.0 m/s) to promote stable locomotion
|
| 1272 |
+
penalty_overspeed = 0.0
|
| 1273 |
+
if torso_speed > 2.0:
|
| 1274 |
+
penalty_overspeed = -3.0 * ((torso_speed - 2.0) ** 2)
|
| 1275 |
+
|
| 1276 |
+
# 6. Turn alignment & braking reward during transition leeway: encourage slowing down & rapid yaw alignment to new goal
|
| 1277 |
+
reward_turn = 0.0
|
| 1278 |
+
reward_brake = 0.0
|
| 1279 |
+
if in_leeway:
|
| 1280 |
+
torso_fwd = torso_xmat[:, 0]
|
| 1281 |
+
cos_torso_goal = float(np.dot(torso_fwd[:2], unit_target_dir[:2]))
|
| 1282 |
+
# Reward rotating body to face new target (+0.0 to +1.0)
|
| 1283 |
+
reward_turn = 1.0 * max(0.0, cos_torso_goal)
|
| 1284 |
+
# Reward decelerating below 1.6 m/s so the agent can pivot cleanly
|
| 1285 |
+
if torso_speed < 1.6:
|
| 1286 |
+
reward_brake = 0.6 * (1.6 - torso_speed)
|
| 1287 |
+
|
| 1288 |
+
# 7. Subsequent Goal Urgency: Time-decay pressure once Goal 1 is cleared
|
| 1289 |
+
penalty_urgency = 0.0
|
| 1290 |
+
if self.goals_completed >= 1 and not in_leeway:
|
| 1291 |
+
penalty_urgency = -0.08 * min(3, self.goals_completed)
|
| 1292 |
+
|
| 1293 |
+
reward_wp = (
|
| 1294 |
+
reward_arrival +
|
| 1295 |
+
reward_dist_progress +
|
| 1296 |
+
penalty_retreat +
|
| 1297 |
+
penalty_retrograde +
|
| 1298 |
+
penalty_overspeed +
|
| 1299 |
+
reward_turn +
|
| 1300 |
+
reward_brake +
|
| 1301 |
+
penalty_urgency
|
| 1302 |
+
)
|
| 1303 |
+
reward_drift = 0.0
|
| 1304 |
+
reward_milestone = 0.0
|
| 1305 |
+
dist_to_wp = dist_to_target
|
| 1306 |
+
else:
|
| 1307 |
+
# Obstacle course mode
|
| 1308 |
+
wp_pos, wp_name, wp_radius = COURSE_WAYPOINTS[self.current_wp_idx]
|
| 1309 |
+
target_pos = wp_pos
|
| 1310 |
+
delta_to_wp = wp_pos - torso_xpos
|
| 1311 |
+
dist_to_wp = float(np.linalg.norm(delta_to_wp))
|
| 1312 |
+
dist_xy = float(np.linalg.norm(delta_to_wp[:2]))
|
| 1313 |
+
|
| 1314 |
+
reward_forward = 3.5 * vx * upright_scale
|
| 1315 |
+
if vx < -0.05:
|
| 1316 |
+
reward_forward += 1.5 * vx
|
| 1317 |
+
|
| 1318 |
+
wp_reached = False
|
| 1319 |
+
if dist_xy < wp_radius:
|
| 1320 |
+
wp_reached = True
|
| 1321 |
+
elif torso_xpos[0] >= wp_pos[0] and abs(torso_xpos[1] - wp_pos[1]) < (wp_radius + 0.3):
|
| 1322 |
+
wp_reached = True
|
| 1323 |
+
|
| 1324 |
+
reward_wp = 0.0
|
| 1325 |
+
if wp_reached and self.current_wp_idx not in self.waypoints_reached:
|
| 1326 |
+
self.waypoints_reached.add(self.current_wp_idx)
|
| 1327 |
+
if self.current_wp_idx == len(COURSE_WAYPOINTS) - 1:
|
| 1328 |
+
reward_wp = 100.0
|
| 1329 |
+
elif 3 <= self.current_wp_idx <= 14:
|
| 1330 |
+
reward_wp = 25.0
|
| 1331 |
+
else:
|
| 1332 |
+
reward_wp = 15.0
|
| 1333 |
+
if self.current_wp_idx < len(COURSE_WAYPOINTS) - 1:
|
| 1334 |
+
self.current_wp_idx += 1
|
| 1335 |
+
wp_pos, wp_name, wp_radius = COURSE_WAYPOINTS[self.current_wp_idx]
|
| 1336 |
+
target_pos = wp_pos
|
| 1337 |
+
delta_to_wp = wp_pos - torso_xpos
|
| 1338 |
+
dist_to_wp = float(np.linalg.norm(delta_to_wp))
|
| 1339 |
+
|
| 1340 |
+
wp_unit_dir = delta_to_wp / max(0.1, dist_to_wp)
|
| 1341 |
+
vel_towards_wp = float(np.dot(torso_cvel[3:], wp_unit_dir))
|
| 1342 |
+
reward_heading = 2.0 * max(-0.5, min(vel_towards_wp, 3.5)) * upright_scale
|
| 1343 |
+
|
| 1344 |
+
target_y = float(wp_pos[1])
|
| 1345 |
+
reward_drift = -0.5 * abs(y - target_y)
|
| 1346 |
+
|
| 1347 |
+
reward_milestone = 0.0
|
| 1348 |
+
for threshold, name in STAGE_MILESTONES:
|
| 1349 |
+
if x >= threshold and threshold not in self.milestones_reached:
|
| 1350 |
+
self.milestones_reached.add(threshold)
|
| 1351 |
+
reward_milestone += 50.0
|
| 1352 |
+
|
| 1353 |
+
# Head Gaze Alignment Reward: mild reward for facing objective with head/visor
|
| 1354 |
+
head_xmat = self.data.xmat[self.head_body_id].reshape(3, 3)
|
| 1355 |
+
head_fwd = head_xmat[:, 0] # Local X axis of head (forward gaze direction)
|
| 1356 |
+
head_pos = self.data.xpos[self.head_body_id]
|
| 1357 |
+
delta_head_target = target_pos - head_pos
|
| 1358 |
+
dist_head_target = float(np.linalg.norm(delta_head_target))
|
| 1359 |
+
u_head_target = delta_head_target / max(1e-3, dist_head_target)
|
| 1360 |
+
cos_gaze = float(np.dot(head_fwd, u_head_target))
|
| 1361 |
+
reward_gaze = 0.15 * max(0.0, cos_gaze)
|
| 1362 |
+
|
| 1363 |
+
# 2. Upright posture reward (staying balanced above 0.70m)
|
| 1364 |
+
reward_upright = 1.0 if z > 0.70 else 0.0
|
| 1365 |
+
|
| 1366 |
+
# 3. Upright orientation bonus & active lateral roll stabilization
|
| 1367 |
+
roll_y = torso_xmat[1, 2]
|
| 1368 |
+
reward_roll = -2.0 * abs(roll_y)
|
| 1369 |
+
reward_orient = 0.8 * max(0.0, up_z) + reward_roll
|
| 1370 |
+
|
| 1371 |
+
# Advance speed for biomechanical stepping gating
|
| 1372 |
+
forward_vec = torso_xmat[:, 0]
|
| 1373 |
+
v_advance = float(np.dot(torso_cvel[3:6], forward_vec)) if self.arena_mode else vx
|
| 1374 |
+
|
| 1375 |
+
# 4. Alternating Bipedal Stepping Cadence
|
| 1376 |
+
reward_gait = 0.0
|
| 1377 |
+
if z > 0.55 and v_advance > 0.10:
|
| 1378 |
+
if touch_r ^ touch_l:
|
| 1379 |
+
reward_gait = 2.0 * min(v_advance, 1.5)
|
| 1380 |
+
elif touch_r and touch_l:
|
| 1381 |
+
reward_gait = -0.4 * min(v_advance, 1.5)
|
| 1382 |
+
|
| 1383 |
+
# 5. Knee Articulation & Stance Stability
|
| 1384 |
+
r_knee_q = self.data.qpos[self.r_knee_qpos]
|
| 1385 |
+
l_knee_q = self.data.qpos[self.l_knee_qpos]
|
| 1386 |
+
reward_knee = 0.0
|
| 1387 |
+
if z > 0.55 and v_advance > 0.10:
|
| 1388 |
+
if not touch_r:
|
| 1389 |
+
if 0.25 <= r_knee_q <= 1.1:
|
| 1390 |
+
reward_knee += 0.3
|
| 1391 |
+
elif r_knee_q > 1.6:
|
| 1392 |
+
reward_knee -= 0.5 * (r_knee_q - 1.6)
|
| 1393 |
+
if not touch_l:
|
| 1394 |
+
if 0.25 <= l_knee_q <= 1.1:
|
| 1395 |
+
reward_knee += 0.3
|
| 1396 |
+
elif l_knee_q > 1.6:
|
| 1397 |
+
reward_knee -= 0.5 * (l_knee_q - 1.6)
|
| 1398 |
+
if touch_r and r_knee_q < 0.50:
|
| 1399 |
+
reward_knee += 0.2
|
| 1400 |
+
if touch_l and l_knee_q < 0.50:
|
| 1401 |
+
reward_knee += 0.2
|
| 1402 |
+
|
| 1403 |
+
# 6. Swing foot forward velocity bonus
|
| 1404 |
+
r_foot_vfwd = float(np.dot(self.data.cvel[self.r_foot_id][3:6], forward_vec)) if self.arena_mode else self.data.cvel[self.r_foot_id][3]
|
| 1405 |
+
l_foot_vfwd = float(np.dot(self.data.cvel[self.l_foot_id][3:6], forward_vec)) if self.arena_mode else self.data.cvel[self.l_foot_id][3]
|
| 1406 |
+
reward_swing = 0.0
|
| 1407 |
+
if z > 0.55 and v_advance > 0.10:
|
| 1408 |
+
if not touch_r and r_foot_vfwd > 0.15:
|
| 1409 |
+
reward_swing += 0.3 * min(r_foot_vfwd, 2.0)
|
| 1410 |
+
if not touch_l and l_foot_vfwd > 0.15:
|
| 1411 |
+
reward_swing += 0.3 * min(l_foot_vfwd, 2.0)
|
| 1412 |
+
|
| 1413 |
+
# 7. Thigh Forward Stride Separation
|
| 1414 |
+
r_hip_q = self.data.qpos[self.r_hip_qpos]
|
| 1415 |
+
l_hip_q = self.data.qpos[self.l_hip_qpos]
|
| 1416 |
+
reward_stride = 0.0
|
| 1417 |
+
if z > 0.55 and v_advance > 0.10:
|
| 1418 |
+
if not touch_r:
|
| 1419 |
+
stride_diff = l_hip_q - r_hip_q
|
| 1420 |
+
if 0.1 < stride_diff < 1.3:
|
| 1421 |
+
reward_stride = 1.0 * stride_diff
|
| 1422 |
+
elif not touch_l:
|
| 1423 |
+
stride_diff = r_hip_q - l_hip_q
|
| 1424 |
+
if 0.1 < stride_diff < 1.3:
|
| 1425 |
+
reward_stride = 1.0 * stride_diff
|
| 1426 |
+
|
| 1427 |
+
# 8. Bilateral Stance Symmetry (Duty-Cycle Equalizer)
|
| 1428 |
+
reward_symmetry = 0.0
|
| 1429 |
+
total_contacts = self.r_foot_contact_steps + self.l_foot_contact_steps
|
| 1430 |
+
if self.current_step > 10 and total_contacts > 6 and z > 0.55:
|
| 1431 |
+
asym = abs(self.r_foot_contact_steps - self.l_foot_contact_steps) / total_contacts
|
| 1432 |
+
if asym > 0.20:
|
| 1433 |
+
reward_symmetry = -2.0 * (asym - 0.20)
|
| 1434 |
+
|
| 1435 |
+
# 9. Arm Windmilling Suppression (Penalize flailing arms to force balance from core and legs)
|
| 1436 |
+
arm_flail_penalty = -0.008 * float(np.sum(np.square(action[15:21])))
|
| 1437 |
+
|
| 1438 |
+
# 10. Energy expenditure penalty
|
| 1439 |
+
reward_ctrl = -0.0015 * torque_cost + arm_flail_penalty
|
| 1440 |
+
|
| 1441 |
+
total_reward = (
|
| 1442 |
+
reward_forward +
|
| 1443 |
+
reward_heading +
|
| 1444 |
+
reward_wp +
|
| 1445 |
+
reward_gaze +
|
| 1446 |
+
reward_upright +
|
| 1447 |
+
reward_orient +
|
| 1448 |
+
reward_gait +
|
| 1449 |
+
reward_knee +
|
| 1450 |
+
reward_swing +
|
| 1451 |
+
reward_stride +
|
| 1452 |
+
reward_symmetry +
|
| 1453 |
+
reward_ctrl +
|
| 1454 |
+
reward_drift +
|
| 1455 |
+
reward_milestone
|
| 1456 |
+
)
|
| 1457 |
+
|
| 1458 |
+
if self.arena_mode:
|
| 1459 |
+
rad_dist = float(np.linalg.norm([x, y]))
|
| 1460 |
+
if rad_dist > self.furthest_x:
|
| 1461 |
+
self.furthest_x = rad_dist
|
| 1462 |
+
|
| 1463 |
+
if in_leeway:
|
| 1464 |
+
# During turning leeway window: reset & do not accumulate moving-away or stagnation counters
|
| 1465 |
+
self.steps_not_approaching = 0
|
| 1466 |
+
self.negative_vx_steps = 0
|
| 1467 |
+
self.steps_since_progress = 0
|
| 1468 |
+
self.min_dist_to_goal = dist_to_target
|
| 1469 |
+
else:
|
| 1470 |
+
# Active approach check: requires advancing toward target (> 0.15 m/s and closing distance)
|
| 1471 |
+
is_actively_approaching = (vel_towards_target >= 0.15) and (dist_delta > 0.001)
|
| 1472 |
+
if is_actively_approaching:
|
| 1473 |
+
self.steps_not_approaching = 0
|
| 1474 |
+
else:
|
| 1475 |
+
self.steps_not_approaching += 1
|
| 1476 |
+
|
| 1477 |
+
# Receding tracker: moving away from target
|
| 1478 |
+
is_receding = (vel_towards_target < -0.05) or (dist_delta < -0.003)
|
| 1479 |
+
if is_receding:
|
| 1480 |
+
self.negative_vx_steps += 1
|
| 1481 |
+
else:
|
| 1482 |
+
self.negative_vx_steps = max(0, self.negative_vx_steps - 1)
|
| 1483 |
+
|
| 1484 |
+
# Record best approach distance
|
| 1485 |
+
if dist_to_target < self.min_dist_to_goal:
|
| 1486 |
+
self.min_dist_to_goal = dist_to_target
|
| 1487 |
+
self.steps_since_progress = 0
|
| 1488 |
+
else:
|
| 1489 |
+
self.steps_since_progress += 1
|
| 1490 |
+
else:
|
| 1491 |
+
if x > self.furthest_x:
|
| 1492 |
+
self.furthest_x = x
|
| 1493 |
+
|
| 1494 |
+
if x > self.last_progress_x + 0.06:
|
| 1495 |
+
self.last_progress_x = x
|
| 1496 |
+
self.steps_since_progress = 0
|
| 1497 |
+
else:
|
| 1498 |
+
self.steps_since_progress += 1
|
| 1499 |
+
|
| 1500 |
+
if vx < -0.15:
|
| 1501 |
+
self.negative_vx_steps += 1
|
| 1502 |
+
else:
|
| 1503 |
+
self.negative_vx_steps = 0
|
| 1504 |
+
|
| 1505 |
+
# ── TERMINATION & TRUNCATION ──────────────────────────────────────────
|
| 1506 |
+
# Exact preserved death conditions:
|
| 1507 |
+
# 1. Physical collision: head, visor, chest, or upper torso directly impacts the floor/obstacles
|
| 1508 |
+
# 2. True collapse: torso center of mass drops below 0.35m or head drops below 0.45m
|
| 1509 |
+
# 3. Complete inversion: torso tilted > 81 deg from vertical (up_z < 0.15)
|
| 1510 |
+
# 4. Arena boundary departure (|x| > 24m or |y| > 24m) or course lateral departure (|y| > 3.0m)
|
| 1511 |
+
# 5. Fast elimination when moving away or not actively approaching target (outside leeway)
|
| 1512 |
+
terminated = False
|
| 1513 |
+
|
| 1514 |
+
is_obstacle_or_ground_impact = False
|
| 1515 |
+
for c_idx in range(self.data.ncon):
|
| 1516 |
+
c = self.data.contact[c_idx]
|
| 1517 |
+
g1, g2 = c.geom1, c.geom2
|
| 1518 |
+
if (g1 in self.body_impact_geoms and g2 in self.world_obstacle_geoms) or \
|
| 1519 |
+
(g2 in self.body_impact_geoms and g1 in self.world_obstacle_geoms):
|
| 1520 |
+
is_obstacle_or_ground_impact = True
|
| 1521 |
+
break
|
| 1522 |
+
|
| 1523 |
+
head_z = self.data.xpos[self.head_body_id][2]
|
| 1524 |
+
is_altitude_collapse = (z < 0.35) or (head_z < 0.45)
|
| 1525 |
+
is_tilt_collapse = (up_z < 0.15)
|
| 1526 |
+
is_physical_fall = is_obstacle_or_ground_impact or is_altitude_collapse or is_tilt_collapse
|
| 1527 |
+
|
| 1528 |
+
if self.arena_mode:
|
| 1529 |
+
is_boundary_departure = (abs(x) > 24.0 or abs(y) > 24.0)
|
| 1530 |
+
|
| 1531 |
+
# FAST KILL TRIGGERS (active outside turning leeway period):
|
| 1532 |
+
# 1. Backing away kill: moved away from target for > 25 steps (~0.6s)
|
| 1533 |
+
is_moving_away_kill = (not in_leeway) and (self.negative_vx_steps > 25)
|
| 1534 |
+
|
| 1535 |
+
# 2. Retreat distance kill: backed up > 0.40m further than closest approach achieved
|
| 1536 |
+
is_retreat_kill = (not in_leeway) and (dist_to_target > self.min_dist_to_goal + 0.40) and (self.current_step > 15)
|
| 1537 |
+
|
| 1538 |
+
# 3. Not actively approaching kill: dawdling, circling, or stationary for > 80 steps (~2.0s)
|
| 1539 |
+
is_not_approaching_kill = (not in_leeway) and (self.current_step > 25 and self.steps_not_approaching > 80)
|
| 1540 |
+
|
| 1541 |
+
# 4. Stagnation kill: no new record distance achieved for > 85 steps (~2.125s)
|
| 1542 |
+
is_stagnant_kill = (not in_leeway) and (self.current_step > 25 and self.steps_since_progress > 85)
|
| 1543 |
+
|
| 1544 |
+
if is_physical_fall:
|
| 1545 |
+
terminated = True
|
| 1546 |
+
total_reward -= 20.0
|
| 1547 |
+
self.health = max(0.0, self.health - 0.2)
|
| 1548 |
+
elif is_boundary_departure:
|
| 1549 |
+
terminated = True
|
| 1550 |
+
total_reward -= 20.0
|
| 1551 |
+
elif is_moving_away_kill or is_retreat_kill:
|
| 1552 |
+
terminated = True
|
| 1553 |
+
total_reward -= 25.0 # Severe penalty for retreating/moving away from target
|
| 1554 |
+
elif is_not_approaching_kill or is_stagnant_kill:
|
| 1555 |
+
terminated = True
|
| 1556 |
+
total_reward -= 25.0 # Severe penalty for failing to actively approach target
|
| 1557 |
+
|
| 1558 |
+
# Dropped Goal Streak Penalty:
|
| 1559 |
+
# If the agent achieved at least 1 goal, but died before reaching the next goal,
|
| 1560 |
+
# apply a moderate penalty to encourage multi-goal chaining without negating arrival returns
|
| 1561 |
+
if terminated and self.goals_completed >= 1:
|
| 1562 |
+
dropped_goal_penalty = -15.0 * (1.0 + 0.2 * min(3, self.goals_completed - 1))
|
| 1563 |
+
total_reward += dropped_goal_penalty
|
| 1564 |
+
else:
|
| 1565 |
+
is_moving_backwards = (x < self.spawn_x - 0.50) or (self.negative_vx_steps > 60)
|
| 1566 |
+
is_stagnant = (self.current_step > 50 and self.steps_since_progress > 90)
|
| 1567 |
+
is_lateral_departure = (abs(y) > 3.0)
|
| 1568 |
+
|
| 1569 |
+
if is_physical_fall:
|
| 1570 |
+
terminated = True
|
| 1571 |
+
total_reward -= 20.0
|
| 1572 |
+
self.health = max(0.0, self.health - 0.2)
|
| 1573 |
+
elif is_moving_backwards:
|
| 1574 |
+
terminated = True
|
| 1575 |
+
total_reward -= 15.0
|
| 1576 |
+
elif is_stagnant:
|
| 1577 |
+
terminated = True
|
| 1578 |
+
total_reward -= 10.0
|
| 1579 |
+
elif is_lateral_departure:
|
| 1580 |
+
terminated = True
|
| 1581 |
+
total_reward -= 20.0
|
| 1582 |
+
|
| 1583 |
+
if x >= 47.0:
|
| 1584 |
+
terminated = True
|
| 1585 |
+
total_reward += 200.0
|
| 1586 |
+
|
| 1587 |
+
truncated = (self.current_step >= self.max_steps)
|
| 1588 |
+
|
| 1589 |
+
info = {
|
| 1590 |
+
"x_pos": float(x),
|
| 1591 |
+
"y_pos": float(y),
|
| 1592 |
+
"z_pos": float(z),
|
| 1593 |
+
"vx": float(vx),
|
| 1594 |
+
"torso_speed": float(torso_speed),
|
| 1595 |
+
"furthest_x": float(self.furthest_x),
|
| 1596 |
+
"milestones": len(self.milestones_reached),
|
| 1597 |
+
"waypoint_idx": int(self.current_wp_idx),
|
| 1598 |
+
"waypoint_name": str(COURSE_WAYPOINTS[self.current_wp_idx][1]) if not self.arena_mode else f"Arena Target #{self.goals_completed + 1}",
|
| 1599 |
+
"dist_to_wp": float(dist_to_wp),
|
| 1600 |
+
"waypoints_completed": len(self.waypoints_reached) if not self.arena_mode else int(self.goals_completed),
|
| 1601 |
+
"goals_completed": int(self.goals_completed),
|
| 1602 |
+
"dist_to_goal": float(dist_to_wp),
|
| 1603 |
+
"goal_arrival_radius": float(self.goal_arrival_radius) if self.arena_mode else 1.0,
|
| 1604 |
+
"goal_pos": self.goal_pos.tolist() if self.arena_mode else list(COURSE_WAYPOINTS[self.current_wp_idx][0]),
|
| 1605 |
+
"target_local_pos": (torso_xmat.T @ (target_pos - torso_xpos)).tolist(),
|
| 1606 |
+
"gaze_alignment": float(cos_gaze),
|
| 1607 |
+
"stamina": float(self.stamina),
|
| 1608 |
+
"health": float(self.health),
|
| 1609 |
+
"hunger": float(self.hunger),
|
| 1610 |
+
"step": self.current_step,
|
| 1611 |
+
"lidar_grid": self.current_lidar_grid.tolist(),
|
| 1612 |
+
"in_leeway": bool(in_leeway) if self.arena_mode else False,
|
| 1613 |
+
"leeway_remaining": int(self.goal_transition_steps) if self.arena_mode else 0,
|
| 1614 |
+
"stream_mode": "cruise",
|
| 1615 |
+
"active_reach_hand": "none",
|
| 1616 |
+
"reach_touches": 0,
|
| 1617 |
+
"getup_success": False,
|
| 1618 |
+
"stand_still_score": 0.0,
|
| 1619 |
+
}
|
| 1620 |
+
|
| 1621 |
+
return self._get_obs(), float(total_reward), terminated, truncated, info
|
| 1622 |
+
|
| 1623 |
+
def render_frame(
|
| 1624 |
+
self,
|
| 1625 |
+
width: int = 480,
|
| 1626 |
+
height: int = 320,
|
| 1627 |
+
camera_name: str = "behind_cam",
|
| 1628 |
+
ghost_datas: Optional[List[Any]] = None,
|
| 1629 |
+
ghost_alpha: Optional[float] = None,
|
| 1630 |
+
) -> np.ndarray:
|
| 1631 |
+
"""
|
| 1632 |
+
Renders an RGB numpy image of the current simulation frame.
|
| 1633 |
+
Optionally injects translucent ghost avatars into mjvScene for parallel multi-agent swarm visualization.
|
| 1634 |
+
"""
|
| 1635 |
+
if self._renderer is None or self._renderer.width != width or self._renderer.height != height:
|
| 1636 |
+
self._renderer = mujoco.Renderer(self.model, height=height, width=width)
|
| 1637 |
+
|
| 1638 |
+
self._renderer.update_scene(self.data, camera=camera_name)
|
| 1639 |
+
|
| 1640 |
+
# Inject dynamic course waypoint goal markers into mjvScene
|
| 1641 |
+
scene = self._renderer.scene
|
| 1642 |
+
identity_mat = np.eye(3).flatten()
|
| 1643 |
+
if self.stream_mode == "reach":
|
| 1644 |
+
# Stream 3 Reach: Glowing magenta hand target orb + vertical guide line
|
| 1645 |
+
rx, ry, rz = float(self.reach_target_pos[0]), float(self.reach_target_pos[1]), float(self.reach_target_pos[2])
|
| 1646 |
+
if scene.ngeom < scene.maxgeom:
|
| 1647 |
+
dst_orb = scene.geoms[scene.ngeom]
|
| 1648 |
+
orb_pos = np.array([rx, ry, rz], dtype=np.float64)
|
| 1649 |
+
orb_size = np.array([0.09, 0.09, 0.09], dtype=np.float64)
|
| 1650 |
+
orb_rgba = np.array([0.88, 0.20, 0.95, 0.95], dtype=np.float32) # Radiant Magenta
|
| 1651 |
+
mujoco.mjv_initGeom(dst_orb, mujoco.mjtGeom.mjGEOM_SPHERE, orb_size, orb_pos, identity_mat, orb_rgba)
|
| 1652 |
+
dst_orb.transparent = 1
|
| 1653 |
+
scene.ngeom += 1
|
| 1654 |
+
|
| 1655 |
+
if scene.ngeom < scene.maxgeom:
|
| 1656 |
+
dst_guide = scene.geoms[scene.ngeom]
|
| 1657 |
+
guide_pos = np.array([rx, ry, max(0.01, rz / 2.0)], dtype=np.float64)
|
| 1658 |
+
guide_size = np.array([0.012, max(0.01, rz / 2.0), 0.0], dtype=np.float64)
|
| 1659 |
+
guide_rgba = np.array([0.88, 0.20, 0.95, 0.35], dtype=np.float32)
|
| 1660 |
+
mujoco.mjv_initGeom(dst_guide, mujoco.mjtGeom.mjGEOM_CYLINDER, guide_size, guide_pos, identity_mat, guide_rgba)
|
| 1661 |
+
dst_guide.transparent = 1
|
| 1662 |
+
scene.ngeom += 1
|
| 1663 |
+
|
| 1664 |
+
elif self.stream_mode == "getup":
|
| 1665 |
+
# Stream 2 Get-Up: Golden upright elevation target disc
|
| 1666 |
+
torso_xpos = self.data.xpos[self.torso_body_id]
|
| 1667 |
+
tx, ty = float(torso_xpos[0]), float(torso_xpos[1])
|
| 1668 |
+
if scene.ngeom < scene.maxgeom:
|
| 1669 |
+
dst_target = scene.geoms[scene.ngeom]
|
| 1670 |
+
target_pos = np.array([tx, ty, 1.33], dtype=np.float64)
|
| 1671 |
+
target_size = np.array([0.25, 0.02, 0.0], dtype=np.float64)
|
| 1672 |
+
target_rgba = np.array([0.98, 0.75, 0.15, 0.65], dtype=np.float32) # Glowing Amber
|
| 1673 |
+
mujoco.mjv_initGeom(dst_target, mujoco.mjtGeom.mjGEOM_CYLINDER, target_size, target_pos, identity_mat, target_rgba)
|
| 1674 |
+
dst_target.transparent = 1
|
| 1675 |
+
scene.ngeom += 1
|
| 1676 |
+
|
| 1677 |
+
elif self.stream_mode == "stand":
|
| 1678 |
+
# Stream 1 Stand: Cyan stillness equilibrium ground pad
|
| 1679 |
+
if scene.ngeom < scene.maxgeom:
|
| 1680 |
+
dst_ring = scene.geoms[scene.ngeom]
|
| 1681 |
+
ring_pos = np.array([0.0, 0.0, 0.015], dtype=np.float64)
|
| 1682 |
+
ring_size = np.array([0.35, 0.015, 0.0], dtype=np.float64)
|
| 1683 |
+
ring_rgba = np.array([0.22, 0.74, 0.97, 0.50], dtype=np.float32) # Cyan Pad
|
| 1684 |
+
mujoco.mjv_initGeom(dst_ring, mujoco.mjtGeom.mjGEOM_CYLINDER, ring_size, ring_pos, identity_mat, ring_rgba)
|
| 1685 |
+
dst_ring.transparent = 1
|
| 1686 |
+
scene.ngeom += 1
|
| 1687 |
+
|
| 1688 |
+
elif self.arena_mode:
|
| 1689 |
+
# ACTIVE ARENA GOAL: Glowing floating target orb + vertical laser beacon column + ground target ring
|
| 1690 |
+
gx, gy, gz = float(self.goal_pos[0]), float(self.goal_pos[1]), float(self.goal_pos[2])
|
| 1691 |
+
|
| 1692 |
+
# 1. Floating target orb (sphere)
|
| 1693 |
+
if scene.ngeom < scene.maxgeom:
|
| 1694 |
+
dst_orb = scene.geoms[scene.ngeom]
|
| 1695 |
+
orb_pos = np.array([gx, gy, gz + 0.35], dtype=np.float64)
|
| 1696 |
+
orb_size = np.array([0.22, 0.22, 0.22], dtype=np.float64)
|
| 1697 |
+
orb_rgba = np.array([0.98, 0.75, 0.15, 0.90], dtype=np.float32) # Vibrant Gold
|
| 1698 |
+
mujoco.mjv_initGeom(dst_orb, mujoco.mjtGeom.mjGEOM_SPHERE, orb_size, orb_pos, identity_mat, orb_rgba)
|
| 1699 |
+
dst_orb.transparent = 1
|
| 1700 |
+
scene.ngeom += 1
|
| 1701 |
+
|
| 1702 |
+
# 2. Vertical laser beacon column (cylinder)
|
| 1703 |
+
if scene.ngeom < scene.maxgeom:
|
| 1704 |
+
dst_beam = scene.geoms[scene.ngeom]
|
| 1705 |
+
beam_pos = np.array([gx, gy, 1.25], dtype=np.float64)
|
| 1706 |
+
beam_size = np.array([0.05, 1.25, 0.0], dtype=np.float64)
|
| 1707 |
+
beam_rgba = np.array([0.22, 0.74, 0.97, 0.55], dtype=np.float32) # Glowing Cyan
|
| 1708 |
+
mujoco.mjv_initGeom(dst_beam, mujoco.mjtGeom.mjGEOM_CYLINDER, beam_size, beam_pos, identity_mat, beam_rgba)
|
| 1709 |
+
dst_beam.transparent = 1
|
| 1710 |
+
scene.ngeom += 1
|
| 1711 |
+
|
| 1712 |
+
# 3. Ground target arrival ring
|
| 1713 |
+
if scene.ngeom < scene.maxgeom:
|
| 1714 |
+
dst_ring = scene.geoms[scene.ngeom]
|
| 1715 |
+
ring_pos = np.array([gx, gy, 0.02], dtype=np.float64)
|
| 1716 |
+
ring_size = np.array([self.goal_arrival_radius, 0.02, 0.0], dtype=np.float64)
|
| 1717 |
+
ring_rgba = np.array([0.22, 0.74, 0.97, 0.45], dtype=np.float32)
|
| 1718 |
+
mujoco.mjv_initGeom(dst_ring, mujoco.mjtGeom.mjGEOM_CYLINDER, ring_size, ring_pos, identity_mat, ring_rgba)
|
| 1719 |
+
dst_ring.transparent = 1
|
| 1720 |
+
scene.ngeom += 1
|
| 1721 |
+
else:
|
| 1722 |
+
for wp_idx, (wp_coord, wp_name, wp_radius) in enumerate(COURSE_WAYPOINTS):
|
| 1723 |
+
if scene.ngeom + 3 >= scene.maxgeom:
|
| 1724 |
+
break
|
| 1725 |
+
wx = float(wp_coord[0])
|
| 1726 |
+
wy = float(wp_coord[1])
|
| 1727 |
+
wz = float(wp_coord[2])
|
| 1728 |
+
|
| 1729 |
+
if wp_idx == self.current_wp_idx:
|
| 1730 |
+
# ACTIVE GOAL: Glowing floating target orb + vertical laser beacon column + ground target ring
|
| 1731 |
+
# 1. Floating target orb (sphere)
|
| 1732 |
+
dst_orb = scene.geoms[scene.ngeom]
|
| 1733 |
+
orb_pos = np.array([wx, wy, wz + 0.35], dtype=np.float64)
|
| 1734 |
+
orb_size = np.array([0.18, 0.18, 0.18], dtype=np.float64)
|
| 1735 |
+
orb_rgba = np.array([0.98, 0.75, 0.15, 0.90], dtype=np.float32) # Vibrant Gold
|
| 1736 |
+
mujoco.mjv_initGeom(dst_orb, mujoco.mjtGeom.mjGEOM_SPHERE, orb_size, orb_pos, identity_mat, orb_rgba)
|
| 1737 |
+
dst_orb.transparent = 1
|
| 1738 |
+
scene.ngeom += 1
|
| 1739 |
+
|
| 1740 |
+
# 2. Vertical laser beacon column (cylinder)
|
| 1741 |
+
if scene.ngeom < scene.maxgeom:
|
| 1742 |
+
dst_beam = scene.geoms[scene.ngeom]
|
| 1743 |
+
beam_pos = np.array([wx, wy, wz + 0.60], dtype=np.float64)
|
| 1744 |
+
beam_size = np.array([0.05, 0.60, 0.0], dtype=np.float64)
|
| 1745 |
+
beam_rgba = np.array([0.22, 0.74, 0.97, 0.55], dtype=np.float32) # Glowing Cyan
|
| 1746 |
+
mujoco.mjv_initGeom(dst_beam, mujoco.mjtGeom.mjGEOM_CYLINDER, beam_size, beam_pos, identity_mat, beam_rgba)
|
| 1747 |
+
dst_beam.transparent = 1
|
| 1748 |
+
scene.ngeom += 1
|
| 1749 |
+
|
| 1750 |
+
# 3. Ground target landing pad ring
|
| 1751 |
+
if scene.ngeom < scene.maxgeom:
|
| 1752 |
+
dst_ring = scene.geoms[scene.ngeom]
|
| 1753 |
+
ring_pos = np.array([wx, wy, max(0.01, wz - 0.05)], dtype=np.float64)
|
| 1754 |
+
ring_size = np.array([float(wp_radius) * 0.6, 0.02, 0.0], dtype=np.float64)
|
| 1755 |
+
ring_rgba = np.array([0.22, 0.74, 0.97, 0.45], dtype=np.float32)
|
| 1756 |
+
mujoco.mjv_initGeom(dst_ring, mujoco.mjtGeom.mjGEOM_CYLINDER, ring_size, ring_pos, identity_mat, ring_rgba)
|
| 1757 |
+
dst_ring.transparent = 1
|
| 1758 |
+
scene.ngeom += 1
|
| 1759 |
+
|
| 1760 |
+
elif wp_idx < self.current_wp_idx:
|
| 1761 |
+
# COMPLETED GOAL: Subtle emerald disc on surface
|
| 1762 |
+
dst_comp = scene.geoms[scene.ngeom]
|
| 1763 |
+
comp_pos = np.array([wx, wy, max(0.01, wz - 0.05)], dtype=np.float64)
|
| 1764 |
+
comp_size = np.array([0.25, 0.015, 0.0], dtype=np.float64)
|
| 1765 |
+
comp_rgba = np.array([0.10, 0.85, 0.35, 0.40], dtype=np.float32) # Emerald Green
|
| 1766 |
+
mujoco.mjv_initGeom(dst_comp, mujoco.mjtGeom.mjGEOM_CYLINDER, comp_size, comp_pos, identity_mat, comp_rgba)
|
| 1767 |
+
dst_comp.transparent = 1
|
| 1768 |
+
scene.ngeom += 1
|
| 1769 |
+
|
| 1770 |
+
else:
|
| 1771 |
+
# UPCOMING GOAL: Translucent waypoint marker disc
|
| 1772 |
+
dst_up = scene.geoms[scene.ngeom]
|
| 1773 |
+
up_pos = np.array([wx, wy, max(0.01, wz - 0.05)], dtype=np.float64)
|
| 1774 |
+
up_size = np.array([0.16, 0.015, 0.0], dtype=np.float64)
|
| 1775 |
+
up_rgba = np.array([0.60, 0.65, 0.80, 0.25], dtype=np.float32) # Translucent Silver
|
| 1776 |
+
mujoco.mjv_initGeom(dst_up, mujoco.mjtGeom.mjGEOM_CYLINDER, up_size, up_pos, identity_mat, up_rgba)
|
| 1777 |
+
dst_up.transparent = 1
|
| 1778 |
+
scene.ngeom += 1
|
| 1779 |
+
|
| 1780 |
+
# Inject translucent ghost avatars if provided
|
| 1781 |
+
if ghost_datas:
|
| 1782 |
+
scene = self._renderer.scene
|
| 1783 |
+
for g_idx, g_data in enumerate(ghost_datas):
|
| 1784 |
+
color = GHOST_PALETTE[g_idx % len(GHOST_PALETTE)]
|
| 1785 |
+
if ghost_alpha is not None:
|
| 1786 |
+
color = color.copy()
|
| 1787 |
+
color[3] = float(ghost_alpha)
|
| 1788 |
+
for gid in self.humanoid_geoms:
|
| 1789 |
+
if scene.ngeom >= scene.maxgeom:
|
| 1790 |
+
break
|
| 1791 |
+
dst = scene.geoms[scene.ngeom]
|
| 1792 |
+
pos = g_data.geom_xpos[gid].astype(np.float64)
|
| 1793 |
+
size = self.model.geom_size[gid].astype(np.float64)
|
| 1794 |
+
mat = g_data.geom_xmat[gid].astype(np.float64)
|
| 1795 |
+
geom_type = int(self.model.geom_type[gid])
|
| 1796 |
+
|
| 1797 |
+
mujoco.mjv_initGeom(dst, geom_type, size, pos, mat, color)
|
| 1798 |
+
dst.transparent = 1
|
| 1799 |
+
scene.ngeom += 1
|
| 1800 |
+
|
| 1801 |
+
return self._renderer.render()
|