1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
use crate::gammaln;
use std::f64::consts::PI;
/// Compute the modified Bessel function of the first kind $I_\nu(x)$ for non-negative real input.
///
/// The modified Bessel function of the first kind is defined as a solution to the
/// modified Bessel differential equation:
/// <math display="block">
/// <msup><mi>x</mi><mn>2</mn></msup><mfrac><mrow><msup><mi>d</mi><mn>2</mn></msup><mi>y</mi></mrow><mrow><mi>d</mi><msup><mi>x</mi><mn>2</mn></msup></mrow></mfrac>
/// <mo>+</mo><mi>x</mi><mfrac><mrow><mi>dy</mi></mrow><mrow><mi>dx</mi></mrow></mfrac>
/// <mo>-</mo><mo>(</mo><msup><mi>x</mi><mn>2</mn></msup><mo>+</mo><msup><mi>ν</mi><mn>2</mn></msup><mo>)</mo><mi>y</mi><mo>=</mo><mn>0</mn>
/// </math>
///
/// This implementation uses a combination of strategies for numerical stability:
/// - **Power Series**: Used for small arguments.
/// - **Asymptotic Expansion (Large x)**: Used when the argument is much larger than the order.
/// - **Debye Asymptotics (Large $\nu$)**: Used when the order is large.
/// - **Miller's Backward Recurrence**: Used for the intermediate region.
///
/// # Parameters
/// * `nu` - Order of the Bessel function (must be non-negative).
/// * `x` - Real argument (must be non-negative).
/// * `scale` - If `true`, returns the exponentially scaled function $I_\nu(x) \cdot e^{-x}$
/// to prevent numerical overflow for large $x$.
///
/// # Domain
/// * Returns `NaN` if `nu < 0` or `x < 0`.
///
/// # Examples
/// ```
/// use abax::besseli;
///
/// // I_0(1.0) is approximately 1.2660658777
/// let val = besseli(0.0, 1.0, false);
/// assert!((val - 1.2660658777520083).abs() < 1e-15);
/// ```
pub fn besseli(nu: f64, x: f64, scale: bool) -> f64 {
if nu < 0.0 || x < 0.0 || nu.is_nan() || x.is_nan() {
return f64::NAN;
}
match (nu, x) {
(0.0, 0.0) => return 1.0,
( _, 0.0) => return 0.0,
_ => (),
}
// Precision-dependent constants (Double precision)
let eps = f64::EPSILON;
// Region 1: Small argument - Power series with backward accumulation
let small_z_boundary = 4.0 * f64::sqrt(nu + 1.0);
if x <= small_z_boundary {
let log_prefactor = nu * (x / 2.0).ln() - gammaln(nu + 1.0);
let check_val = if scale { log_prefactor - x } else { log_prefactor };
if check_val < f64::ln(f64::MIN_POSITIVE) {
return 0.0;
}
return power_series(nu, x, scale, eps);
}
// Region 2: Large argument asymptotic expansion with truncation tracking
let z_s3: f64 = 16.0;
if x >= z_s3.max(nu.powi(2) / 2.0 + 15.0) {
if !scale {
let log_approx = x - 0.5 * (2.0 * std::f64::consts::PI * x).ln();
if log_approx >= f64::ln(f64::MAX) {
return f64::INFINITY;
}
}
return asymptotic_large_z(nu, x, scale, eps);
}
// Region 3: Large order asymptotic expansion (Debye Asymptotics via Horner's Method)
let c1: f64 = 52.0;
let large_nu_threshold = c1 + x;
if nu >= large_nu_threshold {
return asymptotic_large_nu(nu, x, scale);
}
// Region 4: Miller's Backward Recurrence for Intermediate Region
backward_recurrence(nu, x, scale, eps)
}
fn power_series(nu: f64, x: f64, scale: bool, eps: f64) -> f64 {
let z_squared_over_4 = (x * x) / 4.0;
// 256 terms are more than enough to underflow double precision naturally
let mut terms = [0.0; 256];
terms[0] = 1.0;
let mut k = 0;
while k < 255 {
let k_f = (k + 1) as f64;
let next_term = terms[k] * z_squared_over_4 / (k_f * (k_f + nu));
if next_term.abs() < eps * terms[0].abs() || next_term == 0.0 {
break;
}
k += 1;
terms[k] = next_term;
}
// High precision: Sum from smallest to largest
let mut sum_series = 0.0;
for i in (0..=k).rev() {
sum_series += terms[i];
}
let prefactor = if nu == 0.0 {
if scale { (-x).exp() } else { 1.0 }
} else {
let mut log_prefactor = nu * (x / 2.0).ln() - gammaln(nu + 1.0);
if scale {
log_prefactor -= x;
}
log_prefactor.exp()
};
prefactor * sum_series
}
fn asymptotic_large_z(nu: f64, x: f64, scale: bool, eps: f64) -> f64 {
let mu = 4.0 * nu.powi(2);
let mut terms = [0.0; 512];
terms[0] = 1.0;
let mut k = 0;
let mut last_abs = f64::INFINITY;
while k < 511 {
let k_f = (k + 1) as f64;
let factor = (mu - (2.0 * k_f - 1.0).powi(2)) / (8.0 * k_f * x);
let next_term = -terms[k] * factor;
let abs_next = next_term.abs();
// Asymptotic series rules: break immediately when the terms begin to diverge
if abs_next > last_abs || next_term.is_nan() {
break;
}
k += 1;
terms[k] = next_term;
if abs_next < eps {
break;
}
last_abs = abs_next;
}
// High precision: Sum backward from the minimum convergent term
let mut sum_series = 0.0;
for i in (0..=k).rev() {
sum_series += terms[i];
}
let mut log_prefactor = -0.5 * (2.0 * std::f64::consts::PI * x).ln();
if !scale {
log_prefactor += x;
}
log_prefactor.exp() * sum_series
}
fn asymptotic_large_nu(nu: f64, x: f64, scale: bool) -> f64 {
let z_scaled = x / nu;
let p = 1.0 / f64::sqrt(1.0 + z_scaled.powi(2));
let eta = f64::sqrt(1.0 + z_scaled.powi(2)) + f64::ln(z_scaled / (1.0 + f64::sqrt(1.0 + z_scaled.powi(2))));
//let log_result = (z_scaled / (1.0 + (1.0 + z_scaled.powi(2)).sqrt())).ln();
let p2 = p * p;
let p3 = p2 * p;
let p4 = p3 * p;
let p5 = p4 * p;
let p6 = p5 * p;
// Exact analytical coefficients derived from Olver's Debye Recurrence
let u1 = p * (3.0 - 5.0 * p2) / 24.0;
let u2 = p2 * (81.0 - 462.0 * p2 + 385.0 * p4) / 1152.0;
let u3 = p3 * (30375.0 - 369603.0 * p2 + 765765.0 * p4 - 425425.0 * p6) / 414720.0;
// High precision evaluation: Horner's nesting scheme to protect least significant bits
let inv_nu = 1.0 / nu;
let correction = 1.0 + inv_nu * (u1 + inv_nu * (u2 + inv_nu * u3));
let mut log_result = nu * eta + 0.5 * (p / (2.0 * PI * nu)).ln();
if scale {
log_result -= x;
}
log_result.exp() * correction
}
fn backward_recurrence(nu: f64, x: f64, scale: bool, eps: f64) -> f64 {
let nu_floor = nu.floor() as i32;
let alpha = nu - nu_floor as f64;
let n_start = (nu_floor + 40).max((x + 30.0) as i32);
let mut i_next = 0.0;
let mut i_curr = 1.0e-30;
let mut target_i_nu = 0.0;
let mut target_i_alpha = 0.0;
for n in (1..=n_start).rev() {
let current_order = (n as f64) + alpha;
let i_prev = (2.0 * current_order / x) * i_curr + i_next;
i_next = i_curr;
i_curr = i_prev;
if n - 1 == nu_floor {
target_i_nu = i_curr;
}
if n - 1 == 0 {
target_i_alpha = i_curr;
}
}
// Always reference an unscaled target to prevent algebraic bias in ratios
let true_i_alpha_unscaled = power_series(alpha, x, false, eps);
let normalization_factor = true_i_alpha_unscaled / target_i_alpha;
let unscaled_target = target_i_nu * normalization_factor;
if scale {
unscaled_target * (-x).exp()
} else {
unscaled_target
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn test_besseli_boundaries() {
assert_eq!(besseli(0.0, 0.0, false), 1.0);
assert_eq!(besseli(1.0, 0.0, false), 0.0);
assert_eq!(besseli(2.5, 0.0, false), 0.0);
assert!(besseli(-1.0, 1.0, false).is_nan());
assert!(besseli(1.0, -1.0, false).is_nan());
}
#[test]
fn test_besseli_small_x() {
let tol = 1e-15;
// I_0(1.0) ref: 1.2660658777520083
assert!((besseli(0.0, 1.0, false) - 1.2660658777520083).abs() < tol);
// I_1(1.0) ref: 0.5651591039924850
assert!((besseli(1.0, 1.0, false) - 0.5651591039924850).abs() < tol);
}
#[test]
fn test_besseli_half_integer() {
let tol = 1e-14;
// I_0.5(x) = sqrt(2 / (pi * x)) * sinh(x)
let x = 1.0;
let expected = (2.0 / (PI * x)).sqrt() * x.sinh();
assert!((besseli(0.5, x, false) - expected).abs() < tol);
}
#[test]
fn test_besseli_large_x_asymptotic() {
let tol = 1e-14;
// Check scaling and asymptotic behavior for large x
let x = 50.0;
let val_scaled = besseli(0.0, x, true);
// Reference value for I_0(50) * exp(-50)
let expected = 0.05656162664745419;
assert!((val_scaled - expected).abs() < tol);
}
#[test]
fn test_besseli_large_nu_debye() {
// Large order case
let nu = 50.0;
let x = 10.0;
let val = besseli(nu, x, false);
// Ref value: 1.34685023964431e-25
assert!((val - 4.7568945607268954e-30).abs() < 1e-43);
}
#[test]
fn test_besseli_intermediate_backward() {
// Region likely triggering backward recurrence
let nu = 5.0;
let x = 10.0;
let val = besseli(nu, x, false);
assert!((val - 777.18828640326).abs() < 1e-10);
}
#[test]
fn test_besseli_overflow_protection() {
// Without scaling, I_0(1000) would overflow f64
assert!(besseli(0.0, 1000.0, false).is_infinite());
// With scaling, it should be a small finite value
assert!(besseli(0.0, 1000.0, true) > 0.0);
}
}