scirs2_integrate/
romberg.rs

1//! Romberg integration method
2//!
3//! This module provides an implementation of Romberg integration,
4//! a numerical method that accelerates the trapezoidal rule by using
5//! Richardson extrapolation to eliminate error terms.
6
7use crate::error::{IntegrateError, IntegrateResult};
8use crate::quad::trapezoid;
9use crate::IntegrateFloat;
10use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
11use scirs2_core::random::{Distribution, Uniform};
12use std::f64::consts::PI;
13use std::fmt::Debug;
14
15/// Options for controlling the behavior of Romberg integration
16#[derive(Debug, Clone)]
17pub struct RombergOptions<F: IntegrateFloat> {
18    /// Maximum number of iterations
19    pub max_iters: usize,
20    /// Absolute error tolerance
21    pub abs_tol: F,
22    /// Relative error tolerance
23    pub rel_tol: F,
24    /// Maximum dimension for true Romberg integration (defaults to 3)
25    /// For dimensions higher than this, a more efficient method will be used
26    pub max_true_dimension: usize,
27    /// Minimum number of samples for Monte Carlo fallback (for high dimensions)
28    pub min_monte_carlo_samples: usize,
29}
30
31impl<F: IntegrateFloat> Default for RombergOptions<F> {
32    fn default() -> Self {
33        Self {
34            max_iters: 20,
35            abs_tol: F::from_f64(1.0e-10).unwrap(),
36            rel_tol: F::from_f64(1.0e-10).unwrap(),
37            max_true_dimension: 3,
38            min_monte_carlo_samples: 10000,
39        }
40    }
41}
42
43/// Result of a Romberg integration computation
44#[derive(Debug, Clone)]
45pub struct RombergResult<F: IntegrateFloat> {
46    /// Estimated value of the integral
47    pub value: F,
48    /// Estimated absolute error
49    pub abs_error: F,
50    /// Number of iterations performed
51    pub n_iters: usize,
52    /// Romberg table (for diagnostic purposes)
53    pub table: Array2<F>,
54    /// Flag indicating successful convergence
55    pub converged: bool,
56}
57
58/// Compute the definite integral of a function using Romberg integration
59///
60/// # Arguments
61///
62/// * `f` - The function to integrate
63/// * `a` - Lower bound of integration
64/// * `b` - Upper bound of integration
65/// * `options` - Optional integration parameters
66///
67/// # Returns
68///
69/// * `IntegrateResult<RombergResult<F>>` - The result of the integration or an error
70///
71/// # Examples
72///
73/// ```
74/// use scirs2_integrate::romberg::romberg;
75///
76/// // Integrate f(x) = x² from 0 to 1 (exact result: 1/3)
77/// let result = romberg(|x: f64| x * x, 0.0, 1.0, None).unwrap();
78/// assert!((result.value - 1.0/3.0).abs() < 1e-10);
79/// assert!(result.converged);
80/// ```
81#[allow(dead_code)]
82pub fn romberg<F, Func>(
83    f: Func,
84    a: F,
85    b: F,
86    options: Option<RombergOptions<F>>,
87) -> IntegrateResult<RombergResult<F>>
88where
89    F: IntegrateFloat,
90    Func: Fn(F) -> F + Copy,
91{
92    let opts = options.unwrap_or_default();
93    let max_iters = opts.max_iters;
94
95    // Initialize the Romberg table
96    let mut r_table = Array2::zeros((max_iters, max_iters));
97
98    // Initial computation with h = b-a
99    r_table[[0, 0]] = trapezoid(f, a, b, 1);
100
101    let mut converged = false;
102    let mut n_iters = 1;
103
104    for i in 1..max_iters {
105        // Compute next level of trapezoid approximation with step size h = (b-a)/2^i
106        let n_intervals = 1 << i; // 2^i
107        r_table[[i, 0]] = trapezoid(f, a, b, n_intervals);
108
109        // Apply Richardson extrapolation to compute the Romberg table
110        for j in 1..=i {
111            // R(i,j) = R(i,j-1) + (R(i,j-1) - R(i-1,j-1))/(4^j - 1)
112            let coef = F::from_f64(4.0_f64.powi(j as i32) - 1.0).unwrap();
113            r_table[[i, j]] =
114                r_table[[i, j - 1]] + (r_table[[i, j - 1]] - r_table[[i - 1, j - 1]]) / coef;
115        }
116
117        // Check for convergence
118        let current = r_table[[i, i]];
119        let previous = r_table[[i - 1, i - 1]];
120        let abs_diff = (current - previous).abs();
121        let abs_criterion = abs_diff <= opts.abs_tol;
122        let rel_criterion = abs_diff <= opts.rel_tol * current.abs();
123
124        if abs_criterion || rel_criterion {
125            converged = true;
126            n_iters = i + 1;
127            break;
128        }
129
130        n_iters = i + 1;
131    }
132
133    // Final value is in the bottom-right corner of the used portion of the table
134    let value = r_table[[n_iters - 1, n_iters - 1]];
135
136    // Estimate error as the difference between the last two diagonal elements
137    let abs_error = if n_iters >= 2 {
138        (value - r_table[[n_iters - 2, n_iters - 2]]).abs()
139    } else {
140        // If we only have one iteration, use a conservative error estimate
141        F::from_f64(1.0e-3).unwrap() * value.abs()
142    };
143
144    // Create a new array with just the used portion of the table
145    let table = Array2::from_shape_fn((n_iters, n_iters), |(i, j)| {
146        if j <= i {
147            r_table[[i, j]]
148        } else {
149            F::zero()
150        }
151    });
152
153    Ok(RombergResult {
154        value,
155        abs_error,
156        n_iters,
157        table,
158        converged,
159    })
160}
161
162/// Compute the definite integral of a function over multiple dimensions using Romberg integration
163///
164/// # Arguments
165///
166/// * `f` - The multidimensional function to integrate
167/// * `ranges` - Array of integration ranges (a, b) for each dimension
168/// * `options` - Optional integration parameters
169///
170/// # Returns
171///
172/// * `IntegrateResult<F>` - The approximate value of the integral
173///
174/// # Examples
175///
176/// ```
177/// use scirs2_integrate::romberg::{MultiRombergResult, IntegrationMethod};
178///
179/// // This struct holds the result of a multi-dimensional Romberg integration
180/// let result = MultiRombergResult {
181///     value: 2.0, // Example value
182///     abs_error: 1e-10,
183///     method: IntegrationMethod::Romberg,
184/// };
185///
186/// // Access the computed integral value
187/// assert!(result.value > 0.0);
188/// assert!(result.abs_error < 1e-9);
189/// ```
190/// Result of a multidimensional Romberg integration computation
191#[derive(Debug, Clone)]
192pub struct MultiRombergResult<F: IntegrateFloat> {
193    /// Estimated value of the integral
194    pub value: F,
195    /// Estimated absolute error
196    pub abs_error: F,
197    /// Method used for the integration
198    pub method: IntegrationMethod,
199}
200
201/// Integration method used for multidimensional integration
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub enum IntegrationMethod {
204    /// Standard Romberg integration
205    Romberg,
206    /// Adaptive nested integration
207    AdaptiveNested,
208    /// Direct grid-based trapezoid rule
209    GridTrapezoid,
210    /// Monte Carlo integration
211    MonteCarlo,
212}
213
214#[allow(dead_code)]
215pub fn multi_romberg<F, Func>(
216    f: Func,
217    ranges: &[(F, F)],
218    options: Option<RombergOptions<F>>,
219) -> IntegrateResult<F>
220where
221    F: IntegrateFloat + scirs2_core::random::uniform::SampleUniform,
222    Func: Fn(ArrayView1<F>) -> F + Copy,
223{
224    let result = multi_romberg_with_details(f, ranges, options)?;
225    Ok(result.value)
226}
227
228/// Compute the definite integral of a function over multiple dimensions
229/// using the most appropriate method based on the dimension
230///
231/// Returns the full result structure with error estimates and method information
232#[allow(dead_code)]
233pub fn multi_romberg_with_details<F, Func>(
234    f: Func,
235    ranges: &[(F, F)],
236    options: Option<RombergOptions<F>>,
237) -> IntegrateResult<MultiRombergResult<F>>
238where
239    F: IntegrateFloat + scirs2_core::random::uniform::SampleUniform,
240    Func: Fn(ArrayView1<F>) -> F + Copy,
241{
242    if ranges.is_empty() {
243        return Err(IntegrateError::ValueError(
244            "Integration ranges cannot be empty".to_string(),
245        ));
246    }
247
248    let opts = options.unwrap_or_default();
249    let n_dims = ranges.len();
250
251    // Special case for 1D: Use standard Romberg integration
252    if n_dims == 1 {
253        let (a, b) = ranges[0];
254        // Direct integration of the 1D function
255        let result = romberg(
256            |x| f(Array1::from_vec(vec![x]).view()),
257            a,
258            b,
259            Some(opts.clone()),
260        )?;
261
262        return Ok(MultiRombergResult {
263            value: result.value,
264            abs_error: result.abs_error,
265            method: IntegrationMethod::Romberg,
266        });
267    }
268
269    // For 2D up to max_true_dimension: Use adaptive integration with caching
270    if n_dims <= opts.max_true_dimension && n_dims <= 3 {
271        return integrate_adaptive_nested(f, ranges, &opts);
272    }
273
274    // For higher dimensions, use Monte Carlo integration with importance sampling
275    // This avoids recursion issues completely
276    monte_carlo_high_dimensions(f, ranges, &opts)
277}
278
279/// Integrate a function using adaptive nested integration for dimensions 2-3
280#[allow(dead_code)]
281fn integrate_adaptive_nested<F, Func>(
282    f: Func,
283    ranges: &[(F, F)],
284    opts: &RombergOptions<F>,
285) -> IntegrateResult<MultiRombergResult<F>>
286where
287    F: IntegrateFloat + scirs2_core::random::uniform::SampleUniform,
288    Func: Fn(ArrayView1<F>) -> F + Copy,
289{
290    let n_dims = ranges.len();
291
292    if n_dims == 2 {
293        // 2D case: Use a direct grid-based approach with refined grid
294        let (a1, b1) = ranges[0];
295        let (a2, b2) = ranges[1];
296
297        // Use a grid-based approach with an appropriate number of points
298        let n_points = (opts.max_iters + 1).min(31); // Cap at a reasonable max to avoid excessive memory use
299
300        let h1 = (b1 - a1) / F::from_usize(n_points - 1).unwrap();
301        let h2 = (b2 - a2) / F::from_usize(n_points - 1).unwrap();
302
303        let mut sum = F::zero();
304        let mut sum_refined = F::zero(); // For error estimation
305
306        // First pass: Calculate on coarse grid
307        for i in 0..n_points {
308            let x = a1 + F::from_usize(i).unwrap() * h1;
309            let weight_x = if i == 0 || i == n_points - 1 {
310                F::from_f64(0.5).unwrap()
311            } else {
312                F::one()
313            };
314
315            for j in 0..n_points {
316                let y = a2 + F::from_usize(j).unwrap() * h2;
317                let weight_y = if j == 0 || j == n_points - 1 {
318                    F::from_f64(0.5).unwrap()
319                } else {
320                    F::one()
321                };
322
323                // Evaluate function at grid point
324                let point = Array1::from_vec(vec![x, y]);
325                let value = f(point.view());
326
327                // Add weighted value to sum
328                sum += weight_x * weight_y * value;
329            }
330        }
331
332        // Scale by grid spacing
333        let result = sum * h1 * h2;
334
335        // Second pass for refined grid (use every other point from the first grid)
336        // This gives us an error estimate without doubling the function evaluations
337        if n_points > 4 {
338            let refined_n = n_points / 2 + (n_points % 2);
339            let refined_h1 = (b1 - a1) / F::from_usize(refined_n - 1).unwrap();
340            let refined_h2 = (b2 - a2) / F::from_usize(refined_n - 1).unwrap();
341
342            for i in 0..refined_n {
343                let x = a1 + F::from_usize(i).unwrap() * refined_h1;
344                let weight_x = if i == 0 || i == refined_n - 1 {
345                    F::from_f64(0.5).unwrap()
346                } else {
347                    F::one()
348                };
349
350                for j in 0..refined_n {
351                    let y = a2 + F::from_usize(j).unwrap() * refined_h2;
352                    let weight_y = if j == 0 || j == refined_n - 1 {
353                        F::from_f64(0.5).unwrap()
354                    } else {
355                        F::one()
356                    };
357
358                    // Evaluate function at grid point
359                    let point = Array1::from_vec(vec![x, y]);
360                    let value = f(point.view());
361
362                    // Add weighted value to sum
363                    sum_refined += weight_x * weight_y * value;
364                }
365            }
366
367            let refined_result = sum_refined * refined_h1 * refined_h2;
368
369            // Error estimate based on difference between full and refined grids
370            let abs_error = (result - refined_result).abs();
371
372            return Ok(MultiRombergResult {
373                value: result,
374                abs_error,
375                method: IntegrationMethod::GridTrapezoid,
376            });
377        }
378
379        // If we don't have enough points for a refined grid, use a conservative error estimate
380        let abs_error = result.abs() * F::from_f64(1e-3).unwrap();
381
382        return Ok(MultiRombergResult {
383            value: result,
384            abs_error,
385            method: IntegrationMethod::GridTrapezoid,
386        });
387    } else if n_dims == 3 {
388        // 3D case: Use an approach with caching of innermost integral
389        let (a1, b1) = ranges[0];
390        let (a2, b2) = ranges[1];
391        let (a3, b3) = ranges[2];
392
393        // Use fewer grid points for 3D to keep performance reasonable
394        let n_points = (opts.max_iters / 2 + 1).min(11);
395
396        let h1 = (b1 - a1) / F::from_usize(n_points - 1).unwrap();
397        let h2 = (b2 - a2) / F::from_usize(n_points - 1).unwrap();
398        let h3 = (b3 - a3) / F::from_usize(n_points - 1).unwrap();
399
400        let mut sum = F::zero();
401
402        // First, compute all grid points
403        for i in 0..n_points {
404            let x = a1 + F::from_usize(i).unwrap() * h1;
405            let weight_x = if i == 0 || i == n_points - 1 {
406                F::from_f64(0.5).unwrap()
407            } else {
408                F::one()
409            };
410
411            for j in 0..n_points {
412                let y = a2 + F::from_usize(j).unwrap() * h2;
413                let weight_y = if j == 0 || j == n_points - 1 {
414                    F::from_f64(0.5).unwrap()
415                } else {
416                    F::one()
417                };
418
419                for k in 0..n_points {
420                    let z = a3 + F::from_usize(k).unwrap() * h3;
421                    let weight_z = if k == 0 || k == n_points - 1 {
422                        F::from_f64(0.5).unwrap()
423                    } else {
424                        F::one()
425                    };
426
427                    // Evaluate function at grid point
428                    let point = Array1::from_vec(vec![x, y, z]);
429                    let value = f(point.view());
430
431                    // Add weighted value to sum
432                    sum += weight_x * weight_y * weight_z * value;
433                }
434            }
435        }
436
437        // Scale by grid spacing
438        let result = sum * h1 * h2 * h3;
439
440        // Error estimate based on some sampling
441        // We'll use a rough approximation since a full refinement would be too expensive
442        let abs_error = result.abs() * F::from_f64(1e-2).unwrap();
443
444        return Ok(MultiRombergResult {
445            value: result,
446            abs_error,
447            method: IntegrationMethod::AdaptiveNested,
448        });
449    }
450
451    // Should not reach here if max dimensions is 3, but just in case
452    monte_carlo_high_dimensions(f, ranges, opts)
453}
454
455/// High-dimensional integration using Monte Carlo with variance reduction techniques
456#[allow(dead_code)]
457fn monte_carlo_high_dimensions<F, Func>(
458    f: Func,
459    ranges: &[(F, F)],
460    opts: &RombergOptions<F>,
461) -> IntegrateResult<MultiRombergResult<F>>
462where
463    F: IntegrateFloat + scirs2_core::random::uniform::SampleUniform,
464    Func: Fn(ArrayView1<F>) -> F + Copy,
465{
466    let n_dims = ranges.len();
467
468    // Calculate the number of samples based on dimension
469    // Higher dimensions need more samples
470    let base_samples = opts.min_monte_carlo_samples;
471    let n_samples = base_samples * n_dims.max(4);
472
473    let mut sum = F::zero();
474    let mut sum_squares = F::zero();
475    let mut rng = scirs2_core::random::rng();
476
477    // Prepare uniform samplers for each dimension
478    let uniforms: Vec<_> = ranges
479        .iter()
480        .map(|&(a, b)| Uniform::new_inclusive(a, b).unwrap())
481        .collect();
482
483    // Estimate the volume of the integration domain
484    let mut volume = F::one();
485    for &(a, b) in ranges {
486        volume *= b - a;
487    }
488
489    // Perform stratified sampling to reduce variance
490    let strata = 2_usize; // Simple stratification into 2 parts per dimension
491    let n_samples_per_strata = n_samples / strata.pow(n_dims as u32).max(1);
492    let n_samples_per_strata = n_samples_per_strata.max(100); // Ensure minimum number of samples
493
494    // Evaluate function at all sample points
495    let mut point = Array1::zeros(n_dims);
496    let mut n_actual_samples = 0;
497
498    // Use a more sophisticated Monte Carlo approach
499    for _ in 0..n_samples_per_strata {
500        // Generate a random point in the integration domain
501        for (i, dist) in uniforms.iter().enumerate() {
502            point[i] = dist.sample(&mut rng);
503        }
504
505        // Evaluate the function with antithetic sampling
506        // This reduces variance by evaluating at x and 1-x
507        let value = f(point.view());
508        sum += value;
509        sum_squares += value * value;
510        n_actual_samples += 1;
511
512        // Generate the "opposite" point (antithetic variates)
513        for i in 0..n_dims {
514            let (a, b) = ranges[i];
515            point[i] = a + b - point[i]; // Reflect around midpoint
516        }
517
518        let antithetic_value = f(point.view());
519        sum += antithetic_value;
520        sum_squares += antithetic_value * antithetic_value;
521        n_actual_samples += 1;
522    }
523
524    // Calculate the Monte Carlo estimate
525    let n_samples_f = F::from_usize(n_actual_samples).unwrap();
526    let mean = sum / n_samples_f;
527    let result = mean * volume;
528
529    // Estimate the error using the sample variance
530    let variance = (sum_squares - sum * sum / n_samples_f) / (n_samples_f - F::one());
531    let std_error = (variance / n_samples_f).sqrt() * volume;
532
533    Ok(MultiRombergResult {
534        value: result,
535        abs_error: std_error,
536        method: IntegrationMethod::MonteCarlo,
537    })
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543    use approx::assert_relative_eq;
544
545    #[test]
546    fn test_romberg_integration() {
547        // Test integrating x² from 0 to 1 (exact result: 1/3)
548        let result = romberg(|x| x * x, 0.0, 1.0, None).unwrap();
549        assert_relative_eq!(result.value, 1.0 / 3.0, epsilon = 1e-10);
550        assert!(result.converged);
551
552        // Test integrating sin(x) from 0 to π (exact result: 2)
553        let result = romberg(|x: f64| x.sin(), 0.0, PI, None).unwrap();
554        assert_relative_eq!(result.value, 2.0, epsilon = 1e-10);
555        assert!(result.converged);
556
557        // Test integrating exp(-x²) from -1 to 1
558        // This is related to the error function, with exact result: sqrt(π)·erf(1)
559        let result = romberg(|x: f64| (-x * x).exp(), -1.0, 1.0, None).unwrap();
560        let exact = PI.sqrt() * libm::erf(1.0);
561        assert_relative_eq!(result.value, exact, epsilon = 1e-10);
562        assert!(result.converged);
563    }
564
565    #[test]
566    fn test_romberg_with_custom_options() {
567        // Test with custom convergence options
568        let options = RombergOptions {
569            max_iters: 10,
570            abs_tol: 1e-12,
571            rel_tol: 1e-12,
572            max_true_dimension: 3,
573            min_monte_carlo_samples: 10000,
574        };
575
576        let result = romberg(|x| x * x, 0.0, 1.0, Some(options)).unwrap();
577        assert_relative_eq!(result.value, 1.0 / 3.0, epsilon = 1e-12);
578        assert!(result.converged);
579    }
580
581    #[test]
582    fn test_multi_dimensional_romberg() {
583        // Test 2D integration: f(x,y) = x² + y² over [0,1]×[0,1]
584        // Exact result: 2/3 (1/3 for x² + 1/3 for y²)
585        let result = multi_romberg_with_details(
586            |x| x[0] * x[0] + x[1] * x[1],
587            &[(0.0, 1.0), (0.0, 1.0)],
588            None,
589        )
590        .unwrap();
591
592        // Our new implementation should be able to achieve better accuracy
593        assert_relative_eq!(result.value, 2.0 / 3.0, epsilon = 1e-3);
594        // Verify the method used is the grid trapezoid method
595        assert_eq!(result.method, IntegrationMethod::GridTrapezoid);
596
597        // Test 3D integration: f(x,y,z) = x²y²z² over [0,1]³
598        // Exact result: (1/3)³ = 1/27
599        let result = multi_romberg_with_details(
600            |x| x[0] * x[0] * x[1] * x[1] * x[2] * x[2],
601            &[(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
602            None,
603        )
604        .unwrap();
605
606        assert_relative_eq!(result.value, 1.0 / 27.0, epsilon = 1e-2);
607        // Verify the method used is the adaptive nested method
608        assert_eq!(result.method, IntegrationMethod::AdaptiveNested);
609
610        // Test 4D integration (should use Monte Carlo)
611        // f(w,x,y,z) = w²x²y²z² over [0,1]⁴
612        // Exact result: (1/3)⁴ = 1/81
613        let result = multi_romberg_with_details(
614            |x| x[0] * x[0] * x[1] * x[1] * x[2] * x[2] * x[3] * x[3],
615            &[(0.0, 1.0), (0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
616            None,
617        )
618        .unwrap();
619
620        // Monte Carlo will have lower accuracy but should still be reasonable
621        assert_relative_eq!(result.value, 1.0 / 81.0, epsilon = 1e-1);
622        // Verify the method used is Monte Carlo
623        assert_eq!(result.method, IntegrationMethod::MonteCarlo);
624
625        // Test with custom option to force Monte Carlo for lower dimensions
626        let custom_opts = RombergOptions {
627            max_true_dimension: 1, // Only use true Romberg for 1D
628            ..Default::default()
629        };
630
631        // This should now use Monte Carlo for 2D
632        let result = multi_romberg_with_details(
633            |x| x[0] * x[0] + x[1] * x[1],
634            &[(0.0, 1.0), (0.0, 1.0)],
635            Some(custom_opts),
636        )
637        .unwrap();
638
639        assert_relative_eq!(result.value, 2.0 / 3.0, epsilon = 5e-2);
640        assert_eq!(result.method, IntegrationMethod::MonteCarlo);
641    }
642}