1#![allow(clippy::needless_range_loop)]
20
21use super::Optimizer;
22use crate::problem::Problem;
23use crate::rng::Rng;
24use crate::solution::{Report, Solution, StopReason};
25use crate::termination::Termination;
26
27#[derive(Debug, Clone, Copy)]
29pub struct CmaEs {
30 pub population: Option<usize>,
32 pub sigma0: f64,
35 pub seed: u64,
37}
38
39impl Default for CmaEs {
40 fn default() -> Self {
41 CmaEs {
42 population: None,
43 sigma0: 0.3,
44 seed: 42,
45 }
46 }
47}
48
49impl Optimizer for CmaEs {
50 fn with_seed(&self, seed: u64) -> Self {
51 CmaEs { seed, ..*self }
52 }
53
54 fn optimize(&self, problem: &dyn Problem, term: &Termination) -> Report {
55 crate::problem::validate(problem).unwrap_or_else(|e| panic!("CmaEs: invalid problem: {e}"));
56 let run = cma_run(problem, term, self.seed, self.population, self.sigma0, 0);
57 let stop = term.reason(run.evaluations, run.best.value).unwrap_or(
58 StopReason::Converged,
60 );
61 Report {
62 solution: run.best,
63 stop,
64 evaluations: run.evaluations,
65 }
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71#[non_exhaustive]
72pub enum Restart {
73 Ipop,
75 Bipop,
80}
81
82#[derive(Debug, Clone, Copy)]
93pub struct RestartCmaEs {
94 pub strategy: Restart,
96 pub sigma0: f64,
98 pub max_restarts: usize,
100 pub seed: u64,
102}
103
104impl Default for RestartCmaEs {
105 fn default() -> Self {
106 RestartCmaEs {
107 strategy: Restart::Bipop,
108 sigma0: 0.3,
109 max_restarts: 100,
110 seed: 42,
111 }
112 }
113}
114
115impl RestartCmaEs {
116 pub fn ipop() -> Self {
118 RestartCmaEs {
119 strategy: Restart::Ipop,
120 ..RestartCmaEs::default()
121 }
122 }
123
124 pub fn bipop() -> Self {
126 RestartCmaEs::default()
127 }
128}
129
130impl Optimizer for RestartCmaEs {
131 fn with_seed(&self, seed: u64) -> Self {
132 RestartCmaEs { seed, ..*self }
133 }
134
135 fn optimize(&self, problem: &dyn Problem, term: &Termination) -> Report {
136 crate::problem::validate(problem)
137 .unwrap_or_else(|e| panic!("RestartCmaEs: invalid problem: {e}"));
138 let n = problem.bounds().len();
139 let lambda_def = (4 + (3.0 * (n as f64).ln()) as usize).max(4);
140
141 let mut ctl = Rng::split(self.seed, u64::MAX);
144
145 let mut best = Solution {
146 x: vec![0.0; n],
147 value: f64::INFINITY,
148 };
149 let mut evaluations = 0usize;
150 let mut lambda_large = lambda_def;
151 let mut budget_large = 0usize;
152 let mut budget_small = 0usize;
153 let mut last_large_run = 0usize;
154 let mut any_converged = false;
155
156 for run_idx in 0..self.max_restarts.max(1) as u64 {
157 if term.reason(evaluations, best.value).is_some() {
158 break;
159 }
160 let (lambda, sigma0, cap, is_large) = if run_idx == 0 {
164 (lambda_def, self.sigma0, None, true)
165 } else {
166 match self.strategy {
167 Restart::Ipop => {
168 lambda_large *= 2;
169 (lambda_large, self.sigma0, None, true)
170 }
171 Restart::Bipop => {
172 if budget_large <= budget_small {
173 lambda_large *= 2;
174 (lambda_large, self.sigma0, None, true)
175 } else {
176 let u1 = ctl.uniform();
179 let u2 = ctl.uniform();
180 let ratio = lambda_large as f64 / (2.0 * lambda_def as f64);
181 let lam = ((lambda_def as f64) * ratio.max(1.0).powf(u1 * u1)) as usize;
182 let sig = self.sigma0 * 10f64.powf(-2.0 * u2);
183 (
186 lam.max(4),
187 sig,
188 Some((last_large_run / 2).max(lam.max(4))),
189 false,
190 )
191 }
192 }
193 }
194 };
195
196 let inner_term = Termination {
197 max_evaluations: match cap {
198 Some(c) => (evaluations + c).min(term.max_evaluations),
199 None => term.max_evaluations,
200 },
201 target: term.target,
202 };
203 let out = cma_run(
204 problem,
205 &inner_term,
206 crate::rng::mix_seed(self.seed, run_idx),
207 Some(lambda),
208 sigma0,
209 evaluations,
210 );
211 let used = out.evaluations - evaluations;
212 evaluations = out.evaluations;
213 if is_large {
214 budget_large += used;
215 last_large_run = used;
216 } else {
217 budget_small += used;
218 }
219 if out.best.value < best.value {
220 best = out.best;
221 }
222 any_converged |= out.converged;
223 if !out.converged && term.reason(evaluations, best.value).is_some() {
226 break;
227 }
228 }
229
230 let stop = term
231 .reason(evaluations, best.value)
232 .unwrap_or(if any_converged {
233 StopReason::Converged
234 } else {
235 StopReason::BudgetExhausted
236 });
237 Report {
238 solution: best,
239 stop,
240 evaluations,
241 }
242 }
243}
244
245pub(crate) struct CmaRunOutcome {
247 pub best: Solution,
248 pub evaluations: usize,
250 pub converged: bool,
253}
254
255pub(crate) fn cma_run(
263 problem: &dyn Problem,
264 term: &Termination,
265 seed: u64,
266 population: Option<usize>,
267 sigma0: f64,
268 start_evals: usize,
269) -> CmaRunOutcome {
270 let bounds = problem.bounds();
271 let n = bounds.len();
272 let mut rng = Rng::new(seed);
273
274 let lambda = population
276 .unwrap_or(4 + (3.0 * (n as f64).ln()) as usize)
277 .max(4);
278 let mu = lambda / 2;
279 let raw: Vec<f64> = (0..mu)
283 .map(|i| ((lambda as f64 + 1.0) / 2.0).ln() - ((i + 1) as f64).ln())
284 .collect();
285 let wsum: f64 = raw.iter().sum();
286 let w: Vec<f64> = raw.iter().map(|&v| v / wsum).collect();
287 let mu_eff = 1.0 / w.iter().map(|&v| v * v).sum::<f64>();
288
289 let nf = n as f64;
290 let c_sigma = (mu_eff + 2.0) / (nf + mu_eff + 5.0);
291 let d_sigma = 1.0 + 2.0 * (((mu_eff - 1.0) / (nf + 1.0)).sqrt() - 1.0).max(0.0) + c_sigma;
292 let c_c = (4.0 + mu_eff / nf) / (nf + 4.0 + 2.0 * mu_eff / nf);
293 let c_1 = 2.0 / ((nf + 1.3).powi(2) + mu_eff);
294 let c_mu = (1.0 - c_1).min(2.0 * (mu_eff - 2.0 + 1.0 / mu_eff) / ((nf + 2.0).powi(2) + mu_eff));
295 let e_n = nf.sqrt() * (1.0 - 1.0 / (4.0 * nf) + 1.0 / (21.0 * nf * nf));
297
298 let mut mean = vec![0.5; n];
300 let mut sigma = sigma0;
301 let mut cov = identity(n);
302 let mut p_sigma = vec![0.0; n];
303 let mut p_c = vec![0.0; n];
304 let mut generation = 0i32;
305
306 let hist_len = 10 + (30.0 * nf / lambda as f64).ceil() as usize;
308 let mut hist: Vec<f64> = Vec::with_capacity(hist_len + 1);
309 let tolx = 1e-12 * sigma0;
310 let mut converged = false;
311
312 let mut best = Solution {
313 x: denormalize(&mean, bounds),
314 value: f64::INFINITY,
315 };
316 let mut evaluations = start_evals;
317
318 'outer: while term.reason(evaluations, best.value).is_none() {
319 let (eigvals, b) = jacobi_eigen(&cov);
323 let max_eig = eigvals.iter().cloned().fold(f64::MIN_POSITIVE, f64::max);
324 let d: Vec<f64> = eigvals
325 .iter()
326 .map(|&v| v.max(max_eig * 1e-14).sqrt())
327 .collect();
328
329 let mut pop: Vec<(f64, Vec<f64>)> = Vec::with_capacity(lambda);
334 for _ in 0..lambda {
335 if term.reason(evaluations, best.value).is_some() {
336 break 'outer; }
338 let z: Vec<f64> = (0..n).map(|_| rng.normal()).collect();
339 let dz: Vec<f64> = (0..n).map(|j| d[j] * z[j]).collect();
340 let y = matvec(&b, &dz); let u_c: Vec<f64> = (0..n)
342 .map(|i| (mean[i] + sigma * y[i]).clamp(0.0, 1.0))
343 .collect();
344 let x = denormalize(&u_c, bounds);
345 let f = problem.objective(&x);
346 evaluations += 1;
347 let f = if f.is_finite() { f } else { f64::INFINITY };
348 if f < best.value {
349 best = Solution { x, value: f };
350 }
351 let y_eff: Vec<f64> = (0..n).map(|i| (u_c[i] - mean[i]) / sigma).collect();
352 pop.push((f, y_eff));
353 }
354
355 pop.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
357 let mut y_w = vec![0.0; n];
358 for (i, wi) in w.iter().enumerate() {
359 for k in 0..n {
360 y_w[k] += wi * pop[i].1[k];
361 }
362 }
363
364 for k in 0..n {
367 mean[k] += sigma * y_w[k];
368 }
369
370 let c_inv_sqrt_yw = c_inv_sqrt_mul(&b, &d, &y_w);
372 let cs_factor = (c_sigma * (2.0 - c_sigma) * mu_eff).sqrt();
373 for k in 0..n {
374 p_sigma[k] = (1.0 - c_sigma) * p_sigma[k] + cs_factor * c_inv_sqrt_yw[k];
375 }
376 let ps_norm = norm(&p_sigma);
377
378 generation += 1;
379 let hsig = if ps_norm / (1.0 - (1.0 - c_sigma).powi(2 * generation)).sqrt()
381 < (1.4 + 2.0 / (nf + 1.0)) * e_n
382 {
383 1.0
384 } else {
385 0.0
386 };
387
388 let cc_factor = (c_c * (2.0 - c_c) * mu_eff).sqrt();
390 for k in 0..n {
391 p_c[k] = (1.0 - c_c) * p_c[k] + hsig * cc_factor * y_w[k];
392 }
393
394 let delta_hsig = (1.0 - hsig) * c_c * (2.0 - c_c);
396 for a in 0..n {
397 for bcol in a..n {
398 let mut rank_mu = 0.0;
399 for (i, wi) in w.iter().enumerate() {
400 rank_mu += wi * pop[i].1[a] * pop[i].1[bcol];
401 }
402 let rank_one = p_c[a] * p_c[bcol];
403 let val = (1.0 - c_1 - c_mu) * cov[a][bcol]
404 + c_1 * (rank_one + delta_hsig * cov[a][bcol])
405 + c_mu * rank_mu;
406 cov[a][bcol] = val;
407 cov[bcol][a] = val; }
409 }
410
411 sigma *= ((c_sigma / d_sigma) * (ps_norm / e_n - 1.0)).exp();
414 if !(1e-300..=1e6).contains(&sigma) {
415 converged = true; break;
417 }
418
419 hist.push(pop[0].0);
421 if hist.len() > hist_len {
422 hist.remove(0);
423 let (lo, hi) = hist
424 .iter()
425 .fold((f64::INFINITY, f64::NEG_INFINITY), |(l, h), &v| {
426 (l.min(v), h.max(v))
427 });
428 if hi - lo < 1e-12 {
429 converged = true; break;
431 }
432 }
433 let tolx_met = (0..n)
434 .all(|k| (sigma * p_c[k]).abs() < tolx && sigma * cov[k][k].max(0.0).sqrt() < tolx);
435 if tolx_met {
436 converged = true; break;
438 }
439 let min_eig = eigvals.iter().cloned().fold(f64::INFINITY, f64::min);
440 if max_eig / min_eig.max(f64::MIN_POSITIVE) > 1e14 {
441 converged = true; break;
443 }
444 }
445
446 CmaRunOutcome {
447 best,
448 evaluations,
449 converged,
450 }
451}
452
453fn denormalize(u: &[f64], bounds: &[(f64, f64)]) -> Vec<f64> {
455 u.iter()
456 .zip(bounds)
457 .map(|(&ui, &(lo, hi))| lo + ui * (hi - lo))
458 .collect()
459}
460
461fn identity(n: usize) -> Vec<Vec<f64>> {
462 let mut m = vec![vec![0.0; n]; n];
463 for (i, row) in m.iter_mut().enumerate() {
464 row[i] = 1.0;
465 }
466 m
467}
468
469fn matvec(m: &[Vec<f64>], v: &[f64]) -> Vec<f64> {
471 m.iter()
472 .map(|row| row.iter().zip(v).map(|(a, b)| a * b).sum())
473 .collect()
474}
475
476fn norm(v: &[f64]) -> f64 {
477 v.iter().map(|x| x * x).sum::<f64>().sqrt()
478}
479
480fn c_inv_sqrt_mul(b: &[Vec<f64>], d: &[f64], v: &[f64]) -> Vec<f64> {
483 let n = v.len();
484 let mut a = vec![0.0; n];
486 for (j, aj) in a.iter_mut().enumerate() {
487 for i in 0..n {
488 *aj += b[i][j] * v[i];
489 }
490 *aj /= d[j];
491 }
492 matvec(b, &a)
494}
495
496fn jacobi_eigen(input: &[Vec<f64>]) -> (Vec<f64>, Vec<Vec<f64>>) {
501 let n = input.len();
502 let mut a: Vec<Vec<f64>> = input.to_vec();
503 let mut v = identity(n);
504 if n == 1 {
505 return (vec![a[0][0]], v);
506 }
507
508 for _ in 0..100 {
509 let mut off = 0.0;
513 for p in 0..n {
514 for q in p + 1..n {
515 off += a[p][q] * a[p][q];
516 }
517 }
518 let scale = (0..n)
519 .map(|i| a[i][i].abs())
520 .fold(f64::MIN_POSITIVE, f64::max);
521 if off.sqrt() < 1e-14 * scale {
522 break;
523 }
524
525 for p in 0..n {
526 for q in p + 1..n {
527 if a[p][q].abs() < 1e-300 {
528 continue;
529 }
530 let theta = (a[q][q] - a[p][p]) / (2.0 * a[p][q]);
532 let t = theta.signum() / (theta.abs() + (theta * theta + 1.0).sqrt());
533 let c = 1.0 / (t * t + 1.0).sqrt();
534 let s = t * c;
535
536 for k in 0..n {
538 let akp = a[k][p];
539 let akq = a[k][q];
540 a[k][p] = c * akp - s * akq;
541 a[k][q] = s * akp + c * akq;
542 }
543 for k in 0..n {
544 let apk = a[p][k];
545 let aqk = a[q][k];
546 a[p][k] = c * apk - s * aqk;
547 a[q][k] = s * apk + c * aqk;
548 }
549 for k in 0..n {
551 let vkp = v[k][p];
552 let vkq = v[k][q];
553 v[k][p] = c * vkp - s * vkq;
554 v[k][q] = s * vkp + c * vkq;
555 }
556 }
557 }
558 }
559
560 let eigvals = (0..n).map(|i| a[i][i]).collect();
561 (eigvals, v)
562}
563
564#[cfg(test)]
565mod tests {
566 use super::*;
567
568 #[test]
569 fn jacobi_recovers_known_eigenpairs() {
570 let (vals, vecs) = jacobi_eigen(&[vec![2.0, 1.0], vec![1.0, 2.0]]);
572 let mut sorted = vals.clone();
573 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
574 assert!((sorted[0] - 1.0).abs() < 1e-9 && (sorted[1] - 3.0).abs() < 1e-9);
575 for j in 0..2 {
577 let col_norm = (0..2).map(|i| vecs[i][j] * vecs[i][j]).sum::<f64>().sqrt();
578 assert!((col_norm - 1.0).abs() < 1e-9);
579 }
580 }
581}