1use crate::common::{FilterError, FloatOps};
7
8#[derive(Debug, Clone, Copy)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub struct SensorSource {
12 pub id: u8,
14 pub variance: f64,
16 pub gate_threshold: f64,
18 pub anomaly_count: u32,
20 pub max_anomalies: u32,
22 pub is_quarantined: bool,
24 pub health_score: f64,
26 pub consecutive_healthy: u32,
28 pub auto_recovery_threshold: u32,
30}
31
32impl SensorSource {
33 pub fn new(id: u8, variance: f64, max_anomalies: u32) -> Result<Self, FilterError> {
35 if !variance.is_valid_float() || variance <= 0.0 {
36 return Err(FilterError::InvalidParameter);
37 }
38 Ok(Self {
39 id,
40 variance,
41 gate_threshold: 3.0,
42 anomaly_count: 0,
43 max_anomalies,
44 is_quarantined: false,
45 health_score: 1.0,
46 consecutive_healthy: 0,
47 auto_recovery_threshold: 0,
48 })
49 }
50
51 pub fn with_gate_threshold(mut self, threshold: f64) -> Result<Self, FilterError> {
53 if !threshold.is_valid_float() || threshold <= 0.0 {
54 return Err(FilterError::InvalidParameter);
55 }
56 self.gate_threshold = threshold;
57 Ok(self)
58 }
59
60 pub fn with_auto_recovery(mut self, threshold: u32) -> Self {
62 self.auto_recovery_threshold = threshold;
63 self
64 }
65
66 pub fn reset_quarantine(&mut self) {
68 self.is_quarantined = false;
69 self.anomaly_count = 0;
70 self.consecutive_healthy = 0;
71 self.health_score = 1.0;
72 }
73}
74
75#[derive(Debug, Clone, Copy)]
77#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
78pub struct SensorMeasurement {
79 pub sensor_id: u8,
81 pub value: f64,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
88pub enum FusionStatus {
89 Fused,
91 Gated,
93 Quarantined,
95 IgnoredQuarantined,
97 Recovered,
99 UnregisteredSensor,
101}
102
103#[derive(Debug, Clone)]
105pub struct MultiSensorFusion1D<const N: usize> {
106 pub x: f64,
108 pub p: f64,
110 pub q: f64,
112 pub sensors: [Option<SensorSource>; N],
114}
115
116impl<const N: usize> MultiSensorFusion1D<N> {
117 pub fn new(x0: f64, p0: f64, q: f64) -> Result<Self, FilterError> {
119 if !x0.is_valid_float() || !p0.is_valid_float() || !q.is_valid_float() {
120 return Err(FilterError::NonFiniteInput);
121 }
122 if p0 <= 0.0 || q < 0.0 {
123 return Err(FilterError::InvalidParameter);
124 }
125
126 Ok(Self {
127 x: x0,
128 p: p0,
129 q,
130 sensors: [None; N],
131 })
132 }
133
134 pub fn register_sensor(&mut self, source: SensorSource) -> Result<(), FilterError> {
136 for slot in self.sensors.iter_mut() {
137 if let Some(existing) = slot {
138 if existing.id == source.id {
139 *slot = Some(source);
140 return Ok(());
141 }
142 } else {
143 *slot = Some(source);
144 return Ok(());
145 }
146 }
147 Err(FilterError::CapacityExceeded)
148 }
149
150 pub fn predict(&mut self, control_input: f64) -> Result<(), FilterError> {
152 if !control_input.is_valid_float() {
153 return Err(FilterError::NonFiniteInput);
154 }
155 self.x += control_input;
156 self.p += self.q;
157 Ok(())
158 }
159
160 pub fn fuse(&mut self, measurement: SensorMeasurement) -> Result<FusionStatus, FilterError> {
162 if !measurement.value.is_valid_float() {
163 return Err(FilterError::NonFiniteInput);
164 }
165
166 let mut target_sensor_idx = None;
168 for (i, slot) in self.sensors.iter().enumerate() {
169 if let Some(ref s) = slot {
170 if s.id == measurement.sensor_id {
171 target_sensor_idx = Some(i);
172 break;
173 }
174 }
175 }
176
177 let idx = match target_sensor_idx {
178 Some(i) => i,
179 None => return Ok(FusionStatus::UnregisteredSensor),
180 };
181
182 let sensor = match self.sensors[idx].as_mut() {
183 Some(s) => s,
184 None => return Ok(FusionStatus::UnregisteredSensor),
185 };
186
187 if sensor.is_quarantined {
188 if sensor.auto_recovery_threshold > 0 {
189 let innovation = measurement.value - self.x;
190 let s = self.p + sensor.variance;
191 let sigma_bound = sensor.gate_threshold * s.sqrt_val();
192 if innovation.abs_val() <= sigma_bound {
193 sensor.consecutive_healthy += 1;
194 if sensor.consecutive_healthy >= sensor.auto_recovery_threshold {
195 sensor.is_quarantined = false;
196 sensor.anomaly_count = 0;
197 sensor.health_score = 0.5;
198 sensor.consecutive_healthy = 0;
199 return Ok(FusionStatus::Recovered);
200 }
201 } else {
202 sensor.consecutive_healthy = 0;
203 }
204 }
205 return Ok(FusionStatus::IgnoredQuarantined);
206 }
207
208 let innovation = measurement.value - self.x;
210 let s = self.p + sensor.variance;
211 let sigma_bound = sensor.gate_threshold * s.sqrt_val();
212
213 if innovation.abs_val() > sigma_bound {
215 sensor.anomaly_count += 1;
216 sensor.health_score = (sensor.health_score * 0.5).max(0.0);
217 sensor.consecutive_healthy = 0;
218
219 if sensor.anomaly_count >= sensor.max_anomalies {
220 sensor.is_quarantined = true;
221 sensor.health_score = 0.0;
222 sensor.consecutive_healthy = 0;
223 return Ok(FusionStatus::Quarantined);
224 }
225 return Ok(FusionStatus::Gated);
226 }
227
228 sensor.anomaly_count = sensor.anomaly_count.saturating_sub(1);
230 sensor.health_score = (sensor.health_score + 0.1).min(1.0);
231
232 let k = self.p / s;
233 self.x += k * innovation;
234
235 let one_minus_k = 1.0 - k;
237 self.p = (one_minus_k * self.p * one_minus_k) + (k * sensor.variance * k);
238
239 Ok(FusionStatus::Fused)
240 }
241
242 pub fn state(&self) -> f64 {
244 self.x
245 }
246
247 pub fn variance(&self) -> f64 {
249 self.p
250 }
251
252 pub fn sensor_health(&self, sensor_id: u8) -> Option<f64> {
254 for s in self.sensors.iter().flatten() {
255 if s.id == sensor_id {
256 return Some(s.health_score);
257 }
258 }
259 None
260 }
261}