#![allow(dead_code)]
use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct ComparatorConfig<S: ControlScalar> {
pub absolute_tolerance: S,
pub relative_tolerance: S,
pub max_divergence_count: u32,
}
impl<S: ControlScalar> ComparatorConfig<S> {
pub fn new(absolute_tolerance: S, relative_tolerance: S, max_divergence_count: u32) -> Self {
Self {
absolute_tolerance,
relative_tolerance,
max_divergence_count: max_divergence_count.max(1),
}
}
pub fn within_tolerance(&self, a: S, b: S) -> bool {
let abs_diff = (a - b).abs();
if abs_diff <= self.absolute_tolerance {
return true;
}
let magnitude = (a.abs() + b.abs()) * S::HALF;
if magnitude > S::ZERO {
let rel_diff = abs_diff / magnitude;
if rel_diff <= self.relative_tolerance {
return true;
}
}
false
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CompareResult<S: Copy> {
Agree(S),
Diverged(S, S),
Faulty(u8),
}
#[derive(Debug, Clone, Copy)]
pub struct DualChannelComparatorExt<S: ControlScalar> {
config: ComparatorConfig<S>,
divergence_count: u32,
tripped: bool,
suspect_channel: u8,
}
impl<S: ControlScalar> DualChannelComparatorExt<S> {
pub fn new(config: ComparatorConfig<S>) -> Self {
Self {
config,
divergence_count: 0,
tripped: false,
suspect_channel: 2,
}
}
pub fn compare(&mut self, ch_a: S, ch_b: S) -> CompareResult<S> {
if self.tripped {
return CompareResult::Faulty(self.suspect_channel);
}
if self.config.within_tolerance(ch_a, ch_b) {
self.divergence_count = 0;
let voted = (ch_a + ch_b) * S::HALF;
CompareResult::Agree(voted)
} else {
self.divergence_count += 1;
if self.divergence_count >= self.config.max_divergence_count {
self.tripped = true;
self.suspect_channel = 2;
CompareResult::Faulty(self.suspect_channel)
} else {
CompareResult::Diverged(ch_a, ch_b)
}
}
}
pub fn compare_with_ref(&mut self, ch_a: S, ch_b: S, reference: S) -> CompareResult<S> {
let result = self.compare(ch_a, ch_b);
if self.tripped {
let da = (ch_a - reference).abs();
let db = (ch_b - reference).abs();
self.suspect_channel = if da > db { 0 } else { 1 };
CompareResult::Faulty(self.suspect_channel)
} else {
result
}
}
pub fn is_tripped(&self) -> bool {
self.tripped
}
pub fn divergence_count(&self) -> u32 {
self.divergence_count
}
pub fn suspect_channel(&self) -> u8 {
self.suspect_channel
}
pub fn reset(&mut self) {
self.divergence_count = 0;
self.tripped = false;
self.suspect_channel = 2;
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct VoteResult<S: Copy> {
pub voted_value: S,
pub outlier_channel: Option<u8>,
}
#[derive(Debug, Clone, Copy)]
pub struct TripleModularRedundancy<S: ControlScalar> {
config: ComparatorConfig<S>,
outlier_counts: [u32; 3],
channel_faulted: [bool; 3],
}
impl<S: ControlScalar> TripleModularRedundancy<S> {
pub fn new(config: ComparatorConfig<S>) -> Self {
Self {
config,
outlier_counts: [0; 3],
channel_faulted: [false; 3],
}
}
pub fn vote(&mut self, ch_a: S, ch_b: S, ch_c: S) -> Result<VoteResult<S>, TmrError> {
let faulted_count = self.channel_faulted.iter().filter(|&&f| f).count();
if faulted_count >= 2 {
return Err(TmrError::MajorityLost);
}
let values = [ch_a, ch_b, ch_c];
let voted = median3(ch_a, ch_b, ch_c);
let deviations = [
(ch_a - voted).abs(),
(ch_b - voted).abs(),
(ch_c - voted).abs(),
];
let mut max_dev = S::ZERO;
let mut max_idx: u8 = 0;
for (i, &dev) in deviations.iter().enumerate() {
if dev > max_dev {
max_dev = dev;
max_idx = i as u8;
}
}
let outlier_channel: Option<u8> = if !self
.config
.within_tolerance(values[max_idx as usize], voted)
{
let idx = max_idx as usize;
self.outlier_counts[idx] += 1;
for j in 0..3usize {
if j != idx {
self.outlier_counts[j] = 0;
}
}
if self.outlier_counts[idx] >= self.config.max_divergence_count {
self.channel_faulted[idx] = true;
}
Some(max_idx)
} else {
self.outlier_counts = [0; 3];
None
};
Ok(VoteResult {
voted_value: voted,
outlier_channel,
})
}
pub fn is_channel_faulted(&self, channel: u8) -> bool {
let idx = channel as usize;
if idx < 3 {
self.channel_faulted[idx]
} else {
false
}
}
pub fn channel_faults(&self) -> [bool; 3] {
self.channel_faulted
}
pub fn outlier_counts(&self) -> [u32; 3] {
self.outlier_counts
}
pub fn reset(&mut self) {
self.outlier_counts = [0; 3];
self.channel_faulted = [false; 3];
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TmrError {
MajorityLost,
}
impl core::fmt::Display for TmrError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
TmrError::MajorityLost => {
write!(f, "TMR: majority lost — two or more channels faulted")
}
}
}
}
fn median3<S: ControlScalar>(a: S, b: S, c: S) -> S {
let lo = if a < b { a } else { b };
let hi = if a > b { a } else { b };
let mid = if hi < c { hi } else { c };
if lo > mid {
lo
} else {
mid
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn within_absolute_tolerance() {
let cfg = ComparatorConfig::new(0.1_f64, 0.001_f64, 3);
assert!(cfg.within_tolerance(10.0, 10.05));
assert!(!cfg.within_tolerance(10.0, 10.2));
}
#[test]
fn within_relative_tolerance() {
let cfg = ComparatorConfig::new(0.05_f64, 0.05_f64, 3);
assert!(cfg.within_tolerance(10.0, 10.2));
assert!(!cfg.within_tolerance(10.0, 20.0));
}
#[test]
fn dual_channels_agree() {
let cfg = ComparatorConfig::new(0.1_f64, 0.01_f64, 3);
let mut cmp = DualChannelComparatorExt::new(cfg);
let result = cmp.compare(5.0, 5.05);
assert!(matches!(result, CompareResult::Agree(_)));
if let CompareResult::Agree(v) = result {
assert!((v - 5.025).abs() < 1e-10);
}
assert!(!cmp.is_tripped());
}
#[test]
fn dual_channels_diverge_below_threshold() {
let cfg = ComparatorConfig::new(0.1_f64, 0.01_f64, 3);
let mut cmp = DualChannelComparatorExt::new(cfg);
let r1 = cmp.compare(0.0, 10.0);
let r2 = cmp.compare(0.0, 10.0);
assert!(matches!(r1, CompareResult::Diverged(_, _)));
assert!(matches!(r2, CompareResult::Diverged(_, _)));
assert!(!cmp.is_tripped());
assert_eq!(cmp.divergence_count(), 2);
}
#[test]
fn dual_channels_trip_after_threshold() {
let cfg = ComparatorConfig::new(0.1_f64, 0.01_f64, 3);
let mut cmp = DualChannelComparatorExt::new(cfg);
for _ in 0..3 {
cmp.compare(0.0, 10.0);
}
assert!(cmp.is_tripped());
assert!(matches!(cmp.compare(0.0, 10.0), CompareResult::Faulty(_)));
}
#[test]
fn dual_channels_trip_identifies_suspect_with_ref() {
let cfg = ComparatorConfig::new(0.1_f64, 0.01_f64, 1);
let mut cmp = DualChannelComparatorExt::new(cfg);
let result = cmp.compare_with_ref(10.0_f64, 20.0, 10.0);
assert!(cmp.is_tripped());
assert_eq!(cmp.suspect_channel(), 1); assert!(matches!(result, CompareResult::Faulty(1)));
}
#[test]
fn dual_reset_clears_state() {
let cfg = ComparatorConfig::new(0.1_f64, 0.01_f64, 1);
let mut cmp = DualChannelComparatorExt::new(cfg);
cmp.compare(0.0, 10.0); assert!(cmp.is_tripped());
cmp.reset();
assert!(!cmp.is_tripped());
assert_eq!(cmp.divergence_count(), 0);
assert!(matches!(cmp.compare(5.0, 5.0), CompareResult::Agree(_)));
}
#[test]
fn divergence_count_resets_on_agreement() {
let cfg = ComparatorConfig::new(0.1_f64, 0.01_f64, 5);
let mut cmp = DualChannelComparatorExt::new(cfg);
cmp.compare(0.0, 10.0); cmp.compare(0.0, 10.0); cmp.compare(5.0, 5.0); assert_eq!(cmp.divergence_count(), 0);
assert!(!cmp.is_tripped());
}
#[test]
fn tmr_all_channels_agree() {
let cfg = ComparatorConfig::new(0.1_f64, 0.01_f64, 3);
let mut tmr = TripleModularRedundancy::new(cfg);
let result = tmr.vote(5.0, 5.05, 4.98).unwrap();
assert!(result.outlier_channel.is_none());
assert!((result.voted_value - 5.0).abs() < 1e-9);
}
#[test]
fn tmr_identifies_outlier_channel() {
let cfg = ComparatorConfig::new(0.1_f64, 0.01_f64, 3);
let mut tmr = TripleModularRedundancy::new(cfg);
let result = tmr.vote(5.0_f64, 5.05, 100.0).unwrap();
assert_eq!(result.outlier_channel, Some(2)); assert!((result.voted_value - 5.05).abs() < 1e-9);
}
#[test]
fn tmr_outlier_a_identified() {
let cfg = ComparatorConfig::new(0.1_f64, 0.01_f64, 3);
let mut tmr = TripleModularRedundancy::new(cfg);
let result = tmr.vote(100.0_f64, 5.0, 5.05).unwrap();
assert_eq!(result.outlier_channel, Some(0)); }
#[test]
fn tmr_channel_faulted_after_repeated_outliers() {
let cfg = ComparatorConfig::new(0.1_f64, 0.01_f64, 3);
let mut tmr = TripleModularRedundancy::new(cfg);
for _ in 0..3 {
let _ = tmr.vote(5.0_f64, 5.05, 100.0);
}
assert!(tmr.is_channel_faulted(2));
assert!(!tmr.is_channel_faulted(0));
assert!(!tmr.is_channel_faulted(1));
}
#[test]
fn tmr_majority_lost_when_two_channels_faulted() {
let cfg = ComparatorConfig::new(0.1_f64, 0.01_f64, 1);
let mut tmr = TripleModularRedundancy::new(cfg);
let _ = tmr.vote(100.0_f64, 5.0, 5.0);
assert!(tmr.is_channel_faulted(0));
let _ = tmr.vote(5.0_f64, 100.0, 5.0);
assert!(tmr.is_channel_faulted(1));
let result = tmr.vote(5.0_f64, 5.0, 5.0);
assert_eq!(result, Err(TmrError::MajorityLost));
}
#[test]
fn tmr_reset_clears_faults() {
let cfg = ComparatorConfig::new(0.1_f64, 0.01_f64, 1);
let mut tmr = TripleModularRedundancy::new(cfg);
let _ = tmr.vote(100.0_f64, 5.0, 5.0); assert!(tmr.is_channel_faulted(0));
tmr.reset();
assert!(!tmr.is_channel_faulted(0));
assert_eq!(tmr.outlier_counts(), [0; 3]);
}
#[test]
fn median3_correctness() {
assert_eq!(median3(1.0_f64, 2.0, 3.0), 2.0);
assert_eq!(median3(3.0_f64, 1.0, 2.0), 2.0);
assert_eq!(median3(2.0_f64, 3.0, 1.0), 2.0);
assert_eq!(median3(5.0_f64, 5.0, 5.0), 5.0);
assert_eq!(median3(1.0_f64, 1.0, 100.0), 1.0);
}
}