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}