Appearance
Design decisions
Each one: the choice, why, and what was rejected. Most were forced by a failure; the failure is named where it explains the choice.
No integrator by default
Choice: PD plus feed-forward. Goal.integrator is False, and ki is never searched.
Why: the goal is fast, responsive motion. A steady-state error should be explained by the feed-forward model (friction, gravity, load), not wound up in an integral. On a sticky geared joint, an integrator large enough to break away from a 1 mrad error starts a stick-slip hunt.
Rejected alternative: searching ki and an integral clamp. The cost is real. On two simulated test joints built from real-joint twins, a small integrator brought held-out stop p90 to 1.8 and 0.8 mrad, where the integrator-free best is 7.0 and 5.1 mrad. Those numbers stay as evidence (K3/K4); the integrator stays out.
Feed-forward first, then feedback
Choice: two levels. One level up, the robot model turns every joint's reference into the torque it needs (gravity, link inertia, coupling), recomputed each command. Each joint's box owns its gains and its own friction feed-forward. On a closed chain or with passive joints the model's feed-forward switches off, and the box carries gravity and inertia itself.
Why: feedback should only fix what the model gets wrong. A single joint cannot know what it carries, and without the model's feed-forward the gantry's Z axis and the UR5e's links sag under factory gains. Within a box, a feed-forward term must still beat feedback alone by 10 %, because every feed-forward is one more model that can drift.
Rejected alternative: tuning stiff enough gains to absorb gravity and coupling, or one box per joint learning everything, coupling included.
A numpy-only core
Choice: the runtime depends on numpy only. MuJoCo is an optional extra, and hardware lives behind the Plant seam.
Why: it installs anywhere and is tested in isolation. A heavy simulator is one kind of plant, not a dependency of the tuner.
Rejected alternative: building the core on a simulator or a robotics framework.
One Plant seam
Choice: everything that moves goes through run(tape, controller) -> Trace.
Why: one code path tunes a numpy servo, a MuJoCo robot or real hardware, and scoring never knows which. Because the same tapes play everywhere, a simulated twin can be checked against the real joint (K5).
Rejected alternative: a tuner that talks to drives directly, or a separate tuner per backend.
The analytic start measures nothing
Choice: stage 0 reads only the description and the configured loop.
Why: it has to be an independent baseline the tuner must beat, and on a real robot the hidden truth is not available. Two earlier versions estimated sensor noise from a second of standing still. Both let a hidden load move the start: a payload's sag, then a lightly damped settle, each read as noise.
Rejected alternative: a short standstill measurement before computing the start.
Five bounds, not one, choose the bandwidth
Choice: the largest ωn that passes gain margin, sensor noise, encoder quantization, a third of the structural mode, and the linear range left after gravity.
Why: on gain margin alone the start asked a lever for 760× its drive's kp, and a belt conveyor for kp above its belt resonance. Details.
Rejected alternative: a stability margin alone, or a fixed bandwidth per actuator class.
Identify first, then tune in (ωn, ζ)
Choice: fit J, b, Coulomb and gravity before any search, then search bandwidth and damping instead of raw gains. The state/torque delay is calibrated per joint, and the fit re-identifies at softer gains if the loop is too fast for the command rate.
Why: with J known, ωn and ζ mean the same thing on a 1e-4 kg·m² direct drive and a 3.6 kg·m² shoulder, so one search and one ringing rule serve both. The corrections each fixed a measured bias: the command latch alone reads as +40 % inertia on a geared joint at 100 Hz; on a real joint the fitted inertia fell as kd rose until a few milliseconds of state/torque delay were modelled; and a loop too fast for the command rate read a simulated finger 2.7× heavy.
Rejected alternative: model-free tuning of kp and kd directly.
Sweep to the first ringing, then back off
Choice: raise ωn ×1.2 per rung on a step tape until a hold rings, crosses its target 4 times, chatters or aborts. Deploy at half the last quiet rung's kp, about 0.35× the first ringing rung's.
Why: this is Ziegler–Nichols' "turn it up until it oscillates, then back off", automated and one guarded rung at a time. It backs off from the last quiet rung because at ~100 Hz logging an instability is visible only once it has started, one rung late. It counts crossings rather than overshoot because a drive commanded in position and velocity overshoots about 20 % at any gain.
Rejected alternative: backing off from the first ringing rung, or a relay or sine-injection test that reads the margin without ringing the joint. The second is gentler; it was considered and is not built.
Gates decide; the cost only ranks
Choice: a candidate must pass every gate (complete, stop evidence, ringing, chatter, saturation, tracking, stop allocation, deploy ceiling). Only then does the cost, stop p90 plus tracking RMS relative to as found, rank it.
Why: a cheap cost can hide a joint that rings, rides the torque cap or buzzes. One candidate alternating ±3 N·m at the cap passed on position alone. A NaN passes every "reject if worse" comparison, so acceptance asks for evidence: finite telemetry, targets and clock, and at least one stop.
Rejected alternative: one weighted score.
Prove it on held-out motion
Choice: choose on one tape seed and prove on another, with every gate applied again. A change must win by 10 %, and the analytic start is the incumbent the search must beat.
Why: a controller picked on the tapes you report flatters itself. One finger's pick was 5 % better than its start alone, and 5–12 % worse with every finger moving.
Rejected alternative: reporting the search's own score.
Never write a board
Choice: the output is a proposal. Applying it is a separate step a person runs, with the old values kept for rollback.
Why: a tune is a recommendation with its evidence attached, and gains on a drive move real hardware.
Rejected alternative: auto-applying the pick at the end of a session.
A gain guard
Choice: every gain tried stays within gain_scale_range of the as-found gains (0.25–2× by default). Plants refuse anything outside rather than clipping it.
Why: a session never strays far from gains known to be safe, and a refusal makes a bug loud instead of quietly testing something else. To go further, write the result and tune again.
Rejected alternative: an unbounded search, or silent clipping in the plant.
The arm is never left worse than as found
Choice: tune tip to base, then play all joints together (the arm check). Back off offenders 0.8× per round, at most five rounds and never above their ceiling. If that fails, keep the best whole combination with no offender, else the whole arm as found.
Why: coupling is invisible one joint at a time. On the five-bar, reverting only the offending joint left it beside a stiff partner it was never tuned with, worse than factory everywhere. A joint pushed by its partners rings more when softened, so the check replays it with its partners holding still before blaming it.
Rejected alternative: accepting the per-joint results, or reverting one joint at a time.
An oracle as the ruler
Choice: per joint, the best admissible controller a brute-force search finds on the true plant is the ceiling. Tuning is graded by its regret against it (K6 ≤ 0.15), and every KPI was fixed before its first result.
Why: "beats factory gains" is easy. The useful question is how far from the best possible the tuner lands, and whether a loss comes from the description, the closed form or the search. The oracle ladder.
Rejected alternative: grading against factory gains only, or adjusting a KPI after seeing a result.