Appearance
Quickstart
pidtuna tunes the position loop of a robot joint: feedback gains (kp, kd) plus the feed-forward that rides with them. Starting from the current gains or a closed-form answer computed from the robot's description, it identifies the joint, raises the gains until the joint first rings, backs off, and proposes the result with a before/after on motion it never tuned on, without writing anything to the drive.
Source: coming soon at github.com/zacharyyamaoka/pidtuna
Install
Once the repository is public:
bash
pip install "pidtuna[arm] @ git+https://github.com/zacharyyamaoka/pidtuna"The core needs only numpy (pip install "pidtuna @ git+https://github.com/zacharyyamaoka/pidtuna"). The [arm] extra adds MuJoCo for the multi-joint simulations.
Tune a joint
Describe the servo you have (its current gains and how far it may move), give the tuner something to move, and call tune. Here the "something" is a simulated geared joint whose true inertia, friction and gearbox spring the tuner never sees.
python
import json
from pidtuna import Envelope, Gains, Goal, Servo
from pidtuna.config import proposal
from pidtuna.plants.servo_sim import ServoSim, Truth
from pidtuna.tune import tune
# The servo as installed: soft factory gains, SI units at the joint output.
servo = Servo(
name="elbow",
gains=Gains(kp=8.0, kd=0.4), # N·m/rad, N·m·s/rad
envelope=Envelope(travel_rad=0.4, max_speed_rad_s=2.0, max_torque_nm=6.0, origin_rad=0.0),
torque_limit_nm=6.0,
)
# A simulated joint. The tuner only sees what it can measure by moving it.
truth = Truth(inertia_kg_m2=0.03, viscous_nm_s_rad=0.05, coulomb_nm=0.25, gravity_sin_nm=1.2,
gear_stiffness_nm_rad=3000.0, link_inertia_fraction=0.6)
if __name__ == "__main__": # ServoSim plays tapes in a process pool
result = tune(servo, ServoSim(servo, truth), Goal())
print(f"stop p90 {result.before.stop_p90_mrad:5.1f} -> {result.after.stop_p90_mrad:4.1f} mrad")
print(f"tracking {result.before.tracking_rms_mrad:5.1f} -> {result.after.tracking_rms_mrad:4.1f} mrad")
print(result.verdict["feedforward"])
doc = proposal(result)
print(json.dumps(doc["board_set"], indent=1))
print(result.notes)One run prints (about 3 s on a desktop):
text
stop p90 32.5 -> 3.8 mrad
tracking 31.3 -> 2.9 mrad
FF (Coulomb x1, viscous, inertia, gravity) + FB
{
"servo.pid_position.kp": 100.531,
"servo.pid_position.kd": 5.0265
}
['No ringing up to the gain guard (2x the as-found gains): the stability limit lies beyond it, so a second session after writing these gains could go further.']The before/after is on held-out tapes: motion from a different random seed than the one the gains were chosen on. The gains stopped at 2× the factory values because the default gain guard never lets a session stray further than that from gains known to be safe.
What you get back
tune returns a TuneResult (fields):
tuned: the chosen controller, feedback gains plus feed-forward. It is the as-found controller unless something beat it by at least 10 % on held-out motion.before/after: the as-found and tuned controllers scored on the same held-out tapes: stop error p90, tracking RMS, ringing, chatter, saturation (scoring).model: the identified joint: inertia, viscous and Coulomb friction, gravity, with 95 % intervals.sweep,candidates: every rung of the bandwidth sweep and every controller the search tried, with why any were rejected.verdict,notes: the stability onset, the deploy ceiling, whether feed-forward was worth it, and plain-language notes like the one above.
config.proposal(result) turns that into one JSON document (the proposal). Writing it to a drive is a separate step you run yourself.
Next
- Start from the robot's description instead of the factory gains: the analytic start.
- Drive your own hardware: write a
Plant. - Tune a whole arm:
tune_armorpidtuna arm-sim. - Watch it work: MuJoCo replays of every robot and a live single-axis tune in the browser.