Skip to main content

comnum_math/
search.rs

1//! One-dimensional searches: bisection, regula falsi and golden-section maximisation,
2//! the largest element of a sequence, and the greatest common divisor.
3//!
4//! # Model
5//!
6//! [`bisect`] halves a bracket $[a, b]$ of a predicate that holds below a threshold
7//! $x^{\star}$ and fails above it: at every step, with $c = \frac{1}{2}(a + b)$,
8//!
9//! $$
10//! [a, b] \leftarrow \begin{cases} [c, b] & \text{if the predicate holds at } c, \\\\
11//! [a, c] & \text{otherwise,} \end{cases}
12//! $$
13//!
14//! and it returns the middle of the last bracket, within $2^{-(n+1)}(b - a)$ of
15//! $x^{\star}$ after $n$ steps.
16//!
17//! [`regula_falsi`] finds a root of a continuous function $f$ whose values at the ends of
18//! the bracket have opposite signs, by the Illinois variant of the false position
19//! (Dowell & Jarratt 1971): the next point is the root of the chord,
20//!
21//! $$
22//! c = \frac{a f(b) - b f(a)}{f(b) - f(a)},
23//! $$
24//!
25//! which replaces the end of the same sign; when the same end is replaced twice in a row,
26//! the value kept at the other end is halved, which restores a superlinear convergence.
27//!
28//! [`golden_max`] shrinks a bracket of a unimodal function by the golden ratio
29//! $\varphi^{-1} = (\sqrt{5} - 1)/2$ at every step, with the two interior points
30//! $c = b - \varphi^{-1}(b - a)$ and $d = a + \varphi^{-1}(b - a)$; one evaluation per
31//! step, the bracket length multiplied by $\varphi^{-n}$ after $n$ steps (Kiefer 1953).
32//!
33//! They keep the order of their operations (`0.5 * (lo + hi)`, no `f64::midpoint`;
34//! `docs/adr/0005-lint-exceptions.md`), so that the published values computed with them
35//! do not change.
36//!
37//! # Examples
38//! ```
39//! use comnum_math::{bisect, gcd, golden_max};
40//! // √2: x² < 2 below the root
41//! let r = bisect(|x| x * x < 2.0, 0.0, 2.0, 60);
42//! assert!((r - 2f64.sqrt()).abs() < 1e-15);
43//! // the maximum of 1 - (x - 0.3)² on [0, 1]
44//! let x = golden_max(|x| 1.0 - (x - 0.3) * (x - 0.3), 0.0, 1.0, 60);
45//! assert!((x - 0.3).abs() < 1e-8);
46//! assert_eq!(gcd(12, 18), 6);
47//! ```
48//!
49//! # References
50//!
51//! 1. J. Kiefer, "Sequential minimax search for a maximum," *Proc. Amer. Math. Soc.*,
52//!    4(3), 502–506, 1953.
53//! 2. W. H. Press, S. A. Teukolsky, W. T. Vetterling and B. P. Flannery, *Numerical
54//!    Recipes*, 3rd ed., Cambridge University Press, 2007, Sec. 9.1 (bisection) and
55//!    Sec. 10.2 (golden section search).
56//! 3. M. Dowell and P. Jarratt, "A modified regula falsi method for computing the root of
57//!    an equation," *BIT* 11(2), 168–174, 1971.
58
59/// The threshold of a predicate by `iters` bisection steps of the bracket `[lo, hi]`:
60/// `below(x)` must hold below the threshold and fail above it (see the module).
61///
62/// # Examples
63/// ```
64/// use comnum_math::bisect;
65/// // the median of the exponential distribution: 1 - e^{-x} = 1/2
66/// let m = bisect(|x| 1.0 - (-x).exp() < 0.5, 0.0, 10.0, 100);
67/// assert!((m - 2f64.ln()).abs() < 1e-14);
68/// ```
69pub fn bisect(mut below: impl FnMut(f64) -> bool, lo: f64, hi: f64, iters: usize) -> f64 {
70    let (mut lo, mut hi) = (lo, hi);
71    for _ in 0..iters {
72        let mid = 0.5 * (lo + hi);
73        if below(mid) {
74            lo = mid;
75        } else {
76            hi = mid;
77        }
78    }
79    0.5 * (lo + hi)
80}
81
82/// A root of `f` between `a` and `b`, where `f` takes values of opposite signs, by at most
83/// `iters` steps of the Illinois regula falsi (see the module): the first point $c$ of
84/// $|f(c)| <$ `f_tol`, or the point computed when the bracket is shorter than `x_tol`;
85/// the middle of the bracket after `iters` steps.
86///
87/// # Examples
88/// ```
89/// use comnum_math::search::regula_falsi;
90/// let r = regula_falsi(|x| x * x * x - 2.0, 0.0, 2.0, 1e-12, 1e-14, 100);
91/// assert!((r - 2f64.cbrt()).abs() < 1e-12);
92/// ```
93pub fn regula_falsi(mut f: impl FnMut(f64) -> f64, a: f64, b: f64, x_tol: f64, f_tol: f64, iters: usize) -> f64 {
94    let (mut a, mut b) = (a, b);
95    let (mut fa, mut fb) = (f(a), f(b));
96    // the end replaced at the previous step: -1 for a, 1 for b
97    let mut side = 0;
98    for _ in 0..iters {
99        let c = (a * fb - b * fa) / (fb - fa);
100        let fc = f(c);
101        if fc.abs() < f_tol || (b - a).abs() < x_tol {
102            return c;
103        }
104        if (fc < 0.0) == (fa < 0.0) {
105            (a, fa) = (c, fc);
106            if side == -1 {
107                fb /= 2.0;
108            }
109            side = -1;
110        } else {
111            (b, fb) = (c, fc);
112            if side == 1 {
113                fa /= 2.0;
114            }
115            side = 1;
116        }
117    }
118    0.5 * (a + b)
119}
120
121/// The maximiser of a unimodal function `f` on `[a, b]` by `iters` steps of the golden
122/// section search (see the module): the middle of the last bracket.
123///
124/// # Examples
125/// ```
126/// use comnum_math::golden_max;
127/// // x e^{-x} is largest at x = 1
128/// let x = golden_max(|x| x * (-x).exp(), 0.0, 5.0, 80);
129/// assert!((x - 1.0).abs() < 1e-7);
130/// ```
131pub fn golden_max(mut f: impl FnMut(f64) -> f64, a: f64, b: f64, iters: usize) -> f64 {
132    let gr = (5f64.sqrt() - 1.0) / 2.0;
133    let (mut a, mut b) = (a, b);
134    let mut c = b - gr * (b - a);
135    let mut d = a + gr * (b - a);
136    let (mut fc, mut fd) = (f(c), f(d));
137    for _ in 0..iters {
138        if fc > fd {
139            b = d;
140            d = c;
141            fd = fc;
142            c = b - gr * (b - a);
143            fc = f(c);
144        } else {
145            a = c;
146            c = d;
147            fc = fd;
148            d = a + gr * (b - a);
149            fd = f(d);
150        }
151    }
152    0.5 * (a + b)
153}
154
155/// The greatest common divisor of `a` and `b` (Euclid's algorithm; `gcd(a, 0) = a`).
156///
157/// # Examples
158/// ```
159/// use comnum_math::gcd;
160/// assert_eq!(gcd(255, 11), 1);
161/// assert_eq!(gcd(0, 7), 7);
162/// ```
163pub fn gcd(a: usize, b: usize) -> usize {
164    let (mut a, mut b) = (a, b);
165    while b != 0 {
166        (a, b) = (b, a % b);
167    }
168    a
169}
170
171/// The index and the value of the largest element of `values`, the first one on ties
172/// (`None` for an empty sequence or one of NaN only; NaN elements are skipped): the
173/// decision of the detectors that pick the strongest candidate.
174///
175/// # Examples
176/// ```
177/// use comnum_math::argmax;
178/// assert_eq!(argmax([0.5, 2.0, -1.0, 2.0]), Some((1, 2.0)));
179/// assert_eq!(argmax([f64::NAN, -3.0]), Some((1, -3.0)));
180/// assert_eq!(argmax(std::iter::empty()), None);
181/// ```
182pub fn argmax(values: impl IntoIterator<Item = f64>) -> Option<(usize, f64)> {
183    let mut best: Option<(usize, f64)> = None;
184    for (i, v) in values.into_iter().enumerate() {
185        if !v.is_nan() && best.is_none_or(|(_, b)| v > b) {
186            best = Some((i, v));
187        }
188    }
189    best
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    /// The folds the detectors wrote before they called `argmax`, with an initial value
197    /// that no element beats: the same index on ties and on every sequence.
198    #[test]
199    fn argmax_is_the_former_fold() {
200        let fold = |v: &[f64]| {
201            v.iter().enumerate().fold((0, f64::NEG_INFINITY), |b, (k, &x)| if x > b.1 { (k, x) } else { b })
202        };
203        let mut r = comnum_core::rng::Rng::new(3);
204        for n in 1..40 {
205            let v: Vec<f64> = (0..n).map(|_| (r.gaussian() * 3.0).round()).collect();
206            assert_eq!(argmax(v.iter().copied()), Some(fold(&v)), "{v:?}");
207        }
208        assert_eq!(argmax([f64::NAN, f64::NAN]), None);
209        assert_eq!(argmax([f64::NEG_INFINITY]), Some((0, f64::NEG_INFINITY)));
210    }
211
212    #[test]
213    fn bisection_and_golden_section_reach_their_targets() {
214        assert_eq!(bisect(|_| true, 0.0, 1.0, 0), 0.5);
215        assert!(bisect(|_| true, 0.0, 1.0, 60) > 1.0 - 1e-15);
216        let x = golden_max(|x| -(x - 2.5).abs(), -10.0, 10.0, 100);
217        assert!((x - 2.5).abs() < 1e-9);
218        assert_eq!((gcd(1, 1), gcd(36, 24), gcd(17, 0)), (1, 12, 17));
219    }
220
221    #[test]
222    fn regula_falsi_finds_the_roots_of_increasing_and_decreasing_functions() {
223        let up = regula_falsi(|x| x.exp() - 3.0, 0.0, 5.0, 1e-13, 1e-15, 200);
224        assert!((up - 3f64.ln()).abs() < 1e-12);
225        let down = regula_falsi(|x| 1.0 - x * x, 0.0, 4.0, 1e-13, 1e-15, 200);
226        assert!((down - 1.0).abs() < 1e-12);
227        // no step: the middle of the bracket
228        assert_eq!(regula_falsi(|x| x, -1.0, 3.0, 0.0, 0.0, 0), 1.0);
229    }
230}