1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
//! Types related to initializing a [`CMAES`]. See [`CMAESOptions`] for full documentation.
use nalgebra::DVector;
use std::time::Duration;
use crate::mode::Mode;
use crate::parameters::Weights;
#[cfg(feature = "plotters")]
use crate::PlotOptions;
use crate::CMAES;
/// A builder for [`CMAES`]. Used to adjust parameters of the algorithm to each particular
/// problem and to change other options. See the fields and methods for a full list of options.
///
/// # Examples
///
/// ```
/// use cmaes::{CMAESOptions, DVector, PlotOptions, Weights};
///
/// let function = |x: &DVector<f64>| x.magnitude();
/// let dim = 3;
/// let mut cmaes_state = CMAESOptions::new(vec![2.0; dim], 5.0)
/// .weights(Weights::Positive)
/// .population_size(100)
/// .enable_plot(PlotOptions::new(0, false))
/// .enable_printing(200)
/// .build(function)
/// .unwrap();
/// ```
#[derive(Clone)]
pub struct CMAESOptions {
/// The mode to use when optimizing the objective function. Default value is [`Mode::Minimize`].
pub mode: Mode,
/// Initial mean of the search distribution, also used to determine the problem dimension (`N`).
/// This should be set to a first guess at the solution.
pub initial_mean: DVector<f64>,
/// Initial step size of the search distribution (`sigma0`). This should be set to a first guess
/// at how far the solution is from the initial mean. To scale the initial search range
/// separately in each dimension, the appropriate transformation should be made to the objective
/// function itself using [`Scale`][crate::objective_function::Scale]
pub initial_step_size: f64,
/// Number of points to generate each generation (`lambda`). Default value is
/// `4 + floor(3 * ln(dimensions))`.
///
/// A larger population size will increase the robustness of the algorithm and help avoid
/// converging to local optima, but will lead to a slower convergence rate. Conversely, a lower
/// value will reduce the robustness of the algorithm but will lead to a higher convergence
/// rate. Generally, the population size should only be left at the default value or increased.
/// It is generally useful to restart the algorithm repeatedly with increasing population sizes,
/// which can be done automatically with the [`IPOP`][crate::restart::IPOP] and
/// [`BIPOP`][crate::restart::BIPOP] restart strategies (see the [`restart`][crate::restart]
/// module).
pub population_size: usize,
/// The distribution to use when assigning weights to individuals. Default value is
/// [`Weights::Negative`].
pub weights: Weights,
/// Whether to perform the state update in parallel using multiple threads. Default value is
/// `false`.
///
/// This will likely degrade performance for smaller population sizes but can be a major
/// performance boost for very large population sizes (e.g., `512 * default`) as might be used
/// with [`IPOP`][crate::restart::IPOP] or [`BIPOP`][crate::restart::BIPOP]). Due to floating
/// point errors this will reduce the determinism guarantee of the [`seed`][Self::seed] option,
/// but the degree to which determinism is affected can vary significantly.
pub parallel_update: bool,
/// The learning rate for adapting the mean. Can be reduced for noisy functions. Default value
/// is `1.0`.
pub cm: f64,
/// The value to use for the
/// [`TerminationReason::MaxFunctionEvals`][crate::TerminationReason::MaxFunctionEvals]
/// termination criterion. Default value is `None`.
pub max_function_evals: Option<usize>,
/// The value to use for the
/// [`TerminationReason::MaxGenerations`][crate::TerminationReason::MaxGenerations]
/// termination criterion. Default value is `None`.
pub max_generations: Option<usize>,
/// The value to use for the [`TerminationReason::MaxTime`][crate::TerminationReason::MaxTime]
/// termination criterion. Default value is `None`.
pub max_time: Option<Duration>,
/// The value to use for the
/// [`TerminationReason::FunTarget`][crate::TerminationReason::FunTarget] termination criterion.
/// Default value is `None`.
pub fun_target: Option<f64>,
/// The value to use for the [`TerminationReason::TolFun`][crate::TerminationReason::TolFun]
/// termination criterion. Default value is `1e-12`.
pub tol_fun: f64,
/// The value to use for the
/// [`TerminationReason::TolFunRel`][crate::TerminationReason::TolFunRel] termination criterion.
/// Default value is `0` (disabled).
pub tol_fun_rel: f64,
/// The value to use for the
/// [`TerminationReason::TolFunHist`][crate::TerminationReason::TolFunHist] termination
/// criterion. Default value is `1e-12`.
pub tol_fun_hist: f64,
/// The value to use for the [`TerminationReason::TolX`][crate::TerminationReason::TolX]
/// termination criterion. Default value is `1e-12 * initial_step_size`, used if this field is
/// `None`.
pub tol_x: Option<f64>,
/// The minimum number of generations over which to measure the
/// [`TerminationReason::TolStagnation`][crate::TerminationReason::TolStagnation] termination
/// criterion. Default value is `100 + 100 * dimensions^1.5 / lambda`, used if this field is
/// `None`.
pub tol_stagnation: Option<usize>,
/// The value to use for the [`TerminationReason::TolXUp`][crate::TerminationReason::TolXUp]
/// termination criterion. Default value is `1e+8`.
pub tol_x_up: f64,
/// The value to use for the
/// [`TerminationReason::TolConditionCov`][crate::TerminationReason::TolConditionCov]
/// termination criterion. Default value is `1e+14`.
pub tol_condition_cov: f64,
/// The seed for the RNG used in the algorithm. Can be set manually for deterministic runs. By
/// default a random seed is used if this field is `None`.
///
/// If used in conjunction with the [`parallel_update`][Self::parallel_update] option, the
/// determinism guarantee is reduced due to floating point errors.
pub seed: Option<u64>,
/// Options for the data plot. Default value is `None`, meaning no plot will be generated. See
/// [`Plot`][crate::plotting::Plot].
#[cfg(feature = "plotters")]
pub plot_options: Option<PlotOptions>,
/// How many function evaluations to wait for in between each automatic
/// [`CMAES::print_info`] call. Default value is `None`, meaning no info will be
/// automatically printed.
pub print_gap_evals: Option<usize>,
}
impl CMAESOptions {
/// Creates a new `CMAESOptions` with default values. Set individual options using the provided
/// methods.
///
/// - `initial_mean` should be set to a first guess at the solution and is used to determine the
/// problem dimension.
/// - `initial_step_size` should be set to a first guess at how far the solution is in each
/// dimension from the initial mean (`solution[i] ~= [initial_mean[i] - initial_step_size,
/// initial_mean[i] + initial_step_size]`). Must be positive.
pub fn new<V: Into<DVector<f64>>>(initial_mean: V, initial_step_size: f64) -> Self {
let initial_mean = initial_mean.into();
let dimensions = initial_mean.len();
Self {
mode: Mode::Minimize,
initial_mean,
initial_step_size,
population_size: 4 + (3.0 * (dimensions as f64).ln()).floor() as usize,
weights: Weights::Negative,
parallel_update: false,
cm: 1.0,
max_function_evals: None,
max_generations: None,
max_time: None,
fun_target: None,
tol_fun: 1e-12,
tol_fun_rel: 0.0,
tol_fun_hist: 1e-12,
tol_x: None,
tol_stagnation: None,
tol_x_up: 1e8,
tol_condition_cov: 1e14,
seed: None,
#[cfg(feature = "plotters")]
plot_options: None,
print_gap_evals: None,
}
}
/// Changes the optimization mode.
pub fn mode(mut self, mode: Mode) -> Self {
self.mode = mode;
self
}
/// Changes the initial mean.
pub fn initial_mean<V: Into<DVector<f64>>>(mut self, initial_mean: V) -> Self {
self.initial_mean = initial_mean.into();
self
}
/// Changes the initial step size. Must be positive.
pub fn initial_step_size(mut self, initial_step_size: f64) -> Self {
self.initial_step_size = initial_step_size;
self
}
/// Changes the population size from the default value. Must be at least 2.
pub fn population_size(mut self, population_size: usize) -> Self {
self.population_size = population_size;
self
}
/// Changes the weight distribution from the default value. See [`Weights`] for
/// possible distributions.
pub fn weights(mut self, weights: Weights) -> Self {
self.weights = weights;
self
}
/// Sets whether to perform the state update in parallel.
pub fn parallel_update(mut self, parallel_update: bool) -> Self {
self.parallel_update = parallel_update;
self
}
/// Changes the learning rate for the mean from the default value. Must be between `0.0` and
/// `1.0`.
pub fn cm(mut self, cm: f64) -> Self {
self.cm = cm;
self
}
/// Changes the value for the `MaxFunctionEvals` termination criterion from the default value
/// (see [`TerminationReason::MaxFunctionEvals`][crate::TerminationReason::MaxFunctionEvals]).
pub fn max_function_evals(mut self, max_function_evals: usize) -> Self {
self.max_function_evals = Some(max_function_evals);
self
}
/// Changes the value for the `MaxGenerations` termination criterion from the default value
/// (see [`TerminationReason::MaxGenerations`][crate::TerminationReason::MaxGenerations]).
pub fn max_generations(mut self, max_generations: usize) -> Self {
self.max_generations = Some(max_generations);
self
}
/// Changes the value for the `MaxTime` termination criterion from the default value
/// (see [`TerminationReason::MaxTime`][crate::TerminationReason::MaxTime]).
pub fn max_time(mut self, max_time: Duration) -> Self {
self.max_time = Some(max_time);
self
}
/// Changes the value for the `FunTarget` termination criterion from the default value (see
/// [`TerminationReason::FunTarget`][crate::TerminationReason::FunTarget]).
pub fn fun_target(mut self, fun_target: f64) -> Self {
self.fun_target = Some(fun_target);
self
}
/// Changes the value for the `TolFun` termination criterion from the default value (see
/// [`TerminationReason::TolFun`][crate::TerminationReason::TolFun]).
pub fn tol_fun(mut self, tol_fun: f64) -> Self {
self.tol_fun = tol_fun;
self
}
/// Changes the value for the `TolFunRel` termination criterion from the default value (see
/// [`TerminationReason::TolFunRel`][crate::TerminationReason::TolFunRel]).
pub fn tol_fun_rel(mut self, tol_fun_rel: f64) -> Self {
self.tol_fun_rel = tol_fun_rel;
self
}
/// Changes the value for the `TolFunHist` termination criterion from the default value (see
/// [`TerminationReason::TolFunHist`][crate::TerminationReason::TolFunHist]).
pub fn tol_fun_hist(mut self, tol_fun_hist: f64) -> Self {
self.tol_fun_hist = tol_fun_hist;
self
}
/// Changes the value for the `TolX` termination criterion from the default value (see
/// [`TerminationReason::TolX`][crate::TerminationReason::TolX]).
pub fn tol_x(mut self, tol_x: f64) -> Self {
self.tol_x = Some(tol_x);
self
}
/// Changes the minimum value for the `TolStagnation` termination criterion from the default
/// value (see [`TerminationReason::TolStagnation`][crate::TerminationReason::TolStagnation]).
pub fn tol_stagnation(mut self, tol_stagnation: usize) -> Self {
self.tol_stagnation = Some(tol_stagnation);
self
}
/// Changes the value for the `TolXUp` termination criterion from the default value (see
/// [`TerminationReason::TolXUp`][crate::TerminationReason::TolXUp]).
pub fn tol_x_up(mut self, tol_x_up: f64) -> Self {
self.tol_x_up = tol_x_up;
self
}
/// Changes the value for the `TolConditionCov` termination criterion from the default value
/// (see [`TerminationReason::TolConditionCov`][crate::TerminationReason::TolConditionCov]).
pub fn tol_condition_cov(mut self, tol_condition_cov: f64) -> Self {
self.tol_condition_cov = tol_condition_cov;
self
}
/// Sets the seed for the RNG.
pub fn seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
/// Enables recording of a data plot for various state variables of the algorithm. See
/// [`Plot`][crate::plotting::Plot].
#[cfg(feature = "plotters")]
pub fn enable_plot(mut self, plot_options: PlotOptions) -> Self {
self.plot_options = Some(plot_options);
self
}
/// Enables automatic printing of various state variables of the algorithm. A minimum of
/// `min_gap_evals` function evaluations will be waited for between each
/// [`CMAES::print_info`] call, but it will always be called for the first few generations.
pub fn enable_printing(mut self, min_gap_evals: usize) -> Self {
self.print_gap_evals = Some(min_gap_evals);
self
}
/// Attempts to build the [`CMAES`] using the chosen options.
pub fn build<F>(self, objective_function: F) -> Result<CMAES<F>, InvalidOptionsError> {
CMAES::new(objective_function, self)
}
}
/// Represents invalid options for CMA-ES.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum InvalidOptionsError {
/// The number of dimensions is set to zero.
Dimensions,
/// The population size is too small (must be at least 2).
PopulationSize,
/// The initial step size is negative or non-normal.
InitialStepSize,
/// The learning rate is outside the valid range (`0.0` to `1.0`).
Cm,
}
/// Returns whether the initial step size is valid (greater than zero and normal)
pub(crate) fn is_initial_step_size_valid(step_size: f64) -> bool {
step_size.is_normal() && step_size > 0.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build() {
let dummy_function = |_: &DVector<f64>| 0.0;
assert!(CMAESOptions::new(vec![1.0; 5], 1.0)
.build(dummy_function)
.is_ok());
assert!(matches!(
CMAESOptions::new(vec![1.0; 5], 1.0)
.population_size(1)
.build(dummy_function),
Err(InvalidOptionsError::PopulationSize),
));
assert!(matches!(
CMAESOptions::new(vec![1.0; 5], 0.0).build(dummy_function),
Err(InvalidOptionsError::InitialStepSize),
));
assert!(matches!(
CMAESOptions::new(vec![1.0; 5], -1.0).build(dummy_function),
Err(InvalidOptionsError::InitialStepSize),
));
assert!(matches!(
CMAESOptions::new(vec![1.0; 5], f64::NAN).build(dummy_function),
Err(InvalidOptionsError::InitialStepSize),
));
assert!(matches!(
CMAESOptions::new(vec![1.0; 5], f64::INFINITY).build(dummy_function),
Err(InvalidOptionsError::InitialStepSize),
));
assert!(matches!(
CMAESOptions::new(vec![], 1.0).build(dummy_function),
Err(InvalidOptionsError::Dimensions),
));
assert!(matches!(
CMAESOptions::new(vec![1.0; 5], 1.0)
.cm(2.0)
.build(dummy_function),
Err(InvalidOptionsError::Cm),
));
assert!(matches!(
CMAESOptions::new(vec![1.0; 5], 1.0)
.cm(-1.0)
.build(dummy_function),
Err(InvalidOptionsError::Cm),
));
assert!(matches!(
CMAESOptions::new(vec![1.0; 5], 1.0)
.cm(f64::NAN)
.build(dummy_function),
Err(InvalidOptionsError::Cm),
));
assert!(matches!(
CMAESOptions::new(vec![1.0; 5], 1.0)
.cm(f64::INFINITY)
.build(dummy_function),
Err(InvalidOptionsError::Cm),
));
}
}