The project is currently in its initial phase, with the core focus on mastering bipedal locomotion. The immediate objectives are:
- Achieve Stable Walking: Train a robust walking policy using reinforcement learning.
- Cross the Sim2Real Gap: Successfully transfer the policy trained in a simulated environment to the physical robot.
At this stage, the project is concentrated on the fundamental mechanics of the body's movement. Expressiveness and the integration of a head are future goals to be explored after mastering stable locomotion.
The Qmini is built with a focus on high-performance components that are accessible to the robotics community. The entire build is being developed with a target budget under $3,000.
- Unitree GO-M8010-6 Motors: These motors provide the necessary torque and precision for dynamic and controlled leg movements.
- NVIDIA Jetson Orin Nano: Serving as the onboard computer, the Jetson Orin Nano has the computational power required to run the trained RL policy in real-time.
The robot's ability to walk is being developed through reinforcement learning within the NVIDIA Isaac Lab simulation environment.
A policy is trained in this virtual space, allowing the Qmini to learn and adapt its movements to maintain balance and achieve forward motion. This process is critical for developing a robust control system before deploying it on the physical hardware.
The robot description (URDF + meshes) lives in the qmini_description repo, checked out as a git submodule at source/Qmini/assets/qmini. Clone with submodules:
git clone --recurse-submodules <this-repo>
# or, in an existing clone:
git submodule update --initTo install the necessary packages for this project, after cloning the repo, run the following command:
cd python_qmini_isaaclab
python -m pip install -e source/Qminipython scripts/rsl_rl/train.py --task=qmini-velocity --headless
# resume training
python scripts/rsl_rl/train.py --task=qmini-velocity --headless --resume --load_run <date> --checkpoint model_*.ptpython scripts/rsl_rl/play.py --task=qmini-velocity-play --num_envs 100
#If you ever need to be explicit, override with --load_run and --checkpoint:
python scripts/rsl_rl/play.py --task=qmini-velocity-play --num_envs 100 --load_run=<date> --checkpoint=model_<?>.ptscripts/rsl_rl/eval_policy.py plays a checkpoint headless for 10 s and prints commanded vs
actual forward velocity plus the per-foot swing rate โ a two-minute answer to "is it actually
walking?" without opening the viewer. Works mid-training on any saved model_*.pt:
python scripts/rsl_rl/eval_policy.py --headless
python scripts/rsl_rl/eval_policy.py --headless --load_run <date> --checkpoint model_5000.ptA healthy walking policy shows mean actual vx close to the commanded value and ~1.5
swings per foot per second (the gait clock frequency).
50 Hz control loop. Input obs = 44 floats in this exact order (scales applied by the
runtime; the policy has no baked-in normalization โ empirical_normalization=False):
| dims | content | scale/notes |
|---|---|---|
| 3 | IMU angular velocity | ร 0.2 |
| 3 | IMU projected gravity | unit vector, (0,0,โ1) upright |
| 10 | joint_pos โ default_pose | sim joint order (printed by play.py) |
| 10 | joint_vel | ร 0.05 |
| 10 | last_action | previous raw network output |
| 3 | command [vx, vy, wz] | joystick; zero whole vector when โcmdโ < 0.15 |
| 4 | gait clock [sin ฯL, sin ฯR, cos ฯL, cos ฯR] | ฯL = (1.5ยทt) mod 1, ฯR = ฯL + 0.5 |
| 1 | static_flag | 1.0 if โcmdโ < 0.15 else 0.0 |
Output action = 10 floats: joint_target = default_pose + 0.5 ยท action, fed to the motor PD
(gains from source/Qmini/robots/qmini.py). The command deadband is the stand/walk switch:
joystick released โ cmd zeroed โ static_flag = 1 โ the policy holds the stand pose.
Before training, it's worth verifying that the robot, URDF, actuator gains, and initial pose are all wired up correctly. The zero_agent.py script does exactly that: it launches the environment and pushes a zero action vector every step, so the only forces acting on the robot are gravity and the PD controller pulling each joint toward its default_joint_pos (i.e. the crouch pose defined in source/Qmini/robots/qmini.py).
If everything is set up correctly, you should see the robot spawn ~45 cm above the ground, fall briefly, and then stand stably in the crouched pose โ no exploding joints, no penetration through the floor, no immediate falls.
python scripts/zero_agent.py --task=qmini-velocity --num_envs 10--taskโ Gym task ID. Useqmini-velocity(training env) orqmini-velocity-play(smaller scene, no randomization).--num_envsโ number of parallel environments. Use1for visual inspection, larger values to stress-test stability.--headlessโ run without the viewer window (faster; useful if you only want to confirm there are no crashes).--disable_fabricโ fall back to USD I/O instead of Fabric. Only needed if Fabric throws unexpected errors.
- Robot stands stably in the crouched pose โ URDF,
stiffness/damping/armature, andinit_stateare consistent. Safe to start training. - Robot drifts and slowly tips over โ likely
stiffnesstoo low,armaturetoo low, or the pose is not statically stable for this geometry. Re-tune PD gains, or runtune_home_pose.pyto search for a stablejoint_pos. - Joints visibly jitter or oscillate โ
stiffnesstoo high relative toarmature. Increasearmatureor reducestiffness(seeLEARN.md). - Feet penetrate the ground or the robot launches into the air โ
init_state.posz is too low, or the URDF collision meshes are wrong. - Simulation crashes with NaN โ almost always an actuator-config issue (effort/velocity limits, or
armature = 0with very stiff PD).
If zero_agent.py shows the robot tipping over, the init_state.joint_pos is not a statically
stable standing pose. Hand-tuning ten joint angles by trial and error is painful โ so
scripts/tune_home_pose.py does it for you, using the simulator itself.
Because the action term uses use_default_offset=True, a zero action commands the PD controller to
hold exactly init_state.joint_pos. So "stand under zero_agent.py" reduces to one question: is
joint_pos a statically-stable pose, spawned at a height where the feet just rest on the ground? This
script answers it physically and, if the answer is no, searches for a pose that works:
- Verify โ spawn the robot alone on flat ground with its real actuators, command the candidate
joint_pos, drop it, and let it settle. If it stays upright with both feet planted, report the rest height. - Search (if unstable) โ drop the robot free-base under load (so the legs sag exactly as they will under a zero action) and run a left/right-symmetric pattern search over the leg joints. The objective rewards staying upright, keeping the center of mass over the feet (the axis a biped topples on), level + flat feet, and a sensible crouch height. Coupled hip_pitch/knee/ankle moves let it translate the stance fore/aft without breaking foot contact.
- Report + write โ print the resulting
joint_posdict + spawnpos.zand patch them straight intosource/Qmini/robots/qmini.py.
# Verify (and, if needed, search) โ writes the result into qmini.py; headless is faster:
python scripts/tune_home_pose.py --headless
# Same, but watch it in the viewer:
python scripts/tune_home_pose.py--target-heightโ desired base standing height (default0.38, the env'sbase_height_deviationtarget). This is a soft guide; the search never sacrifices stability to hit it.--settle-time,--upright-tol-deg,--clearanceโ tune the verify horizon, the max tilt still counted as "standing", and the spawn clearance above the rest height.
STABLE STANDING POSE FOUNDโ prints thejoint_posdict and spawn height, and writes them intoqmini.py. Confirm withzero_agent.py.Could not find a statically-stable standing poseโ the geometry likely can't balance under pure PD at the requested height. Try a larger--target-height(more extended legs balance more easily), or check the URDF joint signs/limits.
Note on height: Qmini cannot passively balance the ~0.31 m crouch the rewards target โ under pure PD (no policy) the lowest stable base height is ~0.41 m, so the committed pose rests there. Training pulls it down to the crouch via the active policy. Don't force
joint_posback to a 0.31 m crouch and expectzero_agent.pyto still stand.
To deploy a trained policy on the Jetson, export it to ONNX with scripts/export_onnx.py. Pass the date folder of the training run; the script picks the latest model_*.pt in logs/rsl_rl/qmini/<date>/ and exports it:
python scripts/export_onnx.py 2026-05-24This writes the result to policy.onnx in the project root. The exported graph is the deterministic actor โ it maps raw observations directly to action means (the Qmini PPO config uses no observation normalization).
play.py also exports ONNX via the built-in Isaac Lab exporter, but that exporter expects the legacy ActorCritic layout and silently skips the export for the newer rsl-rl mlp.* weight layout this project trains with. export_onnx.py sidesteps that: it reconstructs the actor MLP directly from the checkpoint's actor_state_dict, so it does not boot Isaac Sim and does not depend on the installed rsl-rl version.
If onnxruntime is installed (it is a project dependency), the script runs the exported graph and compares it against the PyTorch model on random inputs, printing the max absolute difference:
[INFO] PyTorch/ONNX max abs diff: 5.364e-07 (OK)
A difference below 1e-5 confirms the export is faithful and safe to deploy.
--activationโ hidden activation, defaultelu(matchesPPORunnerCfg).--opsetโ ONNX opset version, default12.--keyโ state-dict key inside the checkpoint, defaultactor_state_dict.
Note: The script always exports the latest checkpoint in the folder, which is not necessarily the best-performing one โ RL reward can be noisy near the end of training. Validate with
play.pyrollouts before deploying.
Tip:
zero_agent.pyis also the fastest way to validate any change toqmini.pyor the URDF before you spend GPU hours training.