geop_core_math/solvers/probe.rs
1//! Probing a sequence of trial values until one decides: a step halved
2//! until a point stepped off a curve lands where it should, a reach
3//! doubled until it brackets a crossing, a point marched on until it
4//! passes one, a ray cast in one direction after another until one is
5//! clear of every vertex.
6//!
7//! What is probed is a free choice — how far to step, how far to reach —
8//! and what the probe finds there is the caller's to judge. The solver
9//! promises only the sequence: `start`, then each value `next` makes of
10//! the one before, at most `most` of them, each asked once and in order.
11//! It answers `None` when none decided, never the last value tried.
12
13use crate::{geop_error::GeopError, scalars::Scalar};
14
15/// What a probe found at one value of the sequence.
16pub enum Probe<T> {
17 /// It decides: the search ends with this.
18 Found(T),
19 /// It does not: go on to the next value.
20 Next,
21 /// It does not, and no later value will: the search ends with nothing.
22 Stop,
23}
24
25/// Asks `at` for `start` and each value `next` makes of the one before, at
26/// most `most` of them, until one is [`Probe::Found`] or [`Probe::Stop`].
27/// `next` is asked only for a value that will be probed. `at` may keep
28/// state of its own (what the probes so far said), since every value is
29/// asked once, in order.
30pub fn probe<X, T, E>(
31 start: X,
32 most: usize,
33 mut next: impl FnMut(&X) -> Result<X, E>,
34 mut at: impl FnMut(&X) -> Result<Probe<T>, E>,
35) -> Result<Option<T>, E> {
36 let mut x = start;
37 for k in 0..most {
38 match at(&x)? {
39 Probe::Found(found) => return Ok(Some(found)),
40 Probe::Stop => return Ok(None),
41 Probe::Next => {}
42 }
43 if k + 1 < most {
44 x = next(&x)?;
45 }
46 }
47 Ok(None)
48}
49
50/// [`probe`] with a step halved each time: `start`, `start / 2`, … — at
51/// most `most` of them.
52pub fn halve<T, E: From<GeopError>>(
53 start: Scalar,
54 most: usize,
55 at: impl FnMut(&Scalar) -> Result<Probe<T>, E>,
56) -> Result<Option<T>, E> {
57 probe(start, most, |s| Ok(s.div(Scalar::TWO)?), at)
58}
59
60/// [`probe`] over attempts `0, 1, 2, …` — at most `most` of them — for a
61/// sequence `at` makes itself (the `k`-th of a set of directions, the next
62/// draw of a seeded generator).
63pub fn attempts<T, E>(
64 most: usize,
65 at: impl FnMut(&usize) -> Result<Probe<T>, E>,
66) -> Result<Option<T>, E> {
67 probe(0, most, |k| Ok(k + 1), at)
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 /// Halving from 1 finds the first step below a third — and asks every
75 /// step before it, in order, and none after.
76 #[test]
77 fn halving_finds_the_first_step_that_decides() {
78 let mut asked = Vec::new();
79 let found = halve::<_, GeopError>(Scalar::ONE, 10, |s| {
80 asked.push(*s);
81 Ok(if s.to_f64() < 1.0 / 3.0 {
82 Probe::Found(*s)
83 } else {
84 Probe::Next
85 })
86 })
87 .unwrap();
88 // Halved in interval arithmetic, each step encloses the exact one.
89 let exact = [1.0, 0.5, 0.25];
90 assert_eq!(asked.len(), exact.len());
91 for (a, e) in asked.iter().zip(exact) {
92 assert!(a.could_be_equal(Scalar::from_f64(e)), "{a:?}");
93 }
94 assert!(found.is_some_and(|f| f.could_be_equal(Scalar::from_f64(0.25))));
95 }
96
97 /// A sequence that never decides reports nothing — not its last value —
98 /// and makes no value past the last it may ask.
99 #[test]
100 fn a_probe_that_never_decides_finds_nothing() {
101 let mut made = 0;
102 let found = probe::<f64, f64, ()>(
103 1.0,
104 4,
105 |x| {
106 made += 1;
107 Ok(x * 2.0)
108 },
109 |_| Ok(Probe::Next),
110 )
111 .unwrap();
112 assert_eq!(found, None);
113 assert_eq!(made, 3);
114 }
115
116 /// `Stop` ends the search at once, with nothing.
117 #[test]
118 fn stop_ends_the_search() {
119 let mut asked = 0;
120 let found = probe::<f64, f64, ()>(
121 1.0,
122 100,
123 |x| Ok(x + 1.0),
124 |&x| {
125 asked += 1;
126 Ok(if x >= 3.0 { Probe::Stop } else { Probe::Next })
127 },
128 )
129 .unwrap();
130 assert_eq!((found, asked), (None, 3));
131 }
132
133 /// Attempts are counted from zero, and none past `most`.
134 #[test]
135 fn attempts_count_from_zero() {
136 let first_odd = attempts::<usize, ()>(5, |&k| {
137 Ok(if k % 2 == 1 {
138 Probe::Found(k)
139 } else {
140 Probe::Next
141 })
142 });
143 assert_eq!(first_odd, Ok(Some(1)));
144 let mut asked = Vec::new();
145 let none = attempts::<(), ()>(3, |&k| {
146 asked.push(k);
147 Ok(Probe::Next)
148 });
149 assert_eq!((none, asked), (Ok(None), vec![0, 1, 2]));
150 }
151}