Appearance
Robot PID auto-tuner · open source
Turns a robot description into tuned joint gains: an analytic start from the model, then measured tuning on the robot.
Point it at a servo or an arm and come back to a proposal: feedback gains, the feed-forward that rides with them, and the before/after on motion the tuner never saw. It never writes a board itself.
A UR5e-class arm in MuJoCo, running in your browser: soft factory gains (left) and pidtuna's result (right) play the same held-out motion. The faint ghost is the commanded pose.
Tracking error (RMS) on held-out motion, simulated, per robot kind. Factory is the drive's as-found gains (deliberately soft defaults); oracle is the best controller a brute-force search finds on the true plant, a ceiling no tuner can know.
Every robot improved at least 19× over its factory gains. Against the oracle the tuner is honest work in progress: within 15 % of its cost on 10 of 33 joints, a median of 33 % above it, and 3× on the lever, where it refused a faster start that saturated the motor. How the oracle is measured →
Five stages, one seam: every motion goes through a Plant you can point at a simulator or a real joint.
From the description alone (inertia, friction, the loop and its sensors): the fastest closed loop that keeps a gain margin of 2 and four other bounds. Moves nothing.
Short motions, then least squares for inertia, viscous and Coulomb friction and gravity.
Raise the bandwidth ×1.2 per rung from the analytic start until a hold rings. Deploy from the last quiet rung, with half its stiffness.
Try feed-forward terms against damping and bandwidth on the gates: stop error, tracking, ringing, chatter, saturation.
Play motion no stage has seen. Keep the as-found controller unless the new one wins there.
Board gains, host feed-forward, the identified model and the before/after, as JSON you review. Nothing is written for you.
The core is pure Python with numpy. MuJoCo arms are an extra.
pip install "pidtuna @ git+https://github.com/zacharyyamaoka/pidtuna" # once the repository is publicfrom pidtuna import Envelope, Gains, Goal, PlantModel, Servo
from pidtuna.analytic import LoopModel, analytic_controller
from pidtuna.config import proposal
from pidtuna.plants.servo_sim import ServoSim, Truth
from pidtuna.tune import tune
if __name__ == "__main__": # the simulator plays tapes in a process pool
# what you know about the servo: its gains now, its travel, its torque cap
servo = Servo(name="elbow", gains=Gains.from_moteus(100.0, 2.0), torque_limit_nm=3.0,
envelope=Envelope(travel_rad=0.5, max_speed_rad_s=3.0, max_torque_nm=3.0, origin_rad=0.0))
# 0. the analytic start, from the description alone (CAD inertia, datasheet friction, the loop)
described = PlantModel(inertia_kg_m2=0.005, viscous_nm_s_rad=0.1, coulomb_nm=0.5, gravity_sin_nm=0.3)
seed, _ = analytic_controller(described, LoopModel(torque_bandwidth_hz=250.0, torque_cap_nm=3.0, travel=0.5))
# 1-4. identify, sweep, refine, verify, on a simulated joint the tuner never sees inside
joint = ServoSim(servo, Truth(inertia_kg_m2=0.006, viscous_nm_s_rad=0.12, coulomb_nm=0.6, static_nm=0.8,
gravity_sin_nm=0.3, torque_bandwidth_hz=250.0, velocity_noise_rad_s=0.003))
result = tune(servo, joint, Goal(stop_allocation_mrad=4.0), seed=seed)
print(result.before.stop_p90_mrad, "->", result.after.stop_p90_mrad, "mrad stop p90")
print(proposal(result)["board_set"]) # the moteus keys to set; nothing is written for youYou get a proposal: the moteus config keys to set, the feed-forward for your host, the evidence. Applying it is your call.
Every gain it tries is held inside a band around what the joint already runs, and the sweep stops at the first hold that rings.
Verification is on motion it never chose on. On an arm, all joints move together before anything is delivered.
Read the design decisions, watch the replays, or tune a joint yourself.