Skip to main content

cjc_runtime/
integrate.rs

1//! Numerical Integration — deterministic quadrature routines.
2//!
3//! # Determinism Contract
4//!
5//! All routines use `KahanAccumulatorF64` from `cjc_repro` for floating-point
6//! summation, guaranteeing bit-identical results across runs for the same inputs.
7//!
8//! # Functions
9//!
10//! - [`trapezoid`] — composite trapezoidal rule from paired (x, y) arrays
11//! - [`simpson`] — composite Simpson's 1/3 rule from paired (x, y) arrays
12//! - [`cumtrapz`] — cumulative trapezoidal integration, returns array of running integrals
13
14use cjc_repro::KahanAccumulatorF64;
15
16// ---------------------------------------------------------------------------
17// Composite Trapezoidal Rule
18// ---------------------------------------------------------------------------
19
20/// Composite trapezoidal rule for numerical integration.
21///
22/// Given arrays `xs` (abscissae) and `ys` (ordinates) of equal length n >= 2,
23/// computes the integral approximation:
24///
25///   sum_{i=0}^{n-2} (xs[i+1] - xs[i]) * (ys[i] + ys[i+1]) / 2
26///
27/// Uses Kahan summation for the accumulation.
28pub fn trapezoid(xs: &[f64], ys: &[f64]) -> Result<f64, String> {
29    if xs.len() != ys.len() {
30        return Err(format!(
31            "trapezoid: xs and ys must have equal length, got {} and {}",
32            xs.len(),
33            ys.len()
34        ));
35    }
36    if xs.len() < 2 {
37        return Err("trapezoid: need at least 2 points".into());
38    }
39
40    let mut acc = KahanAccumulatorF64::new();
41    for i in 0..xs.len() - 1 {
42        let dx = xs[i + 1] - xs[i];
43        let term = dx * (ys[i] + ys[i + 1]) * 0.5;
44        acc.add(term);
45    }
46    Ok(acc.finalize())
47}
48
49// ---------------------------------------------------------------------------
50// Composite Simpson's 1/3 Rule
51// ---------------------------------------------------------------------------
52
53/// Composite Simpson's 1/3 rule for numerical integration.
54///
55/// Given arrays `xs` (abscissae) and `ys` (ordinates) of equal length n >= 3,
56/// where n-1 (number of subintervals) must be even, computes:
57///
58///   sum_{i=0,2,4,...} (xs[i+2] - xs[i]) / 6 * (ys[i] + 4*ys[i+1] + ys[i+2])
59///
60/// Uses Kahan summation for the accumulation.
61pub fn simpson(xs: &[f64], ys: &[f64]) -> Result<f64, String> {
62    if xs.len() != ys.len() {
63        return Err(format!(
64            "simpson: xs and ys must have equal length, got {} and {}",
65            xs.len(),
66            ys.len()
67        ));
68    }
69    let n = xs.len();
70    if n < 3 {
71        return Err("simpson: need at least 3 points".into());
72    }
73    let intervals = n - 1;
74    if intervals % 2 != 0 {
75        return Err(format!(
76            "simpson: number of subintervals must be even, got {}",
77            intervals
78        ));
79    }
80
81    let mut acc = KahanAccumulatorF64::new();
82    let mut i = 0;
83    while i + 2 < n {
84        let h = (xs[i + 2] - xs[i]) / 6.0;
85        let term = h * (ys[i] + 4.0 * ys[i + 1] + ys[i + 2]);
86        acc.add(term);
87        i += 2;
88    }
89    Ok(acc.finalize())
90}
91
92// ---------------------------------------------------------------------------
93// Cumulative Trapezoidal Integration
94// ---------------------------------------------------------------------------
95
96/// Cumulative trapezoidal integration.
97///
98/// Returns an array of length n-1 where result[i] is the integral from
99/// xs[0] to xs[i+1], computed via cumulative trapezoidal summation.
100///
101/// Uses Kahan summation for the running total.
102pub fn cumtrapz(xs: &[f64], ys: &[f64]) -> Result<Vec<f64>, String> {
103    if xs.len() != ys.len() {
104        return Err(format!(
105            "cumtrapz: xs and ys must have equal length, got {} and {}",
106            xs.len(),
107            ys.len()
108        ));
109    }
110    if xs.len() < 2 {
111        return Err("cumtrapz: need at least 2 points".into());
112    }
113
114    let mut acc = KahanAccumulatorF64::new();
115    let mut result = Vec::with_capacity(xs.len() - 1);
116    for i in 0..xs.len() - 1 {
117        let dx = xs[i + 1] - xs[i];
118        let term = dx * (ys[i] + ys[i + 1]) * 0.5;
119        acc.add(term);
120        result.push(acc.finalize());
121    }
122    Ok(result)
123}
124
125// ---------------------------------------------------------------------------
126// Tests
127// ---------------------------------------------------------------------------
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_trapezoid_constant() {
135        // Integral of f(x) = 3 from 0 to 4 = 12
136        let xs: Vec<f64> = (0..=100).map(|i| i as f64 * 0.04).collect();
137        let ys: Vec<f64> = xs.iter().map(|_| 3.0).collect();
138        let result = trapezoid(&xs, &ys).unwrap();
139        assert!((result - 12.0).abs() < 1e-12);
140    }
141
142    #[test]
143    fn test_trapezoid_linear() {
144        // Integral of f(x) = x from 0 to 1 = 0.5 (exact for trapezoidal)
145        let n = 100;
146        let xs: Vec<f64> = (0..=n).map(|i| i as f64 / n as f64).collect();
147        let ys: Vec<f64> = xs.clone();
148        let result = trapezoid(&xs, &ys).unwrap();
149        assert!((result - 0.5).abs() < 1e-12);
150    }
151
152    #[test]
153    fn test_trapezoid_sin() {
154        // Integral of sin(x) from 0 to pi = 2.0
155        let n = 10000;
156        let xs: Vec<f64> = (0..=n)
157            .map(|i| i as f64 * std::f64::consts::PI / n as f64)
158            .collect();
159        let ys: Vec<f64> = xs.iter().map(|&x| x.sin()).collect();
160        let result = trapezoid(&xs, &ys).unwrap();
161        assert!(
162            (result - 2.0).abs() < 1e-6,
163            "trapezoid sin: expected ~2.0, got {}",
164            result
165        );
166    }
167
168    #[test]
169    fn test_simpson_quadratic() {
170        // Integral of x^2 from 0 to 1 = 1/3 (exact for Simpson on quadratics)
171        let n = 100; // must be even
172        let xs: Vec<f64> = (0..=n).map(|i| i as f64 / n as f64).collect();
173        let ys: Vec<f64> = xs.iter().map(|&x| x * x).collect();
174        let result = simpson(&xs, &ys).unwrap();
175        assert!(
176            (result - 1.0 / 3.0).abs() < 1e-10,
177            "simpson x^2: expected ~0.333, got {}",
178            result
179        );
180    }
181
182    #[test]
183    fn test_simpson_odd_intervals_error() {
184        let xs = vec![0.0, 1.0, 2.0, 3.0];
185        let ys = vec![0.0, 1.0, 4.0, 9.0];
186        assert!(simpson(&xs, &ys).is_err());
187    }
188
189    #[test]
190    fn test_cumtrapz_linear() {
191        // Cumulative integral of f(x)=x from 0..4 with step 1
192        let xs = vec![0.0, 1.0, 2.0, 3.0, 4.0];
193        let ys = vec![0.0, 1.0, 2.0, 3.0, 4.0];
194        let result = cumtrapz(&xs, &ys).unwrap();
195        assert_eq!(result.len(), 4);
196        assert!((result[0] - 0.5).abs() < 1e-12);
197        assert!((result[1] - 2.0).abs() < 1e-12);
198        assert!((result[2] - 4.5).abs() < 1e-12);
199        assert!((result[3] - 8.0).abs() < 1e-12);
200    }
201
202    #[test]
203    fn test_length_mismatch_error() {
204        assert!(trapezoid(&[0.0, 1.0], &[0.0]).is_err());
205        assert!(simpson(&[0.0, 1.0], &[0.0]).is_err());
206        assert!(cumtrapz(&[0.0, 1.0], &[0.0]).is_err());
207    }
208
209    #[test]
210    fn test_determinism() {
211        let n = 10000;
212        let xs: Vec<f64> = (0..=n)
213            .map(|i| i as f64 * std::f64::consts::PI / n as f64)
214            .collect();
215        let ys: Vec<f64> = xs.iter().map(|&x| x.sin()).collect();
216        let r1 = trapezoid(&xs, &ys).unwrap();
217        let r2 = trapezoid(&xs, &ys).unwrap();
218        assert_eq!(r1.to_bits(), r2.to_bits());
219    }
220}