PID tuning 'subreddit'?

It’s my understanding that two independent actuators are at work in the current generation of BrewPi, with one actuator in control at a given time (presumably the one deemed most in need of taking control, and dead time between hand-offs). Optimizing actuator behavior involves making tuning adjustments to the params in any of three PIDs. This could imply a fair amount of trial-and-error.

Indeed it seems that a lot of activity on this community site deals with asking for help tuning PID settings, with Elco as a frequent solver on such threads. Personally I’d like to see Elco spend more time coding and creating products, and so I’ve been trying to do a little self-help and diagnosis, using external information like The Idiot’s Guide to the PID Algorithm.

Proposed: set up a new category in the forum specifically around PID Tuning, and people can gravitate to the sub-thread which most closely resembles their own config. For example I use BrewPi with a 1-bbl conical fermenter inside an upright freezer, which also houses a (likely under-powered) ceramic heater. Presumably my needs are significantly different from those of a person using BrewPi for a 5-gallon HERMS mash or a 1-gallon sous-vide.

p.s. I note that the author of The Idiot’s Guide to the PID Algorithm also sells a more expensive PID Tuning Blueprint. I’m curious if anybody on this forum has successfully used that particular blueprint, or anything like it, to tune their BrewPi. (note: I am not affiliated in any way with the author of that blueprint)

1 Like

I think an article on the wiki will be best. With examples of wrong scenarios and the resulting graph.
As part of our development process, we simulate a fermentation and a mashing scenario. That might be an easy way to generate all the scenarios.

I think we could come up with a routine that works for all. The right approach is to first tune the heater and cooler PID separately and then the beer-to-fridge-setting PID.

For mashing, the right parameters can be mostly calculated from your kettle volumes.

If you can help me write it, that would be great.

That books seems really overpriced.

Yes, I am a bit curious what I’d get if I paid $97 for that PID tuning manual. If it helped me optimize my freezer setup I might just go for it since I like to read that kind of stuff.

To that end I’ve been trying to solve my PID problems without advertising them to the whole community (with you being the most likely solver). I figure it helps me learn & apply later if I solve on my own, but it does involve a fair amount of tweaking and observing effects.

One thing I would like to take on in the near term is some copy editing of the “Advanced Settings” page, as I have a fair amount of writing experience. For example “Beer-to-Fridge Derivative filter delay time” is currently explained this way:

Input to the differential gain is filtered, to prevent bit flips from causing a high derivative. This causes a delay, because of the moving average. More delay means more filtering.

But I think it should instead say:

Input to the differential gain is smoothed / filtered, to prevent sensor bit flips from causing a high input derivative. Smoothing uses a moving average; a long delay time implies a slower reaction of the Beer-to-Fridge PID, while a short delay time may cause fluctuations in the fridge setting and unpredictable actuator activity.

(however I’m not sure if that is accurately described. What I will freely admit is that your English is way better than my Dutch)

Indeed, much better. I haven’t given the text on that page that much thought, I was more focused on code. I would change it to this to be more correct:

It’s not technically moving average, but I don’t think we should go into the details of filtering.

But I think the best approach is to show sample graphs of wrong settings on the wiki and the effect that they have. A more visual approach will be easier to understand.

3 Likes

I am trying to tune one PID at a time so that I can possibly co-author a wiki article on tuning. I’m starting with the heater1 PID, so I set Kp, Ti, Td all = 0 for the cooler PID. I’ve also set Ti and Td = 0 for heater1 so that I can optimize Kp (trying to find out the ultimate gain) before tweaking the integral and derivative parameters. This is basically a manual version of Ziegler-Nichols tuning.

I’ve been pleasantly surprised by how well the beer temp approaches the set point when just the P part of the PID is enabled. But what I was definitely not expecting was to check on my Advanced Settings and see that heater1 Kp had been changed, presumably by code, to a negative number. If I manually reset it to the presumptive value of ultimate gain, it reverts to a negative value again. A search over this forum didn’t provide an explanation.

By the way, I recommend that people read this Wikipedia article if they’re new to PID tuning: https://en.wikipedia.org/wiki/PID_controller#Manual_tuning

What is the value you are setting it to? If you look at the value in the JSON on the control algorithm tab, do you get the same value? If you give me the steps to reproduce it, I can give you a fix quickly.

I had Kp set to 100 when I first observed this, and have been setting it to other large positive numbers to see if there is some logic in the repro. (reason so high: I have a 30-watt mat heater trying to take on 40 gallons of water, admittedly underpowered, and I was trying to cause oscillations around the setpoint so that I could figure out ultimate gain).

Here is a screencast of a repro: https://goo.gl/2UK5pn. In the video I set Kp to 200, verify in the algorithm window and in the log, then come back to advanced settings. If I click “Update from Core” the value has gone negative again. When Kp = 200 the associated negative value = -56, and if Kp = 400 the negative value = -112 (so ironically the behavior is proportional!)

The JSON value in the Control Algorithm tab says the expected value, even when the Advanced Settings box shows a negative value:

{
  "kind": "Control",
  "pids": [
    {
      "kind": "Pid",
      "name": "heater1pid",
      "enabled": true,
      "input": {
        "kind": "SensorSetPointPair",
        "sensor": {
          "kind": "TempSensorFallback",
          "onBackupSensor": false,
          "sensor": {
            "kind": "TempSensorDelegate",
            "name": "fridge",
            "delegate": {
              "kind": "OneWireTempSensor",
              "value": 22.1875,
              "connected": true,
              "address": "28A6A273060000B1",
              "calibrationOffset": 0
            }
          }
        },
        "setPoint": {
          "kind": "SetPointSimple",
          "name": "fridgeset",
          "value": 32.2227
        }
      },
      "output": {
        "kind": "ActuatorPwm",
        "dutySetting": 100,
        "period": 4,
        "minVal": 0,
        "maxVal": 100,
        "target": {
          "kind": "ActuatorMutexDriver",
          "mutexGroup": {
            "kind": "ActuatorMutexGroup",
            "deadTime": 1800000,
            "waitTime": 1799237
          },
          "target": {
            "kind": "ActuatorDigitalDelegate",
            "name": "heater1",
            "delegate": {
              "kind": "ActuatorPin",
              "state": true,
              "pin": 11,
              "invert": false
            }
          }
        }
      },
      "inputError": -10.0391,
      "Kp": 100,
      "Ti": 0,
      "Td": 0,
      "p": 1003.9063,
      "i": 0,
      "d": 0,
      "actuatorIsNegative": false
    },
    {
      "kind": "Pid",
      "name": "heater2pid",
      "enabled": true,
      "input": {
        "kind": "SensorSetPointPair",
        "sensor": {
          "kind": "TempSensorDelegate",
          "name": "beer2",
          "delegate": {
            "kind": "TempSensorDisconnected",
            "value": null,
            "connected": false
          }
        },
        "setPoint": {
          "kind": "SetPointSimple",
          "name": "beer2set",
          "value": null
        }
      },
      "output": {
        "kind": "ActuatorPwm",
        "dutySetting": 0,
        "period": 4,
        "minVal": 0,
        "maxVal": 100,
        "target": {
          "kind": "ActuatorMutexDriver",
          "mutexGroup": {
            "kind": "ActuatorMutexGroup",
            "deadTime": 1800000,
            "waitTime": 1799159
          },
          "target": {
            "kind": "ActuatorDigitalDelegate",
            "name": "heater2",
            "delegate": {
              "kind": "ActuatorNop",
              "state": false
            }
          }
        }
      },
      "inputError": null,
      "Kp": 10,
      "Ti": 600,
      "Td": 60,
      "p": 0,
      "i": 0,
      "d": 0,
      "actuatorIsNegative": false
    },
    {
      "kind": "Pid",
      "name": "coolerpid",
      "enabled": true,
      "input": {
        "kind": "SensorSetPointPair",
        "sensor": {
          "kind": "TempSensorFallback",
          "onBackupSensor": false,
          "sensor": {
            "kind": "TempSensorDelegate",
            "name": "fridge",
            "delegate": {
              "kind": "OneWireTempSensor",
              "value": 22.1875,
              "connected": true,
              "address": "28A6A273060000B1",
              "calibrationOffset": 0
            }
          }
        },
        "setPoint": {
          "kind": "SetPointSimple",
          "name": "fridgeset",
          "value": 32.2227
        }
      },
      "output": {
        "kind": "ActuatorPwm",
        "dutySetting": 0,
        "period": 1200,
        "minVal": 0,
        "maxVal": 100,
        "target": {
          "kind": "ActuatorMutexDriver",
          "mutexGroup": {
            "kind": "ActuatorMutexGroup",
            "deadTime": 1800000,
            "waitTime": 1799071
          },
          "target": {
            "kind": "ActuatorTimeLimited",
            "minOnTime": 180,
            "minOffTime": 180,
            "maxOnTime": 65535,
            "state": false,
            "target": {
              "kind": "ActuatorDigitalDelegate",
              "name": "cooler",
              "delegate": {
                "kind": "ActuatorPin",
                "state": false,
                "pin": 16,
                "invert": false
              }
            }
          }
        }
      },
      "inputError": -10.0391,
      "Kp": 0,
      "Ti": 0,
      "Td": 0,
      "p": 0,
      "i": 0,
      "d": 0,
      "actuatorIsNegative": true
    },
    {
      "kind": "Pid",
      "name": "beer2fridgepid",
      "enabled": true,
      "input": {
        "kind": "SensorSetPointPair",
        "sensor": {
          "kind": "TempSensorDelegate",
          "name": "beer1",
          "delegate": {
            "kind": "OneWireTempSensor",
            "value": 18.5,
            "connected": true,
            "address": "28DF41A9080000C5",
            "calibrationOffset": 0
          }
        },
        "setPoint": {
          "kind": "SetPointSimple",
          "name": "beer1set",
          "value": 22.2227
        }
      },
      "output": {
        "kind": "ActuatorOffset",
        "target": {
          "kind": "SensorSetPointPair",
          "sensor": {
            "kind": "TempSensorFallback",
            "onBackupSensor": false,
            "sensor": {
              "kind": "TempSensorDelegate",
              "name": "fridge",
              "delegate": {
                "kind": "OneWireTempSensor",
                "value": 22.1875,
                "connected": true,
                "address": "28A6A273060000B1",
                "calibrationOffset": 0
              }
            }
          },
          "setPoint": {
            "kind": "SetPointSimple",
            "name": "fridgeset",
            "value": 32.2227
          }
        },
        "reference": {
          "kind": "SensorSetPointPair",
          "sensor": {
            "kind": "TempSensorDelegate",
            "name": "beer1",
            "delegate": {
              "kind": "OneWireTempSensor",
              "value": 18.5,
              "connected": true,
              "address": "28DF41A9080000C5",
              "calibrationOffset": 0
            }
          },
          "setPoint": {
            "kind": "SetPointSimple",
            "name": "beer1set",
            "value": 22.2227
          }
        },
        "useReferenceValue": false,
        "setting": 10,
        "achieved": -0.0352,
        "minimum": -10,
        "maximum": 10
      },
      "inputError": -3.7266,
      "Kp": 5,
      "Ti": 14400,
      "Td": 3600,
      "p": 18.6328,
      "i": 0,
      "d": -1.5039,
      "actuatorIsNegative": false
    }
  ]
}

Thank you for the bug report. I think I know what causes this.
It is a long fixed point variable, but when it is queried from the web interface it is interpreted as a short fixed point and not printed correctly. 129 overflows to -127.

The value that is used by the algorithm is actually 200 and this is also stored correctly between reboots. It might be an issue though when settings are read and restored during a firmware update.

I have fixed this bug in 0.5.2 (now online).