use crate::error::SearchError;
use crate::search::{search_monotone, SEARCH_BOUND};
use crate::special::beta_inc;
use crate::special::{beta_log, psi};
use crate::traits::{Continuous, ContinuousCdf, Entropy, Mean, Variance};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FisherSnedecor {
dfn: f64,
dfd: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Error)]
pub enum FisherSnedecorError {
#[error("numerator df must be positive, got {0}")]
DfnNotPositive(f64),
#[error("numerator df must be finite, got {0}")]
DfnNotFinite(f64),
#[error("denominator df must be positive, got {0}")]
DfdNotPositive(f64),
#[error("denominator df must be finite, got {0}")]
DfdNotFinite(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("p ({p}) and q ({q}) are not complementary: |p + q - 1| > 3ε")]
PQSumNotOne { p: f64, q: f64 },
#[error(transparent)]
Search(#[from] SearchError),
}
impl FisherSnedecor {
#[inline]
pub fn new(dfn: f64, dfd: f64) -> Self {
Self::try_new(dfn, dfd).unwrap()
}
#[inline]
pub fn try_new(dfn: f64, dfd: f64) -> Result<Self, FisherSnedecorError> {
if !dfn.is_finite() {
return Err(FisherSnedecorError::DfnNotFinite(dfn));
}
if dfn <= 0.0 {
return Err(FisherSnedecorError::DfnNotPositive(dfn));
}
if !dfd.is_finite() {
return Err(FisherSnedecorError::DfdNotFinite(dfd));
}
if dfd <= 0.0 {
return Err(FisherSnedecorError::DfdNotPositive(dfd));
}
Ok(Self { dfn, dfd })
}
#[inline]
pub const fn dfn(&self) -> f64 {
self.dfn
}
#[inline]
pub const fn dfd(&self) -> f64 {
self.dfd
}
#[inline]
pub fn search_dfn(p: f64, q: f64, f: f64, dfd: f64) -> Result<f64, FisherSnedecorError> {
check_pq(p, q)?;
if !f.is_finite() {
return Err(FisherSnedecorError::FNotFinite(f));
}
if f <= 0.0 {
return Err(FisherSnedecorError::FNotPositive(f));
}
if !dfd.is_finite() {
return Err(FisherSnedecorError::DfdNotFinite(dfd));
}
if dfd <= 0.0 {
return Err(FisherSnedecorError::DfdNotPositive(dfd));
}
let func = |dfn: f64| {
let dist = FisherSnedecor { dfn, dfd };
let cum = dist.cdf(f);
let ccum = dist.ccdf(f);
if p <= q {
cum - p
} else {
ccum - q
}
};
Ok(search_monotone(
1.0,
SEARCH_BOUND,
5.0,
1.0,
SEARCH_BOUND,
func,
)?)
}
#[inline]
pub fn search_dfd(p: f64, q: f64, f: f64, dfn: f64) -> Result<f64, FisherSnedecorError> {
check_pq(p, q)?;
if !f.is_finite() {
return Err(FisherSnedecorError::FNotFinite(f));
}
if f <= 0.0 {
return Err(FisherSnedecorError::FNotPositive(f));
}
if !dfn.is_finite() {
return Err(FisherSnedecorError::DfnNotFinite(dfn));
}
if dfn <= 0.0 {
return Err(FisherSnedecorError::DfnNotPositive(dfn));
}
let func = |dfd: f64| {
let dist = FisherSnedecor { dfn, dfd };
let cum = dist.cdf(f);
let ccum = dist.ccdf(f);
if p <= q {
cum - p
} else {
ccum - q
}
};
Ok(search_monotone(
1.0,
SEARCH_BOUND,
5.0,
1.0,
SEARCH_BOUND,
func,
)?)
}
}
#[inline]
fn check_p(p: f64) -> Result<(), FisherSnedecorError> {
if !(0.0..=1.0).contains(&p) || !p.is_finite() {
Err(FisherSnedecorError::PNotInRange(p))
} else {
Ok(())
}
}
#[inline]
fn check_q(q: f64) -> Result<(), FisherSnedecorError> {
if !(0.0..=1.0).contains(&q) || !q.is_finite() {
Err(FisherSnedecorError::QNotInRange(q))
} else {
Ok(())
}
}
#[inline]
fn check_pq(p: f64, q: f64) -> Result<(), FisherSnedecorError> {
check_p(p)?;
check_q(q)?;
if (p + q - 1.0).abs() > 3.0 * f64::EPSILON {
return Err(FisherSnedecorError::PQSumNotOne { p, q });
}
Ok(())
}
fn cumf(f: f64, dfn: f64, dfd: f64) -> (f64, f64) {
if f <= 0.0 {
return (0.0, 1.0);
}
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);
(q, p)
}
impl ContinuousCdf for FisherSnedecor {
type Error = FisherSnedecorError;
#[inline]
fn cdf(&self, x: f64) -> f64 {
cumf(x, self.dfn, self.dfd).0
}
#[inline]
fn ccdf(&self, x: f64) -> f64 {
cumf(x, self.dfn, self.dfd).1
}
#[inline]
fn inverse_cdf(&self, p: f64) -> Result<f64, FisherSnedecorError> {
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 q = 1.0 - p;
let func = |x: f64| {
let (cum, ccum) = cumf(x, dfn, dfd);
if p <= q {
cum - p
} else {
ccum - q
}
};
Ok(search_monotone(
0.0,
SEARCH_BOUND,
5.0,
0.0,
SEARCH_BOUND,
func,
)?)
}
}
impl FisherSnedecor {
#[inline]
pub fn inverse_ccdf(&self, q: f64) -> Result<f64, FisherSnedecorError> {
check_q(q)?;
if q == 1.0 {
return Ok(0.0);
}
if q == 0.0 {
return Ok(f64::INFINITY);
}
let dfn = self.dfn;
let dfd = self.dfd;
let p = 1.0 - q;
let func = |x: f64| {
let (cum, ccum) = cumf(x, dfn, dfd);
if p <= q {
cum - p
} else {
ccum - q
}
};
Ok(search_monotone(
0.0,
SEARCH_BOUND,
5.0,
0.0,
SEARCH_BOUND,
func,
)?)
}
}
impl Continuous for FisherSnedecor {
#[inline]
fn pdf(&self, x: f64) -> f64 {
if x <= 0.0 {
return 0.0;
}
self.ln_pdf(x).exp()
}
#[inline]
fn ln_pdf(&self, x: f64) -> f64 {
if x <= 0.0 {
return f64::NEG_INFINITY;
}
let dfn = self.dfn;
let dfd = self.dfd;
let half_dfn = dfn / 2.0;
let half_dfd = dfd / 2.0;
half_dfn * (dfn / dfd).ln() + (half_dfn - 1.0) * x.ln()
- (half_dfn + half_dfd) * (1.0 + dfn * x / dfd).ln()
- beta_log(half_dfn, half_dfd)
}
}
impl Mean for FisherSnedecor {
#[inline]
fn mean(&self) -> f64 {
if self.dfd > 2.0 {
self.dfd / (self.dfd - 2.0)
} else {
f64::NAN
}
}
}
impl Variance for FisherSnedecor {
#[inline]
fn variance(&self) -> f64 {
let dfn = self.dfn;
let dfd = self.dfd;
if dfd > 4.0 {
2.0 * dfd * dfd * (dfn + dfd - 2.0) / (dfn * (dfd - 2.0).powi(2) * (dfd - 4.0))
} else {
f64::NAN
}
}
}
impl Entropy for FisherSnedecor {
#[inline]
fn entropy(&self) -> f64 {
let dfn = self.dfn;
let dfd = self.dfd;
(dfd / dfn).ln() + beta_log(dfn / 2.0, dfd / 2.0) + (1.0 - dfn / 2.0) * psi(dfn / 2.0)
- (1.0 + dfd / 2.0) * psi(dfd / 2.0)
+ 0.5 * (dfn + dfd) * psi((dfn + dfd) / 2.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_invalid_parameters() {
assert!(matches!(
FisherSnedecor::try_new(0.0, 1.0),
Err(FisherSnedecorError::DfnNotPositive(0.0))
));
assert!(matches!(
FisherSnedecor::try_new(1.0, 0.0),
Err(FisherSnedecorError::DfdNotPositive(0.0))
));
}
#[test]
fn inverse_and_density_edges() {
let d = FisherSnedecor::new(5.0, 10.0);
assert_eq!(d.inverse_cdf(0.0).unwrap(), 0.0);
assert_eq!(d.inverse_ccdf(1.0).unwrap(), 0.0);
assert_eq!(d.pdf(0.0), 0.0);
assert_eq!(d.ln_pdf(0.0), f64::NEG_INFINITY);
assert!(d.inverse_ccdf(0.25).unwrap().is_finite());
assert!(d.pdf(1.5).is_finite());
assert!(d.ln_pdf(1.5).is_finite());
assert!(d.entropy().is_finite());
}
#[test]
fn moment_thresholds_and_invalid_solves() {
assert!(FisherSnedecor::new(5.0, 2.0).mean().is_nan());
assert!(FisherSnedecor::new(5.0, 4.0).variance().is_nan());
assert!(FisherSnedecor::new(5.0, 10.0).mean().is_finite());
assert!(FisherSnedecor::new(5.0, 10.0).variance().is_finite());
assert!(matches!(
FisherSnedecor::search_dfn(-0.1, 1.1, 1.0, 5.0),
Err(FisherSnedecorError::PNotInRange(-0.1))
));
assert!(matches!(
FisherSnedecor::search_dfn(0.5, 0.5, 0.0, 5.0),
Err(FisherSnedecorError::FNotPositive(0.0))
));
assert!(matches!(
FisherSnedecor::search_dfn(0.5, 0.5, 1.0, 0.0),
Err(FisherSnedecorError::DfdNotPositive(0.0))
));
assert!(matches!(
FisherSnedecor::search_dfd(0.5, 0.5, 0.0, 5.0),
Err(FisherSnedecorError::FNotPositive(0.0))
));
assert!(matches!(
FisherSnedecor::search_dfd(0.5, 0.5, 1.0, 0.0),
Err(FisherSnedecorError::DfnNotPositive(0.0))
));
}
}