coco-rs 0.5.0

Rust bindings for COCO benchmarking framework
Documentation
use coco_sys::{coco_observer_t, coco_problem_t, coco_random_state_t, coco_suite_t};
use std::{
    ffi::{CStr, CString},
    marker::PhantomData,
    ops::RangeInclusive,
    ptr,
};

pub enum LogLevel {
    Error,
    Warning,
    Info,
    Debug,
}
impl Default for LogLevel {
    fn default() -> Self {
        LogLevel::Info
    }
}
impl LogLevel {
    pub fn as_str(&self) -> &'static str {
        match self {
            LogLevel::Error => "error",
            LogLevel::Warning => "warning",
            LogLevel::Info => "info",
            LogLevel::Debug => "debug",
        }
    }
}

/// Sets COCO's log level.
pub fn set_log_level(level: LogLevel) {
    let level = CString::new(level.as_str()).unwrap();
    unsafe {
        coco_sys::coco_set_log_level(level.as_ptr());
    }
}

/// A COCO suite
pub struct Suite {
    inner: *mut coco_suite_t,
}

unsafe impl Send for Suite {}

pub enum SuiteName {
    Bbob,
    BbobBiobj,
    BbobBiobjExt,
    BbobLargescale,
    BbobConstrained,
    BbobMixint,
    BbobBiobjMixint,
    Toy,
}
impl SuiteName {
    pub fn as_str(&self) -> &'static str {
        match self {
            SuiteName::Bbob => "bbob",
            SuiteName::BbobBiobj => "bbob-biobj",
            SuiteName::BbobBiobjExt => "bbob-biobj-ext",
            SuiteName::BbobLargescale => "bbob-largescale",
            SuiteName::BbobConstrained => "bbob-constrained",
            SuiteName::BbobMixint => "bbob-mixint",
            SuiteName::BbobBiobjMixint => "bbob-biobj-mixint",
            SuiteName::Toy => "toy",
        }
    }
}

impl Suite {
    /// Instantiates the specified COCO suite.
    ///
    /// # suite_instance
    /// A string used for defining the suite instances. Two ways are supported:
    /// - "year: YEAR", where YEAR is the year of the BBOB workshop, includes the instances (to be) used in that
    /// year's workshop;
    /// - "instances: VALUES", where VALUES are instance numbers from 1 on written as a comma-separated list or a
    /// range m-n.
    ///
    /// # suite_options
    /// A string of pairs "key: value" used to filter the suite (especially useful for
    /// parallelizing the experiments). Supported options:
    /// - "dimensions: LIST", where LIST is the list of dimensions to keep in the suite (range-style syntax is
    /// not allowed here),
    /// - "dimension_indices: VALUES", where VALUES is a list or a range of dimension indices (starting from 1) to keep
    /// in the suite, and
    /// - "function_indices: VALUES", where VALUES is a list or a range of function indices (starting from 1) to keep
    /// in the suite, and
    /// - "instance_indices: VALUES", where VALUES is a list or a range of instance indices (starting from 1) to keep
    /// in the suite.
    pub fn new(name: SuiteName, instance: &str, options: &str) -> Option<Suite> {
        let name = CString::new(name.as_str()).unwrap();
        let instance = CString::new(instance).unwrap();
        let options = CString::new(options).unwrap();

        let inner =
            unsafe { coco_sys::coco_suite(name.as_ptr(), instance.as_ptr(), options.as_ptr()) };

        if inner.is_null() {
            None
        } else {
            Some(Suite { inner })
        }
    }

    /// Returns the next problem or `None` when the suite completed.
    pub fn next_problem<'s>(&'s mut self, observer: Option<&mut Observer>) -> Option<Problem<'s>> {
        let observer = observer.map(|o| o.inner).unwrap_or(ptr::null_mut());
        let inner = unsafe { coco_sys::coco_suite_get_next_problem(self.inner, observer) };

        if inner.is_null() {
            return None;
        }

        unsafe {
            coco_sys::coco_suite_forget_current_problem(self.inner);
        }

        let mut function = 0;
        let mut dimension = 0;
        let mut instance = 0;

        unsafe {
            let suite_index = coco_sys::coco_problem_get_suite_dep_index(inner);

            coco_sys::coco_suite_decode_problem_index(
                self.inner,
                suite_index,
                &mut function,
                &mut dimension,
                &mut instance,
            );
        }

        Some(Problem {
            inner,
            function: function as usize,
            dimension: dimension as usize,
            instance: instance as usize,
            _phantom: PhantomData,
        })
    }

    /// Returns the problem for the given function, dimension and instance.
    ///
    /// While a suite can contain multiple problems with equal function, dimension and instance, this
    /// function always returns the first problem in the suite with the given function, dimension and instance
    /// values. If the given values don't correspond to a problem, the function returns `None`.
    pub fn problem_by_function_dimension_instance(
        &mut self,
        function: usize,
        dimension: usize,
        instance: usize,
    ) -> Option<Problem> {
        let problem = unsafe {
            coco_sys::coco_suite_get_problem_by_function_dimension_instance(
                self.inner,
                function as u64,
                dimension as u64,
                instance as u64,
            )
        };

        if problem.is_null() {
            return None;
        }

        Some(Problem {
            inner: problem,
            function,
            dimension,
            instance,
            _phantom: PhantomData,
        })
    }

    /// Returns the total number of problems in the suite.
    pub fn number_of_problems(&self) -> usize {
        unsafe {
            coco_sys::coco_suite_get_number_of_problems(self.inner)
                .try_into()
                .unwrap()
        }
    }
}

impl Drop for Suite {
    fn drop(&mut self) {
        unsafe {
            coco_sys::coco_suite_free(self.inner);
        }
    }
}

/// A specific problem instance.
///
/// Instances can be optained using [Suite::next_problem]
/// and [Suite::problem_by_function_dimension_instance].
pub struct Problem<'suite> {
    inner: *mut coco_problem_t,
    function: usize,
    instance: usize,
    dimension: usize,
    _phantom: PhantomData<&'suite Suite>,
}

unsafe impl Send for Problem<'_> {}

impl Problem<'_> {
    /// Returns the ID of the problem.
    ///
    /// For the `toy` suite this is
    /// - `{function-name}_d{dimension}`
    ///
    /// For `bbob` it is
    /// - bbob_f{function-index}_i{instance}_d{dimension}
    pub fn id(&self) -> &str {
        unsafe {
            CStr::from_ptr(coco_sys::coco_problem_get_id(self.inner))
                .to_str()
                .unwrap()
        }
    }

    /// Returns the name of the problem.
    pub fn name(&self) -> &str {
        unsafe {
            CStr::from_ptr(coco_sys::coco_problem_get_name(self.inner))
                .to_str()
                .unwrap()
        }
    }

    /// Returns the index of the problem.
    pub fn function_index(&self) -> usize {
        self.function
    }

    /// Returns the dimension index of the problem.
    pub fn dimension_index(&self) -> usize {
        self.dimension
    }

    /// Returns the instance of the problem.
    pub fn instance_index(&self) -> usize {
        self.instance
    }

    /// Evaluates the problem at `x` and returns the result in `y`.
    ///
    /// The length of `x` must match [Problem::dimension] and the
    /// length of `y` must match [Problem::number_of_objectives].
    pub fn evaluate_function(&mut self, x: &[f64], y: &mut [f64]) {
        assert_eq!(self.dimension(), x.len());
        assert_eq!(self.number_of_objectives(), y.len());

        unsafe {
            coco_sys::coco_evaluate_function(self.inner, x.as_ptr(), y.as_mut_ptr());
        }
    }

    /// Evaluates the problem constraints in point x and save the result in y.
    ///
    /// The length of `x` must match [Problem::dimension] and the
    /// length of `y` must match [Problem::number_of_constraints].
    pub fn evaluate_constraint(&mut self, x: &[f64], y: &mut [f64]) {
        assert_eq!(self.dimension(), x.len());
        assert_eq!(self.number_of_constraints(), y.len());

        unsafe {
            coco_sys::coco_evaluate_constraint(self.inner, x.as_ptr(), y.as_mut_ptr());
        }
    }

    /// Returns true if a previous evaluation hit the target value.
    pub fn final_target_hit(&self) -> bool {
        unsafe { coco_sys::coco_problem_final_target_hit(self.inner) == 1 }
    }

    /// Returns the dimension of the problem.
    pub fn dimension(&self) -> usize {
        unsafe {
            coco_sys::coco_problem_get_dimension(self.inner)
                .try_into()
                .unwrap()
        }
    }

    /// Returns the number of objectives of the problem.
    pub fn number_of_objectives(&self) -> usize {
        unsafe {
            coco_sys::coco_problem_get_number_of_objectives(self.inner)
                .try_into()
                .unwrap()
        }
    }

    /// Returns the number of constraints of the problem.
    pub fn number_of_constraints(&self) -> usize {
        unsafe {
            coco_sys::coco_problem_get_number_of_constraints(self.inner)
                .try_into()
                .unwrap()
        }
    }

    /// Returns the numver of integer variables of the problem.
    ///
    /// The first `n` variables will be integers then.
    /// Returns `0` if all variables are continuous.
    pub fn number_of_integer_variables(&self) -> usize {
        unsafe {
            coco_sys::coco_problem_get_number_of_integer_variables(self.inner)
                .try_into()
                .unwrap()
        }
    }

    /// Returns the upper and lover bounds of the problem.
    pub fn get_ranges_of_interest(&self) -> Vec<RangeInclusive<f64>> {
        let dimension = self.dimension() as isize;
        unsafe {
            let smallest = coco_sys::coco_problem_get_smallest_values_of_interest(self.inner);
            let largest = coco_sys::coco_problem_get_largest_values_of_interest(self.inner);

            (0..dimension)
                .into_iter()
                .map(|i| (*smallest.offset(i))..=(*largest.offset(i)))
                .collect()
        }
    }

    /// Returns how often this instance has been evaluated.
    pub fn evaluations(&self) -> u64 {
        unsafe {
            #[allow(clippy::useless_conversion)]
            coco_sys::coco_problem_get_evaluations(self.inner)
                .try_into()
                .unwrap()
        }
    }

    /// Returns how often this instances constrants have been evaluated.
    pub fn evaluations_constraints(&self) -> u64 {
        unsafe {
            #[allow(clippy::useless_conversion)]
            coco_sys::coco_problem_get_evaluations_constraints(self.inner)
                .try_into()
                .unwrap()
        }
    }

    /// Writes a feasible initial solution into `x`.
    ///
    /// If the problem does not provide a specific solution,
    /// it will be the center of the problem's region of interest.
    pub fn initial_solution(&self, x: &mut [f64]) {
        assert_eq!(self.dimension(), x.len());
        unsafe {
            coco_sys::coco_problem_get_initial_solution(self.inner, x.as_mut_ptr());
        }
    }
}

impl Drop for Problem<'_> {
    fn drop(&mut self) {
        unsafe {
            coco_sys::coco_problem_free(self.inner);
        }
    }
}

/// An observer to log results in COCO's data format.
///
/// Can be provided to [Suite::next_problem] and it will
/// automatically be attached to the returned problem.
pub struct Observer {
    inner: *mut coco_observer_t,
}

pub enum ObserverName {
    Bbob,
    BbobBiobj,
    Toy,
    None,
}
impl ObserverName {
    fn as_str(&self) -> &'static str {
        match self {
            ObserverName::Bbob => "bbob",
            ObserverName::BbobBiobj => "bbob-biobj",
            ObserverName::Toy => "toy",
            ObserverName::None => "no-observer",
        }
    }
}

impl Observer {
    /// Creates a new observer.
    ///
    /// # observer_options
    /// A string of pairs "key: value" used to pass the options to the observer. Some
    /// observer options are general, while others are specific to some observers. Here we list only the general
    /// options, see observer_bbob, observer_biobj and observer_toy for options of the specific observers.
    /// - "result_folder: NAME" determines the folder within the "exdata" folder into which the results will be
    /// output. If the folder with the given name already exists, first NAME_001 will be tried, then NAME_002 and
    /// so on. The default value is "default".
    /// - "algorithm_name: NAME", where NAME is a short name of the algorithm that will be used in plots (no
    /// spaces are allowed). The default value is "ALG".
    /// - "algorithm_info: STRING" stores the description of the algorithm. If it contains spaces, it must be
    /// surrounded by double quotes. The default value is "" (no description).
    /// - "number_target_triggers: VALUE" defines the number of targets between each 10^i and 10^(i+1)
    /// (equally spaced in the logarithmic scale) that trigger logging. The default value is 100.
    /// - "target_precision: VALUE" defines the precision used for targets (there are no targets for
    /// abs(values) < target_precision). The default value is 1e-8.
    /// - "number_evaluation_triggers: VALUE" defines the number of evaluations to be logged between each 10^i
    /// and 10^(i+1). The default value is 20.
    /// - "base_evaluation_triggers: VALUES" defines the base evaluations used to produce an additional
    /// evaluation-based logging. The numbers of evaluations that trigger logging are every
    /// base_evaluation * dimension * (10^i). For example, if base_evaluation_triggers = "1,2,5", the logger will
    /// be triggered by evaluations dim*1, dim*2, dim*5, 10*dim*1, 10*dim*2, 10*dim*5, 100*dim*1, 100*dim*2,
    /// 100*dim*5, ... The default value is "1,2,5".
    /// - "precision_x: VALUE" defines the precision used when outputting variables and corresponds to the number
    /// of digits to be printed after the decimal point. The default value is 8.
    /// - "precision_f: VALUE" defines the precision used when outputting f values and corresponds to the number of
    /// digits to be printed after the decimal point. The default value is 15.
    /// - "precision_g: VALUE" defines the precision used when outputting constraints and corresponds to the number
    /// of digits to be printed after the decimal point. The default value is 3.
    /// - "log_discrete_as_int: VALUE" determines whether the values of integer variables (in mixed-integer problems)
    /// are logged as integers (1) or not (0 - in this case they are logged as doubles). The default value is 0.
    pub fn new(name: ObserverName, options: &str) -> Option<Observer> {
        let name = CString::new(name.as_str()).unwrap();
        let options = CString::new(options).unwrap();

        let inner = unsafe { coco_sys::coco_observer(name.as_ptr(), options.as_ptr()) };

        if inner.is_null() {
            None
        } else {
            Some(Observer { inner })
        }
    }

    /// Prints where the result is written to.
    pub fn result_folder(&self) -> &str {
        unsafe {
            CStr::from_ptr(coco_sys::coco_observer_get_result_folder(self.inner))
                .to_str()
                .unwrap()
        }
    }
}

impl Drop for Observer {
    fn drop(&mut self) {
        unsafe {
            coco_sys::coco_observer_free(self.inner);
        }
    }
}

/// COCO specific random number generator.
pub struct RandomState {
    inner: *mut coco_random_state_t,
}

impl RandomState {
    /// Creates a new random number state using the given seed.
    pub fn new(seed: u32) -> Self {
        let inner = unsafe { coco_sys::coco_random_new(seed) };

        RandomState { inner }
    }

    /// Generates an approximately normal random number.
    pub fn normal(&mut self) -> f64 {
        unsafe { coco_sys::coco_random_normal(self.inner) }
    }

    /// Returns one uniform [0, 1) random value.
    pub fn uniform(&mut self) -> f64 {
        unsafe { coco_sys::coco_random_uniform(self.inner) }
    }
}

impl Drop for RandomState {
    fn drop(&mut self) {
        unsafe {
            coco_sys::coco_random_free(self.inner);
        }
    }
}