> ## 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. 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`](/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: from `AxolConfig`)                                                                                    |
| `--save`                    | Write the fitted CoM (and refit `Fo`) to `~/.almond/calibration.json`; 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
```
