|
1 | 1 | /********************************************************************* |
2 | 2 | * @file radiation_ss_model.cpp |
3 | 3 | * @brief Implementation of RadiationStateSpaceModel. |
| 4 | + * |
| 5 | + * EXACT INTEGRATION FORMULAS: |
| 6 | + * |
| 7 | + * 1. EXPONENTIAL MODE (z' = -α*z + b*v) |
| 8 | + * With constant v over [t, t+dt]: |
| 9 | + * z(t+dt) = exp(-α*dt) * z(t) + b * (1 - exp(-α*dt)) / α * v |
| 10 | + * |
| 11 | + * 2. OSCILLATORY MODE |
| 12 | + * [z_c]' = [-α -ω] [z_c] + [b_c] * v |
| 13 | + * [z_s] [ ω -α] [z_s] [b_s] |
| 14 | + * |
| 15 | + * The homogeneous solution uses rotation + decay: |
| 16 | + * exp(A*t) = exp(-α*t) * [cos(ω*t) -sin(ω*t)] |
| 17 | + * [sin(ω*t) cos(ω*t)] |
| 18 | + * |
| 19 | + * The particular integral for constant v involves: |
| 20 | + * ∫[0,dt] exp(-α*s) * cos(ω*s) ds |
| 21 | + * ∫[0,dt] exp(-α*s) * sin(ω*s) ds |
| 22 | + * |
4 | 23 | *********************************************************************/ |
5 | 24 |
|
6 | 25 | #include "radiation_ss_model.h" |
| 26 | +#include "radiation_ss_fitter.h" |
7 | 27 |
|
8 | 28 | #include <cmath> |
| 29 | +#include <complex> |
| 30 | +#include <algorithm> |
9 | 31 | #include <stdexcept> |
10 | | -#include <string> |
11 | 32 |
|
12 | 33 | namespace hydrochrono::hydro { |
13 | 34 |
|
14 | | -RadiationStateSpaceModel::RadiationStateSpaceModel(int num_dofs, int num_modes) |
15 | | - : num_dofs_(num_dofs), num_modes_(num_modes) { |
16 | | - |
17 | | - if (num_dofs <= 0) { |
18 | | - throw std::invalid_argument( |
19 | | - "RadiationStateSpaceModel: num_dofs must be > 0 (got " + |
20 | | - std::to_string(num_dofs) + ")"); |
| 35 | +RadiationStateSpaceModel RadiationStateSpaceModel::FromFitResult(const StateSpaceFitResult& result) { |
| 36 | + RadiationStateSpaceModel model; |
| 37 | + |
| 38 | + if (!result.IsValid()) { |
| 39 | + return model; |
21 | 40 | } |
22 | | - if (num_modes <= 0) { |
23 | | - throw std::invalid_argument( |
24 | | - "RadiationStateSpaceModel: num_modes must be > 0 (got " + |
25 | | - std::to_string(num_modes) + ")"); |
| 41 | + |
| 42 | + const int n = result.order; |
| 43 | + const Eigen::MatrixXd& A = result.A; |
| 44 | + const Eigen::VectorXd& B = result.B; |
| 45 | + const Eigen::RowVectorXd& C = result.C; |
| 46 | + |
| 47 | + // Compute eigendecomposition of A |
| 48 | + Eigen::EigenSolver<Eigen::MatrixXd> solver(A); |
| 49 | + const Eigen::VectorXcd& eigenvalues = solver.eigenvalues(); |
| 50 | + const Eigen::MatrixXcd& eigenvectors = solver.eigenvectors(); |
| 51 | + |
| 52 | + // Compute left eigenvectors (rows of V^{-1}) |
| 53 | + Eigen::MatrixXcd V_inv = eigenvectors.inverse(); |
| 54 | + |
| 55 | + // Track which eigenvalues we've processed (for conjugate pairs) |
| 56 | + std::vector<bool> processed(n, false); |
| 57 | + |
| 58 | + for (int k = 0; k < n; ++k) { |
| 59 | + if (processed[k]) continue; |
| 60 | + |
| 61 | + std::complex<double> lambda = eigenvalues(k); |
| 62 | + double real_part = lambda.real(); |
| 63 | + double imag_part = lambda.imag(); |
| 64 | + |
| 65 | + // Check for stability (real part should be negative for decay) |
| 66 | + if (real_part >= 0) { |
| 67 | + processed[k] = true; |
| 68 | + continue; // Skip unstable modes |
| 69 | + } |
| 70 | + |
| 71 | + if (std::abs(imag_part) < 1e-10) { |
| 72 | + // Real eigenvalue -> pure exponential mode |
| 73 | + double alpha = -real_part; // α > 0 for decay |
| 74 | + |
| 75 | + // Compute input/output gains |
| 76 | + std::complex<double> b_complex = V_inv.row(k) * B.cast<std::complex<double>>(); |
| 77 | + std::complex<double> H_complex = C.cast<std::complex<double>>() * eigenvectors.col(k); |
| 78 | + |
| 79 | + double b = b_complex.real(); |
| 80 | + double H = H_complex.real(); |
| 81 | + |
| 82 | + model.AddExponentialMode(alpha, b, H); |
| 83 | + processed[k] = true; |
| 84 | + } else { |
| 85 | + // Complex eigenvalue -> look for conjugate pair |
| 86 | + int conj_idx = -1; |
| 87 | + for (int j = k + 1; j < n; ++j) { |
| 88 | + if (!processed[j]) { |
| 89 | + std::complex<double> other = eigenvalues(j); |
| 90 | + if (std::abs(other.real() - real_part) < 1e-10 && |
| 91 | + std::abs(other.imag() + imag_part) < 1e-10) { |
| 92 | + conj_idx = j; |
| 93 | + break; |
| 94 | + } |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + if (conj_idx < 0) { |
| 99 | + // No conjugate found, skip |
| 100 | + processed[k] = true; |
| 101 | + continue; |
| 102 | + } |
| 103 | + |
| 104 | + // Process conjugate pair |
| 105 | + double alpha = -real_part; // α > 0 |
| 106 | + double omega = std::abs(imag_part); // ω > 0 |
| 107 | + |
| 108 | + // For conjugate pair, compute the real representation |
| 109 | + // K(t) = 2*Re[g * exp(λ*t)] where g = (C*r_k) * (l_k*B) |
| 110 | + // = 2*exp(-α*t) * [Re(g)*cos(ωt) - Im(g)*sin(ωt)] |
| 111 | + std::complex<double> l_k_B = V_inv.row(k) * B.cast<std::complex<double>>(); |
| 112 | + std::complex<double> C_r_k = C.cast<std::complex<double>>() * eigenvectors.col(k); |
| 113 | + std::complex<double> g = C_r_k * l_k_B; |
| 114 | + |
| 115 | + double H_c = 2.0 * g.real(); |
| 116 | + double H_s = -2.0 * g.imag(); |
| 117 | + |
| 118 | + // Standard representation with b_c=1, b_s=0 |
| 119 | + double b_c = 1.0; |
| 120 | + double b_s = 0.0; |
| 121 | + |
| 122 | + model.AddOscillatoryMode(alpha, omega, b_c, b_s, H_c, H_s); |
| 123 | + |
| 124 | + processed[k] = true; |
| 125 | + processed[conj_idx] = true; |
| 126 | + } |
26 | 127 | } |
27 | 128 |
|
28 | | - // Initialize storage |
29 | | - alphas_.setZero(num_modes_); |
30 | | - H_.setZero(num_dofs_, num_modes_); |
31 | | - z_.setZero(num_dofs_, num_modes_); |
| 129 | + return model; |
32 | 130 | } |
33 | 131 |
|
34 | | -void RadiationStateSpaceModel::SetModeParameters( |
35 | | - int mode_index, |
36 | | - double alpha, |
37 | | - const Eigen::VectorXd& h_column) { |
38 | | - |
39 | | - if (mode_index < 0 || mode_index >= num_modes_) { |
40 | | - throw std::out_of_range( |
41 | | - "RadiationStateSpaceModel::SetModeParameters: mode_index " + |
42 | | - std::to_string(mode_index) + " out of range [0, " + |
43 | | - std::to_string(num_modes_) + ")"); |
| 132 | +void RadiationStateSpaceModel::AddExponentialMode(double alpha, double b, double H) { |
| 133 | + if (alpha <= 0) { |
| 134 | + throw std::invalid_argument("alpha must be positive"); |
44 | 135 | } |
45 | | - if (alpha <= 0.0) { |
46 | | - throw std::invalid_argument( |
47 | | - "RadiationStateSpaceModel::SetModeParameters: alpha must be > 0 (got " + |
48 | | - std::to_string(alpha) + ")"); |
| 136 | + exp_modes_.emplace_back(alpha, b, H); |
| 137 | +} |
| 138 | + |
| 139 | +void RadiationStateSpaceModel::AddOscillatoryMode(double alpha, double omega, |
| 140 | + double b_c, double b_s, |
| 141 | + double H_c, double H_s) { |
| 142 | + if (alpha <= 0) { |
| 143 | + throw std::invalid_argument("alpha must be positive"); |
49 | 144 | } |
50 | | - if (h_column.size() != num_dofs_) { |
51 | | - throw std::invalid_argument( |
52 | | - "RadiationStateSpaceModel::SetModeParameters: h_column size mismatch " |
53 | | - "(expected " + std::to_string(num_dofs_) + ", got " + |
54 | | - std::to_string(h_column.size()) + ")"); |
| 145 | + if (omega <= 0) { |
| 146 | + throw std::invalid_argument("omega must be positive"); |
55 | 147 | } |
56 | | - |
57 | | - alphas_(mode_index) = alpha; |
58 | | - H_.col(mode_index) = h_column; |
| 148 | + OscillatoryMode mode; |
| 149 | + mode.alpha = alpha; |
| 150 | + mode.omega = omega; |
| 151 | + mode.b_c = b_c; |
| 152 | + mode.b_s = b_s; |
| 153 | + mode.H_c = H_c; |
| 154 | + mode.H_s = H_s; |
| 155 | + mode.z_c = 0.0; |
| 156 | + mode.z_s = 0.0; |
| 157 | + osc_modes_.push_back(mode); |
59 | 158 | } |
60 | 159 |
|
61 | 160 | void RadiationStateSpaceModel::Reset() { |
62 | | - z_.setZero(); |
| 161 | + for (auto& mode : exp_modes_) { |
| 162 | + mode.z = 0.0; |
| 163 | + } |
| 164 | + for (auto& mode : osc_modes_) { |
| 165 | + mode.z_c = 0.0; |
| 166 | + mode.z_s = 0.0; |
| 167 | + } |
63 | 168 | } |
64 | 169 |
|
65 | | -void RadiationStateSpaceModel::Step(double dt, const Eigen::VectorXd& v) { |
66 | | - if (dt <= 0.0) { |
67 | | - throw std::invalid_argument( |
68 | | - "RadiationStateSpaceModel::Step: dt must be > 0 (got " + |
69 | | - std::to_string(dt) + ")"); |
| 170 | +void RadiationStateSpaceModel::Step(double dt, double v) { |
| 171 | + if (dt <= 0) { |
| 172 | + throw std::invalid_argument("dt must be positive"); |
70 | 173 | } |
71 | | - if (v.size() != num_dofs_) { |
72 | | - throw std::invalid_argument( |
73 | | - "RadiationStateSpaceModel::Step: velocity vector size mismatch " |
74 | | - "(expected " + std::to_string(num_dofs_) + ", got " + |
75 | | - std::to_string(v.size()) + ")"); |
| 174 | + |
| 175 | + // Update exponential modes |
| 176 | + // z(t+dt) = exp(-α*dt) * z(t) + b * (1 - exp(-α*dt)) / α * v |
| 177 | + for (auto& mode : exp_modes_) { |
| 178 | + double exp_decay = std::exp(-mode.alpha * dt); |
| 179 | + double gain = mode.b * (1.0 - exp_decay) / mode.alpha; |
| 180 | + mode.z = exp_decay * mode.z + gain * v; |
76 | 181 | } |
77 | 182 |
|
78 | | - // Exact exponential integration for each mode: |
79 | | - // z_new = exp(-α dt) * z_old + [1 - exp(-α dt)] / α * v |
80 | | - // |
81 | | - // This is unconditionally stable for any α > 0 and dt > 0. |
82 | | - // The coefficient [1 - exp(-α dt)] / α approaches dt as α → 0, |
83 | | - // but we assume α > 0 (validated in SetModeParameters). |
84 | | - |
85 | | - for (int m = 0; m < num_modes_; ++m) { |
86 | | - const double alpha = alphas_(m); |
87 | | - |
88 | | - // Skip uninitialized modes (alpha == 0 shouldn't happen after proper setup) |
89 | | - if (alpha <= 0.0) { |
90 | | - continue; |
91 | | - } |
| 183 | + // Update oscillatory modes using rotation + decay + particular integral |
| 184 | + for (auto& mode : osc_modes_) { |
| 185 | + double alpha = mode.alpha; |
| 186 | + double omega = mode.omega; |
| 187 | + double exp_decay = std::exp(-alpha * dt); |
| 188 | + double cos_wt = std::cos(omega * dt); |
| 189 | + double sin_wt = std::sin(omega * dt); |
| 190 | + |
| 191 | + // Homogeneous solution (rotation + decay) |
| 192 | + double z_c_old = mode.z_c; |
| 193 | + double z_s_old = mode.z_s; |
| 194 | + double z_c_hom = exp_decay * (cos_wt * z_c_old - sin_wt * z_s_old); |
| 195 | + double z_s_hom = exp_decay * (sin_wt * z_c_old + cos_wt * z_s_old); |
| 196 | + |
| 197 | + // Particular integral for constant input v |
| 198 | + double denom = alpha * alpha + omega * omega; |
| 199 | + double int_cos = (alpha - exp_decay * (alpha * cos_wt - omega * sin_wt)) / denom; |
| 200 | + double int_sin = (omega - exp_decay * (omega * cos_wt + alpha * sin_wt)) / denom; |
92 | 201 |
|
93 | | - const double exp_factor = std::exp(-alpha * dt); |
94 | | - const double input_coeff = (1.0 - exp_factor) / alpha; |
| 202 | + // Contribution from input [b_c; b_s] * v |
| 203 | + double z_c_part = (mode.b_c * int_cos - mode.b_s * int_sin) * v; |
| 204 | + double z_s_part = (mode.b_c * int_sin + mode.b_s * int_cos) * v; |
95 | 205 |
|
96 | | - // Update state for mode m: |
97 | | - // z_.col(m) = exp_factor * z_.col(m) + input_coeff * v |
98 | | - z_.col(m) = exp_factor * z_.col(m) + input_coeff * v; |
| 206 | + mode.z_c = z_c_hom + z_c_part; |
| 207 | + mode.z_s = z_s_hom + z_s_part; |
99 | 208 | } |
100 | 209 | } |
101 | 210 |
|
102 | | -Eigen::VectorXd RadiationStateSpaceModel::GetForces() const { |
103 | | - // Radiation force: f_rad = Σₘ Hₘ ⊙ zₘ |
104 | | - // |
105 | | - // With H_ and z_ both [num_dofs × num_modes]: |
106 | | - // f_rad[i] = Σₘ H_[i,m] * z_[i,m] |
107 | | - // |
108 | | - // This is the row-wise sum of element-wise product. |
| 211 | +double RadiationStateSpaceModel::GetForce() const { |
| 212 | + double force = 0.0; |
109 | 213 |
|
110 | | - // Element-wise product, then sum across columns (modes) |
111 | | - return (H_.array() * z_.array()).rowwise().sum(); |
| 214 | + // Contribution from exponential modes |
| 215 | + for (const auto& mode : exp_modes_) { |
| 216 | + force += mode.H * mode.z; |
| 217 | + } |
| 218 | + |
| 219 | + // Contribution from oscillatory modes |
| 220 | + for (const auto& mode : osc_modes_) { |
| 221 | + force += mode.H_c * mode.z_c + mode.H_s * mode.z_s; |
| 222 | + } |
| 223 | + |
| 224 | + return force; |
112 | 225 | } |
113 | 226 |
|
114 | | -} // namespace hydrochrono::hydro |
| 227 | +Eigen::VectorXd RadiationStateSpaceModel::ReconstructKernel(double dt, int num_samples) const { |
| 228 | + Eigen::VectorXd K(num_samples); |
| 229 | + |
| 230 | + for (int k = 0; k < num_samples; ++k) { |
| 231 | + double t = k * dt; |
| 232 | + double val = 0.0; |
115 | 233 |
|
| 234 | + // Exponential mode contributions: K(t) = H * b * exp(-α*t) |
| 235 | + for (const auto& mode : exp_modes_) { |
| 236 | + val += mode.H * mode.b * std::exp(-mode.alpha * t); |
| 237 | + } |
| 238 | + |
| 239 | + // Oscillatory mode contributions |
| 240 | + // Impulse response: z_c(t), z_s(t) starting from z_c(0)=b_c, z_s(0)=b_s |
| 241 | + for (const auto& mode : osc_modes_) { |
| 242 | + double exp_decay = std::exp(-mode.alpha * t); |
| 243 | + double cos_wt = std::cos(mode.omega * t); |
| 244 | + double sin_wt = std::sin(mode.omega * t); |
| 245 | + |
| 246 | + double z_c_t = exp_decay * (mode.b_c * cos_wt - mode.b_s * sin_wt); |
| 247 | + double z_s_t = exp_decay * (mode.b_c * sin_wt + mode.b_s * cos_wt); |
| 248 | + |
| 249 | + val += mode.H_c * z_c_t + mode.H_s * z_s_t; |
| 250 | + } |
| 251 | + |
| 252 | + K(k) = val; |
| 253 | + } |
| 254 | + |
| 255 | + return K; |
| 256 | +} |
| 257 | + |
| 258 | +} // namespace hydrochrono::hydro |
0 commit comments