1use serde::{Deserialize, Serialize};
114
115use hpr_core::random::SeededRng;
116
117use super::eigen::symmetric_eigen;
118use super::{Evaluation, Variable, check_variables, normal};
119use crate::error::AnalysisError;
120
121pub const MAX_DRAWS: usize = 1000;
123
124pub const MAX_POPULATION: usize = 1 << 16;
126
127pub const MAX_CONDITION: f64 = 1e14;
130
131#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
134#[serde(try_from = "CmaesData")]
135pub struct Cmaes {
136 variables: Vec<Variable>,
137 population: usize,
138 max_evaluations: usize,
139 target: Option<f64>,
140 tolerance_x: f64,
141 tolerance_value: f64,
142}
143
144#[derive(Deserialize)]
146#[serde(deny_unknown_fields)]
147struct CmaesData {
148 variables: Vec<Variable>,
149 population: usize,
150 max_evaluations: usize,
151 target: Option<f64>,
152 tolerance_x: f64,
153 tolerance_value: f64,
154}
155
156impl TryFrom<CmaesData> for Cmaes {
157 type Error = AnalysisError;
158
159 fn try_from(data: CmaesData) -> Result<Self, AnalysisError> {
160 let cmaes = Cmaes::new(data.variables)?
161 .with_population(data.population)?
162 .with_max_evaluations(data.max_evaluations)?
163 .with_tolerance_x(data.tolerance_x)?
164 .with_tolerance_value(data.tolerance_value)?;
165 match data.target {
166 Some(target) => cmaes.with_target(target),
167 None => Ok(cmaes),
168 }
169 }
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
174#[non_exhaustive]
175pub enum Stop {
176 Target,
178 Evaluations,
180 TolX,
183 TolFun,
185 Condition,
188 Bounds,
190}
191
192#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
194#[non_exhaustive]
195pub struct Optimum {
196 pub point: Vec<f64>,
199 #[serde(with = "infinity_as_none")]
202 pub value: f64,
203 #[serde(default, with = "infinity_as_none")]
207 pub violation: f64,
208 pub evaluation: usize,
210 pub evaluations: usize,
212 pub generations: usize,
214 pub mean: Vec<f64>,
217 #[serde(with = "infinity_as_none")]
223 pub spread: f64,
224 pub stop: Stop,
226}
227
228pub(crate) mod infinity_as_none {
232 use serde::ser::Error as _;
233 use serde::{Deserialize, Deserializer, Serialize, Serializer};
234
235 pub(crate) fn serialize<S: Serializer>(x: &f64, serializer: S) -> Result<S::Ok, S::Error> {
236 if x.is_nan() || *x == f64::NEG_INFINITY {
237 return Err(S::Error::custom(format!("{x} is neither finite nor +∞")));
238 }
239 let value = (*x != f64::INFINITY).then_some(*x);
240 value.serialize(serializer)
241 }
242
243 pub(crate) fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<f64, D::Error> {
244 Ok(Option::<f64>::deserialize(deserializer)?.unwrap_or(f64::INFINITY))
245 }
246}
247
248impl Cmaes {
249 pub fn new(variables: Vec<Variable>) -> Result<Self, AnalysisError> {
259 check_variables(&variables)?;
260 let n = variables.len() as f64;
261 let population = 4 + (3.0 * n.ln()).floor() as usize;
263 Ok(Self {
264 variables,
265 population,
266 max_evaluations: 10_000,
267 target: None,
268 tolerance_x: 1e-12,
269 tolerance_value: 1e-12,
270 })
271 }
272
273 pub fn with_population(mut self, lambda: usize) -> Result<Self, AnalysisError> {
281 if lambda < 2 {
282 return Err(AnalysisError::TooFew {
283 what: "population",
284 count: lambda,
285 minimum: 2,
286 });
287 }
288 if lambda > MAX_POPULATION {
289 return Err(AnalysisError::Count {
290 what: "population",
291 count: lambda,
292 limit: MAX_POPULATION,
293 });
294 }
295 self.population = lambda;
296 Ok(self)
297 }
298
299 pub fn with_max_evaluations(mut self, max: usize) -> Result<Self, AnalysisError> {
306 if max == 0 {
307 return Err(AnalysisError::TooFew {
308 what: "evaluations",
309 count: 0,
310 minimum: 1,
311 });
312 }
313 self.max_evaluations = max;
314 Ok(self)
315 }
316
317 pub fn with_target(mut self, target: f64) -> Result<Self, AnalysisError> {
324 if !target.is_finite() {
325 return Err(AnalysisError::Domain {
326 what: "target (finite)",
327 value: target,
328 });
329 }
330 self.target = Some(target);
331 Ok(self)
332 }
333
334 pub fn with_tolerance_x(mut self, tolerance: f64) -> Result<Self, AnalysisError> {
342 self.tolerance_x = check_tolerance("x tolerance", tolerance)?;
343 Ok(self)
344 }
345
346 pub fn with_tolerance_value(mut self, tolerance: f64) -> Result<Self, AnalysisError> {
353 self.tolerance_value = check_tolerance("value tolerance", tolerance)?;
354 Ok(self)
355 }
356
357 pub fn variables(&self) -> &[Variable] {
359 &self.variables
360 }
361
362 pub fn population(&self) -> usize {
364 self.population
365 }
366
367 pub fn start(&self, seed: u64) -> Result<Run, AnalysisError> {
375 let mut run = Run::new(self, seed);
376 match run.draw() {
377 None => Ok(run),
378 Some(Stop::Bounds) => Err(AnalysisError::OutOfBounds { draws: MAX_DRAWS }),
379 Some(_) => Err(AnalysisError::Domain {
380 what: "first candidate (not finite: the steps are too large)",
381 value: f64::INFINITY,
382 }),
383 }
384 }
385
386 pub fn minimize_constrained(
395 &self,
396 seed: u64,
397 mut model: impl FnMut(&[f64]) -> Evaluation,
398 ) -> Result<Optimum, AnalysisError> {
399 let mut run = self.start(seed)?;
400 loop {
401 let evaluations: Vec<Evaluation> = run.candidates().iter().map(|x| model(x)).collect();
402 if let Some(optimum) = run.tell_constrained(&evaluations)? {
403 return Ok(optimum);
404 }
405 }
406 }
407
408 pub fn minimize(
414 &self,
415 seed: u64,
416 mut model: impl FnMut(&[f64]) -> f64,
417 ) -> Result<Optimum, AnalysisError> {
418 let mut run = self.start(seed)?;
419 loop {
420 let values: Vec<f64> = run.candidates().iter().map(|x| model(x)).collect();
421 if let Some(optimum) = run.tell(&values)? {
422 return Ok(optimum);
423 }
424 }
425 }
426}
427
428fn check_tolerance(what: &'static str, tolerance: f64) -> Result<f64, AnalysisError> {
430 if tolerance.is_finite() && tolerance >= 0.0 {
431 Ok(tolerance)
432 } else {
433 Err(AnalysisError::Domain {
434 what,
435 value: tolerance,
436 })
437 }
438}
439
440#[derive(Debug, Clone, PartialEq, Serialize)]
444#[non_exhaustive]
445pub struct Parameters {
446 pub mu: usize,
448 pub weights: Vec<f64>,
450 pub mu_eff: f64,
452 pub c_sigma: f64,
454 pub d_sigma: f64,
456 pub c_c: f64,
458 pub c_1: f64,
460 pub c_mu: f64,
462 pub chi_n: f64,
464}
465
466impl Parameters {
467 pub(crate) fn new(n: usize, lambda: usize) -> Self {
470 let nf = n as f64;
471 let mu = lambda / 2;
472 let half = (lambda as f64 + 1.0) / 2.0;
473 let raw: Vec<f64> = (1..=mu).map(|i| half.ln() - (i as f64).ln()).collect();
474 let sum: f64 = raw.iter().sum();
475 let weights: Vec<f64> = raw.iter().map(|w| w / sum).collect();
476 let mu_eff = 1.0 / weights.iter().map(|w| w * w).sum::<f64>();
477 let c_sigma = (mu_eff + 2.0) / (nf + mu_eff + 5.0);
478 let d_sigma = 1.0 + 2.0 * (((mu_eff - 1.0) / (nf + 1.0)).sqrt() - 1.0).max(0.0) + c_sigma;
479 let c_c = (4.0 + mu_eff / nf) / (nf + 4.0 + 2.0 * mu_eff / nf);
480 let c_1 = 2.0 / ((nf + 1.3).powi(2) + mu_eff);
482 let c_mu = (1.0 - c_1).min(
483 2.0 * (0.25 + mu_eff + 1.0 / mu_eff - 2.0) / ((nf + 2.0).powi(2) + 2.0 * mu_eff / 2.0),
484 );
485 let chi_n = nf.sqrt() * (1.0 - 1.0 / (4.0 * nf) + 1.0 / (21.0 * nf * nf));
486 Self {
487 mu,
488 weights,
489 mu_eff,
490 c_sigma,
491 d_sigma,
492 c_c,
493 c_1,
494 c_mu,
495 chi_n,
496 }
497 }
498}
499
500#[derive(Debug, Clone)]
503pub struct Run {
504 variables: Vec<Variable>,
505 seed: u64,
506 lambda: usize,
507 max_evaluations: usize,
508 target: Option<f64>,
509 tolerance_x: f64,
510 tolerance_value: f64,
511 p: Parameters,
512 n: usize,
513 scale: Vec<f64>,
515 mean: Vec<f64>,
517 sigma: f64,
518 p_sigma: Vec<f64>,
519 p_c: Vec<f64>,
520 c: Vec<f64>,
522 b: Vec<f64>,
524 d: Vec<f64>,
526 a: Vec<f64>,
529 alpha: f64,
532 generation: usize,
533 evaluations: usize,
534 candidates: Vec<Vec<f64>>,
535 steps: Vec<Vec<f64>>,
537 best: Option<(Vec<f64>, Evaluation, usize)>,
538 history: Vec<Evaluation>,
540 stop: Option<Stop>,
541}
542
543impl Run {
544 fn new(cmaes: &Cmaes, seed: u64) -> Self {
545 let n = cmaes.variables.len();
546 let mut identity = vec![0.0; n * n];
547 for i in 0..n {
548 identity[i * n + i] = 1.0;
549 }
550 Self {
551 seed,
552 lambda: cmaes.population,
553 max_evaluations: cmaes.max_evaluations,
554 target: cmaes.target,
555 tolerance_x: cmaes.tolerance_x,
556 tolerance_value: cmaes.tolerance_value,
557 p: Parameters::new(n, cmaes.population),
558 n,
559 scale: cmaes.variables.iter().map(Variable::step).collect(),
560 mean: cmaes.variables.iter().map(Variable::start).collect(),
561 sigma: 1.0,
562 p_sigma: vec![0.0; n],
563 p_c: vec![0.0; n],
564 c: identity.clone(),
565 b: identity,
566 d: vec![1.0; n],
567 a: vec![1.0; n],
568 alpha: 1.0 / (n as f64 * cmaes.population as f64),
570 generation: 0,
571 evaluations: 0,
572 candidates: Vec::new(),
573 steps: Vec::new(),
574 best: None,
575 history: Vec::new(),
576 stop: None,
577 variables: cmaes.variables.clone(),
578 }
579 }
580
581 pub fn parameters(&self) -> &Parameters {
583 &self.p
584 }
585
586 pub fn candidates(&self) -> &[Vec<f64>] {
590 &self.candidates
591 }
592
593 pub fn generation(&self) -> usize {
595 self.generation
596 }
597
598 pub fn mean(&self) -> &[f64] {
601 &self.mean
602 }
603
604 pub fn sigma(&self) -> f64 {
606 self.sigma
607 }
608
609 pub fn covariance(&self) -> &[f64] {
613 &self.c
614 }
615
616 pub fn margin_scale(&self) -> &[f64] {
620 &self.a
621 }
622
623 pub fn integer_margin(&self) -> f64 {
627 self.alpha
628 }
629
630 pub fn stopped(&self) -> Option<Stop> {
632 self.stop
633 }
634
635 pub fn tell(&mut self, values: &[f64]) -> Result<Option<Optimum>, AnalysisError> {
648 let evaluations: Vec<Evaluation> =
649 values.iter().map(|&v| Evaluation::feasible(v)).collect();
650 self.tell_constrained(&evaluations)
651 }
652
653 pub fn tell_constrained(
666 &mut self,
667 evaluations: &[Evaluation],
668 ) -> Result<Option<Optimum>, AnalysisError> {
669 let values: Vec<f64> = evaluations.iter().map(|e| e.value).collect();
670 if values.len() != self.candidates.len() || self.candidates.is_empty() {
671 return Err(AnalysisError::Length {
672 what: "values, against the generation's candidates",
673 length: values.len(),
674 expected: self.candidates.len(),
675 });
676 }
677 if let Some(e) = evaluations
678 .iter()
679 .find(|e| e.violation.is_nan() || e.violation < 0.0)
680 {
681 return Err(AnalysisError::Domain {
682 what: "constraint violation (must not be negative or NaN)",
683 value: e.violation,
684 });
685 }
686 if let Some((k, &value)) = values
687 .iter()
688 .enumerate()
689 .find(|(_, v)| v.is_nan() || **v == f64::NEG_INFINITY)
690 {
691 return Err(AnalysisError::Output {
692 index: self.evaluations + k,
693 value,
694 });
695 }
696 let mut order: Vec<usize> = (0..self.lambda).collect();
698 order.sort_by(|&i, &j| evaluations[i].rank(&evaluations[j]));
699 let first = order[0];
700 if self
701 .best
702 .as_ref()
703 .is_none_or(|(_, e, _)| evaluations[first].beats(e))
704 {
705 self.best = Some((
706 self.candidates[first].clone(),
707 evaluations[first],
708 self.evaluations + first + 1,
709 ));
710 }
711 self.evaluations += self.lambda;
712 self.update(&order);
713 self.generation += 1;
714 self.history.push(evaluations[first]);
715 let feasible: Vec<f64> = evaluations
716 .iter()
717 .filter(|e| e.is_feasible())
718 .map(|e| e.value)
719 .collect();
720 self.stop = self.check_stop(&feasible);
721 if self.stop.is_none() {
722 self.stop = self.draw();
723 }
724 Ok(self.stop.map(|stop| self.optimum(stop)))
725 }
726
727 fn update(&mut self, order: &[usize]) {
729 let n = self.n;
730 let p = &self.p;
731 let selected: Vec<&Vec<f64>> = order[..p.mu].iter().map(|&k| &self.steps[k]).collect();
732 let mut y_w = vec![0.0; n];
733 for (w, y) in p.weights.iter().zip(&selected) {
734 for (a, yi) in y_w.iter_mut().zip(y.iter()) {
735 *a += w * yi;
736 }
737 }
738 for ((m, y), s) in self.mean.iter_mut().zip(&y_w).zip(&self.scale) {
740 *m += self.sigma * s * y;
741 }
742 let bt_y: Vec<f64> = (0..n)
744 .map(|k| (0..n).map(|i| self.b[i * n + k] * y_w[i]).sum::<f64>() / self.d[k])
745 .collect();
746 let whitened: Vec<f64> = (0..n)
747 .map(|i| (0..n).map(|k| self.b[i * n + k] * bt_y[k]).sum())
748 .collect();
749 let cs = p.c_sigma;
750 let scale_sigma = (cs * (2.0 - cs) * p.mu_eff).sqrt();
751 for (ps, w) in self.p_sigma.iter_mut().zip(&whitened) {
752 *ps = (1.0 - cs) * *ps + scale_sigma * w;
753 }
754 let norm_ps = self.p_sigma.iter().map(|x| x * x).sum::<f64>().sqrt();
755 let exponent = 2.0 * (self.generation as f64 + 1.0);
757 let h_sigma = norm_ps / (1.0 - (1.0 - cs).powf(exponent)).sqrt()
758 < (1.4 + 2.0 / (n as f64 + 1.0)) * p.chi_n;
759 let cc = p.c_c;
760 let scale_c = (cc * (2.0 - cc) * p.mu_eff).sqrt();
761 for (pc, y) in self.p_c.iter_mut().zip(&y_w) {
762 *pc = (1.0 - cc) * *pc + if h_sigma { scale_c * y } else { 0.0 };
763 }
764 let delta = if h_sigma { 0.0 } else { cc * (2.0 - cc) };
765 let keep = 1.0 + p.c_1 * delta - p.c_1 - p.c_mu;
767 for i in 0..n {
768 for j in 0..=i {
769 let rank_mu: f64 = p
770 .weights
771 .iter()
772 .zip(&selected)
773 .map(|(w, y)| w * y[i] * y[j])
774 .sum();
775 let value =
776 keep * self.c[i * n + j] + p.c_1 * self.p_c[i] * self.p_c[j] + p.c_mu * rank_mu;
777 self.c[i * n + j] = value;
778 self.c[j * n + i] = value;
779 }
780 }
781 self.sigma *= ((cs / p.d_sigma) * (norm_ps / p.chi_n - 1.0)).exp();
782 let (values, vectors) = symmetric_eigen(&self.c, n);
783 self.b = vectors;
784 self.d = values.iter().map(|v| v.max(0.0).sqrt()).collect();
785 self.correct_margin();
786 }
787
788 fn correct_margin(&mut self) {
794 let n = self.n;
795 let alpha = self.alpha;
796 for j in 0..n {
797 let variable = &self.variables[j];
798 if !variable.is_integer() {
799 continue;
800 }
801 let spread_unit = self.sigma * self.scale[j] * self.c[j * n + j].sqrt();
803 let spread = self.a[j] * spread_unit;
804 if !(spread > 0.0 && spread.is_finite()) {
805 continue;
807 }
808 let m = self.mean[j];
809 let first = variable.low() + 0.5;
811 let last = variable.high() - 0.5;
812 if m <= first || m > last {
813 let threshold = if m <= first { first } else { last };
817 let reach = -normal::quantile(alpha) * spread;
819 if (m - threshold).abs() <= reach {
820 continue;
821 }
822 let mut mean = threshold + reach.copysign(m - threshold);
823 if (mean - threshold).abs() > reach {
825 mean = if mean > threshold {
826 mean.next_down()
827 } else {
828 mean.next_up()
829 };
830 }
831 self.mean[j] = mean;
832 continue;
833 }
834 let value = super::nearest_whole(m);
836 let (low, up) = (value - 0.5, value + 0.5);
837 let p_low = normal::cdf((low - m) / spread);
838 let p_up = normal::cdf((m - up) / spread);
839 let half = alpha / 2.0;
840 if p_low >= half && p_up >= half {
841 continue;
843 }
844 let p_mid = 1.0 - p_low - p_up;
845 let (q_low, q_up) = (p_low.max(half), p_up.max(half));
847 let excess = 1.0 - q_low - q_up - p_mid;
848 let share = q_low + q_up + p_mid - 3.0 * half;
849 let r_low = q_low + excess * (q_low - half) / share;
850 let r_up = q_up + excess * (q_up - half) / share;
851 let chi_low = -normal::quantile(r_low);
854 let chi_up = -normal::quantile(r_up);
855 let mean = (low * chi_up + up * chi_low) / (chi_low + chi_up);
856 let a = (up - low) / ((chi_low + chi_up) * spread_unit);
857 if mean.is_finite() && a.is_finite() && a > 0.0 {
858 self.mean[j] = mean;
859 self.a[j] = a;
860 }
861 }
862 }
863
864 fn check_stop(&self, values: &[f64]) -> Option<Stop> {
866 let n = self.n;
867 if let (Some(target), Some((_, best, _))) = (self.target, &self.best)
868 && best.is_feasible()
869 && best.value <= target
870 {
871 return Some(Stop::Target);
872 }
873 if self.evaluations >= self.max_evaluations {
874 return Some(Stop::Evaluations);
875 }
876 let d_max = self.d.iter().copied().fold(0.0, f64::max);
877 let d_min = self.d.iter().copied().fold(f64::INFINITY, f64::min);
878 let finite = (0..n).all(|i| {
881 (self.sigma * self.c[i * n + i].sqrt() * self.scale[i] * self.a[i]).is_finite()
882 && self.mean[i].is_finite()
883 });
884 if !(d_min > 0.0
885 && d_max / d_min <= MAX_CONDITION.sqrt()
886 && (self.sigma * d_max).is_finite()
887 && finite)
888 {
889 return Some(Stop::Condition);
890 }
891 let converged_x = (0..n).all(|i| {
893 self.sigma * self.c[i * n + i].sqrt() < self.tolerance_x
894 && self.sigma * self.p_c[i].abs() < self.tolerance_x
895 });
896 if converged_x {
897 return Some(Stop::TolX);
898 }
899 let window = 10 + (30.0 * n as f64 / self.lambda as f64).ceil() as usize;
901 if self.history.len() >= window && !values.is_empty() {
902 let bests = &self.history[self.history.len() - window..];
903 if !bests.iter().all(Evaluation::is_feasible) {
904 return None;
905 }
906 let recent = bests.iter().map(|e| &e.value).chain(values);
907 let (low, high) = recent.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), &v| {
908 (lo.min(v), hi.max(v))
909 });
910 if high - low < self.tolerance_value {
911 return Some(Stop::TolFun);
912 }
913 }
914 None
915 }
916
917 fn draw(&mut self) -> Option<Stop> {
921 let n = self.n;
922 self.candidates.clear();
923 self.steps.clear();
924 let mut bd = self.b.clone();
926 for row in bd.chunks_exact_mut(n) {
927 for (x, d) in row.iter_mut().zip(&self.d) {
928 *x *= d;
929 }
930 }
931 let mut z = vec![0.0; n];
932 let mut y = vec![0.0; n];
933 let mut x = vec![0.0; n];
934 for k in 0..self.lambda {
935 let mut rng = SeededRng::for_stream(self.seed, &[self.generation as u64, k as u64]);
937 let mut drawn = None;
938 for _ in 0..MAX_DRAWS {
939 z.iter_mut().for_each(|zi| *zi = rng.standard_normal());
940 for (yi, row) in y.iter_mut().zip(bd.chunks_exact(n)) {
941 *yi = row.iter().zip(&z).map(|(a, b)| a * b).sum();
942 }
943 for i in 0..n {
944 x[i] = self.mean[i] + self.sigma * self.scale[i] * self.a[i] * y[i];
945 }
946 if !x.iter().all(|xi| xi.is_finite()) {
947 drawn = Some(Stop::Condition);
948 break;
949 }
950 if x.iter()
952 .zip(&self.variables)
953 .all(|(xi, v)| v.is_integer() || v.contains(*xi))
954 {
955 drawn = None;
956 self.candidates.push(
957 x.iter()
958 .zip(&self.variables)
959 .map(|(xi, v)| v.encode(*xi))
960 .collect(),
961 );
962 self.steps.push(y.clone());
963 break;
964 }
965 drawn = Some(Stop::Bounds);
966 }
967 if drawn.is_some() {
968 self.candidates.clear();
969 self.steps.clear();
970 return drawn;
971 }
972 }
973 None
974 }
975
976 fn optimum(&mut self, stop: Stop) -> Optimum {
978 self.candidates.clear();
979 self.steps.clear();
980 let n = self.n;
981 let spread = (0..n)
984 .map(|i| self.sigma * self.c[i * n + i].sqrt() * self.scale[i] * self.a[i])
985 .map(|t| if t.is_nan() { f64::INFINITY } else { t })
986 .fold(0.0, f64::max);
987 let (point, best, evaluation) =
990 self.best
991 .clone()
992 .unwrap_or((Vec::new(), Evaluation::feasible(f64::INFINITY), 0));
993 Optimum {
994 point,
995 value: best.value,
996 violation: best.violation,
997 evaluation,
998 evaluations: self.evaluations,
999 generations: self.generation,
1000 mean: self.mean.clone(),
1001 spread,
1002 stop,
1003 }
1004 }
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009 use super::*;
1010 use crate::optimize::benchmark::constrained::sphere_above;
1011 use crate::optimize::benchmark::sphere;
1012
1013 #[test]
1017 fn zero_violations_change_nothing() {
1018 let cmaes = Cmaes::new(variables(5, 1.0, 0.5)).unwrap();
1019 let plain = cmaes.minimize(7, sphere).unwrap();
1020 let constrained = cmaes
1021 .minimize_constrained(7, |x| Evaluation::feasible(sphere(x)))
1022 .unwrap();
1023 assert_eq!(plain, constrained);
1024 }
1025
1026 #[test]
1027 fn violations_must_not_be_negative_or_nan() {
1028 let cmaes = Cmaes::new(variables(2, 1.0, 0.5)).unwrap();
1029 for bad in [-1e-300, f64::NAN, f64::NEG_INFINITY] {
1030 let mut run = cmaes.start(1).unwrap();
1031 let told: Vec<Evaluation> = run
1032 .candidates()
1033 .iter()
1034 .map(|_| Evaluation {
1035 value: 1.0,
1036 violation: bad,
1037 })
1038 .collect();
1039 match run.tell_constrained(&told) {
1040 Err(AnalysisError::Domain { what, value }) => {
1041 assert!(what.starts_with("constraint violation"));
1042 assert!(value.total_cmp(&bad).is_eq());
1043 }
1044 other => panic!("{bad}: {other:?}"),
1045 }
1046 }
1047 }
1048
1049 #[test]
1051 fn json_refuses_what_would_read_back_as_a_failure() {
1052 for bad in [f64::NAN, f64::NEG_INFINITY] {
1053 let e = Evaluation {
1054 value: 1.0,
1055 violation: bad,
1056 };
1057 assert!(serde_json::to_string(&e).is_err(), "{bad}");
1058 assert!(
1059 serde_json::to_string(&Evaluation::feasible(bad)).is_err(),
1060 "{bad}"
1061 );
1062 }
1063 }
1064
1065 #[test]
1068 fn failed_candidates_rank_last() {
1069 let cmaes = Cmaes::new(variables(3, 3.0, 1.0))
1070 .unwrap()
1071 .with_max_evaluations(300)
1072 .unwrap();
1073 let none = cmaes
1074 .minimize_constrained(5, |_| Evaluation::failed())
1075 .unwrap();
1076 assert_eq!((none.value, none.violation), (f64::INFINITY, f64::INFINITY));
1077 let json = serde_json::to_string(&none).unwrap();
1078 assert_eq!(serde_json::from_str::<Optimum>(&json).unwrap(), none);
1079 let cmaes = Cmaes::new(variables(3, 3.0, 1.0))
1081 .unwrap()
1082 .with_max_evaluations(20_000)
1083 .unwrap();
1084 let some = cmaes
1085 .minimize_constrained(5, |x| {
1086 if x[1] < 0.0 {
1087 Evaluation::failed()
1088 } else {
1089 sphere_above(x)
1090 }
1091 })
1092 .unwrap();
1093 assert_eq!(some.violation, 0.0);
1094 assert!(some.point[1] >= 0.0);
1095 assert!((some.value - 1.0).abs() < 1e-8, "{}", some.value);
1096 }
1097
1098 #[test]
1101 fn a_target_needs_a_feasible_point() {
1102 let cmaes = Cmaes::new(variables(3, 0.0, 1.0))
1103 .unwrap()
1104 .with_target(1.5)
1105 .unwrap();
1106 let optimum = cmaes.minimize_constrained(3, sphere_above).unwrap();
1107 assert_eq!(optimum.stop, Stop::Target);
1108 assert_eq!(optimum.violation, 0.0);
1109 assert!(optimum.value <= 1.5 && optimum.point[0] >= 1.0);
1110 }
1111
1112 fn variables(n: usize, start: f64, step: f64) -> Vec<Variable> {
1113 (0..n)
1114 .map(|i| Variable::new(format!("x{i}"), start, step).unwrap())
1115 .collect()
1116 }
1117
1118 #[test]
1120 fn parameters_follow_table_1() {
1121 let p = Parameters::new(10, 10);
1122 assert_eq!(p.mu, 5);
1123 let raw: Vec<f64> = (1..=5).map(|i| 5.5f64.ln() - (i as f64).ln()).collect();
1124 let sum: f64 = raw.iter().sum();
1125 for (w, r) in p.weights.iter().zip(&raw) {
1126 assert!((w - r / sum).abs() <= 1e-16);
1127 }
1128 assert!((p.weights.iter().sum::<f64>() - 1.0).abs() <= 1e-15);
1129 let mu_eff = sum * sum / raw.iter().map(|r| r * r).sum::<f64>();
1130 assert!((p.mu_eff - mu_eff).abs() <= 1e-13);
1131 assert!((p.c_sigma - (mu_eff + 2.0) / (15.0 + mu_eff)).abs() <= 1e-16);
1132 assert!((p.c_1 - 2.0 / (11.3f64.powi(2) + mu_eff)).abs() <= 1e-16);
1133 assert!((p.d_sigma - (1.0 + p.c_sigma)).abs() <= 1e-16);
1135 }
1136
1137 #[test]
1139 fn invariant_under_an_increasing_transformation() {
1140 let cmaes = Cmaes::new(variables(4, 1.0, 0.5))
1141 .unwrap()
1142 .with_max_evaluations(400)
1143 .unwrap();
1144 let a = cmaes.minimize(3, sphere).unwrap();
1145 let b = cmaes.minimize(3, |x| sphere(x).sqrt().exp()).unwrap();
1146 assert_eq!(a.point, b.point);
1147 assert_eq!(a.mean, b.mean);
1148 assert_eq!(a.evaluations, b.evaluations);
1149 }
1150
1151 #[test]
1153 fn a_seed_fixes_the_run() {
1154 let cmaes = Cmaes::new(variables(3, 2.0, 1.0)).unwrap();
1155 let a = cmaes.minimize(11, sphere).unwrap();
1156 let b = cmaes.minimize(11, sphere).unwrap();
1157 let c = cmaes.minimize(12, sphere).unwrap();
1158 assert_eq!(a, b);
1159 assert_ne!(a.point, c.point);
1160 }
1161
1162 #[test]
1164 fn bounds_hold_every_candidate() {
1165 let vars: Vec<Variable> = (0..3)
1166 .map(|i| {
1167 Variable::new(format!("x{i}"), 1.5, 1.0)
1168 .unwrap()
1169 .within(-0.5, 2.0)
1170 .unwrap()
1171 })
1172 .collect();
1173 let cmaes = Cmaes::new(vars).unwrap().with_target(1e-12).unwrap();
1174 let mut run = cmaes.start(5).unwrap();
1175 let optimum = loop {
1176 for x in run.candidates() {
1177 assert!(x.iter().all(|xi| (-0.5..=2.0).contains(xi)), "{x:?}");
1178 }
1179 let values: Vec<f64> = run.candidates().iter().map(|x| sphere(x)).collect();
1180 if let Some(optimum) = run.tell(&values).unwrap() {
1181 break optimum;
1182 }
1183 };
1184 assert_eq!(optimum.stop, Stop::Target);
1185 assert!(optimum.value <= 1e-12);
1186 }
1187
1188 #[test]
1190 fn steps_too_large_for_the_bounds_are_refused() {
1191 let vars: Vec<Variable> = (0..4)
1192 .map(|i| {
1193 Variable::new(format!("x{i}"), 0.0, 1e6)
1194 .unwrap()
1195 .within(-1e-6, 1e-6)
1196 .unwrap()
1197 })
1198 .collect();
1199 let error = Cmaes::new(vars).unwrap().start(1).unwrap_err();
1200 assert!(matches!(error, AnalysisError::OutOfBounds { draws } if draws == MAX_DRAWS));
1201 }
1202
1203 #[test]
1205 fn values_are_checked() {
1206 let cmaes = Cmaes::new(variables(2, 1.0, 0.5)).unwrap();
1207 let mut run = cmaes.start(1).unwrap();
1208 let lambda = run.candidates().len();
1209 assert_eq!(lambda, 6);
1210 let mut values = vec![1.0; lambda];
1211 values[4] = f64::NAN;
1212 assert!(matches!(
1213 run.tell(&values),
1214 Err(AnalysisError::Output { index: 4, .. })
1215 ));
1216 values[4] = f64::NEG_INFINITY;
1217 assert!(run.tell(&values).is_err());
1218 assert!(matches!(
1219 run.tell(&values[..3]),
1220 Err(AnalysisError::Length { length: 3, .. })
1221 ));
1222 values[4] = f64::INFINITY;
1223 assert!(run.tell(&values).unwrap().is_none());
1224 assert_eq!(run.generation(), 1);
1225 let values = vec![1.0; lambda];
1226 assert!(matches!(run.tell(&values), Ok(None)));
1227 let mut values = vec![1.0; lambda];
1229 values[0] = f64::NAN;
1230 assert!(matches!(
1231 run.tell(&values),
1232 Err(AnalysisError::Output { index, .. }) if index == 2 * lambda
1233 ));
1234 }
1235
1236 #[test]
1238 fn stops_are_reported() {
1239 let cmaes = Cmaes::new(variables(2, 1.0, 0.5)).unwrap();
1240 let flat = cmaes.minimize(1, |_| 7.0).unwrap();
1241 assert_eq!(flat.stop, Stop::TolFun);
1242 assert_eq!(flat.generations, 20);
1244 let short = cmaes
1245 .clone()
1246 .with_max_evaluations(30)
1247 .unwrap()
1248 .minimize(1, sphere)
1249 .unwrap();
1250 assert_eq!(short.stop, Stop::Evaluations);
1251 assert_eq!(short.evaluations, 30);
1252 let converged = cmaes
1253 .with_tolerance_x(1e-6)
1254 .unwrap()
1255 .with_tolerance_value(0.0)
1256 .unwrap()
1257 .minimize(1, sphere)
1258 .unwrap();
1259 assert_eq!(converged.stop, Stop::TolX);
1260 assert!(converged.spread < 1e-6 * 0.5);
1262 }
1263
1264 #[test]
1266 fn settings_are_checked() {
1267 let cmaes = Cmaes::new(variables(2, 0.0, 1.0)).unwrap();
1268 assert_eq!(cmaes.population(), 6);
1269 assert!(cmaes.clone().with_population(1).is_err());
1270 assert!(cmaes.clone().with_population(MAX_POPULATION + 1).is_err());
1271 assert!(cmaes.clone().with_max_evaluations(0).is_err());
1272 assert!(cmaes.clone().with_target(f64::NAN).is_err());
1273 assert!(cmaes.clone().with_target(f64::INFINITY).is_err());
1274 assert!(cmaes.clone().with_tolerance_x(-1.0).is_err());
1275 assert!(cmaes.clone().with_tolerance_value(f64::INFINITY).is_err());
1276 assert!(Cmaes::new(Vec::new()).is_err());
1277 }
1278
1279 #[test]
1281 fn settings_serialize_and_are_checked_on_reading() {
1282 let cmaes = Cmaes::new(variables(2, 0.0, 1.0))
1283 .unwrap()
1284 .with_target(0.5)
1285 .unwrap();
1286 let json = serde_json::to_string(&cmaes).unwrap();
1287 assert_eq!(serde_json::from_str::<Cmaes>(&json).unwrap(), cmaes);
1288 for (field, bad) in [
1289 ("\"population\":6", "\"population\":1"),
1290 ("\"max_evaluations\":10000", "\"max_evaluations\":0"),
1291 ("\"tolerance_x\":1e-12", "\"tolerance_x\":-1.0"),
1292 ] {
1293 assert!(json.contains(field), "{json}");
1294 let edited = json.replace(field, bad);
1295 assert!(serde_json::from_str::<Cmaes>(&edited).is_err(), "{edited}");
1296 }
1297 let empty = json.replace(
1298 &json[json.find('[').unwrap()..=json.find(']').unwrap()],
1299 "[]",
1300 );
1301 assert!(serde_json::from_str::<Cmaes>(&empty).is_err(), "{empty}");
1302 let extra = json.replacen('{', "{\"extra\":1,", 1);
1303 assert!(serde_json::from_str::<Cmaes>(&extra).is_err());
1304 }
1305
1306 #[test]
1308 fn smallest_populations_run() {
1309 for lambda in [2, 3] {
1310 let optimum = Cmaes::new(variables(2, 1.0, 0.5))
1311 .unwrap()
1312 .with_population(lambda)
1313 .unwrap()
1314 .with_target(1e-10)
1315 .unwrap()
1316 .minimize(1, sphere)
1317 .unwrap();
1318 assert_eq!(optimum.stop, Stop::Target, "λ = {lambda}");
1319 }
1320 }
1321
1322 #[test]
1324 fn stops_at_the_condition_limit() {
1325 let optimum = Cmaes::new(variables(2, 1.0, 0.5))
1326 .unwrap()
1327 .with_tolerance_x(0.0)
1328 .unwrap()
1329 .with_tolerance_value(0.0)
1330 .unwrap()
1331 .minimize(1, |x| x[0] * x[0] + 1e16 * x[1] * x[1])
1332 .unwrap();
1333 assert_eq!(optimum.stop, Stop::Condition);
1334 }
1335
1336 #[test]
1340 fn steps_far_apart_converge_alike() {
1341 let steps = [1e-4, 1e4];
1342 let vars = vec![
1343 Variable::new("small", steps[0], steps[0]).unwrap(),
1344 Variable::new("large", steps[1], steps[1]).unwrap(),
1345 ];
1346 let model = |x: &[f64]| (x[0] / 1e-4).powi(2) + (x[1] / 1e4).powi(2);
1347 let cmaes = Cmaes::new(vars)
1348 .unwrap()
1349 .with_tolerance_x(1e-6)
1350 .unwrap()
1351 .with_tolerance_value(0.0)
1352 .unwrap();
1353 let mut run = cmaes.start(1).unwrap();
1354 let optimum = loop {
1355 let values: Vec<f64> = run.candidates().iter().map(|x| model(x)).collect();
1356 if let Some(optimum) = run.tell(&values).unwrap() {
1357 break optimum;
1358 }
1359 };
1360 assert_eq!(optimum.stop, Stop::TolX);
1361 assert!(optimum.value < 1e-10, "{}", optimum.value);
1362 for i in 0..2 {
1363 let sd = run.sigma() * run.covariance()[i * 2 + i].sqrt();
1364 assert!(sd < 1e-6, "variable {i}: {sd} of its step");
1365 }
1366 assert!(optimum.spread < 1e-6 * steps[1]);
1367 }
1368
1369 #[test]
1372 fn infinite_results_read_back() {
1373 let cmaes = Cmaes::new(variables(1, 0.0, 1.0))
1374 .unwrap()
1375 .with_max_evaluations(50)
1376 .unwrap();
1377 let failed = cmaes.minimize(1, |_| f64::INFINITY).unwrap();
1378 assert_eq!(failed.value, f64::INFINITY);
1379 let json = serde_json::to_string(&failed).unwrap();
1380 assert!(json.contains("\"value\":null"), "{json}");
1381 assert_eq!(serde_json::from_str::<Optimum>(&json).unwrap(), failed);
1382 let overflowed = Optimum {
1383 spread: f64::INFINITY,
1384 ..failed.clone()
1385 };
1386 let json = serde_json::to_string(&overflowed).unwrap();
1387 assert!(json.contains("\"spread\":null"), "{json}");
1388 assert_eq!(serde_json::from_str::<Optimum>(&json).unwrap(), overflowed);
1389 let vars = vec![Variable::new("x", 0.0, 1000.0).unwrap()];
1392 let diverged = Cmaes::new(vars)
1393 .unwrap()
1394 .with_max_evaluations(1_000_000)
1395 .unwrap()
1396 .minimize(7, |x| x[0])
1397 .unwrap();
1398 assert_eq!(diverged.stop, Stop::Condition);
1399 }
1400
1401 #[test]
1403 fn overflowing_first_candidates_are_refused() {
1404 let vars = vec![Variable::new("x", 0.0, 1e308).unwrap()];
1405 let error = Cmaes::new(vars).unwrap().start(1).unwrap_err();
1406 assert!(matches!(
1407 error,
1408 AnalysisError::Domain { what, .. } if what.starts_with("first candidate")
1409 ));
1410 }
1411
1412 #[test]
1418 fn integer_draws_keep_the_margin() {
1419 let variables = vec![
1420 Variable::new("x0", 1.0, 0.5).unwrap(),
1421 Variable::new("x1", -2.0, 0.5).unwrap(),
1422 Variable::new("binary", 0.5, 1.0)
1423 .unwrap()
1424 .within(0.0, 1.0)
1425 .unwrap()
1426 .integer()
1427 .unwrap(),
1428 Variable::new("to an end", 3.0, 1.0)
1429 .unwrap()
1430 .within(-10.0, 10.0)
1431 .unwrap()
1432 .integer()
1433 .unwrap(),
1434 Variable::new("inner", -4.0, 2.0)
1435 .unwrap()
1436 .within(-10.0, 10.0)
1437 .unwrap()
1438 .integer()
1439 .unwrap(),
1440 ];
1441 let f = |x: &[f64]| {
1442 x[0] * x[0] + x[1] * x[1] + (1.0 - x[2]) + (x[3] - 10.0).abs() + (x[4] - 2.0).powi(2)
1443 };
1444 let mut run = Cmaes::new(variables).unwrap().start(3).unwrap();
1445 let n = 5;
1446 let alpha = run.integer_margin();
1447 assert_eq!(alpha, 1.0 / (5.0 * 8.0));
1448 let mut generations = 0;
1449 let mut corrected = 0;
1450 loop {
1451 let values: Vec<f64> = run.candidates().iter().map(|x| f(x)).collect();
1452 for x in run.candidates() {
1453 assert!(x[2] == 0.0 || x[2] == 1.0);
1454 assert!(x[3].fract() == 0.0 && (-10.0..=10.0).contains(&x[3]));
1455 }
1456 let done = run.tell(&values).unwrap();
1457 if done.is_some() {
1458 break;
1459 }
1460 generations += 1;
1461 for (j, (low, high)) in [(2, (0.0, 1.0)), (3, (-10.0, 10.0)), (4, (-10.0, 10.0))] {
1462 let step = run.variables[j].step();
1463 let spread =
1464 run.sigma() * run.margin_scale()[j] * step * run.covariance()[j * n + j].sqrt();
1465 let m = run.mean()[j];
1466 let (first, last) = (low + 0.5, high - 0.5);
1467 let floor = |p: f64| p * (1.0 - 1e-13);
1470 if m <= first || m > last {
1471 let threshold = if m <= first { first } else { last };
1472 let far = normal::cdf(-(m - threshold).abs() / spread);
1473 assert!(
1474 far >= floor(alpha),
1475 "gen {generations} x{j}: {far} < {alpha}"
1476 );
1477 } else {
1478 let value = crate::optimize::nearest_whole(m);
1479 let below = normal::cdf((value - 0.5 - m) / spread);
1480 let above = normal::cdf((m - value - 0.5) / spread);
1481 assert!(
1482 below >= floor(alpha / 2.0),
1483 "gen {generations} x{j}: {below}"
1484 );
1485 assert!(
1486 above >= floor(alpha / 2.0),
1487 "gen {generations} x{j}: {above}"
1488 );
1489 }
1490 if run.margin_scale()[j] != 1.0 {
1491 corrected += 1;
1492 }
1493 }
1494 assert_eq!(&run.margin_scale()[..2], &[1.0, 1.0]);
1495 }
1496 assert!(generations > 50, "{generations}");
1498 assert!(corrected > 0);
1499 }
1500
1501 #[test]
1506 #[expect(
1507 clippy::excessive_precision,
1508 reason = "17 digits of each 40-digit reference, which round to the nearest f64"
1509 )]
1510 fn margin_correction_matches_the_equations() {
1511 let variables = vec![
1512 Variable::new("x", 0.0, 1.0).unwrap(),
1513 Variable::new("k", 0.0, 1.0)
1514 .unwrap()
1515 .within(-10.0, 10.0)
1516 .unwrap()
1517 .integer()
1518 .unwrap(),
1519 ];
1520 let fresh = || {
1521 let mut run = Cmaes::new(variables.clone()).unwrap().start(1).unwrap();
1522 run.c = vec![1.0, 0.0, 0.0, 1.0];
1523 run
1524 };
1525 assert_eq!(fresh().integer_margin(), 1.0 / 12.0);
1526 let close = |got: f64, want: f64| (got - want).abs() <= 1e-14 * want.abs();
1527
1528 let mut run = fresh();
1531 (run.mean[1], run.sigma, run.a[1]) = (2.3, 0.125, 2.0);
1532 run.correct_margin();
1533 assert!(
1534 close(run.mean[1], 2.176_899_187_754_804_1),
1535 "{}",
1536 run.mean[1]
1537 );
1538 assert!(close(run.a[1], 3.127_161_079_343_547_5), "{}", run.a[1]);
1539 assert_eq!((run.mean[0], run.a[0]), (0.0, 1.0));
1540
1541 let mut run = fresh();
1543 (run.mean[1], run.sigma) = (-9.8, 0.1);
1544 run.correct_margin();
1545 assert!(
1546 close(run.mean[1], -9.638_299_412_710_063_8),
1547 "{}",
1548 run.mean[1]
1549 );
1550 assert_eq!(run.a[1], 1.0);
1551
1552 let reach_per_spread = -normal::quantile(1.0 / 12.0);
1555 let mut stepped = 0;
1556 for k in 0..2000 {
1557 let sigma = 0.01 + 1e-4 * f64::from(k);
1558 for (m, threshold) in [(-9.9, -9.5), (9.9, 9.5)] {
1559 let mut run = fresh();
1560 (run.mean[1], run.sigma) = (m, sigma);
1561 run.correct_margin();
1562 let reach = reach_per_spread * sigma;
1563 let gap = (run.mean[1] - threshold).abs();
1564 assert!(gap <= reach, "σ = {sigma}: {gap} > {reach}");
1565 assert!(reach - gap <= 4.0 * f64::EPSILON * 10.0, "σ = {sigma}");
1566 if (threshold + reach.copysign(m - threshold) - threshold).abs() > reach {
1567 stepped += 1;
1568 }
1569 }
1570 }
1571 assert!(stepped > 0, "no spread rounded outward");
1572
1573 let mut run = fresh();
1575 (run.mean[1], run.sigma) = (-0.499_999_999_999_999_94, 0.05);
1576 run.correct_margin();
1577 assert!(run.mean[1] > -0.5 && run.a[1] > 1.0, "{}", run.mean[1]);
1578
1579 for (m, sigma) in [(2.3, 1.0), (-9.6, 0.1)] {
1581 let mut run = fresh();
1582 (run.mean[1], run.sigma) = (m, sigma);
1583 run.correct_margin();
1584 assert_eq!((run.mean[1], run.a[1]), (m, 1.0));
1585 }
1586 }
1587
1588 #[test]
1591 fn integer_runs_reach_whole_minima() {
1592 let variables = vec![
1593 Variable::new("x", 2.5, 1.0).unwrap(),
1594 Variable::new("k", 2.3, 1.0)
1595 .unwrap()
1596 .within(-10.0, 10.0)
1597 .unwrap()
1598 .integer()
1599 .unwrap(),
1600 ];
1601 let optimum = Cmaes::new(variables)
1602 .unwrap()
1603 .with_target(1e-12)
1604 .unwrap()
1605 .minimize(5, |x| x[0] * x[0] + (x[1] - 3.0).powi(2))
1606 .unwrap();
1607 assert_eq!(optimum.stop, Stop::Target);
1608 assert_eq!(optimum.point[1], 3.0);
1609 }
1610}