|
Mujoco KDL Wrapper
0.2.2
MuJoCo + KDL bridge for robot kinematics and dynamics
|
This document explains how torque-mode control works in mj-kdl-wrapper, why KDL is used for all dynamics computations, how MuJoCo's equation of motion relates to the torques you send, and how ACHD and RNEA fit together.
This document covers torque mode only. Position and velocity modes write directly to data->ctrl and do not involve KDL dynamics.
When ctrl_mode = CtrlMode::TORQUE, joint commands are written directly to data->qfrc_applied – bypassing MuJoCo actuators entirely. This means the caller is fully responsible for computing the correct torques at every step.
A natural candidate for feedforward is data->qfrc_bias (MuJoCo's gravity/Coriolis bias), but this has a critical limitation: it is computed from the full model, so its values change whenever the gripper or any distal mass moves. Reading qfrc_bias back as a feedforward therefore couples your control law to the simulation internals and makes it impossible to reason about dynamics independently.
The correct approach – and the only one supported by this wrapper – is to build a KDL chain that contains all links from base to tool tip and use KDL::ChainDynParam::JntToGravity (or JntToMass, JntToCoriolis) for every torque-mode computation.
Rule: if ctrl_mode == TORQUE, all feedforward torques come from KDL.
MuJoCo's continuous-time equations of motion are:
which rearranges to the forward dynamics solve:
The key quantities stored in mjData are:
| Field | Meaning |
|---|---|
| qfrc_bias | Bias force c = Coriolis + centrifugal + gravity |
| qfrc_applied | User-supplied generalized forces (what jnt_trq_cmd writes to) |
| qfrc_actuator | Forces from model actuators through their moment arms |
| qfrc_passive | Spring/damper passive forces |
The total applied force is tau = qfrc_applied + qfrc_actuator + qfrc_passive.
Critical sign convention: c is subtracted in the forward dynamics. To hold a static pose (v_dot = 0, f_constraint = 0):
This means the controller must explicitly supply the gravity and Coriolis compensation – MuJoCo does not provide it passively regardless of the actuator type used.
MuJoCo position actuators (such as those in the Kinova GEN3 Menagerie model) compute:
For the GEN3:
| Actuator class | kp | kv |
|---|---|---|
| large (j0-j3) | 2000 | 100 |
| small (j4-j6) | 500 | 50 |
Case 1 – partial nulling (ctrl = qpos): Setting ctrl = qpos zeroes the stiffness term kp*(ctrl-pos) but leaves the velocity term -kv*vel active. With full RNEA computed-torque the desired closed-loop is qddot = qddot_des, but the residual damping breaks this:
For the GEN3's lightweight wrist links (M ~ 0.01 kg*m^2) and kv = 50, the extra term is kv/M ~ 5000 rad/s – orders of magnitude larger than any Kd gain. The arm barely moves.
Case 2 – full nulling (ctrl = qpos + (kv/kp) * qvel): Reading kp and kv from the compiled model and setting:
drives the complete actuator force to zero:
qfrc_applied is then the sole torque source, exactly matching a real robot's torque interface. The relevant code is in src/mj_kdl_wrapper.cpp:
This works for any actuator type that stores its bias as biasprm[2] = -kv (MuJoCo's mjBIAS_AFFINE / biastype=1), which covers all standard <position> actuators.
With the actuator fully nulled, the MuJoCo plant (from the equation of motion above) reduces to:
Applying tau = M(q)*qddot_des + C(q,qdot)*qdot + g(q) – which equals c when qddot_des = 0, and generally equals c + M*qddot_des – yields:
This is exact computed-torque cancellation. KDL::ChainIdSolver_RNE computes M*qddot_des + C*qdot + g in O(n) with the Recursive Newton-Euler algorithm:
With Kp[i] acting as a squared natural frequency (rad/s^2 per rad) and Kd[i] ~ 2*sqrt(Kp[i]) for critical damping, the closed loop per joint is a decoupled second-order system: e_ddot + Kd*e_dot + Kp*e = 0.
The KDL chain built by init_robot_from_mjcf reads body_inertia (principal moments) and body_iquat (principal-axis orientation) from the compiled MuJoCo model and correctly rotates them into the body frame (I = R * diag(lambda) * R^T).
Neither model includes reflected motor/gear inertia (armature). For the real GEN3 this is the dominant inertia term; for simulation it is irrelevant since MuJoCo also omits it. If you add armature to the MJCF joints, update the KDL chain inertias accordingly (or the computed-torque feedforward will be inaccurate).
The KDL::ChainHdSolver_Vereshchagin_Fixed_Joint solver (ACHD) takes Cartesian acceleration constraints at the end-effector and computes both a forward-dynamics result (joint accelerations qdd) and the joint torques that enforce those constraints (constraint_tau).
Its key inputs and outputs:
The solver models gravity via a root acceleration acc_root = (0, 0, -g) – the standard ABA pseudo-force trick that makes gravity appear as an inertial effect. This means constraint_tau is computed in an ABA frame where gravity is already absorbed, and total_tau (obtainable via getTotalTorque()) is near zero for a static constrained hold:
The TCP's spatial acceleration from getTransformedLinkAcceleration() correctly shows xdd_lin = (0, 0, g) at the end-effector – confirming the solver holds the TCP stationary in the inertial frame (xdd_inertial = xdd_ABA + acc_root = 0).
From MuJoCo's equation of motion:
To produce the desired qdd from ACHD, we need:
ACHD's constraint_tau is defined as:
Since bias_ACHD ~= c = qfrc_bias, this gives:
But MuJoCo requires tau = M * qdd + c. Sending constraint_tau as qfrc_applied therefore produces:
The 2 * M^-1 * c residual is an uncompensated double-gravity term that causes the arm to drift. Measured on the Kinova GEN3 home-hold task:
| Command sent to qfrc_applied | TCP position error (2500 steps) |
|---|---|
| constraint_tau only | ~31 mm |
| RNEA (M*qdd + C*qd + G) | ~7 mm |
Adding an integral term (Ki) to the Cartesian PD reduces the steady-state offset but does not fix the underlying mismatch – it just accumulates enough integral action to fight the double-gravity residual.
The proper way to use ACHD output in MuJoCo torque control is:
For the combined ACHD -> RNEA controller, do not pass ACHD task/support wrenches into RNEA. The wrench is part of the ACHD constrained dynamics solve; RNEA is only the inverse-dynamics bridge that converts the resulting qdd into the full joint torque needed by MuJoCo or the robot. Keep the RNEA external-wrench vector zero in this path.
The constraint_tau output is still useful for diagnostics – it isolates the task-space contribution – but tau_cmd from RNEA is what gets sent to the actuators.
ACHD does not require all six TCP axes to be constrained. The table-slide example uses this to let the table/contact system handle vertical support while the controller regulates only the useful task axes:
In src/examples/ex_achd_table_slide.cpp, set_alpha_no_linear_z() builds this 5-column alpha, and beta contains only the X/Y position and orientation errors. There is no linear-Z beta term, so ACHD is free to choose the vertical joint acceleration that best satisfies the remaining constraints and the system dynamics. This is the right model for a supported slide: wrist/table contact can carry the vertical reaction, while the controller commands forward motion and orientation.
The RNEA bridge is unchanged for partial constraints:
RNEA does not need to know which Cartesian axes were constrained. It only sees the resolved joint acceleration qdd from ACHD and computes the full torque needed to realise that acceleration in the robot dynamics. The one-shot printout in ex_achd_table_slide compares the nc=6 and nc=5 cases so the torque difference from disabling linear Z is visible.
Note on real robots: On a real robot with an inner gravity-compensation loop (common on torque-controlled manipulators), constraint_tau would be the correct outer-loop command because the inner loop already provides c. MuJoCo does not provide such a layer – it simulates raw physics – so the full RNEA is necessary.