1#![allow(clippy::needless_range_loop)]
2#![allow(dead_code)]
13#![allow(clippy::too_many_arguments)]
14
15use std::f64::consts::PI;
16
17pub fn trapezoid_rule(f: impl Fn(f64) -> f64, a: f64, b: f64, n: usize) -> f64 {
25 assert!(n >= 1, "n must be at least 1");
26 let h = (b - a) / n as f64;
27 let mut sum = f(a) + f(b);
28 for i in 1..n {
29 sum += 2.0 * f(a + i as f64 * h);
30 }
31 sum * h / 2.0
32}
33
34pub fn simpson_rule(f: impl Fn(f64) -> f64, a: f64, b: f64, n: usize) -> f64 {
42 let n = if !n.is_multiple_of(2) { n + 1 } else { n };
43 assert!(n >= 2);
44 let h = (b - a) / n as f64;
45 let mut sum = f(a) + f(b);
46 for i in 1..n {
47 let x = a + i as f64 * h;
48 sum += if i % 2 == 0 { 2.0 } else { 4.0 } * f(x);
49 }
50 sum * h / 3.0
51}
52
53pub fn romberg_integration(
62 f: impl Fn(f64) -> f64,
63 a: f64,
64 b: f64,
65 max_levels: usize,
66 tol: f64,
67) -> f64 {
68 let max_levels = max_levels.max(1);
69 let mut r = vec![vec![0.0_f64; max_levels]; max_levels];
70 r[0][0] = trapezoid_rule(&f, a, b, 1);
71 for i in 1..max_levels {
72 let n_steps = 1usize << i; r[i][0] = trapezoid_rule(&f, a, b, n_steps);
74 for j in 1..=i {
75 let factor = (4u64.pow(j as u32)) as f64;
76 r[i][j] = (factor * r[i][j - 1] - r[i - 1][j - 1]) / (factor - 1.0);
77 }
78 if i >= 1 && (r[i][i] - r[i - 1][i - 1]).abs() < tol {
79 return r[i][i];
80 }
81 }
82 r[max_levels - 1][max_levels - 1]
83}
84
85pub fn gauss_legendre_5pt(f: impl Fn(f64) -> f64, a: f64, b: f64) -> f64 {
93 let nodes = [
95 -0.906_179_845_938_664,
96 -0.538_469_310_105_683,
97 0.0,
98 0.538_469_310_105_683,
99 0.906_179_845_938_664,
100 ];
101 let weights = [
102 0.236_926_885_056_189,
103 0.478_628_670_499_366,
104 0.568_888_888_888_889,
105 0.478_628_670_499_366,
106 0.236_926_885_056_189,
107 ];
108 let mid = (a + b) / 2.0;
109 let half = (b - a) / 2.0;
110 let mut sum = 0.0;
111 for (xi, wi) in nodes.iter().zip(weights.iter()) {
112 sum += wi * f(mid + half * xi);
113 }
114 sum * half
115}
116
117pub fn gauss_legendre_nodes_weights(n: usize) -> (Vec<f64>, Vec<f64>) {
126 match n {
127 2 => (
128 vec![-0.577_350_269_189_626, 0.577_350_269_189_626],
129 vec![1.0, 1.0],
130 ),
131 3 => (
132 vec![-0.774_596_669_241_483, 0.0, 0.774_596_669_241_483],
133 vec![
134 0.555_555_555_555_556,
135 0.888_888_888_888_889,
136 0.555_555_555_555_556,
137 ],
138 ),
139 4 => (
140 vec![
141 -0.861_136_311_594_953,
142 -0.339_981_043_584_856,
143 0.339_981_043_584_856,
144 0.861_136_311_594_953,
145 ],
146 vec![
147 0.347_854_845_137_454,
148 0.652_145_154_862_546,
149 0.652_145_154_862_546,
150 0.347_854_845_137_454,
151 ],
152 ),
153 5 => {
154 let (nodes, weights) = (
155 vec![
156 -0.906_179_845_938_664,
157 -0.538_469_310_105_683,
158 0.0,
159 0.538_469_310_105_683,
160 0.906_179_845_938_664,
161 ],
162 vec![
163 0.236_926_885_056_189,
164 0.478_628_670_499_366,
165 0.568_888_888_888_889,
166 0.478_628_670_499_366,
167 0.236_926_885_056_189,
168 ],
169 );
170 (nodes, weights)
171 }
172 _ => panic!("gauss_legendre_nodes_weights: n must be 2, 3, 4, or 5"),
173 }
174}
175
176pub fn adaptive_simpson(f: impl Fn(f64) -> f64, a: f64, b: f64, tol: f64, max_depth: usize) -> f64 {
185 fn simpson_one(f: &impl Fn(f64) -> f64, a: f64, b: f64) -> f64 {
186 let c = (a + b) / 2.0;
187 (b - a) / 6.0 * (f(a) + 4.0 * f(c) + f(b))
188 }
189
190 fn recurse(f: &impl Fn(f64) -> f64, a: f64, b: f64, tol: f64, whole: f64, depth: usize) -> f64 {
191 let c = (a + b) / 2.0;
192 let left = simpson_one(f, a, c);
193 let right = simpson_one(f, c, b);
194 let delta = left + right - whole;
195 if depth == 0 || delta.abs() < 15.0 * tol {
196 left + right + delta / 15.0
197 } else {
198 let half_tol = tol / 2.0;
199 recurse(f, a, c, half_tol, left, depth - 1)
200 + recurse(f, c, b, half_tol, right, depth - 1)
201 }
202 }
203
204 let whole = simpson_one(&f, a, b);
205 recurse(&f, a, b, tol, whole, max_depth)
206}
207
208pub fn monte_carlo_integrate(f: impl Fn(f64) -> f64, a: f64, b: f64, n_samples: usize) -> f64 {
216 use rand::RngExt;
217 let mut rng = rand::rng();
218 let mut sum = 0.0;
219 for _ in 0..n_samples {
220 let x: f64 = rng.random_range(a..b);
221 sum += f(x);
222 }
223 (b - a) * sum / n_samples as f64
224}
225
226pub fn clenshaw_curtis(f: impl Fn(f64) -> f64, n: usize) -> f64 {
234 let n = n.max(1);
235 let vals: Vec<f64> = (0..=n)
237 .map(|k| f((k as f64 * PI / n as f64).cos()))
238 .collect();
239
240 let mut sum = 0.0;
242 let nf = n as f64;
243 for j in 0..=n {
244 let cj = if j == 0 || j == n { 1.0 } else { 2.0 };
245 let mut w = 0.0_f64;
246 for k in (0..=(n / 2)).map(|m| 2 * m) {
247 let bk = if k == 0 { 1.0 } else { 2.0 };
248 let denom = 1.0 - (k as f64).powi(2);
249 if denom.abs() < 1e-14 {
250 continue;
251 }
252 let cos_term = ((k * j) as f64 * PI / nf).cos();
253 w += bk / denom * cos_term;
254 }
255 w /= nf;
256 sum += cj * w * vals[j];
257 }
258 sum
259}
260
261pub fn double_exponential(f: impl Fn(f64) -> f64, a: f64, b: f64, n: usize) -> f64 {
269 let n = n.max(2);
270 let half_n = n as i64 / 2;
271 let h = 3.0 / (half_n as f64); let mid = (a + b) / 2.0;
273 let half = (b - a) / 2.0;
274 let mut sum = 0.0;
275 for ki in (-half_n)..=half_n {
276 let t = ki as f64 * h;
277 let sinh_t = t.sinh();
278 let cosh_t = t.cosh();
279 let cosh_sinh = (PI / 2.0 * sinh_t).cosh();
280 let x = mid + half * (PI / 2.0 * sinh_t).tanh();
281 let w = half * PI / 2.0 * cosh_t / (cosh_sinh * cosh_sinh);
282 sum += w * f(x);
283 }
284 sum * h
285}
286
287pub fn multidimensional_trap(f: impl Fn(&[f64]) -> f64, bounds: &[(f64, f64)], n: usize) -> f64 {
296 let dim = bounds.len();
297 if dim == 0 {
298 return f(&[]);
299 }
300 let n = n.max(1);
301
302 let steps_per_dim: Vec<f64> = bounds.iter().map(|(a, b)| (b - a) / n as f64).collect();
304 let total_points = (n + 1).pow(dim as u32);
305
306 let mut integral = 0.0;
307 let mut point = vec![0.0_f64; dim];
308
309 for idx in 0..total_points {
310 let mut weight = 1.0_f64;
312 for d in 0..dim {
313 let stride = (n + 1).pow(d as u32);
314 let i_d = (idx / stride) % (n + 1);
315 point[d] = bounds[d].0 + i_d as f64 * steps_per_dim[d];
316 let w = if i_d == 0 || i_d == n { 1.0 } else { 2.0 };
317 weight *= w;
318 }
319 integral += weight * f(&point);
320 }
321
322 let mut factor = integral;
324 for (a, b) in bounds.iter() {
325 factor *= (b - a) / (2.0 * n as f64);
326 }
327 factor
328}
329
330#[derive(Debug, Clone)]
338pub struct QuadratureRule {
339 pub nodes: Vec<f64>,
341 pub weights: Vec<f64>,
343 pub name: String,
345}
346
347impl QuadratureRule {
348 pub fn new(nodes: Vec<f64>, weights: Vec<f64>, name: impl Into<String>) -> Self {
350 assert_eq!(nodes.len(), weights.len(), "nodes and weights must match");
351 Self {
352 nodes,
353 weights,
354 name: name.into(),
355 }
356 }
357
358 pub fn apply(&self, f: impl Fn(f64) -> f64, a: f64, b: f64) -> f64 {
360 let mid = (a + b) / 2.0;
361 let half = (b - a) / 2.0;
362 let mut sum = 0.0;
363 for (xi, wi) in self.nodes.iter().zip(self.weights.iter()) {
364 sum += wi * f(mid + half * xi);
365 }
366 sum * half
367 }
368
369 pub fn n_points(&self) -> usize {
371 self.nodes.len()
372 }
373}
374
375#[cfg(test)]
380mod tests {
381 use super::*;
382
383 const TOL: f64 = 1e-6;
384
385 #[test]
388 fn test_trapezoid_constant() {
389 let result = trapezoid_rule(|_| 3.0, 0.0, 1.0, 100);
391 assert!((result - 3.0).abs() < 1e-12);
392 }
393
394 #[test]
395 fn test_trapezoid_linear() {
396 let result = trapezoid_rule(|x| x, 0.0, 1.0, 1);
398 assert!((result - 0.5).abs() < 1e-12);
399 }
400
401 #[test]
402 fn test_trapezoid_sin_pi() {
403 let result = trapezoid_rule(|x| x.sin(), 0.0, PI, 10_000);
405 assert!((result - 2.0).abs() < 1e-6);
406 }
407
408 #[test]
409 fn test_trapezoid_x_squared() {
410 let result = trapezoid_rule(|x| x * x, 0.0, 1.0, 1_000);
412 assert!((result - 1.0 / 3.0).abs() < 1e-5);
413 }
414
415 #[test]
416 fn test_trapezoid_negative_interval() {
417 let result = trapezoid_rule(|_| 1.0, -2.0, -1.0, 10);
419 assert!((result - 1.0).abs() < 1e-12);
420 }
421
422 #[test]
425 fn test_simpson_constant() {
426 let result = simpson_rule(|_| 5.0, 0.0, 1.0, 2);
427 assert!((result - 5.0).abs() < 1e-12);
428 }
429
430 #[test]
431 fn test_simpson_x_squared_exact() {
432 let result = simpson_rule(|x| x * x, 0.0, 1.0, 2);
434 assert!((result - 1.0 / 3.0).abs() < 1e-12);
435 }
436
437 #[test]
438 fn test_simpson_cubic_exact() {
439 let result = simpson_rule(|x| x * x * x, 0.0, 1.0, 2);
441 assert!((result - 0.25).abs() < 1e-12);
442 }
443
444 #[test]
445 fn test_simpson_sin_pi() {
446 let result = simpson_rule(|x| x.sin(), 0.0, PI, 100);
447 assert!((result - 2.0).abs() < 1e-5);
448 }
449
450 #[test]
451 fn test_simpson_odd_n_corrected() {
452 let result = simpson_rule(|x| x * x, 0.0, 1.0, 3);
454 assert!((result - 1.0 / 3.0).abs() < 1e-6);
455 }
456
457 #[test]
460 fn test_romberg_constant() {
461 let result = romberg_integration(|_| 2.0, 0.0, 1.0, 5, 1e-10);
462 assert!((result - 2.0).abs() < 1e-12);
463 }
464
465 #[test]
466 fn test_romberg_x_squared() {
467 let result = romberg_integration(|x| x * x, 0.0, 1.0, 6, 1e-12);
468 assert!((result - 1.0 / 3.0).abs() < 1e-10);
469 }
470
471 #[test]
472 fn test_romberg_pi_integral() {
473 let result = romberg_integration(|x| 4.0 / (1.0 + x * x), 0.0, 1.0, 8, 1e-10);
475 assert!((result - PI).abs() < 1e-8);
476 }
477
478 #[test]
479 fn test_romberg_sin_pi() {
480 let result = romberg_integration(|x| x.sin(), 0.0, PI, 6, 1e-10);
481 assert!((result - 2.0).abs() < 1e-8);
482 }
483
484 #[test]
487 fn test_gauss_legendre_5pt_constant() {
488 let result = gauss_legendre_5pt(|_| 1.0, 0.0, 1.0);
489 assert!((result - 1.0).abs() < TOL);
490 }
491
492 #[test]
493 fn test_gauss_legendre_5pt_poly_degree9() {
494 let result = gauss_legendre_5pt(|x| x.powi(9), 0.0, 1.0);
496 assert!((result - 0.1).abs() < 1e-12);
497 }
498
499 #[test]
500 fn test_gauss_legendre_5pt_sin() {
501 let result = gauss_legendre_5pt(|x| x.sin(), 0.0, PI);
502 assert!((result - 2.0).abs() < 1e-4);
503 }
504
505 #[test]
508 fn test_gl_nodes_weights_n2_sum_weights() {
509 let (_nodes, weights) = gauss_legendre_nodes_weights(2);
510 let sum: f64 = weights.iter().sum();
511 assert!((sum - 2.0).abs() < 1e-12);
512 }
513
514 #[test]
515 fn test_gl_nodes_weights_n3_sum_weights() {
516 let (_nodes, weights) = gauss_legendre_nodes_weights(3);
517 let sum: f64 = weights.iter().sum();
518 assert!((sum - 2.0).abs() < 1e-12);
519 }
520
521 #[test]
522 fn test_gl_nodes_weights_n4_sum_weights() {
523 let (_nodes, weights) = gauss_legendre_nodes_weights(4);
524 let sum: f64 = weights.iter().sum();
525 assert!((sum - 2.0).abs() < 1e-12);
526 }
527
528 #[test]
529 fn test_gl_nodes_weights_n5_sum_weights() {
530 let (_nodes, weights) = gauss_legendre_nodes_weights(5);
531 let sum: f64 = weights.iter().sum();
532 assert!((sum - 2.0).abs() < 1e-12);
533 }
534
535 #[test]
536 fn test_gl_nodes_exact_for_quadratic() {
537 let (nodes, weights) = gauss_legendre_nodes_weights(3);
539 let result: f64 = nodes
540 .iter()
541 .zip(weights.iter())
542 .map(|(x, w)| w * x.powi(4))
543 .sum();
544 assert!((result - 2.0 / 5.0).abs() < 1e-12);
545 }
546
547 #[test]
550 fn test_adaptive_simpson_sin() {
551 let result = adaptive_simpson(|x| x.sin(), 0.0, PI, 1e-8, 20);
553 assert!((result - 2.0).abs() < 1e-6);
554 }
555
556 #[test]
557 fn test_adaptive_simpson_constant() {
558 let result = adaptive_simpson(|_| 7.0, 0.0, 1.0, 1e-10, 10);
559 assert!((result - 7.0).abs() < 1e-8);
560 }
561
562 #[test]
563 fn test_adaptive_simpson_x_cubed() {
564 let result = adaptive_simpson(|x| x * x * x, 0.0, 2.0, 1e-10, 15);
566 assert!((result - 4.0).abs() < 1e-8);
567 }
568
569 #[test]
572 fn test_monte_carlo_constant() {
573 let result = monte_carlo_integrate(|_| 1.0, 0.0, 1.0, 1_000);
575 assert!((result - 1.0).abs() < 1e-12);
576 }
577
578 #[test]
579 fn test_monte_carlo_approx_pi() {
580 let result = monte_carlo_integrate(|x| 4.0 / (1.0 + x * x), 0.0, 1.0, 100_000);
582 assert!((result - PI).abs() < 0.05, "MC result: {result}");
584 }
585
586 #[test]
587 fn test_monte_carlo_negative_interval() {
588 let result = monte_carlo_integrate(|_| 2.0, -1.0, 0.0, 100);
590 assert!((result - 2.0).abs() < 1e-12);
591 }
592
593 #[test]
596 fn test_clenshaw_curtis_constant() {
597 let result = clenshaw_curtis(|_| 1.0, 8);
599 assert!((result - 2.0).abs() < 1e-6, "CC constant: {result}");
600 }
601
602 #[test]
603 fn test_clenshaw_curtis_linear_zero() {
604 let result = clenshaw_curtis(|x| x, 8);
606 assert!(result.abs() < 1e-10, "CC odd: {result}");
607 }
608
609 #[test]
612 fn test_double_exponential_constant() {
613 let result = double_exponential(|_| 1.0, 0.0, 1.0, 20);
615 assert!((result - 1.0).abs() < 1e-6, "DE constant: {result}");
616 }
617
618 #[test]
619 fn test_double_exponential_sin() {
620 let result = double_exponential(|x| x.sin(), 0.0, PI, 40);
622 assert!((result - 2.0).abs() < 1e-4, "DE sin: {result}");
623 }
624
625 #[test]
628 fn test_quadrature_rule_n_points() {
629 let (nodes, weights) = gauss_legendre_nodes_weights(4);
630 let rule = QuadratureRule::new(nodes, weights, "GL-4");
631 assert_eq!(rule.n_points(), 4);
632 }
633
634 #[test]
635 fn test_quadrature_rule_apply_constant() {
636 let (nodes, weights) = gauss_legendre_nodes_weights(3);
637 let rule = QuadratureRule::new(nodes, weights, "GL-3");
638 let result = rule.apply(|_| 2.0, -1.0, 1.0);
639 assert!((result - 4.0).abs() < 1e-12);
640 }
641
642 #[test]
643 fn test_quadrature_rule_apply_quadratic() {
644 let (nodes, weights) = gauss_legendre_nodes_weights(3);
646 let rule = QuadratureRule::new(nodes, weights, "GL-3");
647 let result = rule.apply(|x| x * x, 0.0, 1.0);
648 assert!((result - 1.0 / 3.0).abs() < 1e-12);
649 }
650
651 #[test]
652 fn test_quadrature_rule_name() {
653 let (nodes, weights) = gauss_legendre_nodes_weights(2);
654 let rule = QuadratureRule::new(nodes, weights, "custom");
655 assert_eq!(rule.name, "custom");
656 }
657}