All posts

The Espresso in the Algorithm: How the Zone 2 PID Works

I am, by any reasonable definition, a coffee nerd. I weigh my beans to the tenth of a gram. I know my group head holds temperature better than the cheap machine it replaced. And the reason that better machine pulls a better shot comes down to four letters that should be familiar to anyone who's gone down the espresso rabbit hole: PID.

A budget espresso machine controls its boiler with a simple thermostat — heat full-blast until it's too hot, switch off until it's too cold, repeat. The temperature saws up and down by several degrees, and your shot tastes different every time. A good machine runs a PID controller instead, holding the water within a fraction of a degree of, say, 93°C. Same boiler, same heating element — vastly better coffee. The magic isn't more power. It's control.

When I set out to keep a rider's heart rate pinned in Zone 2, I realised I'd already met this problem. Holding a heart rate is holding a temperature. So Justzone2's zone targeting is, at its heart, the same controller that's in a nice espresso machine — pointed at your cardiovascular system instead of a boiler.

What a PID actually does (the coffee version)

A PID controller has one job: drive a measured value (the process variable) to a target (the setpoint) and hold it there. It does this by watching the error — the gap between where you are and where you want to be — and reacting in three ways:

P — Proportional. React to the error right now. Boiler 8° too cold? Push hard. 1° off? Ease up. The bigger the gap, the bigger the response.

I — Integral. React to the error that won't go away. If your machine stubbornly sits 2° cool no matter what P does, the integral term quietly accumulates that persistent miss and trims it out. It's what kills the steady-state offset.

D — Derivative. React to how fast the error is changing. Temperature rocketing up toward the target? Start backing off early, before you overshoot and scorch the shot.

Now swap the boiler for a rider. The setpoint is the middle of your Zone 2 heart-rate band. The process variable is your live heart rate. And the "heating element" is the target power we send to the Wahoo KICKR over Bluetooth — more watts drives your heart rate up, fewer lets it drift down.

Zone 2 target setpoint (bpm) Σ error PI controller Kp·e + Ki·∫e watts KICKR + you the "boiler" HR measured heart rate (sensor feedback)
The loop, once per second: compare target to measured HR, compute a power correction, send it to the KICKR, measure the new HR, repeat. Identical in shape to the loop holding your espresso boiler at temperature.

The core, in about five lines

Strip away the safety rails and the controller is genuinely small. Every second we read the heart rate, compute the error against the setpoint, and turn it into a wattage to add to (or subtract from) your target power:

output = Kp · e + Ki · ∫ e dt     where  e = target − HR
// runs once per second (1 Hz) let error = setpoint - smoothedHR // bpm, +ve ⇒ HR too low integral += gradientFactor * error * dt // accumulate (anti-windup clamped) let output = Kp * error + Ki * integral // watts vs. your target // slew-limit + clamp to ±30 W, then drive the trainer kickr.setTargetPower(targetPower + output)

A positive error means your heart rate is below the zone, so we add watts. Negative means you're above it, so we back off. The proportional term does the bulk of the work; the integral term mops up the slow, stubborn drift — exactly like the boiler that always runs a touch cool.

The real values, all tuned on actual rides:

ParameterValueWhat it's for
Kp0.5 W/bpmProportional gain — react to the error now
Ki0.008 W/(bpm·s)Integral gain — erase steady-state drift
setpointmid Zone 2Midpoint of your HR band
HR smoothing45 s averageKill beat-to-beat noise before it hits the loop
output clamp±30 WAssist your target — never hijack it
grace period180 sLet HR settle before the loop engages
sample rate1 HzOne correction per second

Where it stops being a textbook PID

Here's the confession the title buried: there's no D in this PID. And the reason is pure coffee-nerd pedantry about signal quality.

A derivative term reacts to the rate of change of the error. That's brilliant when your sensor is clean — but a heart-rate signal is anything but. It jitters beat to beat, a strap shifts, a packet drops over Bluetooth and you get a phantom 20-bpm spike. Feed that into a derivative term and it amplifies every bit of that noise, yanking the power up and down on garbage data. Coffee people know the equivalent: a noisy thermocouple makes a D-heavy controller chatter the heater. The cure in both worlds is the same — don't differentiate noise.

So instead of a classic derivative, we do something gentler. We keep a heavily smoothed view of your heart rate — a 300-second exponential moving average — and look at the trend of that slow signal: is your HR drifting up, holding, or falling? That trend (in bpm per minute) becomes a gradient factor that schedules the gain:

gradientFactor = clamp( 1 − HRtrend / 2 bpm·min⁻¹ , 0…1 )

If your heart rate is already climbing — the classic cardiac drift of a long session — the gradient factor shrinks toward zero and we stop adding power, because the last thing a rising heart rate needs is more watts. It's a derivative's anticipation without a derivative's noise: feedforward damping from a signal slow enough to trust.

You can't un-burn a shot

The other place coffee instinct shaped the code: asymmetry. Over-extracting an espresso is unrecoverable — once it's bitter, it's bitter — so a careful barista creeps up on the target and bails the moment it tips. The controller treats your heart rate the same way. Pushing HR too high is the costly direction: it kicks you out of Zone 2, spikes fatigue, and ruins the whole point of the session. So the loop is deliberately lopsided:

DirectionMax rateWhy
add power+5 W / 90 sCreep up cautiously — never chase HR over the line
shed power−5 W / 45 sRetreat twice as fast when HR runs hot

Quick to back off, slow to push — and that increase rate is itself throttled by the gradient factor, so a rising heart rate makes the controller even more reluctant to add load. The result is a power line that eases rather than lurches.

ZONE 2 180 s grace Heart rate Power (KICKR) time →
Power leads, heart rate follows. The controller pushes early to lift HR into the band, then trims the watts as you approach — easing in with no overshoot, then holding through cardiac drift.

The rails that keep it honest

Two more safeguards, both with espresso cousins. Anti-windup: the integral term is clamped so it can't silently accumulate a huge correction during the grace period or a sensor dropout and then dump it all at once — the controller equivalent of a boiler that forgets it's already been heating for a minute. And output clamping: the whole correction is bounded to ±30 W of the power you chose. The app assists your target; it never seizes control of the workout. There's a hard floor at 50 W so it can never stall you out.

Tuning is just dialling in

Every coffee nerd has dialled in a new bag: tweak one variable, taste, adjust, repeat. Tuning these gains was the same loop. Too much proportional gain and the system hunts — power and heart rate oscillating around the target like a machine overshooting its temperature every cycle. Too much integral and it lurches: a slow, ponderous overshoot as the accumulated term overcorrects. I landed deliberately conservative, because a Zone 2 ride is long and the human body is a laggy, slow-responding plant — a big thermal mass, basically. Better smooth and patient than snappy and twitchy.

The best control systems are invisible. You don't think about the PID in your espresso machine — you just get a good shot.

That's the whole goal here too. You set a power target, switch on zone targeting, and forget the controller exists. It sits in the background a thousand corrections deep, creeping up and easing off, holding you in the zone while you get lost in a podcast. Just like a good espresso machine: all that careful control, entirely so you never have to think about it.

Now if you'll excuse me, my next shot is calling — and I've got a base-building hour to spin first.

Let the controller hold the line.

Set your power, flip on zone targeting, and a thousand tiny corrections a session keep your heart rate exactly where the adaptations happen.

Download on the App Store