STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
robust_calibration.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""
3Robust Calibration Framework for Pressure Transducers
4Based on the mathematical framework from PressureTransducerCalibrationFramework.tex
5
6Implements:
7- Bayesian regression with hierarchical priors
8- Total Least Squares (TLS) for robust calibration
9- Recursive Least Squares (RLS) with forgetting
10- Generalized Likelihood Ratio (GLR) testing for drift detection
11- Environmental-robust calibration maps
12- Extended Kalman Filter integration for bias correction
13"""
14
15import time
16import numpy as np
17import scipy.linalg as la
18import scipy.stats as stats
19from typing import Dict, List, Tuple, Optional, TYPE_CHECKING
20
21if TYPE_CHECKING:
22 from allan_variance import NoiseCoefficients
23from dataclasses import dataclass
24from collections import deque
25import logging
26
27logger = logging.getLogger(__name__)
28
29
30@dataclass
32 """Environmental state vector: [T, H, V, A, M]"""
33
34 temperature: float = 25.0 # °C
35 humidity: float = 50.0 # %
36 vibration: float = 0.0 # g
37 aging_factor: float = 1.0 # dimensionless
38 mounting_torque: float = 1.0 # dimensionless
39
40
42 """
43 Paper Section 4: Stochastic bias db/dt = -b/τ_b + w_b.
44 3-state bias: τ_fast=1s, τ_medium=100s, τ_slow=10⁴s.
45 Discrete: b_{k+1} = Φ_b b_k + w_b.
46 """
47
49 self,
50 tau_fast: float = 1.0,
51 tau_medium: float = 100.0,
52 tau_slow: float = 1e4,
53 process_noise: float = 1e-4,
54 ):
55 self.tau = np.array([tau_fast, tau_medium, tau_slow])
56 self.b = np.zeros(3)
57 self.P_b = np.eye(3) * 0.01 # bias covariance
58 self.process_noise = process_noise
59 self._last_t = 0.0
60 self.h = np.ones(3) # observation: pressure += h'b
61
62 def propagate(self, dt: float) -> None:
63 """b_{k+1} = Φ_b b_k, P_{k+1} = Φ_b P Φ_b' + Q."""
64 phi_b = np.diag(np.exp(-dt / self.tau))
65 q = self.process_noise * (1 - np.exp(-2 * dt / self.tau))
66 Q = np.diag(q)
67 self.b = phi_b @ self.b
68 self.P_b = phi_b @ self.P_b @ phi_b.T + Q
69
70 def update(self, residual: float, sigma_meas: float, dt: float) -> None:
71 """Innovation update: residual = y_obs - (φ'θ + h'b)."""
72 self.propagate(dt)
73 S = float(self.h @ self.P_b @ self.h + sigma_meas**2)
74 K = self.P_b @ self.h / max(S, 1e-12)
75 self.b = self.b + K * residual
76 self.P_b = (np.eye(3) - np.outer(K, self.h)) @ self.P_b
77
78 def bias_contribution(self) -> float:
79 """h'b for pressure prediction."""
80 return float(self.h @ self.b)
81
82 def bias_variance(self) -> float:
83 """h' Σ_b h for uncertainty."""
84 return float(self.h @ self.P_b @ self.h)
85
86
87@dataclass
89 """Single calibration data point"""
90
91 adc_code: float # Raw ADC code (signed 32-bit, not voltage)
92 pressure: float
93 timestamp: float
94 environmental_state: EnvironmentalState
95 uncertainty: float = 0.01 # Default 1% uncertainty
96
97
99 """
100 Mathematically robust calibration framework implementing the paper's algorithms
101 """
102
103 def __init__(self, sensor_id: int):
104 self.sensor_id = sensor_id
105
106 # Calibration data
107 self.calibration_points: List[CalibrationPoint] = []
108
109 # Bayesian parameters — 9-parameter basis (paper φ₀–φ₈)
110 self.n_params = 9
111 self.population_prior_mean = np.array(
112 [0.0, 200.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
113 ) # θ₀ offset, θ₁ linear (PSI/V), rest env terms
114 self.population_prior_cov = np.eye(9) * 0.1
115 self.individual_prior_cov = np.eye(9) * 0.05
116
117 # Current calibration parameters
120
121 # RLS parameters
122 self.rls_P = np.eye(9) * 100.0 # Initial covariance
124
125 # GLR test parameters
126 self.glr_threshold = 2.0 # Chi-squared threshold
128
129 # Environmental variance model (paper Section 7)
130 self.env_variance_base = 1e-3 # σ²_base
131 self.alpha_v = 1e-4 # voltage-dependent noise
132 self.alpha_extrap = 100.0 # extrapolation penalty (PSI²)
134 "env": np.eye(5) * 0.0001,
135 "interaction": np.eye(5) * 0.00001,
136 }
137
138 # Autonomy score (paper Section 8): α = 0.4·α_cal + 0.3·α_unc + 0.2·α_agree + 0.1·α_quality
139 self.recent_residuals: deque = deque(maxlen=10) # for α_agree
140 self.recent_uncertainties: deque = deque(maxlen=10) # for α_unc
142 50 # cap to prevent unbounded growth over long sessions
143 )
144 self.sigma_ref = 10.0 # PSI reference for α_unc
145
146 # Physical calibration map parameters
148 "alpha1": 0.001,
149 "alpha2": 0.0001,
150 "beta1": 0.0001,
151 "beta2": 0.00001,
152 "gamma1": 0.0001,
153 "delta1": 0.001,
154 "delta2": 0.0001,
155 }
156
157 # Gauss-Markov bias (paper Section 4)
161
162 # Inflation/deflation (paper Section 7): σ_adj = σ * inflation_factor
164
165 # Optional Allan variance: when set, use for σ²_meas
166 self._noise_coeffs: Optional["NoiseCoefficients"] = None
167 self._allan_tau0 = 0.01
168
170 self,
171 poly_coeffs: List[float],
172 adc_norm_min: float,
173 adc_norm_scale: float,
174 ) -> None:
175 """
176 Initialize theta from polynomial calibration P = c0 + c1*x + c2*x^2 + c3*x^3
177 where x = (adc - adc_norm_min) / adc_norm_scale.
178 Gives robust calibration the correct slope so ZERO ALL + CAPTURE match polynomial accuracy.
179 """
180 if len(poly_coeffs) < 2 or adc_norm_scale <= 0:
181 return
182 c0, c1 = float(poly_coeffs[0]), float(poly_coeffs[1])
183 # Robust: P = theta[0] + theta[1]*adc_norm, adc_norm = adc/1e9
184 # Polynomial slope dP/d(adc) = c1/scale. So theta[1]/1e9 = c1/scale → theta[1] = c1*1e9/scale
185 theta1 = c1 * 1e9 / adc_norm_scale
186 # At adc=adc_norm_min, x=0, P=c0. So theta[0] + theta[1]*min/1e9 = c0
187 theta0 = c0 - theta1 * adc_norm_min / 1e9
188 self.theta_mean = np.array([theta0, theta1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
189 self.population_prior_mean = self.theta_mean.copy()
190 logger.info(
191 f"Sensor {self.sensor_id}: prior from polynomial θ₀={theta0:.2f} θ₁={theta1:.0f}"
192 )
193
195 self, adc_code: float, env: EnvironmentalState
196 ) -> np.ndarray:
197 """
198 Environmental-robust basis functions from equation (66-72)
199 Works directly on ADC codes (not voltage).
200 """
201 T, H, V, A, M = (
202 env.temperature,
203 env.humidity,
204 env.vibration,
205 env.aging_factor,
206 env.mounting_torque,
207 )
208
209 # Normalize ADC code (v ≈ adc*2.5/2^31); use adc_norm ~ [-1,1] for stability
210 adc_norm = adc_code / 1e9
211 v = max(adc_norm, 1e-6) # clamp for log/sqrt
212
213 # Paper: 9-parameter environmental-robust basis (φ₀–φ₈)
214 phi = np.zeros(9)
215 phi[0] = 1.0
216 phi[1] = adc_norm
217 phi[2] = (
218 adc_norm**2
219 + self.physical_params["alpha1"] * T * adc_norm
220 + self.physical_params["alpha2"] * H * adc_norm
221 )
222 phi[3] = (
223 adc_norm**3
224 + self.physical_params["beta1"] * T * adc_norm**2
225 + self.physical_params["beta2"] * V * adc_norm
226 )
227 phi[4] = np.sqrt(v) + self.physical_params["gamma1"] * A * np.log(v)
228 phi[5] = (
229 np.log(1 + adc_norm)
230 + self.physical_params["delta1"] * T
231 + self.physical_params["delta2"] * H
232 )
233 phi[6] = adc_norm * T * H # temp-humidity interaction
234 phi[7] = adc_norm**2 * V * M # vibration-mounting interaction
235 phi[8] = A * adc_norm**3 # aging-nonlinearity coupling
236
237 return phi
238
240 self, points: List[CalibrationPoint]
241 ) -> Tuple[np.ndarray, np.ndarray]:
242 """
243 Total Least Squares calibration accounting for errors in both voltage and pressure
244 Equation (112-118)
245 """
246 if len(points) < 3:
247 return self.theta_mean, self.theta_cov
248
249 n = len(points)
250 Phi = np.zeros((n, 9))
251 p_obs = np.zeros(n)
252 weights = np.zeros(n)
253
254 for i, point in enumerate(points):
256 point.adc_code, point.environmental_state
257 )
258 p_obs[i] = point.pressure
259 weights[i] = 1.0 / (point.uncertainty**2 + self.env_variance_base)
260
261 # Weighted least squares
262 W = np.diag(weights)
263 Phi_T_W = Phi.T @ W
264 try:
265 cov_inv = Phi_T_W @ Phi + np.eye(9) * 1e-6 # Regularization
266 theta_mean = la.solve(cov_inv, Phi_T_W @ p_obs)
267 theta_cov = la.inv(cov_inv)
268 except la.LinAlgError:
269 logger.warning(
270 f"TLS calibration failed for sensor {self.sensor_id}, using previous parameters"
271 )
272 return self.theta_mean, self.theta_cov
273
274 return theta_mean, theta_cov
275
277 self, new_points: List[CalibrationPoint]
278 ) -> Tuple[np.ndarray, np.ndarray]:
279 """
280 Bayesian update with hierarchical priors
281 Equations (126-149)
282 """
283 if not new_points:
284 return self.theta_mean, self.theta_cov
285
286 # Compute likelihood parameters
287 theta_mean_tls, theta_cov_tls = self.total_least_squares_calibration(new_points)
288
289 # Hierarchical Bayesian update
290 # Prior: N(theta_mean, theta_cov)
291 # Likelihood: N(theta_mean_tls, theta_cov_tls)
292
293 # Posterior precision matrix
294 prior_precision = la.inv(self.theta_cov + np.eye(9) * 1e-6)
295 likelihood_precision = la.inv(theta_cov_tls + np.eye(9) * 1e-6)
296 posterior_precision = prior_precision + likelihood_precision
297
298 # Posterior mean
299 posterior_cov = la.inv(posterior_precision)
300 posterior_mean = posterior_cov @ (
301 prior_precision @ self.theta_mean + likelihood_precision @ theta_mean_tls
302 )
303
304 return posterior_mean, posterior_cov
305
307 self, point: CalibrationPoint
308 ) -> Tuple[np.ndarray, np.ndarray]:
309 """
310 Recursive Least Squares with forgetting factor
311 Equations (162-166)
312 """
314 point.adc_code, point.environmental_state
315 )
316
317 # RLS update equations
318 Pphi = self.rls_P @ phi # (9,)
319 denom = self.forgetting_factor + phi @ Pphi # scalar
320 K = Pphi / denom # (6,)
321 prediction_error = point.pressure - phi @ self.theta_mean
322
323 # Update parameters
324 self.theta_mean = self.theta_mean + K * prediction_error
325 self.rls_P = (self.rls_P - np.outer(K, Pphi)) / self.forgetting_factor
326
327 # Add forgetting covariance
328 forgetting_cov = np.eye(9) * 0.001
329 self.theta_cov = self.rls_P + forgetting_cov
330
331 return self.theta_mean, self.theta_cov
332
334 self, new_points: List[CalibrationPoint]
335 ) -> Tuple[float, bool]:
336 """
337 GLR test for drift detection
338 Equations (235-248)
339 """
340 if len(new_points) < self.glr_window_size:
341 return 0.0, False
342
343 # Use recent points for GLR test
344 recent_points = new_points[-self.glr_window_size :]
345
346 # Compute likelihood under current model
347 log_likelihood_current = 0.0
348 for point in recent_points:
350 point.adc_code, point.environmental_state
351 )
352 predicted_pressure = phi.T @ self.theta_mean
353 residual = point.pressure - predicted_pressure
354 variance = point.uncertainty**2 + self.env_variance_base
355 log_likelihood_current += -0.5 * (
356 residual**2 / variance + np.log(2 * np.pi * variance)
357 )
358
359 # Compute likelihood under unconstrained model (TLS fit to recent data)
360 theta_tls, _ = self.total_least_squares_calibration(recent_points)
361 log_likelihood_unconstrained = 0.0
362 for point in recent_points:
364 point.adc_code, point.environmental_state
365 )
366 predicted_pressure = phi.T @ theta_tls
367 residual = point.pressure - predicted_pressure
368 variance = point.uncertainty**2 + self.env_variance_base
369 log_likelihood_unconstrained += -0.5 * (
370 residual**2 / variance + np.log(2 * np.pi * variance)
371 )
372
373 # GLR statistic
374 glr_statistic = 2 * (log_likelihood_unconstrained - log_likelihood_current)
375
376 # Drift detected if GLR exceeds threshold
377 drift_detected = glr_statistic > self.glr_threshold
378
379 return glr_statistic, drift_detected
380
381 def _extrapolation_variance(self, adc_code: float) -> float:
382 """Paper Section 7: σ²_extrap when adc outside calibrated range."""
383 if not self.calibration_points:
384 return 0.0
385 adcs = np.array([p.adc_code for p in self.calibration_points])
386 v_min, v_max = adcs.min(), adcs.max()
387 delta_v = max(v_max - v_min, 1e6)
388 # Cap exp(2*d) to avoid overflow (~709); use 100 for reasonable extrap penalty
389 exp_cap = 100.0
390 if adc_code < v_min:
391 d = (v_min - adc_code) / delta_v
392 exp_val = np.exp(min(2 * d, exp_cap))
393 return self.alpha_extrap * d**2 * exp_val
394 if adc_code > v_max:
395 d = (adc_code - v_max) / delta_v
396 exp_val = np.exp(min(2 * d, exp_cap))
397 return self.alpha_extrap * d**2 * exp_val
398 return 0.0
399
400 def set_noise_coeffs(self, coeffs: "NoiseCoefficients", tau0: float = 0.01) -> None:
401 """Use Allan variance NoiseCoefficients for σ²_meas (paper Section 3→7)."""
402 self._noise_coeffs = coeffs
403 self._allan_tau0 = tau0
404
405 def _measurement_variance(self, adc_code: float, tau: float = 0.1) -> float:
406 """σ²_meas: Allan-derived if available, else env_variance_base + α_v·v²."""
407 adc_norm = adc_code / 1e9
408 if self._noise_coeffs is not None:
409 from allan_variance import measurement_uncertainty
410
411 sigma = measurement_uncertainty(
412 tau,
413 self._noise_coeffs,
414 voltage=adc_norm * 2.5,
415 sigma_base=np.sqrt(self.env_variance_base),
416 )
417 return sigma**2
418 return self.env_variance_base + self.alpha_v * (adc_norm**2)
419
421 self, adc_code: float, env: EnvironmentalState
422 ) -> Tuple[float, float]:
423 """
424 Predict pressure with full uncertainty quantification (paper Section 7).
425 σ²_pred = σ²_meas + φ'Σφ + σ²_extrap + h'Σ_b h + env + interaction
426 Mean: ŷ = φ'θ + h'b (Gauss-Markov bias).
427 """
428 now = time.time()
429 dt = min(
430 max(now - self._last_pred_t, 0.001), 10.0
431 ) # cap for first call / long gaps
432 self._last_pred_t = now
433
434 phi = self.environmental_robust_basis_functions(adc_code, env)
435
436 # Mean: φ'θ + h'b (paper Section 4)
437 predicted_pressure = float(phi.T @ self.theta_mean)
438 self.bias_model.propagate(dt)
439 predicted_pressure += self.bias_model.bias_contribution()
440
441 # 1. Measurement variance (Allan or heteroscedastic)
442 measurement_variance = self._measurement_variance(adc_code)
443
444 # 2. Parameter uncertainty
445 parameter_variance = float(phi.T @ self.theta_cov @ phi)
446
447 # 3. Extrapolation uncertainty
448 extrap_variance = self._extrapolation_variance(adc_code)
449
450 # 4. Bias uncertainty (paper Section 4)
451 bias_variance = self.bias_model.bias_variance()
452
453 # 5. Environmental variance
454 adc_norm = adc_code / 1e9
455 env_vector = np.array(
456 [
457 env.temperature,
458 env.humidity,
459 env.vibration,
460 env.aging_factor,
461 env.mounting_torque,
462 ]
463 )
464 env_variance = float(
465 env_vector.T @ self.env_variance_matrices["env"] @ env_vector
466 )
467 interaction_variance = float(
468 adc_norm**2
469 * env_vector.T
470 @ self.env_variance_matrices["interaction"]
471 @ env_vector
472 )
473
474 total_variance = (
475 measurement_variance
476 + parameter_variance
477 + extrap_variance
478 + bias_variance
479 + env_variance
480 + interaction_variance
481 )
482 sigma = np.sqrt(max(total_variance, 1e-12)) * self.inflation_factor
483 self.recent_uncertainties.append(sigma)
484 return predicted_pressure, sigma
485
486 def add_calibration_point(self, point: CalibrationPoint) -> Dict[str, any]:
487 """
488 Add a new calibration point and update the model
489 """
490 self.calibration_points.append(point)
491 if len(self.calibration_points) > self.calibration_points_max:
494 ]
496 point.adc_code, point.environmental_state
497 )
498 pred_before = (
499 float(phi.T @ self.theta_mean) + self.bias_model.bias_contribution()
500 )
501 residual = point.pressure - pred_before
502 dt = point.timestamp - self._last_bias_update_t
503 self._last_bias_update_t = point.timestamp
504 sigma_meas = max(point.uncertainty, 1e-4)
505 self.bias_model.update(residual, sigma_meas, min(max(dt, 0.001), 10.0))
506
507 # Single zero-point (ZERO ALL): force prediction = 0 at this ADC.
508 # Prior theta[1]=200 gives pressure ≈ 200*adc_norm, which is ~-30 at vacuum ADC.
509 # One RLS step does not fully correct; set theta[0] to cancel the rest.
510 if len(self.calibration_points) == 1 and abs(point.pressure) < 1e-6:
511 contrib_rest = float(phi[1:].T @ self.theta_mean[1:])
512 self.theta_mean[0] = -contrib_rest
513 self.bias_model.b = np.zeros(3)
514 self.bias_model.P_b = np.eye(3) * 0.01
515 return {
516 "drift_detected": False,
517 "glr_statistic": 0.0,
518 "calibration_points": 1,
519 "confidence_level": self.get_confidence_level(),
520 }
521
522 # Check for drift using GLR test
523 glr_statistic, drift_detected = self.generalized_likelihood_ratio_test(
525 )
526
527 if drift_detected:
528 logger.info(
529 f"Sensor {self.sensor_id}: Drift detected (GLR={glr_statistic:.3f}), recalibrating"
530 )
531 # Full Bayesian recalibration
532 self.theta_mean, self.theta_cov = self.bayesian_update(
534 )
535 else:
536 # RLS update
538
539 # Update RLS covariance matrix
541 point.adc_code, point.environmental_state
542 )
543 Pphi = self.rls_P @ phi
544 K = Pphi / (self.forgetting_factor + phi @ Pphi)
545 self.rls_P = (self.rls_P - np.outer(K, Pphi)) / self.forgetting_factor
546
547 # Record residual for autonomy α_agree
548 pred = phi.T @ self.theta_mean
549 self.recent_residuals.append(point.pressure - pred)
550
551 return {
552 "drift_detected": drift_detected,
553 "glr_statistic": glr_statistic,
554 "calibration_points": len(self.calibration_points),
555 "confidence_level": self.get_confidence_level(),
556 }
557
558 def get_autonomy_score(self) -> Tuple[float, Dict[str, float]]:
559 """
560 Paper Section 8: α = 0.4·α_cal + 0.3·α_unc + 0.2·α_agree + 0.1·α_quality
561 Returns (α, {α_cal, α_unc, α_agree, α_quality})
562 """
563 n = len(self.calibration_points)
564 alpha_cal = min(1.0, n / 5.0)
565
566 sigma = (
567 np.mean(self.recent_uncertainties) if self.recent_uncertainties else 100.0
568 )
569 alpha_unc = float(np.exp(-(sigma**2) / (self.sigma_ref**2 + 1e-12)))
570
571 if self.recent_residuals and self.recent_uncertainties:
572 res = np.array(list(self.recent_residuals))
573 sigs = np.array(list(self.recent_uncertainties))
574 n = min(len(res), len(sigs))
575 if n > 0:
576 sigs = np.where(sigs[:n] > 1e-6, sigs[:n], 1.0)
577 alpha_agree = float(np.mean(np.abs(res[:n]) < 2 * sigs))
578 else:
579 alpha_agree = 0.5
580 else:
581 alpha_agree = 0.5
582
583 summary = self.get_calibration_summary()
584 rmse = summary.get("rmse", 10.0)
585 p_range = 1.0
586 if self.calibration_points:
587 pressures = np.array([p.pressure for p in self.calibration_points])
588 p_range = max(np.ptp(pressures), 1.0)
589 nrmse = rmse / p_range
590 alpha_quality = float(1.0 - min(nrmse, 1.0))
591
592 alpha = (
593 0.4 * alpha_cal + 0.3 * alpha_unc + 0.2 * alpha_agree + 0.1 * alpha_quality
594 )
595 return float(np.clip(alpha, 0, 1)), {
596 "alpha_cal": alpha_cal,
597 "alpha_unc": alpha_unc,
598 "alpha_agree": alpha_agree,
599 "alpha_quality": alpha_quality,
600 }
601
602 def get_confidence_level(self) -> str:
603 """
604 Determine confidence level based on calibration quality
605 """
606 n_points = len(self.calibration_points)
607
608 if n_points < 3:
609 return "LOW"
610 elif n_points < 10:
611 return "MEDIUM"
612 elif n_points < 20:
613 return "HIGH"
614 else:
615 return "MAXIMUM"
616
617 def get_calibration_summary(self) -> Dict[str, any]:
618 """
619 Get comprehensive calibration summary
620 """
621 if not self.calibration_points:
622 return {
623 "sensor_id": self.sensor_id,
624 "status": "UNCALIBRATED",
625 "confidence_level": "LOW",
626 "calibration_points": 0,
627 "parameters": self.theta_mean.tolist(),
628 "uncertainty": np.sqrt(np.diag(self.theta_cov)).tolist(),
629 }
630
631 # Compute calibration quality metrics
632 residuals = []
633 for point in self.calibration_points:
635 point.adc_code, point.environmental_state
636 )
637 predicted = phi.T @ self.theta_mean
638 residuals.append(abs(point.pressure - predicted))
639
640 rmse = np.sqrt(np.mean(np.array(residuals) ** 2))
641 max_residual = np.max(residuals)
642
643 return {
644 "sensor_id": self.sensor_id,
645 "status": "CALIBRATED",
646 "confidence_level": self.get_confidence_level(),
647 "calibration_points": len(self.calibration_points),
648 "rmse": rmse,
649 "max_residual": max_residual,
650 "parameters": self.theta_mean.tolist(),
651 "uncertainty": np.sqrt(np.diag(self.theta_cov)).tolist(),
652 "forgetting_factor": self.forgetting_factor,
653 "glr_threshold": self.glr_threshold,
654 }
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
None update(self, float residual, float sigma_meas, float dt)
__init__(self, float tau_fast=1.0, float tau_medium=100.0, float tau_slow=1e4, float process_noise=1e-4)
Tuple[float, float] predict_pressure_with_uncertainty(self, float adc_code, EnvironmentalState env)
None set_theta_from_polynomial(self, List[float] poly_coeffs, float adc_norm_min, float adc_norm_scale)
Tuple[np.ndarray, np.ndarray] bayesian_update(self, List[CalibrationPoint] new_points)
Tuple[float, bool] generalized_likelihood_ratio_test(self, List[CalibrationPoint] new_points)
Tuple[np.ndarray, np.ndarray] recursive_least_squares_update(self, CalibrationPoint point)
Tuple[np.ndarray, np.ndarray] total_least_squares_calibration(self, List[CalibrationPoint] points)
Dict[str, any] add_calibration_point(self, CalibrationPoint point)
None set_noise_coeffs(self, "NoiseCoefficients" coeffs, float tau0=0.01)
np.ndarray environmental_robust_basis_functions(self, float adc_code, EnvironmentalState env)
float _measurement_variance(self, float adc_code, float tau=0.1)
Tuple[float, Dict[str, float]] get_autonomy_score(self)
TOML_NODISCARD_CTOR time() noexcept=default