#![cfg_attr(not(feature = "std"), no_std)]
use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FdiError {
InvalidParameter,
DimensionMismatch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FaultStatus {
Normal,
FaultDetected,
}
#[derive(Debug, Clone)]
pub struct ParitySpaceDetector<S: ControlScalar, const N: usize, const M: usize, const I: usize> {
a: [[S; N]; N],
b: [[S; N]; I],
c: [[S; M]; N],
x_hat: [S; N],
threshold: S,
consecutive_needed: usize,
fault_count: usize,
last_residual_norm: S,
}
impl<S: ControlScalar, const N: usize, const M: usize, const I: usize>
ParitySpaceDetector<S, N, M, I>
{
pub fn new(
a: [[S; N]; N],
b: [[S; N]; I],
c: [[S; M]; N],
threshold: S,
consecutive_needed: usize,
) -> Result<Self, FdiError> {
if threshold <= S::ZERO || consecutive_needed == 0 {
return Err(FdiError::InvalidParameter);
}
Ok(Self {
a,
b,
c,
x_hat: [S::ZERO; N],
threshold,
consecutive_needed,
fault_count: 0,
last_residual_norm: S::ZERO,
})
}
#[allow(clippy::needless_range_loop)]
pub fn update(&mut self, u: &[S; I], y: &[S; M]) -> Result<FaultStatus, FdiError> {
let mut y_hat = [S::ZERO; M];
for i in 0..M {
for j in 0..N {
y_hat[i] += self.c[j][i] * self.x_hat[j];
}
}
let mut norm_sq = S::ZERO;
for i in 0..M {
let ri = y[i] - y_hat[i];
norm_sq += ri * ri;
}
let norm = S::from_f64(libm::sqrt(norm_sq.to_f64()));
self.last_residual_norm = norm;
let mut x_new = [S::ZERO; N];
for j in 0..N {
for k in 0..N {
x_new[j] += self.a[j][k] * self.x_hat[k];
}
for k in 0..I {
x_new[j] += self.b[k][j] * u[k];
}
}
self.x_hat = x_new;
if norm > self.threshold {
self.fault_count += 1;
} else {
self.fault_count = 0;
}
if self.fault_count >= self.consecutive_needed {
Ok(FaultStatus::FaultDetected)
} else {
Ok(FaultStatus::Normal)
}
}
pub fn residual_norm(&self) -> S {
self.last_residual_norm
}
pub fn reset(&mut self, x_init: [S; N]) {
self.x_hat = x_init;
self.fault_count = 0;
self.last_residual_norm = S::ZERO;
}
pub fn fault_count(&self) -> usize {
self.fault_count
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_scalar_detector(threshold: f64, consec: usize) -> ParitySpaceDetector<f64, 1, 1, 1> {
let a = [[1.0_f64]];
let b = [[0.0_f64]];
let c = [[1.0_f64]];
ParitySpaceDetector::new(a, b, c, threshold, consec).expect("valid params")
}
#[test]
fn zero_residual_perfect_model() {
let mut det = make_scalar_detector(0.1, 1);
for _ in 0..10 {
let status = det.update(&[0.0], &[0.0]).expect("update ok");
assert_eq!(status, FaultStatus::Normal);
assert!(det.residual_norm() < 1e-12);
}
}
#[test]
fn step_fault_detected_after_consecutive() {
let mut det = make_scalar_detector(0.5, 3);
let s1 = det.update(&[0.0], &[10.0]).expect("update ok");
assert_eq!(s1, FaultStatus::Normal); let s2 = det.update(&[0.0], &[10.0]).expect("update ok");
assert_eq!(s2, FaultStatus::Normal); let s3 = det.update(&[0.0], &[10.0]).expect("update ok");
assert_eq!(s3, FaultStatus::FaultDetected); }
#[test]
fn fault_count_resets_on_normal() {
let mut det = make_scalar_detector(0.5, 3);
det.update(&[0.0], &[10.0]).expect("ok");
det.update(&[0.0], &[10.0]).expect("ok");
assert_eq!(det.fault_count(), 2);
det.update(&[0.0], &[0.0]).expect("ok");
assert_eq!(det.fault_count(), 0);
det.update(&[0.0], &[10.0]).expect("ok");
assert_eq!(det.fault_count(), 1);
}
#[test]
fn reset_clears_state() {
let mut det = make_scalar_detector(0.5, 1);
det.update(&[0.0], &[10.0]).expect("ok");
assert_eq!(det.fault_count(), 1);
det.reset([0.0]);
assert_eq!(det.fault_count(), 0);
assert_eq!(det.residual_norm(), 0.0);
let status = det.update(&[0.0], &[0.0]).expect("ok");
assert_eq!(status, FaultStatus::Normal);
}
#[test]
fn threshold_boundary() {
let mut det = make_scalar_detector(1.0, 1);
let status = det.update(&[0.0], &[1.0]).expect("ok"); assert_eq!(status, FaultStatus::Normal);
let mut det2 = make_scalar_detector(1.0, 1);
let status2 = det2.update(&[0.0], &[1.001]).expect("ok");
assert_eq!(status2, FaultStatus::FaultDetected);
}
#[test]
fn multiple_outputs_residual_norm() {
let a = [[1.0_f64, 0.0], [0.0, 1.0]];
let b: [[f64; 2]; 1] = [[0.0_f64, 0.0]];
let c = [[1.0_f64, 0.0], [0.0, 1.0]];
let mut det: ParitySpaceDetector<f64, 2, 2, 1> =
ParitySpaceDetector::new(a, b, c, 0.5, 1).expect("ok");
let status = det.update(&[0.0], &[3.0, 4.0]).expect("ok");
assert_eq!(status, FaultStatus::FaultDetected);
let norm = det.residual_norm();
assert!((norm - 5.0).abs() < 1e-9, "expected norm=5, got {norm}");
}
#[test]
fn invalid_parameter_rejection() {
let a = [[1.0_f64]];
let b = [[0.0_f64]];
let c = [[1.0_f64]];
assert!(
ParitySpaceDetector::<f64, 1, 1, 1>::new(a, b, c, 0.0, 1).is_err(),
"zero threshold should fail"
);
assert!(
ParitySpaceDetector::<f64, 1, 1, 1>::new(a, b, c, 1.0, 0).is_err(),
"zero consecutive_needed should fail"
);
}
#[test]
fn noise_free_prediction_with_input() {
let a = [[1.0_f64]];
let b = [[1.0_f64]]; let c = [[1.0_f64]];
let mut det: ParitySpaceDetector<f64, 1, 1, 1> =
ParitySpaceDetector::new(a, b, c, 0.5, 1).expect("ok");
let mut x_true = 0.0_f64;
for _ in 0..5 {
let u = 1.0_f64;
let y = x_true;
let status = det.update(&[u], &[y]).expect("ok");
assert_eq!(status, FaultStatus::Normal, "residual should be zero");
x_true += u;
}
}
}