use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SilLevel {
None,
Sil1,
Sil2,
Sil3,
Sil4,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticCoverage {
Low,
Medium,
High,
VeryHigh,
}
impl DiagnosticCoverage {
pub fn fraction(self) -> f64 {
match self {
Self::Low => 0.30,
Self::Medium => 0.75,
Self::High => 0.945,
Self::VeryHigh => 0.995,
}
}
pub fn classify(dc: f64) -> Self {
if dc >= 0.99 {
Self::VeryHigh
} else if dc >= 0.90 {
Self::High
} else if dc >= 0.60 {
Self::Medium
} else {
Self::Low
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SafetyFunctionCoverage<S: ControlScalar> {
pub lambda_total: S,
pub dangerous_fraction: S,
pub dc: S,
pub t_proof: S,
}
impl<S: ControlScalar> SafetyFunctionCoverage<S> {
pub fn new(lambda_total: S, dangerous_fraction: S, dc: S, t_proof: S) -> Self {
Self {
lambda_total,
dangerous_fraction,
dc,
t_proof,
}
}
pub fn lambda_dangerous(&self) -> S {
self.lambda_total * self.dangerous_fraction
}
pub fn lambda_du(&self) -> S {
self.lambda_dangerous() * (S::ONE - self.dc)
}
pub fn pfh(&self) -> S {
self.lambda_du()
}
pub fn pfd_avg(&self) -> S {
self.lambda_du() * self.t_proof / S::TWO
}
pub fn sil_low_demand(&self) -> SilLevel {
let pfd = self.pfd_avg();
let pfd_f = pfd.abs() * S::from_f64(1.0); if pfd_f < S::from_f64(1e-5) {
SilLevel::Sil4
} else if pfd_f < S::from_f64(1e-4) {
SilLevel::Sil3
} else if pfd_f < S::from_f64(1e-3) {
SilLevel::Sil2
} else if pfd_f < S::from_f64(1e-2) {
SilLevel::Sil1
} else {
SilLevel::None
}
}
pub fn sil_continuous(&self) -> SilLevel {
let pfh = self.pfh();
if pfh < S::from_f64(1e-8) {
SilLevel::Sil4
} else if pfh < S::from_f64(1e-7) {
SilLevel::Sil3
} else if pfh < S::from_f64(1e-6) {
SilLevel::Sil2
} else if pfh < S::from_f64(1e-5) {
SilLevel::Sil1
} else {
SilLevel::None
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct RedundancyCoverage<S: ControlScalar> {
pub channel_pfd: S,
pub n_channels: u32,
pub m_of_n: u32,
}
impl<S: ControlScalar> RedundancyCoverage<S> {
pub fn new(channel_pfd: S, n_channels: u32, m_of_n: u32) -> Self {
Self {
channel_pfd,
n_channels,
m_of_n,
}
}
pub fn system_pfd(&self) -> S {
match (self.m_of_n, self.n_channels) {
(1, 1) => self.channel_pfd,
(1, 2) => self.channel_pfd * self.channel_pfd, (2, 2) => S::TWO * self.channel_pfd, (1, n) => {
let mut p = S::ONE;
for _ in 0..n {
p *= self.channel_pfd;
}
p
}
_ => self.channel_pfd, }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sil2_classification() {
let sf = SafetyFunctionCoverage::new(1e-6_f64, 0.5, 0.90, 8760.0);
let pfd = sf.pfd_avg();
assert!((pfd - 2.19e-4).abs() < 1e-5, "PFD={pfd:.2e}");
assert_eq!(sf.sil_low_demand(), SilLevel::Sil2);
}
#[test]
fn pfh_matches_lambda_du() {
let sf = SafetyFunctionCoverage::new(1e-5_f64, 0.8, 0.95, 1000.0);
let lambda_du = 1e-5 * 0.8 * 0.05;
assert!((sf.pfh() - lambda_du).abs() < 1e-12);
}
#[test]
fn dc_classification() {
assert_eq!(
DiagnosticCoverage::classify(0.99),
DiagnosticCoverage::VeryHigh
);
assert_eq!(DiagnosticCoverage::classify(0.95), DiagnosticCoverage::High);
assert_eq!(
DiagnosticCoverage::classify(0.75),
DiagnosticCoverage::Medium
);
assert_eq!(DiagnosticCoverage::classify(0.30), DiagnosticCoverage::Low);
}
#[test]
fn redundancy_1oo2_lower_pfd() {
let single_pfd = 1e-3_f64;
let r1oo2 = RedundancyCoverage::new(single_pfd, 2, 1);
let pfd_sys = r1oo2.system_pfd();
assert!(pfd_sys < single_pfd, "1oo2 should have lower PFD");
assert!((pfd_sys - 1e-6).abs() < 1e-10);
}
#[test]
fn redundancy_2oo2_higher_pfd() {
let single_pfd = 1e-3_f64;
let r2oo2 = RedundancyCoverage::new(single_pfd, 2, 2);
let pfd_sys = r2oo2.system_pfd();
assert!(pfd_sys > single_pfd, "2oo2 should have higher PFD");
}
#[test]
fn sil_continuous_mode() {
let sf = SafetyFunctionCoverage::new(1e-6_f64, 0.5, 0.90, 8760.0);
assert_eq!(sf.sil_continuous(), SilLevel::Sil3);
}
}