use thiserror::Error;
use crate::error::SearchError;
use crate::search::search_monotone;
use crate::special::beta_inc;
use crate::special::gamma_log;
use crate::traits::{ContinuousCdf, Mean, Variance};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FisherSnedecorNoncentral {
dfn: f64,
dfd: f64,
ncp: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Error)]
pub enum FisherSnedecorNoncentralError {
#[error("numerator df must be > 0, got {0}")]
DfnNotPositive(f64),
#[error("numerator df must be >= 1, got {0}")]
DfnTooSmall(f64),
#[error("numerator df must be finite, got {0}")]
DfnNotFinite(f64),
#[error("denominator df must be > 0, got {0}")]
DfdNotPositive(f64),
#[error("denominator df must be >= 1, got {0}")]
DfdTooSmall(f64),
#[error("denominator df must be finite, got {0}")]
DfdNotFinite(f64),
#[error("noncentrality parameter must be ≥ 0, got {0}")]
NcpNegative(f64),
#[error("noncentrality parameter must be finite, got {0}")]
NcpNotFinite(f64),
#[error("f must be positive, got {0}")]
FNotPositive(f64),
#[error("f must be finite, got {0}")]
FNotFinite(f64),
#[error("probability {0} outside [0..1]")]
PNotInRange(f64),
#[error("probability {0} outside [0..1]")]
QNotInRange(f64),
#[error(transparent)]
Search(#[from] SearchError),
}
impl FisherSnedecorNoncentral {
#[inline]
pub fn new(dfn: f64, dfd: f64, ncp: f64) -> Self {
Self::try_new(dfn, dfd, ncp).unwrap()
}
#[inline]
pub fn try_new(dfn: f64, dfd: f64, ncp: f64) -> Result<Self, FisherSnedecorNoncentralError> {
if !dfn.is_finite() {
return Err(FisherSnedecorNoncentralError::DfnNotFinite(dfn));
}
if dfn <= 0.0 {
return Err(FisherSnedecorNoncentralError::DfnNotPositive(dfn));
}
if dfn < 1.0 {
return Err(FisherSnedecorNoncentralError::DfnTooSmall(dfn));
}
if !dfd.is_finite() {
return Err(FisherSnedecorNoncentralError::DfdNotFinite(dfd));
}
if dfd <= 0.0 {
return Err(FisherSnedecorNoncentralError::DfdNotPositive(dfd));
}
if dfd < 1.0 {
return Err(FisherSnedecorNoncentralError::DfdTooSmall(dfd));
}
if !ncp.is_finite() {
return Err(FisherSnedecorNoncentralError::NcpNotFinite(ncp));
}
if ncp < 0.0 {
return Err(FisherSnedecorNoncentralError::NcpNegative(ncp));
}
Ok(Self { dfn, dfd, ncp })
}
#[inline]
pub const fn dfn(&self) -> f64 {
self.dfn
}
#[inline]
pub const fn dfd(&self) -> f64 {
self.dfd
}
#[inline]
pub const fn ncp(&self) -> f64 {
self.ncp
}
#[inline]
pub fn search_dfn(
p: f64,
f: f64,
dfd: f64,
ncp: f64,
) -> Result<f64, FisherSnedecorNoncentralError> {
check_p(p)?;
if !f.is_finite() {
return Err(FisherSnedecorNoncentralError::FNotFinite(f));
}
if f <= 0.0 {
return Err(FisherSnedecorNoncentralError::FNotPositive(f));
}
if !dfd.is_finite() {
return Err(FisherSnedecorNoncentralError::DfdNotFinite(dfd));
}
if dfd <= 0.0 {
return Err(FisherSnedecorNoncentralError::DfdNotPositive(dfd));
}
if dfd < 1.0 {
return Err(FisherSnedecorNoncentralError::DfdTooSmall(dfd));
}
if !ncp.is_finite() {
return Err(FisherSnedecorNoncentralError::NcpNotFinite(ncp));
}
if ncp < 0.0 {
return Err(FisherSnedecorNoncentralError::NcpNegative(ncp));
}
let func = |dfn: f64| cumfnc(f, dfn, dfd, ncp).0 - p;
Ok(search_monotone(1.0, 1.0e30, 5.0, 0.0, 1.0e30, func)?)
}
#[inline]
pub fn search_dfd(
p: f64,
f: f64,
dfn: f64,
ncp: f64,
) -> Result<f64, FisherSnedecorNoncentralError> {
check_p(p)?;
if !f.is_finite() {
return Err(FisherSnedecorNoncentralError::FNotFinite(f));
}
if f <= 0.0 {
return Err(FisherSnedecorNoncentralError::FNotPositive(f));
}
if !dfn.is_finite() {
return Err(FisherSnedecorNoncentralError::DfnNotFinite(dfn));
}
if dfn <= 0.0 {
return Err(FisherSnedecorNoncentralError::DfnNotPositive(dfn));
}
if dfn < 1.0 {
return Err(FisherSnedecorNoncentralError::DfnTooSmall(dfn));
}
if !ncp.is_finite() {
return Err(FisherSnedecorNoncentralError::NcpNotFinite(ncp));
}
if ncp < 0.0 {
return Err(FisherSnedecorNoncentralError::NcpNegative(ncp));
}
let func = |dfd: f64| cumfnc(f, dfn, dfd, ncp).0 - p;
Ok(search_monotone(1.0, 1.0e30, 5.0, 0.0, 1.0e30, func)?)
}
#[inline]
pub fn search_ncp(
p: f64,
f: f64,
dfn: f64,
dfd: f64,
) -> Result<f64, FisherSnedecorNoncentralError> {
check_p(p)?;
if !f.is_finite() {
return Err(FisherSnedecorNoncentralError::FNotFinite(f));
}
if f <= 0.0 {
return Err(FisherSnedecorNoncentralError::FNotPositive(f));
}
if !dfn.is_finite() {
return Err(FisherSnedecorNoncentralError::DfnNotFinite(dfn));
}
if dfn <= 0.0 {
return Err(FisherSnedecorNoncentralError::DfnNotPositive(dfn));
}
if dfn < 1.0 {
return Err(FisherSnedecorNoncentralError::DfnTooSmall(dfn));
}
if !dfd.is_finite() {
return Err(FisherSnedecorNoncentralError::DfdNotFinite(dfd));
}
if dfd <= 0.0 {
return Err(FisherSnedecorNoncentralError::DfdNotPositive(dfd));
}
if dfd < 1.0 {
return Err(FisherSnedecorNoncentralError::DfdTooSmall(dfd));
}
let func = |ncp: f64| cumfnc(f, dfn, dfd, ncp).0 - p;
Ok(search_monotone(0.0, 1.0e4, 5.0, 0.0, 1.0e4, func)?)
}
}
#[inline]
fn check_p(p: f64) -> Result<(), FisherSnedecorNoncentralError> {
if !(0.0..=1.0).contains(&p) || !p.is_finite() {
Err(FisherSnedecorNoncentralError::PNotInRange(p))
} else {
Ok(())
}
}
fn cumfnc(f: f64, dfn: f64, dfd: f64, pnonc: f64) -> (f64, f64) {
if f.is_nan() || dfn.is_nan() || dfd.is_nan() || pnonc.is_nan() {
return (f64::NAN, f64::NAN);
}
if f <= 0.0 {
return (0.0, 1.0);
}
if pnonc < 1e-10 {
let prod = dfn * f;
let dsum = dfd + prod;
let mut xx = dfd / dsum;
let yy;
if xx > 0.5 {
yy = prod / dsum;
xx = 1.0 - yy;
} else {
yy = 1.0 - xx;
}
let (p, q) = beta_inc(0.5 * dfd, 0.5 * dfn, xx, yy);
return (q, p);
}
let eps = 1e-4;
let xnonc = pnonc / 2.0;
let mut icent = xnonc as i32;
if icent == 0 {
icent = 1;
}
let centwt = (-xnonc + (icent as f64) * xnonc.ln() - gamma_log((icent + 1) as f64)).exp();
let prod = dfn * f;
let dsum = dfd + prod;
let mut yy = dfd / dsum;
let xx;
if yy > 0.5 {
xx = prod / dsum;
yy = 1.0 - xx;
} else {
xx = 1.0 - yy;
}
let (mut betdn, _) = beta_inc(0.5 * dfn + icent as f64, 0.5 * dfd, xx, yy);
let mut adn = dfn / 2.0 + icent as f64;
let mut aup = adn;
let b = dfd / 2.0;
let mut betup = betdn;
let mut sum = centwt * betdn;
let mut xmult = centwt;
let mut i = icent;
let mut dnterm =
(gamma_log(adn + b) - gamma_log(adn + 1.0) - gamma_log(b) + adn * xx.ln() + b * yy.ln())
.exp();
loop {
let small = sum < f64::EPSILON || xmult * betdn < eps * sum;
if small || i <= 0 {
break;
}
xmult *= i as f64 / xnonc;
i -= 1;
adn -= 1.0;
dnterm *= (adn + 1.0) / ((adn + b) * xx);
betdn += dnterm;
sum += xmult * betdn;
}
let mut i = icent + 1;
let mut xmult = centwt;
let expon = if aup - 1.0 + b == 0.0 {
-gamma_log(aup) - gamma_log(b) + (aup - 1.0) * xx.ln() + b * yy.ln()
} else {
gamma_log(aup - 1.0 + b) - gamma_log(aup) - gamma_log(b)
+ (aup - 1.0) * xx.ln()
+ b * yy.ln()
};
let mut upterm = if expon <= f64::EPSILON.ln() {
0.0
} else {
expon.exp()
};
loop {
xmult *= xnonc / i as f64;
i += 1;
aup += 1.0;
upterm *= (aup + b - 2.0) * xx / (aup - 1.0);
betup -= upterm;
sum += xmult * betup;
let small = sum < f64::EPSILON || xmult * betup < eps * sum;
if small {
break;
}
}
(sum, 0.5 + (0.5 - sum))
}
impl ContinuousCdf for FisherSnedecorNoncentral {
type Error = FisherSnedecorNoncentralError;
#[inline]
fn cdf(&self, x: f64) -> f64 {
cumfnc(x, self.dfn, self.dfd, self.ncp).0
}
#[inline]
fn ccdf(&self, x: f64) -> f64 {
cumfnc(x, self.dfn, self.dfd, self.ncp).1
}
#[inline]
fn inverse_cdf(&self, p: f64) -> Result<f64, FisherSnedecorNoncentralError> {
check_p(p)?;
if p == 0.0 {
return Ok(0.0);
}
if p == 1.0 {
return Ok(f64::INFINITY);
}
let dfn = self.dfn;
let dfd = self.dfd;
let ncp = self.ncp;
let func = |x: f64| cumfnc(x, dfn, dfd, ncp).0 - p;
Ok(search_monotone(0.0, 1.0e30, 5.0, 0.0, 1.0e30, func)?)
}
}
impl Mean for FisherSnedecorNoncentral {
#[inline]
fn mean(&self) -> f64 {
if self.dfd > 2.0 {
self.dfd * (self.dfn + self.ncp) / (self.dfn * (self.dfd - 2.0))
} else {
f64::NAN
}
}
}
impl Variance for FisherSnedecorNoncentral {
#[inline]
fn variance(&self) -> f64 {
let dfn = self.dfn;
let dfd = self.dfd;
let ncp = self.ncp;
if dfd > 4.0 {
2.0 * dfd * dfd * ((dfn + ncp).powi(2) + (dfd - 2.0) * (dfn + 2.0 * ncp))
/ (dfn * dfn * (dfd - 2.0).powi(2) * (dfd - 4.0))
} else {
f64::NAN
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_invalid_inputs() {
assert!(matches!(
FisherSnedecorNoncentral::try_new(0.0, 5.0, 1.0),
Err(FisherSnedecorNoncentralError::DfnNotPositive(0.0))
));
assert!(matches!(
FisherSnedecorNoncentral::try_new(0.5, 5.0, 1.0),
Err(FisherSnedecorNoncentralError::DfnTooSmall(0.5))
));
assert!(matches!(
FisherSnedecorNoncentral::try_new(5.0, 0.0, 1.0),
Err(FisherSnedecorNoncentralError::DfdNotPositive(0.0))
));
assert!(matches!(
FisherSnedecorNoncentral::try_new(5.0, 0.5, 1.0),
Err(FisherSnedecorNoncentralError::DfdTooSmall(0.5))
));
assert!(matches!(
FisherSnedecorNoncentral::try_new(5.0, 5.0, -1.0),
Err(FisherSnedecorNoncentralError::NcpNegative(-1.0))
));
assert!(matches!(
FisherSnedecorNoncentral::search_ncp(-0.1, 1.0, 5.0, 10.0),
Err(FisherSnedecorNoncentralError::PNotInRange(-0.1))
));
assert!(matches!(
FisherSnedecorNoncentral::search_dfd(0.5, 1.0, 0.5, 1.0),
Err(FisherSnedecorNoncentralError::DfnTooSmall(0.5))
));
assert!(matches!(
FisherSnedecorNoncentral::search_ncp(0.5, 1.0, 5.0, 0.5),
Err(FisherSnedecorNoncentralError::DfdTooSmall(0.5))
));
}
#[test]
fn inverse_and_moment_edges() {
let d = FisherSnedecorNoncentral::new(5.0, 10.0, 2.0);
assert_eq!(d.inverse_cdf(0.0).unwrap(), 0.0);
assert!(d.inverse_cdf(0.25).unwrap().is_finite());
assert!(d.mean().is_finite());
assert!(d.variance().is_finite());
assert!(FisherSnedecorNoncentral::new(5.0, 2.0, 2.0).mean().is_nan());
assert!(FisherSnedecorNoncentral::new(5.0, 4.0, 2.0)
.variance()
.is_nan());
}
#[test]
fn central_reduction_path_is_consistent() {
let d = FisherSnedecorNoncentral::new(5.0, 10.0, 0.0);
let x = 1.5;
let cdf = d.cdf(x);
let ccdf = d.ccdf(x);
assert!((cdf + ccdf - 1.0).abs() < 1e-12);
}
}