multicalc 0.6.0

Rust scientific computing for single and multi-variable calculus
Documentation
use crate::numerical_integration::integrator::*;
use crate::numerical_integration::mode::IterativeMethod;
use crate::utils::error_codes::CalcError;

/// Default interval count. A multiple of 12 so Boole (needs a multiple of 4) and
/// Simpson 3/8 (needs a multiple of 3) both align with the composite-rule weights.
pub const DEFAULT_TOTAL_ITERATIONS: u64 = 120;

/// Configuration shared by the single- and multi-variable iterative integrators.
#[derive(Debug, Clone, Copy)]
pub struct IterativeConfig {
    /// Number of intervals the composite rule walks. See [`DEFAULT_TOTAL_ITERATIONS`].
    pub total_iterations: u64,
    /// The composite rule to use: Booles, Simpsons or Trapezoidal.
    pub integration_method: IterativeMethod,
}

impl Default for IterativeConfig {
    /// Boole's rule with [`DEFAULT_TOTAL_ITERATIONS`] intervals; optimal for most generic equations.
    fn default() -> Self {
        IterativeConfig {
            total_iterations: DEFAULT_TOTAL_ITERATIONS,
            integration_method: IterativeMethod::Booles,
        }
    }
}

impl IterativeConfig {
    /// Builds a config with an explicit iteration count and rule.
    pub fn from_parameters(total_iterations: u64, integration_method: IterativeMethod) -> Self {
        IterativeConfig {
            total_iterations,
            integration_method,
        }
    }

    /// Checks that the iteration count is non-zero and every limit is well-defined.
    /// The iteration count is checked before the limits so a zero count reports
    /// [`CalcError::IterationsZero`] regardless of the limits.
    fn check_for_errors<const NUM_INTEGRATIONS: usize>(
        &self,
        integration_limit: &[[f64; 2]; NUM_INTEGRATIONS],
    ) -> Result<(), CalcError> {
        if self.total_iterations == 0 {
            return Err(CalcError::IterationsZero);
        }

        for limit in integration_limit {
            classify(limit)?;
        }

        Ok(())
    }
}

/// Dispatches to the chosen rule, integrating `g` over `[lo, hi]` with `iterations`
/// intervals. The caller decides the domain branch before building `g`, so a finite
/// integral passes `func` straight through with no per-sample transform.
fn integrate_rule<G: FnMut(f64) -> f64>(
    method: IterativeMethod,
    iterations: u64,
    lo: f64,
    hi: f64,
    g: G,
) -> f64 {
    match method {
        IterativeMethod::Booles => booles(iterations, lo, hi, g),
        IterativeMethod::Simpsons => simpsons(iterations, lo, hi, g),
        IterativeMethod::Trapezoidal => trapezoidal(iterations, lo, hi, g),
    }
}

/// Boole's composite rule over `[lo, hi]`.
fn booles<G: FnMut(f64) -> f64>(iterations: u64, lo: f64, hi: f64, mut g: G) -> f64 {
    let delta = (hi - lo) / iterations as f64;
    let mut point = lo;

    let mut ans = 7.0 * g(point);
    let mut multiplier = 32.0;

    for iter in 0..iterations - 1 {
        point += delta;
        ans += multiplier * g(point);

        if (iter + 2) % 2 != 0 {
            multiplier = 32.0;
        } else if (iter + 2) % 4 == 0 {
            multiplier = 14.0;
        } else {
            multiplier = 12.0;
        }
    }

    ans += 7.0 * g(hi);

    2.0 * delta * ans / 45.0
}

/// Simpson's 3/8 composite rule over `[lo, hi]`.
fn simpsons<G: FnMut(f64) -> f64>(iterations: u64, lo: f64, hi: f64, mut g: G) -> f64 {
    let delta = (hi - lo) / iterations as f64;
    let mut point = lo;

    let mut ans = g(point);
    let mut multiplier = 3.0;

    for iter in 0..iterations - 1 {
        point += delta;
        ans += multiplier * g(point);

        if (iter + 2) % 3 == 0 {
            multiplier = 2.0;
        } else {
            multiplier = 3.0;
        }
    }

    ans += g(hi);

    3.0 * delta * ans / 8.0
}

/// Trapezoidal composite rule over `[lo, hi]`.
fn trapezoidal<G: FnMut(f64) -> f64>(iterations: u64, lo: f64, hi: f64, mut g: G) -> f64 {
    let delta = (hi - lo) / iterations as f64;
    let mut point = lo;

    let mut ans = g(point);

    for _ in 0..iterations - 1 {
        point += delta;
        ans += 2.0 * g(point);
    }

    ans += g(hi);

    0.5 * delta * ans
}

/// Implements the iterative methods for numerical integration for single variable functions
#[derive(Debug, Clone, Copy, Default)]
pub struct IterativeSingle {
    pub config: IterativeConfig,
}

impl IterativeSingle {
    /// custom constructor. Optimal for fine-tuning for more complex equations
    pub fn from_parameters(total_iterations: u64, integration_method: IterativeMethod) -> Self {
        IterativeSingle {
            config: IterativeConfig::from_parameters(total_iterations, integration_method),
        }
    }

    /// Integrates the `level`-th limit (1-based). Inner folds of a single-variable
    /// integral are constant in the outer variable, so the inner result is computed
    /// once and reused; an infinite outer limit weights it by `dx/dt`. A finite limit
    /// skips the domain transform entirely.
    fn integrate<F: Fn(f64) -> f64, const NUM_INTEGRATIONS: usize>(
        &self,
        level: usize,
        func: &F,
        integration_limit: &[[f64; 2]; NUM_INTEGRATIONS],
    ) -> f64 {
        let method = self.config.integration_method;
        let iterations = self.config.total_iterations;

        let domain = match classify(&integration_limit[level - 1]) {
            Ok(d) => d,
            Err(_) => return f64::NAN, // limits validated in check_for_errors; unreachable
        };

        if level == 1 {
            return match domain {
                Domain::Finite(a, b) => integrate_rule(method, iterations, a, b, func),
                _ => {
                    let (lo, hi) = t_bounds(&domain);
                    integrate_rule(method, iterations, lo, hi, |t| {
                        let (x, jacobian) = map_sample(&domain, t);
                        func(x) * jacobian
                    })
                }
            };
        }

        let inner = self.integrate(level - 1, func, integration_limit);
        match domain {
            Domain::Finite(a, b) => integrate_rule(method, iterations, a, b, |_| inner),
            _ => {
                let (lo, hi) = t_bounds(&domain);
                integrate_rule(method, iterations, lo, hi, |t| {
                    let (_, jacobian) = map_sample(&domain, t);
                    inner * jacobian
                })
            }
        }
    }
}

impl IntegratorSingleVariable for IterativeSingle {
    /// Integrates `func`, once for each limit in `integration_limit` (so the array length
    /// sets the number of integrations).
    ///
    /// A limit may be finite, or use `f64::INFINITY` / `f64::NEG_INFINITY` for an infinite or
    /// semi-infinite range. Infinite ranges are mapped onto a finite interval and are accurate
    /// only for integrands that decay toward the infinite end.
    ///
    /// # Arguments
    /// * `func` - the function to integrate.
    /// * `integration_limit` - the `[lower, upper]` limit for each level of integration.
    ///
    /// # Errors
    /// [`CalcError::IterationsZero`] if the configured iteration count is zero, or
    /// [`CalcError::IntegrationLimitsIllDefined`] if any limit is ill-defined.
    ///
    /// # Examples
    /// ```
    /// use multicalc::numerical_integration::integrator::IntegratorSingleVariable;
    /// use multicalc::numerical_integration::iterative_integration::IterativeSingle;
    ///
    /// let my_func = |x: f64| 2.0 * x;
    /// let integrator = IterativeSingle::default();
    ///
    /// // single integration of 2x over [0, 2] is 4
    /// let val = integrator.get(&my_func, &[[0.0, 2.0]; 1]).unwrap();
    /// assert!(f64::abs(val - 4.0) < 1e-6);
    ///
    /// // double integration over [0, 2] then [-1, 1] is 8
    /// let val = integrator.get(&my_func, &[[0.0, 2.0], [-1.0, 1.0]]).unwrap();
    /// assert!(f64::abs(val - 8.0) < 1e-6);
    ///
    /// // an infinite limit, for a decaying integrand: integral of e^(-x^2) over the real line is sqrt(pi)
    /// let val = integrator.get(&|x| (-x * x).exp(), &[[f64::NEG_INFINITY, f64::INFINITY]]).unwrap();
    /// assert!(f64::abs(val - std::f64::consts::PI.sqrt()) < 1e-6);
    /// ```
    fn get<F: Fn(f64) -> f64, const NUM_INTEGRATIONS: usize>(
        &self,
        func: &F,
        integration_limit: &[[f64; 2]; NUM_INTEGRATIONS],
    ) -> Result<f64, CalcError> {
        self.config.check_for_errors(integration_limit)?;
        Ok(self.integrate(NUM_INTEGRATIONS, func, integration_limit))
    }
}

/// Implements the iterative methods for numerical integration for multi variable functions
#[derive(Debug, Clone, Copy, Default)]
pub struct IterativeMulti {
    pub config: IterativeConfig,
}

impl IterativeMulti {
    /// custom constructor, optimal for fine-tuning the integrator for more complex equations
    pub fn from_parameters(total_iterations: u64, integration_method: IterativeMethod) -> Self {
        IterativeMulti {
            config: IterativeConfig::from_parameters(total_iterations, integration_method),
        }
    }

    /// Integrates the `level`-th limit (1-based) of a partial integral. The sampled
    /// abscissa is written into the integrated variable's slot before recursing, and
    /// an infinite limit weights the whole inner integral by `dx/dt`. A finite limit
    /// skips the domain transform entirely.
    fn integrate<
        F: Fn(&[f64; NUM_VARS]) -> f64,
        const NUM_VARS: usize,
        const NUM_INTEGRATIONS: usize,
    >(
        &self,
        level: usize,
        idx_to_integrate: [usize; NUM_INTEGRATIONS],
        func: &F,
        integration_limits: &[[f64; 2]; NUM_INTEGRATIONS],
        point: &[f64; NUM_VARS],
    ) -> f64 {
        let method = self.config.integration_method;
        let iterations = self.config.total_iterations;

        let domain = match classify(&integration_limits[level - 1]) {
            Ok(d) => d,
            Err(_) => return f64::NAN, // limits validated in check_for_errors; unreachable
        };
        let var = idx_to_integrate[level - 1];

        if level == 1 {
            let mut current = *point;
            return match domain {
                Domain::Finite(a, b) => integrate_rule(method, iterations, a, b, |x| {
                    current[var] = x;
                    func(&current)
                }),
                _ => {
                    let (lo, hi) = t_bounds(&domain);
                    integrate_rule(method, iterations, lo, hi, |t| {
                        let (x, jacobian) = map_sample(&domain, t);
                        current[var] = x;
                        func(&current) * jacobian
                    })
                }
            };
        }

        let mut current = *point;
        match domain {
            Domain::Finite(a, b) => integrate_rule(method, iterations, a, b, |x| {
                current[var] = x;
                self.integrate(
                    level - 1,
                    idx_to_integrate,
                    func,
                    integration_limits,
                    &current,
                )
            }),
            _ => {
                let (lo, hi) = t_bounds(&domain);
                integrate_rule(method, iterations, lo, hi, |t| {
                    let (x, jacobian) = map_sample(&domain, t);
                    current[var] = x;
                    let inner = self.integrate(
                        level - 1,
                        idx_to_integrate,
                        func,
                        integration_limits,
                        &current,
                    );
                    inner * jacobian
                })
            }
        }
    }
}

impl IntegratorMultiVariable for IterativeMulti {
    /// Partially integrates `func` over the variables in `idx_to_integrate`, once for each
    /// limit in `integration_limits` (so the array length sets the number of integrations).
    ///
    /// # Arguments
    /// * `idx_to_integrate` - the variable index integrated at each level.
    /// * `func` - the function to integrate.
    /// * `integration_limits` - the `[lower, upper]` limit for each level of integration.
    /// * `point` - the value of every variable. A variable being integrated holds its final
    ///   upper limit; a variable held constant holds that constant.
    ///
    /// # Errors
    /// [`CalcError::IterationsZero`] if the configured iteration count is zero, or
    /// [`CalcError::IntegrationLimitsIllDefined`] if any limit is ill-defined.
    ///
    /// # Examples
    /// ```
    /// use multicalc::numerical_integration::integrator::IntegratorMultiVariable;
    /// use multicalc::numerical_integration::iterative_integration::IterativeMulti;
    ///
    /// // f(x, y, z) = 2x + yz, integrated over x in [0, 1] with (y, z) = (2, 3); result is 7
    /// let func = |args: &[f64; 3]| 2.0 * args[0] + args[1] * args[2];
    /// let point = [1.0, 2.0, 3.0];
    /// let integrator = IterativeMulti::default();
    ///
    /// let val = integrator.get([0; 1], &func, &[[0.0, 1.0]; 1], &point).unwrap();
    /// assert!(f64::abs(val - 7.0) < 1e-6);
    /// ```
    fn get<F: Fn(&[f64; NUM_VARS]) -> f64, const NUM_VARS: usize, const NUM_INTEGRATIONS: usize>(
        &self,
        idx_to_integrate: [usize; NUM_INTEGRATIONS],
        func: &F,
        integration_limits: &[[f64; 2]; NUM_INTEGRATIONS],
        point: &[f64; NUM_VARS],
    ) -> Result<f64, CalcError> {
        self.config.check_for_errors(integration_limits)?;
        Ok(self.integrate(
            NUM_INTEGRATIONS,
            idx_to_integrate,
            func,
            integration_limits,
            point,
        ))
    }
}