use crate::ErrorModelError;
use statrs::distribution::{ContinuousCDF, Normal};
pub(crate) const LOG_2PI: f64 = 1.8378770664093453_f64;
#[inline(always)]
pub fn lognormpdf(obs: f64, pred: f64, sigma: f64) -> f64 {
let diff = obs - pred;
-0.5 * LOG_2PI - sigma.ln() - (diff * diff) / (2.0 * sigma * sigma)
}
#[inline(always)]
pub fn lognormcdf(obs: f64, pred: f64, sigma: f64) -> Result<f64, ErrorModelError> {
let norm = Normal::new(pred, sigma).map_err(|_| ErrorModelError::NegativeSigma)?;
let cdf = norm.cdf(obs);
if cdf <= 0.0 {
let z = (obs - pred) / sigma;
if z < -37.0 {
Ok(lognormpdf(obs, pred, sigma) - z.abs().ln())
} else {
Err(ErrorModelError::NegativeSigma) }
} else {
Ok(cdf.ln())
}
}
#[inline(always)]
pub fn lognormccdf(obs: f64, pred: f64, sigma: f64) -> Result<f64, ErrorModelError> {
let norm = Normal::new(pred, sigma).map_err(|_| ErrorModelError::NegativeSigma)?;
let sf = 1.0 - norm.cdf(obs);
if sf <= 0.0 {
let z = (obs - pred) / sigma;
if z > 37.0 {
Ok(lognormpdf(obs, pred, sigma) - z.ln())
} else {
Err(ErrorModelError::NegativeSigma)
}
} else {
Ok(sf.ln())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lognormpdf_standard_normal() {
let log_pdf = lognormpdf(0.0, 0.0, 1.0);
let expected = -0.5 * LOG_2PI;
assert!(
(log_pdf - expected).abs() < 1e-10,
"lognormpdf at mean should be -0.5*ln(2π)"
);
}
#[test]
fn test_lognormpdf_matches_exp_pdf() {
let obs = 1.5;
let pred = 1.0;
let sigma = 0.5;
let log_pdf = lognormpdf(obs, pred, sigma);
let pdf = log_pdf.exp();
let diff = obs - pred;
let expected_pdf = (1.0 / (sigma * (2.0 * std::f64::consts::PI).sqrt()))
* (-diff * diff / (2.0 * sigma * sigma)).exp();
assert!(
(pdf - expected_pdf).abs() < 1e-10,
"exp(lognormpdf) should match manual PDF calculation"
);
}
#[test]
fn test_lognormcdf_basic() {
let log_cdf = lognormcdf(0.0, 0.0, 1.0).unwrap();
let expected = 0.5_f64.ln();
assert!(
(log_cdf - expected).abs() < 1e-10,
"lognormcdf at mean should be ln(0.5)"
);
}
#[test]
fn test_lognormccdf_basic() {
let log_sf = lognormccdf(0.0, 0.0, 1.0).unwrap();
let expected = 0.5_f64.ln();
assert!(
(log_sf - expected).abs() < 1e-10,
"lognormccdf at mean should be ln(0.5)"
);
}
#[test]
fn test_lognormcdf_extreme() {
let result = lognormcdf(-40.0, 0.0, 1.0);
assert!(result.is_ok(), "lognormcdf should handle extreme values");
assert!(
result.unwrap().is_finite(),
"lognormcdf should return finite value"
);
}
#[test]
fn test_lognormccdf_extreme() {
let result = lognormccdf(40.0, 0.0, 1.0);
assert!(result.is_ok(), "lognormccdf should handle extreme values");
assert!(
result.unwrap().is_finite(),
"lognormccdf should return finite value"
);
}
}