[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:
parent
28ce7ebaed
commit
3520621c1b
@ -24,7 +24,7 @@ Select the Item you like to control in the "Item Action" and leave the command e
|
|||||||
### Trigger
|
### Trigger
|
||||||
|
|
||||||
This module triggers whenever the `input` or the `setpoint` changes or the `loopTime` expires.
|
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.
|
This is then transferred to the action module.
|
||||||
|
|
||||||
| Name | Type | Description | Required |
|
| 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 |
|
| `ki` | Decimal | I: [Integral Gain](#integral-i-gain-parameter) Parameter | Y |
|
||||||
| `kd` | Decimal | D: [Derivative Gain](#derivative-d-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 |
|
| `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 |
|
| `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 |
|
| `integralMinValue` | Decimal | The I-part will be limited (min) to this value. | N |
|
||||||
| `iInspector` | Item | Name of the debug Item for the current I part | N |
|
| `integralMaxValue` | Decimal | The I-part will be limited (max) to this value. | N |
|
||||||
| `dInspector` | Item | Name of the debug Item for the current D part | 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 |
|
| `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.
|
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.
|
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.
|
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.
|
These values are useful when tuning the controller.
|
||||||
They are updated every time the output is updated.
|
They are updated every time the output is updated.
|
||||||
|
|
||||||
@ -54,7 +61,7 @@ They are updated every time the output is updated.
|
|||||||
|
|
||||||
Parameter: `kp`
|
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).
|
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.
|
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 purpose of this parameter is to let the output drift towards the setpoint.
|
||||||
The bigger this parameter, the faster the drifting.
|
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).
|
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.
|
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.
|
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.
|
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.
|
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).
|
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`
|
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.
|
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).
|
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.
|
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
|
## Tuning
|
||||||
|
|
||||||
@ -108,7 +115,7 @@ This results in quite reasonable working systems in most cases.
|
|||||||
So, this will be described in the following.
|
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.
|
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.
|
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:
|
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
|
3. Decrease `kp` a bit, that the system doesn't oscillate anymore
|
||||||
4. Repeat the two steps for the `ki` parameter (keep `kp` set)
|
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)
|
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).
|
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).
|
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).
|
||||||
|
|||||||
@ -32,6 +32,8 @@ public class PIDControllerConstants {
|
|||||||
public static final String CONFIG_KI_GAIN = "ki";
|
public static final String CONFIG_KI_GAIN = "ki";
|
||||||
public static final String CONFIG_KD_GAIN = "kd";
|
public static final String CONFIG_KD_GAIN = "kd";
|
||||||
public static final String CONFIG_KD_TIMECONSTANT = "kdTimeConstant";
|
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 P_INSPECTOR = "pInspector";
|
||||||
public static final String I_INSPECTOR = "iInspector";
|
public static final String I_INSPECTOR = "iInspector";
|
||||||
public static final String D_INSPECTOR = "dInspector";
|
public static final String D_INSPECTOR = "dInspector";
|
||||||
|
|||||||
@ -34,12 +34,27 @@ class PIDController {
|
|||||||
private double ki;
|
private double ki;
|
||||||
private double kd;
|
private double kd;
|
||||||
private double derivativeTimeConstantSec;
|
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.kp = kpAdjuster;
|
||||||
this.ki = kiAdjuster;
|
this.ki = kiAdjuster;
|
||||||
this.kd = kdAdjuster;
|
this.kd = kdAdjuster;
|
||||||
this.derivativeTimeConstantSec = derivativeTimeConstantSec;
|
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) {
|
public PIDOutputDTO calculate(double input, double setpoint, long lastInvocationMs, int loopTimeMs) {
|
||||||
@ -55,11 +70,20 @@ class PIDController {
|
|||||||
|
|
||||||
// integral calculation
|
// integral calculation
|
||||||
integralResult += error * lastInvocationMs / loopTimeMs;
|
integralResult += error * lastInvocationMs / loopTimeMs;
|
||||||
|
if (Double.isFinite(iMinResult)) {
|
||||||
|
integralResult = Math.max(integralResult, iMinResult);
|
||||||
|
}
|
||||||
|
if (Double.isFinite(iMaxResult)) {
|
||||||
|
integralResult = Math.min(integralResult, iMaxResult);
|
||||||
|
}
|
||||||
|
|
||||||
// calculate parts
|
// calculate parts
|
||||||
final double proportionalPart = kp * error;
|
final double proportionalPart = kp * error;
|
||||||
final double integralPart = ki * integralResult;
|
|
||||||
|
double integralPart = ki * integralResult;
|
||||||
|
|
||||||
final double derivativePart = kd * derivativeResult;
|
final double derivativePart = kd * derivativeResult;
|
||||||
|
|
||||||
output = proportionalPart + integralPart + derivativePart;
|
output = proportionalPart + integralPart + derivativePart;
|
||||||
|
|
||||||
return new PIDOutputDTO(output, proportionalPart, integralPart, derivativePart, error);
|
return new PIDOutputDTO(output, proportionalPart, integralPart, derivativePart, error);
|
||||||
|
|||||||
@ -16,7 +16,6 @@ import static org.openhab.automation.pidcontroller.internal.PIDControllerConstan
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -108,6 +107,8 @@ public class PIDControllerTriggerHandler extends BaseTriggerModuleHandler implem
|
|||||||
double kiAdjuster = getDoubleFromConfig(config, CONFIG_KI_GAIN);
|
double kiAdjuster = getDoubleFromConfig(config, CONFIG_KI_GAIN);
|
||||||
double kdAdjuster = getDoubleFromConfig(config, CONFIG_KD_GAIN);
|
double kdAdjuster = getDoubleFromConfig(config, CONFIG_KD_GAIN);
|
||||||
double kdTimeConstant = getDoubleFromConfig(config, CONFIG_KD_TIMECONSTANT);
|
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);
|
pInspector = (String) config.get(P_INSPECTOR);
|
||||||
iInspector = (String) config.get(I_INSPECTOR);
|
iInspector = (String) config.get(I_INSPECTOR);
|
||||||
dInspector = (String) config.get(D_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"))
|
loopTimeMs = ((BigDecimal) requireNonNull(config.get(CONFIG_LOOP_TIME), CONFIG_LOOP_TIME + " is not set"))
|
||||||
.intValue();
|
.intValue();
|
||||||
|
|
||||||
controller = new PIDController(kpAdjuster, kiAdjuster, kdAdjuster, kdTimeConstant);
|
controller = new PIDController(kpAdjuster, kiAdjuster, kdAdjuster, kdTimeConstant, iMinValue, iMaxValue);
|
||||||
|
|
||||||
eventFilter = event -> {
|
eventFilter = event -> {
|
||||||
String topic = event.getTopic();
|
String topic = event.getTopic();
|
||||||
@ -146,7 +147,13 @@ public class PIDControllerTriggerHandler extends BaseTriggerModuleHandler implem
|
|||||||
}
|
}
|
||||||
|
|
||||||
private double getDoubleFromConfig(Configuration config, String key) {
|
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() {
|
private void calculate() {
|
||||||
@ -184,7 +191,8 @@ public class PIDControllerTriggerHandler extends BaseTriggerModuleHandler implem
|
|||||||
if (itemName != null) {
|
if (itemName != null) {
|
||||||
try {
|
try {
|
||||||
itemRegistry.getItem(itemName);
|
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) {
|
} catch (ItemNotFoundException e) {
|
||||||
logger.warn("Item doesn't exist: {}", itemName);
|
logger.warn("Item doesn't exist: {}", itemName);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,7 +83,7 @@ public class PIDControllerTriggerType extends TriggerType {
|
|||||||
.withMinimum(BigDecimal.ZERO) //
|
.withMinimum(BigDecimal.ZERO) //
|
||||||
.withDefault("1.0") //
|
.withDefault("1.0") //
|
||||||
.withLabel("Derivative Time Constant") //
|
.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") //
|
.withUnit("s") //
|
||||||
.build());
|
.build());
|
||||||
configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_LOOP_TIME, Type.DECIMAL) //
|
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") //
|
.withDescription("The interval the output value is updated in ms") //
|
||||||
.withUnit("ms") //
|
.withUnit("ms") //
|
||||||
.build());
|
.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) //
|
configDescriptions.add(ConfigDescriptionParameterBuilder.create(P_INSPECTOR, Type.TEXT) //
|
||||||
.withRequired(false) //
|
.withRequired(false) //
|
||||||
.withMultiple(false) //
|
.withMultiple(false) //
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user