Skip to main content

qualia_core_db/solvers/optimization/
metaheuristics.rs

1//! General-dimension metaheuristic optimizers (CI-SKM ch 4/6) — global / non-convex
2//! search beyond the fixed-`[f64;4]` local solvers in this category. Generic local
3//! search + simulated annealing work over **any** state (continuous vectors or
4//! combinatorial structures via a neighbour closure — the engine ontology alignment
5//! consumes), plus a continuous population optimizer (Artificial Bee Colony).
6//!
7//! All minimize the objective. Kernel-class `Divergent` (branch-heavy search) with
8//! the CPU path always present (§13). Deterministic given the seed.
9
10/// Deterministic RNG (LCG + Box-Muller) so searches are reproducible.
11pub struct Rng(pub u64);
12impl Rng {
13    pub fn unit(&mut self) -> f64 {
14        self.0 = self
15            .0
16            .wrapping_mul(6364136223846793005)
17            .wrapping_add(1442695040888963407);
18        ((self.0 >> 11) as f64) / ((1u64 << 53) as f64)
19    }
20    pub fn below(&mut self, b: usize) -> usize {
21        (self.unit() * b as f64) as usize % b.max(1)
22    }
23    pub fn gaussian(&mut self) -> f64 {
24        let u1 = self.unit().max(1e-12);
25        let u2 = self.unit();
26        (-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos()
27    }
28}
29
30/// Generic hill-climbing: from `initial`, repeatedly move to the best improving
31/// neighbour until none improves (a local minimum) or `max_iter` is reached.
32/// `neighbors` enumerates candidate moves; `objective` is minimized.
33pub fn hill_climbing<S, N, O>(initial: S, neighbors: N, objective: O, max_iter: usize) -> (S, f64)
34where
35    S: Clone,
36    N: Fn(&S) -> Vec<S>,
37    O: Fn(&S) -> f64,
38{
39    let mut current = initial;
40    let mut current_val = objective(&current);
41    for _ in 0..max_iter {
42        let mut best: Option<(S, f64)> = None;
43        for cand in neighbors(&current) {
44            let v = objective(&cand);
45            if v < current_val && best.as_ref().map(|(_, bv)| v < *bv).unwrap_or(true) {
46                best = Some((cand, v));
47            }
48        }
49        match best {
50            Some((s, v)) => {
51                current = s;
52                current_val = v;
53            }
54            None => break, // local optimum
55        }
56    }
57    (current, current_val)
58}
59
60/// Generic simulated annealing: accept worsening moves with probability
61/// `exp(−Δ/T)`, cooling `T ← cooling·T` each step, to escape local minima.
62/// `neighbor` proposes a single random move.
63pub fn simulated_annealing<S, NB, O>(
64    initial: S,
65    neighbor: NB,
66    objective: O,
67    t0: f64,
68    cooling: f64,
69    max_iter: usize,
70    seed: u64,
71) -> (S, f64)
72where
73    S: Clone,
74    NB: Fn(&S, &mut Rng) -> S,
75    O: Fn(&S) -> f64,
76{
77    let mut rng = Rng(seed ^ 0x9E3779B97F4A7C15);
78    let mut current = initial;
79    let mut current_val = objective(&current);
80    let mut best = current.clone();
81    let mut best_val = current_val;
82    let mut t = t0.max(1e-12);
83    for _ in 0..max_iter {
84        let cand = neighbor(&current, &mut rng);
85        let v = objective(&cand);
86        let delta = v - current_val;
87        if delta < 0.0 || rng.unit() < (-delta / t).exp() {
88            current = cand;
89            current_val = v;
90            if current_val < best_val {
91                best = current.clone();
92                best_val = current_val;
93            }
94        }
95        t *= cooling;
96    }
97    (best, best_val)
98}
99
100/// Artificial Bee Colony (continuous): a swarm explores a box `[lower, upper]^d`,
101/// exploiting good food sources and scouting new ones. Minimizes `objective`.
102pub fn artificial_bee_colony<O>(
103    objective: O,
104    lower: &[f64],
105    upper: &[f64],
106    n_bees: usize,
107    max_iter: usize,
108    seed: u64,
109) -> Option<(Vec<f64>, f64)>
110where
111    O: Fn(&[f64]) -> f64,
112{
113    let d = lower.len();
114    if d == 0 || upper.len() != d || n_bees < 2 {
115        return None;
116    }
117    let mut rng = Rng(seed ^ 0xD1B54A32D192ED03);
118    let sample = |rng: &mut Rng| -> Vec<f64> {
119        (0..d)
120            .map(|j| lower[j] + rng.unit() * (upper[j] - lower[j]))
121            .collect()
122    };
123    let mut foods: Vec<Vec<f64>> = (0..n_bees).map(|_| sample(&mut rng)).collect();
124    let mut vals: Vec<f64> = foods.iter().map(|f| objective(f)).collect();
125    let mut trials = vec![0usize; n_bees];
126    let limit = (n_bees * d).max(20);
127
128    let mut best = foods[0].clone();
129    let mut best_val = vals[0];
130    for (i, &v) in vals.iter().enumerate() {
131        if v < best_val {
132            best_val = v;
133            best = foods[i].clone();
134        }
135    }
136
137    for _ in 0..max_iter {
138        // Employed + onlooker phase: perturb one coordinate toward a partner.
139        for i in 0..n_bees {
140            let k = {
141                let mut k = rng.below(n_bees);
142                if k == i {
143                    k = (k + 1) % n_bees;
144                }
145                k
146            };
147            let j = rng.below(d);
148            let phi = 2.0 * rng.unit() - 1.0;
149            let mut cand = foods[i].clone();
150            cand[j] = (cand[j] + phi * (cand[j] - foods[k][j])).clamp(lower[j], upper[j]);
151            let cv = objective(&cand);
152            if cv < vals[i] {
153                foods[i] = cand;
154                vals[i] = cv;
155                trials[i] = 0;
156                if cv < best_val {
157                    best_val = cv;
158                    best = foods[i].clone();
159                }
160            } else {
161                trials[i] += 1;
162            }
163        }
164        // Scout phase: abandon exhausted sources.
165        for i in 0..n_bees {
166            if trials[i] > limit {
167                foods[i] = sample(&mut rng);
168                vals[i] = objective(&foods[i]);
169                trials[i] = 0;
170                if vals[i] < best_val {
171                    best_val = vals[i];
172                    best = foods[i].clone();
173                }
174            }
175        }
176    }
177    Some((best, best_val))
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn abc_minimizes_the_sphere() {
186        // f(x) = Σ x² has its global minimum 0 at the origin.
187        let f = |x: &[f64]| x.iter().map(|v| v * v).sum::<f64>();
188        let (x, val) =
189            artificial_bee_colony(f, &[-5.0, -5.0, -5.0], &[5.0, 5.0, 5.0], 30, 300, 1).unwrap();
190        assert!(val < 1e-2, "ABC sphere min {val}");
191        assert!(x.iter().all(|&xi| xi.abs() < 0.2));
192    }
193
194    #[test]
195    fn sa_escapes_a_local_minimum() {
196        // A 1-D double well with a *modest* barrier (×0.1 so the peak at x=0 is
197        // ~1.6, crossable by annealing): global min near x=2, local near x=-2.
198        let f = |s: &Vec<f64>| {
199            let x = s[0];
200            0.1 * (x * x - 4.0).powi(2) - 0.5 * x // right well (x≈2) is global
201        };
202        let neighbor = |s: &Vec<f64>, rng: &mut Rng| vec![s[0] + 0.5 * rng.gaussian()];
203        let (best, _) = simulated_annealing(vec![-2.0], neighbor, f, 5.0, 0.997, 8000, 3);
204        assert!(
205            best[0] > 0.0,
206            "SA should find the right well, got {}",
207            best[0]
208        );
209    }
210
211    #[test]
212    fn hill_climbing_reaches_a_discrete_optimum() {
213        // Combinatorial: maximize the number of 1s in a bit-vector (minimize its
214        // negative) by single-bit flips — proves the generic combinatorial path.
215        let objective = |s: &Vec<bool>| -(s.iter().filter(|&&b| b).count() as f64);
216        let neighbors = |s: &Vec<bool>| {
217            (0..s.len())
218                .map(|i| {
219                    let mut c = s.clone();
220                    c[i] = !c[i];
221                    c
222                })
223                .collect()
224        };
225        let (best, val) = hill_climbing(vec![false; 8], neighbors, objective, 100);
226        assert!(best.iter().all(|&b| b), "should turn on all bits");
227        assert_eq!(val, -8.0);
228    }
229
230    #[test]
231    fn guards() {
232        assert!(artificial_bee_colony(|_| 0.0, &[], &[], 10, 10, 0).is_none());
233    }
234}