1#![cfg_attr(not(feature = "std"), no_std)]
10
11pub mod common;
12pub mod fusion;
13pub mod linear;
14pub mod robust;
15pub mod stats;
16pub mod unscented;
17
18pub use common::FilterError;
19pub use fusion::{FusionStatus, MultiSensorFusion1D, SensorMeasurement, SensorSource};
20pub use linear::{LinearKalmanFilter, ScalarKalmanFilter};
21pub use robust::{AdaptiveFilter1D, HInfinityFilter1D};
22pub use stats::WindowedStats;
23pub use unscented::UnscentedKalmanFilter2D;
24
25#[cfg(test)]
26mod tests {
27 use super::*;
28
29 #[test]
30 fn test_scalar_filter_convergence() {
31 let mut filter = ScalarKalmanFilter::new(0.0, 10.0, 0.01, 0.5).unwrap();
32 let target = 25.0;
33 for _ in 0..50 {
34 filter.predict(0.0).unwrap();
35 let _ = filter.update(target + 0.05, None).unwrap();
36 }
37 assert!((filter.state() - target).abs() < 0.2);
38 }
39
40 #[test]
41 fn test_scalar_filter_outlier_gating() {
42 let mut filter = ScalarKalmanFilter::new(10.0, 1.0, 0.01, 0.5).unwrap();
43 let err = filter.update(9999.0, Some(3.0));
45 assert_eq!(err, Err(FilterError::MeasurementGated));
46 assert!((filter.state() - 10.0).abs() < 1e-9);
47 }
48
49 #[test]
50 fn test_ukf_linear_observation_multi_step() {
51 let mut ukf = UnscentedKalmanFilter2D::new(
54 [0.0, 0.0],
55 [[2.0, 0.0], [0.0, 2.0]],
56 [[0.001, 0.0], [0.0, 0.001]],
57 0.5,
58 )
59 .unwrap();
60
61 let target = 5.0;
62 for _ in 0..30 {
63 ukf.predict(0.1, |x, _dt| [x[0], x[1]]).unwrap();
64 let _ = ukf.update(target, |x| x[0], None).unwrap();
65 }
66
67 assert!((ukf.x[0] - target).abs() < 0.3, "ukf.x[0] = {}", ukf.x[0]);
69 }
70
71 #[test]
72 fn test_ukf_nonlinear_tight_covariance() {
73 let true_pos: f64 = 3.0;
77 let mut ukf = UnscentedKalmanFilter2D::new(
78 [true_pos + 0.5, 0.0],
79 [[0.04, 0.0], [0.0, 0.04]], [[0.001, 0.0], [0.0, 0.001]],
81 0.1,
82 )
83 .unwrap();
84
85 ukf.predict(0.0, |x, _dt| [x[0], x[1]]).unwrap();
86
87 let z = true_pos * true_pos; let before = ukf.x[0];
89 let res = ukf.update(z, |x| x[0] * x[0], None).unwrap();
90
91 assert!(
93 res[0] < before,
94 "Expected x[0] to decrease toward 3.0, before={before}, after={}",
95 res[0]
96 );
97 assert!(res[0] > 2.5, "Expected x[0] > 2.5, got {}", res[0]);
98 }
99
100 #[test]
101 fn test_h_infinity_robustness() {
102 let mut h_inf = HInfinityFilter1D::new(10.0, 1.0, 0.05, 0.5, 5.0).unwrap();
103
104 for _ in 0..20 {
105 h_inf.predict(0.0).unwrap();
106 let _ = h_inf.update(10.1).unwrap();
107 }
108
109 assert!((h_inf.state() - 10.1).abs() < 0.3);
110 assert!(h_inf.bound() > 0.0);
111 }
112
113 #[test]
114 fn test_adaptive_filter_noise_tracking() {
115 let mut af = AdaptiveFilter1D::new(0.0, 5.0, 0.01, 1.0, 0.05).unwrap();
116 for _ in 0..40 {
117 af.predict(0.0);
118 let _ = af.update(20.0).unwrap();
119 }
120 assert!((af.x - 20.0).abs() < 0.5, "af.x = {}", af.x);
121 }
122
123 #[test]
124 fn test_nan_inf_rejection() {
125 let result = ScalarKalmanFilter::new(f64::NAN, 1.0, 0.1, 1.0);
126 assert_eq!(result.err(), Some(FilterError::NonFiniteInput));
127
128 let mut f = ScalarKalmanFilter::new(0.0, 1.0, 0.1, 1.0).unwrap();
129 assert_eq!(
130 f.update(f64::INFINITY, None).err(),
131 Some(FilterError::NonFiniteInput)
132 );
133 }
134
135 #[test]
136 fn test_multi_sensor_fusion_and_quarantine() {
137 let mut fusion = MultiSensorFusion1D::<3>::new(0.0, 5.0, 0.05).unwrap();
139
140 let baro = SensorSource::new(1, 0.2, 3).unwrap();
142 let gps = SensorSource::new(2, 1.5, 2).unwrap();
144
145 fusion.register_sensor(baro).unwrap();
146 fusion.register_sensor(gps).unwrap();
147
148 fusion.predict(1.0).unwrap();
150 let status = fusion
151 .fuse(SensorMeasurement {
152 sensor_id: 1,
153 value: 1.05,
154 })
155 .unwrap();
156 assert_eq!(status, FusionStatus::Fused);
157
158 let status = fusion
161 .fuse(SensorMeasurement {
162 sensor_id: 2,
163 value: 500.0,
164 })
165 .unwrap();
166 assert_eq!(status, FusionStatus::Gated);
167
168 let status = fusion
170 .fuse(SensorMeasurement {
171 sensor_id: 2,
172 value: 500.0,
173 })
174 .unwrap();
175 assert_eq!(status, FusionStatus::Quarantined);
176 assert_eq!(fusion.sensor_health(2), Some(0.0));
177
178 let status = fusion
180 .fuse(SensorMeasurement {
181 sensor_id: 2,
182 value: 1.0,
183 })
184 .unwrap();
185 assert_eq!(status, FusionStatus::IgnoredQuarantined);
186
187 let status = fusion
189 .fuse(SensorMeasurement {
190 sensor_id: 1,
191 value: 1.02,
192 })
193 .unwrap();
194 assert_eq!(status, FusionStatus::Fused);
195 }
196
197 #[test]
198 fn test_sensor_auto_recovery() {
199 let mut fusion = MultiSensorFusion1D::<2>::new(10.0, 1.0, 0.01).unwrap();
200 let gps = SensorSource::new(1, 0.5, 2).unwrap().with_auto_recovery(2);
202 fusion.register_sensor(gps).unwrap();
203
204 let _ = fusion.fuse(SensorMeasurement {
206 sensor_id: 1,
207 value: 100.0,
208 });
209 let status = fusion
210 .fuse(SensorMeasurement {
211 sensor_id: 1,
212 value: 100.0,
213 })
214 .unwrap();
215 assert_eq!(status, FusionStatus::Quarantined);
216
217 let status = fusion
219 .fuse(SensorMeasurement {
220 sensor_id: 1,
221 value: 10.05,
222 })
223 .unwrap();
224 assert_eq!(status, FusionStatus::IgnoredQuarantined);
225
226 let status = fusion
228 .fuse(SensorMeasurement {
229 sensor_id: 1,
230 value: 10.02,
231 })
232 .unwrap();
233 assert_eq!(status, FusionStatus::Recovered);
234
235 let status = fusion
237 .fuse(SensorMeasurement {
238 sensor_id: 1,
239 value: 10.01,
240 })
241 .unwrap();
242 assert_eq!(status, FusionStatus::Fused);
243 }
244
245 #[test]
246 fn test_windowed_stats_rolling_metrics() {
247 let mut stats = WindowedStats::<4>::new();
248 assert!(stats.is_empty());
249 assert_eq!(stats.len(), 0);
250
251 stats.push(2.0).unwrap();
252 stats.push(4.0).unwrap();
253 assert_eq!(stats.len(), 2);
254 assert!((stats.mean().unwrap() - 3.0).abs() < 1e-9);
255 assert_eq!(stats.min(), Some(2.0));
256 assert_eq!(stats.max(), Some(4.0));
257 assert!((stats.peak_to_peak().unwrap() - 2.0).abs() < 1e-9);
258
259 stats.push(6.0).unwrap();
261 stats.push(8.0).unwrap();
262 assert_eq!(stats.len(), 4);
263 assert!((stats.mean().unwrap() - 5.0).abs() < 1e-9);
264
265 stats.push(10.0).unwrap();
267 assert_eq!(stats.len(), 4);
268 assert!((stats.mean().unwrap() - 7.0).abs() < 1e-9);
269 assert_eq!(stats.min(), Some(4.0));
270 assert_eq!(stats.max(), Some(10.0));
271 assert!(stats.variance().unwrap() > 0.0);
272 }
273}