1use crate::error::{IntegrateError, IntegrateResult};
8use crate::IntegrateFloat;
9use scirs2_core::ndarray::Array1;
10use std::f64::consts::PI;
11use std::fmt::Debug;
12
13#[derive(Debug, Clone)]
15pub struct CubatureOptions<F: IntegrateFloat> {
16 pub abs_tol: F,
18 pub rel_tol: F,
20 pub max_evals: usize,
22 pub max_recursion_depth: usize,
24 pub vectorized: bool,
26 pub log: bool,
28}
29
30impl<F: IntegrateFloat> Default for CubatureOptions<F> {
31 fn default() -> Self {
32 Self {
33 abs_tol: F::from_f64(1.49e-8).unwrap(),
34 rel_tol: F::from_f64(1.49e-8).unwrap(),
35 max_evals: 50_000,
36 max_recursion_depth: 15,
37 vectorized: false,
38 log: false,
39 }
40 }
41}
42
43#[derive(Debug, Clone)]
45pub struct CubatureResult<F: IntegrateFloat> {
46 pub value: F,
48 pub abs_error: F,
50 pub n_evals: usize,
52 pub converged: bool,
54}
55
56#[derive(Debug, Clone, Copy)]
58pub enum Bound<F: IntegrateFloat> {
59 Finite(F),
61 NegInf,
63 PosInf,
65}
66
67impl<F: IntegrateFloat> Bound<F> {
68 fn is_infinite(&self) -> bool {
70 matches!(self, Bound::NegInf | Bound::PosInf)
71 }
72}
73
74#[allow(dead_code)]
76fn transform_for_infinite_bounds<F: IntegrateFloat>(x: F, a: &Bound<F>, b: &Bound<F>) -> (F, F) {
77 match (a, b) {
78 (Bound::Finite(_), Bound::Finite(_)) => (x, F::one()),
80
81 (Bound::Finite(a_val), Bound::PosInf) => {
83 let half_pi = F::from_f64(std::f64::consts::FRAC_PI_2).unwrap();
87 let scaled_x = half_pi * x; let tan_x = scaled_x.tan();
89 let mapped_val = *a_val + tan_x;
90
91 let sec_squared = F::one() + tan_x * tan_x;
93 let weight = sec_squared * half_pi;
94
95 (mapped_val, weight)
96 }
97
98 (Bound::NegInf, Bound::Finite(b_val)) => {
100 let half_pi = F::from_f64(std::f64::consts::FRAC_PI_2).unwrap();
103 let scaled_x = half_pi * x; let tan_x = scaled_x.tan();
105 let mapped_val = *b_val - tan_x;
106
107 let sec_squared = F::one() + tan_x * tan_x;
109 let weight = sec_squared * half_pi;
110
111 (mapped_val, weight)
112 }
113
114 (Bound::NegInf, Bound::PosInf) => {
116 let pi = F::from_f64(std::f64::consts::PI).unwrap();
119 let half = F::from_f64(0.5).unwrap();
120 let scaled_x = (x - half) * pi;
121 let mapped_val = scaled_x.tan();
122
123 let sec_squared = F::one() + mapped_val * mapped_val;
125 let weight = pi * sec_squared;
126
127 (mapped_val, weight)
128 }
129
130 (Bound::Finite(_), Bound::NegInf) | (Bound::NegInf, Bound::NegInf) | (Bound::PosInf, _) => {
132 (F::zero(), F::zero())
135 }
136 }
137}
138
139#[allow(dead_code)]
171pub fn cubature<F, Func>(
172 f: Func,
173 bounds: &[(Bound<F>, Bound<F>)],
174 options: Option<CubatureOptions<F>>,
175) -> IntegrateResult<CubatureResult<F>>
176where
177 F: IntegrateFloat,
178 Func: Fn(&Array1<F>) -> F,
179{
180 let opts = options.unwrap_or_default();
181 let ndim = bounds.len();
182
183 if ndim == 0 {
184 return Err(IntegrateError::ValueError(
185 "At least one dimension is required for integration".to_string(),
186 ));
187 }
188
189 for (lower, upper) in bounds {
191 match (lower, upper) {
192 (Bound::PosInf, _) => {
193 return Err(IntegrateError::ValueError(
194 "Lower bound cannot be positive infinity".to_string(),
195 ));
196 }
197 (Bound::Finite(a), Bound::Finite(b)) if *a >= *b => {
198 return Err(IntegrateError::ValueError(
199 "Upper bound must be greater than lower bound".to_string(),
200 ));
201 }
202 (_, Bound::NegInf) => {
203 return Err(IntegrateError::ValueError(
204 "Upper bound cannot be negative infinity".to_string(),
205 ));
206 }
207 _ => {}
208 }
209 }
210
211 let mut point = Array1::zeros(ndim);
213 let mut n_evals = 0;
215
216 let mut mapped_bounds = Vec::with_capacity(ndim);
218 for (lower, upper) in bounds {
219 let mapped_lower = match lower {
222 Bound::Finite(v) => *v,
223 Bound::NegInf => F::zero(),
224 _ => unreachable!(), };
226
227 let mapped_upper = match upper {
228 Bound::Finite(v) => *v,
229 Bound::PosInf => F::one(),
230 _ => unreachable!(), };
232
233 mapped_bounds.push((mapped_lower, mapped_upper));
234 }
235
236 let result = adaptive_cubature_impl(
238 &f,
239 &mapped_bounds,
240 &mut point,
241 0, bounds,
243 &mut n_evals,
244 &opts,
245 )?;
246
247 Ok(CubatureResult {
248 value: result.0,
249 abs_error: result.1,
250 n_evals,
251 converged: result.2,
252 })
253}
254
255#[allow(clippy::too_many_arguments)]
257#[allow(dead_code)]
258fn adaptive_cubature_impl<F, Func>(
259 f: &Func,
260 mapped_bounds: &[(F, F)],
261 point: &mut Array1<F>,
262 dim: usize,
263 original_bounds: &[(Bound<F>, Bound<F>)],
264 n_evals: &mut usize,
265 options: &CubatureOptions<F>,
266) -> IntegrateResult<(F, F, bool)>
267where
269 F: IntegrateFloat,
270 Func: Fn(&Array1<F>) -> F,
271{
272 let ndim = mapped_bounds.len();
273
274 if dim == ndim {
276 let val = f(point);
277 *n_evals += 1;
278
279 let result = if options.log { val.exp() } else { val };
281
282 let error = result.abs() * F::epsilon() * F::from_f64(1.0).unwrap();
285
286 return Ok((result, error, true));
287 }
288
289 let (a_bound, b_bound) = &original_bounds[dim];
291 let has_infinite_bound = a_bound.is_infinite() || b_bound.is_infinite();
292
293 if has_infinite_bound {
295 integrate_with_infinite_bounds(
297 f,
298 mapped_bounds,
299 point,
300 dim,
301 original_bounds,
302 n_evals,
303 options,
304 )
305 } else {
306 integrate_with_finite_bounds(
308 f,
309 mapped_bounds,
310 point,
311 dim,
312 original_bounds,
313 n_evals,
314 options,
315 )
316 }
317}
318
319#[allow(dead_code)]
321fn integrate_with_finite_bounds<F, Func>(
322 f: &Func,
323 mapped_bounds: &[(F, F)],
324 point: &mut Array1<F>,
325 dim: usize,
326 original_bounds: &[(Bound<F>, Bound<F>)],
327 n_evals: &mut usize,
328 options: &CubatureOptions<F>,
329) -> IntegrateResult<(F, F, bool)>
330where
331 F: IntegrateFloat,
332 Func: Fn(&Array1<F>) -> F,
333{
334 let (a, b) = mapped_bounds[dim];
336
337 let points = [
339 F::from_f64(-0.9491079123427585).unwrap(),
340 F::from_f64(-0.7415311855993944).unwrap(),
341 F::from_f64(-0.4058451513773972).unwrap(),
342 F::zero(),
343 F::from_f64(0.4058451513773972).unwrap(),
344 F::from_f64(0.7415311855993944).unwrap(),
345 F::from_f64(0.9491079123427585).unwrap(),
346 ];
347
348 let weights = [
349 F::from_f64(0.1294849661688697).unwrap(),
350 F::from_f64(0.2797053914892766).unwrap(),
351 F::from_f64(0.3818300505051189).unwrap(),
352 F::from_f64(0.4179591836734694).unwrap(),
353 F::from_f64(0.3818300505051189).unwrap(),
354 F::from_f64(0.2797053914892766).unwrap(),
355 F::from_f64(0.1294849661688697).unwrap(),
356 ];
357
358 let mid = (a + b) / F::from_f64(2.0).unwrap();
360 let scale = (b - a) / F::from_f64(2.0).unwrap();
361
362 let mut result = F::zero();
363 let mut error_est = F::zero();
364 let mut all_converged = true;
365
366 let mut gauss_rule_result = F::zero();
368
369 for i in 0..7 {
371 let x = mid + scale * points[i];
373
374 point[dim] = x;
376
377 let sub_result = adaptive_cubature_impl(
379 f,
380 mapped_bounds,
381 point,
382 dim + 1,
383 original_bounds,
384 n_evals,
385 options,
386 )?;
387
388 let val = sub_result.0 * weights[i];
390 result += val;
391
392 if i % 2 == 0 {
394 gauss_rule_result += val;
395 }
396
397 error_est += sub_result.1 * weights[i];
399
400 all_converged = all_converged && sub_result.2;
402 }
403
404 result *= scale;
406
407 error_est *= scale;
410
411 let tol = options.abs_tol + options.rel_tol * result.abs();
413 let converged = error_est <= tol && all_converged;
414
415 Ok((result, error_est, converged))
416}
417
418#[allow(dead_code)]
420fn integrate_with_infinite_bounds<F, Func>(
421 f: &Func,
422 mapped_bounds: &[(F, F)],
423 point: &mut Array1<F>,
424 dim: usize,
425 original_bounds: &[(Bound<F>, Bound<F>)],
426 n_evals: &mut usize,
427 options: &CubatureOptions<F>,
428) -> IntegrateResult<(F, F, bool)>
429where
430 F: IntegrateFloat,
431 Func: Fn(&Array1<F>) -> F,
432{
433 let (a_bound, b_bound) = &original_bounds[dim];
435
436 let nodes = [
438 F::from_f64(-0.9931285991850949).unwrap(),
439 F::from_f64(-0.9639719272779138).unwrap(),
440 F::from_f64(-0.912_234_428_251_326).unwrap(),
441 F::from_f64(-0.8391169718222188).unwrap(),
442 F::from_f64(-0.7463319064601508).unwrap(),
443 F::from_f64(-0.636_053_680_726_515).unwrap(),
444 F::from_f64(-0.5108670019508271).unwrap(),
445 F::from_f64(-0.3737060887154195).unwrap(),
446 F::from_f64(-0.2277858511416451).unwrap(),
447 F::from_f64(-0.0765265211334973).unwrap(),
448 F::from_f64(0.0765265211334973).unwrap(),
449 F::from_f64(0.2277858511416451).unwrap(),
450 F::from_f64(0.3737060887154195).unwrap(),
451 F::from_f64(0.5108670019508271).unwrap(),
452 F::from_f64(0.636_053_680_726_515).unwrap(),
453 F::from_f64(0.7463319064601508).unwrap(),
454 F::from_f64(0.8391169718222188).unwrap(),
455 F::from_f64(0.912_234_428_251_326).unwrap(),
456 F::from_f64(0.9639719272779138).unwrap(),
457 F::from_f64(0.9931285991850949).unwrap(),
458 ];
459
460 let weights = [
461 F::from_f64(0.0176140071391521).unwrap(),
462 F::from_f64(0.0406014298003869).unwrap(),
463 F::from_f64(0.0626720483341091).unwrap(),
464 F::from_f64(0.0832767415767048).unwrap(),
465 F::from_f64(0.1019301198172404).unwrap(),
466 F::from_f64(0.1181945319615184).unwrap(),
467 F::from_f64(0.1316886384491766).unwrap(),
468 F::from_f64(0.142_096_109_318_382).unwrap(),
469 F::from_f64(0.1491729864726037).unwrap(),
470 F::from_f64(0.1527533871307258).unwrap(),
471 F::from_f64(0.1527533871307258).unwrap(),
472 F::from_f64(0.1491729864726037).unwrap(),
473 F::from_f64(0.142_096_109_318_382).unwrap(),
474 F::from_f64(0.1316886384491766).unwrap(),
475 F::from_f64(0.1181945319615184).unwrap(),
476 F::from_f64(0.1019301198172404).unwrap(),
477 F::from_f64(0.0832767415767048).unwrap(),
478 F::from_f64(0.0626720483341091).unwrap(),
479 F::from_f64(0.0406014298003869).unwrap(),
480 F::from_f64(0.0176140071391521).unwrap(),
481 ];
482
483 let mut result = F::zero();
484 let mut error_est = F::zero();
485 let mut all_converged = true;
486
487 let scale_factor = match (a_bound, b_bound) {
490 (Bound::Finite(_), Bound::PosInf) | (Bound::NegInf, Bound::Finite(_)) => {
491 F::from_f64(0.4999).unwrap()
492 }
493 (Bound::NegInf, Bound::PosInf) => F::from_f64(0.499).unwrap(),
494 _ => unreachable!(),
495 };
496
497 let offset = F::from_f64(0.5).unwrap();
498
499 for i in 0..20 {
500 let x = offset + nodes[i] * scale_factor;
502
503 let (mapped_x, jacobian) = transform_for_infinite_bounds(x, a_bound, b_bound);
505
506 point[dim] = mapped_x;
508
509 let sub_result = adaptive_cubature_impl(
511 f,
512 mapped_bounds,
513 point,
514 dim + 1,
515 original_bounds,
516 n_evals,
517 options,
518 )?;
519
520 let contribution = sub_result.0 * weights[i] * jacobian * scale_factor;
522 result += contribution;
523
524 error_est += sub_result.1 * weights[i] * jacobian.abs() * scale_factor;
526
527 all_converged = all_converged && sub_result.2;
529 }
530
531 let tol = options.abs_tol + options.rel_tol * result.abs();
533 let converged = error_est < tol && all_converged;
534
535 Ok((result, error_est, converged))
536}
537
538#[allow(dead_code)]
567pub fn nquad<F, Func>(
568 func: Func,
569 ranges: &[(F, F)],
570 options: Option<CubatureOptions<F>>,
571) -> IntegrateResult<CubatureResult<F>>
572where
573 F: IntegrateFloat,
574 Func: Fn(&[F]) -> F,
575{
576 let bounds: Vec<(Bound<F>, Bound<F>)> = ranges
578 .iter()
579 .map(|(a, b)| (Bound::Finite(*a), Bound::Finite(*b)))
580 .collect();
581
582 let f_adapter = |x: &Array1<F>| {
584 let slice = x.as_slice().unwrap();
585 func(slice)
586 };
587
588 cubature(f_adapter, &bounds, options)
589}
590
591#[cfg(test)]
592mod tests {
593 use super::*;
594
595 #[test]
596 fn test_simple_2d_integral() {
597 let f = |x: &Array1<f64>| x[0] * x[1];
599
600 let bounds = vec![
601 (Bound::Finite(0.0), Bound::Finite(1.0)),
602 (Bound::Finite(0.0), Bound::Finite(1.0)),
603 ];
604
605 let options = CubatureOptions {
606 abs_tol: 1e-6,
607 rel_tol: 1e-6,
608 max_evals: 10000,
609 ..Default::default()
610 };
611
612 let result = cubature(f, &bounds, Some(options)).unwrap();
613 println!("Cubature result:");
614 println!(" Value: {}", result.value);
615 println!(" Expected: 0.25");
616 println!(" Error: {}", result.abs_error);
617 println!(" Converged: {}", result.converged);
618 println!(" Evaluations: {}", result.n_evals);
619 assert!((result.value - 0.25).abs() < 1e-6);
620 assert!(result.converged);
621 }
622
623 #[test]
624 fn test_3d_integral() {
625 let f = |x: &Array1<f64>| x[0] * x[1] * x[2];
627
628 let bounds = vec![
629 (Bound::Finite(0.0), Bound::Finite(1.0)),
630 (Bound::Finite(0.0), Bound::Finite(1.0)),
631 (Bound::Finite(0.0), Bound::Finite(1.0)),
632 ];
633
634 let result = cubature(f, &bounds, None).unwrap();
635 assert!((result.value - 0.125).abs() < 1e-10);
636 assert!(result.converged);
637 }
638
639 #[test]
640 fn test_nquad_simple() {
641 let f = |args: &[f64]| args[0] * args[1];
643 let ranges = vec![(0.0, 1.0), (0.0, 1.0)];
644
645 let result = nquad(f, &ranges, None).unwrap();
646 assert!((result.value - 0.25).abs() < 1e-10);
647 assert!(result.converged);
648 }
649
650 #[test]
651 fn test_infinite_bounds() {
652 let f = |x: &Array1<f64>| (-x[0] * x[0]).exp();
654
655 let bounds = vec![(Bound::NegInf, Bound::PosInf)];
656
657 let options = CubatureOptions {
658 abs_tol: 1e-4,
659 rel_tol: 1e-4,
660 max_evals: 50000,
661 ..Default::default()
662 };
663
664 let result = cubature(f, &bounds, Some(options)).unwrap();
665 assert!((result.value - PI.sqrt()).abs() < 1e-3); assert!(result.converged);
667 }
668
669 #[test]
670 fn test_semi_infinite_bounds() {
671 let f = |x: &Array1<f64>| (-x[0]).exp();
673
674 let bounds = vec![(Bound::Finite(0.0), Bound::PosInf)];
675
676 let options = CubatureOptions {
677 abs_tol: 1e-4,
678 rel_tol: 1e-4,
679 max_evals: 50000,
680 ..Default::default()
681 };
682
683 let result = cubature(f, &bounds, Some(options)).unwrap();
684 assert!((result.value - 1.0).abs() < 1e-3); assert!(result.converged);
686 }
687
688 #[test]
689 fn test_gaussian_2d() {
690 let f = |x: &Array1<f64>| (-x[0] * x[0] - x[1] * x[1]).exp();
692
693 let bounds = vec![
694 (Bound::NegInf, Bound::PosInf),
695 (Bound::NegInf, Bound::PosInf),
696 ];
697
698 let options = CubatureOptions {
699 abs_tol: 1e-3,
700 rel_tol: 1e-3,
701 max_evals: 100000,
702 ..Default::default()
703 };
704
705 let result = cubature(f, &bounds, Some(options)).unwrap();
706 assert!((result.value - PI).abs() < 1e-2); }
708}