11#include " fluxgraph/model/thermal_mass.hpp"
22#include < limits>
3+ #include < stdexcept>
34#include < sstream>
45
56namespace fluxgraph {
67
8+ namespace {
9+
10+ constexpr double kRk4NegativeRealAxisStabilityLimit = 2.785293563405282 ;
11+
12+ } // namespace
13+
14+ const char *to_string (ThermalIntegrationMethod method) {
15+ switch (method) {
16+ case ThermalIntegrationMethod::ForwardEuler:
17+ return " forward_euler" ;
18+ case ThermalIntegrationMethod::Rk4:
19+ return " rk4" ;
20+ }
21+ return " forward_euler" ;
22+ }
23+
24+ ThermalIntegrationMethod
25+ parse_thermal_integration_method (const std::string &method_name) {
26+ if (method_name == " forward_euler" ) {
27+ return ThermalIntegrationMethod::ForwardEuler;
28+ }
29+ if (method_name == " rk4" ) {
30+ return ThermalIntegrationMethod::Rk4;
31+ }
32+
33+ throw std::invalid_argument (
34+ " Unknown thermal integration method '" + method_name +
35+ " ' (expected one of: forward_euler, rk4)" );
36+ }
37+
738ThermalMassModel::ThermalMassModel (const std::string &id, double thermal_mass,
839 double heat_transfer_coeff,
940 double initial_temp,
1041 const std::string &temp_signal_path,
1142 const std::string &power_signal_path,
1243 const std::string &ambient_signal_path,
13- SignalNamespace &ns)
44+ SignalNamespace &ns,
45+ ThermalIntegrationMethod integration_method)
1446 : id_(id), temp_signal_(ns.intern(temp_signal_path)),
1547 power_signal_ (ns.intern(power_signal_path)),
1648 ambient_signal_(ns.intern(ambient_signal_path)),
1749 thermal_mass_(thermal_mass), heat_transfer_coeff_(heat_transfer_coeff),
18- temperature_(initial_temp), initial_temp_(initial_temp) {}
50+ temperature_(initial_temp), initial_temp_(initial_temp),
51+ integration_method_(integration_method) {}
52+
53+ double ThermalMassModel::derivative (double temperature, double net_power,
54+ double ambient) const {
55+ const double heat_loss = heat_transfer_coeff_ * (temperature - ambient);
56+ return (net_power - heat_loss) / thermal_mass_;
57+ }
1958
2059void ThermalMassModel::tick (double dt, SignalStore &store) {
2160 // Read inputs
2261 double net_power = store.read_value (power_signal_);
2362 double ambient = store.read_value (ambient_signal_);
2463
25- // Compute heat loss to ambient
26- double heat_loss = heat_transfer_coeff_ * (temperature_ - ambient);
27-
28- // Forward Euler integration: T += dT/dt * dt
29- double dT = (net_power - heat_loss) / thermal_mass_ * dt;
30- temperature_ += dT;
64+ if (integration_method_ == ThermalIntegrationMethod::ForwardEuler) {
65+ // Forward Euler integration: T += dT/dt * dt
66+ temperature_ += derivative (temperature_, net_power, ambient) * dt;
67+ } else {
68+ // Classic RK4 with fixed inputs over the tick interval.
69+ const double k1 = derivative (temperature_, net_power, ambient);
70+ const double k2 = derivative (temperature_ + 0.5 * dt * k1, net_power, ambient);
71+ const double k3 = derivative (temperature_ + 0.5 * dt * k2, net_power, ambient);
72+ const double k4 = derivative (temperature_ + dt * k3, net_power, ambient);
73+ temperature_ += (dt / 6.0 ) * (k1 + 2.0 * k2 + 2.0 * k3 + k4);
74+ }
3175
3276 // Write output with unit
3377 store.write (temp_signal_, temperature_, " degC" );
@@ -37,21 +81,26 @@ void ThermalMassModel::tick(double dt, SignalStore &store) {
3781void ThermalMassModel::reset () { temperature_ = initial_temp_; }
3882
3983double ThermalMassModel::compute_stability_limit () const {
40- // Forward Euler stability for dT/dt = -k*T: dt < 2/k
41- // For this model: k = h/C
42- // Therefore: dt < 2*C/h
84+ // Stability for dT/dt = -k*T with k = h/C.
4385 if (heat_transfer_coeff_ <= 0.0 ) {
4486 return std::numeric_limits<double >::infinity (); // No cooling =
4587 // unconditionally stable
4688 }
47- return 2.0 * thermal_mass_ / heat_transfer_coeff_;
89+
90+ const double tau = thermal_mass_ / heat_transfer_coeff_;
91+ if (integration_method_ == ThermalIntegrationMethod::ForwardEuler) {
92+ return 2.0 * tau;
93+ }
94+
95+ return kRk4NegativeRealAxisStabilityLimit * tau;
4896}
4997
5098std::string ThermalMassModel::describe () const {
5199 std::ostringstream oss;
52100 oss << " ThermalMass(id=" << id_ << " , C=" << thermal_mass_ << " J/K"
53101 << " , h=" << heat_transfer_coeff_ << " W/K"
54- << " , T0=" << initial_temp_ << " degC)" ;
102+ << " , T0=" << initial_temp_ << " degC"
103+ << " , method=" << to_string (integration_method_) << " )" ;
55104 return oss.str ();
56105}
57106
0 commit comments