Skip to main content

copula_core/numerical/
mod.rs

1//! Numerical utilities for copula computations.
2//!
3//! This module provides various numerical methods useful for copula analysis:
4//! - Numerical integration (trapezoidal rule, Simpson's rule)
5//! - Root finding (bisection, Brent's method)
6//! - Numerical differentiation
7//! - Interpolation methods
8//!
9//! ## Example
10//! ```
11//! use copula_core::numerical::{bisection, trapezoid_integrate};
12//!
13//! // Find root of f(x) = x^2 - 2 on [0, 2]
14//! let root = bisection(|x| x * x - 2.0, 0.0, 2.0, 1e-10, 100).unwrap();
15//! assert!((root - 2.0_f64.sqrt()).abs() < 1e-9);
16//!
17//! // Integrate f(x) = x^2 from 0 to 1
18//! let integral = trapezoid_integrate(|x| x * x, 0.0, 1.0, 1000);
19//! assert!((integral - 1.0/3.0).abs() < 1e-6);
20//! ```
21
22use crate::{CopulaError, Result};
23
24/// Find a root of f(x) = 0 using the bisection method.
25///
26/// # Arguments
27/// * `f` - The function to find the root of
28/// * `a` - Left bound of the interval
29/// * `b` - Right bound of the interval
30/// * `tol` - Tolerance for convergence
31/// * `max_iter` - Maximum number of iterations
32///
33/// # Returns
34/// The approximate root x such that f(x) ≈ 0
35///
36/// # Errors
37/// Returns an error if f(a) and f(b) have the same sign or max iterations exceeded
38pub fn bisection<F>(f: F, mut a: f64, mut b: f64, tol: f64, max_iter: usize) -> Result<f64>
39where
40    F: Fn(f64) -> f64,
41{
42    let mut fa = f(a);
43
44    if fa * f(b) > 0.0 {
45        return Err(CopulaError::numerical(
46            "bisection: f(a) and f(b) must have opposite signs",
47        ));
48    }
49
50    for _ in 0..max_iter {
51        let c = (a + b) / 2.0;
52        let fc = f(c);
53
54        if fc.abs() < tol || (b - a) / 2.0 < tol {
55            return Ok(c);
56        }
57
58        if fa * fc < 0.0 {
59            b = c;
60        } else {
61            a = c;
62            fa = fc;
63        }
64    }
65
66    Err(CopulaError::numerical(
67        "bisection: maximum iterations exceeded",
68    ))
69}
70
71/// Numerical integration using the trapezoidal rule.
72///
73/// # Arguments
74/// * `f` - The function to integrate
75/// * `a` - Lower bound of integration
76/// * `b` - Upper bound of integration
77/// * `n` - Number of subintervals
78///
79/// # Returns
80/// Approximate value of ∫_a^b f(x) dx
81pub fn trapezoid_integrate<F>(f: F, a: f64, b: f64, n: usize) -> f64
82where
83    F: Fn(f64) -> f64,
84{
85    let h = (b - a) / n as f64;
86    let mut sum = 0.5 * (f(a) + f(b));
87
88    for i in 1..n {
89        sum += f(a + i as f64 * h);
90    }
91
92    h * sum
93}
94
95/// Numerical integration using Simpson's rule.
96///
97/// # Arguments
98/// * `f` - The function to integrate
99/// * `a` - Lower bound of integration
100/// * `b` - Upper bound of integration
101/// * `n` - Number of subintervals (must be even)
102///
103/// # Returns
104/// Approximate value of ∫_a^b f(x) dx
105///
106/// # Panics
107/// Panics if n is not even
108pub fn simpson_integrate<F>(f: F, a: f64, b: f64, n: usize) -> f64
109where
110    F: Fn(f64) -> f64,
111{
112    assert!(n.is_multiple_of(2), "n must be even for Simpson's rule");
113
114    let h = (b - a) / n as f64;
115    let mut sum = f(a) + f(b);
116
117    for i in 1..n {
118        let x = a + i as f64 * h;
119        if i % 2 == 0 {
120            sum += 2.0 * f(x);
121        } else {
122            sum += 4.0 * f(x);
123        }
124    }
125
126    h * sum / 3.0
127}
128
129/// Numerical derivative using forward difference.
130///
131/// # Arguments
132/// * `f` - The function to differentiate
133/// * `x` - Point at which to compute the derivative
134/// * `h` - Step size (default: 1e-8)
135///
136/// # Returns
137/// Approximate value of f'(x)
138pub fn forward_diff<F>(f: F, x: f64, h: f64) -> f64
139where
140    F: Fn(f64) -> f64,
141{
142    (f(x + h) - f(x)) / h
143}
144
145/// Numerical derivative using central difference (more accurate).
146///
147/// # Arguments
148/// * `f` - The function to differentiate
149/// * `x` - Point at which to compute the derivative
150/// * `h` - Step size (default: 1e-5)
151///
152/// # Returns
153/// Approximate value of f'(x)
154pub fn central_diff<F>(f: F, x: f64, h: f64) -> f64
155where
156    F: Fn(f64) -> f64,
157{
158    (f(x + h) - f(x - h)) / (2.0 * h)
159}
160
161/// Second derivative using central difference.
162///
163/// # Arguments
164/// * `f` - The function to differentiate
165/// * `x` - Point at which to compute the second derivative
166/// * `h` - Step size
167///
168/// # Returns
169/// Approximate value of f''(x)
170pub fn second_diff<F>(f: F, x: f64, h: f64) -> f64
171where
172    F: Fn(f64) -> f64,
173{
174    (f(x + h) - 2.0 * f(x) + f(x - h)) / (h * h)
175}
176
177/// Linear interpolation between two points.
178///
179/// # Arguments
180/// * `x0`, `y0` - First point
181/// * `x1`, `y1` - Second point
182/// * `x` - Point to interpolate at
183///
184/// # Returns
185/// Interpolated y value at x
186pub fn linear_interp(x0: f64, y0: f64, x1: f64, y1: f64, x: f64) -> f64 {
187    y0 + (y1 - y0) * (x - x0) / (x1 - x0)
188}
189
190/// Compute the log-sum-exp trick for numerical stability.
191///
192/// Computes log(∑ exp(x_i)) in a numerically stable way.
193///
194/// # Arguments
195/// * `values` - Slice of values to sum
196///
197/// # Returns
198/// log(∑ exp(values))
199pub fn log_sum_exp(values: &[f64]) -> f64 {
200    if values.is_empty() {
201        return f64::NEG_INFINITY;
202    }
203
204    let max_val = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
205    if !max_val.is_finite() {
206        return max_val;
207    }
208
209    let sum: f64 = values.iter().map(|&x| (x - max_val).exp()).sum();
210    max_val + sum.ln()
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_bisection() {
219        // Find root of x^2 - 2 = 0 on [0, 2]
220        let root = bisection(|x| x * x - 2.0, 0.0, 2.0, 1e-10, 100).unwrap();
221        assert!((root - 2.0_f64.sqrt()).abs() < 1e-9);
222    }
223
224    #[test]
225    fn test_trapezoid() {
226        // Integrate x^2 from 0 to 1, should be 1/3
227        let integral = trapezoid_integrate(|x| x * x, 0.0, 1.0, 1000);
228        assert!((integral - 1.0 / 3.0).abs() < 1e-6);
229    }
230
231    #[test]
232    fn test_simpson() {
233        // Integrate x^3 from 0 to 2, should be 4
234        let integral = simpson_integrate(|x| x * x * x, 0.0, 2.0, 100);
235        assert!((integral - 4.0).abs() < 1e-10);
236    }
237
238    #[test]
239    fn test_central_diff() {
240        // Derivative of x^2 at x=3 should be 6
241        let deriv = central_diff(|x| x * x, 3.0, 1e-5);
242        assert!((deriv - 6.0).abs() < 1e-8);
243    }
244
245    #[test]
246    fn test_log_sum_exp() {
247        let values = vec![1.0, 2.0, 3.0];
248        let result = log_sum_exp(&values);
249        let expected = (1.0_f64.exp() + 2.0_f64.exp() + 3.0_f64.exp()).ln();
250        assert!((result - expected).abs() < 1e-10);
251    }
252
253    #[test]
254    fn test_bisection_no_sign_change() {
255        let result = bisection(|x| x * x + 1.0, 0.0, 2.0, 1e-10, 100);
256        assert!(result.is_err());
257    }
258
259    #[test]
260    fn test_bisection_exact_root() {
261        let root = bisection(|x| x, -1.0, 1.0, 1e-10, 100).unwrap();
262        assert!(root.abs() < 1e-10);
263    }
264
265    #[test]
266    fn test_forward_diff() {
267        let deriv = forward_diff(|x| x * x, 3.0, 1e-7);
268        assert!((deriv - 6.0).abs() < 1e-5);
269    }
270
271    #[test]
272    fn test_second_diff() {
273        // f(x) = x^2 => f''(x) = 2 for all x
274        let deriv2 = second_diff(|x| x * x, 5.0, 1e-4);
275        assert!((deriv2 - 2.0).abs() < 1e-4);
276    }
277
278    #[test]
279    fn test_linear_interp() {
280        let y = linear_interp(0.0, 0.0, 1.0, 1.0, 0.5);
281        assert!((y - 0.5).abs() < 1e-12);
282
283        let y = linear_interp(0.0, 10.0, 10.0, 20.0, 5.0);
284        assert!((y - 15.0).abs() < 1e-12);
285    }
286
287    #[test]
288    fn test_log_sum_exp_empty() {
289        assert_eq!(log_sum_exp(&[]), f64::NEG_INFINITY);
290    }
291
292    #[test]
293    fn test_log_sum_exp_single() {
294        let result = log_sum_exp(&[5.0]);
295        assert!((result - 5.0).abs() < 1e-10);
296    }
297
298    #[test]
299    fn test_log_sum_exp_large_values() {
300        // Should handle large values without overflow
301        let result = log_sum_exp(&[1000.0, 1001.0]);
302        assert!(result.is_finite());
303        assert!(result > 1000.0);
304    }
305
306    #[test]
307    fn test_simpson_accuracy() {
308        // sin(x) from 0 to pi = 2
309        let integral = simpson_integrate(|x| x.sin(), 0.0, std::f64::consts::PI, 1000);
310        assert!((integral - 2.0).abs() < 1e-8);
311    }
312
313    #[test]
314    fn test_trapezoid_accuracy() {
315        // e^x from 0 to 1 = e - 1
316        let integral = trapezoid_integrate(|x| x.exp(), 0.0, 1.0, 10000);
317        let expected = std::f64::consts::E - 1.0;
318        assert!((integral - expected).abs() < 1e-6);
319    }
320}