qualia_core_db/solvers/optimization/
metaheuristics.rs1pub 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
30pub 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(¤t);
41 for _ in 0..max_iter {
42 let mut best: Option<(S, f64)> = None;
43 for cand in neighbors(¤t) {
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, }
56 }
57 (current, current_val)
58}
59
60pub 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(¤t);
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(¤t, &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
100pub 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 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 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 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 let f = |s: &Vec<f64>| {
199 let x = s[0];
200 0.1 * (x * x - 4.0).powi(2) - 0.5 * x };
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 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}