use rand::Rng;
use rand_chacha::ChaChaRng;
use super::options::InvalidRestartStrategyOptionsError;
use super::strategy::{RestartControl, Strategy};
use crate::{options, CMAESOptions, TerminationData, TerminationReason, CMAES};
const MAX_IPOP_RUNS: usize = 10;
const DEFAULT_IPOP_INCREASE_FACTOR: usize = 2;
const DEFAULT_LR_INITIAL_STEP_SIZE_FACTOR: f64 = 0.2;
#[derive(Clone, Debug)]
pub struct BIPOP {
ipop_runs: usize,
ipop_function_evals: usize,
ipop_current_multiplier: usize,
ipop_increase_factor: usize,
last_ipop_function_evals_used: usize,
last_ipop_multiplier: usize,
lr_initial_step_size_factor: f64,
lr_function_evals: usize,
}
impl BIPOP {
pub fn new(
lr_initial_step_size_factor: Option<f64>,
ipop_increase_factor: Option<usize>,
) -> Result<Self, InvalidRestartStrategyOptionsError> {
if let Some(step_size_factor) = lr_initial_step_size_factor {
if !options::is_initial_step_size_valid(step_size_factor) {
return Err(InvalidRestartStrategyOptionsError::InitialStepSize);
}
}
if let Some(increase_factor) = ipop_increase_factor {
if increase_factor == 0 {
return Err(InvalidRestartStrategyOptionsError::PopulationSize);
}
}
Ok(Self {
ipop_runs: 0,
ipop_function_evals: 0,
ipop_current_multiplier: 1,
ipop_increase_factor: ipop_increase_factor.unwrap_or(DEFAULT_IPOP_INCREASE_FACTOR),
last_ipop_function_evals_used: 0,
last_ipop_multiplier: 1,
lr_initial_step_size_factor: lr_initial_step_size_factor
.unwrap_or(DEFAULT_LR_INITIAL_STEP_SIZE_FACTOR),
lr_function_evals: 0,
})
}
fn get_initial_step_size_lr(&self, search_range_size: f64, rng: &mut ChaChaRng) -> f64 {
search_range_size * self.lr_initial_step_size_factor * 10f64.powf(-2.0 * rng.gen::<f64>())
}
fn get_population_size_lr(&self, default_population_size: usize, rng: &mut ChaChaRng) -> usize {
let multiplier = (self.last_ipop_multiplier as f64 / 2.0)
.powf(rng.gen::<f64>().powi(2))
.max(1.0);
(default_population_size as f64 * multiplier).floor() as usize
}
fn get_initial_step_size_ipop(&self, search_range_size: f64) -> f64 {
search_range_size / 5.0
}
fn next_run_lr<F, R: FnOnce(&mut CMAES<F>) -> TerminationData>(
&mut self,
mut options: CMAESOptions,
search_range_size: f64,
objective_function: F,
run: R,
rng: &mut ChaChaRng,
) -> (CMAES<F>, Vec<TerminationReason>, RestartControl) {
options.initial_step_size = self.get_initial_step_size_lr(search_range_size, rng);
options.population_size = self.get_population_size_lr(options.population_size, rng);
options.max_function_evals = options
.max_function_evals
.map(|max_fevals| max_fevals.min(self.last_ipop_function_evals_used / 2));
let mut cmaes_state = options.build(objective_function).unwrap();
let results = run(&mut cmaes_state);
self.lr_function_evals += cmaes_state.function_evals();
(cmaes_state, results.reasons, RestartControl::Continue)
}
fn next_run_ipop<F, R: FnOnce(&mut CMAES<F>) -> TerminationData>(
&mut self,
mut options: CMAESOptions,
search_range_size: f64,
objective_function: F,
run: R,
) -> (CMAES<F>, Vec<TerminationReason>, RestartControl) {
options.initial_step_size = self.get_initial_step_size_ipop(search_range_size);
options.population_size *= self.ipop_current_multiplier;
let mut cmaes_state = options.build(objective_function).unwrap();
let results = run(&mut cmaes_state);
self.ipop_runs += 1;
self.ipop_function_evals += cmaes_state.function_evals();
self.last_ipop_function_evals_used = cmaes_state.function_evals();
self.last_ipop_multiplier = self.ipop_current_multiplier;
self.ipop_current_multiplier *= self.ipop_increase_factor;
let control = if self.ipop_runs >= MAX_IPOP_RUNS {
RestartControl::MaxRunsReached
} else {
RestartControl::Continue
};
(cmaes_state, results.reasons, control)
}
}
impl Default for BIPOP {
fn default() -> Self {
Self::new(None, None).unwrap()
}
}
impl Strategy for BIPOP {
fn has_zero_max_runs(&self) -> bool {
false
}
fn next_run<F, R: FnOnce(&mut CMAES<F>) -> TerminationData>(
&mut self,
options: CMAESOptions,
search_range_size: f64,
objective_function: F,
run: R,
rng: &mut ChaChaRng,
) -> (CMAES<F>, Vec<TerminationReason>, RestartControl) {
if self.ipop_runs != 0 && self.lr_function_evals < self.ipop_function_evals {
self.next_run_lr(options, search_range_size, objective_function, run, rng)
} else {
self.next_run_ipop(options, search_range_size, objective_function, run)
}
}
fn get_algorithm_name(&self) -> &'static str {
"BIPOP-aCMA-ES"
}
fn get_parameters_as_strings(&self) -> Vec<(String, String)> {
[
(
"lr_sigma0_factor".to_string(),
format!("{}", self.lr_initial_step_size_factor),
),
(
"ipop_increase_factor".to_string(),
format!("{}", self.ipop_increase_factor),
),
]
.into()
}
}
#[cfg(test)]
mod tests {
use nalgebra::DVector;
use rand::SeedableRng;
use super::*;
#[test]
fn test_new() {
assert!(BIPOP::new(None, None).is_ok());
assert!(BIPOP::new(Some(1.0), Some(3)).is_ok());
assert!(matches!(
BIPOP::new(Some(-1.0), None),
Err(InvalidRestartStrategyOptionsError::InitialStepSize),
));
assert!(matches!(
BIPOP::new(None, Some(0)),
Err(InvalidRestartStrategyOptionsError::PopulationSize),
));
}
#[test]
fn test_get_initial_step_size_lr() {
let bipop = BIPOP::default();
let mut rng = ChaChaRng::seed_from_u64(rand::random());
for _ in 0..1000 {
let initial_step_size = bipop.get_initial_step_size_lr(100.0, &mut rng);
assert!(initial_step_size >= 0.2, "{}", initial_step_size);
assert!(initial_step_size <= 20.0, "{}", initial_step_size);
}
}
#[test]
fn test_get_population_size_lr() {
let mut bipop = BIPOP::default();
let mut rng = ChaChaRng::seed_from_u64(rand::random());
bipop.last_ipop_multiplier = 10;
for _ in 0..1000 {
let population_size = bipop.get_population_size_lr(200, &mut rng);
assert!(population_size >= 200, "{}", population_size);
assert!(population_size <= 200 * 5, "{}", population_size);
}
}
#[test]
fn test_get_initial_step_size_ipop() {
let bipop = BIPOP::default();
assert_eq!(20.0, bipop.get_initial_step_size_ipop(100.0));
}
#[test]
fn test_bipop_lr() {
let mut bipop = BIPOP::default();
let function = |x: &DVector<f64>| x.magnitude();
for _ in 0..20 {
let (_, _, control) = bipop.next_run_lr(
CMAESOptions::new(vec![1.0; 2], 0.5),
1.0,
function,
|state| state.run(),
&mut ChaChaRng::seed_from_u64(rand::random()),
);
assert!(!control.should_terminate());
}
assert_eq!(0, bipop.ipop_runs);
assert_eq!(1, bipop.ipop_current_multiplier);
}
#[test]
fn test_bipop_ipop() {
let mut bipop = BIPOP::default();
let function = |x: &DVector<f64>| x.magnitude();
for i in 0..10 {
let (_, _, control) = bipop.next_run_ipop(
CMAESOptions::new(vec![1.0; 2], 0.5),
1.0,
function,
|state| state.run(),
);
assert_eq!(i + 1 == 10, control.should_terminate());
}
assert_eq!(10, bipop.ipop_runs);
assert_eq!(2usize.pow(10), bipop.ipop_current_multiplier);
assert_eq!(2usize.pow(9), bipop.last_ipop_multiplier);
}
#[test]
fn test_bipop() {
let mut bipop = BIPOP::default();
let function = |x: &DVector<f64>| x.magnitude();
let _ = bipop.next_run(
CMAESOptions::new(vec![1.0; 2], 0.5),
1.0,
function,
|state| state.run(),
&mut ChaChaRng::seed_from_u64(rand::random()),
);
assert_eq!(1, bipop.ipop_runs);
assert_eq!(2, bipop.ipop_current_multiplier);
assert_eq!(1, bipop.last_ipop_multiplier);
assert_eq!(0, bipop.lr_function_evals);
let bipop_fevals = bipop.ipop_function_evals;
assert!(bipop_fevals > 0);
let _ = bipop.next_run(
CMAESOptions::new(vec![1.0; 2], 0.5),
1.0,
function,
|state| state.run(),
&mut ChaChaRng::seed_from_u64(rand::random()),
);
assert_eq!(1, bipop.ipop_runs);
assert_eq!(2, bipop.ipop_current_multiplier);
assert_eq!(1, bipop.last_ipop_multiplier);
assert!(bipop.lr_function_evals > 0);
assert_eq!(bipop_fevals, bipop.ipop_function_evals);
}
}