Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 2 additions & 33 deletions src/distribution/beta.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::distribution::{Continuous, ContinuousCDF, InverseCdfError};
use crate::distribution::{Continuous, ContinuousCDF};
use crate::function::{beta, gamma};
use crate::statistics::*;
#[cfg(not(feature = "std"))]
Expand Down Expand Up @@ -181,10 +181,6 @@ impl ContinuousCDF<f64, f64> for Beta {
/// Calculates the inverse cumulative distribution function for the beta
/// distribution at `x`.
///
/// # Panics
///
/// If x is not in `[0, 1]`.
///
/// # Formula
///
/// ```text
Expand All @@ -194,34 +190,7 @@ impl ContinuousCDF<f64, f64> for Beta {
/// where `α` is shapeA, `β` is shapeB, and `I_x` is the inverse of the
/// regularized lower incomplete beta function.
fn inverse_cdf(&self, x: f64) -> f64 {
if !(0.0..=1.0).contains(&x) {
panic!("x must be in [0, 1]");
} else {
beta::inv_beta_reg(self.shape_a, self.shape_b, x)
}
}

/// Calculates the inverse cumulative distribution function for the beta
/// distribution at `x`.
///
/// # Returns an error instead of a panic
///
/// If x is not in `[0, 1]`.
///
/// # Formula
///
/// ```text
/// I^{-1}_x(α, β)
/// ```
///
/// where `α` is shapeA, `β` is shapeB, and `I_x` is the inverse of the
/// regularized lower incomplete beta function.
fn try_inverse_cdf(&self, x: f64) -> Result<f64, InverseCdfError> {
if !(0.0..=1.0).contains(&x) {
Err(InverseCdfError::ArgumentOutOfRange)
} else {
Ok(beta::inv_beta_reg(self.shape_a, self.shape_b, x))
}
beta::inv_beta_reg(self.shape_a, self.shape_b, x)
}
}

Expand Down
50 changes: 35 additions & 15 deletions src/distribution/categorical.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::distribution::{Discrete, DiscreteCDF};
use crate::distribution::{Discrete, DiscreteCDF, DiscreteInverseCdfError};
use crate::statistics::*;
use alloc::vec::Vec;
#[cfg(not(feature = "std"))]
Expand Down Expand Up @@ -181,10 +181,6 @@ impl DiscreteCDF<u64, f64> for Categorical {
/// categorical
/// distribution at `x`
///
/// # Panics
///
/// If `x <= 0.0` or `x >= 1.0`
///
/// # Formula
///
/// ```text
Expand All @@ -195,12 +191,23 @@ impl DiscreteCDF<u64, f64> for Categorical {
/// and `f(x)` is defined as `p_x + f(x - 1)` and `f(0) = p_0` where
/// `p_x` is the `x`th probability mass
fn inverse_cdf(&self, x: f64) -> u64 {
if x >= 1.0 || x <= 0.0 {
panic!("x must be in [0, 1]")
}

self.locate(x)
}

/// Calculates the inverse cumulative distribution function for the
/// categorical distribution at `x`, returning an error instead of
/// panicking if `x` is not in the open interval `(0.0, 1.0)`, including
/// when `x` is NaN.
fn try_inverse_cdf(&self, x: f64) -> Result<u64, DiscreteInverseCdfError<f64>> {
// Written as a conjunction of `>`/`<`, not `x >= 1.0 || x <= 0.0`, so

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it means the same in code below, are u sure we need explanation of code which is longer then code?

// that NaN (which compares false against everything) is rejected
// rather than falling through to `locate`, which panics on NaN.
if x > 0.0 && x < 1.0 {
Ok(self.inverse_cdf(x))
} else {
Err(DiscreteInverseCdfError::ArgumentOutOfRange)
}
}
}

impl Min<u64> for Categorical {
Expand Down Expand Up @@ -517,17 +524,30 @@ mod tests {
}

#[test]
#[should_panic]
fn test_inverse_cdf_input_low() {
fn test_try_inverse_cdf_input_low() {
let dist = create_ok(&[4.0, 2.5, 2.5, 1.0]);
assert_eq!(
dist.try_inverse_cdf(0.0),
Err(DiscreteInverseCdfError::ArgumentOutOfRange)
);
}

#[test]
fn test_try_inverse_cdf_input_high() {
let dist = create_ok(&[4.0, 2.5, 2.5, 1.0]);
dist.inverse_cdf(0.0);
assert_eq!(
dist.try_inverse_cdf(1.0),
Err(DiscreteInverseCdfError::ArgumentOutOfRange)
);
}

#[test]
#[should_panic]
fn test_inverse_cdf_input_high() {
fn test_try_inverse_cdf_nan_does_not_panic() {
let dist = create_ok(&[4.0, 2.5, 2.5, 1.0]);
dist.inverse_cdf(1.0);
assert_eq!(
dist.try_inverse_cdf(f64::NAN),
Err(DiscreteInverseCdfError::ArgumentOutOfRange)
);
}

#[test]
Expand Down
6 changes: 1 addition & 5 deletions src/distribution/cauchy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,7 @@ impl ContinuousCDF<f64, f64> for Cauchy {
///
/// where `x_0` is the location and `γ` is the scale
fn inverse_cdf(&self, x: f64) -> f64 {
if !(0.0..=1.0).contains(&x) {
panic!("x must be in [0, 1]");
} else {
self.location + self.scale * (f64_consts::PI * (x - 0.5)).tan()
}
self.location + self.scale * (f64_consts::PI * (x - 0.5)).tan()
}
}

Expand Down
24 changes: 11 additions & 13 deletions src/distribution/chi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,7 @@ impl ContinuousCDF<f64, f64> for Chi {

/// Calculates the inverse cumulative distribution function for the chi
/// distribution at `p`, i.e. the `p`-quantile.
///
/// # Panics
///
/// If `p` is not in `[0, 1]`.
fn inverse_cdf(&self, p: f64) -> f64 {
if !(0.0..=1.0).contains(&p) {
panic!("p must be in [0, 1]")
}
if p == 0.0 {
return self.min();
}
Expand Down Expand Up @@ -367,6 +360,7 @@ impl Continuous<f64, f64> for Chi {
mod tests {
use super::*;
use crate::distribution::internal::density_util;
use crate::distribution::InverseCdfError;
crate::distribution::internal::testing_boiler!(freedom: u64; Chi; ChiError);

#[test]
Expand Down Expand Up @@ -588,14 +582,18 @@ mod tests {
}

#[test]
#[should_panic(expected = "p must be in [0, 1]")]
fn test_inverse_cdf_p_above_one() {
create_ok(3).inverse_cdf(1.0 + f64::EPSILON);
fn test_try_inverse_cdf_p_above_one() {
assert_eq!(
create_ok(3).try_inverse_cdf(1.0 + f64::EPSILON),
Err(InverseCdfError::ArgumentOutOfRange)
);
}

#[test]
#[should_panic(expected = "p must be in [0, 1]")]
fn test_inverse_cdf_p_below_zero() {
create_ok(3).inverse_cdf(-1e-300);
fn test_try_inverse_cdf_p_below_zero() {
assert_eq!(
create_ok(3).try_inverse_cdf(-1e-300),
Err(InverseCdfError::ArgumentOutOfRange)
);
}
}
6 changes: 5 additions & 1 deletion src/distribution/chi_squared.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::distribution::{Continuous, ContinuousCDF, Gamma, GammaError};
use crate::distribution::{Continuous, ContinuousCDF, Gamma, GammaError, InverseCdfError};
use crate::statistics::*;

/// Implements the
Expand Down Expand Up @@ -152,6 +152,10 @@ impl ContinuousCDF<f64, f64> for ChiSquared {
fn inverse_cdf(&self, p: f64) -> f64 {
self.g.inverse_cdf(p)
}

fn try_inverse_cdf(&self, p: f64) -> Result<f64, InverseCdfError> {
self.g.try_inverse_cdf(p)
}
}

impl Min<f64> for ChiSquared {
Expand Down
6 changes: 5 additions & 1 deletion src/distribution/erlang.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::distribution::{Continuous, ContinuousCDF, Gamma, GammaError};
use crate::distribution::{Continuous, ContinuousCDF, Gamma, GammaError, InverseCdfError};
use crate::statistics::*;

/// Implements the [Erlang](https://en.wikipedia.org/wiki/Erlang_distribution)
Expand Down Expand Up @@ -137,6 +137,10 @@ impl ContinuousCDF<f64, f64> for Erlang {
fn inverse_cdf(&self, p: f64) -> f64 {
self.g.inverse_cdf(p)
}

fn try_inverse_cdf(&self, p: f64) -> Result<f64, InverseCdfError> {
self.g.try_inverse_cdf(p)
}
}

impl Min<f64> for Erlang {
Expand Down
8 changes: 2 additions & 6 deletions src/distribution/fisher_snedecor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,12 +201,8 @@ impl ContinuousCDF<f64, f64> for FisherSnedecor {
/// the second degree of freedom, and `I` is the regularized incomplete
/// beta function
fn inverse_cdf(&self, x: f64) -> f64 {
if !(0.0..=1.0).contains(&x) {
panic!("x must be in [0, 1]");
} else {
let z = beta::inv_beta_reg(self.freedom_1 / 2.0, self.freedom_2 / 2.0, x);
self.freedom_2 / (self.freedom_1 * (1.0 / z - 1.0))
}
let z = beta::inv_beta_reg(self.freedom_1 / 2.0, self.freedom_2 / 2.0, x);
self.freedom_2 / (self.freedom_1 * (1.0 / z - 1.0))
}
}

Expand Down
3 changes: 0 additions & 3 deletions src/distribution/gamma.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,6 @@ impl ContinuousCDF<f64, f64> for Gamma {
}

fn inverse_cdf(&self, p: f64) -> f64 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fter removing the domain panic, while cdf(high) < p { high *= 2 } never finishes for p > 1 (and low /= 2 spins at 0 for p < 0). ChiSquared/Erlang inherit this.

Checked against scipy 1.17 / mpmath:

  • gamma(a=2).ppf(1) = +∞ (this method already special-cases p == 1)
  • gamma(a=2).ppf(1+eps) = NaN
  • gamma(a=2).ppf(2) = NaN
  • gamma(a=2).ppf(-1e-16) = NaN

The new path hangs on those inputs. 1+eps is the realistic inverse_cdf(cdf(x)) rounding case. Unchecked should mean NaN/Inf like scipy, not a hang.

if !(0.0..=1.0).contains(&p) {
panic!("default inverse_cdf implementation should be provided probability on [0,1]")
}
if p == 0.0 {
return self.min();
};
Expand Down
Loading