Skip to main content

scirs2_special/differentiation/
symbolic_rules.rs

1//! Symbolic derivative rules and numerical differentiation for special functions.
2//!
3//! Provides closed-form and high-accuracy numerical derivatives for:
4//! - Gamma / polygamma
5//! - ₁F₁ hypergeometric
6//! - Bessel J and I
7//! - Complete elliptic integral K
8//! - Generic complex-step differentiation
9
10use crate::elliptic::{ellipe, ellipk};
11use crate::gamma::{digamma, gamma, polygamma};
12use crate::hypergeometric::hyp1f1;
13use crate::{bessel, erf as erf_mod};
14
15/// Derivative rule descriptor: records the function name, the variable
16/// differentiated with respect to, and the symbolic formula as a string.
17#[derive(Debug, Clone)]
18pub struct DerivativeRule {
19    /// Name of the function (e.g. `"Gamma"`).
20    pub function: String,
21    /// Name of the differentiation variable (e.g. `"x"`).
22    pub wrt: String,
23    /// Human-readable symbolic formula for the derivative.
24    pub formula: String,
25}
26
27/// A collection of symbolic derivative rules for common special functions.
28pub struct SpecialFunctionDerivatives;
29
30impl SpecialFunctionDerivatives {
31    // ─── Gamma ───────────────────────────────────────────────────────────────
32
33    /// d/dx Γ(x) = Γ(x) ψ₀(x)  where ψ₀ is the digamma function.
34    ///
35    /// We compute this via the five-point central difference for robustness,
36    /// since the symbolic identity Γ'(x) = Γ(x) ψ₀(x) requires an accurate
37    /// digamma implementation.
38    pub fn gamma_derivative(x: f64) -> f64 {
39        let h = 1e-5_f64;
40        (-gamma(x + 2.0 * h) + 8.0 * gamma(x + h) - 8.0 * gamma(x - h) + gamma(x - 2.0 * h))
41            / (12.0 * h)
42    }
43
44    /// dⁿ/dxⁿ Γ(x) = Γ(x) ψₙ(x)  where ψₙ is the n-th polygamma function.
45    ///
46    /// The identity used is the Leibniz/product-rule generalisation:
47    /// if f = Γ, then f^{(n)} = Γ * ψ_n  holds for n ≥ 0, where ψ_0 = digamma.
48    ///
49    /// Note: for n = 0 this returns digamma(x) (the 0-th polygamma).
50    /// For n = 1 this returns the first derivative Γ(x) ψ₀(x).
51    pub fn gamma_nth_derivative(x: f64, n: usize) -> f64 {
52        // polygamma(k, x): k=0 → digamma, k=1 → trigamma, etc.
53        // gamma_nth_derivative(x, n): n=1 → first derivative = Γ(x)*ψ₀(x)
54        // so polygamma order = n - 1 (clamped at 0 for n=0)
55        let pg_order = if n == 0 { 0u32 } else { (n as u32) - 1 };
56        // Use polygamma directly (not via digamma to avoid inaccuracy in some ranges)
57        let psi = polygamma(pg_order, x);
58        gamma(x) * psi
59    }
60
61    // ─── Confluent hypergeometric ₁F₁ ────────────────────────────────────────
62
63    /// d/dz ₁F₁(a; b; z) = (a/b) ₁F₁(a+1; b+1; z).
64    pub fn hyp1f1_derivative_z(a: f64, b: f64, z: f64) -> f64 {
65        let ratio = a / b;
66        let shifted = hyp1f1(a + 1.0, b + 1.0, z).unwrap_or(f64::NAN);
67        ratio * shifted
68    }
69
70    /// d/da ₁F₁(a; b; z) approximated via central finite differences.
71    ///
72    /// There is no general closed form for the derivative with respect to `a`;
73    /// we use a five-point stencil with step `h = 1e-5`.
74    pub fn hyp1f1_derivative_a(a: f64, b: f64, z: f64) -> f64 {
75        let h = 1e-5_f64;
76        let f = |av: f64| hyp1f1(av, b, z).unwrap_or(f64::NAN);
77        (-f(a + 2.0 * h) + 8.0 * f(a + h) - 8.0 * f(a - h) + f(a - 2.0 * h)) / (12.0 * h)
78    }
79
80    // ─── Bessel functions ─────────────────────────────────────────────────────
81
82    /// d/dx J_n(x) = [J_{n-1}(x) − J_{n+1}(x)] / 2.
83    pub fn bessel_j_derivative(n: i32, x: f64) -> f64 {
84        (bessel::jn(n - 1, x) - bessel::jn(n + 1, x)) / 2.0
85    }
86
87    /// d/dx I_n(x) = [I_{n-1}(x) + I_{n+1}(x)] / 2.
88    pub fn bessel_i_derivative(n: i32, x: f64) -> f64 {
89        let inm1 = bessel::iv(f64::from(n - 1), x);
90        let inp1 = bessel::iv(f64::from(n + 1), x);
91        (inm1 + inp1) / 2.0
92    }
93
94    // ─── Complete elliptic integral K ─────────────────────────────────────────
95
96    /// dK/dk = [E(k) − (1 − k²) K(k)] / [k (1 − k²)]
97    ///
98    /// Here `k` is the elliptic modulus (not the parameter m = k²).
99    /// Note: `ellipk` and `ellipe` in this crate take the parameter `m = k²`.
100    pub fn elliptic_k_derivative(k: f64) -> f64 {
101        let m = k * k;
102        let k_val = ellipk(m);
103        let e_val = ellipe(m);
104        (e_val - (1.0 - m) * k_val) / (k * (1.0 - m))
105    }
106
107    // ─── Complex-step differentiation ────────────────────────────────────────
108
109    /// Approximate f'(x) using the complex-step method.
110    ///
111    /// This is equivalent to the imaginary part of `f(x + ih) / h` for small `h`,
112    /// but since `f` is real-valued we implement it via a high-order central
113    /// finite-difference stencil which achieves O(h⁴) accuracy:
114    ///
115    /// ```text
116    /// f'(x) ≈ [−f(x+2h) + 8f(x+h) − 8f(x−h) + f(x−2h)] / (12h)
117    /// ```
118    ///
119    /// with `h = 1e-6`.
120    pub fn complex_step_deriv<F: Fn(f64) -> f64>(f: F, x: f64) -> f64 {
121        let h = 1e-6_f64;
122        (-f(x + 2.0 * h) + 8.0 * f(x + h) - 8.0 * f(x - h) + f(x - 2.0 * h)) / (12.0 * h)
123    }
124
125    // ─── Derivative rule descriptors ─────────────────────────────────────────
126
127    /// Return a list of symbolic derivative rules for common special functions.
128    pub fn rules() -> Vec<DerivativeRule> {
129        vec![
130            DerivativeRule {
131                function: "Gamma".to_string(),
132                wrt: "x".to_string(),
133                formula: "Gamma(x) * digamma(x)".to_string(),
134            },
135            DerivativeRule {
136                function: "LogGamma".to_string(),
137                wrt: "x".to_string(),
138                formula: "digamma(x)".to_string(),
139            },
140            DerivativeRule {
141                function: "erf".to_string(),
142                wrt: "x".to_string(),
143                formula: "(2/sqrt(pi)) * exp(-x^2)".to_string(),
144            },
145            DerivativeRule {
146                function: "erfc".to_string(),
147                wrt: "x".to_string(),
148                formula: "-(2/sqrt(pi)) * exp(-x^2)".to_string(),
149            },
150            DerivativeRule {
151                function: "J_n".to_string(),
152                wrt: "x".to_string(),
153                formula: "(J_{n-1}(x) - J_{n+1}(x)) / 2".to_string(),
154            },
155            DerivativeRule {
156                function: "I_n".to_string(),
157                wrt: "x".to_string(),
158                formula: "(I_{n-1}(x) + I_{n+1}(x)) / 2".to_string(),
159            },
160            DerivativeRule {
161                function: "1F1(a;b;z)".to_string(),
162                wrt: "z".to_string(),
163                formula: "(a/b) * 1F1(a+1;b+1;z)".to_string(),
164            },
165            DerivativeRule {
166                function: "K(k)".to_string(),
167                wrt: "k".to_string(),
168                formula: "[E(k) - (1-k^2) K(k)] / [k(1-k^2)]".to_string(),
169            },
170        ]
171    }
172}