Tuning PID Controllers with Automatic Differentiation
PID controllers are the workhorses of industrial control. They are simple enough to fit on a microcontroller, yet surprisingly effective across a wide range of systems. The catch is that tuning them well takes time and, if you are being honest, a fair amount of intuition. Classic recipes like Ziegler-Nichols are fast but often leave performance on the table. Model-free autotuning approaches work but can require live experiments on the real plant.
Here is a different take: write down a differentiable simulation of your system, define a loss that captures what “good control” means to you, and let gradient descent find the gains. Automatic differentiation handles the calculus; you just specify the objective.
This post walks through exactly that, using JAX1. The controller structure follows the practical implementation guidelines of Sundström et al.2
The Example: A Second-Order Mass-Spring-Damper
The plant is a linear mass-spring-damper:
with mass , damping , and spring stiffness . The PID controller drives the position to a unit step setpoint over a simulation.
This system has a natural frequency of and a damping ratio of , so the open-loop step response is lightly damped and oscillatory — a good stress test for the tuning method.
The Incremental (Velocity) Form
Rather than computing the control output directly at each step, the simulation uses the incremental (or velocity) form: only the increment is computed, and the output is accumulated as .
This structure has several practical advantages over the position form. Actuator saturation and bumpless switching are easier to handle because the accumulator naturally keeps track of where the integrator state is. There is also no integral wind-up from the proportional or derivative paths — only the explicit integral increment can cause wind-up.
Setpoint weighting
A standard PID controller applies the setpoint equally to all three terms. In practice it is often better to split this: let the proportional and derivative paths see a weighted version of the setpoint, while the integral path always sees the full error.
Two weights and parameterise this:
Setting reduces the proportional kick when the setpoint steps, giving a smoother response without sacrificing integral action. Setting makes the derivative act only on measurement changes — eliminating the large derivative spike that would otherwise appear at when jumps discontinuously.
Derivative approximation
Rather than using a filtered derivative, the derivative is approximated separately for and via raw finite differences:
The second-difference increments and then enter the derivative increment directly.
The full incremental update at step is:
where and .
Here, setting and implements derivative-on-measurement. For a constant setpoint on every step after the initial one, the proportional increment reduces to and only responds to how the output is changing. The integral term carries the absolute error information needed to eliminate steady-state offset.
The plant is integrated with semi-implicit Euler (velocity updated before position), which is slightly more stable than explicit Euler for oscillatory systems at the same step size .
Saturation and Anti-Windup
Rate-limited bounds
Rather than applying fixed saturation limits directly, the active bounds at each step are tightened by a rate constraint on the previous output :
This limits how fast the actuator output can change between steps, independently of the absolute limits. In the current simulation the rate limits are set large enough to be inactive, but the structure is in place for the general case.
Anti-windup by soft pull-back
After computing , a soft correction pulls the signal back toward the feasible region before the hard clip:
When the signal is within bounds and the correction vanishes. When saturated, the correction partially unwinds the accumulator at a rate governed by the time constant . The state carried forward is (after soft correction, before the final hard clip), so the integrator is reset smoothly rather than hard-clamped. The hard clip on ensures the plant never sees an out-of-range command.
The Loss Function
Rather than simply minimizing error relative to a step input (which can result in overshoot as the optimizer pushes for the fastest possible rise time), we use a Model-Reference Loss. We define a desired critically damped second-order response:
with . This reference trajectory has zero overshoot and a settling time of approximately — a perfect template for high-performance, smooth tracking.
The objective minimizes the tracking error relative to this reference, plus a tiny control effort regulariser:
with .
Model Reference Tracking. By penalizing deviations from , the optimizer is guided to find gains that yield zero overshoot and follow a smooth, physical trajectory.
Effort regularizes the gains with a very small coefficient, preventing them from becoming unnecessarily large while allowing the controller to saturate at its physical limit () to match MATLAB tuning conditions.
Why JAX?
JAX’s jit + grad pipeline means the gradient computation costs roughly the same as two forward passes — that is the magic of reverse-mode AD. With jax.lax.scan you can unroll the simulation loop without materialising every intermediate in Python, keeping compilation time and memory under control even for long horizons.
There are no finite-difference approximations and no symbolic manipulations. The gradients are numerically exact (up to floating point) regardless of the complexity of the integrator or the loss. Crucially, jnp.clip is differentiable in JAX: its subgradient is zero in the saturated region and one elsewhere, so the optimizer receives a correct signal even when the saturation bounds are active.
Optimisation Setup
The optimised parameters are , initialised at . The setpoint weights and are fixed constants. Setting implements derivative-on-measurement, ensuring that setpoint steps do not create derivative spikes (derivative kick).
Adam runs for 3000 iterations with a cosine-decaying learning rate starting at . The decay allows the optimizer to make large early updates and then freeze the gains as it approaches convergence.
Results
Step Response Comparison

Orange is the initial controller (). With the incremental form and no integral action, the controller accumulates a small control output on the first step (proportional kick from the setpoint step) and then only reacts to changes in the output — there is no mechanism to correct the residual position error once the output stops moving, so the response oscillates slowly without converging to the setpoint.
Blue is the optimised controller (). The step response rises smoothly, reaching the setpoint at around with zero overshoot, matching the desired critically damped response. Damping from the derivative term prevents transient oscillations, while the integral action guarantees zero steady-state error.
Loss Curve
The loss drops by over two orders of magnitude over 3000 iterations.

The steepest descent happens in the first ~200 iterations when the learning rate is near its peak value. After roughly iteration 500 the curve flattens, indicating convergence. The cosine schedule ensures the gains freeze smoothly rather than continuing to drift.
Gain Trajectories

and grow first, establishing the necessary damping and proportional action to track the rise of the reference model. ramps up smoothly to eliminate the steady-state tracking error. All three curves flatten as the cosine schedule drives the learning rate toward zero.
Tracking Error
![]()
Orange (initial, ): the error never settles to zero due to the lack of integral action.
Blue (optimised): the tracking error relative to the reference model is driven to zero, resulting in a perfect match between the plant output and the desired critically damped response.
Control Effort

At , the setpoint step from to creates a proportional kick of . Because , the derivative term acts only on measurement changes, completely eliminating the setpoint-related derivative kick at . The control signal drops smoothly to , then rises to settle at (the spring force required to hold the mass at ), with no actuator saturation.
Practical Considerations
Setpoint weights as design parameters. and control the trade-off between setpoint tracking aggressiveness and smooth transients. softens the proportional response to reference steps. turns the derivative into pure derivative-on-measurement, removing any setpoint-related derivative kick entirely. These are typically tuned by the engineer rather than optimised, because gradient descent can satisfy them in unintended ways (e.g. by exploiting the interaction between and ).
Anti-windup time constant. governs how quickly the integrator is unwound when the actuator saturates. A small (aggressive reset) can introduce instability if the plant is slow to respond. A value in the range to the closed-loop settling time is a common practical starting point. Here as a loose default.
Simulation fidelity. Gradient-based tuning is only as good as your model. If the simulation drifts significantly from the real plant, the optimal gains may not transfer. Sim-to-real gap is the main risk.
Numerical stability. Long rollout horizons can cause gradient magnitudes to grow or shrink exponentially — the same instability that plagues RNN training. For stiff systems or long horizons, consider a smaller step size, a more stable integrator (e.g. RK4), or gradient clipping.
Local minima. The closed-loop simulation loss is generally non-convex in the gains. In practice the basin of attraction is large for common plant types and reasonable initial guesses, but running from several starting points builds confidence.
Takeaways
- You can treat PID gain tuning as a standard gradient-based optimisation problem by differentiating through a closed-loop simulation.
- JAX makes this straightforward: write the rollout as
jax.lax.scan, define the loss, calljax.grad. - The incremental form simplifies saturation and anti-windup handling: only the integral increment accumulates wind-up, and the accumulator state is transparent.
- Setpoint weighting (, ) decouples transient aggressiveness from steady-state performance. Fixing these as design constants and optimising only the gains avoids the optimizer finding degenerate solutions.
- The soft anti-windup pull-back keeps the integrator state smooth when the actuator saturates, without the discontinuity of hard clamping the increment.
- Gradient descent discovers control structure. Starting from a P-only controller, the optimiser finds on its own that integral action is needed — because without it the time-weighted loss is unavoidably large.
The Python script used to generate all plots is available if you want to run the experiments yourself.
Footnotes
-
J. Bradbury et al., “JAX: Composable transformations of Python+NumPy programs,” version 0.3.13, 2018. [Online]. Available: http://github.com/jax-ml/jax ↩
-
E. Sundström, M. Bauer, J. L. Guzmán, T. Hägglund, K. Soltesz, “A Practical Guide to PID Controller Implementation,” arXiv:2604.15918 [eess.SY], 2026. https://arxiv.org/abs/2604.15918 ↩