1#[inline]
13pub fn binomial_coefficient_f64(n: usize, k: usize) -> f64 {
14 if k > n {
15 return 0.0;
16 }
17 if k == 0 || k == n {
18 return 1.0;
19 }
20 let k_eff = k.min(n - k);
21 let mut num: u128 = 1;
31 for j in 0..k_eff {
32 match num.checked_mul((n - j) as u128) {
33 Some(scaled) => num = scaled / (j as u128 + 1),
34 None => {
35 let mut out = num as f64;
39 for jj in j..k_eff {
40 out = out * (n - jj) as f64 / (jj + 1) as f64;
41 }
42 return out;
43 }
44 }
45 }
46 num as f64
47}
48
49#[inline]
50fn horner_polynomial(x: f64, coeffs: &[f64]) -> f64 {
51 coeffs.iter().rev().fold(0.0, |acc, &c| acc * x + c)
52}
53
54#[inline]
60pub fn stable_polynomial_times_exp_neg(x: f64, coeffs: &[f64]) -> f64 {
61 if coeffs.is_empty() || !x.is_finite() {
62 return 0.0;
63 }
64 const DIRECT_EXP_SWITCH: f64 = 600.0;
69 if x <= DIRECT_EXP_SWITCH {
70 return horner_polynomial(x, coeffs) * (-x).exp();
71 }
72
73 let inv_x = x.recip();
74 let mut tail = 0.0;
75 for &c in coeffs {
76 tail = tail * inv_x + c;
77 }
78 let degree = (coeffs.len() - 1) as f64;
79 let scale = (degree * x.ln() - x).exp();
80 scale * tail
81}
82
83#[inline]
89fn bessel_i0_scaled_polynomial_and_centered_log_derivative(ax: f64) -> (f64, f64) {
90 let y = 3.75 / ax;
91 const COEFFICIENTS: [f64; 9] = [
92 0.39894228,
93 0.01328592,
94 0.00225319,
95 -0.00157565,
96 0.00916281,
97 -0.02057706,
98 0.02635537,
99 -0.01647633,
100 0.00392377,
101 ];
102 let mut polynomial = COEFFICIENTS[COEFFICIENTS.len() - 1];
103 let mut derivative = 0.0_f64;
104 for &coefficient in COEFFICIENTS[..COEFFICIENTS.len() - 1].iter().rev() {
105 derivative = derivative * y + polynomial;
106 polynomial = polynomial * y + coefficient;
107 }
108 let scaled_centered_log_derivative = -0.5 - y * derivative / polynomial;
114 (polynomial, scaled_centered_log_derivative)
115}
116
117#[inline]
122fn bessel_i1_scaled_polynomial(ax: f64) -> f64 {
123 let y = 3.75 / ax;
124 0.39894228
125 + y * (-0.03988024
126 + y * (-0.00362018
127 + y * (0.00163801
128 + y * (-0.01031555
129 + y * (0.02282967
130 + y * (-0.02895312 + y * (0.01787654 - y * 0.00420059)))))))
131}
132
133#[inline]
134fn bessel_i0_small(ax: f64) -> f64 {
135 let t = ax / 3.75;
136 let t2 = t * t;
137 1.0 + t2
138 * (3.5156229
139 + t2 * (3.0899424
140 + t2 * (1.2067492 + t2 * (0.2659732 + t2 * (0.0360768 + t2 * 0.0045813)))))
141}
142
143#[inline]
144fn bessel_i1_small(ax: f64) -> f64 {
145 let t = ax / 3.75;
146 let t2 = t * t;
147 ax * (0.5
148 + t2 * (0.87890594
149 + t2 * (0.51498869
150 + t2 * (0.15084934 + t2 * (0.02658733 + t2 * (0.00301532 + t2 * 0.00032411))))))
151}
152
153pub fn bessel_i0_centered_terms(eta: f64) -> (f64, f64, f64) {
161 let ax = eta.abs();
162 if ax < 3.75 {
163 let i0 = bessel_i0_small(ax);
164 let i1 = bessel_i1_small(ax);
165 let ratio = i1 / i0;
166 (i0.ln() - ax, ratio, ax * (ratio - 1.0))
167 } else {
168 let (polynomial_0, scaled_centered_log_derivative) =
169 bessel_i0_scaled_polynomial_and_centered_log_derivative(ax);
170 let polynomial_1 = bessel_i1_scaled_polynomial(ax);
171 (
172 -0.5 * ax.ln() + polynomial_0.ln(),
173 polynomial_1 / polynomial_0,
174 scaled_centered_log_derivative,
175 )
176 }
177}
178
179pub fn bessel_i0_centered_terms_from_log_abs(log_abs_eta: f64) -> (f64, f64, f64) {
187 if log_abs_eta.is_nan() {
188 return (f64::NAN, f64::NAN, f64::NAN);
189 }
190 if log_abs_eta == f64::NEG_INFINITY {
191 return (0.0, 0.0, 0.0);
192 }
193 if log_abs_eta <= f64::MAX.ln() {
194 return bessel_i0_centered_terms(log_abs_eta.exp());
195 }
196 (-0.5 * (std::f64::consts::TAU.ln() + log_abs_eta), 1.0, -0.5)
197}
198
199pub fn bessel_i0_centered_second_log_derivative_from_log_abs(log_abs_eta: f64) -> f64 {
222 if log_abs_eta.is_nan() {
223 return f64::NAN;
224 }
225 if log_abs_eta == f64::NEG_INFINITY {
226 return 0.0;
227 }
228 if log_abs_eta > f64::MAX.ln() {
229 return 0.0;
230 }
231 let eta = log_abs_eta.exp();
232 if eta > 30.0 {
239 let inv = 1.0 / eta;
240 return inv * (0.125 + inv * (0.25 + inv * (75.0 / 128.0)));
241 }
242 let (_centered, _ratio, d1) = bessel_i0_centered_terms(eta);
243 -eta * (2.0 * d1 + 1.0) - d1 * d1
244}
245
246pub fn bessel_i0_log_minus_abs_and_ratio(eta: f64) -> (f64, f64) {
254 let (centered_log_i0, ratio, _) = bessel_i0_centered_terms(eta);
255 (centered_log_i0, ratio)
256}
257
258pub fn bessel_i0_log_and_ratio(eta: f64) -> (f64, f64) {
264 let (centered_log_i0, ratio) = bessel_i0_log_minus_abs_and_ratio(eta);
265 (eta.abs() + centered_log_i0, ratio)
266}
267
268pub fn gauss_legendre(n: usize) -> (Vec<f64>, Vec<f64>) {
279 let mut tmp: Vec<(f64, f64)> = Vec::with_capacity(n);
280 let half = n.div_ceil(2);
281 for i in 0..half {
282 let mut z = (std::f64::consts::PI * (i as f64 + 0.75) / (n as f64 + 0.5)).cos();
283 let mut pp = 0.0_f64;
284 for _ in 0..200 {
285 let mut p1 = 1.0_f64;
286 let mut p2 = 0.0_f64;
287 for j in 0..n {
288 let p3 = p2;
289 p2 = p1;
290 p1 = ((2.0 * j as f64 + 1.0) * z * p2 - j as f64 * p3) / (j as f64 + 1.0);
291 }
292 pp = n as f64 * (z * p1 - p2) / (z * z - 1.0);
293 let z_prev = z;
294 z = z_prev - p1 / pp;
295 if (z - z_prev).abs() < 1e-15 {
296 break;
297 }
298 }
299 let w = 2.0 / ((1.0 - z * z) * pp * pp);
300 if !n.is_multiple_of(2) && i == half - 1 {
302 tmp.push((0.0, w));
303 } else {
304 tmp.push((-z.abs(), w));
305 tmp.push((z.abs(), w));
306 }
307 }
308 tmp.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
309 let mut nodes = Vec::with_capacity(n);
310 let mut weights = Vec::with_capacity(n);
311 for (z, w) in tmp.into_iter().take(n) {
312 nodes.push(z);
313 weights.push(w);
314 }
315 (nodes, weights)
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn centered_bessel_log_is_finite_and_derivative_consistent() {
324 for eta in [0.25_f64, 1.0, 3.74, 3.76, 12.0, 900.0] {
325 let (centered, ratio, scaled_derivative) = bessel_i0_centered_terms(eta);
326 assert!(centered.is_finite());
327 assert!((0.0..=1.0).contains(&ratio));
328
329 let h = 1.0e-4 * eta.max(1.0);
330 let (plus, _) = bessel_i0_log_and_ratio(eta + h);
331 let (minus, _) = bessel_i0_log_and_ratio(eta - h);
332 let derivative = (plus - minus) / (2.0 * h);
333 assert!((derivative - ratio).abs() <= 1.0e-6 + 1.0e-5 * ratio.abs());
334
335 let log_step = 1.0e-5_f64;
336 let (centered_plus, _, _) = bessel_i0_centered_terms(eta * log_step.exp());
337 let (centered_minus, _, _) = bessel_i0_centered_terms(eta * (-log_step).exp());
338 let finite_difference = (centered_plus - centered_minus) / (2.0 * log_step);
339 assert!(
340 (finite_difference - scaled_derivative).abs() < 2.0e-5,
341 "centered Bessel value/gradient mismatch at eta={eta}: analytic={scaled_derivative}, finite_difference={finite_difference}"
342 );
343 }
344 for eta in [1.0e20_f64, 1.0e100, 1.0e300] {
345 let (centered, ratio, scaled_derivative) = bessel_i0_centered_terms(eta);
346 let asymptotic = -0.5 * (std::f64::consts::TAU * eta).ln();
347 assert!(centered.is_finite() && ratio.is_finite());
348 assert!((centered - asymptotic).abs() < 2.0e-8);
349 assert!(
350 (scaled_derivative + 0.5).abs() < 1.0e-12,
351 "large-eta centered derivative must retain its -1/2 limit; eta={eta:e}, derivative={scaled_derivative}"
352 );
353 }
354
355 assert_eq!(bessel_i0_centered_terms(0.0), (0.0, 0.0, 0.0));
356
357 let log_eta = 1_200.0;
358 let (centered, ratio, scaled_derivative) = bessel_i0_centered_terms_from_log_abs(log_eta);
359 assert!(centered.is_finite());
360 assert_eq!(ratio, 1.0);
361 assert_eq!(scaled_derivative, -0.5);
362 assert_eq!(centered, -0.5 * (std::f64::consts::TAU.ln() + log_eta));
363 }
364
365 #[test]
366 fn centered_bessel_second_log_derivative_matches_finite_difference() {
367 let first_log_derivative = |x: f64| bessel_i0_centered_terms(x).2;
374 for eta in [0.02_f64, 0.05, 0.25, 1.0, 2.0, 3.5, 4.0, 8.0] {
375 let log_eta = eta.ln();
376 let analytic = bessel_i0_centered_second_log_derivative_from_log_abs(log_eta);
377
378 let log_step = 1.0e-6_f64;
379 let first_plus = first_log_derivative(eta * log_step.exp());
380 let first_minus = first_log_derivative(eta * (-log_step).exp());
381 let finite_difference = (first_plus - first_minus) / (2.0 * log_step);
382 assert!(
383 (analytic - finite_difference).abs() < 5.0e-5 + 1.0e-3 * analytic.abs(),
384 "centered Bessel second log-derivative mismatch at eta={eta}: \
385 analytic={analytic}, finite_difference={finite_difference}"
386 );
387 }
388 for eta in [50.0_f64, 200.0, 1.0e4] {
393 let c2 = bessel_i0_centered_second_log_derivative_from_log_abs(eta.ln());
394 let leading = 1.0 / (8.0 * eta);
395 assert!(
396 c2 > 0.0 && (c2 - leading).abs() < 0.25 * leading,
397 "large-eta centered second derivative must track 1/(8 eta); \
398 eta={eta}, c2={c2}, leading={leading}"
399 );
400 }
401 assert_eq!(
403 bessel_i0_centered_second_log_derivative_from_log_abs(f64::NEG_INFINITY),
404 0.0
405 );
406 assert_eq!(
407 bessel_i0_centered_second_log_derivative_from_log_abs(1_200.0),
408 0.0
409 );
410 }
411
412 #[test]
413 fn gauss_legendre_integrates_polynomials_exactly() {
414 for n in [1usize, 2, 3, 5, 8, 40, 64] {
416 let (nodes, weights) = gauss_legendre(n);
417 assert_eq!(nodes.len(), n);
418 assert_eq!(weights.len(), n);
419 assert!(nodes.windows(2).all(|w| w[0] < w[1]), "nodes ascending");
420 if !n.is_multiple_of(2) {
421 assert_eq!(nodes[n / 2], 0.0, "odd-n central node is exact zero");
422 }
423 let total: f64 = weights.iter().sum();
424 assert!((total - 2.0).abs() < 1e-13, "∫1 dx = 2, got {total}");
425 if n >= 2 {
426 let x2: f64 = nodes.iter().zip(&weights).map(|(x, w)| w * x * x).sum();
427 assert!((x2 - 2.0 / 3.0).abs() < 1e-13, "∫x² dx = 2/3, got {x2}");
428 }
429 }
430 }
431
432 #[test]
433 fn binom_k_exceeds_n_returns_zero() {
434 assert_eq!(binomial_coefficient_f64(3, 5), 0.0);
435 assert_eq!(binomial_coefficient_f64(0, 1), 0.0);
436 assert_eq!(binomial_coefficient_f64(10, 11), 0.0);
437 }
438
439 #[test]
440 fn binom_k_zero_returns_one() {
441 assert_eq!(binomial_coefficient_f64(0, 0), 1.0);
442 assert_eq!(binomial_coefficient_f64(5, 0), 1.0);
443 assert_eq!(binomial_coefficient_f64(100, 0), 1.0);
444 }
445
446 #[test]
447 fn binom_k_equals_n_returns_one() {
448 assert_eq!(binomial_coefficient_f64(1, 1), 1.0);
449 assert_eq!(binomial_coefficient_f64(5, 5), 1.0);
450 assert_eq!(binomial_coefficient_f64(20, 20), 1.0);
451 }
452
453 #[test]
454 fn binom_small_exact_values() {
455 assert_eq!(binomial_coefficient_f64(5, 2), 10.0);
456 assert_eq!(binomial_coefficient_f64(10, 3), 120.0);
457 assert_eq!(binomial_coefficient_f64(20, 10), 184_756.0);
458 assert_eq!(binomial_coefficient_f64(6, 3), 20.0);
459 }
460
461 #[test]
462 fn binom_symmetry() {
463 assert_eq!(
464 binomial_coefficient_f64(10, 3),
465 binomial_coefficient_f64(10, 7)
466 );
467 assert_eq!(
468 binomial_coefficient_f64(20, 5),
469 binomial_coefficient_f64(20, 15)
470 );
471 assert_eq!(
472 binomial_coefficient_f64(54, 24),
473 binomial_coefficient_f64(54, 30)
474 );
475 }
476
477 #[test]
478 fn binom_c54_24_is_exact() {
479 assert_eq!(binomial_coefficient_f64(54, 24), 1_402_659_561_581_460.0);
482 }
483
484 #[test]
485 fn poly_exp_empty_coeffs_returns_zero() {
486 assert_eq!(stable_polynomial_times_exp_neg(1.0, &[]), 0.0);
487 assert_eq!(stable_polynomial_times_exp_neg(0.0, &[]), 0.0);
488 assert_eq!(stable_polynomial_times_exp_neg(700.0, &[]), 0.0);
489 }
490
491 #[test]
492 fn poly_exp_nonfinite_x_returns_zero() {
493 assert_eq!(
494 stable_polynomial_times_exp_neg(f64::INFINITY, &[1.0, 2.0]),
495 0.0
496 );
497 assert_eq!(
498 stable_polynomial_times_exp_neg(f64::NEG_INFINITY, &[1.0, 2.0]),
499 0.0
500 );
501 assert_eq!(stable_polynomial_times_exp_neg(f64::NAN, &[1.0]), 0.0);
502 }
503
504 #[test]
505 fn poly_exp_constant_at_zero() {
506 assert_eq!(stable_polynomial_times_exp_neg(0.0, &[5.0]), 5.0);
508 assert_eq!(stable_polynomial_times_exp_neg(0.0, &[3.0, 1.0, 2.0]), 3.0);
509 }
510
511 #[test]
512 fn poly_exp_constant_poly_direct_path() {
513 let x = 2.0;
515 let got = stable_polynomial_times_exp_neg(x, &[3.0]);
516 let expected = 3.0 * (-x).exp();
517 assert!(
518 (got - expected).abs() < 1e-14,
519 "got={got} expected={expected}"
520 );
521 }
522
523 #[test]
524 fn poly_exp_linear_poly_direct_path() {
525 let x = 1.5;
527 let (a, b) = (2.0, 3.0);
528 let got = stable_polynomial_times_exp_neg(x, &[a, b]);
529 let expected = (a + b * x) * (-x).exp();
530 assert!(
531 (got - expected).abs() < 1e-14,
532 "got={got} expected={expected}"
533 );
534 }
535
536 #[test]
537 fn poly_exp_constant_poly_asymptotic_path() {
538 let x = 700.0_f64;
540 let got = stable_polynomial_times_exp_neg(x, &[1.0]);
541 let expected = (-x).exp();
542 let rel = (got - expected).abs() / expected;
543 assert!(rel < 1e-12, "got={got} expected={expected} rel={rel}");
544 }
545
546 #[test]
547 fn poly_exp_quadratic_asymptotic_path() {
548 let x = 620.0_f64;
554 let got = stable_polynomial_times_exp_neg(x, &[0.0, 0.0, 1.0]);
555 let expected = (2.0 * x.ln() - x).exp();
556 let rel = (got - expected).abs() / expected.abs();
557 assert!(rel < 1e-12, "got={got} expected={expected} rel={rel}");
558 }
559}