STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
RobustCalibrationFramework.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cmath>
5#include <iostream>
6
8
9namespace fsw {
10namespace calibration {
11
12namespace {
13constexpr double kLog2 = 0.69314718055994530942;
14
15double steady_secs(std::chrono::steady_clock::time_point t0,
16 std::chrono::steady_clock::time_point t1) {
17 return std::chrono::duration<double>(t1 - t0).count();
18}
19} // namespace
20
23 theta_mean_(Eigen::VectorXd::Zero(N)),
24 theta_cov_(Eigen::MatrixXd::Identity(N, N) * 0.05),
25 rls_P_(Eigen::MatrixXd::Identity(N, N) * 100.0),
27 glr_threshold_(2.0),
30 alpha_v_(1e-4),
31 alpha_extrap_(100.0),
32 bias_b_(Eigen::Vector3d::Zero()),
33 bias_P_(Eigen::Matrix3d::Identity() * 0.01),
34 bias_tau_(1.0, 100.0, 1e4),
36 last_pred_time_(std::chrono::steady_clock::now()),
39 allan_tau0_(0.01),
40 sigma_ref_(10.0) {
41 theta_mean_ << 0.0, 200.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0;
42 env_var_env_ = Eigen::MatrixXd::Identity(5, 5) * 0.0001;
43 env_var_interaction_ = Eigen::MatrixXd::Identity(5, 5) * 0.00001;
44}
45
46namespace {
47constexpr int kFactorySeedGridPoints = 48;
48constexpr double kRidgeLambda = 1e-3;
49
50int32_t clamp_adc_i32(double adc_d) {
51 constexpr double lo = -2147483648.0;
52 constexpr double hi = 2147483647.0;
53 if (adc_d <= lo)
54 return static_cast<int32_t>(lo);
55 if (adc_d >= hi)
56 return static_cast<int32_t>(hi);
57 return static_cast<int32_t>(std::llround(adc_d));
58}
59} // namespace
60
62 // Factory: psi = A*adc^3 + ... in raw int32 counts. Robust model: psi ≈ phi(adc)^T theta with
63 // 9-D environmental basis. Fit theta by ridge regression on a grid so streaming predictions
64 // match the factory curve (not a linear approximation of a cubic).
66 Eigen::MatrixXd Phi(kFactorySeedGridPoints, N);
67 Eigen::VectorXd y(kFactorySeedGridPoints);
68 const double lo = -2e8;
69 const double hi = 1.8e9;
70 for (int i = 0; i < kFactorySeedGridPoints; ++i) {
71 double t = static_cast<double>(i) / static_cast<double>(kFactorySeedGridPoints - 1);
72 double adc_d = lo + t * (hi - lo);
73 int32_t adc_i = clamp_adc_i32(adc_d);
74 Phi.row(i) = environmental_basis(static_cast<double>(adc_i), env).transpose();
75 y(i) = c.calculate_pressure(adc_i);
76 }
77 Eigen::MatrixXd normal = Phi.transpose() * Phi + kRidgeLambda * Eigen::MatrixXd::Identity(N, N);
78 Eigen::VectorXd rhs = Phi.transpose() * y;
79 Eigen::LDLT<Eigen::MatrixXd> ldlt(normal);
80 if (ldlt.info() != Eigen::Success) {
81 theta_mean_.setZero();
82 theta_mean_ << 0.0, 200.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0;
83 theta_cov_ = Eigen::MatrixXd::Identity(N, N) * 0.05;
84 rls_P_ = Eigen::MatrixXd::Identity(N, N) * 100.0;
85 return;
86 }
87 theta_mean_ = ldlt.solve(rhs);
88 Eigen::VectorXd resid = Phi * theta_mean_ - y;
89 double rmse = std::sqrt(resid.squaredNorm() / static_cast<double>(kFactorySeedGridPoints));
90 double var = std::max(rmse * rmse, 1e-8);
91 theta_cov_ = Eigen::MatrixXd::Identity(N, N) * var;
92 rls_P_ = Eigen::MatrixXd::Identity(N, N) * 100.0;
93}
94
97 double max_e = 0.0;
98 const int n = 64;
99 const double lo = -2e8;
100 const double hi = 1.8e9;
101 for (int i = 0; i < n; ++i) {
102 double t = static_cast<double>(i) / static_cast<double>(n - 1);
103 double adc_d = lo + t * (hi - lo);
104 int32_t adc_i = clamp_adc_i32(adc_d);
105 Eigen::VectorXd phi = environmental_basis(static_cast<double>(adc_i), env);
106 double pr = phi.dot(theta_mean_);
107 double pf = c.calculate_pressure(adc_i);
108 max_e = std::max(max_e, std::abs(pr - pf));
109 }
110 return max_e;
111}
112
113void RobustCalibrationFramework::set_theta_from_polynomial(const std::vector<double>& poly_coeffs,
114 double adc_norm_min,
115 double adc_norm_scale) {
116 if (poly_coeffs.size() < 2 || adc_norm_scale <= 0)
117 return;
118 double c0 = poly_coeffs[0];
119 double c1 = poly_coeffs[1];
120 double theta1 = c1 * 1e9 / adc_norm_scale;
121 double theta0 = c0 - theta1 * adc_norm_min / 1e9;
122 theta_mean_.setZero();
123 theta_mean_(0) = theta0;
124 theta_mean_(1) = theta1;
125}
126
128 noise_coeffs_ = c;
129 allan_tau0_ = tau0;
130}
131
133 if (dt <= 0)
134 return;
135 Eigen::Vector3d phi_b = (-dt * bias_tau_.array().inverse()).array().exp();
136 Eigen::Matrix3d Phi = phi_b.asDiagonal();
137 Eigen::Vector3d q =
138 bias_process_noise_ * (1.0 - (-2.0 * dt * bias_tau_.array().inverse()).array().exp());
139 Eigen::Matrix3d Q = q.asDiagonal();
140 bias_b_ = Phi * bias_b_;
141 bias_P_ = Phi * bias_P_ * Phi.transpose() + Q;
142}
143
144void RobustCalibrationFramework::bias_update(double residual, double sigma_meas, double dt) {
145 bias_propagate(dt);
146 Eigen::Vector3d h = Eigen::Vector3d::Ones();
147 double S = (h.transpose() * bias_P_ * h)(0, 0) + sigma_meas * sigma_meas;
148 S = std::max(S, 1e-12);
149 Eigen::Vector3d K = bias_P_ * h / S;
150 bias_b_ = bias_b_ + K * residual;
151 bias_P_ = (Eigen::Matrix3d::Identity() - K * h.transpose()) * bias_P_;
152}
153
155 return bias_b_.sum();
156}
157
159 Eigen::Vector3d h = Eigen::Vector3d::Ones();
160 return (h.transpose() * bias_P_ * h)(0, 0);
161}
162
164 double adc_code, const EnvironmentalState& env) const {
165 const double T = env.temperature;
166 const double H = env.humidity;
167 const double V = env.vibration;
168 const double A = env.aging_factor;
169 const double M = env.mounting_torque;
170 const double adc_norm = adc_code / 1e9;
171 const double v = std::max(adc_norm, 1e-6);
172
173 Eigen::VectorXd phi(N);
174 phi(0) = 1.0;
175 phi(1) = adc_norm;
176 phi(2) =
177 adc_norm * adc_norm + physical_.alpha1 * T * adc_norm + physical_.alpha2 * H * adc_norm;
178 phi(3) = adc_norm * adc_norm * adc_norm + physical_.beta1 * T * adc_norm * adc_norm +
179 physical_.beta2 * V * adc_norm;
180 phi(4) = std::sqrt(v) + physical_.gamma1 * A * std::log(v);
181 phi(5) = std::log1p(adc_norm) + physical_.delta1 * T + physical_.delta2 * H;
182 phi(6) = adc_norm * T * H;
183 phi(7) = adc_norm * adc_norm * V * M;
184 phi(8) = A * adc_norm * adc_norm * adc_norm;
185 return phi;
186}
187
188std::pair<Eigen::VectorXd, Eigen::MatrixXd>
190 const std::vector<CalibrationPoint>& points) const {
191 if (points.size() < 3)
192 return {theta_mean_, theta_cov_};
193
194 const int n = static_cast<int>(points.size());
195 Eigen::MatrixXd Phi(n, N);
196 Eigen::VectorXd p_obs(n);
197 Eigen::VectorXd weights(n);
198 for (int i = 0; i < n; ++i) {
199 Phi.row(i) = environmental_basis(points[static_cast<size_t>(i)].adc_code,
200 points[static_cast<size_t>(i)].env)
201 .transpose();
202 p_obs(i) = points[static_cast<size_t>(i)].pressure;
203 double u = points[static_cast<size_t>(i)].uncertainty;
204 weights(i) = 1.0 / (u * u + env_variance_base_);
205 }
206
207 Eigen::MatrixXd Phi_T_W = Phi.transpose() * weights.asDiagonal();
208 Eigen::MatrixXd cov_inv = Phi_T_W * Phi + Eigen::MatrixXd::Identity(N, N) * 1e-6;
209 Eigen::LDLT<Eigen::MatrixXd> ldlt(cov_inv);
210 if (ldlt.info() != Eigen::Success)
211 return {theta_mean_, theta_cov_};
212 Eigen::VectorXd rhs = Phi_T_W * p_obs;
213 Eigen::VectorXd theta_mean = ldlt.solve(rhs);
214 Eigen::MatrixXd I = Eigen::MatrixXd::Identity(N, N);
215 Eigen::MatrixXd theta_cov = ldlt.solve(I);
216 return {theta_mean, theta_cov};
217}
218
219std::pair<Eigen::VectorXd, Eigen::MatrixXd> RobustCalibrationFramework::bayesian_update(
220 const std::vector<CalibrationPoint>& new_points) {
221 if (new_points.empty())
222 return {theta_mean_, theta_cov_};
223 auto [theta_tls, theta_cov_tls] = total_least_squares_calibration(new_points);
224 Eigen::MatrixXd I = Eigen::MatrixXd::Identity(N, N) * 1e-6;
225 Eigen::MatrixXd prior_cov = theta_cov_ + I;
226 Eigen::MatrixXd lik_cov = theta_cov_tls + I;
227 Eigen::LDLT<Eigen::MatrixXd> ldlt_prior(prior_cov);
228 Eigen::LDLT<Eigen::MatrixXd> ldlt_lik(lik_cov);
229 if (ldlt_prior.info() != Eigen::Success || ldlt_lik.info() != Eigen::Success)
230 return {theta_mean_, theta_cov_};
231 Eigen::MatrixXd prior_prec = ldlt_prior.solve(Eigen::MatrixXd::Identity(N, N));
232 Eigen::MatrixXd lik_prec = ldlt_lik.solve(Eigen::MatrixXd::Identity(N, N));
233 Eigen::MatrixXd post_prec = prior_prec + lik_prec;
234 Eigen::LDLT<Eigen::MatrixXd> ldlt_post(post_prec);
235 if (ldlt_post.info() != Eigen::Success)
236 return {theta_mean_, theta_cov_};
237 Eigen::MatrixXd post_cov = ldlt_post.solve(Eigen::MatrixXd::Identity(N, N));
238 Eigen::VectorXd post_mean = post_cov * (prior_prec * theta_mean_ + lik_prec * theta_tls);
239 return {post_mean, post_cov};
240}
241
242std::pair<Eigen::VectorXd, Eigen::MatrixXd>
244 Eigen::VectorXd phi = environmental_basis(point.adc_code, point.env);
245 Eigen::VectorXd Pphi = rls_P_ * phi;
246 double denom = forgetting_factor_ + phi.dot(Pphi);
247 if (std::abs(denom) < 1e-18)
248 return {theta_mean_, theta_cov_};
249 Eigen::VectorXd K = Pphi / denom;
250 double prediction_error = point.pressure - phi.dot(theta_mean_);
251 Eigen::VectorXd theta_new = theta_mean_ + K * prediction_error;
252 rls_P_ = (rls_P_ - K * Pphi.transpose()) / forgetting_factor_;
253 Eigen::MatrixXd theta_cov_new = rls_P_ + Eigen::MatrixXd::Identity(N, N) * 0.001;
254 return {theta_new, theta_cov_new};
255}
256
258 const std::vector<CalibrationPoint>& points) const {
259 if (static_cast<int>(points.size()) < glr_window_size_)
260 return {0.0, false};
261
262 std::vector<CalibrationPoint> recent(points.end() - glr_window_size_, points.end());
263
264 double log_lik_current = 0;
265 for (const auto& point : recent) {
266 Eigen::VectorXd phi = environmental_basis(point.adc_code, point.env);
267 double pred = phi.dot(theta_mean_);
268 double res = point.pressure - pred;
269 double var = point.uncertainty * point.uncertainty + env_variance_base_;
270 log_lik_current += -0.5 * (res * res / var + std::log(2.0 * std::acos(-1.0) * var));
271 }
272
273 auto [theta_tls, _] = total_least_squares_calibration(recent);
274
275 double log_lik_unc = 0;
276 for (const auto& point : recent) {
277 Eigen::VectorXd phi = environmental_basis(point.adc_code, point.env);
278 double pred = phi.dot(theta_tls);
279 double res = point.pressure - pred;
280 double var = point.uncertainty * point.uncertainty + env_variance_base_;
281 log_lik_unc += -0.5 * (res * res / var + std::log(2.0 * std::acos(-1.0) * var));
282 }
283
284 double glr = 2.0 * (log_lik_unc - log_lik_current);
285 return {glr, glr > glr_threshold_};
286}
287
289 if (calibration_points_.empty())
290 return 0.0;
291 double v_min = calibration_points_.front().adc_code;
292 double v_max = calibration_points_.front().adc_code;
293 for (const auto& p : calibration_points_) {
294 v_min = std::min(v_min, p.adc_code);
295 v_max = std::max(v_max, p.adc_code);
296 }
297 const double delta_v = std::max(v_max - v_min, 1e6);
298 const double exp_cap = 100.0;
299 auto penalty = [&](double d) {
300 double exp_val = std::exp(std::min(2.0 * d, exp_cap));
301 return alpha_extrap_ * d * d * exp_val;
302 };
303 if (adc_code < v_min) {
304 double d = (v_min - adc_code) / delta_v;
305 return penalty(d);
306 }
307 if (adc_code > v_max) {
308 double d = (adc_code - v_max) / delta_v;
309 return penalty(d);
310 }
311 return 0.0;
312}
313
314double RobustCalibrationFramework::compute_measurement_variance(double adc_code, double tau) const {
315 const double adc_norm = adc_code / 1e9;
316 if (noise_coeffs_) {
317 double sigma = measurement_uncertainty(tau, *noise_coeffs_, adc_norm * 2.5,
318 std::sqrt(env_variance_base_));
319 return sigma * sigma;
320 }
321 return env_variance_base_ + alpha_v_ * adc_norm * adc_norm;
322}
323
325 double adc_code, const EnvironmentalState& env) {
326 auto now = std::chrono::steady_clock::now();
327 double dt = steady_secs(last_pred_time_, now);
328 dt = std::clamp(dt, 0.001, 10.0);
329 last_pred_time_ = now;
330
331 Eigen::VectorXd phi = environmental_basis(adc_code, env);
332 double predicted = phi.dot(theta_mean_);
333 bias_propagate(dt);
334 predicted += bias_contribution();
335
336 double meas_var = compute_measurement_variance(adc_code, 0.1);
337 double parameter_variance = (phi.transpose() * theta_cov_ * phi)(0, 0);
338 double extrap_var = extrapolation_variance(adc_code);
339 double bias_var = bias_variance();
340
341 Eigen::VectorXd ev(5);
342 ev << env.temperature, env.humidity, env.vibration, env.aging_factor, env.mounting_torque;
343 double env_var = (ev.transpose() * env_var_env_ * ev)(0, 0);
344 const double adc_norm = adc_code / 1e9;
345 double interaction_var =
346 adc_norm * adc_norm * (ev.transpose() * env_var_interaction_ * ev)(0, 0);
347
348 double total_var =
349 meas_var + parameter_variance + extrap_var + bias_var + env_var + interaction_var;
350 double sigma = std::sqrt(std::max(total_var, 1e-12)) * inflation_factor_;
351 recent_uncertainties_.push_back(sigma);
352 if (recent_uncertainties_.size() > 10)
353 recent_uncertainties_.pop_front();
354 // Cubic seed + bad ADC / θ can yield ±inf / 1e12; never publish that to Elodin.
355 constexpr double kPsiAbsMax = 25000.0;
356 constexpr double kPsiNegMin = -3000.0;
357 if (!std::isfinite(predicted))
358 predicted = 0.0;
359 else
360 predicted = std::clamp(predicted, kPsiNegMin, kPsiAbsMax);
361 return {predicted, sigma};
362}
363
366 return predict_pressure_with_uncertainty(adc_code, env).first;
367}
368
370 calibration_points_.push_back(point);
373 calibration_points_.begin(),
374 calibration_points_.begin() +
375 static_cast<std::ptrdiff_t>(calibration_points_.size() - kCalibrationPointsMax));
376
377 Eigen::VectorXd phi = environmental_basis(point.adc_code, point.env);
378 double pred_before = phi.dot(theta_mean_) + bias_contribution();
379 double residual = point.pressure - pred_before;
380 double dt_point = point.timestamp - last_bias_update_t_;
381 if (last_bias_update_t_ <= 0)
382 dt_point = 0.01;
383 dt_point = std::clamp(dt_point, 0.001, 10.0);
385 double sigma_meas = std::max(point.uncertainty, 1e-4);
386 bias_update(residual, sigma_meas, dt_point);
387
388 // First point at ~0 PSI: cancel θ₀ contribution from higher basis terms (Python)
389 if (calibration_points_.size() == 1 && std::abs(point.pressure) < 1e-6) {
390 double contrib_rest = theta_mean_.tail(N - 1).dot(phi.tail(N - 1));
391 theta_mean_(0) = -contrib_rest;
392 bias_b_.setZero();
393 bias_P_ = Eigen::Matrix3d::Identity() * 0.01;
394 return;
395 }
396
398 (void)glr_stat;
399 if (drift) {
401 theta_mean_ = tm;
402 theta_cov_ = tc;
403 } else {
404 auto [tm, tc] = recursive_least_squares_update(point);
405 theta_mean_ = tm;
406 theta_cov_ = tc;
407 }
408
409 phi = environmental_basis(point.adc_code, point.env);
410 Eigen::VectorXd Pphi = rls_P_ * phi;
411 double denom = forgetting_factor_ + phi.dot(Pphi);
412 if (std::abs(denom) > 1e-18) {
413 Eigen::VectorXd K = Pphi / denom;
414 rls_P_ = (rls_P_ - K * Pphi.transpose()) / forgetting_factor_;
415 }
416
417 double pred = phi.dot(theta_mean_);
418 recent_residuals_.push_back(std::abs(point.pressure - pred));
419 if (recent_residuals_.size() > 10)
420 recent_residuals_.pop_front();
421}
422
424 if (t.size() == N)
425 theta_mean_ = t;
426}
427
429 if (cov.rows() == N && cov.cols() == N)
430 theta_cov_ = cov;
431}
432
434 if (p.rows() == N && p.cols() == N)
435 rls_P_ = p;
436}
437
438} // namespace calibration
439} // namespace fsw
std::pair< double, bool > generalized_likelihood_ratio_test(const std::vector< CalibrationPoint > &points) const
std::pair< Eigen::VectorXd, Eigen::MatrixXd > bayesian_update(const std::vector< CalibrationPoint > &new_points)
Eigen::VectorXd environmental_basis(double adc_code, const EnvironmentalState &env) const
void seed_from_factory_cubic(const PTCalibrationCoeffs &c)
Seed θ by ridge LS so Φ·θ ≈ factory cubic P(adc) on an ADC grid (same env as streaming).
void bias_update(double residual, double sigma_meas, double dt)
struct fsw::calibration::RobustCalibrationFramework::PhysicalParams physical_
double predict_pressure_psi(double adc_code)
Mean PSI for streaming (default env, used by calibration_service).
void set_theta_from_polynomial(const std::vector< double > &poly_coeffs, double adc_norm_min, double adc_norm_scale)
std::pair< double, double > predict_pressure_with_uncertainty(double adc_code, const EnvironmentalState &env)
Mean + σ; updates bias propagation clock.
void add_calibration_point(const CalibrationPoint &point)
Zero / capture / reference — full Python add_calibration_point logic.
double max_abs_error_vs_factory(const PTCalibrationCoeffs &factory_cubic) const
Max |Φ·θ − P_factory(adc)| over a dense ADC grid — sanity-check saved adjustments....
std::pair< Eigen::VectorXd, Eigen::MatrixXd > recursive_least_squares_update(const CalibrationPoint &point)
std::chrono::steady_clock::time_point last_pred_time_
std::pair< Eigen::VectorXd, Eigen::MatrixXd > total_least_squares_calibration(const std::vector< CalibrationPoint > &points) const
double compute_measurement_variance(double adc_code, double tau) const
void set_noise_coeffs(const NoiseCoefficients &c, double tau0=0.01)
int32_t p
Definition flight_e2e.c:78
json_pointer res
Definition json.hpp:13957
*brief create a new JSON pointer by appending the right JSON pointer at the end of the left JSON pointer *sa const json_pointer & rhs
Definition json.hpp:13930
double steady_secs(std::chrono::steady_clock::time_point t0, std::chrono::steady_clock::time_point t1)
Output smoothing for calibrated PT pressure, per sensor uid (ESP32 and LabJack T7 alike).
double measurement_uncertainty(double tau, const NoiseCoefficients &coeffs, double voltage=0.0, double sigma_base=1e-3)
Measurement σ from paper Section 7 (Python measurement_uncertainty).
Motorized ball valve position: raw ADC counts -> % open.
*brief user defined string literal for JSON values *sa std::size_t n
Definition json.hpp:24474
STL namespace.
Environmental state [T, H, V, A, M] — scripts/calibration/robust_calibration.py.
Noise coefficients from Allan variance fit (scripts/calibration/allan_variance.py).
PT Calibration Coefficients.
double calculate_pressure(int32_t adc_code) const
Calculate pressure (psi) from ADC code.