> ## Documentation Index
> Fetch the complete documentation index at: https://docs.almond.bot/llms.txt
> Use this file to discover all available pages before exploring further.

# tune.gravity

> Fit one link's real centre of mass from the motors, correcting the gravity feedforward.

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`](/cli/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. (For a custom gripper much heavier than stock, set the link mass first with [`tune.factory --mass`](/cli/tune-factory#custom-end-effectors).) 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 to humerus-horizontal (+90° left, −90° right — the frame is mirrored and +90° is the right arm's hard stop) 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`](/cli/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`](/cli/tune-friction) 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).

| Flag | Description |
| - | - |
| `--l` / `--r` | Arm side (required) |
| `--channel IFACE` | SocketCAN interface override for setups without the Axol hub adapter |
| `--joint JOINT` | `shoulder_1`, `shoulder_2`, `shoulder_3`, `elbow`, `wrist_1`, `wrist_2`, `wrist_3` (required) |
| `--velocity DEG_S` | Sweep velocity in deg/s (default: 18). Keep ≤25 so shoulder torque telemetry stays clean |
| `--lo DEG` / `--hi DEG` | Override the sweep limits (degrees) |
| `--kp FLOAT` / `--kd FLOAT` | Sweep-hold gains (default: the calibration gains — see [`tune.friction`](/cli/tune-friction)) |
| `--save` | Write the fitted CoM (and refit `Fo`) to `~/.almond/calibration.json`, together with the link mass it was fitted against; the gravity model then uses it everywhere on this machine |
| `--save-run` | Persist the sweep (measured vs model torque per angle) as a tuning run for the dashboard charts |
| `--label NOTE` | Free-form note stored with the saved run |

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.

```bash theme={null}
axol tune.gravity --l --joint shoulder_1
axol tune.gravity --l --joint elbow --save
axol tune.gravity --r --joint wrist_1 --velocity 25 --save
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.