1#![allow(clippy::needless_range_loop)]
45
46use hyperopt_core::{Direction, Distribution, Sampler, StudyState, Trial, Value};
47use rand::rngs::StdRng;
48use rand::{RngExt, SeedableRng};
49use std::collections::HashMap;
50
51use crate::random::sample_value;
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum BoundHandling {
57 Reflect,
61 Clamp,
64}
65
66impl BoundHandling {
67 fn repair(self, v: f64) -> f64 {
69 match self {
70 BoundHandling::Clamp => v.clamp(0.0, 1.0),
71 BoundHandling::Reflect => {
72 let t = v.rem_euclid(2.0);
74 if t > 1.0 {
75 2.0 - t
76 } else {
77 t
78 }
79 }
80 }
81 }
82}
83
84pub struct CmaEsSampler {
92 seed: u64,
93 rng: StdRng,
94 popsize: Option<usize>,
95 n_startup_trials: Option<usize>,
96 sigma0: f64,
97 bound_handling: BoundHandling,
98 cache: HashMap<usize, HashMap<String, Value>>,
100}
101
102impl CmaEsSampler {
103 pub fn new() -> Self {
105 let mut seeder = rand::rng();
106 Self::seeded(seeder.random())
107 }
108
109 pub fn seeded(seed: u64) -> Self {
111 CmaEsSampler {
112 seed,
113 rng: StdRng::seed_from_u64(seed),
114 popsize: None,
115 n_startup_trials: None,
116 sigma0: 0.2,
117 bound_handling: BoundHandling::Reflect,
118 cache: HashMap::new(),
119 }
120 }
121
122 pub fn population_size(mut self, lambda: usize) -> Self {
124 self.popsize = Some(lambda.max(2));
125 self
126 }
127
128 pub fn n_startup_trials(mut self, n: usize) -> Self {
132 self.n_startup_trials = Some(n);
133 self
134 }
135
136 pub fn sigma0(mut self, sigma0: f64) -> Self {
139 self.sigma0 = sigma0;
140 self
141 }
142
143 pub fn bound_handling(mut self, handling: BoundHandling) -> Self {
145 self.bound_handling = handling;
146 self
147 }
148
149 fn search_space(study_state: &StudyState) -> Vec<(String, Distribution)> {
152 let mut per_name: HashMap<String, Distribution> = HashMap::new();
153 let mut counts: HashMap<String, usize> = HashMap::new();
154 let mut n_completed = 0usize;
155
156 for t in study_state.completed_trials() {
157 n_completed += 1;
158 for p in &t.params {
159 if is_numeric(&p.distribution) {
160 per_name.entry(p.name.clone()).or_insert_with(|| p.distribution.clone());
161 *counts.entry(p.name.clone()).or_insert(0) += 1;
162 }
163 }
164 }
165
166 let mut space: Vec<(String, Distribution)> = per_name
167 .into_iter()
168 .filter(|(name, _)| counts.get(name).copied().unwrap_or(0) == n_completed)
169 .collect();
170 space.sort_by(|a, b| a.0.cmp(&b.0));
171 space
172 }
173
174 fn candidate_for(
176 &mut self,
177 study_state: &StudyState,
178 trial: &Trial,
179 ) -> Option<HashMap<String, Value>> {
180 if let Some(cached) = self.cache.get(&trial.number) {
181 return Some(cached.clone());
182 }
183
184 let space = Self::search_space(study_state);
185 if space.is_empty() {
186 return None;
187 }
188 let n = space.len();
189
190 let popsize = self.popsize.unwrap_or_else(|| default_popsize(n));
191 let n_startup = self.n_startup_trials.unwrap_or(popsize);
192
193 let mut samples: Vec<(Vec<f64>, f64)> = Vec::new();
196 let mut completed: Vec<&Trial> = study_state.completed_trials().collect();
197 completed.sort_by_key(|t| t.number);
198 for t in &completed {
199 if let Some(x) = encode_vector(&space, t) {
200 let fitness = match study_state.direction() {
201 Direction::Minimize => t.value.unwrap(),
202 Direction::Maximize => -t.value.unwrap(),
203 };
204 samples.push((x, fitness));
205 }
206 }
207
208 if samples.len() < n_startup {
210 return None;
211 }
212
213 let mut engine = Engine::new(n, self.sigma0);
215 for gen in samples.chunks(popsize) {
216 if gen.len() == popsize {
217 engine.tell(gen);
218 }
219 }
220
221 let mut ask_rng = StdRng::seed_from_u64(self.seed ^ (trial.number as u64).wrapping_mul(0x9E3779B97F4A7C15));
224 let raw = engine.ask(&mut ask_rng);
225
226 let mut decoded = HashMap::new();
228 for (i, (name, dist)) in space.iter().enumerate() {
229 let z = self.bound_handling.repair(raw[i]);
230 decoded.insert(name.clone(), decode(dist, z));
231 }
232 self.cache.insert(trial.number, decoded.clone());
233 Some(decoded)
234 }
235}
236
237impl Default for CmaEsSampler {
238 fn default() -> Self {
239 Self::new()
240 }
241}
242
243impl Sampler for CmaEsSampler {
244 fn suggest(
245 &mut self,
246 study_state: &StudyState,
247 trial: &Trial,
248 param_name: &str,
249 distribution: &Distribution,
250 ) -> Value {
251 if is_numeric(distribution) {
253 if let Some(vector) = self.candidate_for(study_state, trial) {
254 if let Some(v) = vector.get(param_name) {
255 if variant_matches(distribution, v) {
258 return v.clone();
259 }
260 }
261 }
262 }
263 sample_value(&mut self.rng, distribution)
264 }
265}
266
267fn is_numeric(dist: &Distribution) -> bool {
268 matches!(
269 dist,
270 Distribution::Uniform { .. }
271 | Distribution::LogUniform { .. }
272 | Distribution::IntUniform { .. }
273 )
274}
275
276fn variant_matches(dist: &Distribution, value: &Value) -> bool {
277 matches!(
278 (dist, value),
279 (Distribution::Uniform { .. }, Value::Float(_))
280 | (Distribution::LogUniform { .. }, Value::Float(_))
281 | (Distribution::IntUniform { .. }, Value::Int(_))
282 )
283}
284
285fn default_popsize(n: usize) -> usize {
287 (4.0 + (3.0 * (n as f64).ln()).floor()) as usize
288}
289
290fn encode_vector(space: &[(String, Distribution)], trial: &Trial) -> Option<Vec<f64>> {
293 let mut out = Vec::with_capacity(space.len());
294 for (name, dist) in space {
295 let v = trial.param_value(name)?;
296 out.push(encode(dist, v)?);
297 }
298 Some(out)
299}
300
301fn encode(dist: &Distribution, value: &Value) -> Option<f64> {
303 match dist {
304 Distribution::Uniform { low, high } => {
305 let x = value.as_float()?;
306 unit(x, *low, *high)
307 }
308 Distribution::LogUniform { low, high } => {
309 let x = value.as_float()?;
310 if *low <= 0.0 || x <= 0.0 {
311 return None;
312 }
313 unit(x.ln(), low.ln(), high.ln())
314 }
315 Distribution::IntUniform { low, high } => {
316 let x = value.as_int()? as f64;
317 unit(x, *low as f64, *high as f64)
318 }
319 Distribution::Categorical { .. } => None,
320 }
321}
322
323fn decode(dist: &Distribution, z: f64) -> Value {
325 match dist {
326 Distribution::Uniform { low, high } => Value::Float(low + z * (high - low)),
327 Distribution::LogUniform { low, high } => {
328 let l = low.ln();
329 let h = high.ln();
330 Value::Float((l + z * (h - l)).exp())
331 }
332 Distribution::IntUniform { low, high } => {
333 let raw = *low as f64 + z * (*high as f64 - *low as f64);
334 Value::Int(raw.round() as i64)
335 }
336 Distribution::Categorical { .. } => Value::Categorical(String::new()),
337 }
338}
339
340fn unit(x: f64, low: f64, high: f64) -> Option<f64> {
341 if high > low {
342 Some(((x - low) / (high - low)).clamp(0.0, 1.0))
343 } else {
344 Some(0.5) }
346}
347
348struct Engine {
350 n: usize,
351 mean: Vec<f64>,
352 sigma: f64,
353 cov: Vec<Vec<f64>>, p_c: Vec<f64>,
355 p_s: Vec<f64>,
356 b: Vec<Vec<f64>>, d: Vec<f64>, generation: usize,
359 mu: usize,
361 weights: Vec<f64>,
362 mu_eff: f64,
363 c_c: f64,
364 c_s: f64,
365 c_1: f64,
366 c_mu: f64,
367 damps: f64,
368 chi_n: f64,
369}
370
371impl Engine {
372 fn new(n: usize, sigma0: f64) -> Self {
373 let lambda = default_popsize(n);
374 let mu = lambda / 2;
375
376 let mut weights: Vec<f64> = (0..mu)
378 .map(|i| ((lambda as f64 + 1.0) / 2.0).ln() - ((i + 1) as f64).ln())
379 .collect();
380 let wsum: f64 = weights.iter().sum();
381 for w in &mut weights {
382 *w /= wsum;
383 }
384 let mu_eff = 1.0 / weights.iter().map(|w| w * w).sum::<f64>();
385
386 let nf = n as f64;
387 let c_c = (4.0 + mu_eff / nf) / (nf + 4.0 + 2.0 * mu_eff / nf);
388 let c_s = (mu_eff + 2.0) / (nf + mu_eff + 5.0);
389 let c_1 = 2.0 / ((nf + 1.3).powi(2) + mu_eff);
390 let c_mu = ((2.0 * (mu_eff - 2.0 + 1.0 / mu_eff) / ((nf + 2.0).powi(2) + mu_eff))
391 .min(1.0 - c_1))
392 .max(0.0);
393 let damps = 1.0
394 + 2.0 * (((mu_eff - 1.0) / (nf + 1.0)).sqrt() - 1.0).max(0.0)
395 + c_s;
396 let chi_n = nf.sqrt() * (1.0 - 1.0 / (4.0 * nf) + 1.0 / (21.0 * nf * nf));
397
398 Engine {
399 n,
400 mean: vec![0.5; n], sigma: sigma0,
402 cov: identity(n),
403 p_c: vec![0.0; n],
404 p_s: vec![0.0; n],
405 b: identity(n),
406 d: vec![1.0; n],
407 generation: 0,
408 mu,
409 weights,
410 mu_eff,
411 c_c,
412 c_s,
413 c_1,
414 c_mu,
415 damps,
416 chi_n,
417 }
418 }
419
420 fn ask(&self, rng: &mut StdRng) -> Vec<f64> {
422 let z: Vec<f64> = (0..self.n).map(|_| standard_normal(rng)).collect();
423 let dz: Vec<f64> = (0..self.n).map(|i| self.d[i] * z[i]).collect();
424 let bdz = mat_vec(&self.b, &dz);
425 (0..self.n).map(|i| self.mean[i] + self.sigma * bdz[i]).collect()
426 }
427
428 fn tell(&mut self, candidates: &[(Vec<f64>, f64)]) {
431 self.generation += 1;
432 let n = self.n;
433
434 let mut order: Vec<usize> = (0..candidates.len()).collect();
436 order.sort_by(|&a, &b| candidates[a].1.total_cmp(&candidates[b].1));
437
438 let mean_old = self.mean.clone();
439
440 let ys: Vec<Vec<f64>> = order
442 .iter()
443 .take(self.mu)
444 .map(|&idx| {
445 (0..n)
446 .map(|k| (candidates[idx].0[k] - mean_old[k]) / self.sigma)
447 .collect()
448 })
449 .collect();
450
451 let mut yw = vec![0.0; n];
453 for (w, y) in self.weights.iter().zip(&ys) {
454 for k in 0..n {
455 yw[k] += w * y[k];
456 }
457 }
458 for k in 0..n {
459 self.mean[k] = mean_old[k] + self.sigma * yw[k];
460 }
461
462 let bt_yw = mat_vec(&transpose(&self.b), &yw);
464 let scaled: Vec<f64> = (0..n).map(|i| bt_yw[i] / self.d[i]).collect();
465 let c_inv_sqrt_yw = mat_vec(&self.b, &scaled);
466
467 let cs_factor = (self.c_s * (2.0 - self.c_s) * self.mu_eff).sqrt();
469 for k in 0..n {
470 self.p_s[k] = (1.0 - self.c_s) * self.p_s[k] + cs_factor * c_inv_sqrt_yw[k];
471 }
472 let ps_norm = norm(&self.p_s);
473
474 let hsig = if ps_norm
476 / (1.0 - (1.0 - self.c_s).powi(2 * self.generation as i32)).sqrt()
477 / self.chi_n
478 < 1.4 + 2.0 / (n as f64 + 1.0)
479 {
480 1.0
481 } else {
482 0.0
483 };
484
485 let cc_factor = (self.c_c * (2.0 - self.c_c) * self.mu_eff).sqrt();
487 for k in 0..n {
488 self.p_c[k] = (1.0 - self.c_c) * self.p_c[k] + hsig * cc_factor * yw[k];
489 }
490
491 let delta = (1.0 - hsig) * self.c_c * (2.0 - self.c_c);
493 for r in 0..n {
494 for c in 0..n {
495 let rank_one = self.p_c[r] * self.p_c[c] + delta * self.cov[r][c];
496 let mut rank_mu = 0.0;
497 for (w, y) in self.weights.iter().zip(&ys) {
498 rank_mu += w * y[r] * y[c];
499 }
500 self.cov[r][c] = (1.0 - self.c_1 - self.c_mu) * self.cov[r][c]
501 + self.c_1 * rank_one
502 + self.c_mu * rank_mu;
503 }
504 }
505
506 self.sigma *= ((self.c_s / self.damps) * (ps_norm / self.chi_n - 1.0)).exp();
508 self.sigma = self.sigma.clamp(1e-12, 1e6);
510
511 self.update_eigen();
513 }
514
515 fn update_eigen(&mut self) {
516 for r in 0..self.n {
518 for c in (r + 1)..self.n {
519 let avg = 0.5 * (self.cov[r][c] + self.cov[c][r]);
520 self.cov[r][c] = avg;
521 self.cov[c][r] = avg;
522 }
523 }
524 let (evals, evecs) = jacobi_eigen(&self.cov);
525 self.b = evecs;
526 self.d = evals.iter().map(|&e| e.max(1e-20).sqrt()).collect();
527 }
528}
529
530fn identity(n: usize) -> Vec<Vec<f64>> {
533 let mut m = vec![vec![0.0; n]; n];
534 for (i, row) in m.iter_mut().enumerate() {
535 row[i] = 1.0;
536 }
537 m
538}
539
540fn transpose(m: &[Vec<f64>]) -> Vec<Vec<f64>> {
541 let n = m.len();
542 let mut t = vec![vec![0.0; n]; n];
543 for r in 0..n {
544 for c in 0..n {
545 t[c][r] = m[r][c];
546 }
547 }
548 t
549}
550
551fn mat_vec(m: &[Vec<f64>], v: &[f64]) -> Vec<f64> {
552 m.iter()
553 .map(|row| row.iter().zip(v).map(|(a, b)| a * b).sum())
554 .collect()
555}
556
557fn norm(v: &[f64]) -> f64 {
558 v.iter().map(|x| x * x).sum::<f64>().sqrt()
559}
560
561fn standard_normal(rng: &mut StdRng) -> f64 {
563 let u1: f64 = rng.random_range(1e-12..1.0);
564 let u2: f64 = rng.random_range(0.0..1.0);
565 (-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos()
566}
567
568fn jacobi_eigen(a_in: &[Vec<f64>]) -> (Vec<f64>, Vec<Vec<f64>>) {
573 let n = a_in.len();
574 let mut a = a_in.to_vec();
575 let mut v = identity(n);
576 if n == 1 {
577 return (vec![a[0][0]], v);
578 }
579
580 for _sweep in 0..100 {
581 let mut off = 0.0;
583 for p in 0..n {
584 for q in (p + 1)..n {
585 off += a[p][q].abs();
586 }
587 }
588 if off < 1e-14 {
589 break;
590 }
591
592 for p in 0..n {
593 for q in (p + 1)..n {
594 if a[p][q].abs() < 1e-18 {
595 continue;
596 }
597 let theta = (a[q][q] - a[p][p]) / (2.0 * a[p][q]);
598 let t = theta.signum() / (theta.abs() + (theta * theta + 1.0).sqrt());
599 let c = 1.0 / (t * t + 1.0).sqrt();
600 let s = t * c;
601
602 for k in 0..n {
604 let akp = a[k][p];
605 let akq = a[k][q];
606 a[k][p] = c * akp - s * akq;
607 a[k][q] = s * akp + c * akq;
608 }
609 for k in 0..n {
610 let apk = a[p][k];
611 let aqk = a[q][k];
612 a[p][k] = c * apk - s * aqk;
613 a[q][k] = s * apk + c * aqk;
614 }
615 for k in 0..n {
617 let vkp = v[k][p];
618 let vkq = v[k][q];
619 v[k][p] = c * vkp - s * vkq;
620 v[k][q] = s * vkp + c * vkq;
621 }
622 }
623 }
624 }
625
626 let evals: Vec<f64> = (0..n).map(|i| a[i][i]).collect();
627 (evals, v)
628}