qualia_core_db/solvers/calculus/
quadrature.rs1#[derive(Debug, Clone, Copy, PartialEq)]
4pub enum QuadratureError {
5 InvalidDomain,
6 NonFiniteIntegrand { x: f64 },
7 EvaluationBudgetExceeded,
8 WorkspaceExceeded,
9}
10
11#[repr(C)]
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct QuadratureResult {
14 pub value: f64,
15 pub absolute_error: f64,
16 pub evaluations: u32,
17 pub intervals: u32,
18}
19
20#[derive(Clone, Copy, Default)]
21struct SimpsonPanel {
22 a: f64,
23 b: f64,
24 fa: f64,
25 fm: f64,
26 fb: f64,
27 whole: f64,
28 tolerance: f64,
29}
30
31const MAX_ADAPTIVE_PANELS: usize = 512;
32
33fn checked_eval<F>(function: &F, x: f64) -> Result<f64, QuadratureError>
34where
35 F: Fn(f64) -> f64,
36{
37 let value = function(x);
38 if value.is_finite() {
39 Ok(value)
40 } else {
41 Err(QuadratureError::NonFiniteIntegrand { x })
42 }
43}
44
45pub fn adaptive_simpson<F>(
46 function: F,
47 a: f64,
48 b: f64,
49 absolute_tolerance: f64,
50 max_evaluations: u32,
51) -> Result<QuadratureResult, QuadratureError>
52where
53 F: Fn(f64) -> f64,
54{
55 if !a.is_finite()
56 || !b.is_finite()
57 || b <= a
58 || !absolute_tolerance.is_finite()
59 || absolute_tolerance <= 0.0
60 || max_evaluations < 3
61 {
62 return Err(QuadratureError::InvalidDomain);
63 }
64
65 let midpoint = 0.5 * (a + b);
66 let fa = checked_eval(&function, a)?;
67 let fm = checked_eval(&function, midpoint)?;
68 let fb = checked_eval(&function, b)?;
69 let whole = (b - a) * (fa + 4.0 * fm + fb) / 6.0;
70 let mut evaluations = 3_u32;
71 let mut stack = [SimpsonPanel::default(); MAX_ADAPTIVE_PANELS];
72 stack[0] = SimpsonPanel {
73 a,
74 b,
75 fa,
76 fm,
77 fb,
78 whole,
79 tolerance: absolute_tolerance,
80 };
81 let mut stack_len = 1;
82 let mut value = 0.0;
83 let mut error = 0.0;
84 let mut intervals = 0;
85
86 while stack_len > 0 {
87 stack_len -= 1;
88 let panel = stack[stack_len];
89 if evaluations + 2 > max_evaluations {
90 return Err(QuadratureError::EvaluationBudgetExceeded);
91 }
92 let midpoint = 0.5 * (panel.a + panel.b);
93 let left_midpoint = 0.5 * (panel.a + midpoint);
94 let right_midpoint = 0.5 * (midpoint + panel.b);
95 let flm = checked_eval(&function, left_midpoint)?;
96 let frm = checked_eval(&function, right_midpoint)?;
97 evaluations += 2;
98
99 let left = (midpoint - panel.a) * (panel.fa + 4.0 * flm + panel.fm) / 6.0;
100 let right = (panel.b - midpoint) * (panel.fm + 4.0 * frm + panel.fb) / 6.0;
101 let delta = left + right - panel.whole;
102 let local_error = delta.abs() / 15.0;
103 if local_error <= panel.tolerance {
104 value += left + right + delta / 15.0;
105 error += local_error;
106 intervals += 2;
107 continue;
108 }
109
110 if stack_len + 2 > stack.len() {
111 return Err(QuadratureError::WorkspaceExceeded);
112 }
113 let child_tolerance = panel.tolerance * 0.5;
114 stack[stack_len] = SimpsonPanel {
115 a: midpoint,
116 b: panel.b,
117 fa: panel.fm,
118 fm: frm,
119 fb: panel.fb,
120 whole: right,
121 tolerance: child_tolerance,
122 };
123 stack[stack_len + 1] = SimpsonPanel {
124 a: panel.a,
125 b: midpoint,
126 fa: panel.fa,
127 fm: flm,
128 fb: panel.fm,
129 whole: left,
130 tolerance: child_tolerance,
131 };
132 stack_len += 2;
133 }
134
135 Ok(QuadratureResult {
136 value,
137 absolute_error: error,
138 evaluations,
139 intervals,
140 })
141}
142
143const GK15_ABSCISSAE: [f64; 8] = [
144 0.991_455_371_120_812_6,
145 0.949_107_912_342_758_5,
146 0.864_864_423_359_769_1,
147 0.741_531_185_599_394_5,
148 0.586_087_235_467_691_1,
149 0.405_845_151_377_397_2,
150 0.207_784_955_007_898_48,
151 0.0,
152];
153const GK15_WEIGHTS: [f64; 8] = [
154 0.022_935_322_010_529_224,
155 0.063_092_092_629_978_55,
156 0.104_790_010_322_250_19,
157 0.140_653_259_715_525_92,
158 0.169_004_726_639_267_9,
159 0.190_350_578_064_785_42,
160 0.204_432_940_075_298_89,
161 0.209_482_141_084_727_82,
162];
163const G7_WEIGHTS: [f64; 4] = [
164 0.129_484_966_168_869_7,
165 0.279_705_391_489_276_64,
166 0.381_830_050_505_118_9,
167 0.417_959_183_673_469_4,
168];
169
170#[derive(Clone, Copy, Default)]
171struct IntervalPanel {
172 a: f64,
173 b: f64,
174 tolerance: f64,
175}
176
177fn gauss_kronrod_15_panel<F>(
178 function: &F,
179 a: f64,
180 b: f64,
181) -> Result<(f64, f64, u32), QuadratureError>
182where
183 F: Fn(f64) -> f64,
184{
185 let center = 0.5 * (a + b);
186 let half = 0.5 * (b - a);
187 let center_value = checked_eval(function, center)?;
188 let mut kronrod = GK15_WEIGHTS[7] * center_value;
189 let mut gauss = G7_WEIGHTS[3] * center_value;
190
191 for index in 0..7 {
192 let offset = half * GK15_ABSCISSAE[index];
193 let pair =
194 checked_eval(function, center - offset)? + checked_eval(function, center + offset)?;
195 kronrod += GK15_WEIGHTS[index] * pair;
196 match index {
197 1 => gauss += G7_WEIGHTS[0] * pair,
198 3 => gauss += G7_WEIGHTS[1] * pair,
199 5 => gauss += G7_WEIGHTS[2] * pair,
200 _ => {}
201 }
202 }
203 let kronrod = kronrod * half;
204 let gauss = gauss * half;
205 Ok((kronrod, (kronrod - gauss).abs(), 15))
206}
207
208pub fn adaptive_gauss_kronrod_15<F>(
209 function: F,
210 a: f64,
211 b: f64,
212 absolute_tolerance: f64,
213 max_evaluations: u32,
214) -> Result<QuadratureResult, QuadratureError>
215where
216 F: Fn(f64) -> f64,
217{
218 if !a.is_finite()
219 || !b.is_finite()
220 || b <= a
221 || !absolute_tolerance.is_finite()
222 || absolute_tolerance <= 0.0
223 || max_evaluations < 15
224 {
225 return Err(QuadratureError::InvalidDomain);
226 }
227
228 let mut stack = [IntervalPanel::default(); MAX_ADAPTIVE_PANELS];
229 stack[0] = IntervalPanel {
230 a,
231 b,
232 tolerance: absolute_tolerance,
233 };
234 let mut stack_len = 1;
235 let mut evaluations = 0;
236 let mut intervals = 0;
237 let mut value = 0.0;
238 let mut error = 0.0;
239
240 while stack_len > 0 {
241 stack_len -= 1;
242 let panel = stack[stack_len];
243 if evaluations + 15 > max_evaluations {
244 return Err(QuadratureError::EvaluationBudgetExceeded);
245 }
246 let (estimate, local_error, used) = gauss_kronrod_15_panel(&function, panel.a, panel.b)?;
247 evaluations += used;
248 if local_error <= panel.tolerance {
249 value += estimate;
250 error += local_error;
251 intervals += 1;
252 continue;
253 }
254 if stack_len + 2 > stack.len() {
255 return Err(QuadratureError::WorkspaceExceeded);
256 }
257 let midpoint = 0.5 * (panel.a + panel.b);
258 let tolerance = panel.tolerance * 0.5;
259 stack[stack_len] = IntervalPanel {
260 a: midpoint,
261 b: panel.b,
262 tolerance,
263 };
264 stack[stack_len + 1] = IntervalPanel {
265 a: panel.a,
266 b: midpoint,
267 tolerance,
268 };
269 stack_len += 2;
270 }
271
272 Ok(QuadratureResult {
273 value,
274 absolute_error: error,
275 evaluations,
276 intervals,
277 })
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn adaptive_simpson_meets_tolerance_and_reports_budget_failure() {
286 let result =
287 adaptive_simpson(|x| x.sin(), 0.0, core::f64::consts::PI, 1e-12, 10_000).unwrap();
288 assert!((result.value - 2.0).abs() < 1e-12);
289 assert!(result.absolute_error < 1e-12);
290 assert_eq!(
291 adaptive_simpson(|x| x.sin(), 0.0, 1.0, 1e-14, 3),
292 Err(QuadratureError::EvaluationBudgetExceeded)
293 );
294 }
295
296 #[test]
297 fn gauss_kronrod_integrates_polynomial_and_oscillation() {
298 let polynomial =
299 adaptive_gauss_kronrod_15(|x| x.powi(12), 0.0, 1.0, 1e-13, 10_000).unwrap();
300 assert!((polynomial.value - 1.0 / 13.0).abs() < 1e-13);
301
302 let oscillatory =
303 adaptive_gauss_kronrod_15(|x| (50.0 * x).sin(), 0.0, 1.0, 1e-10, 50_000).unwrap();
304 let expected = (1.0 - 50.0_f64.cos()) / 50.0;
305 assert!((oscillatory.value - expected).abs() < 1e-10);
306 }
307
308 #[test]
309 fn quadrature_rejects_reversed_and_non_finite_domains() {
310 assert_eq!(
311 adaptive_simpson(|x| x, 1.0, 0.0, 1e-6, 100),
312 Err(QuadratureError::InvalidDomain)
313 );
314 assert!(matches!(
315 adaptive_simpson(|x| if x > 0.4 { f64::NAN } else { x }, 0.0, 1.0, 1e-6, 100),
316 Err(QuadratureError::NonFiniteIntegrand { .. })
317 ));
318 }
319}