mathhook_core/functions/special/
gamma.rs

1//! Gamma, Beta, Digamma, and Polygamma special functions with high-precision numerical evaluation.
2//!
3//! Implements the Gamma function, Beta function, Digamma function, and Polygamma functions
4//! with comprehensive numerical evaluation support.
5//!
6//! # Numerical Evaluation
7//!
8//! Float inputs are automatically evaluated numerically using the Lanczos approximation
9//! (14-digit precision). Half-integer values are handled symbolically for exact results.
10//!
11//! # Half-Integer Special Cases
12//!
13//! The gamma function has exact symbolic forms for half-integers:
14//! - Γ(1/2) = √π
15//! - Γ(3/2) = √π/2
16//! - Γ(5/2) = 3√π/4
17//! - Γ(n+1/2) = (2n-1)!! · √π / 2^n
18//!
19//! # Beta Function
20//!
21//! The beta function B(a, b) = Γ(a)·Γ(b)/Γ(a+b) supports both symbolic and numerical
22//! evaluation. Float inputs are evaluated numerically using Lanczos gamma.
23//!
24//! # Digamma Function
25//!
26//! The digamma function ψ(z) = d/dz ln(Γ(z)) = Γ'(z)/Γ(z) is the logarithmic derivative
27//! of the gamma function. Special values are computed exactly.
28//!
29//! # Polygamma Function
30//!
31//! The polygamma function ψ^(n)(z) is the (n+1)-th derivative of ln(Γ(z)).
32//! ψ^(0) = digamma, ψ^(1) = trigamma, etc.
33//!
34//! # Input Validation
35//!
36//! All numerical functions validate inputs for NaN, infinity, and mathematical poles.
37//! Non-positive integers are poles for the gamma function and return infinity.
38
39use crate::core::{Expression, Number};
40use std::f64::consts::PI;
41
42/// Gamma function Γ(z)
43///
44/// The Gamma function extends the factorial to complex numbers:
45/// Γ(n) = (n-1)! for positive integers n
46///
47/// # Mathematical Properties
48///
49/// - Γ(n+1) = n·Γ(n) (functional equation)
50/// - Γ(1) = 1
51/// - Γ(1/2) = √π
52/// - Pole at non-positive integers
53///
54/// # Numerical Evaluation
55///
56/// Float inputs are evaluated numerically using Lanczos approximation (14-digit precision).
57/// Half-integers return exact symbolic forms (e.g., Γ(1/2) = √π).
58///
59/// # Input Validation
60///
61/// - NaN or infinity inputs return NaN
62/// - Non-positive integers are poles (return symbolic or error)
63///
64/// # Arguments
65///
66/// * `z` - Expression to evaluate gamma function at
67///
68/// # Examples
69///
70/// ```rust
71/// use mathhook_core::{Expression, Number};
72/// use mathhook_core::functions::special::gamma;
73///
74/// let result = gamma(&Expression::Number(Number::Integer(5)));
75/// assert_eq!(result, Expression::Number(Number::Integer(24)));
76///
77/// let half = gamma(&Expression::Number(Number::Float(0.5)));
78/// ```
79pub fn gamma(z: &Expression) -> Expression {
80    match z {
81        Expression::Number(Number::Integer(n)) if *n > 0 => {
82            let val = *n;
83            if val == 1 {
84                Expression::Number(Number::Integer(1))
85            } else {
86                let mut result = 1i64;
87                for i in 1..val {
88                    result *= i;
89                }
90                Expression::Number(Number::Integer(result))
91            }
92        }
93        Expression::Number(Number::Float(x)) => {
94            let twice = x * 2.0;
95            if (twice - twice.round()).abs() < 1e-10 {
96                gamma_half_integer(*x)
97            } else {
98                let result = lanczos_gamma(*x);
99                Expression::Number(Number::Float(result))
100            }
101        }
102        _ => Expression::function("gamma", vec![z.clone()]),
103    }
104}
105
106/// Computes gamma function for half-integers symbolically
107///
108/// Returns exact symbolic expressions using sqrt(pi):
109/// - Γ(1/2) = √π
110/// - Γ(3/2) = √π/2
111/// - Γ(5/2) = 3√π/4
112/// - Γ(n+1/2) = (2n-1)!! · √π / 2^n
113fn gamma_half_integer(x: f64) -> Expression {
114    let n = (x - 0.5).round() as i64;
115    if (x - (n as f64 + 0.5)).abs() < 1e-10 && n >= 0 {
116        let sqrt_pi = Expression::sqrt(Expression::pi());
117        if n == 0 {
118            return sqrt_pi;
119        }
120        let mut double_fact = Expression::integer(1);
121        for k in 0..n {
122            let term = Expression::integer(2 * k + 1);
123            double_fact = Expression::mul(vec![double_fact, term]);
124        }
125        let numerator = Expression::mul(vec![double_fact, sqrt_pi]);
126        let denominator = Expression::pow(Expression::integer(2), Expression::integer(n));
127        Expression::div(numerator, denominator)
128    } else {
129        Expression::Number(Number::Float(lanczos_gamma(x)))
130    }
131}
132
133/// Lanczos approximation for Gamma function (for numerical evaluation)
134///
135/// Provides accurate numerical evaluation using the Lanczos approximation
136/// with 14-digit precision. This is used for non-special values.
137///
138/// # Input Validation
139///
140/// - NaN or infinity inputs return NaN
141/// - Non-positive integers (poles) return infinity
142///
143/// # Examples
144///
145/// ```rust
146/// use mathhook_core::functions::special::lanczos_gamma;
147///
148/// let result = lanczos_gamma(5.0);
149/// assert!((result - 24.0).abs() < 1e-10);
150///
151/// let half = lanczos_gamma(0.5);
152/// let sqrt_pi = std::f64::consts::PI.sqrt();
153/// assert!((half - sqrt_pi).abs() < 1e-14);
154/// ```
155pub fn lanczos_gamma(z: f64) -> f64 {
156    if z.is_nan() || z.is_infinite() {
157        return f64::NAN;
158    }
159    if z <= 0.0 && (z - z.round()).abs() < 1e-10 {
160        return f64::INFINITY;
161    }
162    const LANCZOS_G: f64 = 7.0;
163    const LANCZOS_COEFFS: [f64; 9] = [
164        0.999_999_999_999_809_9,
165        676.5203681218851,
166        -1259.1392167224028,
167        771.323_428_777_653_1,
168        -176.615_029_162_140_6,
169        12.507343278686905,
170        -0.13857109526572012,
171        9.984_369_578_019_572e-6,
172        1.5056327351493116e-7,
173    ];
174    if z < 0.5 {
175        PI / (f64::sin(PI * z) * lanczos_gamma(1.0 - z))
176    } else {
177        let z = z - 1.0;
178        let mut x = LANCZOS_COEFFS[0];
179        for (i, coef) in LANCZOS_COEFFS.iter().enumerate().skip(1) {
180            x += coef / (z + i as f64);
181        }
182        let t = z + LANCZOS_G + 0.5;
183        f64::sqrt(2.0 * PI) * f64::powf(t, z + 0.5) * f64::exp(-t) * x
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn test_gamma_positive_integers() {
193        assert_eq!(
194            gamma(&Expression::Number(Number::Integer(1))),
195            Expression::Number(Number::Integer(1))
196        );
197        assert_eq!(
198            gamma(&Expression::Number(Number::Integer(2))),
199            Expression::Number(Number::Integer(1))
200        );
201        assert_eq!(
202            gamma(&Expression::Number(Number::Integer(3))),
203            Expression::Number(Number::Integer(2))
204        );
205        assert_eq!(
206            gamma(&Expression::Number(Number::Integer(4))),
207            Expression::Number(Number::Integer(6))
208        );
209        assert_eq!(
210            gamma(&Expression::Number(Number::Integer(5))),
211            Expression::Number(Number::Integer(24))
212        );
213    }
214
215    #[test]
216    fn test_lanczos_gamma_numerical() {
217        let result = lanczos_gamma(5.0);
218        assert!((result - 24.0).abs() < 1e-10);
219    }
220
221    #[test]
222    fn test_lanczos_gamma_accuracy() {
223        let result_half = lanczos_gamma(0.5);
224        let expected_half = std::f64::consts::PI.sqrt();
225        assert!(
226            (result_half - expected_half).abs() < 1e-14,
227            "Γ(1/2) accuracy: expected {}, got {}",
228            expected_half,
229            result_half
230        );
231        let result_one = lanczos_gamma(1.0);
232        assert!((result_one - 1.0).abs() < 1e-14, "Γ(1) = 1");
233        let result_two = lanczos_gamma(2.0);
234        assert!((result_two - 1.0).abs() < 1e-14, "Γ(2) = 1");
235        let result_three = lanczos_gamma(3.0);
236        assert!((result_three - 2.0).abs() < 1e-14, "Γ(3) = 2");
237    }
238
239    #[test]
240    fn test_gamma_half_integer_symbolic() {
241        let result = gamma(&Expression::Number(Number::Float(0.5)));
242        let expected = Expression::sqrt(Expression::pi());
243        assert_eq!(result, expected, "Γ(1/2) should be √π symbolically");
244        let result_1_5 = gamma(&Expression::Number(Number::Float(1.5)));
245        let sqrt_pi = Expression::sqrt(Expression::pi());
246        let expected_1_5 = Expression::div(sqrt_pi, Expression::integer(2));
247        assert_eq!(
248            result_1_5, expected_1_5,
249            "Γ(3/2) should be √π/2 symbolically"
250        );
251    }
252
253    #[test]
254    fn test_gamma_float_numerical() {
255        let result = gamma(&Expression::Number(Number::Float(3.7)));
256        match result {
257            Expression::Number(Number::Float(_)) => {}
258            _ => panic!("Γ(3.7) should return numerical Float"),
259        }
260    }
261
262    #[test]
263    fn test_lanczos_gamma_input_validation() {
264        assert!(lanczos_gamma(f64::NAN).is_nan());
265        assert!(lanczos_gamma(f64::INFINITY).is_nan());
266        assert!(lanczos_gamma(0.0).is_infinite());
267        assert!(lanczos_gamma(-1.0).is_infinite());
268        assert!(lanczos_gamma(-2.0).is_infinite());
269    }
270}