use crate::DType;
use super::special::{self, LN_SQRT_2PI};
use crate::stats::distribution::{ContinuousDistribution, Distribution};
use crate::stats::error::{StatsError, StatsResult};
use numr::algorithm::special::SpecialFunctions;
use numr::error::Result;
use numr::ops::{ScalarOps, TensorOps};
use numr::runtime::{Runtime, RuntimeClient};
use numr::tensor::Tensor;
#[derive(Debug, Clone, Copy)]
pub struct LogNormal {
mu: f64,
sigma: f64,
}
impl LogNormal {
pub fn new(mu: f64, sigma: f64) -> StatsResult<Self> {
if sigma <= 0.0 {
return Err(StatsError::InvalidParameter {
name: "sigma".to_string(),
value: sigma,
reason: "must be positive".to_string(),
});
}
if !mu.is_finite() {
return Err(StatsError::InvalidParameter {
name: "mu".to_string(),
value: mu,
reason: "must be finite".to_string(),
});
}
Ok(Self { mu, sigma })
}
pub fn standard() -> Self {
Self {
mu: 0.0,
sigma: 1.0,
}
}
pub fn from_mean_var(mean: f64, var: f64) -> StatsResult<Self> {
if mean <= 0.0 {
return Err(StatsError::InvalidParameter {
name: "mean".to_string(),
value: mean,
reason: "must be positive".to_string(),
});
}
if var <= 0.0 {
return Err(StatsError::InvalidParameter {
name: "var".to_string(),
value: var,
reason: "must be positive".to_string(),
});
}
let sigma_sq = (1.0 + var / (mean * mean)).ln();
let sigma = sigma_sq.sqrt();
let mu = mean.ln() - sigma_sq / 2.0;
Self::new(mu, sigma)
}
pub fn mu(&self) -> f64 {
self.mu
}
pub fn sigma(&self) -> f64 {
self.sigma
}
}
impl Distribution for LogNormal {
fn mean(&self) -> f64 {
(self.mu + self.sigma * self.sigma / 2.0).exp()
}
fn var(&self) -> f64 {
let sigma_sq = self.sigma * self.sigma;
((2.0 * self.mu + sigma_sq).exp()) * (sigma_sq.exp() - 1.0)
}
fn entropy(&self) -> f64 {
self.mu + 0.5 + self.sigma.ln() + LN_SQRT_2PI
}
fn median(&self) -> f64 {
self.mu.exp()
}
fn mode(&self) -> f64 {
(self.mu - self.sigma * self.sigma).exp()
}
fn skewness(&self) -> f64 {
let sigma_sq = self.sigma * self.sigma;
(sigma_sq.exp() + 2.0) * (sigma_sq.exp() - 1.0).sqrt()
}
fn kurtosis(&self) -> f64 {
let sigma_sq = self.sigma * self.sigma;
(4.0 * sigma_sq).exp() + 2.0 * (3.0 * sigma_sq).exp() + 3.0 * (2.0 * sigma_sq).exp() - 6.0
}
}
impl ContinuousDistribution for LogNormal {
fn pdf(&self, x: f64) -> f64 {
if x <= 0.0 {
return 0.0;
}
self.log_pdf(x).exp()
}
fn log_pdf(&self, x: f64) -> f64 {
if x <= 0.0 {
return f64::NEG_INFINITY;
}
let log_x = x.ln();
let z = (log_x - self.mu) / self.sigma;
-log_x - LN_SQRT_2PI - self.sigma.ln() - 0.5 * z * z
}
fn cdf(&self, x: f64) -> f64 {
if x <= 0.0 {
return 0.0;
}
let z = (x.ln() - self.mu) / self.sigma;
special::norm_cdf(z)
}
fn sf(&self, x: f64) -> f64 {
if x <= 0.0 {
return 1.0;
}
let z = (x.ln() - self.mu) / self.sigma;
special::norm_cdf(-z)
}
fn ppf(&self, p: f64) -> StatsResult<f64> {
if !(0.0..=1.0).contains(&p) {
return Err(StatsError::InvalidProbability { value: p });
}
if p == 0.0 {
return Ok(0.0);
}
if p == 1.0 {
return Ok(f64::INFINITY);
}
let z = special::norm_ppf(p);
Ok((self.mu + self.sigma * z).exp())
}
fn pdf_tensor<R: Runtime<DType = DType>, C>(
&self,
x: &Tensor<R>,
client: &C,
) -> Result<Tensor<R>>
where
C: TensorOps<R> + ScalarOps<R> + RuntimeClient<R>,
{
self.log_pdf_tensor(x, client)
.and_then(|log_pdf| client.exp(&log_pdf))
}
fn log_pdf_tensor<R: Runtime<DType = DType>, C>(
&self,
x: &Tensor<R>,
client: &C,
) -> Result<Tensor<R>>
where
C: TensorOps<R> + ScalarOps<R> + RuntimeClient<R>,
{
let ln_x = client.log(x)?;
let centered = client.sub_scalar(&ln_x, self.mu)?;
let z = client.mul_scalar(¢ered, 1.0 / self.sigma)?;
let z_sq = client.square(&z)?;
let neg_half_z_sq = client.mul_scalar(&z_sq, -0.5)?;
let log_const = -self.sigma.ln() - LN_SQRT_2PI;
let result = client.add_scalar(&neg_half_z_sq, log_const)?;
client.sub(&result, &ln_x)
}
fn cdf_tensor<R: Runtime<DType = DType>, C>(
&self,
x: &Tensor<R>,
client: &C,
) -> Result<Tensor<R>>
where
C: TensorOps<R> + ScalarOps<R> + SpecialFunctions<R> + RuntimeClient<R>,
{
let ln_x = client.log(x)?;
let centered = client.sub_scalar(&ln_x, self.mu)?;
let z = client.mul_scalar(¢ered, 1.0 / self.sigma)?;
let z_scaled = client.mul_scalar(&z, -std::f64::consts::FRAC_1_SQRT_2)?;
let erfc_val = client.erfc(&z_scaled)?;
client.mul_scalar(&erfc_val, 0.5)
}
fn sf_tensor<R: Runtime<DType = DType>, C>(
&self,
x: &Tensor<R>,
client: &C,
) -> Result<Tensor<R>>
where
C: TensorOps<R> + ScalarOps<R> + SpecialFunctions<R> + RuntimeClient<R>,
{
let ln_x = client.log(x)?;
let centered = client.sub_scalar(&ln_x, self.mu)?;
let z = client.mul_scalar(¢ered, 1.0 / self.sigma)?;
let z_scaled = client.mul_scalar(&z, std::f64::consts::FRAC_1_SQRT_2)?;
let erfc_val = client.erfc(&z_scaled)?;
client.mul_scalar(&erfc_val, 0.5)
}
fn log_cdf_tensor<R: Runtime<DType = DType>, C>(
&self,
x: &Tensor<R>,
client: &C,
) -> Result<Tensor<R>>
where
C: TensorOps<R> + ScalarOps<R> + SpecialFunctions<R> + RuntimeClient<R>,
{
let cdf = self.cdf_tensor(x, client)?;
client.log(&cdf)
}
fn ppf_tensor<R: Runtime<DType = DType>, C>(
&self,
p: &Tensor<R>,
client: &C,
) -> Result<Tensor<R>>
where
C: TensorOps<R> + ScalarOps<R> + SpecialFunctions<R> + RuntimeClient<R>,
{
let two_p_minus_1 = client.sub_scalar(&client.mul_scalar(p, 2.0)?, 1.0)?;
let erfinv_val = client.erfinv(&two_p_minus_1)?;
let z = client.mul_scalar(&erfinv_val, std::f64::consts::SQRT_2)?;
let log_result = client.add_scalar(&client.mul_scalar(&z, self.sigma)?, self.mu)?;
client.exp(&log_result)
}
fn isf_tensor<R: Runtime<DType = DType>, C>(
&self,
p: &Tensor<R>,
client: &C,
) -> Result<Tensor<R>>
where
C: TensorOps<R> + ScalarOps<R> + SpecialFunctions<R> + RuntimeClient<R>,
{
let one_minus_p = client.rsub_scalar(p, 1.0)?;
self.ppf_tensor(&one_minus_p, client)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn test_lognormal_creation() {
let ln = LogNormal::new(0.0, 1.0).unwrap();
assert!((ln.mu() - 0.0).abs() < 1e-10);
assert!((ln.sigma() - 1.0).abs() < 1e-10);
assert!(LogNormal::new(0.0, 0.0).is_err());
assert!(LogNormal::new(0.0, -1.0).is_err());
}
#[test]
fn test_lognormal_from_mean_var() {
let ln = LogNormal::from_mean_var(2.0, 1.0).unwrap();
assert!((ln.mean() - 2.0).abs() < 1e-6);
assert!((ln.var() - 1.0).abs() < 1e-6);
}
#[test]
fn test_lognormal_moments() {
let ln = LogNormal::standard();
assert!((ln.mean() - 0.5_f64.exp()).abs() < 1e-10);
assert!((ln.median() - 1.0).abs() < 1e-10);
assert!((ln.mode() - (-1.0_f64).exp()).abs() < 1e-10);
}
#[test]
fn test_lognormal_pdf() {
let ln = LogNormal::standard();
let expected = 1.0 / (2.0 * PI).sqrt();
assert!((ln.pdf(1.0) - expected).abs() < 1e-10);
assert!((ln.pdf(0.0) - 0.0).abs() < 1e-10);
assert!((ln.pdf(-1.0) - 0.0).abs() < 1e-10);
}
#[test]
fn test_lognormal_cdf() {
let ln = LogNormal::standard();
assert!((ln.cdf(1.0) - 0.5).abs() < 1e-10);
assert!((ln.cdf(ln.median()) - 0.5).abs() < 1e-10);
assert!((ln.cdf(0.0) - 0.0).abs() < 1e-10);
}
#[test]
fn test_lognormal_ppf() {
let ln = LogNormal::new(1.0, 0.5).unwrap();
assert!((ln.ppf(0.5).unwrap() - ln.median()).abs() < 1e-10);
for p in [0.1, 0.25, 0.5, 0.75, 0.9] {
let x = ln.ppf(p).unwrap();
assert!((ln.cdf(x) - p).abs() < 1e-8, "Failed for p={}", p);
}
}
#[test]
fn test_lognormal_sf() {
let ln = LogNormal::standard();
for x in [0.5, 1.0, 2.0, 5.0] {
assert!((ln.sf(x) + ln.cdf(x) - 1.0).abs() < 1e-10);
}
}
}