Skip to main content
Identifies one link’s real centre of mass from a friction-cancelled torque sweep instead of trusting the CAD constants, and corrects the gravity feedforward with it. Also available from the diagnostics dashboard (axol serve). Why: gravity feedforward comes from the URDF masses/CoMs — CAD values shared by every robot. If the real link is a few percent off (cables, end-effector variance, build spread), the kp spring has to make up the missing torque and the joint parks with a static droop under load: droop = unmodeled torque / kp. No amount of kp/kd tuning fixes that cleanly; correcting the model does, at every angle at once. How: the same bidirectional constant-velocity sweep as tune.friction — averaging forward and backward torque at the same position cancels friction exactly, leaving gravity(q) + Fo. The residual against the current model is fit to a shift of this link’s centre of mass, by ridge-regularized least squares straight through the MuJoCo gravity model, so the correction generalizes to every arm pose. Mass stays at CAD: gravity torque only depends on the first moment m·c, so a CoM shift with fixed mass covers every identifiable error. A single-joint sweep is a one-dimensional slice of pose space and can never observe all three CoM components equally — the ridge keeps weakly observed directions at their current value instead of letting them absorb torque noise with a huge lever arm, and a fitted shift beyond 60 mm is rejected as bad data. Loaded sweep poses: gravity has zero moment about a vertical axis, no matter where the mass sits — so a joint whose axis hangs vertical at rest carries no CoM signal at all. Three joints are therefore swept with other joints posed to tilt and load them: shoulder_3 runs with shoulder_1 raised 90° and the elbow bent; wrist_1 with the elbow bent and wrist_2 rotated 85°; wrist_2 keeps the elbow raise (base clearance) and adds wrist_1 at 90° to restore the load the raise removed. Clearance and load poses were verified against the torso collision model, are ramped proximal-first, and feed the model predictions so the fit is computed at the pose the sweep actually ran at. A sweep the model says is still unloaded is refused as unobservable rather than fit to noise. Run distal → proximal (wrist_3 → … → shoulder_1): a proximal joint’s sweep rotates every distal link with it, so on an uncalibrated arm their errors get lumped into the proximal link’s CoM — exact at the sweep pose, approximate once the elbow/wrists bend away from it. The tool prints a note when distal links are still uncalibrated. Sweep safety otherwise matches tune.friction: shoulder_2 sweeps only its outboard side, capped at 0 (starting at rest is fine — travel past it swings into the base); shoulder_3 / wrist_1 sweeps hold shoulder_2 10° outboard to clear the chest cameras and are capped at ±90° at their loaded poses. Run tune.friction --save beforehand — the friction offset Fo is refit against the corrected model and saved with the CoM (the two are coupled: what the old Fo absorbed as a constant may really have been gravity shape). The report prints the CoM shift in mm, the shape residual before/after, and the worst parked droop before/after at the joint’s config kp — the user-visible payoff.