Skip to content

Commit 0102fed

Browse files
committed
lgamma
1 parent 9000902 commit 0102fed

File tree

3 files changed

+69
-1
lines changed

3 files changed

+69
-1
lines changed

proptest-regressions/gamma.txt

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
cc e8ed768221998086795d95c68921437e80c4b7fe68fe9da15ca40faa216391b5 # shrinks to x = 0.0
88
cc 23c7f86ab299daa966772921d8c615afda11e1b77944bed40e88264a68e62ac3 # shrinks to x = -19.80948467648103
99
cc f57954d91904549b9431755f196b630435a43cbefd558b932efad487a403c6c8 # shrinks to x = 0.003585187864492183
10+
cc 7a9a04aed4ed7e3d23eb7b32b748542b1062e349ae83cc1fad39672a5b2156cd # shrinks to x = -3.8510064710745118

src/gamma.rs

+67
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,52 @@ pub fn tgamma(x: f64) -> Result<f64, Error> {
203203
}
204204
}
205205

206+
// natural log of the absolute value of the Gamma function.
207+
// For large arguments, Lanczos' formula works extremely well here.
208+
pub fn lgamma(x: f64) -> Result<f64, Error> {
209+
// special cases
210+
if !x.is_finite() {
211+
if x.is_nan() {
212+
return Ok(x); // lgamma(nan) = nan
213+
} else {
214+
return Ok(f64::INFINITY); // lgamma(+-inf) = +inf
215+
}
216+
}
217+
218+
// integer arguments
219+
if x == x.floor() && x <= 2.0 {
220+
if x <= 0.0 {
221+
// lgamma(n) = inf, divide-by-zero for integers n <= 0
222+
return Err(Error::EDOM);
223+
} else {
224+
// lgamma(1) = lgamma(2) = 0.0
225+
return Ok(0.0);
226+
}
227+
}
228+
229+
let absx = x.abs();
230+
// tiny arguments: lgamma(x) ~ -log(fabs(x)) for small x
231+
if absx < 1e-20 {
232+
return Ok(-absx.ln());
233+
}
234+
235+
// Lanczos' formula. We could save a fraction of a ulp in accuracy by
236+
// having a second set of numerator coefficients for lanczos_sum that
237+
// absorbed the exp(-lanczos_g) term, and throwing out the lanczos_g
238+
// subtraction below; it's probably not worth it.
239+
let mut r = lanczos_sum(absx).ln() - LANCZOS_G;
240+
r += (absx - 0.5) * ((absx + LANCZOS_G - 0.5).ln() - 1.0);
241+
242+
if x < 0.0 {
243+
// Use reflection formula to get value for negative x
244+
r = LOG_PI - m_sinpi(absx).abs().ln() - absx.ln() - r;
245+
}
246+
if r.is_infinite() {
247+
return Err(Error::ERANGE);
248+
}
249+
Ok(r)
250+
}
251+
206252
#[cfg(test)]
207253
mod tests {
208254
use super::*;
@@ -259,5 +305,26 @@ mod tests {
259305
assert!((py_gamma_repr - rs_gamma_repr).abs() <= 1, "x = {x} diff: {}, py_gamma = {py_gamma} ({py_gamma_repr:x}), rs_gamma = {rs_gamma} ({rs_gamma_repr:x})", py_gamma_repr ^ rs_gamma_repr);
260306
});
261307
}
308+
309+
#[test]
310+
fn test_lgamma(x: f64) {
311+
let rs_lgamma = lgamma(x);
312+
313+
pyo3::prepare_freethreaded_python();
314+
Python::with_gil(|py| {
315+
let math = PyModule::import(py, "math").unwrap();
316+
let py_lgamma_func = math
317+
.getattr("lgamma")
318+
.unwrap();
319+
let r = py_lgamma_func.call1((x,));
320+
let Some((py_lgamma, rs_lgamma)) = unwrap(py, r, rs_lgamma) else {
321+
return;
322+
};
323+
let py_lgamma_repr = unsafe { std::mem::transmute::<f64, i64>(py_lgamma) };
324+
let rs_lgamma_repr = unsafe { std::mem::transmute::<f64, i64>(rs_lgamma) };
325+
// allow 6 bit error for now
326+
assert!((py_lgamma_repr - rs_lgamma_repr).abs() <= 6, "x = {x} diff: {}, py_lgamma = {py_lgamma} ({py_lgamma_repr:x}), rs_lgamma = {rs_lgamma} ({rs_lgamma_repr:x})", py_lgamma_repr ^ rs_lgamma_repr);
327+
});
328+
}
262329
}
263330
}

src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ mod err;
22
mod gamma;
33

44
pub use err::Error;
5-
pub use gamma::tgamma as gamma;
5+
pub use gamma::{tgamma as gamma, lgamma};

0 commit comments

Comments
 (0)