[pidcontroller] Add ability to limit the I-part (#12565)

* [pidcontroller] Add ability to limit the I-part

* Apply iMinValue & iMaxValue to the integral result accumulator

Signed-off-by: Fabian Wolter <github@fabian-wolter.de>

* Set iMinResult, iMaxResult default value to NaN

Co-authored-by: Lenno Nagel <lenno@nagel.ee>
This commit is contained in:
Fabian Wolter 2022-04-03 17:38:56 +02:00 committed by GitHub
parent 28ce7ebaed
commit 3520621c1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 75 additions and 22 deletions

View File

@ -24,7 +24,7 @@ Select the Item you like to control in the "Item Action" and leave the command e
### Trigger
This module triggers whenever the `input` or the `setpoint` changes or the `loopTime` expires.
Every trigger calculates the P, the I and the D part and sums them up to form the `output` value.
Every trigger calculates the P-, the I- and the D-part and sums them up to form the `output` value.
This is then transferred to the action module.
| Name | Type | Description | Required |
@ -35,18 +35,25 @@ This is then transferred to the action module.
| `ki` | Decimal | I: [Integral Gain](#integral-i-gain-parameter) Parameter | Y |
| `kd` | Decimal | D: [Derivative Gain](#derivative-d-gain-parameter) Parameter | Y |
| `kdTimeConstant` | Decimal | D-T1: [Derivative Gain Time Constant](#derivative-time-constant-d-t1-parameter) in sec. | Y |
| `commandItem` | String | Send a String "RESET" to this item to reset the I and the D part to 0. | N |
| `commandItem` | String | Send a String "RESET" to this item to reset the I- and the D-part to 0. | N |
| `loopTime` | Decimal | The interval the output value will be updated in milliseconds. Note: the output will also be updated when the input value or the setpoint changes. | Y |
| `pInspector` | Item | Name of the debug Item for the current P part | N |
| `iInspector` | Item | Name of the debug Item for the current I part | N |
| `dInspector` | Item | Name of the debug Item for the current D part | N |
| `integralMinValue` | Decimal | The I-part will be limited (min) to this value. | N |
| `integralMaxValue` | Decimal | The I-part will be limited (max) to this value. | N |
| `pInspector` | Item | Name of the debug Item for the current P-part | N |
| `iInspector` | Item | Name of the debug Item for the current I-part | N |
| `dInspector` | Item | Name of the debug Item for the current D-part | N |
| `eInspector` | Item | Name of the debug Item for the current regulation difference (error) | N |
The `loopTime` should be max a tenth of the system response.
E.g. the heating needs 10 min to heat up the room, the loop time should be max 1 min.
Lower values won't harm, but need more calculation resources.
You can view the internal P, I and D parts of the controller with the inspector Items.
The I-part can be limited via `integralMinValue`/`integralMaxValue`.
This is useful if the regulation cannot meet its setpoint from time to time.
E.g. a heating controller in the summer, which can not cool (min limit) or when the heating valve is already at 100% and the room is only slowly heating up (max limit).
When controlling a heating valve, reasonable values are 0% (min limit) and 100% (max limit).
You can view the internal P-, I- and D-parts of the controller with the inspector Items.
These values are useful when tuning the controller.
They are updated every time the output is updated.
@ -54,7 +61,7 @@ They are updated every time the output is updated.
Parameter: `kp`
A value of 0 disables the P part.
A value of 0 disables the P-part.
A value of 1 sets the output to the current setpoint deviation (error).
E.g. the setpoint is 25°C and the measured value is 20°C, the output will be set to 5.
@ -67,7 +74,7 @@ Parameter: `ki`
The purpose of this parameter is to let the output drift towards the setpoint.
The bigger this parameter, the faster the drifting.
A value of 0 disables the I part.
A value of 0 disables the I-part.
A value of 1 adds the current setpoint deviation (error) to the output each `loopTime` (in milliseconds).
E.g. (`loopTimeMs=1000`) the setpoint is 25°C and the measured value is 20°C, the output will be set to 5 after 1 sec.
@ -81,7 +88,7 @@ Parameter: `kd`
The purpose of this parameter is to react to sudden changes (e.g. an opened window) and also to damp the regulation.
This makes the regulation more resilient against oscillations, i.e. bigger `kp` and `ki` values can be set.
A value of 0 disables the D part.
A value of 0 disables the D-part.
A value of 1 sets the output to the difference between the last setpoint deviation (error) and the current.
E.g. the setpoint is 25°C and the measured value is 20°C (error=5°C).
@ -91,13 +98,13 @@ When the temperature drops to 10°C due to an opened window (error=15°C), the o
Parameter: `kdTimeConstant`
The purpose of this parameter is to slow down the impact of the D part.
The purpose of this parameter is to slow down the impact of the D-part.
This parameter behaves like a [low-pass](https://en.wikipedia.org/wiki/Low-pass_filter) filter.
The D part will become 63% of its actual value after `kdTimeConstant` seconds and 99% after 5 times `kdTimeConstant`. E.g. `kdTimeConstant` is set to 10s, the D part will become 99% after 50s.
The D-part will become 63% of its actual value after `kdTimeConstant` seconds and 99% after 5 times `kdTimeConstant`. E.g. `kdTimeConstant` is set to 10s, the D-part will become 99% after 50s.
Higher values lead to a longer lasting impact of the D part (stretching) after a change in the setpoint deviation (error).
The "stretching" also results in a lower amplitude, i.e. if you increase this value, you might want to also increase `kd` to keep the height of the D part at the same level.
Higher values lead to a longer lasting impact of the D-part (stretching) after a change in the setpoint deviation (error).
The "stretching" also results in a lower amplitude, i.e. if you increase this value, you might want to also increase `kd` to keep the height of the D-part at the same level.
## Tuning
@ -108,7 +115,7 @@ This results in quite reasonable working systems in most cases.
So, this will be described in the following.
To be able to proceed with this method, you need to visualize the input and the output value of the PID controller over time.
It's also good to visualize the individual P, I and D parts (these are forming the output value) via the inspector items.
It's also good to visualize the individual P-, I- and D-parts (these are forming the output value) via the inspector items.
The visualization could be done by adding a persistence and use Grafana for example.
After you added a [Rule](https://www.openhab.org/docs/configuration/rules-dsl.html) with above trigger and action module and configured those, proceed with the following steps:
@ -121,7 +128,7 @@ E.g. the time it takes from opening the heater valve and seeing an effect of the
3. Decrease `kp` a bit, that the system doesn't oscillate anymore
4. Repeat the two steps for the `ki` parameter (keep `kp` set)
5. Repeat the two steps for the `kd` parameter (keep `kp` and `ki` set)
6. As the D part acts as a damper, you should now be able to increase `kp` and `ki` further without resulting in oscillations
6. As the D-part acts as a damper, you should now be able to increase `kp` and `ki` further without resulting in oscillations
After each modification of above parameters, test the system response by introducing a setpoint deviation (error).
This can be done either by changing the setpoint (e.g. 20°C -> 25°C) or by forcing the measured value to change (e.g. by opening a window).

View File

@ -32,6 +32,8 @@ public class PIDControllerConstants {
public static final String CONFIG_KI_GAIN = "ki";
public static final String CONFIG_KD_GAIN = "kd";
public static final String CONFIG_KD_TIMECONSTANT = "kdTimeConstant";
public static final String CONFIG_I_MAX = "integralMaxValue";
public static final String CONFIG_I_MIN = "integralMinValue";
public static final String P_INSPECTOR = "pInspector";
public static final String I_INSPECTOR = "iInspector";
public static final String D_INSPECTOR = "dInspector";

View File

@ -34,12 +34,27 @@ class PIDController {
private double ki;
private double kd;
private double derivativeTimeConstantSec;
private double iMinResult;
private double iMaxResult;
public PIDController(double kpAdjuster, double kiAdjuster, double kdAdjuster, double derivativeTimeConstantSec) {
public PIDController(double kpAdjuster, double kiAdjuster, double kdAdjuster, double derivativeTimeConstantSec,
double iMinValue, double iMaxValue) {
this.kp = kpAdjuster;
this.ki = kiAdjuster;
this.kd = kdAdjuster;
this.derivativeTimeConstantSec = derivativeTimeConstantSec;
this.iMinResult = Double.NaN;
this.iMaxResult = Double.NaN;
// prepare min/max for the integral result accumulator
if (Double.isFinite(kiAdjuster) && Math.abs(kiAdjuster) > 0.0) {
if (Double.isFinite(iMinValue)) {
this.iMinResult = iMinValue / kiAdjuster;
}
if (Double.isFinite(iMaxValue)) {
this.iMaxResult = iMaxValue / kiAdjuster;
}
}
}
public PIDOutputDTO calculate(double input, double setpoint, long lastInvocationMs, int loopTimeMs) {
@ -55,11 +70,20 @@ class PIDController {
// integral calculation
integralResult += error * lastInvocationMs / loopTimeMs;
if (Double.isFinite(iMinResult)) {
integralResult = Math.max(integralResult, iMinResult);
}
if (Double.isFinite(iMaxResult)) {
integralResult = Math.min(integralResult, iMaxResult);
}
// calculate parts
final double proportionalPart = kp * error;
final double integralPart = ki * integralResult;
double integralPart = ki * integralResult;
final double derivativePart = kd * derivativeResult;
output = proportionalPart + integralPart + derivativePart;
return new PIDOutputDTO(output, proportionalPart, integralPart, derivativePart, error);

View File

@ -16,7 +16,6 @@ import static org.openhab.automation.pidcontroller.internal.PIDControllerConstan
import java.math.BigDecimal;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@ -108,6 +107,8 @@ public class PIDControllerTriggerHandler extends BaseTriggerModuleHandler implem
double kiAdjuster = getDoubleFromConfig(config, CONFIG_KI_GAIN);
double kdAdjuster = getDoubleFromConfig(config, CONFIG_KD_GAIN);
double kdTimeConstant = getDoubleFromConfig(config, CONFIG_KD_TIMECONSTANT);
double iMinValue = getDoubleFromConfig(config, CONFIG_I_MIN);
double iMaxValue = getDoubleFromConfig(config, CONFIG_I_MAX);
pInspector = (String) config.get(P_INSPECTOR);
iInspector = (String) config.get(I_INSPECTOR);
dInspector = (String) config.get(D_INSPECTOR);
@ -116,7 +117,7 @@ public class PIDControllerTriggerHandler extends BaseTriggerModuleHandler implem
loopTimeMs = ((BigDecimal) requireNonNull(config.get(CONFIG_LOOP_TIME), CONFIG_LOOP_TIME + " is not set"))
.intValue();
controller = new PIDController(kpAdjuster, kiAdjuster, kdAdjuster, kdTimeConstant);
controller = new PIDController(kpAdjuster, kiAdjuster, kdAdjuster, kdTimeConstant, iMinValue, iMaxValue);
eventFilter = event -> {
String topic = event.getTopic();
@ -146,7 +147,13 @@ public class PIDControllerTriggerHandler extends BaseTriggerModuleHandler implem
}
private double getDoubleFromConfig(Configuration config, String key) {
return ((BigDecimal) Objects.requireNonNull(config.get(key), key + " is not set")).doubleValue();
Object rawValue = config.get(key);
if (rawValue == null) {
return Double.NaN;
}
return ((BigDecimal) rawValue).doubleValue();
}
private void calculate() {
@ -184,7 +191,8 @@ public class PIDControllerTriggerHandler extends BaseTriggerModuleHandler implem
if (itemName != null) {
try {
itemRegistry.getItem(itemName);
eventPublisher.post(ItemEventFactory.createCommandEvent(itemName, new DecimalType(value)));
eventPublisher.post(ItemEventFactory.createStateEvent(itemName,
Double.isFinite(value) ? new DecimalType(value) : UnDefType.UNDEF));
} catch (ItemNotFoundException e) {
logger.warn("Item doesn't exist: {}", itemName);
}

View File

@ -83,7 +83,7 @@ public class PIDControllerTriggerType extends TriggerType {
.withMinimum(BigDecimal.ZERO) //
.withDefault("1.0") //
.withLabel("Derivative Time Constant") //
.withDescription("Slows the rate of change of the D part (T1) in seconds.") //
.withDescription("Slows the rate of change of the D-part (T1) in seconds.") //
.withUnit("s") //
.build());
configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_LOOP_TIME, Type.DECIMAL) //
@ -94,6 +94,18 @@ public class PIDControllerTriggerType extends TriggerType {
.withDescription("The interval the output value is updated in ms") //
.withUnit("ms") //
.build());
configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_I_MIN, Type.DECIMAL) //
.withRequired(false) //
.withMultiple(false) //
.withLabel("I-part Lower Limit") //
.withDescription("The I-part will be min this value. Can be left empty for no limit.") //
.build());
configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_I_MAX, Type.DECIMAL) //
.withRequired(false) //
.withMultiple(false) //
.withLabel("I-part Upper Limit") //
.withDescription("The I-part will be max this value. Can be left empty for no limit.") //
.build());
configDescriptions.add(ConfigDescriptionParameterBuilder.create(P_INSPECTOR, Type.TEXT) //
.withRequired(false) //
.withMultiple(false) //