#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct DiagnosticCoverageValue(f64);
impl DiagnosticCoverageValue {
pub fn new(dc: f64) -> Result<Self, DiagnosticError> {
if dc.is_nan() || !(0.0..=1.0).contains(&dc) {
Err(DiagnosticError::InvalidFraction { value: dc })
} else {
Ok(Self(dc))
}
}
pub fn fraction(self) -> f64 {
self.0
}
pub fn tier(self) -> DcTier {
DcTier::from_fraction(self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum DcTier {
Low,
Medium,
High,
VeryHigh,
}
impl DcTier {
pub fn from_fraction(dc: f64) -> Self {
if dc >= 0.99 {
DcTier::VeryHigh
} else if dc >= 0.90 {
DcTier::High
} else if dc >= 0.60 {
DcTier::Medium
} else {
DcTier::Low
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct SafeFailureFraction(f64);
impl SafeFailureFraction {
pub fn new(sff: f64) -> Result<Self, DiagnosticError> {
if sff.is_nan() || !(0.0..=1.0).contains(&sff) {
Err(DiagnosticError::InvalidFraction { value: sff })
} else {
Ok(Self(sff))
}
}
pub fn fraction(self) -> f64 {
self.0
}
}
#[derive(Debug, Clone, Copy)]
pub struct ReliabilityBlock {
pub failure_rate_per_hour: f64,
pub safe_failure_fraction: f64,
pub diagnostic_coverage: f64,
}
impl ReliabilityBlock {
pub fn new(
failure_rate_per_hour: f64,
safe_failure_fraction: f64,
diagnostic_coverage: f64,
) -> Result<Self, DiagnosticError> {
if failure_rate_per_hour < 0.0 || !failure_rate_per_hour.is_finite() {
return Err(DiagnosticError::InvalidFailureRate {
value: failure_rate_per_hour,
});
}
if !(0.0..=1.0).contains(&safe_failure_fraction) || safe_failure_fraction.is_nan() {
return Err(DiagnosticError::InvalidFraction {
value: safe_failure_fraction,
});
}
if !(0.0..=1.0).contains(&diagnostic_coverage) || diagnostic_coverage.is_nan() {
return Err(DiagnosticError::InvalidFraction {
value: diagnostic_coverage,
});
}
Ok(Self {
failure_rate_per_hour,
safe_failure_fraction,
diagnostic_coverage,
})
}
pub fn lambda_dangerous(&self) -> f64 {
self.failure_rate_per_hour * (1.0 - self.safe_failure_fraction)
}
pub fn lambda_detected_dangerous(&self) -> f64 {
self.lambda_dangerous() * self.diagnostic_coverage
}
pub fn lambda_undetected_dangerous(&self) -> f64 {
self.lambda_dangerous() * (1.0 - self.diagnostic_coverage)
}
pub fn pfd_avg(&self, proof_test_interval_hours: f64) -> f64 {
self.lambda_undetected_dangerous() * proof_test_interval_hours * 0.5
}
pub fn pfh(&self) -> f64 {
self.lambda_undetected_dangerous()
}
}
pub const MAX_CHILDREN: usize = 8;
pub const MAX_TREE_NODES: usize = 32;
#[derive(Debug, Clone, Copy)]
enum NodeKind {
BasicEvent { lambda_per_hour: f64, beta: f64 },
And {
child_indices: [u8; MAX_CHILDREN],
child_count: u8,
},
Or {
child_indices: [u8; MAX_CHILDREN],
child_count: u8,
},
}
#[derive(Debug, Clone, Copy)]
pub struct FaultTreeNode {
kind: NodeKind,
}
#[derive(Debug, Clone, Copy)]
pub struct FaultTree {
nodes: [FaultTreeNode; MAX_TREE_NODES],
count: usize,
}
impl FaultTree {
pub fn new() -> Self {
Self {
nodes: [FaultTreeNode {
kind: NodeKind::BasicEvent {
lambda_per_hour: 0.0,
beta: 0.0,
},
}; MAX_TREE_NODES],
count: 0,
}
}
pub fn add_basic_event(
&mut self,
lambda_per_hour: f64,
beta: f64,
) -> Result<u8, DiagnosticError> {
self.push(FaultTreeNode {
kind: NodeKind::BasicEvent {
lambda_per_hour,
beta,
},
})
}
pub fn add_and(&mut self, children: &[u8]) -> Result<u8, DiagnosticError> {
if children.len() > MAX_CHILDREN {
return Err(DiagnosticError::TooManyChildren {
count: children.len(),
max: MAX_CHILDREN,
});
}
let mut idx = [0u8; MAX_CHILDREN];
for (i, &c) in children.iter().enumerate() {
idx[i] = c;
}
self.push(FaultTreeNode {
kind: NodeKind::And {
child_indices: idx,
child_count: children.len() as u8,
},
})
}
pub fn add_or(&mut self, children: &[u8]) -> Result<u8, DiagnosticError> {
if children.len() > MAX_CHILDREN {
return Err(DiagnosticError::TooManyChildren {
count: children.len(),
max: MAX_CHILDREN,
});
}
let mut idx = [0u8; MAX_CHILDREN];
for (i, &c) in children.iter().enumerate() {
idx[i] = c;
}
self.push(FaultTreeNode {
kind: NodeKind::Or {
child_indices: idx,
child_count: children.len() as u8,
},
})
}
pub fn compute_pfd(&self, root: u8, test_interval_hours: f64) -> Result<f64, DiagnosticError> {
if !test_interval_hours.is_finite() || test_interval_hours < 0.0 {
return Err(DiagnosticError::InvalidTestInterval {
value: test_interval_hours,
});
}
Ok(self.pfd_for(root as usize, test_interval_hours))
}
fn pfd_for(&self, idx: usize, ti: f64) -> f64 {
if idx >= self.count {
return 0.0;
}
match self.nodes[idx].kind {
NodeKind::BasicEvent {
lambda_per_hour,
beta,
} => {
let eff = lambda_per_hour * (1.0 - beta.clamp(0.0, 1.0));
eff * ti * 0.5
}
NodeKind::And {
child_indices,
child_count,
} => {
let mut product = 1.0_f64;
for &ci in child_indices.iter().take(child_count as usize) {
product *= self.pfd_for(ci as usize, ti);
}
product
}
NodeKind::Or {
child_indices,
child_count,
} => {
let mut complement = 1.0_f64;
for &ci in child_indices.iter().take(child_count as usize) {
complement *= 1.0 - self.pfd_for(ci as usize, ti);
}
1.0 - complement
}
}
}
fn push(&mut self, node: FaultTreeNode) -> Result<u8, DiagnosticError> {
if self.count >= MAX_TREE_NODES {
return Err(DiagnosticError::TooManyChildren {
count: self.count + 1,
max: MAX_TREE_NODES,
});
}
let idx = self.count;
self.nodes[idx] = node;
self.count += 1;
Ok(idx as u8)
}
pub fn node_count(&self) -> usize {
self.count
}
}
impl Default for FaultTree {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy)]
pub struct DiagnosticMonitor {
lambda_detected: f64,
lambda_undetected: f64,
lambda_safe: f64,
}
impl DiagnosticMonitor {
pub fn new() -> Self {
Self {
lambda_detected: 0.0,
lambda_undetected: 0.0,
lambda_safe: 0.0,
}
}
pub fn record_detected_failure(&mut self, lambda: f64) -> Result<(), DiagnosticError> {
validate_rate(lambda)?;
self.lambda_detected += lambda;
Ok(())
}
pub fn record_undetected_failure(&mut self, lambda: f64) -> Result<(), DiagnosticError> {
validate_rate(lambda)?;
self.lambda_undetected += lambda;
Ok(())
}
pub fn record_safe_failure(&mut self, lambda: f64) -> Result<(), DiagnosticError> {
validate_rate(lambda)?;
self.lambda_safe += lambda;
Ok(())
}
pub fn diagnostic_coverage(&self) -> f64 {
let total_dangerous = self.lambda_detected + self.lambda_undetected;
if total_dangerous <= 0.0 {
0.0
} else {
self.lambda_detected / total_dangerous
}
}
pub fn safe_failure_fraction(&self) -> f64 {
let total = self.lambda_safe + self.lambda_detected + self.lambda_undetected;
if total <= 0.0 {
1.0 } else {
(self.lambda_safe + self.lambda_detected) / total
}
}
pub fn lambda_dangerous_total(&self) -> f64 {
self.lambda_detected + self.lambda_undetected
}
pub fn lambda_total(&self) -> f64 {
self.lambda_safe + self.lambda_detected + self.lambda_undetected
}
pub fn lambda_detected(&self) -> f64 {
self.lambda_detected
}
pub fn lambda_undetected(&self) -> f64 {
self.lambda_undetected
}
pub fn reset(&mut self) {
self.lambda_detected = 0.0;
self.lambda_undetected = 0.0;
self.lambda_safe = 0.0;
}
}
impl Default for DiagnosticMonitor {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DiagnosticError {
InvalidFraction { value: f64 },
InvalidFailureRate { value: f64 },
InvalidTestInterval { value: f64 },
TooManyChildren { count: usize, max: usize },
}
impl core::fmt::Display for DiagnosticError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
DiagnosticError::InvalidFraction { value } => {
write!(f, "fraction {value} is not in [0, 1]")
}
DiagnosticError::InvalidFailureRate { value } => {
write!(f, "failure rate {value} is negative or non-finite")
}
DiagnosticError::InvalidTestInterval { value } => {
write!(f, "test interval {value} is negative or non-finite")
}
DiagnosticError::TooManyChildren { count, max } => {
write!(f, "gate has {count} children but maximum is {max}")
}
}
}
}
fn validate_rate(lambda: f64) -> Result<(), DiagnosticError> {
if !lambda.is_finite() || lambda < 0.0 {
Err(DiagnosticError::InvalidFailureRate { value: lambda })
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dc_value_valid_range() {
assert!(DiagnosticCoverageValue::new(0.0).is_ok());
assert!(DiagnosticCoverageValue::new(1.0).is_ok());
assert!(DiagnosticCoverageValue::new(0.95).is_ok());
}
#[test]
fn dc_value_invalid_range() {
assert!(matches!(
DiagnosticCoverageValue::new(-0.1),
Err(DiagnosticError::InvalidFraction { .. })
));
assert!(matches!(
DiagnosticCoverageValue::new(1.1),
Err(DiagnosticError::InvalidFraction { .. })
));
assert!(matches!(
DiagnosticCoverageValue::new(f64::NAN),
Err(DiagnosticError::InvalidFraction { .. })
));
}
#[test]
fn dc_tier_classification() {
assert_eq!(DcTier::from_fraction(0.30), DcTier::Low);
assert_eq!(DcTier::from_fraction(0.75), DcTier::Medium);
assert_eq!(DcTier::from_fraction(0.95), DcTier::High);
assert_eq!(DcTier::from_fraction(0.995), DcTier::VeryHigh);
assert_eq!(DcTier::from_fraction(0.99), DcTier::VeryHigh);
assert_eq!(DcTier::from_fraction(0.90), DcTier::High);
}
#[test]
fn sff_valid_range() {
assert!(SafeFailureFraction::new(0.0).is_ok());
assert!(SafeFailureFraction::new(0.6).is_ok());
assert!(SafeFailureFraction::new(1.0).is_ok());
}
#[test]
fn sff_invalid_range() {
assert!(SafeFailureFraction::new(-0.01).is_err());
assert!(SafeFailureFraction::new(1.01).is_err());
}
#[test]
fn reliability_block_pfd_calculation() {
let block = ReliabilityBlock::new(1e-6, 0.9, 0.9).unwrap();
let pfd = block.pfd_avg(8760.0);
assert!((pfd - 4.38e-5).abs() < 1e-8, "pfd={pfd:.4e}");
}
#[test]
fn reliability_block_pfh() {
let block = ReliabilityBlock::new(1e-5, 0.8, 0.95).unwrap();
let expected = 1e-5 * 0.2 * 0.05;
assert!((block.pfh() - expected).abs() < 1e-12);
}
#[test]
fn reliability_block_invalid_inputs() {
assert!(ReliabilityBlock::new(-1e-6, 0.5, 0.9).is_err());
assert!(ReliabilityBlock::new(1e-6, 1.5, 0.9).is_err());
assert!(ReliabilityBlock::new(1e-6, 0.5, -0.1).is_err());
}
#[test]
fn basic_event_pfd() {
let mut tree = FaultTree::new();
let root = tree.add_basic_event(1e-6, 0.0).unwrap();
let pfd = tree.compute_pfd(root, 8760.0).unwrap();
assert!((pfd - 4.38e-3).abs() < 1e-7, "pfd={pfd:.4e}");
}
#[test]
fn basic_event_with_beta_reduces_pfd() {
let mut tree_no = FaultTree::new();
let r_no = tree_no.add_basic_event(1e-6, 0.0).unwrap();
let pfd_no = tree_no.compute_pfd(r_no, 8760.0).unwrap();
let mut tree_b = FaultTree::new();
let r_b = tree_b.add_basic_event(1e-6, 0.1).unwrap();
let pfd_beta = tree_b.compute_pfd(r_b, 8760.0).unwrap();
assert!(pfd_beta < pfd_no);
let expected = 1e-6 * 0.9 * 8760.0 * 0.5;
assert!((pfd_beta - expected).abs() < 1e-9);
}
#[test]
fn and_gate_pfd_is_product() {
let mut tree = FaultTree::new();
let a = tree.add_basic_event(1e-4, 0.0).unwrap();
let b = tree.add_basic_event(2e-4, 0.0).unwrap();
let gate = tree.add_and(&[a, b]).unwrap();
let pfd = tree.compute_pfd(gate, 1000.0).unwrap();
let expected = 0.05 * 0.10;
assert!((pfd - expected).abs() < 1e-10, "and_pfd={pfd:.4e}");
}
#[test]
fn or_gate_pfd_complement_product() {
let mut tree = FaultTree::new();
let a = tree.add_basic_event(1e-4, 0.0).unwrap();
let b = tree.add_basic_event(2e-4, 0.0).unwrap();
let gate = tree.add_or(&[a, b]).unwrap();
let pfd = tree.compute_pfd(gate, 1000.0).unwrap();
let expected = 1.0 - (1.0 - 0.05) * (1.0 - 0.10);
assert!((pfd - expected).abs() < 1e-10, "or_pfd={pfd:.4e}");
}
#[test]
fn nested_and_or_tree() {
let mut tree = FaultTree::new();
let a = tree.add_basic_event(1e-4, 0.0).unwrap();
let b = tree.add_basic_event(2e-4, 0.0).unwrap();
let c = tree.add_basic_event(5e-5, 0.0).unwrap();
let and_ab = tree.add_and(&[a, b]).unwrap();
let top = tree.add_or(&[and_ab, c]).unwrap();
let pfd = tree.compute_pfd(top, 1000.0).unwrap();
let expected = 1.0 - (1.0 - 0.05 * 0.10) * (1.0 - 0.025);
assert!((pfd - expected).abs() < 1e-9, "nested_pfd={pfd:.6e}");
}
#[test]
fn fault_tree_invalid_interval() {
let mut tree = FaultTree::new();
let root = tree.add_basic_event(1e-6, 0.0).unwrap();
assert!(matches!(
tree.compute_pfd(root, -1.0),
Err(DiagnosticError::InvalidTestInterval { .. })
));
assert!(matches!(
tree.compute_pfd(root, f64::NAN),
Err(DiagnosticError::InvalidTestInterval { .. })
));
}
#[test]
fn fault_tree_too_many_children() {
let mut tree = FaultTree::new();
let indices: [u8; 9] = [0; 9];
assert!(matches!(
tree.add_and(&indices),
Err(DiagnosticError::TooManyChildren { .. })
));
}
#[test]
fn monitor_dc_calculation() {
let mut mon = DiagnosticMonitor::new();
mon.record_detected_failure(9e-7).unwrap();
mon.record_undetected_failure(1e-7).unwrap();
let dc = mon.diagnostic_coverage();
assert!((dc - 0.9).abs() < 1e-10, "DC={dc}");
}
#[test]
fn monitor_sff_calculation() {
let mut mon = DiagnosticMonitor::new();
mon.record_safe_failure(5e-7).unwrap();
mon.record_detected_failure(4e-7).unwrap();
mon.record_undetected_failure(1e-7).unwrap();
let sff = mon.safe_failure_fraction();
assert!((sff - 0.9).abs() < 1e-10, "SFF={sff}");
}
#[test]
fn monitor_dc_zero_when_no_dangerous_failures() {
let mon = DiagnosticMonitor::new();
assert_eq!(mon.diagnostic_coverage(), 0.0);
}
#[test]
fn monitor_sff_one_when_no_failures() {
let mon = DiagnosticMonitor::new();
assert_eq!(mon.safe_failure_fraction(), 1.0);
}
#[test]
fn monitor_rejects_negative_rate() {
let mut mon = DiagnosticMonitor::new();
assert!(matches!(
mon.record_detected_failure(-1e-6),
Err(DiagnosticError::InvalidFailureRate { .. })
));
}
#[test]
fn monitor_reset_clears_all() {
let mut mon = DiagnosticMonitor::new();
mon.record_detected_failure(1e-6).unwrap();
mon.record_undetected_failure(1e-7).unwrap();
mon.reset();
assert_eq!(mon.lambda_total(), 0.0);
assert_eq!(mon.diagnostic_coverage(), 0.0);
}
}