simplers_optimization 0.2.0

A Rust implementation of the Simple(x) black-box optimization algorithm.
Documentation
use crate::point::*;
use crate::simplex::*;
use crate::search_space::*;
use priority_queue::PriorityQueue;
use ordered_float::OrderedFloat;
use num_traits::Float;
use std::rc::Rc;

/// Stores the parameters and current state of a search.
///
/// - `ValueFloat` is the float type used to represent the evaluations (such as f64)
/// - `CoordFloat` is the float type used to represent the coordinates (such as f32)
pub struct Optimizer<CoordFloat: Float, ValueFloat: Float>
{
   search_space: SearchSpace<CoordFloat, ValueFloat>,
   best_point: Rc<Point<CoordFloat, ValueFloat>>,
   k: ValueFloat,
   queue: PriorityQueue<Simplex<CoordFloat, ValueFloat>, OrderedFloat<ValueFloat>>
}

impl<CoordFloat: Float, ValueFloat: Float> Optimizer<CoordFloat, ValueFloat>
{
   /// Creates a new optimizer to explore the given search space with the iterator interface.
   ///
   /// Takes a function, a vector of intervals describing the input and a boolean describing wether it is a minimization problem (as oppozed to a miximization problem).
   /// Each cal to the `.next()` function (cf iterator trait) will run an iteration of search and output the best result so far.
   ///
   /// **Warning:** In d dimenssions, this function will perform d+1 evaluation (call to f) for the initialisation of the search (those should be taken into account when counting iterations).
   ///
   /// ```rust
   /// # fn main() {
   /// let f = |v| v[0] * v[1];
   /// let input_interval = vec![(-10., 10.), (-20., 20.)];
   /// let should_minimize = true;
   ///
   /// // runs the search for 30 iterations
   /// // then waits until we find a point good enough
   /// // finally stores the best value so far
   /// let (min_value, coordinates) = Optimizer::new(f, input_interval, should_minimize)
   ///                                          .skip(30)
   ///                                          .take_while(|(value,coordinates)| value > 1. )
   ///                                          .next().unwrap();
   ///
   /// println!("min value: {} found in [{}, {}]", min_value, coordinates[0], coordinates[1]);
   /// # }
   /// ```
   pub fn new(f: impl Fn(&[CoordFloat]) -> ValueFloat + 'static,
              input_interval: Vec<(CoordFloat, CoordFloat)>,
              should_minimize: bool)
              -> Self
   {
      // builds initial conditions
      let search_space = SearchSpace::new(f, input_interval, should_minimize);
      let initial_simplex = Simplex::initial_simplex(&search_space);

      // various values track through the iterations
      let best_point = initial_simplex.corners
                                      .iter()
                                      .max_by_key(|c| OrderedFloat(c.value))
                                      .expect("You need at least one dimension!")
                                      .clone();
      let k = initial_simplex.corners
                             .iter()
                             .map(|corner| {
                                initial_simplex.corners.iter().map(move |corner2| corner.compute_k(corner2))
                             })
                             .flatten()
                             .max_by_key(|&x| OrderedFloat(x))
                             .unwrap();

      // initialize priority queue
      // no need to evaluate the initial simplex as it will be popped immediatly
      let mut queue: PriorityQueue<Simplex<CoordFloat, ValueFloat>, OrderedFloat<ValueFloat>> =
         PriorityQueue::new();
      queue.push(initial_simplex, OrderedFloat(ValueFloat::zero()));

      Optimizer { search_space, best_point, k, queue }
   }

   /// Self contained optimization algorithm.
   ///
   /// Takes a function to maximize, a vector of intervals describing the input and a number of iterations.
   ///
   /// ```rust
   /// # fn main() {
   /// let f = |v| v[0] + v[1];
   /// let input_interval = vec![(-10., 10.), (-20., 20.)];
   /// let nb_iterations = 100;
   ///
   /// let (max_value, coordinates) = Optimizer::maximize(f, input_interval, nb_iterations);
   /// println!("max value: {} found in [{}, {}]", max_value, coordinates[0], coordinates[1]);
   /// # }
   /// ```
   pub fn maximize(f: impl Fn(&[CoordFloat]) -> ValueFloat + 'static,
                   input_interval: Vec<(CoordFloat, CoordFloat)>,
                   nb_iterations: usize)
                   -> (ValueFloat, Coordinates<CoordFloat>)
   {
      let initial_iteration_number = input_interval.len() + 1;
      let should_minimize = false;
      Optimizer::new(f, input_interval, should_minimize).skip(nb_iterations - initial_iteration_number)
                                                        .next()
                                                        .unwrap()
   }

   /// Self contained optimization algorithm.
   ///
   /// Takes a function to minimize, a vector of intervals describing the input and a number of iterations.
   ///
   /// ```rust
   /// # fn main() {
   /// let f = |v| v[0] * v[1];
   /// let input_interval = vec![(-10., 10.), (-20., 20.)];
   /// let nb_iterations = 100;
   ///
   /// let (min_value, coordinates) = Optimizer::minimize(f, input_interval, nb_iterations);
   /// println!("min value: {} found in [{}, {}]", min_value, coordinates[0], coordinates[1]);
   /// # }
   /// ```
   pub fn minimize(f: impl Fn(&[CoordFloat]) -> ValueFloat + 'static,
                   input_interval: Vec<(CoordFloat, CoordFloat)>,
                   nb_iterations: usize)
                   -> (ValueFloat, Coordinates<CoordFloat>)
   {
      let initial_iteration_number = input_interval.len() + 1;
      let should_minimize = true;
      Optimizer::new(f, input_interval, should_minimize).skip(nb_iterations - initial_iteration_number)
                                                        .next()
                                                        .unwrap()
   }
}

/// implements iterator for the Optimizer to give full control on the stopping condition to the user
impl<CoordFloat: Float, ValueFloat: Float> Iterator for Optimizer<CoordFloat, ValueFloat>
{
   type Item = (ValueFloat, Coordinates<CoordFloat>);

   /// runs an iteration of the optimization algorithm and returns the best result so far
   fn next(&mut self) -> Option<Self::Item>
   {
      // gets an up to date simplex
      let simplex = self.queue.pop().expect("Impossible: The queue cannot be empty!").0;

      // evaluate the center of the simplex
      let coordinates = simplex.center.clone();
      let value = self.search_space.evaluate(&coordinates);
      let new_point = Rc::new(Point { coordinates, value });

      // updates k
      let k = simplex.corners
                     .iter()
                     .map(|corner| corner.compute_k(&new_point))
                     .max_by_key(|x| OrderedFloat(*x))
                     .expect("You need at least one point");
      if k > self.k
      {
         // updates k and reevaluates all simplexes in memory
         self.k = k;
         self.queue.iter_mut().for_each(|(s, e)| *e = OrderedFloat(s.evaluate(k)));
      }
      let k = self.k;

      // splits the simplex around its center and push the subsimplexes into the queue
      simplex.split(new_point.clone())
             .into_iter()
             .map(|s| (OrderedFloat(s.evaluate(k)), s))
             .for_each(|(e, s)| {
                self.queue.push(s, e);
             });

      // updates the difference
      if value > self.best_point.value
      {
         self.best_point = new_point;
      }

      // gets the best value so far
      let best_value =
         if self.search_space.minimize { -self.best_point.value } else { self.best_point.value };
      let best_coordinate = self.search_space.to_hypercube(self.best_point.coordinates.clone());
      Some((best_value, best_coordinate))
   }
}