Skip to content

Instantly share code, notes, and snippets.

@mstksg
Last active August 29, 2015 14:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mstksg/25faeca4a427872f1f02 to your computer and use it in GitHub Desktop.
Save mstksg/25faeca4a427872f1f02 to your computer and use it in GitHub Desktop.
Comparison of PID controller implementation approaches, and a case for a compositional, denotative, locally stateful style

PID Controller: A comparison of approaches

And a case for a compositional, denotative, and locally stateful style.

High Level Description

  1. The (stateful) system takes a control and produces a response.
  2. The goal is the desired response.
  3. The error is the difference between the goal and the response.
  4. The integral is the cumulative sum of all recorded errors.
  5. The derivative is the change in error at each step.
  6. The control is the cumulative sum of all three terms, each multipled by a tuning factor (kp, ki, and kd, respectively).

Imperative Approach

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!)

Haskell "Imperative" Approach

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.

Denotative and Compositional

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?".

Auto

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!)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment