PID Controller: A comparison of approaches
And a case for a compositional, denotative, and locally stateful style.
- The (stateful) system takes a control and produces a response.
- The goal is the desired response.
- The error is the difference between the goal and the response.
- The integral is the cumulative sum of all recorded errors.
- The derivative is the change in error at each step.
- The control is the cumulative sum of all three terms, each multipled by a tuning factor (kp, ki, and kd, respectively).
in C:
double control, response, last_response, err;
double integral, derivative, p, i, d;
response = adjust_system(initial_control);
last_response = response;
integral = 0;
control = initial_control;
while (TRUE) {
response = adjust_system(control);
err = goal - response;
integral += err;
derivative = response - last_response;
last_response = response;
p = kp * err;
i = ki * integral;
d = kd * derivative;
control += p + i + d
}
where we have double adjust_system(double control)
. Might not actually be
IO; could just be a deterministic function on some global state.
We assume initial_control
, goal
, kp
, ki
, and kd
are predefined
constants for all of these examples.
Note the translation from the high level "description" of the process to an
imperative "procedure". Note also that going backwards is considerably
harder; there is no way to really tell that integral
is a cumulative sum
unless you check the entire scope to make sure you understand everything that
could possibly be affecting it. Comments are nice, but can be deceiving (and
are, of course, not enforceable by a compiler!)
We might have
adjustSystem :: Double -> State SystemState Double
But we also have a PID state too. So. We have to write a conversion function
State SystemState a -> State PIDState a
; zoom
from the lens library gives
us this "for free" if PIDState
is an ADT.
data PIDState = PIDState { integral :: Double
, lastResponse :: Double
, currControl :: Double
, systemState :: SystemState
}
liftSystemState :: State SystemState a -> State PIDState a
liftsystemState s = do
(res, sysState') <- runState s <$> gets systemState
modify $ \s -> s { systemState = sysState' }
return res
pidLoop :: State PIDState a
pidLoop = do
control <- gets currControl
response <- liftSystemState $ adjustSystem control
let err = goal - response
currIntegral <- (+ err) <$> gets integral
currDeriv <- (response -) <$> gets lastResponse
let p = kp * err
i = ki * currIntegral
d = kd * currDeriv
newControl = control + p + i + d
modify $ \s -> s { integral = currIntegral
, lastResponse = response
, currControl = newControl
}
doStuffWithResponse response
pidLoop
Yeah...this is all pretty ugly. And if we took full advantage of lens, the result would be basically not too different from the C version. So many things messy with this; and using different state types is just awkward and is potentially unscalable.
Also, it's kind of silly that at any point you can modify the SystemState
.
Here, we have system :: Auto' Double Double
, which is an arrow from
Double
(the control) to Double
(the response) which keeps track of its own
state.
pidController :: Auto' () Double
pidController = proc _ -> do
rec response <- system -< control
let err = goal - response
integral <- sumFrom 0 -< err
derivative <- deltas -< response
let p = kp * err
i = ki * integral
d = kd * fromMaybe 0 derivative
control <- sumFromD initialControl -< p + i + d
id -< response
Compare this to the original high level description.
Note how it perfectly captures the "feedback" nature of the description.
response
depends on control
, which depends on response
. The two are
mutually recursively defined. And this really captures it, I feel. The
description is inherently feedbacky and recursive, and so is the
denotative declaration!
Note that the idea of "state" is abstracted away; there is no SystemState
that you can touch, and you can't arbitrary modify running sums of integral
.
We aren't dealing with "what happens when" and explicit state. State only
exists as is necessary to really capture what "sumFrom 0
" means. What it
says. You're describing a definition of the system, a description of
what everything means. Not "this then that", but "how does this quantity
relate to that one...forever in time and always?".
The Auto
type comes from my upcoming library, auto (working title), which doesn't have a
release version yet. This post was mostly to demonstrate a couple of ideas of
local statefulness and denotative & compositional style...to sort of motivate
the ideas before the release of the library :) Note that most of this can be
implemented almost verbatim in the current library netwire, albeit in
continuous time (so even better!)