mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-25 14:55:55 +01:00
[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
|
||||
|
||||
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).
|
||||
|
@ -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";
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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) //
|
||||
|
Loading…
Reference in New Issue
Block a user