Skip to content

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 ​

Source: coming soon.