Skip to main content

ogeom_math/
integrate.rs

1//! Numerical integration.
2//!
3//! Gauss-Legendre quadrature, applied adaptively. A kernel integrates for arc
4//! length, for area and volume over a parametric patch, and for the moments
5//! that follow from those: all integrands that are smooth almost everywhere
6//! and awkward exactly where a feature is.
7//!
8//! # Why Gauss rather than Simpson
9//!
10//! An `n`-point Gauss rule is exact for polynomials of degree `2n - 1`, against
11//! Simpson's `3` for the same three evaluations. Since a B-spline *is* a
12//! piecewise polynomial and the speed of a curve along one is a square root of
13//! a polynomial, the integrands here are close enough to polynomial that the
14//! difference is large. Ten points integrate most spans to machine precision in
15//! one go.
16//!
17//! # Why adaptive on top
18//!
19//! A fixed rule cannot report its own error. A Gauss-Kronrod pair can: the
20//! fifteen-point Kronrod rule shares the seven-point Gauss rule's nodes, so
21//! one pass over an interval yields two estimates, and their difference is
22//! a fair measure of what the better one still misses. Where that is inside
23//! the budget the estimate has converged *there*; where it is not, only that
24//! part is subdivided. So a curve that is straight over most of its length
25//! and sharp in one place costs what the sharp place costs, not what the
26//! sharp place would cost applied everywhere. The same pair, in tensor
27//! form, integrates over a rectangle of parameters the same way.
28//!
29//! The recursion is bounded, and a result that hit the bound says so rather
30//! than being returned as though it converged.
31
32use ogeom_core::{OgeomResult, ogeom_bail};
33
34/// Nodes of the ten-point Gauss-Legendre rule on `[-1, 1]`, positive half.
35///
36/// The rule is symmetric, so the negative nodes are these negated and the
37/// weights are shared. Values are the standard ones (roots of the degree-ten
38/// Legendre polynomial), quoted to full `f64` precision.
39const 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
47/// Weights matching [`NODES`].
48const 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
56/// Nodes of the fifteen-point Kronrod rule on `[-1, 1]`, positive half,
57/// outermost first. Every other one from the second is a node of the
58/// seven-point Gauss rule it extends.
59const 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
70/// Weights matching [`KRONROD_NODES`].
71const 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
82/// Weights of the seven-point Gauss rule, at the Kronrod nodes of odd
83/// index (the second, fourth, sixth and the centre).
84const 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
91/// Nodes the twenty-one-point Kronrod rule adds to the ten-point Gauss rule
92/// on `[-1, 1]`, positive half, centre last.
93const 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
102/// The twenty-one-point Kronrod weights at [`KRONROD21_NODES`].
103const 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
112/// The twenty-one-point Kronrod weights at the Gauss [`NODES`].
113const 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
121/// The most times [`integrate`] will subdivide one interval.
122///
123/// An integrand that has not converged by here has a singularity rather than a
124/// resolution problem, and the depth limit turns that into a reported failure
125/// instead of an exhausted stack.
126const MAX_DEPTH: u32 = 24;
127
128/// The most times [`integrate_2d`] will halve one cell, either way: a
129/// singular integrand is told about at the bottom rather than pursued.
130const MAX_DEPTH_2D: u32 = 24;
131
132/// Integrate `f` over `[a, b]` with the fixed ten-point rule.
133///
134/// Exact for polynomials up to degree nineteen. No error estimate. For that,
135/// use [`integrate`], which is this applied adaptively.
136///
137/// A reversed interval integrates to the negative, as it should: the rule
138/// carries the sign of `b - a` rather than quietly sorting its arguments.
139pub 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/// The ten-point Gauss-Legendre rule on `[a, b]`: each node with its weight.
151///
152/// For a caller that needs the samples themselves rather than one scalar
153/// integral, such as a sum of several integrands over the same nodes. The
154/// weights carry the sign of `b - a`, as [`gauss_legendre`]'s do.
155#[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/// The ten-point Gauss and twenty-one-point Kronrod pair on `[a, b]`: each
169/// node with its Kronrod weight and its Gauss weight.
170///
171/// The first ten are the nodes of [`gauss_legendre_rule`], in its order and
172/// with its weights as their Gauss weights; the last eleven are the nodes
173/// the Kronrod rule adds, whose Gauss weight is zero. The Kronrod rule is
174/// exact for polynomials up to degree thirty-one, the Gauss rule up to
175/// nineteen, and the gap between the two sums is the usual estimate of
176/// what the Gauss sum still misses. The weights carry the sign of `b - a`.
177#[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
202/// Integrate `f` over `[a, b]` with the seven-point Gauss and fifteen-point
203/// Kronrod pair: the Kronrod value, and the magnitude of its difference
204/// from the Gauss value as the estimate of what it still misses.
205///
206/// Fifteen evaluations, shared. The Kronrod rule is exact for polynomials
207/// up to degree twenty-two, the Gauss rule up to thirteen. Where the two
208/// agree the integrand is polynomial enough that both are right, and the
209/// gap is a fair measure of the error where they are not. A reversed
210/// interval integrates to the negative, as [`gauss_legendre`] does.
211pub 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
231/// Integrate `f` over `[a, b]` to an absolute tolerance.
232///
233/// Subdivides where, and only where, the estimate has not settled, so a
234/// mostly-smooth integrand costs about what the smooth part costs.
235///
236/// # What it will not do
237///
238/// Each half is given half its parent's budget, so the budgets sum to the one
239/// asked for and the result is bounded by it. The cost is that an integrand
240/// with an *infinite derivative* at an endpoint (`sqrt(1 - x^2)` at `x = 1`,
241/// which is a circle's own equation) has a budget shrinking faster than its
242/// error does, and cannot be squeezed arbitrarily. In practice it manages
243/// about `1e-7` on that shape, and lands within `1e-14` when it does. Asked for
244/// `1e-8` it reports that it could not rather than returning the number it
245/// reached.
246///
247/// This does not affect arc length, which is what the routine is mostly for:
248/// the speed along a curve is `|c'(u)|`, smooth and positive wherever the
249/// parameterization is regular. A singularity here means a genuinely singular
250/// parameterization, which is worth being told about.
251///
252/// # Errors
253///
254/// [`OgeomError::Domain`](ogeom_core::OgeomError::Domain) if the interval is not finite;
255/// [`OgeomError::NotDone`](ogeom_core::OgeomError::NotDone) if some part of it did not
256/// converge within the depth limit. That is reported rather than returned as a
257/// number, because an integral that silently stopped improving is the shape of
258/// answer that gets trusted.
259pub 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
277/// One step of the adaptive halving.
278fn 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    // Nothing left here worth resolving. A Gauss rule does not converge in
290    // *relative* terms against a square-root singularity (the error stays a
291    // roughly fixed fraction of the contribution), so an interval containing
292    // one can fail the comparison above at every depth, while the quantity it
293    // is failing about shrinks to nothing. Once the total magnitude on this
294    // interval is inside the budget, no amount of refining it can move the
295    // answer by more than the budget, so refining it is not worth doing.
296    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    // Half the tolerance to each half, so the halves' errors sum to the whole's
308    // rather than each being allowed the whole budget.
309    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
314/// Integrate `f(u, v)` over the rectangle `[a, b] x [c, d]` to an absolute
315/// tolerance: a patch's area, its moments, anything spread over a chart.
316///
317/// The Gauss-Kronrod pair in tensor form: one pass over a cell is fifteen
318/// by fifteen evaluations and yields the Kronrod estimate and, from the
319/// same values, the estimate with the Gauss rule in `u` and the one with
320/// it in `v`, each gap the error attributed to that direction. A cell whose
321/// worse gap is inside its budget is done. One whose is not is halved
322/// *along the rougher direction*, each half given half the budget, so the
323/// cells' errors sum to the whole's and a ridge running across the chart
324/// (a crease in an integrand, a seam) costs a line of cells rather than a
325/// field of them.
326///
327/// # Errors
328///
329/// [`OgeomError::Domain`](ogeom_core::OgeomError::Domain) if a bound is not
330/// finite or the tolerance is not positive;
331/// [`OgeomError::NotDone`](ogeom_core::OgeomError::NotDone) if some cell did
332/// not converge within the depth limit.
333pub 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
353/// The tensor Gauss-Kronrod pair over one cell: the Kronrod value, and the
354/// gaps to the estimates with the Gauss rule in `u` and in `v`.
355fn 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    // Each node's two mirror images, or the centre once.
363    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
400/// One step of the adaptive quartering.
401fn 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        // Degree nineteen is what a ten-point rule integrates exactly, and
443        // "exactly" here should mean to rounding, not to a tolerance.
444        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        // The quarter circle. Its integrand's derivative blows up at x = 1, so
466        // the halved budget shrinks faster than the error there does and the
467        // method has a floor. Where it converges it is far better than asked.
468        // Where it does not, it says so instead of returning what it reached.
469        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    /// The pair's constants, checked by what they must do: the weights sum
479    /// to the interval, the Gauss rule is exact to degree thirteen and the
480    /// Kronrod rule to twenty-two, and the gap between them is zero where
481    /// both are exact.
482    #[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    /// The ten- and twenty-one-point pair, checked by what it must do: its
500    /// Gauss half is the ten-point rule, the Kronrod sum is exact to degree
501    /// thirty-one and the Gauss sum to nineteen and no further, on a panel
502    /// with an end at the origin, about which no power is even or odd.
503    #[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    /// A rectangle of parameters: exact for a product of polynomials in one
522    /// pass, the area of a sphere from its own chart, and a ridge that
523    /// forces quartering on one side of the cell only.
524    #[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        // ∫|u − 0.3| du over [0, 1] = 0.045 + 0.245 = 0.29, and ∫ v dv = 0.5.
533        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        // Rather than being quietly sorted, which would make an arc length
544        // computed backwards come out positive and hide the caller's mistake.
545        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        // 1/x towards zero has no finite integral. Returning a large number
558        // would be worse than failing, because a caller cannot tell it apart
559        // from a genuinely large answer.
560        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}