blob: 601ed46700dae8b7d04c15615c23db900dcb5c47 [file]
#include "tlbmc/thermal/controller/algorithm/pid.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include "absl/log/log.h"
#include "absl/strings/str_format.h"
namespace milotic_tlbmc {
namespace thermal {
// The equations are based on the following (i.e., PID with feedforward):
// https://apmonitor.com/pdc/index.php/Main/TCLabPIDFeedforward
double PidThermalLoop::CalculateProportionalGain(double error_value) const {
return GetCoeffProportional() * error_value;
}
double PidThermalLoop::CalculateIntegralGain(double error_value) const {
double coeff_integral = GetCoeffIntegral();
double integral_term = 0.0;
if (std::fabs(coeff_integral) > 0.001) {
integral_term =
GetIntegral() + error_value * coeff_integral * GetSampleTime();
integral_term =
std::clamp(integral_term, GetIntegralLimitMin(), GetIntegralLimitMax());
}
return integral_term;
}
double PidThermalLoop::CalculateDerivativeGain(double error_value) const {
return GetCoeffDerivative() *
((error_value - GetLastError()) / GetSampleTime());
}
double PidThermalLoop::CalculateFeedForward(double setpoint) const {
return (setpoint + GetFeedForwardOffset()) * GetFeedForwardGain();
}
double PidThermalLoop::ClampOutputWithSlewRates(double pid_output) const {
double last_output = GetLastOutput();
double sample_time = GetSampleTime();
double slew_neg = GetSlewNeg();
double min_pid_output = std::numeric_limits<double>::lowest();
// Slew rate with absolute value less than 1.0 is meaningless, and will be
// ignored.
if (std::fabs(slew_neg) >= 1.0) {
min_pid_output = last_output + slew_neg * sample_time;
}
double slew_pos = GetSlewPos();
double max_pid_output = std::numeric_limits<double>::max();
if (std::fabs(slew_pos) >= 1.0) {
max_pid_output = last_output + slew_pos * sample_time;
}
return std::clamp(pid_output, min_pid_output, max_pid_output);
}
double PidThermalLoop::CalculatePidOutput(double input, double setpoint,
bool enable_calculation_log) {
double error_value = setpoint - input;
// Calculate Kp, Ki, Kd, Feed-Forward terms
double proportional_term = CalculateProportionalGain(error_value);
double integral_term = CalculateIntegralGain(error_value);
double derivative_term = CalculateDerivativeGain(error_value);
double feed_forward_term = CalculateFeedForward(setpoint);
double pid_output =
proportional_term + integral_term + derivative_term + feed_forward_term;
if (enable_calculation_log) {
LOG(WARNING)
<< "[Thermal Debug]: "
<< absl::StrFormat(
"Kp: %lf; Ki: %lf; Kd: %lf; FfGain: %lf; FfOffset: %lf; "
"PID output: %lf; proportional term: %lf; integral term: "
"%lf; derivative term: %lf; feed forward term: %lf",
GetCoeffProportional(), GetCoeffIntegral(), GetCoeffDerivative(),
GetFeedForwardGain(), GetFeedForwardOffset(), pid_output,
proportional_term, integral_term, derivative_term,
feed_forward_term);
}
// Clamping the output with the lower/upper bounds and slew rates.
// Skip output limit clamping for thermal PIDs here
if (GetOutputLimitMax() != 0.0 || GetOutputLimitMin() != 0.0) {
pid_output =
std::clamp(pid_output, GetOutputLimitMin(), GetOutputLimitMax());
}
if (enable_calculation_log) {
LOG(WARNING)
<< "[Thermal Debug]: "
<< absl::StrFormat(
"Output limit clamped PID output: %lf (min: %lf, max: %lf)",
pid_output, GetOutputLimitMin(), GetOutputLimitMax());
}
// If this is not the first iteration, applying slew rate to avoid
// increasing or decreasing too fast
if (GetInitialized() &&
(std::fabs(GetSlewNeg()) >= 1.0 || std::fabs(GetSlewPos()) >= 1.0)) {
pid_output = ClampOutputWithSlewRates(pid_output);
// Back calculate integral term in case the output is limited.
integral_term = pid_output - proportional_term;
// Clamp again because having `pid_output` limited by slew rates may
// result in a larger integral term
integral_term =
std::clamp(integral_term, GetIntegralLimitMin(), GetIntegralLimitMax());
if (enable_calculation_log) {
LOG(WARNING)
<< "[Thermal Debug]: "
<< absl::StrFormat(
"Slew rate clamped PID output: %lf (slew_neg: %lf, slew_pos: "
"%lf); back calculated integral term: %lf (min: %lf, max: "
"%lf)",
pid_output, GetSlewNeg(), GetSlewPos(), integral_term,
GetIntegralLimitMin(), GetIntegralLimitMax());
}
}
SetIntegral(integral_term);
SetInitialized(true);
SetLastError(error_value);
SetLastOutput(pid_output);
return pid_output;
}
double PidThermalLoop::ExecutePidLoop(double input, double setpoint,
bool enable_calculation_log) {
// If hysteresis check with setpoint is enabled, check the bound using
// `setpoint` instead of `last_input`. Even if the hysteresis bound value is
// insignificant (i.e., |hysteresis_bound| < 0.5), this check should not be
// skipped.
if (GetCheckHysteresisWithSetpoint()) {
double output;
if (input > (setpoint + GetHysteresisPos())) {
// The delta exceeds the positive hysteresis bound, perform one PID
// iteration.
output = CalculatePidOutput(input, setpoint, enable_calculation_log);
SetLastInput(input);
} else if (input < (setpoint - GetHysteresisNeg())) {
// The delta is less than the negative hysteresis bound, reset PID.
output = 0.0;
SetLastInput(setpoint);
SetIntegral(0.0);
} else {
// The delta is within the hysteresis bounds, use the last output.
output = GetLastOutput();
SetLastInput(input);
}
SetLastOutput(output);
return output;
}
if (!IsWithinInputHysteresisBounds(input)) {
SetLastInput(input);
}
return CalculatePidOutput(GetLastInput(), setpoint, enable_calculation_log);
}
} // namespace thermal
} // namespace milotic_tlbmc