Skip to content

Commit b21c4f6

Browse files
authored
Merge pull request #14 from SyntaxSpirits/fix/accurate-gamma-ln
2 parents acc40ff + 53619b0 commit b21c4f6

1 file changed

Lines changed: 66 additions & 5 deletions

File tree

src/distributions.rs

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -698,8 +698,9 @@ fn gamma_ln(x: f64) -> f64 {
698698
return f64::NEG_INFINITY;
699699
}
700700

701-
// Coefficients for g=7, n=9 from Numerical Recipes.
702-
const COEFFS: [f64; 9] = [
701+
// Lanczos coefficients for g=7, n=9 from Numerical Recipes.
702+
const LANCZOS_G: f64 = 7.0;
703+
const LANCZOS_COEFFS: [f64; 9] = [
703704
0.999_999_999_999_809_9,
704705
676.520_368_121_885_1,
705706
-1_259.139_216_722_402_8,
@@ -716,11 +717,11 @@ fn gamma_ln(x: f64) -> f64 {
716717
}
717718

718719
let z = x - 1.0;
719-
let mut a = COEFFS[0];
720-
for (i, coeff) in COEFFS.iter().enumerate().skip(1) {
720+
let mut a = LANCZOS_COEFFS[0];
721+
for (i, coeff) in LANCZOS_COEFFS.iter().enumerate().skip(1) {
721722
a += coeff / (z + i as f64);
722723
}
723-
let t = z + 7.5;
724+
let t = z + LANCZOS_G + 0.5;
724725

725726
0.5 * (2.0 * PI).ln() + (z + 0.5) * t.ln() - t + a.ln()
726727
}
@@ -853,6 +854,7 @@ mod tests {
853854
assert_eq!(poisson.variance(), 3.0);
854855

855856
assert_abs_diff_eq!(poisson.pmf(0), (-3.0_f64).exp(), epsilon = 1e-12);
857+
assert_abs_diff_eq!(poisson.log_pmf(0), -3.0, epsilon = 1e-12);
856858
assert_abs_diff_eq!(poisson.pmf(2), 4.5 * (-3.0_f64).exp(), epsilon = 1e-12);
857859
assert_abs_diff_eq!(
858860
poisson.log_pmf(2),
@@ -892,6 +894,65 @@ mod tests {
892894
assert_abs_diff_eq!(gamma_ln(10.0), 362_880.0_f64.ln(), epsilon = 1e-10);
893895
}
894896

897+
#[test]
898+
fn test_gamma_ln_regression_absolute_log_probabilities() {
899+
// These analytical checks pin absolute normalizing constants used by
900+
// distributions that depend on gamma_ln, including non-integer and
901+
// larger combinatorial cases. Decimal expectations were computed from
902+
// the closed-form definitions with Python's math.lgamma at double
903+
// precision so future changes can be independently re-verified.
904+
let exponential_gamma = Gamma::new(1.0, 1.0).unwrap();
905+
assert_abs_diff_eq!(exponential_gamma.log_pdf(1.0), -1.0, epsilon = 1e-12);
906+
907+
let non_integer_gamma = Gamma::new(2.5, 1.5).unwrap();
908+
assert_abs_diff_eq!(
909+
non_integer_gamma.log_pdf(1.2),
910+
-0.797_537_765_011_576_5,
911+
epsilon = 1e-12
912+
);
913+
914+
let uniform_beta = Beta::new(1.0, 1.0).unwrap();
915+
assert_abs_diff_eq!(uniform_beta.log_pdf(0.5), 0.0, epsilon = 1e-12);
916+
917+
let fractional_beta = Beta::new(2.5, 3.5).unwrap();
918+
assert_abs_diff_eq!(
919+
fractional_beta.log_pdf(0.4),
920+
0.650_335_112_735_843_9,
921+
epsilon = 1e-12
922+
);
923+
924+
let cauchy = StudentT::new(1.0, 0.0, 1.0).unwrap();
925+
assert_abs_diff_eq!(cauchy.log_pdf(0.0), -PI.ln(), epsilon = 1e-12);
926+
927+
let scaled_student_t = StudentT::new(5.0, 0.5, 1.25).unwrap();
928+
assert_abs_diff_eq!(
929+
scaled_student_t.log_pdf(1.75),
930+
-1.738_727_810_750_798_4,
931+
epsilon = 1e-12
932+
);
933+
934+
let poisson = Poisson::new(3.0).unwrap();
935+
assert_abs_diff_eq!(poisson.log_pmf(0), -3.0, epsilon = 1e-12);
936+
assert_abs_diff_eq!(
937+
poisson.log_pmf(4),
938+
-1.783_604_675_675_505_7,
939+
epsilon = 1e-12
940+
);
941+
942+
let fair_coin = Binomial::new(4, 0.5).unwrap();
943+
assert_abs_diff_eq!(fair_coin.log_pmf(2), (6.0_f64 / 16.0).ln(), epsilon = 1e-12);
944+
assert_abs_diff_eq!(
945+
log_binomial_coefficient(5, 2),
946+
10.0_f64.ln(),
947+
epsilon = 1e-12
948+
);
949+
assert_abs_diff_eq!(
950+
log_binomial_coefficient(10, 3),
951+
120.0_f64.ln(),
952+
epsilon = 1e-12
953+
);
954+
}
955+
895956
#[test]
896957
fn test_normal_creation() {
897958
let normal = Normal::new(0.0, 1.0).unwrap();

0 commit comments

Comments
 (0)