STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
allan_variance.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""
3Allan Variance Analysis for Pressure Transducer Noise Characterization
4
5Paper Section 3: Decomposes measurement noise into fundamental stochastic processes:
6- White noise (Q): thermal, quantization
7- Flicker noise (B): 1/f, bias instability
8- Bias random walk (K): long-term drift
9- Rate random walk (R): acceleration of drift
10
11σ²_A(τ) = Q²/(2τ) + 2·ln(2)·B² + K²·τ/6 + R²·τ²/20
12
13Usage:
14 from allan_variance import compute_allan_variance, fit_noise_coefficients, measurement_uncertainty
15"""
16
17import numpy as np
18from typing import Tuple, Optional
19from dataclasses import dataclass
20import logging
21
22logger = logging.getLogger(__name__)
23
24
25@dataclass
27 """Noise coefficients from Allan variance fit"""
28
29 Q: float # White noise (quantization)
30 B: float # Bias instability (flicker)
31 K: float # Bias random walk
32 R: float # Rate random walk
33 tau_min: float # τ at minimum Allan variance
34 sigma_min: float # Minimum Allan deviation
35
36
38 timeseries: np.ndarray,
39 tau0: float,
40 max_clusters: Optional[int] = None,
41) -> Tuple[np.ndarray, np.ndarray]:
42 """
43 Compute Allan variance at multiple averaging times τ = m·τ0.
44
45 Args:
46 timeseries: Voltage or ADC time series (1D)
47 tau0: Sampling interval (seconds)
48 max_clusters: Max cluster size (default: N/4)
49
50 Returns:
51 tau_arr: Averaging times [τ0, 2τ0, 4τ0, ...]
52 sigma2_arr: Allan variance σ²_A(τ) at each τ
53 """
54 n = len(timeseries)
55 if n < 10:
56 return np.array([tau0]), np.array([np.var(timeseries)])
57
58 max_m = max_clusters or max(2, n // 4)
59 tau_list = []
60 sigma2_list = []
61
62 m = 1
63 while m <= max_m and n >= 3 * m:
64 # Cluster averages: x̄_i = (1/m) Σ_{k=0}^{m-1} x_{i*m+k}
65 num_triplets = (n - 2 * m) // m # need i, i+m, i+2m
66 if num_triplets < 1:
67 break
68
69 clusters = np.array(
70 [np.mean(timeseries[i * m : (i + 1) * m]) for i in range(num_triplets + 2)]
71 )
72
73 # Allan variance: (1/2) · mean of (x̄_{i+2} - 2·x̄_{i+1} + x̄_i)²
74 diffs = clusters[2:] - 2 * clusters[1:-1] + clusters[:-2]
75 sigma2 = 0.5 * np.mean(diffs**2)
76 tau = m * tau0
77
78 tau_list.append(tau)
79 sigma2_list.append(sigma2)
80 m = 2 * m if m >= 2 else m + 1
81
82 return np.array(tau_list), np.array(sigma2_list)
83
84
86 tau_arr: np.ndarray,
87 sigma2_arr: np.ndarray,
88) -> NoiseCoefficients:
89 """
90 Fit noise model: σ²_A(τ) = Q²/(2τ) + 2·ln(2)·B² + K²·τ/6 + R²·τ²/20
91
92 Uses least-squares on log-scale for stability.
93
94 Returns:
95 NoiseCoefficients(Q, B, K, R, tau_min, sigma_min)
96 """
97 if len(tau_arr) < 3:
98 return NoiseCoefficients(
99 Q=1e-3,
100 B=1e-4,
101 K=1e-5,
102 R=1e-6,
103 tau_min=tau_arr[0] if len(tau_arr) > 0 else 1.0,
104 sigma_min=np.sqrt(sigma2_arr[0]) if len(sigma2_arr) > 0 else 1e-3,
105 )
106
107 # Design matrix: [1/τ, 1, τ, τ²]
108 X = np.column_stack(
109 [
110 1.0 / (2 * tau_arr),
111 np.full_like(tau_arr, 2 * np.log(2)),
112 tau_arr / 6,
113 tau_arr**2 / 20,
114 ]
115 )
116 y = sigma2_arr
117
118 try:
119 beta, _, _, _ = np.linalg.lstsq(X, y, rcond=None)
120 Q = np.sqrt(max(beta[0], 1e-12))
121 B = np.sqrt(max(beta[1], 1e-12))
122 K = np.sqrt(max(beta[2], 1e-12))
123 R = np.sqrt(max(beta[3], 1e-12))
124 except np.linalg.LinAlgError:
125 Q, B, K, R = 1e-3, 1e-4, 1e-5, 1e-6
126
127 # τ_min ≈ sqrt(3*Q²/K²) for white + random walk
128 tau_min = (
129 np.sqrt(3 * Q**2 / (K**2 + 1e-20)) if K > 1e-10 else tau_arr[len(tau_arr) // 2]
130 )
131 tau_min = np.clip(tau_min, tau_arr.min(), tau_arr.max())
132 sigma_min = np.sqrt(
133 Q**2 / (2 * tau_min)
134 + 2 * np.log(2) * B**2
135 + K**2 * tau_min / 6
136 + R**2 * tau_min**2 / 20
137 )
138
139 return NoiseCoefficients(Q=Q, B=B, K=K, R=R, tau_min=tau_min, sigma_min=sigma_min)
140
141
143 tau: float,
144 coeffs: NoiseCoefficients,
145 voltage: float = 0.0,
146 sigma_base: float = 1e-3,
147) -> float:
148 """
149 Measurement uncertainty σ_meas(v, τ) from paper Section 7.
150
151 σ²_meas = σ²_base + Q²/(2τ) + 2·ln(2)·B² + K²·τ/6 + α_v·v²
152
153 Args:
154 tau: Averaging time (seconds)
155 coeffs: Noise coefficients from fit_noise_coefficients
156 voltage: Optional voltage for heteroscedastic term
157 sigma_base: Base noise floor
158
159 Returns:
160 σ_meas in same units as input (PSI if voltage→PSI calibrated)
161 """
162 var = sigma_base**2
163 var += coeffs.Q**2 / (2 * tau)
164 var += 2 * np.log(2) * coeffs.B**2
165 var += coeffs.K**2 * tau / 6
166 var += 1e-4 * voltage**2 # α_v · v²
167 return np.sqrt(max(var, 1e-12))
168
169
171 timeseries: np.ndarray,
172 tau0: float = 0.01,
173) -> Tuple[NoiseCoefficients, np.ndarray, np.ndarray]:
174 """
175 Full analysis: compute Allan variance, fit coefficients, return all.
176
177 Args:
178 timeseries: Voltage or ADC series
179 tau0: Sampling interval (s)
180
181 Returns:
182 (coeffs, tau_arr, sigma2_arr)
183 """
184 tau_arr, sigma2_arr = compute_allan_variance(timeseries, tau0)
185 coeffs = fit_noise_coefficients(tau_arr, sigma2_arr)
186 return coeffs, tau_arr, sigma2_arr
187
188
189if __name__ == "__main__":
190 # Demo with synthetic noise
191 np.random.seed(42)
192 n = 10000
193 tau0 = 0.01
194 t = np.arange(n) * tau0
195 # White + random walk
196 white = np.random.randn(n) * 0.01
197 rw = np.cumsum(np.random.randn(n) * 0.001)
198 x = white + rw
199
200 coeffs, tau_arr, sigma2_arr = analyze_sensor_noise(x, tau0)
201 print(f"Q={coeffs.Q:.2e} B={coeffs.B:.2e} K={coeffs.K:.2e} R={coeffs.R:.2e}")
202 print(f"τ_min={coeffs.tau_min:.3f}s σ_min={coeffs.sigma_min:.2e}")
203 sigma_1s = measurement_uncertainty(1.0, coeffs)
204 print(f"σ_meas(τ=1s) = {sigma_1s:.2e}")
#define max(a, b)
Definition Arduino.h:43
Tuple[np.ndarray, np.ndarray] compute_allan_variance(np.ndarray timeseries, float tau0, Optional[int] max_clusters=None)
NoiseCoefficients fit_noise_coefficients(np.ndarray tau_arr, np.ndarray sigma2_arr)
Tuple[NoiseCoefficients, np.ndarray, np.ndarray] analyze_sensor_noise(np.ndarray timeseries, float tau0=0.01)
float measurement_uncertainty(float tau, NoiseCoefficients coeffs, float voltage=0.0, float sigma_base=1e-3)