1use ogeom_core::{OgeomResult, ogeom_bail};
33
34const NODES: [f64; 5] = [
40 0.148_874_338_981_631_21,
41 0.433_395_394_129_247_2,
42 0.679_409_568_299_024_4,
43 0.865_063_366_688_984_5,
44 0.973_906_528_517_171_7,
45];
46
47const WEIGHTS: [f64; 5] = [
49 0.295_524_224_714_752_87,
50 0.269_266_719_309_996_35,
51 0.219_086_362_515_982_04,
52 0.149_451_349_150_580_6,
53 0.066_671_344_308_688_14,
54];
55
56const KRONROD_NODES: [f64; 8] = [
60 0.991_455_371_120_812_6,
61 0.949_107_912_342_758_5,
62 0.864_864_423_359_769_1,
63 0.741_531_185_599_394_5,
64 0.586_087_235_467_691_1,
65 0.405_845_151_377_397_2,
66 0.207_784_955_007_898_48,
67 0.0,
68];
69
70const KRONROD_WEIGHTS: [f64; 8] = [
72 0.022_935_322_010_529_224,
73 0.063_092_092_629_978_56,
74 0.104_790_010_322_250_19,
75 0.140_653_259_715_525_92,
76 0.169_004_726_639_267_9,
77 0.190_350_578_064_785_42,
78 0.204_432_940_075_298_89,
79 0.209_482_141_084_727_82,
80];
81
82const GAUSS7_WEIGHTS: [f64; 4] = [
85 0.129_484_966_168_869_7,
86 0.279_705_391_489_276_64,
87 0.381_830_050_505_118_9,
88 0.417_959_183_673_469_4,
89];
90
91const KRONROD21_NODES: [f64; 6] = [
94 0.995_657_163_025_808_1,
95 0.930_157_491_355_708_2,
96 0.780_817_726_586_416_9,
97 0.562_757_134_668_604_7,
98 0.294_392_862_701_460_2,
99 0.0,
100];
101
102const KRONROD21_WEIGHTS: [f64; 6] = [
104 0.011_694_638_867_371_874,
105 0.054_755_896_574_352,
106 0.093_125_454_583_697_6,
107 0.123_491_976_262_065_85,
108 0.142_775_938_577_060_08,
109 0.149_445_554_002_916_9,
110];
111
112const KRONROD21_AT_GAUSS: [f64; 5] = [
114 0.147_739_104_901_338_5,
115 0.134_709_217_311_473_33,
116 0.109_387_158_802_297_64,
117 0.075_039_674_810_919_95,
118 0.032_558_162_307_964_73,
119];
120
121const MAX_DEPTH: u32 = 24;
127
128const MAX_DEPTH_2D: u32 = 24;
131
132pub fn gauss_legendre<F: FnMut(f64) -> f64>(mut f: F, a: f64, b: f64) -> f64 {
140 let half = (b - a) * 0.5;
141 let middle = f64::midpoint(a, b);
142 let mut total = 0.0;
143 for (node, weight) in NODES.iter().zip(&WEIGHTS) {
144 let offset = half * node;
145 total += weight * (f(middle - offset) + f(middle + offset));
146 }
147 total * half
148}
149
150#[must_use]
156pub fn gauss_legendre_rule(a: f64, b: f64) -> [(f64, f64); 10] {
157 let half = (b - a) * 0.5;
158 let middle = f64::midpoint(a, b);
159 let mut rule = [(0.0, 0.0); 10];
160 for (i, (node, weight)) in NODES.iter().zip(&WEIGHTS).enumerate() {
161 let offset = half * node;
162 rule[2 * i] = (middle - offset, weight * half);
163 rule[2 * i + 1] = (middle + offset, weight * half);
164 }
165 rule
166}
167
168#[must_use]
178pub fn gauss_kronrod_21_rule(a: f64, b: f64) -> [(f64, f64, f64); 21] {
179 let half = (b - a) * 0.5;
180 let middle = f64::midpoint(a, b);
181 let mut rule = [(0.0, 0.0, 0.0); 21];
182 for (i, ((node, weight), kronrod)) in NODES
183 .iter()
184 .zip(&WEIGHTS)
185 .zip(&KRONROD21_AT_GAUSS)
186 .enumerate()
187 {
188 let offset = half * node;
189 rule[2 * i] = (middle - offset, kronrod * half, weight * half);
190 rule[2 * i + 1] = (middle + offset, kronrod * half, weight * half);
191 }
192 for (i, (node, weight)) in KRONROD21_NODES.iter().zip(&KRONROD21_WEIGHTS).enumerate() {
193 let offset = half * node;
194 rule[10 + 2 * i] = (middle - offset, weight * half, 0.0);
195 if *node != 0.0 {
196 rule[11 + 2 * i] = (middle + offset, weight * half, 0.0);
197 }
198 }
199 rule
200}
201
202pub fn gauss_kronrod<F: FnMut(f64) -> f64>(mut f: F, a: f64, b: f64) -> (f64, f64) {
212 let half = (b - a) * 0.5;
213 let middle = f64::midpoint(a, b);
214 let mut kronrod = 0.0;
215 let mut gauss = 0.0;
216 for (i, (node, weight)) in KRONROD_NODES.iter().zip(&KRONROD_WEIGHTS).enumerate() {
217 let offset = half * node;
218 let pair = if *node == 0.0 {
219 f(middle)
220 } else {
221 f(middle - offset) + f(middle + offset)
222 };
223 kronrod += weight * pair;
224 if i % 2 == 1 {
225 gauss += GAUSS7_WEIGHTS[i / 2] * pair;
226 }
227 }
228 (kronrod * half, ((kronrod - gauss) * half).abs())
229}
230
231pub fn integrate<F: FnMut(f64) -> f64>(
260 mut f: F,
261 a: f64,
262 b: f64,
263 tolerance: f64,
264) -> OgeomResult<f64> {
265 if !a.is_finite() || !b.is_finite() {
266 ogeom_bail!(Domain, "cannot integrate over [{a}, {b}]");
267 }
268 if !tolerance.is_finite() || tolerance <= 0.0 {
269 ogeom_bail!(Domain, "integration tolerance {tolerance} must be positive");
270 }
271 if a == b {
272 return Ok(0.0);
273 }
274 refine(&mut f, a, b, tolerance, 0)
275}
276
277fn refine<F: FnMut(f64) -> f64>(
279 f: &mut F,
280 a: f64,
281 b: f64,
282 tolerance: f64,
283 depth: u32,
284) -> OgeomResult<f64> {
285 let (value, error) = gauss_kronrod(&mut *f, a, b);
286 if error <= tolerance {
287 return Ok(value);
288 }
289 if value.abs() + error <= tolerance {
297 return Ok(value);
298 }
299 if depth >= MAX_DEPTH {
300 ogeom_bail!(
301 NotDone,
302 "the integral over [{a}, {b}] did not converge to {tolerance} \
303 within {MAX_DEPTH} subdivisions; the integrand has a singularity \
304 there rather than a resolution problem"
305 );
306 }
307 let middle = f64::midpoint(a, b);
310 let half = tolerance * 0.5;
311 Ok(refine(f, a, middle, half, depth + 1)? + refine(f, middle, b, half, depth + 1)?)
312}
313
314pub fn integrate_2d<F: FnMut(f64, f64) -> f64>(
334 mut f: F,
335 u: (f64, f64),
336 v: (f64, f64),
337 tolerance: f64,
338) -> OgeomResult<f64> {
339 let (a, b) = u;
340 let (c, d) = v;
341 if !a.is_finite() || !b.is_finite() || !c.is_finite() || !d.is_finite() {
342 ogeom_bail!(Domain, "cannot integrate over [{a}, {b}] x [{c}, {d}]");
343 }
344 if !tolerance.is_finite() || tolerance <= 0.0 {
345 ogeom_bail!(Domain, "integration tolerance {tolerance} must be positive");
346 }
347 if a == b || c == d {
348 return Ok(0.0);
349 }
350 refine_2d(&mut f, (a, b), (c, d), tolerance, 0)
351}
352
353fn gauss_kronrod_2d<F: FnMut(f64, f64) -> f64>(
356 f: &mut F,
357 (a, b): (f64, f64),
358 (c, d): (f64, f64),
359) -> (f64, [f64; 2]) {
360 let (half_u, middle_u) = ((b - a) * 0.5, f64::midpoint(a, b));
361 let (half_v, middle_v) = ((d - c) * 0.5, f64::midpoint(c, d));
362 let stations = |half: f64, middle: f64, node: f64| -> [Option<f64>; 2] {
364 if node == 0.0 {
365 [Some(middle), None]
366 } else {
367 [Some(middle - half * node), Some(middle + half * node)]
368 }
369 };
370 let mut kronrod = 0.0;
371 let mut gauss_u = 0.0;
372 let mut gauss_v = 0.0;
373 for (i, (node_u, weight_u)) in KRONROD_NODES.iter().zip(&KRONROD_WEIGHTS).enumerate() {
374 for (j, (node_v, weight_v)) in KRONROD_NODES.iter().zip(&KRONROD_WEIGHTS).enumerate() {
375 let mut cell = 0.0;
376 for uu in stations(half_u, middle_u, *node_u).into_iter().flatten() {
377 for vv in stations(half_v, middle_v, *node_v).into_iter().flatten() {
378 cell += f(uu, vv);
379 }
380 }
381 kronrod += weight_u * weight_v * cell;
382 if i % 2 == 1 {
383 gauss_u += GAUSS7_WEIGHTS[i / 2] * weight_v * cell;
384 }
385 if j % 2 == 1 {
386 gauss_v += weight_u * GAUSS7_WEIGHTS[j / 2] * cell;
387 }
388 }
389 }
390 let scale = half_u * half_v;
391 (
392 kronrod * scale,
393 [
394 ((kronrod - gauss_u) * scale).abs(),
395 ((kronrod - gauss_v) * scale).abs(),
396 ],
397 )
398}
399
400fn refine_2d<F: FnMut(f64, f64) -> f64>(
402 f: &mut F,
403 (a, b): (f64, f64),
404 (c, d): (f64, f64),
405 tolerance: f64,
406 depth: u32,
407) -> OgeomResult<f64> {
408 let (value, [error_u, error_v]) = gauss_kronrod_2d(f, (a, b), (c, d));
409 let error = error_u.max(error_v);
410 if error <= tolerance || value.abs() + error <= tolerance {
411 return Ok(value);
412 }
413 if depth >= MAX_DEPTH_2D {
414 ogeom_bail!(
415 NotDone,
416 "the integral over [{a}, {b}] x [{c}, {d}] did not converge to \
417 {tolerance} within {MAX_DEPTH_2D} halvings; the integrand has a \
418 singularity there rather than a resolution problem"
419 );
420 }
421 let half = tolerance * 0.5;
422 if error_u >= error_v {
423 let mu = f64::midpoint(a, b);
424 Ok(refine_2d(f, (a, mu), (c, d), half, depth + 1)?
425 + refine_2d(f, (mu, b), (c, d), half, depth + 1)?)
426 } else {
427 let mv = f64::midpoint(c, d);
428 Ok(refine_2d(f, (a, b), (c, mv), half, depth + 1)?
429 + refine_2d(f, (a, b), (mv, d), half, depth + 1)?)
430 }
431}
432
433#[cfg(test)]
434#[allow(clippy::unwrap_used)]
435mod tests {
436 use super::*;
437 use approx::assert_relative_eq;
438 use core::f64::consts::PI;
439
440 #[test]
441 fn a_polynomial_within_the_rules_degree_is_exact_in_one_go() {
442 let f = |x: f64| x.powi(19) + 3.0 * x.powi(4) - 7.0 * x + 2.0;
445 let exact = 1.0 / 20.0 + 3.0 / 5.0 - 7.0 / 2.0 + 2.0;
446 assert_relative_eq!(gauss_legendre(f, 0.0, 1.0), exact, epsilon = 1e-14);
447 }
448
449 #[test]
450 fn transcendental_integrands_converge() {
451 assert_relative_eq!(
452 integrate(f64::sin, 0.0, PI, 1e-12).unwrap(),
453 2.0,
454 epsilon = 1e-12
455 );
456 assert_relative_eq!(
457 integrate(|x| 1.0 / x, 1.0, core::f64::consts::E, 1e-12).unwrap(),
458 1.0,
459 epsilon = 1e-12
460 );
461 }
462
463 #[test]
464 fn an_infinite_derivative_at_an_endpoint_is_handled_to_a_stated_limit() {
465 let quarter = |x: f64| (1.0 - x * x).max(0.0).sqrt();
470 let found = integrate(quarter, 0.0, 1.0, 1e-7).unwrap();
471 assert_relative_eq!(found, PI / 4.0, epsilon = 1e-12);
472 assert!(
473 integrate(quarter, 0.0, 1.0, 1e-8).is_err(),
474 "asked for more than the method can give, it should say so"
475 );
476 }
477
478 #[test]
483 fn the_gauss_kronrod_pair_is_exact_to_its_degrees() {
484 let (weight_sum, _) = gauss_kronrod(|_| 1.0, -1.0, 1.0);
485 assert_relative_eq!(weight_sum, 2.0, epsilon = 1e-15);
486 let thirteen = |x: f64| x.powi(13) + 2.0 * x.powi(8) - x;
487 let (value, error) = gauss_kronrod(thirteen, 0.0, 1.0);
488 assert_relative_eq!(value, 1.0 / 14.0 + 2.0 / 9.0 - 0.5, epsilon = 1e-14);
489 assert!(error < 1e-14, "both rules exact, no gap: {error}");
490 let twenty_two = |x: f64| x.powi(22) - 3.0 * x.powi(17);
491 let (value, error) = gauss_kronrod(twenty_two, 0.0, 1.0);
492 assert_relative_eq!(value, 1.0 / 23.0 - 3.0 / 18.0, epsilon = 1e-14);
493 assert!(
494 error > 1e-6,
495 "the Gauss rule is not exact here, and the gap says so: {error}"
496 );
497 }
498
499 #[test]
504 fn the_twenty_one_point_pair_is_exact_to_its_degrees() {
505 let (a, b) = (0.0, 2.0);
506 let rule = gauss_kronrod_21_rule(a, b);
507 for (&(x, _, wg), (y, w)) in rule.iter().zip(gauss_legendre_rule(a, b)) {
508 assert_eq!((x, wg), (y, w));
509 }
510 assert!(rule[10..].iter().all(|&(_, _, wg)| wg == 0.0));
511 for degree in 0..=31 {
512 let exact = (b.powi(degree + 1) - a.powi(degree + 1)) / f64::from(degree + 1);
513 let kronrod: f64 = rule.iter().map(|&(x, wk, _)| wk * x.powi(degree)).sum();
514 let gauss: f64 = rule.iter().map(|&(x, _, wg)| wg * x.powi(degree)).sum();
515 let miss = |sum: f64| ((sum - exact) / exact).abs();
516 assert!(miss(kronrod) < 1e-14, "Kronrod, degree {degree}");
517 assert_eq!(miss(gauss) < 1e-14, degree <= 19, "Gauss, degree {degree}");
518 }
519 }
520
521 #[test]
525 fn a_rectangle_integrates_to_a_stated_tolerance() {
526 let product =
527 integrate_2d(|u, v| u * u * v * v * v, (0.0, 1.0), (0.0, 1.0), 1e-12).unwrap();
528 assert_relative_eq!(product, 1.0 / 12.0, epsilon = 1e-13);
529 let sphere = integrate_2d(|_, v| v.sin(), (0.0, 2.0 * PI), (0.0, PI), 1e-10).unwrap();
530 assert_relative_eq!(sphere, 4.0 * PI, epsilon = 1e-9);
531 let ridge = integrate_2d(|u, v| (u - 0.3).abs() + v, (0.0, 1.0), (0.0, 1.0), 1e-9).unwrap();
532 assert_relative_eq!(ridge, 0.29 + 0.5, epsilon = 1e-8);
534 assert!(integrate_2d(|u, _| 1.0 / u, (0.0, 1.0), (0.0, 1.0), 1e-9).is_err());
535 assert_eq!(
536 integrate_2d(|u, v| u + v, (1.0, 1.0), (0.0, 1.0), 1e-9).unwrap(),
537 0.0
538 );
539 }
540
541 #[test]
542 fn a_reversed_interval_integrates_to_the_negative() {
543 let forward = integrate(f64::sin, 0.0, PI, 1e-12).unwrap();
546 let backward = integrate(f64::sin, PI, 0.0, 1e-12).unwrap();
547 assert_relative_eq!(forward, -backward, epsilon = 1e-12);
548 }
549
550 #[test]
551 fn an_empty_interval_integrates_to_nothing() {
552 assert_eq!(integrate(f64::sin, 1.0, 1.0, 1e-12).unwrap(), 0.0);
553 }
554
555 #[test]
556 fn an_integrand_that_will_not_converge_says_so() {
557 assert!(integrate(|x| 1.0 / x, 0.0, 1.0, 1e-12).is_err());
561 }
562
563 #[test]
564 fn non_finite_bounds_and_tolerances_are_refused() {
565 assert!(integrate(f64::sin, 0.0, f64::NAN, 1e-9).is_err());
566 assert!(integrate(f64::sin, f64::NEG_INFINITY, 0.0, 1e-9).is_err());
567 assert!(integrate(f64::sin, 0.0, 1.0, 0.0).is_err());
568 assert!(integrate(f64::sin, 0.0, 1.0, -1.0).is_err());
569 }
570}