use super::mle::{two_sided_z, weibull_mle_censored, WeibullInterval};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WeibullQuantity {
Shape,
Scale,
BLife(f64),
}
struct Likelihood {
ln_t: Vec<f64>,
r: f64,
sum_ln_fail: f64,
}
impl Likelihood {
fn new(failures: &[f64], suspensions: &[f64]) -> Self {
let ln_t: Vec<f64> = failures.iter().chain(suspensions).map(|t| t.ln()).collect();
let sum_ln_fail = ln_t[..failures.len()].iter().sum();
Self {
ln_t,
r: failures.len() as f64,
sum_ln_fail,
}
}
fn at(&self, beta: f64, ln_eta: f64) -> f64 {
let sum_z: f64 = self
.ln_t
.iter()
.map(|lt| (beta * (lt - ln_eta)).exp())
.sum();
self.r * beta.ln() - self.r * beta * ln_eta + (beta - 1.0) * self.sum_ln_fail - sum_z
}
fn profile_shape(&self, beta: f64) -> f64 {
let s0: f64 = self.ln_t.iter().map(|lt| (beta * lt).exp()).sum();
self.at(beta, (s0 / self.r).ln() / beta)
}
fn profile_over_shape(&self, beta_hat: f64, ln_eta: impl Fn(f64) -> f64) -> f64 {
let f = |s: f64| {
let beta = s.exp();
self.at(beta, ln_eta(beta))
};
let (mut a, mut b) = (beta_hat.ln() - 8.0, beta_hat.ln() + 8.0);
let g = (5f64.sqrt() - 1.0) / 2.0;
let (mut c, mut d) = (b - g * (b - a), a + g * (b - a));
let (mut fc, mut fd) = (f(c), f(d));
while b - a > 1e-12 {
if fc > fd {
b = d;
d = c;
fd = fc;
c = b - g * (b - a);
fc = f(c);
} else {
a = c;
c = d;
fc = fd;
d = a + g * (b - a);
fd = f(d);
}
}
f((a + b) / 2.0)
}
}
pub fn weibull_likelihood_interval(
failure_times: &[f64],
suspension_times: &[f64],
quantity: WeibullQuantity,
confidence: f64,
) -> Option<(f64, WeibullInterval)> {
let z = two_sided_z(confidence)?;
let fit = weibull_mle_censored(failure_times, suspension_times)?;
let data = Likelihood::new(failure_times, suspension_times);
let (beta_hat, eta_hat) = (fit.shape, fit.scale);
let max = data.at(beta_hat, eta_hat.ln());
let cutoff = z * z;
let ra = super::ReliabilityAnalysis::from_mle(&fit);
let (estimate, se_log, profile): (f64, f64, Box<dyn Fn(f64) -> f64 + '_>) = match quantity {
WeibullQuantity::Shape => (
beta_hat,
fit.covariance[0][0].sqrt() / beta_hat,
Box::new(|s: f64| data.profile_shape(s.exp())),
),
WeibullQuantity::Scale => (
eta_hat,
fit.covariance[1][1].sqrt() / eta_hat,
Box::new(|s: f64| data.profile_over_shape(beta_hat, |_| s)),
),
WeibullQuantity::BLife(p) => {
let (t_p, wald) = ra.b_life_interval(p, confidence)?;
let w = (-(1.0 - p).ln()).ln();
(
t_p,
(wald.upper / t_p).ln() / z,
Box::new(move |s: f64| data.profile_over_shape(beta_hat, |beta| s - w / beta)),
)
}
};
let deviance = |s: f64| 2.0 * (max - profile(s)) - cutoff;
let s0 = estimate.ln();
let step = if se_log.is_finite() && se_log > 0.0 {
se_log
} else {
0.1
};
let limit = |direction: f64| -> Option<f64> {
let mut inner = s0;
let mut h = step;
let mut outer = s0 + direction * h;
let mut tries = 0;
while deviance(outer) < 0.0 {
inner = outer;
h *= 2.0;
outer = s0 + direction * h;
tries += 1;
if tries > 40 || !outer.is_finite() {
return None;
}
}
for _ in 0..200 {
let mid = (inner + outer) / 2.0;
if (outer - inner).abs() < 1e-12 {
break;
}
if deviance(mid) < 0.0 {
inner = mid;
} else {
outer = mid;
}
}
Some(((inner + outer) / 2.0).exp())
};
let lower = limit(-1.0)?;
let upper = limit(1.0)?;
Some((estimate, WeibullInterval { lower, upper }))
}
#[cfg(test)]
mod tests {
use super::*;
const FAILURES: [f64; 6] = [55.0, 187.0, 216.0, 240.0, 244.0, 335.0];
const SURVIVORS: [f64; 4] = [361.0, 373.0, 375.0, 386.0];
fn chi2_1(confidence: f64) -> f64 {
let z = two_sided_z(confidence).unwrap();
z * z
}
#[test]
fn limits_sit_on_the_chi_square_cutoff() {
let data = Likelihood::new(&FAILURES, &SURVIVORS);
let fit = weibull_mle_censored(&FAILURES, &SURVIVORS).unwrap();
let max = data.at(fit.shape, fit.scale.ln());
let grid_max = |ln_eta: &dyn Fn(f64) -> f64| {
(1..=200_000)
.map(|i| {
let beta = i as f64 * 1e-4;
data.at(beta, ln_eta(beta))
})
.fold(f64::NEG_INFINITY, f64::max)
};
let q = chi2_1(0.9);
let (_, scale) =
weibull_likelihood_interval(&FAILURES, &SURVIVORS, WeibullQuantity::Scale, 0.9)
.unwrap();
for eta in [scale.lower, scale.upper] {
let dev = 2.0 * (max - grid_max(&|_| eta.ln()));
assert!(
(dev - q).abs() < 1e-4,
"scale limit {eta}: deviance {dev} vs {q}"
);
}
let (_, b10) =
weibull_likelihood_interval(&FAILURES, &SURVIVORS, WeibullQuantity::BLife(0.1), 0.9)
.unwrap();
let w = (-(0.9f64).ln()).ln();
for t in [b10.lower, b10.upper] {
let dev = 2.0 * (max - grid_max(&|beta| t.ln() - w / beta));
assert!(
(dev - q).abs() < 1e-4,
"B10 limit {t}: deviance {dev} vs {q}"
);
}
let (_, shape) =
weibull_likelihood_interval(&FAILURES, &SURVIVORS, WeibullQuantity::Shape, 0.9)
.unwrap();
for beta in [shape.lower, shape.upper] {
let dev = 2.0 * (max - data.profile_shape(beta));
assert!(
(dev - q).abs() < 1e-8,
"shape limit {beta}: deviance {dev} vs {q}"
);
}
}
#[test]
fn with_much_data_it_meets_the_wald_interval() {
use u_numflow::distributions::{Sample, Weibull};
let law = Weibull::new(2.0, 100.0).unwrap();
let mut rng = u_numflow::random::create_rng(7);
let data: Vec<f64> = (0..2000).map(|_| law.sample(&mut rng)).collect();
let fit = weibull_mle_censored(&data, &[]).unwrap();
let (_, lr) =
weibull_likelihood_interval(&data, &[], WeibullQuantity::Shape, 0.95).unwrap();
let wald = fit.shape_interval(0.95).unwrap();
assert!(
(lr.lower / wald.lower - 1.0).abs() < 0.005,
"{lr:?} vs {wald:?}"
);
assert!(
(lr.upper / wald.upper - 1.0).abs() < 0.005,
"{lr:?} vs {wald:?}"
);
}
#[test]
fn with_few_failures_it_covers_near_its_level() {
use u_numflow::distributions::{Sample, Weibull};
let law = Weibull::new(1.5, 100.0).unwrap();
let truth = 100.0 * (-(0.9f64).ln()).powf(1.0 / 1.5);
let mut rng = u_numflow::random::create_rng(31);
let runs = 800;
let (mut lr_in, mut wald_in, mut used) = (0, 0, 0);
for _ in 0..runs {
let data: Vec<f64> = (0..8).map(|_| law.sample(&mut rng)).collect();
let Some((_, lr)) =
weibull_likelihood_interval(&data, &[], WeibullQuantity::BLife(0.1), 0.9)
else {
continue;
};
let (_, wald) = weibull_mle_censored(&data, &[])
.unwrap()
.b_life_interval(0.1, 0.9)
.unwrap();
used += 1;
lr_in += usize::from(lr.lower <= truth && truth <= lr.upper);
wald_in += usize::from(wald.lower <= truth && truth <= wald.upper);
}
let (lr_c, wald_c) = (lr_in as f64 / used as f64, wald_in as f64 / used as f64);
assert!(used >= runs * 99 / 100, "{used} of {runs} fitted");
assert!(
(0.84..=0.94).contains(&lr_c),
"likelihood-ratio coverage {lr_c} (Wald {wald_c})"
);
assert!(
lr_c > wald_c + 0.025,
"likelihood-ratio {lr_c} vs Wald {wald_c}"
);
}
#[test]
fn refuses_what_it_cannot_answer() {
assert!(
weibull_likelihood_interval(&FAILURES, &SURVIVORS, WeibullQuantity::Shape, 1.0)
.is_none()
);
assert!(weibull_likelihood_interval(
&FAILURES,
&SURVIVORS,
WeibullQuantity::BLife(1.0),
0.9
)
.is_none());
assert!(weibull_likelihood_interval(&[10.0], &[], WeibullQuantity::Scale, 0.9).is_none());
}
}