Skip to content

Commit 63ecd31

Browse files
committed
Unified state-space model with oscillatory mode support
1 parent 014e33b commit 63ecd31

3 files changed

Lines changed: 626 additions & 423 deletions

File tree

Lines changed: 221 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,258 @@
11
/*********************************************************************
22
* @file radiation_ss_model.cpp
33
* @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+
*
423
*********************************************************************/
524

625
#include "radiation_ss_model.h"
26+
#include "radiation_ss_fitter.h"
727

828
#include <cmath>
29+
#include <complex>
30+
#include <algorithm>
931
#include <stdexcept>
10-
#include <string>
1132

1233
namespace hydrochrono::hydro {
1334

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;
2140
}
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+
}
26127
}
27128

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;
32130
}
33131

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");
44135
}
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");
49144
}
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");
55147
}
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);
59158
}
60159

61160
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+
}
63168
}
64169

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");
70173
}
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;
76181
}
77182

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;
92201

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;
95205

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;
99208
}
100209
}
101210

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;
109213

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;
112225
}
113226

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;
115233

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

Comments
 (0)