| #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 |