blob: d9004384aeb058555eb6c7e5ab1ff4e1d6c51f4a [file]
#ifndef THIRD_PARTY_MILOTIC_EXTERNAL_CC_TLBMC_THERMAL_CONTROLLER_ADRC_H_
#define THIRD_PARTY_MILOTIC_EXTERNAL_CC_TLBMC_THERMAL_CONTROLLER_ADRC_H_
#include <cmath>
#include <optional>
#include <vector>
#include "absl/log/log.h"
#include "absl/strings/str_format.h"
#include "absl/types/span.h"
#include "thermal_config.pb.h"
#include "tlbmc/thermal/controller/algorithm/utils/nonlinear_tracking_differentiator.h"
namespace milotic_tlbmc {
namespace thermal {
struct DisturbanceFactors {
double d; // Control gain for this disturbance.
double reference_setpoint;
};
/*
* `SecondOrderAdrcThermalLoop` is an adaptive controller based on 2nd-order
* Active Disturbance Rejection Control (ADRC), capable of adjusting its
* coefficients during runtime to adapt to transient system dynamics. It serves
* as a powerful alternative to both PID control and modern model-based control,
* as it requires little tuning, and is robust to external disturbances and
* system parameter variations (e.g., hardware manufactural differences, aging).
*
* The strategy of implemented ADRC here is linear state error feedback (LESF)
* that primarily counteracts disturbances and eliminates the output
* error of the tracking differentiator (TD) and extended state observer (ESO).
*
* This class is not thread-safe.
*
* The comment format of numeric expressions follows LaTeX style.
*
* More resources can be found at:
* https://www.mdpi.com/2079-9292/2/3/246
*/
class SecondOrderAdrcThermalLoop {
public:
explicit SecondOrderAdrcThermalLoop(
const SecondOrderAdrcLoopConfig& second_order_adrc_params)
: second_order_adrc_params_(second_order_adrc_params) {
Initialize(second_order_adrc_params);
}
explicit SecondOrderAdrcThermalLoop(
const SecondOrderAdrcLoopConfig& second_order_adrc_params,
const std::vector<DisturbanceFactors>& disturbance_factors)
: second_order_adrc_params_(second_order_adrc_params),
disturbance_factors_(disturbance_factors) {
Initialize(second_order_adrc_params);
}
void Initialize(const SecondOrderAdrcLoopConfig& second_order_adrc_params) {
if (second_order_adrc_params.sample_time_sec() <= 0) {
second_order_adrc_params_.set_sample_time_sec(1.0);
}
if (second_order_adrc_params.settle_time_sec() <= 0) {
second_order_adrc_params_.set_settle_time_sec(5.0);
}
// Initialize the nonlinear tracking differentiator (NTD) if configured.
if (second_order_adrc_params
.has_nonlinear_tracking_differentiator_config()) {
tracking_differentiator_ = NonlinearTrackingDifferentiator(
second_order_adrc_params_.nonlinear_tracking_differentiator_config()
.sample_time_sec(),
second_order_adrc_params_.nonlinear_tracking_differentiator_config()
.r(),
second_order_adrc_params_.nonlinear_tracking_differentiator_config()
.h0(),
second_order_adrc_params_.nonlinear_tracking_differentiator_config()
.initial_filtered_output(),
second_order_adrc_params_.nonlinear_tracking_differentiator_config()
.initial_filtered_derivative());
}
double controller_bandwidth =
6.0 / second_order_adrc_params_.settle_time_sec();
double observer_bandwidth =
second_order_adrc_params_.observer_bandwidth_factor() *
controller_bandwidth;
double kp = std::pow(controller_bandwidth, 2);
double kd = 2.0 * controller_bandwidth;
// Precompute `K_{p} / b_{0}` for the update step.
kp_over_b0_ = kp / second_order_adrc_params_.b0();
// Observer gains with discrete-time Zero-Order-Hold mapping.
double z_pole = std::exp(-observer_bandwidth *
second_order_adrc_params_.sample_time_sec());
double l1 = 1.0 - std::pow(z_pole, 3);
double l2 = (3.0 / (2.0 * second_order_adrc_params_.sample_time_sec())) *
std::pow(1.0 - z_pole, 2) * (1.0 + z_pole);
double l3 =
(1.0 / std::pow(second_order_adrc_params_.sample_time_sec(), 2)) *
std::pow(1.0 - z_pole, 3);
// Coordinate transformation scaling
l_hat_[0] = (kp / second_order_adrc_params_.b0()) * l1;
l_hat_[1] = (kd / second_order_adrc_params_.b0()) * l2;
l_hat_[2] = (1.0 / second_order_adrc_params_.b0()) * l3;
gain_sum_ = l_hat_[0] + l_hat_[1] + l_hat_[2];
// `\hat{A} = T^{-1} * A_{d} * T`
a_hat_[0][0] = 1.0;
a_hat_[0][1] = second_order_adrc_params_.sample_time_sec() * (kp / kd);
a_hat_[0][2] = second_order_adrc_params_.sample_time_sec() *
second_order_adrc_params_.sample_time_sec() / 2.0 * kp;
a_hat_[1][0] = 0.0;
a_hat_[1][1] = 1.0;
a_hat_[1][2] = second_order_adrc_params_.sample_time_sec() * kd;
a_hat_[2][0] = 0.0;
a_hat_[2][1] = 0.0;
a_hat_[2][2] = 1.0;
// `\hat{B} = T^{-1} * B_{d}`
b_hat_[0] = (kp / second_order_adrc_params_.b0()) *
(second_order_adrc_params_.b0() *
second_order_adrc_params_.sample_time_sec() *
second_order_adrc_params_.sample_time_sec() / 2.0);
b_hat_[1] = (kd / second_order_adrc_params_.b0()) *
(second_order_adrc_params_.b0() *
second_order_adrc_params_.sample_time_sec());
b_hat_[2] = 0.0;
LOG(WARNING) << absl::StrFormat(
"Initialized 2nd-order ADRC coefficients: sample_time_sec = %lf; "
"settle_time_sec = %lf; b0 = %lf; observer_bandwidth_factor = %lf; "
"l_hat_ = [%lf, %lf, %lf]; gain_sum_ = %lf; kp_over_b0_ = %lf; "
"u_precompute_ = %lf; a_hat_ = [[%lf, %lf, %lf], [%lf, %lf, %lf], "
"[%lf, %lf, %lf]]; b_hat_ = [%lf, %lf, %lf]",
second_order_adrc_params_.sample_time_sec(),
second_order_adrc_params_.settle_time_sec(),
second_order_adrc_params_.b0(),
second_order_adrc_params_.observer_bandwidth_factor(), l_hat_[0],
l_hat_[1], l_hat_[2], gain_sum_, kp_over_b0_, u_precompute_,
a_hat_[0][0], a_hat_[0][1], a_hat_[0][2], a_hat_[1][0], a_hat_[1][1],
a_hat_[1][2], a_hat_[2][0], a_hat_[2][1], a_hat_[2][2], b_hat_[0],
b_hat_[1], b_hat_[2]);
}
// `ExecuteSecondOrderAdrcLoop` executes one-step of the 2nd-order ADRC
// controller, and returns the computed control output `u(t)`.
// By providing `disturbances`, the ADRC controller can explicitly compensate
// for the effects of external disturbances with specified tuned factors.
//
// The formula is:
// `u(t+1)
// = \hat{A} * \hat{x}(t) +
// \hat{B} * (u(t) + d(t)) +
// \hat{L} * (y(t) - \hat{y}(t))`
//
// Note that if `disturbances` is emtpy, we have `d(t) = 0`.
double ExecuteSecondOrderAdrcLoop(double setpoint, double input,
absl::Span<const double> disturbances,
bool enable_calculation_log = false);
double GetSampleTimeSec() const {
return second_order_adrc_params_.sample_time_sec();
}
double GetSettleTimeSec() const {
return second_order_adrc_params_.settle_time_sec();
}
double GetB0() const { return second_order_adrc_params_.b0(); }
double GetObserverBandwidthFactor() const {
return second_order_adrc_params_.observer_bandwidth_factor();
}
double GetOutputLimitMax() const {
return second_order_adrc_params_.u_limit_max();
}
double GetOutputLimitMin() const {
return second_order_adrc_params_.u_limit_min();
}
protected:
bool IsSaturated(double u) {
return u >= second_order_adrc_params_.u_limit_max() ||
u <= second_order_adrc_params_.u_limit_min();
}
private:
SecondOrderAdrcLoopConfig
second_order_adrc_params_; // The parameters for this ADRC thermal loop.
std::optional<NonlinearTrackingDifferentiator> tracking_differentiator_ =
std::nullopt;
// Precomputed Transformed Gains and Matrices
double gain_sum_; // `\sum_{i=1}^{3} \hat{L}_{i}`
double kp_over_b0_;
double a_hat_[3][3];
double b_hat_[3];
double l_hat_[3];
// State Variables
double x_hat_[3] = {0.0, 0.0, 0.0};
double u_precompute_ = 0.0; // `u(k|k - 1)`
std::vector<DisturbanceFactors> disturbance_factors_;
};
} // namespace thermal
} // namespace milotic_tlbmc
#endif // THIRD_PARTY_MILOTIC_EXTERNAL_CC_TLBMC_THERMAL_CONTROLLER_ADRC_H_