1use crate::{
36 geop_error::{GeopError, GeopResult},
37 scalars::Scalar,
38};
39
40#[allow(clippy::excessive_precision)]
45const XGK: [f64; 8] = [
46 0.991_455_371_120_812_639_206_854_697_526_329,
47 0.949_107_912_342_758_524_526_189_684_047_851,
48 0.864_864_423_359_769_072_789_712_788_640_926,
49 0.741_531_185_599_394_439_863_864_773_280_788,
50 0.586_087_235_467_691_130_294_144_845_693_013,
51 0.405_845_151_377_397_166_906_606_412_076_961,
52 0.207_784_955_007_898_467_600_689_403_773_245,
53 0.0,
54];
55
56#[allow(clippy::excessive_precision)]
59const WGK: [f64; 8] = [
60 0.022_935_322_010_529_224_963_732_008_058_970,
61 0.063_092_092_629_978_553_290_700_663_189_204,
62 0.104_790_010_322_250_183_839_876_322_541_518,
63 0.140_653_259_715_525_918_745_189_590_510_238,
64 0.169_004_726_639_267_902_826_583_426_598_550,
65 0.190_350_578_064_785_409_913_256_402_421_014,
66 0.204_432_940_075_298_892_414_161_999_234_649,
67 0.209_482_141_084_727_828_012_999_174_891_714,
68];
69
70#[allow(clippy::excessive_precision)]
74const WG: [f64; 4] = [
75 0.129_484_966_168_869_693_270_611_432_679_082,
76 0.279_705_391_489_276_667_901_467_771_423_780,
77 0.381_830_050_505_118_944_950_369_775_488_975,
78 0.417_959_183_673_469_387_755_102_040_816_327,
79];
80
81#[allow(clippy::excessive_precision)]
88const GAUSS_LEGENDRE: [&[(f64, f64)]; 10] = [
89 &[(0.0, 2.0)],
90 &[(0.577_350_269_189_625_764_509_148_780_501_957_456, 1.0)],
91 &[
92 (
93 0.774_596_669_241_483_377_035_853_079_956_479_922,
94 0.555_555_555_555_555_555_555_555_555_555_555_556,
95 ),
96 (0.0, 0.888_888_888_888_888_888_888_888_888_888_888_889),
97 ],
98 &[
99 (
100 0.861_136_311_594_052_575_223_946_488_892_809_505,
101 0.347_854_845_137_453_857_373_063_949_221_999_407,
102 ),
103 (
104 0.339_981_043_584_856_264_802_665_759_103_244_687,
105 0.652_145_154_862_546_142_626_936_050_778_000_593,
106 ),
107 ],
108 &[
109 (
110 0.906_179_845_938_663_992_797_626_878_299_392_965,
111 0.236_926_885_056_189_087_514_264_040_719_917_363,
112 ),
113 (
114 0.538_469_310_105_683_091_036_314_420_700_208_805,
115 0.478_628_670_499_366_468_041_291_514_835_638_193,
116 ),
117 (0.0, 0.568_888_888_888_888_888_888_888_888_888_888_889),
118 ],
119 &[
120 (
121 0.932_469_514_203_152_027_812_301_554_493_994_609,
122 0.171_324_492_379_170_345_040_296_142_172_732_894,
123 ),
124 (
125 0.661_209_386_466_264_513_661_399_595_019_905_347,
126 0.360_761_573_048_138_607_569_833_513_837_716_112,
127 ),
128 (
129 0.238_619_186_083_196_908_630_501_721_680_711_935,
130 0.467_913_934_572_691_047_389_870_343_989_550_995,
131 ),
132 ],
133 &[
134 (
135 0.949_107_912_342_758_524_526_189_684_047_851_262,
136 0.129_484_966_168_869_693_270_611_432_679_082_018,
137 ),
138 (
139 0.741_531_185_599_394_439_863_864_773_280_788_407,
140 0.279_705_391_489_276_667_901_467_771_423_779_582,
141 ),
142 (
143 0.405_845_151_377_397_166_906_606_412_076_961_463,
144 0.381_830_050_505_118_944_950_369_775_488_975_134,
145 ),
146 (0.0, 0.417_959_183_673_469_387_755_102_040_816_326_531),
147 ],
148 &[
149 (
150 0.960_289_856_497_536_231_683_560_868_569_472_99,
151 0.101_228_536_290_376_259_152_531_354_309_962_19,
152 ),
153 (
154 0.796_666_477_413_626_739_591_553_936_475_830_437,
155 0.222_381_034_453_374_470_544_355_994_426_240_884,
156 ),
157 (
158 0.525_532_409_916_328_985_817_739_049_189_246_349,
159 0.313_706_645_877_887_287_337_962_201_986_601_313,
160 ),
161 (
162 0.183_434_642_495_649_804_939_476_142_360_183_981,
163 0.362_683_783_378_361_982_965_150_449_277_195_612,
164 ),
165 ],
166 &[
167 (
168 0.968_160_239_507_626_089_835_576_202_903_672_87,
169 0.081_274_388_361_574_411_971_892_158_110_523_650_7,
170 ),
171 (
172 0.836_031_107_326_635_794_299_429_788_069_734_877,
173 0.180_648_160_694_857_404_058_472_031_242_912_81,
174 ),
175 (
176 0.613_371_432_700_590_397_308_702_039_341_474_185,
177 0.260_610_696_402_935_462_318_742_869_418_632_85,
178 ),
179 (
180 0.324_253_423_403_808_929_038_538_014_643_336_609,
181 0.312_347_077_040_002_840_068_630_406_584_443_666,
182 ),
183 (0.0, 0.330_239_355_001_259_763_164_525_069_286_974_049),
184 ],
185 &[
186 (
187 0.973_906_528_517_171_720_077_964_012_084_452_053,
188 0.066_671_344_308_688_137_593_568_809_893_331_792_9,
189 ),
190 (
191 0.865_063_366_688_984_510_732_096_688_423_493_049,
192 0.149_451_349_150_580_593_145_776_339_657_697_332,
193 ),
194 (
195 0.679_409_568_299_024_406_234_327_365_114_873_576,
196 0.219_086_362_515_982_043_995_534_934_228_163_192,
197 ),
198 (
199 0.433_395_394_129_247_190_799_265_943_165_784_162,
200 0.269_266_719_309_996_355_091_226_921_569_469_353,
201 ),
202 (
203 0.148_874_338_981_631_210_884_826_001_129_719_985,
204 0.295_524_224_714_752_870_173_892_994_651_338_329,
205 ),
206 ],
207];
208
209pub const MAX_POLYNOMIAL_DEGREE: usize = 2 * GAUSS_LEGENDRE.len() - 1;
211
212fn around(x: f64) -> Scalar {
216 if x == 0.0 {
217 return Scalar::ZERO;
218 }
219 let step = |up: bool| {
220 let bits = x.to_bits();
221 f64::from_bits(if (x > 0.0) == up { bits + 1 } else { bits - 1 })
222 };
223 Scalar::from_f64(step(false)).union(Scalar::from_f64(step(true)))
224}
225
226#[derive(Clone, Copy, Debug)]
230pub struct Quadrature {
231 pub relative_tolerance: f64,
232 pub max_panels: usize,
233}
234
235impl Default for Quadrature {
236 fn default() -> Self {
237 Self {
238 relative_tolerance: 1e-10,
239 max_panels: 200,
240 }
241 }
242}
243
244#[derive(Clone, Debug)]
246pub struct Integral {
247 pub value: Vec<Scalar>,
250 pub converged: bool,
254 pub evaluations: usize,
256}
257
258struct Panel {
262 a: Scalar,
263 b: Scalar,
264 kronrod: Vec<Scalar>,
265 error: Vec<f64>,
266 absolute: Vec<f64>,
267 width: Vec<f64>,
269}
270
271fn panel(
273 f: &mut impl FnMut(Scalar) -> GeopResult<Vec<Scalar>>,
274 a: Scalar,
275 b: Scalar,
276 components: usize,
277) -> GeopResult<Panel> {
278 let half = b.sub(a).div(Scalar::TWO)?;
279 let center = a.add(half);
280 let mut kronrod = vec![Scalar::ZERO; components];
281 let mut gauss = vec![Scalar::ZERO; components];
282 let mut absolute = vec![0.0; components];
283 let mut add = |values: &[Scalar], k: usize| -> GeopResult<()> {
284 if values.len() != components {
285 return Err(GeopError::new(format!(
286 "integrate: the integrand gave {} components, {components} expected",
287 values.len()
288 )));
289 }
290 let wk = around(WGK[k]).mul(half);
291 let wg = (k % 2 == 1).then(|| around(WG[k / 2]).mul(half));
293 for (c, &value) in values.iter().enumerate() {
294 kronrod[c] = kronrod[c].add(wk.mul(value));
295 absolute[c] += WGK[k] * half.to_f64().abs() * value.to_f64().abs();
296 if let Some(wg) = wg {
297 gauss[c] = gauss[c].add(wg.mul(value));
298 }
299 }
300 Ok(())
301 };
302 for (k, &node) in XGK.iter().enumerate() {
303 if node == 0.0 {
304 add(&f(center)?, k)?;
305 continue;
306 }
307 let offset = half.mul(around(node));
308 add(&f(center.sub(offset))?, k)?;
309 add(&f(center.add(offset))?, k)?;
310 }
311 let error = kronrod
312 .iter()
313 .zip(&gauss)
314 .map(|(k, g)| (k.midpoint().to_f64() - g.midpoint().to_f64()).abs())
315 .collect();
316 let width = kronrod
317 .iter()
318 .zip(&gauss)
319 .map(|(k, g)| k.width().to_f64() + g.width().to_f64())
320 .collect();
321 Ok(Panel {
322 a,
323 b,
324 kronrod,
325 error,
326 absolute,
327 width,
328 })
329}
330
331pub fn integrate(
340 mut f: impl FnMut(Scalar) -> GeopResult<Vec<Scalar>>,
341 breaks: &[Scalar],
342 components: usize,
343 quadrature: &Quadrature,
344) -> GeopResult<Integral> {
345 let mut evaluations = 0;
346 let mut f = |x: Scalar| {
347 evaluations += 1;
348 f(x)
349 };
350 if breaks.len() < 2 {
351 return Ok(Integral {
352 value: vec![Scalar::ZERO; components],
353 converged: true,
354 evaluations: 0,
355 });
356 }
357 let mut panels = breaks
358 .windows(2)
359 .map(|w| panel(&mut f, w[0], w[1], components))
360 .collect::<GeopResult<Vec<_>>>()?;
361 let total = |panels: &[Panel], pick: fn(&Panel) -> &Vec<f64>| -> Vec<f64> {
362 (0..components)
363 .map(|c| panels.iter().map(|p| pick(p)[c]).sum())
364 .collect()
365 };
366 let converged = loop {
367 let scale = total(&panels, |p| &p.absolute);
368 let error = total(&panels, |p| &p.error);
369 let width = total(&panels, |p| &p.width);
374 let pending: Vec<usize> = (0..components)
375 .filter(|&c| error[c] > quadrature.relative_tolerance * scale[c] && error[c] > width[c])
376 .collect();
377 if pending.is_empty() {
378 break true;
379 }
380 if panels.len() >= quadrature.max_panels {
381 break false;
382 }
383 let worst = |p: &Panel| {
390 pending
391 .iter()
392 .filter(|&&c| scale[c] > 0.0)
393 .map(|&c| p.error[c] / scale[c])
394 .fold(0.0, f64::max)
395 };
396 let (index, _) = panels
397 .iter()
398 .enumerate()
399 .map(|(i, p)| (i, worst(p)))
400 .max_by(|a, b| a.1.total_cmp(&b.1))
401 .expect("there is a panel");
402 let Panel { a, b, .. } = panels.swap_remove(index);
403 let mid = a.add(b).div(Scalar::TWO)?.sharpen();
404 panels.push(panel(&mut f, a, mid, components)?);
405 panels.push(panel(&mut f, mid, b, components)?);
406 };
407 let error = total(&panels, |p| &p.error);
408 let value = (0..components)
409 .map(|c| {
410 let sum = panels
411 .iter()
412 .fold(Scalar::ZERO, |acc, p| acc.add(p.kronrod[c]));
413 sum.add(Scalar::from_f64(-error[c]).union(Scalar::from_f64(error[c])))
414 })
415 .collect();
416 Ok(Integral {
417 value,
418 converged,
419 evaluations,
420 })
421}
422
423pub fn integrate_polynomial(
433 mut f: impl FnMut(Scalar) -> GeopResult<Vec<Scalar>>,
434 breaks: &[Scalar],
435 components: usize,
436 degree: usize,
437) -> GeopResult<Integral> {
438 let Some(rule) = GAUSS_LEGENDRE.get(degree / 2) else {
439 return Err(GeopError::new(format!(
440 "integrate_polynomial: no rule for degree {degree}, at most \
441 {MAX_POLYNOMIAL_DEGREE}"
442 )));
443 };
444 let mut value = vec![Scalar::ZERO; components];
445 let mut evaluations = 0;
446 for w in breaks.windows(2) {
447 let half = w[1].sub(w[0]).div(Scalar::TWO)?;
448 let center = w[0].add(half);
449 let mut add = |x: Scalar, weight: f64| -> GeopResult<()> {
450 let values = f(x)?;
451 evaluations += 1;
452 if values.len() != components {
453 return Err(GeopError::new(format!(
454 "integrate_polynomial: the integrand gave {} components, {components} \
455 expected",
456 values.len()
457 )));
458 }
459 let weight = around(weight).mul(half);
460 for (sum, v) in value.iter_mut().zip(values) {
461 *sum = sum.add(weight.mul(v));
462 }
463 Ok(())
464 };
465 for &(node, weight) in rule.iter() {
466 if node == 0.0 {
467 add(center, weight)?;
468 continue;
469 }
470 let offset = half.mul(around(node));
471 add(center.sub(offset), weight)?;
472 add(center.add(offset), weight)?;
473 }
474 }
475 Ok(Integral {
476 value,
477 converged: true,
478 evaluations,
479 })
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485 use crate::scalars::Scalar;
486
487 #[test]
493 fn halving_follows_the_components_not_converged() {
494 type Scalar = crate::scalars::Scalar;
495 let f = |x: Scalar| {
496 let t = x.to_f64();
497 let peak = 1.0 / (1.0 + 1e4 * (t + 0.5) * (t + 0.5));
498 let noise = if t > 0.0 {
499 1e-18 * (1e7 * t).sin()
500 } else {
501 0.0
502 };
503 Ok(vec![
504 Scalar::from_f64(peak),
505 Scalar::from_f64(noise - 1e-15).union(Scalar::from_f64(noise + 1e-15)),
506 ])
507 };
508 let quadrature = Quadrature {
509 relative_tolerance: 1e-10,
510 max_panels: 64,
511 };
512 let integral =
513 integrate(f, &[Scalar::from_f64(-1.0), Scalar::ONE], 2, &quadrature).unwrap();
514 assert!(integral.converged, "{integral:?}");
515 let exact = (50.0_f64.atan() + 150.0_f64.atan()) / 100.0;
517 assert!(
518 integral.value[0].could_be_equal(Scalar::from_f64(exact)),
519 "{integral:?}"
520 );
521 }
522
523 #[test]
524 fn polynomials_are_exact() {
525 let f = |x: Scalar| {
527 Ok(vec![
528 x.mul(x)
529 .mul(x)
530 .mul(x)
531 .mul(x)
532 .sub(Scalar::TWO.mul(x).mul(x))
533 .add(Scalar::ONE),
534 ])
535 };
536 let integral =
537 integrate(f, &[Scalar::ZERO, Scalar::TWO], 1, &Quadrature::default()).unwrap();
538 assert!(integral.converged);
539 let exact = Scalar::from_ratio(22, 3).unwrap();
540 assert!(
541 integral.value[0].could_be_equal(exact),
542 "{:?}",
543 integral.value[0]
544 );
545 assert!(
546 integral.value[0].width().to_f64() < 1e-6,
547 "{:?}",
548 integral.value[0]
549 );
550 }
551
552 #[test]
555 fn smooth_functions_converge_and_are_enclosed() {
556 use crate::scalars::Scalar;
557 let f = |x: Scalar| Ok(vec![Scalar::ONE.div(Scalar::ONE.add(x.mul(x)))?]);
559 let integral =
560 integrate(f, &[Scalar::ZERO, Scalar::ONE], 1, &Quadrature::default()).unwrap();
561 assert!(integral.converged);
562 assert!(integral.value[0].could_be_equal(Scalar::PI.div(Scalar::from_f64(4.0)).unwrap()));
563 let f = |x: Scalar| Ok(vec![x.sqrt()?]);
566 let integral =
567 integrate(f, &[Scalar::ZERO, Scalar::ONE], 1, &Quadrature::default()).unwrap();
568 assert!(integral.value[0].could_be_equal(Scalar::from_ratio(2, 3).unwrap()));
569 assert!(
570 integral.value[0].width().to_f64() < 1e-6,
571 "{:?}",
572 integral.value[0]
573 );
574 }
575
576 #[test]
580 fn gauss_legendre_is_exact() {
581 for degree in 0..=MAX_POLYNOMIAL_DEGREE {
582 let f = |x: Scalar| {
583 Ok((0..=degree)
584 .map(|k| (0..k).fold(Scalar::ONE, |p, _| p.mul(x)))
585 .collect())
586 };
587 let breaks = [Scalar::from_f64(-1.0), Scalar::from_f64(0.25), Scalar::ONE];
588 let integral = integrate_polynomial(f, &breaks, degree + 1, degree).unwrap();
589 assert!(integral.converged);
590 assert_eq!(integral.evaluations, 2 * (degree / 2 + 1));
591 for (k, value) in integral.value.iter().enumerate() {
592 let exact = if k % 2 == 0 {
594 Scalar::from_ratio(2, k as i64 + 1).unwrap()
595 } else {
596 Scalar::ZERO
597 };
598 assert!(
599 value.could_be_equal(exact),
600 "x^{k}, degree {degree}: {value:?}"
601 );
602 assert!(value.width().to_f64() < 1e-6, "x^{k}: {value:?}");
603 }
604 }
605 let too_high = integrate_polynomial(
606 |_: Scalar| Ok(vec![Scalar::ONE]),
607 &[Scalar::ZERO, Scalar::ONE],
608 1,
609 MAX_POLYNOMIAL_DEGREE + 1,
610 );
611 assert!(too_high.is_err());
612 }
613
614 #[test]
616 fn a_spent_budget_is_reported() {
617 use crate::scalars::Scalar;
618 let f = |x: Scalar| Ok(vec![x.sqrt()?]);
619 let tight = Quadrature {
620 relative_tolerance: 1e-14,
621 max_panels: 2,
622 };
623 let integral = integrate(f, &[Scalar::ZERO, Scalar::ONE], 1, &tight).unwrap();
624 assert!(!integral.converged);
625 assert!(integral.value[0].could_be_equal(Scalar::from_ratio(2, 3).unwrap()));
626 }
627}