blob: cc5c686a3959ad584da5afd3d9782ba4ee3e3e2e [file]
#include "tlbmc/thermal/controller/pid_controller.h"
#include <cmath>
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "absl/memory/memory.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "g3/macros.h"
#include <nlohmann/json_fwd.hpp>
#include "thermal_config.pb.h"
#include "thermal_controller_specification.pb.h"
#include "tlbmc/thermal/controller/algorithm/pid.h"
#include "tlbmc/thermal/controller/optimizer/pid/pid_optimizer.h"
#include "tlbmc/thermal/zone_manager.h"
namespace milotic_tlbmc {
namespace thermal {
absl::StatusOr<std::unique_ptr<PidController>> PidController::Create(
PidControllerParameters params) {
if (params.input_sensors.empty()) {
return absl::InvalidArgumentError(absl::StrFormat(
"No input sensor is assigned to PID controller `%s`!", params.id));
}
auto pid_controller = absl::WrapUnique(
params.pid_optimizer_info.has_value()
? new PidController(params.id, params.zone_manager,
std::move(params.pid_loop), params.setpoint,
std::move(params.input_sensors),
std::move(params.pid_optimizer_info.value()))
: new PidController(params.id, params.zone_manager,
std::move(params.pid_loop), params.setpoint,
std::move(params.input_sensors)));
ECCLESIA_RETURN_IF_ERROR(
pid_controller->SetInputProcessType(params.input_process_type));
return pid_controller;
}
void PidController::ProcessThermalLoop() {
double input = ProcessInput();
if (pid_optimizer_info_.optimizer != nullptr &&
!zone_owner_->GetFailsafeMode()) {
if (!pid_optimizer_info_.states.pid_optimizer_metadata.initialized) {
pid_optimizer_info_.optimizer->Initialize(
pid_optimizer_info_.states.pid_optimizer_init_params);
pid_optimizer_info_.states.pid_optimizer_metadata.initialized = true;
}
if (!pid_optimizer_info_.optimizer->FinishedTuning()) {
zone_owner_->TryLogThermalDebugMessage(absl::StrFormat(
"PID controller %s is tuning PID coefficients.", GetId()));
pid_optimizer_info_.states.pid_optimizer_tuning_params.input = input;
double output = pid_optimizer_info_.optimizer->TunePid(
pid_optimizer_info_.states.pid_optimizer_tuning_params);
// For further optimization, we may use actual system time instead of the
// PID loop config to improve the tuning accuracy.
// However, PID is already an overly simplified model, and such a
// precision loss may be negligible in practice.
pid_optimizer_info_.states.pid_optimizer_tuning_params.current_time_ms +=
static_cast<uint32_t>(pid_loop_->GetSampleTime() * 1000);
pid_loop_->SetCoeffProportional(
pid_optimizer_info_.optimizer->GetCoeffP());
pid_loop_->SetCoeffIntegral(pid_optimizer_info_.optimizer->GetCoeffI());
pid_loop_->SetCoeffDerivative(pid_optimizer_info_.optimizer->GetCoeffD());
ProcessOutput(output);
return;
}
}
double setpoint = GetSetpoint();
ProcessOutput(GetPidLoop()->ExecutePidLoop(
input, setpoint, zone_owner_->GetDebugPidEnabled()));
}
double PidController::ProcessInput() {
const std::vector<PidControllerSensorInfo>& input_sensors = GetInputSensors();
// If no input sensor is assigned to this PID, always use `setpoint_` as the
// input.
if (input_sensors.empty()) {
zone_owner_->TryLogThermalDebugMessage(
absl::StrFormat("PID controller %s is assigned with no input sensor. "
"Use setpoint %lf as the input.\n",
GetId(), GetSetpoint()));
return GetSetpoint();
}
std::string leader_sensor = input_sensors[0].sensor_key();
double input_value = InitializeInputBasedOnPidThermalType();
bool is_input_acceptable = false;
for (const PidControllerSensorInfo& sensor : input_sensors) {
double sensor_value =
zone_owner_->GetSensorReadings(sensor.sensor_key()).scaled_value;
// The sensor reading must not be NaN or infinite.
// Note that it is fine to get negative values.
if (!std::isfinite(sensor_value)) {
continue;
}
// Perform temperature to margin conversion if needed.
double org_sensor_value = sensor_value;
sensor_value = ProcessSensorReadingForMarginPid(sensor_value, sensor);
if (org_sensor_value != sensor_value) {
zone_owner_->TryLogThermalDebugMessage(absl::StrFormat(
"Converting temperature to margin: (temperature=%lf, Tjmax=%lf, "
"margin=%lf)\n",
org_sensor_value, sensor.max_junction_temperature(), sensor_value));
}
double org_input_value = input_value;
input_value =
IntegrateSensorReadingBasedOnPidThermalType(input_value, sensor_value);
if (org_input_value != input_value) {
leader_sensor = sensor.sensor_key();
}
is_input_acceptable = true;
}
if (is_input_acceptable) {
zone_owner_->TryLogThermalDebugMessage(absl::StrFormat(
"PID controller %s chose the input value %lf from sensor %s\n", GetId(),
input_value, leader_sensor));
return input_value;
}
// If no valid input available, use the setpoint.
// This provides safe behavior until the input becomes acceptable.
zone_owner_->TryLogThermalDebugMessage(absl::StrFormat(
"PID controller %s chose the setpoint %lf as the input value\n", GetId(),
GetSetpoint()));
return GetSetpoint();
}
void PidController::ProcessOutput(double output) {
zone_owner_->AddSetpoint(GetId(), output);
zone_owner_->TryLogThermalDebugMessage(absl::StrFormat(
"PID controller %s outputs setpoint: %lf\n", GetId(), output));
if (zone_owner_->GetDebugEnabled()) {
zone_owner_->AppendSampledData(absl::StrCat(output, ","));
}
}
void PidController::SetCoefficients(
const PidControllerCoefficients& coefficients) {
if (coefficients.coeff_proportional.has_value()) {
pid_loop_->SetCoeffProportional(*coefficients.coeff_proportional);
}
if (coefficients.coeff_integral.has_value()) {
pid_loop_->SetCoeffIntegral(*coefficients.coeff_integral);
}
if (coefficients.coeff_derivative.has_value()) {
pid_loop_->SetCoeffDerivative(*coefficients.coeff_derivative);
}
if (coefficients.feed_forward_offset.has_value()) {
pid_loop_->SetFeedForwardOffset(*coefficients.feed_forward_offset);
}
if (coefficients.feed_forward_gain.has_value()) {
pid_loop_->SetFeedForwardGain(*coefficients.feed_forward_gain);
}
if (coefficients.setpoint.has_value()) {
setpoint_ = kDefaultPidSetpointValue;
if (coefficients.setpoint > 0) {
setpoint_ = *coefficients.setpoint;
}
}
}
void PidController::PreparePidOptimizer() {
// This function will only be called when `pid_optimizer_info_` is present.
// Therefore, no need for nullability check.
pid_optimizer_info_.states.pid_optimizer_init_params.coeff_p =
pid_loop_->GetCoeffProportional();
pid_optimizer_info_.states.pid_optimizer_init_params.coeff_i =
pid_loop_->GetCoeffIntegral();
pid_optimizer_info_.states.pid_optimizer_init_params.coeff_d =
pid_loop_->GetCoeffDerivative();
pid_optimizer_info_.states.pid_optimizer_metadata =
optimizer::PidOptimizerMetadata{
.initialized = false,
};
}
nlohmann::json PidController::ToJson() const {
nlohmann::json json;
json["DCoefficient"] = pid_loop_->GetCoeffDerivative();
json["FFGainCoefficient"] = pid_loop_->GetFeedForwardGain();
json["FFOffCoefficient"] = pid_loop_->GetFeedForwardOffset();
json["ICoefficient"] = pid_loop_->GetCoeffIntegral();
json["ILimitMax"] = pid_loop_->GetIntegralLimitMax();
json["ILimitMin"] = pid_loop_->GetIntegralLimitMin();
json["OutLimitMax"] = pid_loop_->GetOutputLimitMax();
json["OutLimitMin"] = pid_loop_->GetOutputLimitMin();
json["PCoefficient"] = pid_loop_->GetCoeffProportional();
json["SetPoint"] = GetSetpoint();
json["SlewNeg"] = pid_loop_->GetSlewNeg();
json["SlewPos"] = pid_loop_->GetSlewPos();
json["Inputs"] = nlohmann::json::array();
for (const auto& input : input_sensors_) {
json["Inputs"].push_back(input.sensor_key());
}
return json;
}
} // namespace thermal
} // namespace milotic_tlbmc