1use std::cmp::Ordering;
20use std::collections::BinaryHeap;
21
22use crate::error::{finite, tolerance, NumericError, NumericResult, Status};
23
24const XGK: [f64; 8] = [
26 0.991_455_371_120_812_6,
27 0.949_107_912_342_758_5,
28 0.864_864_423_359_769_1,
29 0.741_531_185_599_394_4,
30 0.586_087_235_467_691_1,
31 0.405_845_151_377_397_2,
32 0.207_784_955_007_898_5,
33 0.0,
34];
35const WGK: [f64; 8] = [
37 0.022_935_322_010_529_22,
38 0.063_092_092_629_978_55,
39 0.104_790_010_322_250_2,
40 0.140_653_259_715_525_9,
41 0.169_004_726_639_267_9,
42 0.190_350_578_064_785_4,
43 0.204_432_940_075_298_9,
44 0.209_482_141_084_727_8,
45];
46const WG: [f64; 4] = [
48 0.129_484_966_168_869_7,
49 0.279_705_391_489_276_7,
50 0.381_830_050_505_118_9,
51 0.417_959_183_673_469_4,
52];
53
54#[derive(Debug, Clone, Copy, PartialEq)]
56pub struct IntegrationOptions {
57 pub absolute_tolerance: f64,
59 pub relative_tolerance: f64,
61 pub max_panels: usize,
63}
64
65impl IntegrationOptions {
66 pub fn new(absolute_tolerance: f64, relative_tolerance: f64) -> Self {
68 Self {
69 absolute_tolerance,
70 relative_tolerance,
71 max_panels: 2000,
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq)]
78#[must_use]
79pub struct Integral {
80 pub value: f64,
82 pub error_estimate: f64,
84 pub evaluations: usize,
86 pub panels: usize,
88 pub status: Status,
91}
92
93impl Integral {
94 pub fn converged(self) -> NumericResult<Self> {
100 if self.status == Status::Converged {
101 Ok(self)
102 } else {
103 Err(NumericError::NotConverged {
104 name: "integrate",
105 error_estimate: self.error_estimate,
106 })
107 }
108 }
109}
110
111#[derive(Debug, Clone, Copy)]
112struct Panel {
113 a: f64,
114 b: f64,
115 value: f64,
116 error: f64,
117}
118
119impl PartialEq for Panel {
120 fn eq(&self, other: &Self) -> bool {
121 self.error.total_cmp(&other.error) == Ordering::Equal
122 }
123}
124impl Eq for Panel {}
125impl PartialOrd for Panel {
126 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
127 Some(self.cmp(other))
128 }
129}
130impl Ord for Panel {
131 fn cmp(&self, other: &Self) -> Ordering {
132 self.error.total_cmp(&other.error)
133 }
134}
135
136pub fn integrate<F>(
148 mut f: F,
149 lower: f64,
150 upper: f64,
151 options: IntegrationOptions,
152) -> NumericResult<Integral>
153where
154 F: FnMut(f64) -> f64,
155{
156 finite(lower, "lower bound")?;
157 finite(upper, "upper bound")?;
158 let abs_tol = tolerance(options.absolute_tolerance, "absolute tolerance")?;
159 let rel_tol = tolerance(options.relative_tolerance, "relative tolerance")?;
160 if abs_tol == 0.0 && rel_tol < 50.0 * f64::EPSILON {
161 return Err(NumericError::InvalidArgument {
162 name: "tolerances",
163 reason: "must allow an error above rounding: absolute > 0 or relative >= 50 eps",
164 });
165 }
166 if options.max_panels == 0 {
167 return Err(NumericError::InvalidArgument {
168 name: "panel budget",
169 reason: "must be at least 1",
170 });
171 }
172 if lower == upper {
173 return Ok(Integral {
174 value: 0.0,
175 error_estimate: 0.0,
176 evaluations: 0,
177 panels: 0,
178 status: Status::Converged,
179 });
180 }
181 let (a, b, sign) = if lower < upper {
182 (lower, upper, 1.0)
183 } else {
184 (upper, lower, -1.0)
185 };
186 if !(b - a).is_finite() {
187 return Err(NumericError::InvalidArgument {
188 name: "interval",
189 reason: "is wider than f64 can represent",
190 });
191 }
192
193 let mut evaluations = 0usize;
194 let mut heap = BinaryHeap::new();
195 let mut settled: Vec<Panel> = Vec::new();
196 let first = kronrod(&mut f, a, b, &mut evaluations)?;
197 let mut total_value = first.value;
198 let mut total_error = first.error;
199 heap.push(first);
200 let target = |value: f64| abs_tol.max(rel_tol * value.abs());
201
202 let mut status = Status::Converged;
203 while total_error > target(total_value) {
204 if heap.len() + settled.len() >= options.max_panels {
205 status = Status::BudgetExhausted;
206 break;
207 }
208 let Some(worst) = heap.pop() else {
209 status = Status::RoundoffLimited;
210 break;
211 };
212 let mid = 0.5 * worst.a + 0.5 * worst.b;
213 let width = worst.b - worst.a;
215 if mid <= worst.a
216 || mid >= worst.b
217 || width <= 100.0 * f64::EPSILON * worst.a.abs().max(worst.b.abs())
218 {
219 settled.push(worst);
220 continue;
221 }
222 let left = kronrod(&mut f, worst.a, mid, &mut evaluations)?;
223 let right = kronrod(&mut f, mid, worst.b, &mut evaluations)?;
224 total_value += left.value + right.value - worst.value;
225 total_error += left.error + right.error - worst.error;
226 heap.push(left);
227 heap.push(right);
228 if heap.len() % 64 == 0 {
229 let (v, e) = sums(heap.iter().chain(settled.iter()));
231 total_value = v;
232 total_error = e;
233 }
234 }
235 let (value, error) = sums(heap.iter().chain(settled.iter()));
236 if status == Status::Converged && error > target(value) {
237 status = Status::RoundoffLimited;
238 }
239 Ok(Integral {
240 value: sign * value,
241 error_estimate: error,
242 evaluations,
243 panels: heap.len() + settled.len(),
244 status,
245 })
246}
247
248fn sums<'a>(panels: impl Iterator<Item = &'a Panel>) -> (f64, f64) {
249 let mut value = 0.0;
250 let mut compensation = 0.0;
251 let mut error = 0.0;
252 for p in panels {
253 let y = p.value - compensation;
255 let t = value + y;
256 compensation = (t - value) - y;
257 value = t;
258 error += p.error;
259 }
260 (value, error)
261}
262
263fn kronrod<F: FnMut(f64) -> f64>(
264 f: &mut F,
265 a: f64,
266 b: f64,
267 evaluations: &mut usize,
268) -> NumericResult<Panel> {
269 let center = 0.5 * a + 0.5 * b;
270 let half = 0.5 * (b - a);
271 let mut eval = |x: f64| -> NumericResult<f64> {
272 *evaluations += 1;
273 let v = f(x);
274 if v.is_finite() {
275 Ok(v)
276 } else {
277 Err(NumericError::NonFiniteEvaluation {
278 name: "integrand",
279 at: Some(x),
280 })
281 }
282 };
283 let fc = eval(center)?;
284 let mut kronrod = WGK[7] * fc;
285 let mut gauss = WG[3] * fc;
286 let mut abs_sum = WGK[7] * fc.abs();
287 let mut values = [0.0f64; 15];
288 values[7] = fc;
289 for j in 0..7 {
290 let dx = half * XGK[j];
291 let f1 = eval(center - dx)?;
292 let f2 = eval(center + dx)?;
293 values[j] = f1;
294 values[14 - j] = f2;
295 kronrod += WGK[j] * (f1 + f2);
296 abs_sum += WGK[j] * (f1.abs() + f2.abs());
297 if j % 2 == 1 {
298 gauss += WG[j / 2] * (f1 + f2);
299 }
300 }
301 let mean = 0.5 * kronrod;
302 let mut asc = WGK[7] * (fc - mean).abs();
303 for j in 0..7 {
304 asc += WGK[j] * ((values[j] - mean).abs() + (values[14 - j] - mean).abs());
305 }
306 let result = kronrod * half;
307 let resabs = abs_sum * half.abs();
308 let resasc = asc * half.abs();
309 let mut error = ((kronrod - gauss) * half).abs();
310 if resasc != 0.0 && error != 0.0 {
311 error = resasc * (1.0f64).min((200.0 * error / resasc).powf(1.5));
312 }
313 if resabs > f64::MIN_POSITIVE / (50.0 * f64::EPSILON) {
314 error = error.max(50.0 * f64::EPSILON * resabs);
315 }
316 Ok(Panel {
317 a,
318 b,
319 value: result,
320 error,
321 })
322}
323
324#[derive(Debug, Clone, PartialEq)]
331pub struct GaussLegendre {
332 nodes: Vec<f64>,
333 weights: Vec<f64>,
334}
335
336impl GaussLegendre {
337 pub const MAX_POINTS: usize = 512;
340
341 pub fn new(n: usize) -> NumericResult<Self> {
347 if n == 0 || n > Self::MAX_POINTS {
348 return Err(NumericError::InvalidArgument {
349 name: "point count",
350 reason: "must be between 1 and 512",
351 });
352 }
353 let mut nodes = vec![0.0; n];
354 let mut weights = vec![0.0; n];
355 let nf = n as f64;
356 for i in 0..n.div_ceil(2) {
357 let mut x = (std::f64::consts::PI * (i as f64 + 0.75) / (nf + 0.5)).cos();
358 let mut derivative = 0.0;
359 for _ in 0..100 {
360 let (p, dp) = legendre(n, x);
361 derivative = dp;
362 let step = p / dp;
363 x -= step;
364 if step.abs() <= 1e-16 * x.abs().max(1e-300) {
365 break;
366 }
367 }
368 let (_, dp) = legendre(n, x);
369 if dp.is_finite() {
370 derivative = dp;
371 }
372 let w = 2.0 / ((1.0 - x * x) * derivative * derivative);
373 nodes[i] = -x;
374 nodes[n - 1 - i] = x;
375 weights[i] = w;
376 weights[n - 1 - i] = w;
377 }
378 if n % 2 == 1 {
379 nodes[n / 2] = 0.0;
380 }
381 Ok(Self { nodes, weights })
382 }
383
384 pub fn nodes(&self) -> &[f64] {
386 &self.nodes
387 }
388
389 pub fn weights(&self) -> &[f64] {
391 &self.weights
392 }
393
394 pub fn apply<F: FnMut(f64) -> f64>(
400 &self,
401 mut f: F,
402 lower: f64,
403 upper: f64,
404 ) -> NumericResult<f64> {
405 finite(lower, "lower bound")?;
406 finite(upper, "upper bound")?;
407 let center = 0.5 * lower + 0.5 * upper;
408 let half = 0.5 * upper - 0.5 * lower;
409 let mut sum = 0.0;
410 for (x, w) in self.nodes.iter().zip(&self.weights) {
411 let t = center + half * x;
412 let v = f(t);
413 if !v.is_finite() {
414 return Err(NumericError::NonFiniteEvaluation {
415 name: "integrand",
416 at: Some(t),
417 });
418 }
419 sum += w * v;
420 }
421 Ok(sum * half)
422 }
423}
424
425fn legendre(n: usize, x: f64) -> (f64, f64) {
427 let (mut p0, mut p1) = (1.0, x);
428 for k in 2..=n {
429 let kf = k as f64;
430 let p2 = ((2.0 * kf - 1.0) * x * p1 - (kf - 1.0) * p0) / kf;
431 p0 = p1;
432 p1 = p2;
433 }
434 if n == 0 {
435 return (1.0, 0.0);
436 }
437 let dp = n as f64 * (x * p1 - p0) / (x * x - 1.0);
438 (p1, dp)
439}