From 33d23c0e316543f3d4ea3e5fa257edd38ade6ab8 Mon Sep 17 00:00:00 2001 From: relf Date: Fri, 17 Nov 2023 11:30:58 +0100 Subject: [PATCH 01/13] Implement suggest in dedicated EgorService --- ego/src/egor.rs | 27 +--- ego/src/egor_service.rs | 331 ++++++++++++++++++++++++++++++++++++++++ ego/src/lib.rs | 2 + 3 files changed, 335 insertions(+), 25 deletions(-) create mode 100644 ego/src/egor_service.rs diff --git a/ego/src/egor.rs b/ego/src/egor.rs index f9732641..c8d16cfd 100644 --- a/ego/src/egor.rs +++ b/ego/src/egor.rs @@ -290,6 +290,8 @@ impl Egor { /// where optimum may occurs regarding the infill criterium. /// This function inverse the control of the optimization and can used /// ask-and-tell interface to the EGO optimizer. + /// + #[deprecated(since = "0.13.0", note = "moved in EgorService struct impl")] pub fn suggest( &self, x_data: &ArrayBase, Ix2>, @@ -351,7 +353,6 @@ mod tests { use ndarray::{array, s, ArrayView2, Ix1, Zip}; use ndarray_npy::read_npy; - use ndarray_stats::QuantileExt; use serial_test::serial; use std::time::Instant; @@ -441,30 +442,6 @@ mod tests { assert_abs_diff_eq!(expected, res.x_opt, epsilon = 1e-1); } - #[test] - #[serial] - fn test_xsinx_suggestions_egor_builder() { - let ego = EgorBuilder::optimize(xsinx) - .random_seed(42) - .min_within(&array![[0., 25.]]) - .regression_spec(RegressionSpec::ALL) - .correlation_spec(CorrelationSpec::ALL) - .infill_strategy(InfillStrategy::EI); - - let mut doe = array![[0.], [7.], [20.], [25.]]; - let mut y_doe = xsinx(&doe.view()); - for _i in 0..10 { - let x_suggested = ego.suggest(&doe, &y_doe); - - doe = concatenate![Axis(0), doe, x_suggested]; - y_doe = xsinx(&doe.view()); - } - - let expected = -15.1; - let y_opt = y_doe.min().unwrap(); - assert_abs_diff_eq!(expected, *y_opt, epsilon = 1e-1); - } - fn rosenb(x: &ArrayView2) -> Array2 { let mut y: Array2 = Array2::zeros((x.nrows(), 1)); Zip::from(y.rows_mut()) diff --git a/ego/src/egor_service.rs b/ego/src/egor_service.rs new file mode 100644 index 00000000..3389eaeb --- /dev/null +++ b/ego/src/egor_service.rs @@ -0,0 +1,331 @@ +//! Egor optimizer implements EGO algorithm with basic handling of constraints. +//! +//! ```no_run +//! # use ndarray::{array, Array2, ArrayView1, ArrayView2, Zip}; +//! # use egobox_doe::{Lhs, SamplingMethod}; +//! # use egobox_ego::{EgorBuilder, InfillStrategy, InfillOptimizer}; +//! +//! # use rand_xoshiro::Xoshiro256Plus; +//! # use ndarray_rand::rand::SeedableRng; +//! use argmin_testfunctions::rosenbrock; +//! +//! // Rosenbrock test function: minimum y_opt = 0 at x_opt = (1, 1) +//! fn rosenb(x: &ArrayView2) -> Array2 { +//! let mut y: Array2 = Array2::zeros((x.nrows(), 1)); +//! Zip::from(y.rows_mut()) +//! .and(x.rows()) +//! .par_for_each(|mut yi, xi| yi.assign(&array![rosenbrock(&xi.to_vec(), 1., 100.)])); +//! y +//! } +//! +//! let xlimits = array![[-2., 2.], [-2., 2.]]; +//! let res = EgorBuilder::optimize(rosenb) +//! .min_within(&xlimits) +//! .infill_strategy(InfillStrategy::EI) +//! .n_doe(10) +//! .target(1e-1) +//! .n_iter(30) +//! .run() +//! .expect("Rosenbrock minimization"); +//! println!("Rosenbrock min result = {:?}", res); +//! ``` +//! +//! Constraints are expected to be evaluated with the objective function +//! meaning that the function passed to the optimizer has to return +//! a vector consisting of [obj, cstr_1, ..., cstr_n] and the cstr values +//! are intended to be negative at the end of the optimization. +//! Constraint number should be declared with `n_cstr` setter. +//! A tolerance can be adjust with `cstr_tol` setter for relaxing constraint violation +//! if specified cstr values should be < `cstr_tol` (instead of < 0) +//! +//! ```no_run +//! # use ndarray::{array, Array2, ArrayView1, ArrayView2, Zip}; +//! # use egobox_doe::{Lhs, SamplingMethod}; +//! # use egobox_ego::{EgorBuilder, InfillStrategy, InfillOptimizer}; +//! # use rand_xoshiro::Xoshiro256Plus; +//! # use ndarray_rand::rand::SeedableRng; +//! +//! // Function G24: 1 global optimum y_opt = -5.5080 at x_opt =(2.3295, 3.1785) +//! fn g24(x: &ArrayView1) -> f64 { +//! -x[0] - x[1] +//! } +//! +//! // Constraints < 0 +//! fn g24_c1(x: &ArrayView1) -> f64 { +//! -2.0 * x[0].powf(4.0) + 8.0 * x[0].powf(3.0) - 8.0 * x[0].powf(2.0) + x[1] - 2.0 +//! } +//! +//! fn g24_c2(x: &ArrayView1) -> f64 { +//! -4.0 * x[0].powf(4.0) + 32.0 * x[0].powf(3.0) +//! - 88.0 * x[0].powf(2.0) + 96.0 * x[0] + x[1] +//! - 36.0 +//! } +//! +//! // Gouped function : objective + constraints +//! fn f_g24(x: &ArrayView2) -> Array2 { +//! let mut y = Array2::zeros((x.nrows(), 3)); +//! Zip::from(y.rows_mut()) +//! .and(x.rows()) +//! .for_each(|mut yi, xi| { +//! yi.assign(&array![g24(&xi), g24_c1(&xi), g24_c2(&xi)]); +//! }); +//! y +//! } +//! +//! let xlimits = array![[0., 3.], [0., 4.]]; +//! let doe = Lhs::new(&xlimits).sample(10); +//! let res = EgorBuilder::optimize(f_g24) +//! .min_within(&xlimits) +//! .n_cstr(2) +//! .infill_strategy(InfillStrategy::EI) +//! .infill_optimizer(InfillOptimizer::Cobyla) +//! .doe(&doe) +//! .n_iter(40) +//! .target(-5.5080) +//! .run() +//! .expect("g24 minimized"); +//! println!("G24 min result = {:?}", res); +//! ``` +//! +use crate::egor_solver::*; +use crate::mixint::*; +use crate::types::*; + +use egobox_moe::{CorrelationSpec, MoeParams, RegressionSpec}; +use ndarray::Array1; +use ndarray::{Array2, ArrayBase, Data, Ix2}; +use ndarray_rand::rand::SeedableRng; +use rand_xoshiro::Xoshiro256Plus; + +/// EGO optimizer service builder allowing to use Egor optimizer +/// with an ask-and-tell interface. +/// +pub struct EgorServiceBuilder { + seed: Option, +} + +impl EgorServiceBuilder { + /// Function to be minimized domain should be basically R^nx -> R^ny + /// where nx is the dimension of input x and ny the output dimension + /// equal to 1 (obj) + n (cstrs). + /// But function has to be able to evaluate several points in one go + /// hence take an (p, nx) matrix and return an (p, ny) matrix + pub fn optimize() -> Self { + EgorServiceBuilder { seed: None } + } + + /// Allow to specify a seed for random number generator to allow + /// reproducible runs. + pub fn random_seed(mut self, seed: u64) -> Self { + self.seed = Some(seed); + self + } + + /// Build an Egor optimizer to minimize the function within + /// the continuous `xlimits` specified as [[lower, upper], ...] array where the + /// number of rows gives the dimension of the inputs (continuous optimization) + /// and the ith row is the interval of the ith component of the input x. + pub fn min_within( + self, + xlimits: &ArrayBase, Ix2>, + ) -> EgorService> { + let rng = if let Some(seed) = self.seed { + Xoshiro256Plus::seed_from_u64(seed) + } else { + Xoshiro256Plus::from_entropy() + }; + EgorService { + solver: EgorSolver::new(xlimits, rng), + } + } + + /// Build an Egor optimizer to minimize the function R^n -> R^p taking + /// inputs specified with given xtypes where some of components may be + /// discrete variables (mixed-integer optimization). + pub fn min_within_mixint_space(self, xtypes: &[XType]) -> EgorService { + let rng = if let Some(seed) = self.seed { + Xoshiro256Plus::seed_from_u64(seed) + } else { + Xoshiro256Plus::from_entropy() + }; + EgorService { + solver: EgorSolver::new_with_xtypes(xtypes, rng), + } + } +} + +/// Egor optimizer structure used to parameterize the underlying `argmin::Solver` +/// and trigger the optimization using `argmin::Executor`. +#[derive(Clone)] +pub struct EgorService { + solver: EgorSolver, +} + +impl EgorService { + /// Sets allowed number of evaluation of the function under optimization + pub fn n_iter(mut self, n_iter: usize) -> Self { + self.solver = self.solver.n_iter(n_iter); + self + } + + /// Sets the number of runs of infill strategy optimizations (best result taken) + pub fn n_start(mut self, n_start: usize) -> Self { + self.solver = self.solver.n_start(n_start); + self + } + + /// Sets Number of parallel evaluations of the function under optimization + pub fn q_points(mut self, q_points: usize) -> Self { + self.solver = self.solver.q_points(q_points); + self + } + + /// Number of samples of initial LHS sampling (used when DOE not provided by the user) + /// + /// When 0 a number of points is computed automatically regarding the number of input variables + /// of the function under optimization. + pub fn n_doe(mut self, n_doe: usize) -> Self { + self.solver = self.solver.n_doe(n_doe); + self + } + + /// Sets the number of constraint functions + pub fn n_cstr(mut self, n_cstr: usize) -> Self { + self.solver = self.solver.n_cstr(n_cstr); + self + } + + /// Sets the tolerance on constraints violation (cstr < tol) + pub fn cstr_tol(mut self, tol: &Array1) -> Self { + self.solver = self.solver.cstr_tol(tol); + self + } + + /// Sets an initial DOE \['ns', `nt`\] containing `ns` samples. + /// + /// Either `nt` = `nx` then only `x` input values are specified and `ns` evals are done to get y ouput doe values, + /// or `nt = nx + ny` then `x = doe\[:, :nx\]` and `y = doe\[:, nx:\]` are specified + pub fn doe(mut self, doe: &Array2) -> Self { + self.solver = self.solver.doe(doe); + self + } + + /// Sets the parallel infill strategy + /// + /// Parallel infill criterion to get virtual next promising points in order to allow + /// n parallel evaluations of the function under optimization. + pub fn qei_strategy(mut self, q_ei: QEiStrategy) -> Self { + self.solver = self.solver.qei_strategy(q_ei); + self + } + + /// Sets the infill strategy + pub fn infill_strategy(mut self, infill: InfillStrategy) -> Self { + self.solver = self.solver.infill_strategy(infill); + self + } + + /// Sets the infill optimizer + pub fn infill_optimizer(mut self, optimizer: InfillOptimizer) -> Self { + self.solver = self.solver.infill_optimizer(optimizer); + self + } + + /// Sets the allowed regression models used in gaussian processes. + pub fn regression_spec(mut self, regression_spec: RegressionSpec) -> Self { + self.solver = self.solver.regression_spec(regression_spec); + self + } + + /// Sets the allowed correlation models used in gaussian processes. + pub fn correlation_spec(mut self, correlation_spec: CorrelationSpec) -> Self { + self.solver = self.solver.correlation_spec(correlation_spec); + self + } + + /// Sets the number of components to be used specifiying PLS projection is used (a.k.a KPLS method). + /// + /// This is used to address high-dimensional problems typically when `nx` > 9 wher `nx` is the dimension of `x`. + pub fn kpls_dim(mut self, kpls_dim: usize) -> Self { + self.solver = self.solver.kpls_dim(kpls_dim); + self + } + + /// Sets the number of clusters used by the mixture of surrogate experts. + /// + /// When set to 0, the number of clusters is determined automatically + /// (warning in this case the optimizer runs slower) + pub fn n_clusters(mut self, n_clusters: usize) -> Self { + self.solver = self.solver.n_clusters(n_clusters); + self + } + + /// Sets a known target minimum to be used as a stopping criterion. + pub fn target(mut self, target: f64) -> Self { + self.solver = self.solver.target(target); + self + } + + /// Sets a directory to write optimization history and used as search path for hot start doe + pub fn outdir(mut self, outdir: impl Into) -> Self { + self.solver = self.solver.outdir(outdir); + self + } + + /// Whether we start by loading last DOE saved in `outdir` as initial DOE + pub fn hot_start(mut self, hot_start: bool) -> Self { + self.solver = self.solver.hot_start(hot_start); + self + } + + /// Given an evaluated doe (x, y) data, return the next promising x point + /// where optimum may occurs regarding the infill criterium. + /// This function inverse the control of the optimization and can used + /// ask-and-tell interface to the EGO optimizer. + pub fn suggest( + &self, + x_data: &ArrayBase, Ix2>, + y_data: &ArrayBase, Ix2>, + ) -> Array2 { + self.solver.suggest(x_data, y_data) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use approx::assert_abs_diff_eq; + use ndarray::{array, concatenate, ArrayView2, Axis}; + + use ndarray_stats::QuantileExt; + + use serial_test::serial; + + fn xsinx(x: &ArrayView2) -> Array2 { + (x - 3.5) * ((x - 3.5) / std::f64::consts::PI).mapv(|v| v.sin()) + } + + #[test] + #[serial] + fn test_xsinx_egor_builder() { + let ego = EgorServiceBuilder::optimize() + .random_seed(42) + .min_within(&array![[0., 25.]]) + .regression_spec(RegressionSpec::ALL) + .correlation_spec(CorrelationSpec::ALL) + .infill_strategy(InfillStrategy::EI); + + let mut doe = array![[0.], [7.], [20.], [25.]]; + let mut y_doe = xsinx(&doe.view()); + for _i in 0..10 { + let x_suggested = ego.suggest(&doe, &y_doe); + + doe = concatenate![Axis(0), doe, x_suggested]; + y_doe = xsinx(&doe.view()); + } + + let expected = -15.1; + let y_opt = y_doe.min().unwrap(); + assert_abs_diff_eq!(expected, *y_opt, epsilon = 1e-1); + } +} diff --git a/ego/src/lib.rs b/ego/src/lib.rs index 96ccad75..57a614cc 100644 --- a/ego/src/lib.rs +++ b/ego/src/lib.rs @@ -188,6 +188,7 @@ //! mod criteria; mod egor; +mod egor_service; mod egor_solver; mod egor_state; mod errors; @@ -201,6 +202,7 @@ mod utils; pub use crate::criteria::*; pub use crate::egor::*; +pub use crate::egor_service::*; pub use crate::egor_solver::*; pub use crate::egor_state::*; pub use crate::errors::*; From b3325d9942ec6b2f7e32c5eacd308a33d1be9ecf Mon Sep 17 00:00:00 2001 From: relf Date: Sat, 18 Nov 2023 12:03:50 +0100 Subject: [PATCH 02/13] Refactor Egor configuration * Create EgorConfig * Add configure method to Egor builders --- ego/examples/ackley.rs | 13 +- ego/examples/mopta08.rs | 27 +-- ego/examples/rosenbrock.rs | 3 +- ego/src/egor.rs | 278 +++++++++----------------- ego/src/egor_config.rs | 236 ++++++++++++++++++++++ ego/src/egor_service.rs | 228 ++++------------------ ego/src/egor_solver.rs | 387 +++++++++---------------------------- ego/src/lib.rs | 9 +- src/egor.rs | 64 +++--- 9 files changed, 523 insertions(+), 722 deletions(-) create mode 100644 ego/src/egor_config.rs diff --git a/ego/examples/ackley.rs b/ego/examples/ackley.rs index 5e27c01b..04920eab 100644 --- a/ego/examples/ackley.rs +++ b/ego/examples/ackley.rs @@ -14,12 +14,15 @@ fn ackley(x: &ArrayView2) -> Array2 { fn main() { let xlimits = array![[-32.768, 32.768], [-32.768, 32.768], [-32.768, 32.768]]; let res = EgorBuilder::optimize(ackley) + .configure(|config| { + config + .regression_spec(RegressionSpec::CONSTANT) + .correlation_spec(CorrelationSpec::ABSOLUTEEXPONENTIAL) + .infill_strategy(InfillStrategy::WB2S) + .n_iter(200) + .target(5e-1) + }) .min_within(&xlimits) - .regression_spec(RegressionSpec::CONSTANT) - .correlation_spec(CorrelationSpec::ABSOLUTEEXPONENTIAL) - .infill_strategy(InfillStrategy::WB2S) - .n_iter(200) - .target(5e-1) .run() .expect("Minimize failure"); println!("Ackley minimum y = {} at x = {}", res.y_opt, res.x_opt); diff --git a/ego/examples/mopta08.rs b/ego/examples/mopta08.rs index bdf1f2e5..21354a1b 100644 --- a/ego/examples/mopta08.rs +++ b/ego/examples/mopta08.rs @@ -262,19 +262,22 @@ fn main() -> anyhow::Result<()> { xlimits.column_mut(1).assign(&Array1::ones(dim)); let res = EgorBuilder::optimize(mopta_func(dim)) + .configure(|config| { + config + .n_cstr(N_CSTR) + .cstr_tol(&cstr_tol) + .n_clusters(1) + .n_start(50) + .n_doe(n_doe) + .n_iter(n_iter) + .regression_spec(RegressionSpec::CONSTANT) + .correlation_spec(CorrelationSpec::SQUAREDEXPONENTIAL) + .infill_optimizer(InfillOptimizer::Slsqp) + .kpls_dim(kpls_dim) + .outdir(outdir) + .hot_start(true) + }) .min_within(&xlimits) - .n_cstr(N_CSTR) - .cstr_tol(&cstr_tol) - .n_clusters(1) - .n_start(50) - .n_doe(n_doe) - .n_iter(n_iter) - .regression_spec(RegressionSpec::CONSTANT) - .correlation_spec(CorrelationSpec::SQUAREDEXPONENTIAL) - .infill_optimizer(InfillOptimizer::Slsqp) - .kpls_dim(kpls_dim) - .outdir(outdir) - .hot_start(true) .run() .expect("Minimize failure"); println!( diff --git a/ego/examples/rosenbrock.rs b/ego/examples/rosenbrock.rs index fc9fc290..2bf088bf 100644 --- a/ego/examples/rosenbrock.rs +++ b/ego/examples/rosenbrock.rs @@ -19,9 +19,8 @@ fn rosenbrock(x: &ArrayView2) -> Array2 { fn main() { let xlimits = array![[-2., 2.], [-2., 2.]]; let res = EgorBuilder::optimize(rosenbrock) + .configure(|config| config.n_iter(100).target(1e-2)) .min_within(&xlimits) - .n_iter(100) - .target(1e-2) .run() .expect("Minimize failure"); println!("Rosenbrock minimum y = {} at x = {}", res.y_opt, res.x_opt); diff --git a/ego/src/egor.rs b/ego/src/egor.rs index c8d16cfd..178fcf81 100644 --- a/ego/src/egor.rs +++ b/ego/src/egor.rs @@ -19,12 +19,13 @@ //! } //! //! let xlimits = array![[-2., 2.], [-2., 2.]]; -//! let res = EgorBuilder::optimize(rosenb) +//! let res = EgorBuilder::optimize(rosenb).configure(|config| +//! config +//! .infill_strategy(InfillStrategy::EI) +//! .n_doe(10) +//! .target(1e-1) +//! .n_iter(30)) //! .min_within(&xlimits) -//! .infill_strategy(InfillStrategy::EI) -//! .n_doe(10) -//! .target(1e-1) -//! .n_iter(30) //! .run() //! .expect("Rosenbrock minimization"); //! println!("Rosenbrock min result = {:?}", res); @@ -74,27 +75,28 @@ //! //! let xlimits = array![[0., 3.], [0., 4.]]; //! let doe = Lhs::new(&xlimits).sample(10); -//! let res = EgorBuilder::optimize(f_g24) +//! let res = EgorBuilder::optimize(f_g24).configure(|config| +//! config +//! .n_cstr(2) +//! .infill_strategy(InfillStrategy::EI) +//! .infill_optimizer(InfillOptimizer::Cobyla) +//! .doe(&doe) +//! .n_iter(40) +//! .target(-5.5080)) //! .min_within(&xlimits) -//! .n_cstr(2) -//! .infill_strategy(InfillStrategy::EI) -//! .infill_optimizer(InfillOptimizer::Cobyla) -//! .doe(&doe) -//! .n_iter(40) -//! .target(-5.5080) //! .run() //! .expect("g24 minimized"); //! println!("G24 min result = {:?}", res); //! ``` //! +use crate::egor_config::*; use crate::egor_solver::*; use crate::errors::Result; use crate::mixint::*; use crate::types::*; -use egobox_moe::{CorrelationSpec, MoeParams, RegressionSpec}; +use egobox_moe::MoeParams; use log::info; -use ndarray::Array1; use ndarray::{concatenate, Array2, ArrayBase, Axis, Data, Ix2}; use ndarray_rand::rand::SeedableRng; use rand_xoshiro::Xoshiro256Plus; @@ -106,6 +108,7 @@ use argmin::core::{Executor, State}; /// pub struct EgorBuilder { fobj: O, + config: EgorConfig, seed: Option, } @@ -116,7 +119,16 @@ impl EgorBuilder { /// But function has to be able to evaluate several points in one go /// hence take an (p, nx) matrix and return an (p, ny) matrix pub fn optimize(fobj: O) -> Self { - EgorBuilder { fobj, seed: None } + EgorBuilder { + fobj, + config: EgorConfig::default(), + seed: None, + } + } + + pub fn configure EgorConfig>(mut self, init: F) -> Self { + self.config = init(self.config); + self } /// Allow to specify a seed for random number generator to allow @@ -141,7 +153,7 @@ impl EgorBuilder { }; Egor { fobj: ObjFunc::new(self.fobj), - solver: EgorSolver::new(xlimits, rng), + solver: EgorSolver::new(self.config, xlimits, rng), } } @@ -170,122 +182,6 @@ pub struct Egor { } impl Egor { - /// Sets allowed number of evaluation of the function under optimization - pub fn n_iter(mut self, n_iter: usize) -> Self { - self.solver = self.solver.n_iter(n_iter); - self - } - - /// Sets the number of runs of infill strategy optimizations (best result taken) - pub fn n_start(mut self, n_start: usize) -> Self { - self.solver = self.solver.n_start(n_start); - self - } - - /// Sets Number of parallel evaluations of the function under optimization - pub fn q_points(mut self, q_points: usize) -> Self { - self.solver = self.solver.q_points(q_points); - self - } - - /// Number of samples of initial LHS sampling (used when DOE not provided by the user) - /// - /// When 0 a number of points is computed automatically regarding the number of input variables - /// of the function under optimization. - pub fn n_doe(mut self, n_doe: usize) -> Self { - self.solver = self.solver.n_doe(n_doe); - self - } - - /// Sets the number of constraint functions - pub fn n_cstr(mut self, n_cstr: usize) -> Self { - self.solver = self.solver.n_cstr(n_cstr); - self - } - - /// Sets the tolerance on constraints violation (cstr < tol) - pub fn cstr_tol(mut self, tol: &Array1) -> Self { - self.solver = self.solver.cstr_tol(tol); - self - } - - /// Sets an initial DOE \['ns', `nt`\] containing `ns` samples. - /// - /// Either `nt` = `nx` then only `x` input values are specified and `ns` evals are done to get y ouput doe values, - /// or `nt = nx + ny` then `x = doe\[:, :nx\]` and `y = doe\[:, nx:\]` are specified - pub fn doe(mut self, doe: &Array2) -> Self { - self.solver = self.solver.doe(doe); - self - } - - /// Sets the parallel infill strategy - /// - /// Parallel infill criterion to get virtual next promising points in order to allow - /// n parallel evaluations of the function under optimization. - pub fn qei_strategy(mut self, q_ei: QEiStrategy) -> Self { - self.solver = self.solver.qei_strategy(q_ei); - self - } - - /// Sets the infill strategy - pub fn infill_strategy(mut self, infill: InfillStrategy) -> Self { - self.solver = self.solver.infill_strategy(infill); - self - } - - /// Sets the infill optimizer - pub fn infill_optimizer(mut self, optimizer: InfillOptimizer) -> Self { - self.solver = self.solver.infill_optimizer(optimizer); - self - } - - /// Sets the allowed regression models used in gaussian processes. - pub fn regression_spec(mut self, regression_spec: RegressionSpec) -> Self { - self.solver = self.solver.regression_spec(regression_spec); - self - } - - /// Sets the allowed correlation models used in gaussian processes. - pub fn correlation_spec(mut self, correlation_spec: CorrelationSpec) -> Self { - self.solver = self.solver.correlation_spec(correlation_spec); - self - } - - /// Sets the number of components to be used specifiying PLS projection is used (a.k.a KPLS method). - /// - /// This is used to address high-dimensional problems typically when `nx` > 9 wher `nx` is the dimension of `x`. - pub fn kpls_dim(mut self, kpls_dim: usize) -> Self { - self.solver = self.solver.kpls_dim(kpls_dim); - self - } - - /// Sets the number of clusters used by the mixture of surrogate experts. - /// - /// When set to 0, the number of clusters is determined automatically - /// (warning in this case the optimizer runs slower) - pub fn n_clusters(mut self, n_clusters: usize) -> Self { - self.solver = self.solver.n_clusters(n_clusters); - self - } - - /// Sets a known target minimum to be used as a stopping criterion. - pub fn target(mut self, target: f64) -> Self { - self.solver = self.solver.target(target); - self - } - - /// Sets a directory to write optimization history and used as search path for hot start doe - pub fn outdir(mut self, outdir: impl Into) -> Self { - self.solver = self.solver.outdir(outdir); - self - } - - /// Whether we start by loading last DOE saved in `outdir` as initial DOE - pub fn hot_start(mut self, hot_start: bool) -> Self { - self.solver = self.solver.hot_start(hot_start); - self - } - /// Given an evaluated doe (x, y) data, return the next promising x point /// where optimum may occurs regarding the infill criterium. /// This function inverse the control of the optimization and can used @@ -302,8 +198,8 @@ impl Egor { /// Runs the (constrained) optimization of the objective function. pub fn run(&self) -> Result> { - let no_discrete = self.solver.no_discrete; - let xtypes = self.solver.xtypes.clone(); + let no_discrete = self.solver.config.no_discrete; + let xtypes = self.solver.config.xtypes.clone(); let result = Executor::new(self.fobj.clone(), self.solver.clone()).run()?; info!("{}", result); @@ -371,14 +267,16 @@ mod tests { fn test_xsinx_ei_quadratic_egor_builder() { let initial_doe = array![[0.], [7.], [25.]]; let res = EgorBuilder::optimize(xsinx) + .configure(|cfg| { + cfg.infill_strategy(InfillStrategy::EI) + .regression_spec(RegressionSpec::QUADRATIC) + .correlation_spec(CorrelationSpec::ALL) + .n_iter(30) + .doe(&initial_doe) + .target(-15.1) + .outdir("target/tests") + }) .min_within(&array![[0.0, 25.0]]) - .infill_strategy(InfillStrategy::EI) - .regression_spec(RegressionSpec::QUADRATIC) - .correlation_spec(CorrelationSpec::ALL) - .n_iter(30) - .doe(&initial_doe) - .target(-15.1) - .outdir("target/tests") .run() .expect("Egor should minimize xsinx"); let expected = array![-15.1]; @@ -391,10 +289,13 @@ mod tests { #[serial] fn test_xsinx_wb2_egor_builder() { let res = EgorBuilder::optimize(xsinx) + .configure(|config| { + config + .n_iter(20) + .regression_spec(RegressionSpec::ALL) + .correlation_spec(CorrelationSpec::ALL) + }) .min_within(&array![[0.0, 25.0]]) - .n_iter(20) - .regression_spec(RegressionSpec::ALL) - .correlation_spec(CorrelationSpec::ALL) .run() .expect("Egor should minimize"); let expected = array![18.9]; @@ -405,9 +306,8 @@ mod tests { #[serial] fn test_xsinx_auto_clustering_egor_builder() { let res = EgorBuilder::optimize(xsinx) + .configure(|config| config.n_clusters(0).n_iter(20)) .min_within(&array![[0.0, 25.0]]) - .n_clusters(0) - .n_iter(20) .run() .expect("Egor with auto clustering should minimize xsinx"); let expected = array![18.9]; @@ -420,22 +320,18 @@ mod tests { let xlimits = array![[0.0, 25.0]]; let doe = Lhs::new(&xlimits).sample(10); let res = EgorBuilder::optimize(xsinx) + .configure(|config| config.n_iter(15).doe(&doe).outdir("target/tests")) .random_seed(42) .min_within(&xlimits) - .n_iter(15) - .doe(&doe) - .outdir("target/tests") .run() .expect("Minimize failure"); let expected = array![18.9]; assert_abs_diff_eq!(expected, res.x_opt, epsilon = 1e-1); let res = EgorBuilder::optimize(xsinx) + .configure(|config| config.n_iter(5).outdir("target/tests").hot_start(true)) .random_seed(42) .min_within(&xlimits) - .n_iter(5) - .outdir("target/tests") - .hot_start(true) .run() .expect("Egor should minimize xsinx"); let expected = array![18.9]; @@ -459,13 +355,16 @@ mod tests { .with_rng(Xoshiro256Plus::seed_from_u64(42)) .sample(10); let res = EgorBuilder::optimize(rosenb) + .configure(|config| { + config + .doe(&doe) + .n_iter(100) + .regression_spec(RegressionSpec::ALL) + .correlation_spec(CorrelationSpec::ALL) + .target(1e-2) + }) .random_seed(42) .min_within(&xlimits) - .doe(&doe) - .n_iter(100) - .regression_spec(RegressionSpec::ALL) - .correlation_spec(CorrelationSpec::ALL) - .target(1e-2) .run() .expect("Minimize failure"); println!("Rosenbrock optim result = {res:?}"); @@ -508,11 +407,9 @@ mod tests { .with_rng(Xoshiro256Plus::seed_from_u64(42)) .sample(3); let res = EgorBuilder::optimize(f_g24) + .configure(|config| config.n_cstr(2).doe(&doe).n_iter(20)) .random_seed(42) .min_within(&xlimits) - .n_cstr(2) - .doe(&doe) - .n_iter(20) .run() .expect("Minimize failure"); println!("G24 optim result = {res:?}"); @@ -528,17 +425,20 @@ mod tests { .with_rng(Xoshiro256Plus::seed_from_u64(42)) .sample(10); let res = EgorBuilder::optimize(f_g24) + .configure(|config| { + config + .regression_spec(RegressionSpec::ALL) + .correlation_spec(CorrelationSpec::ALL) + .n_cstr(2) + .cstr_tol(&array![2e-6, 2e-6]) + .q_points(2) + .qei_strategy(QEiStrategy::KrigingBeliever) + .doe(&doe) + .target(-5.5030) + .n_iter(30) + }) .random_seed(42) .min_within(&xlimits) - .regression_spec(RegressionSpec::ALL) - .correlation_spec(CorrelationSpec::ALL) - .n_cstr(2) - .cstr_tol(&array![2e-6, 2e-6]) - .q_points(2) - .qei_strategy(QEiStrategy::KrigingBeliever) - .doe(&doe) - .target(-5.5030) - .n_iter(30) .run() .expect("Egor minimization"); println!("G24 optim result = {res:?}"); @@ -564,12 +464,15 @@ mod tests { let xtypes = vec![XType::Int(0, 25)]; let res = EgorBuilder::optimize(mixsinx) + .configure(|config| { + config + .doe(&doe) + .n_iter(n_iter) + .target(-15.1) + .infill_strategy(InfillStrategy::EI) + }) .random_seed(42) .min_within_mixint_space(&xtypes) - .doe(&doe) - .n_iter(n_iter) - .target(-15.1) - .infill_strategy(InfillStrategy::EI) .run() .unwrap(); assert_abs_diff_eq!(array![18.], res.x_opt, epsilon = 2.); @@ -583,12 +486,15 @@ mod tests { let xtypes = vec![XType::Int(0, 25)]; let res = EgorBuilder::optimize(mixsinx) + .configure(|config| { + config + .doe(&doe) + .n_iter(n_iter) + .target(-15.1) + .infill_strategy(InfillStrategy::EI) + }) .random_seed(42) .min_within_mixint_space(&xtypes) - .doe(&doe) - .n_iter(n_iter) - .target(-15.1) - .infill_strategy(InfillStrategy::EI) .run() .unwrap(); assert_abs_diff_eq!(array![18.], res.x_opt, epsilon = 2.); @@ -601,11 +507,14 @@ mod tests { let xtypes = vec![XType::Int(0, 25)]; let res = EgorBuilder::optimize(mixsinx) + .configure(|config| { + config + .regression_spec(egobox_moe::RegressionSpec::CONSTANT) + .correlation_spec(egobox_moe::CorrelationSpec::SQUAREDEXPONENTIAL) + .n_iter(n_iter) + }) .random_seed(42) .min_within_mixint_space(&xtypes) - .regression_spec(egobox_moe::RegressionSpec::CONSTANT) - .correlation_spec(egobox_moe::CorrelationSpec::SQUAREDEXPONENTIAL) - .n_iter(n_iter) .run() .unwrap(); assert_abs_diff_eq!(&array![18.], &res.x_opt, epsilon = 3.); @@ -649,11 +558,14 @@ mod tests { ]; let res = EgorBuilder::optimize(mixobj) + .configure(|config| { + config + .regression_spec(egobox_moe::RegressionSpec::CONSTANT) + .correlation_spec(egobox_moe::CorrelationSpec::SQUAREDEXPONENTIAL) + .n_iter(n_iter) + }) .random_seed(42) .min_within_mixint_space(&xtypes) - .regression_spec(egobox_moe::RegressionSpec::CONSTANT) - .correlation_spec(egobox_moe::CorrelationSpec::SQUAREDEXPONENTIAL) - .n_iter(n_iter) .run() .unwrap(); println!("res={:?}", res); diff --git a/ego/src/egor_config.rs b/ego/src/egor_config.rs new file mode 100644 index 00000000..cef72536 --- /dev/null +++ b/ego/src/egor_config.rs @@ -0,0 +1,236 @@ +//! Egor optimizer configuration. +use crate::criteria::*; +use crate::types::*; +use egobox_moe::{CorrelationSpec, RegressionSpec}; +use ndarray::Array1; +use ndarray::Array2; +use rand_xoshiro::rand_core::SeedableRng; +use rand_xoshiro::Xoshiro256Plus; + +use serde::{Deserialize, Serialize}; + +/// Egor optimizer configuration +#[derive(Clone, Serialize, Deserialize)] +pub struct EgorConfig { + /// Number of function iterations allocated to find the optimum (aka iteration budget) + /// Note 1 : The number of cost function evaluations is deduced using the following formula (n_doe + n_iter) + /// Note 2 : When q_points > 1, the number of cost function evaluations is (n_doe + n_iter * q_points) + /// is is an upper bounds as some points may be rejected as being to close to previous ones. + pub(crate) n_iter: usize, + /// Number of starts for multistart approach used for hyperparameters optimization + pub(crate) n_start: usize, + /// Number of points returned by EGO iteration (aka qEI Multipoint strategy) + /// Actually as some point determination may fail (at most q_points are returned) + pub(crate) q_points: usize, + /// Number of initial doe drawn using Latin hypercube sampling + /// Note: n_doe > 0; otherwise n_doe = max(xdim + 1, 5) + pub(crate) n_doe: usize, + /// Number of Constraints + /// Note: dim function ouput = 1 objective + n_cstr constraints + pub(crate) n_cstr: usize, + /// Optional constraints violation tolerance meaning cstr < cstr_tol is considered valid + pub(crate) cstr_tol: Option>, + /// Initial doe can be either \[x\] with x inputs only or an evaluated doe \[x, y\] + /// Note: x dimension is determined using `xlimits.nrows()` + pub(crate) doe: Option>, + /// Multipoint strategy used to get several points to be evaluated at each iteration + pub(crate) q_ei: QEiStrategy, + /// Criterion to select next point to evaluate + pub(crate) infill_criterion: Box, + /// The optimizer used to optimize infill criterium + pub(crate) infill_optimizer: InfillOptimizer, + /// Regression specification for GP models used by mixture of experts (see [egobox_moe]) + pub(crate) regression_spec: RegressionSpec, + /// Correlation specification for GP models used by mixture of experts (see [egobox_moe]) + pub(crate) correlation_spec: CorrelationSpec, + /// Optional dimension reduction (see [egobox_moe]) + pub(crate) kpls_dim: Option, + /// Number of clusters used by mixture of experts (see [egobox_moe]) + /// When set to 0 the clusters are computes automatically and refreshed + /// every 10-points (tentative) additions + pub(crate) n_clusters: usize, + /// Specification of a target objective value which is used to stop the algorithm once reached + pub(crate) target: f64, + /// Directory to save intermediate results: inital doe + evalutions at each iteration + pub(crate) outdir: Option, + /// If true use `outdir` to retrieve and start from previous results + pub(crate) hot_start: bool, + /// List of x types allowing the handling of discrete input variables + pub(crate) xtypes: Option>, + /// Flag for discrete handling, true if mixed-integer type present in xtypes, otherwise false + pub(crate) no_discrete: bool, + /// A random generator used to get reproductible results. + /// For instance: Xoshiro256Plus::from_u64_seed(42) for reproducibility + pub(crate) rng: Xoshiro256Plus, +} + +impl Default for EgorConfig { + fn default() -> Self { + EgorConfig { + n_iter: 20, + n_start: 20, + q_points: 1, + n_doe: 0, + n_cstr: 0, + cstr_tol: None, + doe: None, + q_ei: QEiStrategy::KrigingBeliever, + infill_criterion: Box::new(WB2), + infill_optimizer: InfillOptimizer::Slsqp, + regression_spec: RegressionSpec::CONSTANT, + correlation_spec: CorrelationSpec::SQUAREDEXPONENTIAL, + kpls_dim: None, + n_clusters: 1, + target: f64::NEG_INFINITY, + outdir: None, + hot_start: false, + xtypes: None, + no_discrete: true, + rng: Xoshiro256Plus::from_entropy(), + } + } +} + +impl EgorConfig { + pub fn infill_criterion(mut self, infill_criterion: Box) -> Self { + self.infill_criterion = infill_criterion; + self + } + + /// Sets allowed number of evaluation of the function under optimization + pub fn n_iter(mut self, n_iter: usize) -> Self { + self.n_iter = n_iter; + self + } + + /// Sets the number of runs of infill strategy optimizations (best result taken) + pub fn n_start(mut self, n_start: usize) -> Self { + self.n_start = n_start; + self + } + + /// Sets Number of parallel evaluations of the function under optimization + pub fn q_points(mut self, q_points: usize) -> Self { + self.q_points = q_points; + self + } + + /// Number of samples of initial LHS sampling (used when DOE not provided by the user) + /// + /// When 0 a number of points is computed automatically regarding the number of input variables + /// of the function under optimization. + pub fn n_doe(mut self, n_doe: usize) -> Self { + self.n_doe = n_doe; + self + } + + /// Sets the number of constraint functions + pub fn n_cstr(mut self, n_cstr: usize) -> Self { + self.n_cstr = n_cstr; + self + } + + /// Sets the tolerance on constraints violation (`cstr < tol`) + pub fn cstr_tol(mut self, tol: &Array1) -> Self { + self.cstr_tol = Some(tol.to_owned()); + self + } + + /// Sets an initial DOE \['ns', `nt`\] containing `ns` samples. + /// + /// Either `nt` = `nx` then only `x` input values are specified and `ns` evals are done to get y ouput doe values, + /// or `nt = nx + ny` then `x = doe\[:, :nx\]` and `y = doe\[:, nx:\]` are specified + pub fn doe(mut self, doe: &Array2) -> Self { + self.doe = Some(doe.to_owned()); + self + } + + /// Removes any previously specified initial doe to get the default doe usage + pub fn default_doe(mut self) -> Self { + self.doe = None; + self + } + + /// Sets the parallel infill strategy + /// + /// Parallel infill criterion to get virtual next promising points in order to allow + /// n parallel evaluations of the function under optimization. + pub fn qei_strategy(mut self, q_ei: QEiStrategy) -> Self { + self.q_ei = q_ei; + self + } + + /// Sets the infill strategy + pub fn infill_strategy(mut self, infill: InfillStrategy) -> Self { + self.infill_criterion = match infill { + InfillStrategy::EI => Box::new(EI), + InfillStrategy::WB2 => Box::new(WB2), + InfillStrategy::WB2S => Box::new(WB2S), + }; + self + } + + /// Sets the infill optimizer + pub fn infill_optimizer(mut self, optimizer: InfillOptimizer) -> Self { + self.infill_optimizer = optimizer; + self + } + + /// Sets the allowed regression models used in gaussian processes. + pub fn regression_spec(mut self, regression_spec: RegressionSpec) -> Self { + self.regression_spec = regression_spec; + self + } + + /// Sets the allowed correlation models used in gaussian processes. + pub fn correlation_spec(mut self, correlation_spec: CorrelationSpec) -> Self { + self.correlation_spec = correlation_spec; + self + } + + /// Sets the number of components to be used specifiying PLS projection is used (a.k.a KPLS method). + /// + /// This is used to address high-dimensional problems typically when `nx` > 9 wher `nx` is the dimension of `x`. + pub fn kpls_dim(mut self, kpls_dim: usize) -> Self { + self.kpls_dim = Some(kpls_dim); + self + } + + /// Removes any PLS dimension reduction usage + pub fn no_kpls(mut self) -> Self { + self.kpls_dim = None; + self + } + + /// Sets the number of clusters used by the mixture of surrogate experts. + /// + /// When set to Some(0), the number of clusters is determined automatically + /// When set None, default to 1 + pub fn n_clusters(mut self, n_clusters: usize) -> Self { + self.n_clusters = n_clusters; + self + } + + /// Sets a known target minimum to be used as a stopping criterion. + pub fn target(mut self, target: f64) -> Self { + self.target = target; + self + } + + /// Sets a directory to write optimization history and used as search path for hot start doe + pub fn outdir(mut self, outdir: impl Into) -> Self { + self.outdir = Some(outdir.into()); + self + } + /// Do not write optimization history + pub fn no_outdir(mut self) -> Self { + self.outdir = None; + self + } + + /// Whether we start by loading last DOE saved in `outdir` as initial DOE + pub fn hot_start(mut self, hot_start: bool) -> Self { + self.hot_start = hot_start; + self + } +} diff --git a/ego/src/egor_service.rs b/ego/src/egor_service.rs index 3389eaeb..388177a1 100644 --- a/ego/src/egor_service.rs +++ b/ego/src/egor_service.rs @@ -1,4 +1,4 @@ -//! Egor optimizer implements EGO algorithm with basic handling of constraints. +//! Egor optimizer service implements Egor optimizer with an ask-and-tell interface. //! //! ```no_run //! # use ndarray::{array, Array2, ArrayView1, ArrayView2, Zip}; @@ -19,80 +19,24 @@ //! } //! //! let xlimits = array![[-2., 2.], [-2., 2.]]; -//! let res = EgorBuilder::optimize(rosenb) -//! .min_within(&xlimits) -//! .infill_strategy(InfillStrategy::EI) -//! .n_doe(10) -//! .target(1e-1) -//! .n_iter(30) -//! .run() -//! .expect("Rosenbrock minimization"); -//! println!("Rosenbrock min result = {:?}", res); -//! ``` -//! -//! Constraints are expected to be evaluated with the objective function -//! meaning that the function passed to the optimizer has to return -//! a vector consisting of [obj, cstr_1, ..., cstr_n] and the cstr values -//! are intended to be negative at the end of the optimization. -//! Constraint number should be declared with `n_cstr` setter. -//! A tolerance can be adjust with `cstr_tol` setter for relaxing constraint violation -//! if specified cstr values should be < `cstr_tol` (instead of < 0) -//! -//! ```no_run -//! # use ndarray::{array, Array2, ArrayView1, ArrayView2, Zip}; -//! # use egobox_doe::{Lhs, SamplingMethod}; -//! # use egobox_ego::{EgorBuilder, InfillStrategy, InfillOptimizer}; -//! # use rand_xoshiro::Xoshiro256Plus; -//! # use ndarray_rand::rand::SeedableRng; -//! -//! // Function G24: 1 global optimum y_opt = -5.5080 at x_opt =(2.3295, 3.1785) -//! fn g24(x: &ArrayView1) -> f64 { -//! -x[0] - x[1] -//! } -//! -//! // Constraints < 0 -//! fn g24_c1(x: &ArrayView1) -> f64 { -//! -2.0 * x[0].powf(4.0) + 8.0 * x[0].powf(3.0) - 8.0 * x[0].powf(2.0) + x[1] - 2.0 -//! } -//! -//! fn g24_c2(x: &ArrayView1) -> f64 { -//! -4.0 * x[0].powf(4.0) + 32.0 * x[0].powf(3.0) -//! - 88.0 * x[0].powf(2.0) + 96.0 * x[0] + x[1] -//! - 36.0 -//! } -//! -//! // Gouped function : objective + constraints -//! fn f_g24(x: &ArrayView2) -> Array2 { -//! let mut y = Array2::zeros((x.nrows(), 3)); -//! Zip::from(y.rows_mut()) -//! .and(x.rows()) -//! .for_each(|mut yi, xi| { -//! yi.assign(&array![g24(&xi), g24_c1(&xi), g24_c2(&xi)]); -//! }); -//! y -//! } -//! -//! let xlimits = array![[0., 3.], [0., 4.]]; -//! let doe = Lhs::new(&xlimits).sample(10); -//! let res = EgorBuilder::optimize(f_g24) -//! .min_within(&xlimits) -//! .n_cstr(2) -//! .infill_strategy(InfillStrategy::EI) -//! .infill_optimizer(InfillOptimizer::Cobyla) -//! .doe(&doe) -//! .n_iter(40) -//! .target(-5.5080) -//! .run() -//! .expect("g24 minimized"); -//! println!("G24 min result = {:?}", res); +//! // TODO +//! //let res = EgorBuilder::optimize(rosenb) +//! // .min_within(&xlimits) +//! // .infill_strategy(InfillStrategy::EI) +//! // .n_doe(10) +//! // .target(1e-1) +//! // .n_iter(30) +//! // .run() +//! // .expect("Rosenbrock minimization"); +//! //println!("Rosenbrock min result = {:?}", res); //! ``` //! +use crate::egor_config::*; use crate::egor_solver::*; use crate::mixint::*; use crate::types::*; -use egobox_moe::{CorrelationSpec, MoeParams, RegressionSpec}; -use ndarray::Array1; +use egobox_moe::MoeParams; use ndarray::{Array2, ArrayBase, Data, Ix2}; use ndarray_rand::rand::SeedableRng; use rand_xoshiro::Xoshiro256Plus; @@ -101,6 +45,7 @@ use rand_xoshiro::Xoshiro256Plus; /// with an ask-and-tell interface. /// pub struct EgorServiceBuilder { + config: EgorConfig, seed: Option, } @@ -111,7 +56,15 @@ impl EgorServiceBuilder { /// But function has to be able to evaluate several points in one go /// hence take an (p, nx) matrix and return an (p, ny) matrix pub fn optimize() -> Self { - EgorServiceBuilder { seed: None } + EgorServiceBuilder { + config: EgorConfig::default(), + seed: None, + } + } + + pub fn configure EgorConfig>(mut self, init: F) -> Self { + self.config = init(self.config); + self } /// Allow to specify a seed for random number generator to allow @@ -135,7 +88,8 @@ impl EgorServiceBuilder { Xoshiro256Plus::from_entropy() }; EgorService { - solver: EgorSolver::new(xlimits, rng), + config: self.config.clone(), + solver: EgorSolver::new(self.config, xlimits, rng), } } @@ -149,6 +103,7 @@ impl EgorServiceBuilder { Xoshiro256Plus::from_entropy() }; EgorService { + config: self.config.clone(), solver: EgorSolver::new_with_xtypes(xtypes, rng), } } @@ -158,123 +113,15 @@ impl EgorServiceBuilder { /// and trigger the optimization using `argmin::Executor`. #[derive(Clone)] pub struct EgorService { + #[allow(dead_code)] + config: EgorConfig, solver: EgorSolver, } impl EgorService { - /// Sets allowed number of evaluation of the function under optimization - pub fn n_iter(mut self, n_iter: usize) -> Self { - self.solver = self.solver.n_iter(n_iter); - self - } - - /// Sets the number of runs of infill strategy optimizations (best result taken) - pub fn n_start(mut self, n_start: usize) -> Self { - self.solver = self.solver.n_start(n_start); - self - } - - /// Sets Number of parallel evaluations of the function under optimization - pub fn q_points(mut self, q_points: usize) -> Self { - self.solver = self.solver.q_points(q_points); - self - } - - /// Number of samples of initial LHS sampling (used when DOE not provided by the user) - /// - /// When 0 a number of points is computed automatically regarding the number of input variables - /// of the function under optimization. - pub fn n_doe(mut self, n_doe: usize) -> Self { - self.solver = self.solver.n_doe(n_doe); - self - } - - /// Sets the number of constraint functions - pub fn n_cstr(mut self, n_cstr: usize) -> Self { - self.solver = self.solver.n_cstr(n_cstr); - self - } - - /// Sets the tolerance on constraints violation (cstr < tol) - pub fn cstr_tol(mut self, tol: &Array1) -> Self { - self.solver = self.solver.cstr_tol(tol); - self - } - - /// Sets an initial DOE \['ns', `nt`\] containing `ns` samples. - /// - /// Either `nt` = `nx` then only `x` input values are specified and `ns` evals are done to get y ouput doe values, - /// or `nt = nx + ny` then `x = doe\[:, :nx\]` and `y = doe\[:, nx:\]` are specified - pub fn doe(mut self, doe: &Array2) -> Self { - self.solver = self.solver.doe(doe); - self - } - - /// Sets the parallel infill strategy - /// - /// Parallel infill criterion to get virtual next promising points in order to allow - /// n parallel evaluations of the function under optimization. - pub fn qei_strategy(mut self, q_ei: QEiStrategy) -> Self { - self.solver = self.solver.qei_strategy(q_ei); - self - } - - /// Sets the infill strategy - pub fn infill_strategy(mut self, infill: InfillStrategy) -> Self { - self.solver = self.solver.infill_strategy(infill); - self - } - - /// Sets the infill optimizer - pub fn infill_optimizer(mut self, optimizer: InfillOptimizer) -> Self { - self.solver = self.solver.infill_optimizer(optimizer); - self - } - - /// Sets the allowed regression models used in gaussian processes. - pub fn regression_spec(mut self, regression_spec: RegressionSpec) -> Self { - self.solver = self.solver.regression_spec(regression_spec); - self - } - - /// Sets the allowed correlation models used in gaussian processes. - pub fn correlation_spec(mut self, correlation_spec: CorrelationSpec) -> Self { - self.solver = self.solver.correlation_spec(correlation_spec); - self - } - - /// Sets the number of components to be used specifiying PLS projection is used (a.k.a KPLS method). - /// - /// This is used to address high-dimensional problems typically when `nx` > 9 wher `nx` is the dimension of `x`. - pub fn kpls_dim(mut self, kpls_dim: usize) -> Self { - self.solver = self.solver.kpls_dim(kpls_dim); - self - } - - /// Sets the number of clusters used by the mixture of surrogate experts. - /// - /// When set to 0, the number of clusters is determined automatically - /// (warning in this case the optimizer runs slower) - pub fn n_clusters(mut self, n_clusters: usize) -> Self { - self.solver = self.solver.n_clusters(n_clusters); - self - } - - /// Sets a known target minimum to be used as a stopping criterion. - pub fn target(mut self, target: f64) -> Self { - self.solver = self.solver.target(target); - self - } - - /// Sets a directory to write optimization history and used as search path for hot start doe - pub fn outdir(mut self, outdir: impl Into) -> Self { - self.solver = self.solver.outdir(outdir); - self - } - - /// Whether we start by loading last DOE saved in `outdir` as initial DOE - pub fn hot_start(mut self, hot_start: bool) -> Self { - self.solver = self.solver.hot_start(hot_start); + #[allow(dead_code)] + fn configure EgorConfig>(mut self, init: F) -> Self { + self.config = init(self.config); self } @@ -299,21 +146,20 @@ mod tests { use ndarray_stats::QuantileExt; - use serial_test::serial; - fn xsinx(x: &ArrayView2) -> Array2 { (x - 3.5) * ((x - 3.5) / std::f64::consts::PI).mapv(|v| v.sin()) } #[test] - #[serial] fn test_xsinx_egor_builder() { let ego = EgorServiceBuilder::optimize() + .configure(|conf| { + conf.regression_spec(RegressionSpec::ALL) + .correlation_spec(CorrelationSpec::ALL) + .infill_strategy(InfillStrategy::EI) + }) .random_seed(42) - .min_within(&array![[0., 25.]]) - .regression_spec(RegressionSpec::ALL) - .correlation_spec(CorrelationSpec::ALL) - .infill_strategy(InfillStrategy::EI); + .min_within(&array![[0., 25.]]); let mut doe = array![[0.], [7.], [20.], [25.]]; let mut y_doe = xsinx(&doe.view()); diff --git a/ego/src/egor_solver.rs b/ego/src/egor_solver.rs index 393acd56..8c355490 100644 --- a/ego/src/egor_solver.rs +++ b/ego/src/egor_solver.rs @@ -99,7 +99,7 @@ //! println!("G24 min result = {:?}", res.state); //! ``` //! -use crate::criteria::*; +use crate::egor_config::EgorConfig; use crate::egor_state::{find_best_result_index, EgorState, MAX_POINT_ADDITION_RETRY}; use crate::errors::{EgoError, Result}; @@ -145,56 +145,10 @@ pub const DEFAULT_CSTR_TOL: f64 = 1e-6; /// from observers and checkpointing features. #[derive(Clone, Serialize, Deserialize)] pub struct EgorSolver { - /// Number of function iterations allocated to find the optimum (aka iteration budget) - /// Note 1 : The number of cost function evaluations is deduced using the following formula (n_doe + n_iter) - /// Note 2 : When q_points > 1, the number of cost function evaluations is (n_doe + n_iter * q_points) - /// is is an upper bounds as some points may be rejected as being to close to previous ones. - pub(crate) n_iter: usize, - /// Number of starts for multistart approach used for hyperparameters optimization - pub(crate) n_start: usize, - /// Number of points returned by EGO iteration (aka qEI Multipoint strategy) - /// Actually as some point determination may fail (at most q_points are returned) - pub(crate) q_points: usize, - /// Number of initial doe drawn using Latin hypercube sampling - /// Note: n_doe > 0; otherwise n_doe = max(xdim + 1, 5) - pub(crate) n_doe: usize, - /// Number of Constraints - /// Note: dim function ouput = 1 objective + n_cstr constraints - pub(crate) n_cstr: usize, - /// Optional constraints violation tolerance meaning cstr < cstr_tol is considered valid - pub(crate) cstr_tol: Option>, - /// Initial doe can be either \[x\] with x inputs only or an evaluated doe \[x, y\] - /// Note: x dimension is determined using `xlimits.nrows()` - pub(crate) doe: Option>, - /// Multipoint strategy used to get several points to be evaluated at each iteration - pub(crate) q_ei: QEiStrategy, - /// Criterion to select next point to evaluate - pub(crate) infill_criterion: Box, - /// The optimizer used to optimize infill criterium - pub(crate) infill_optimizer: InfillOptimizer, - /// Regression specification for GP models used by mixture of experts (see [egobox_moe]) - pub(crate) regression_spec: RegressionSpec, - /// Correlation specification for GP models used by mixture of experts (see [egobox_moe]) - pub(crate) correlation_spec: CorrelationSpec, - /// Optional dimension reduction (see [egobox_moe]) - pub(crate) kpls_dim: Option, - /// Number of clusters used by mixture of experts (see [egobox_moe]) - /// When set to 0 the clusters are computes automatically and refreshed - /// every 10-points (tentative) additions - pub(crate) n_clusters: usize, - /// Specification of a target objective value which is used to stop the algorithm once reached - pub(crate) target: f64, - /// Directory to save intermediate results: inital doe + evalutions at each iteration - pub(crate) outdir: Option, - /// If true use `outdir` to retrieve and start from previous results - pub(crate) hot_start: bool, + pub(crate) config: EgorConfig, /// Matrix (nx, 2) of [lower bound, upper bound] of the nx components of x /// Note: used for continuous variables handling, the optimizer base. pub(crate) xlimits: Array2, - /// List of x types allowing the handling of discrete input variables - pub(crate) xtypes: Option>, - /// Flag for discrete handling, true if mixed-integer type present in xtypes, otherwise false - pub(crate) no_discrete: bool, /// An optional surrogate builder used to model objective and constraint /// functions, otherwise [mixture of expert](egobox_moe) is used /// Note: if specified takes precedence over individual settings @@ -258,32 +212,21 @@ impl EgorSolver { /// The function `f` should return an objective value but also constraint values if any. /// Design space is specified by the matrix `xlimits` which is `[nx, 2]`-shaped /// the ith row contains lower and upper bounds of the ith component of `x`. - pub fn new(xlimits: &ArrayBase, Ix2>, rng: Xoshiro256Plus) -> Self { + pub fn new( + config: EgorConfig, + xlimits: &ArrayBase, Ix2>, + rng: Xoshiro256Plus, + ) -> Self { let env = Env::new().filter_or("EGOBOX_LOG", "info"); let mut builder = Builder::from_env(env); let builder = builder.target(env_logger::Target::Stdout); builder.try_init().ok(); EgorSolver { - n_iter: 20, - n_start: 20, - q_points: 1, - n_doe: 0, - n_cstr: 0, - cstr_tol: None, - doe: None, - q_ei: QEiStrategy::KrigingBeliever, - infill_criterion: Box::new(WB2), - infill_optimizer: InfillOptimizer::Slsqp, - regression_spec: RegressionSpec::CONSTANT, - correlation_spec: CorrelationSpec::SQUAREDEXPONENTIAL, - kpls_dim: None, - n_clusters: 1, - target: f64::NEG_INFINITY, - outdir: None, - hot_start: false, + config: EgorConfig { + xtypes: Some(continuous_xlimits_to_xtypes(xlimits)), // align xlimits and xtypes + ..config + }, xlimits: xlimits.to_owned(), - xtypes: Some(continuous_xlimits_to_xtypes(xlimits)), - no_discrete: true, surrogate_builder: SB::new_with_xtypes_rng(&continuous_xlimits_to_xtypes(xlimits)), rng, } @@ -301,175 +244,18 @@ impl EgorSolver { let builder = builder.target(env_logger::Target::Stdout); builder.try_init().ok(); let v_xtypes = xtypes.to_vec(); - let xlimits = unfold_xtypes_as_continuous_limits(xtypes); EgorSolver { - n_iter: 20, - n_start: 20, - q_points: 1, - n_doe: 0, - n_cstr: 0, - cstr_tol: None, - doe: None, - q_ei: QEiStrategy::KrigingBeliever, - infill_criterion: Box::new(WB2), - infill_optimizer: InfillOptimizer::Slsqp, - regression_spec: RegressionSpec::CONSTANT, - correlation_spec: CorrelationSpec::SQUAREDEXPONENTIAL, - kpls_dim: None, - n_clusters: 1, - target: f64::NEG_INFINITY, - outdir: None, - hot_start: false, - xlimits, - xtypes: Some(v_xtypes), + config: EgorConfig { + xtypes: Some(v_xtypes), + no_discrete: no_discrete(xtypes), + ..EgorConfig::default() + }, + xlimits: unfold_xtypes_as_continuous_limits(xtypes), surrogate_builder: SB::new_with_xtypes_rng(xtypes), - no_discrete: no_discrete(xtypes), rng, } } - pub fn infill_criterion(mut self, infill_criterion: Box) -> Self { - self.infill_criterion = infill_criterion; - self - } - - /// Sets allowed number of evaluation of the function under optimization - pub fn n_iter(mut self, n_iter: usize) -> Self { - self.n_iter = n_iter; - self - } - - /// Sets the number of runs of infill strategy optimizations (best result taken) - pub fn n_start(mut self, n_start: usize) -> Self { - self.n_start = n_start; - self - } - - /// Sets Number of parallel evaluations of the function under optimization - pub fn q_points(mut self, q_points: usize) -> Self { - self.q_points = q_points; - self - } - - /// Number of samples of initial LHS sampling (used when DOE not provided by the user) - /// - /// When 0 a number of points is computed automatically regarding the number of input variables - /// of the function under optimization. - pub fn n_doe(mut self, n_doe: usize) -> Self { - self.n_doe = n_doe; - self - } - - /// Sets the number of constraint functions - pub fn n_cstr(mut self, n_cstr: usize) -> Self { - self.n_cstr = n_cstr; - self - } - - /// Sets the tolerance on constraints violation (`cstr < tol`) - pub fn cstr_tol(mut self, tol: &Array1) -> Self { - self.cstr_tol = Some(tol.to_owned()); - self - } - - /// Sets an initial DOE \['ns', `nt`\] containing `ns` samples. - /// - /// Either `nt` = `nx` then only `x` input values are specified and `ns` evals are done to get y ouput doe values, - /// or `nt = nx + ny` then `x = doe\[:, :nx\]` and `y = doe\[:, nx:\]` are specified - pub fn doe(mut self, doe: &Array2) -> Self { - self.doe = Some(doe.to_owned()); - self - } - - /// Removes any previously specified initial doe to get the default doe usage - pub fn default_doe(mut self) -> Self { - self.doe = None; - self - } - - /// Sets the parallel infill strategy - /// - /// Parallel infill criterion to get virtual next promising points in order to allow - /// n parallel evaluations of the function under optimization. - pub fn qei_strategy(mut self, q_ei: QEiStrategy) -> Self { - self.q_ei = q_ei; - self - } - - /// Sets the infill strategy - pub fn infill_strategy(mut self, infill: InfillStrategy) -> Self { - self.infill_criterion = match infill { - InfillStrategy::EI => Box::new(EI), - InfillStrategy::WB2 => Box::new(WB2), - InfillStrategy::WB2S => Box::new(WB2S), - }; - self - } - - /// Sets the infill optimizer - pub fn infill_optimizer(mut self, optimizer: InfillOptimizer) -> Self { - self.infill_optimizer = optimizer; - self - } - - /// Sets the allowed regression models used in gaussian processes. - pub fn regression_spec(mut self, regression_spec: RegressionSpec) -> Self { - self.regression_spec = regression_spec; - self - } - - /// Sets the allowed correlation models used in gaussian processes. - pub fn correlation_spec(mut self, correlation_spec: CorrelationSpec) -> Self { - self.correlation_spec = correlation_spec; - self - } - - /// Sets the number of components to be used specifiying PLS projection is used (a.k.a KPLS method). - /// - /// This is used to address high-dimensional problems typically when `nx` > 9 wher `nx` is the dimension of `x`. - pub fn kpls_dim(mut self, kpls_dim: usize) -> Self { - self.kpls_dim = Some(kpls_dim); - self - } - - /// Removes any PLS dimension reduction usage - pub fn no_kpls(mut self) -> Self { - self.kpls_dim = None; - self - } - - /// Sets the number of clusters used by the mixture of surrogate experts. - /// - /// When set to Some(0), the number of clusters is determined automatically - /// When set None, default to 1 - pub fn n_clusters(mut self, n_clusters: usize) -> Self { - self.n_clusters = n_clusters; - self - } - - /// Sets a known target minimum to be used as a stopping criterion. - pub fn target(mut self, target: f64) -> Self { - self.target = target; - self - } - - /// Sets a directory to write optimization history and used as search path for hot start doe - pub fn outdir(mut self, outdir: impl Into) -> Self { - self.outdir = Some(outdir.into()); - self - } - /// Do not write optimization history - pub fn no_outdir(mut self) -> Self { - self.outdir = None; - self - } - - /// Whether we start by loading last DOE saved in `outdir` as initial DOE - pub fn hot_start(mut self, hot_start: bool) -> Self { - self.hot_start = hot_start; - self - } - /// Given an evaluated doe (x, y) data, return the next promising x point /// where optimum may occurs regarding the infill criterium. /// This function inverse the control of the optimization and can used @@ -481,11 +267,12 @@ impl EgorSolver { ) -> Array2 { let rng = self.rng.clone(); let sampling = Lhs::new(&self.xlimits).with_rng(rng).kind(LhsKind::Maximin); - let mut clusterings = vec![None; 1 + self.n_cstr]; + let mut clusterings = vec![None; 1 + self.config.n_cstr]; let cstr_tol = self + .config .cstr_tol .clone() - .unwrap_or(Array1::from_elem(self.n_cstr, DEFAULT_CSTR_TOL)); + .unwrap_or(Array1::from_elem(self.config.n_cstr, DEFAULT_CSTR_TOL)); let (x_dat, _) = self.next_points( true, false, // done anyway @@ -524,24 +311,25 @@ where let rng = self.rng.clone(); let sampling = Lhs::new(&self.xlimits).with_rng(rng).kind(LhsKind::Maximin); - let hstart_doe: Option> = if self.hot_start && self.outdir.is_some() { - let path: &String = self.outdir.as_ref().unwrap(); - let filepath = std::path::Path::new(&path).join(DOE_FILE); - if filepath.is_file() { - info!("Reading DOE from {:?}", filepath); - Some(read_npy(filepath)?) - } else if std::path::Path::new(&path).join(DOE_INITIAL_FILE).is_file() { - let filepath = std::path::Path::new(&path).join(DOE_INITIAL_FILE); - info!("Reading DOE from {:?}", filepath); - Some(read_npy(filepath)?) + let hstart_doe: Option> = + if self.config.hot_start && self.config.outdir.is_some() { + let path: &String = self.config.outdir.as_ref().unwrap(); + let filepath = std::path::Path::new(&path).join(DOE_FILE); + if filepath.is_file() { + info!("Reading DOE from {:?}", filepath); + Some(read_npy(filepath)?) + } else if std::path::Path::new(&path).join(DOE_INITIAL_FILE).is_file() { + let filepath = std::path::Path::new(&path).join(DOE_INITIAL_FILE); + info!("Reading DOE from {:?}", filepath); + Some(read_npy(filepath)?) + } else { + None + } } else { None - } - } else { - None - }; + }; - let doe = hstart_doe.as_ref().or(self.doe.as_ref()); + let doe = hstart_doe.as_ref().or(self.config.doe.as_ref()); let (y_data, x_data) = if let Some(doe) = doe { if doe.ncols() == self.xlimits.nrows() { @@ -557,25 +345,25 @@ where ) } } else { - let n_doe = if self.n_doe == 0 { + let n_doe = if self.config.n_doe == 0 { (self.xlimits.nrows() + 1).max(5) } else { - self.n_doe + self.config.n_doe }; info!("Compute initial LHS with {} points", n_doe); let x = sampling.sample(n_doe); (self.eval_obj(problem, &x), x) }; let doe = concatenate![Axis(1), x_data, y_data]; - if self.outdir.is_some() { - let path = self.outdir.as_ref().unwrap(); + if self.config.outdir.is_some() { + let path = self.config.outdir.as_ref().unwrap(); std::fs::create_dir_all(path)?; let filepath = std::path::Path::new(path).join(DOE_INITIAL_FILE); info!("Save initial doe {:?} in {:?}", doe.shape(), filepath); write_npy(filepath, &doe).expect("Write initial doe"); } - let clusterings = vec![None; self.n_cstr + 1]; + let clusterings = vec![None; self.config.n_cstr + 1]; let no_point_added_retries = MAX_POINT_ADDITION_RETRY; let mut initial_state = state @@ -583,14 +371,15 @@ where .clusterings(clusterings) .sampling(sampling); initial_state.doe_size = doe.nrows(); - initial_state.max_iters = self.n_iter as u64; + initial_state.max_iters = self.config.n_iter as u64; initial_state.added = doe.nrows(); initial_state.no_point_added_retries = no_point_added_retries; initial_state.cstr_tol = self + .config .cstr_tol .clone() - .unwrap_or(Array1::from_elem(self.n_cstr, DEFAULT_CSTR_TOL)); - initial_state.target_cost = self.target; + .unwrap_or(Array1::from_elem(self.config.n_cstr, DEFAULT_CSTR_TOL)); + initial_state.target_cost = self.config.target; debug!("INITIAL STATE = {:?}", initial_state); Ok((initial_state, None)) } @@ -701,7 +490,7 @@ where } }; - let add_count = (self.q_points - rejected_count) as i32; + let add_count = (self.config.q_points - rejected_count) as i32; let x_to_eval = x_data.slice(s![-add_count.., ..]).to_owned(); debug!( "Eval {} point{} {}", @@ -723,8 +512,8 @@ where .and(y_actual.columns()) .for_each(|mut y, val| y.assign(&val)); let doe = concatenate![Axis(1), x_data, y_data]; - if self.outdir.is_some() { - let path = self.outdir.as_ref().unwrap(); + if self.config.outdir.is_some() { + let path = self.config.outdir.as_ref().unwrap(); std::fs::create_dir_all(path)?; let filepath = std::path::Path::new(path).join(DOE_FILE); info!("Save doe in {:?}", filepath); @@ -761,7 +550,7 @@ where SB: SurrogateBuilder, { fn have_to_recluster(&self, added: usize, prev_added: usize) -> bool { - self.n_clusters == 0 && (added != 0 && added % 10 == 0 && added - prev_added > 0) + self.config.n_clusters == 0 && (added != 0 && added % 10 == 0 && added - prev_added > 0) } fn make_clustered_surrogate( @@ -774,10 +563,10 @@ where model_name: &str, ) -> Box { let mut builder = self.surrogate_builder.clone(); - builder.set_kpls_dim(self.kpls_dim); - builder.set_regression_spec(self.regression_spec); - builder.set_correlation_spec(self.correlation_spec); - builder.set_n_clusters(self.n_clusters); + builder.set_kpls_dim(self.config.kpls_dim); + builder.set_regression_spec(self.config.regression_spec); + builder.set_correlation_spec(self.config.correlation_spec); + builder.set_n_clusters(self.config.n_clusters); if init || recluster { if recluster { @@ -819,7 +608,7 @@ where debug!("Make surrogate with {}", x_data); let mut x_dat = Array2::zeros((0, x_data.ncols())); let mut y_dat = Array2::zeros((0, y_data.ncols())); - for i in 0..self.q_points { + for i in 0..self.config.q_points { let (xt, yt) = if i == 0 { (x_data.to_owned(), y_data.to_owned()) } else { @@ -830,26 +619,26 @@ where }; info!("Train surrogates with {} points...", xt.nrows()); - let models: Vec> = (0..=self - .n_cstr) - .into_par_iter() - .map(|k| { - let name = if k == 0 { - "Objective".to_string() - } else { - format!("Constraint[{k}]") - }; - self.make_clustered_surrogate( - &xt, - &yt.slice(s![.., k..k + 1]).to_owned(), - init && i == 0, - recluster, - &clusterings[k], - &name, - ) - }) - .collect(); - (0..=self.n_cstr).for_each(|k| clusterings[k] = Some(models[k].to_clustering())); + let models: Vec> = + (0..=self.config.n_cstr) + .into_par_iter() + .map(|k| { + let name = if k == 0 { + "Objective".to_string() + } else { + format!("Constraint[{k}]") + }; + self.make_clustered_surrogate( + &xt, + &yt.slice(s![.., k..k + 1]).to_owned(), + init && i == 0, + recluster, + &clusterings[k], + &name, + ) + }) + .collect(); + (0..=self.config.n_cstr).for_each(|k| clusterings[k] = Some(models[k].to_clustering())); let (obj_model, cstr_models) = models.split_first().unwrap(); debug!("... surrogates trained"); @@ -864,12 +653,13 @@ where lhs_optim, ) { Ok(xk) => { + println!(">>>>>>>>>> {}", self.config.n_cstr); match self.get_virtual_point(&xk, y_data, obj_model.as_ref(), cstr_models) { Ok(yk) => { y_dat = concatenate![ Axis(0), y_dat, - Array2::from_shape_vec((1, 1 + self.n_cstr), yk).unwrap() + Array2::from_shape_vec((1, 1 + self.config.n_cstr), yk).unwrap() ]; x_dat = concatenate![Axis(0), x_dat, xk.insert_axis(Axis(0))]; } @@ -918,9 +708,10 @@ where info!("Constraints scaling is updated to {}", scale_cstr); scale_cstr }; - let scale_ic = self - .infill_criterion - .scaling(&scaling_points.view(), obj_model, f_min); + let scale_ic = + self.config + .infill_criterion + .scaling(&scaling_points.view(), obj_model, f_min); (scale_infill_obj, scale_cstr, scale_ic) } @@ -945,7 +736,7 @@ where let (scale_infill_obj, scale_cstr, scale_wb2) = self.compute_scaling(sampling, obj_model, cstr_models, *f_min); - let algorithm = match self.infill_optimizer { + let algorithm = match self.config.infill_optimizer { InfillOptimizer::Slsqp => crate::optimizer::Algorithm::Slsqp, InfillOptimizer::Cobyla => crate::optimizer::Algorithm::Cobyla, }; @@ -976,7 +767,7 @@ where self.eval_infill_obj(x, obj_model, *f_min, *scale_infill_obj, *scale_wb2) }; - let cstrs: Vec<_> = (0..self.n_cstr) + let cstrs: Vec<_> = (0..self.config.n_cstr) .map(|i| { let index = i; let cstr = move |x: &[f64], @@ -1038,7 +829,7 @@ where scale_wb2, }; while !success && n_optim <= n_max_optim { - let x_start = sampling.sample(self.n_start); + let x_start = sampling.sample(self.config.n_start); if let Some(seed) = lhs_optim_seed { let (_, x_opt) = @@ -1052,7 +843,7 @@ where success = true; } else { let dim = x_data.ncols(); - let res = (0..self.n_start) + let res = (0..self.config.n_start) .into_par_iter() .map(|i| { Optimizer::new(algorithm, &obj, &cstr_refs, &obj_data, &self.xlimits) @@ -1101,10 +892,10 @@ where cstr_models: &[Box], ) -> Result> { let mut res: Vec = Vec::with_capacity(3); - if self.q_ei == QEiStrategy::ConstantLiarMinimum { + if self.config.q_ei == QEiStrategy::ConstantLiarMinimum { let index_min = y_data.slice(s![.., 0_usize]).argmin().unwrap(); res.push(y_data[[index_min, 0]]); - for ic in 1..=self.n_cstr { + for ic in 1..=self.config.n_cstr { res.push(y_data[[index_min, ic]]); } Ok(res) @@ -1112,7 +903,7 @@ where let x = &xk.view().insert_axis(Axis(0)); let pred = obj_model.predict_values(x)?[[0, 0]]; let var = obj_model.predict_variances(x)?[[0, 0]]; - let conf = match self.q_ei { + let conf = match self.config.q_ei { QEiStrategy::KrigingBeliever => 0., QEiStrategy::KrigingBelieverLowerBound => -3., QEiStrategy::KrigingBelieverUpperBound => 3., @@ -1170,6 +961,7 @@ where ) -> f64 { let x_f = x.to_vec(); let obj = -(self + .config .infill_criterion .value(&x_f, obj_model, f_min, Some(scale_ic))); obj / scale @@ -1185,6 +977,7 @@ where ) -> Vec { let x_f = x.to_vec(); let grad = -(self + .config .infill_criterion .grad(&x_f, obj_model, f_min, Some(scale_ic))); (grad / scale).to_vec() @@ -1195,7 +988,7 @@ where pb: &mut Problem, x: &Array2, ) -> Array2 { - let params = if let Some(xtypes) = &self.xtypes { + let params = if let Some(xtypes) = &self.config.xtypes { let xcast = cast_to_discrete_values(xtypes, x); fold_with_enum_index(xtypes, &xcast.view()) } else { diff --git a/ego/src/lib.rs b/ego/src/lib.rs index 57a614cc..8ba7b5f3 100644 --- a/ego/src/lib.rs +++ b/ego/src/lib.rs @@ -71,11 +71,12 @@ //! let xtypes = vec![XType::Int(0, 25)]; //! //! let res = EgorBuilder::optimize(mixsinx) +//! .configure(|config| +//! config.doe(&doe) // we pass the initial doe +//! .n_iter(n_iter) +//! .infill_strategy(InfillStrategy::EI)) //! .random_seed(42) //! .min_within_mixint_space(&xtypes) // We build a mixed-integer optimizer -//! .doe(&doe) // we pass the initial doe -//! .n_iter(n_iter) -//! .infill_strategy(InfillStrategy::EI) //! .run() //! .expect("Egor minimization"); //! println!("min f(x)={} at x={}", res.y_opt, res.x_opt); @@ -188,6 +189,7 @@ //! mod criteria; mod egor; +mod egor_config; mod egor_service; mod egor_solver; mod egor_state; @@ -202,6 +204,7 @@ mod utils; pub use crate::criteria::*; pub use crate::egor::*; +pub use crate::egor_config::*; pub use crate::egor_service::*; pub use crate::egor_solver::*; pub use crate::egor_state::*; diff --git a/src/egor.rs b/src/egor.rs index ff7697ae..c82aaf05 100644 --- a/src/egor.rs +++ b/src/egor.rs @@ -310,35 +310,41 @@ impl Egor { let cstr_tol = self.cstr_tol.clone().unwrap_or(vec![0.0; self.n_cstr]); let cstr_tol = Array1::from_vec(cstr_tol); - let mut mixintegor = mixintegor_build - .min_within_mixint_space(&xtypes) - .n_cstr(self.n_cstr) - .n_iter(n_iter) - .n_start(self.n_start) - .n_doe(self.n_doe) - .cstr_tol(&cstr_tol) - .regression_spec(egobox_moe::RegressionSpec::from_bits(self.regression_spec.0).unwrap()) - .correlation_spec( - egobox_moe::CorrelationSpec::from_bits(self.correlation_spec.0).unwrap(), - ) - .infill_strategy(infill_strategy) - .q_points(self.q_points) - .qei_strategy(qei_strategy) - .infill_optimizer(infill_optimizer) - .target(self.target) - .hot_start(self.hot_start); - if let Some(doe) = self.doe.as_ref() { - mixintegor = mixintegor.doe(doe); - }; - if let Some(kpls_dim) = self.kpls_dim { - mixintegor = mixintegor.kpls_dim(kpls_dim); - }; - if let Some(n_clusters) = self.n_clusters { - mixintegor = mixintegor.n_clusters(n_clusters); - }; - if let Some(outdir) = self.outdir.as_ref().cloned() { - mixintegor = mixintegor.outdir(outdir); - }; + let mixintegor = mixintegor_build + .configure(|config| { + let mut config = config + .n_cstr(self.n_cstr) + .n_iter(n_iter) + .n_start(self.n_start) + .n_doe(self.n_doe) + .cstr_tol(&cstr_tol) + .regression_spec( + egobox_moe::RegressionSpec::from_bits(self.regression_spec.0).unwrap(), + ) + .correlation_spec( + egobox_moe::CorrelationSpec::from_bits(self.correlation_spec.0).unwrap(), + ) + .infill_strategy(infill_strategy) + .q_points(self.q_points) + .qei_strategy(qei_strategy) + .infill_optimizer(infill_optimizer) + .target(self.target) + .hot_start(self.hot_start); + if let Some(doe) = self.doe.as_ref() { + config = config.doe(doe); + }; + if let Some(kpls_dim) = self.kpls_dim { + config = config.kpls_dim(kpls_dim); + }; + if let Some(n_clusters) = self.n_clusters { + config = config.n_clusters(n_clusters); + }; + if let Some(outdir) = self.outdir.as_ref().cloned() { + config = config.outdir(outdir); + }; + config + }) + .min_within_mixint_space(&xtypes); let res = py.allow_threads(|| { mixintegor From 6bfb21e19571749640a0807e87482849c8c1b0b8 Mon Sep 17 00:00:00 2001 From: relf Date: Sat, 18 Nov 2023 18:54:36 +0100 Subject: [PATCH 03/13] Add an example of optimization as a service --- ego/examples/g24.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 ego/examples/g24.rs diff --git a/ego/examples/g24.rs b/ego/examples/g24.rs new file mode 100644 index 00000000..bf4059f7 --- /dev/null +++ b/ego/examples/g24.rs @@ -0,0 +1,51 @@ +use egobox_doe::{Lhs, SamplingMethod}; +use egobox_ego::EgorServiceBuilder; +use ndarray::{array, concatenate, Array2, ArrayBase, ArrayView2, Axis, Data, Ix1, Zip}; + +// Objective +fn g24(x: &ArrayBase, Ix1>) -> f64 { + // Function G24: 1 global optimum y_opt = -5.5080 at x_opt =(2.3295, 3.1785) + -x[0] - x[1] +} + +// Constraints < 0 +fn g24_c1(x: &ArrayBase, Ix1>) -> f64 { + -2.0 * x[0].powf(4.0) + 8.0 * x[0].powf(3.0) - 8.0 * x[0].powf(2.0) + x[1] - 2.0 +} + +fn g24_c2(x: &ArrayBase, Ix1>) -> f64 { + -4.0 * x[0].powf(4.0) + 32.0 * x[0].powf(3.0) - 88.0 * x[0].powf(2.0) + 96.0 * x[0] + x[1] + - 36.0 +} + +fn f_g24(x: &ArrayView2) -> Array2 { + let mut y = Array2::zeros((x.nrows(), 3)); + Zip::from(y.rows_mut()) + .and(x.rows()) + .for_each(|mut yi, xi| { + yi.assign(&array![g24(&xi), g24_c1(&xi), g24_c2(&xi)]); + }); + y +} + +fn main() { + let xlimits = array![[0., 3.], [0., 4.]]; + let mut doe = Lhs::new(&xlimits).sample(3); + + // We use Egor optimizer as a service + let egor = EgorServiceBuilder::optimize() + .configure(|config| config.n_cstr(2)) + .random_seed(42) + .min_within(&xlimits); + + let mut y_doe = f_g24(&doe.view()); + for _i in 0..10 { + // We tell function values and ask for next x location + let x_suggested = egor.suggest(&doe, &y_doe); + + doe = concatenate![Axis(0), doe, x_suggested]; + y_doe = f_g24(&doe.view()); + } + + println!("G24 optim x suggestion history = {doe:?}"); +} From 65508bb316cd5a45c027817246220e6969a88c2c Mon Sep 17 00:00:00 2001 From: relf Date: Sat, 18 Nov 2023 18:55:00 +0100 Subject: [PATCH 04/13] Fix doc tests --- ego/src/egor.rs | 2 +- ego/src/egor_service.rs | 61 ++++++++++++++++++++++------------------- ego/src/egor_solver.rs | 19 +++++++------ ego/src/lib.rs | 42 ++++++++++------------------ 4 files changed, 60 insertions(+), 64 deletions(-) diff --git a/ego/src/egor.rs b/ego/src/egor.rs index 178fcf81..a5f94254 100644 --- a/ego/src/egor.rs +++ b/ego/src/egor.rs @@ -168,7 +168,7 @@ impl EgorBuilder { }; Egor { fobj: ObjFunc::new(self.fobj), - solver: EgorSolver::new_with_xtypes(xtypes, rng), + solver: EgorSolver::new_with_xtypes(self.config, xtypes, rng), } } } diff --git a/ego/src/egor_service.rs b/ego/src/egor_service.rs index 388177a1..acdda956 100644 --- a/ego/src/egor_service.rs +++ b/ego/src/egor_service.rs @@ -1,34 +1,42 @@ -//! Egor optimizer service implements Egor optimizer with an ask-and-tell interface. +//! Egor optimizer service implements [`Egor`] optimizer with an ask-and-tell interface. +//! It allows to keep the control on the iteration loop by asking for optimum location +//! suggestions and telling objective function values at these points. //! //! ```no_run -//! # use ndarray::{array, Array2, ArrayView1, ArrayView2, Zip}; +//! # use ndarray::{array, Array2, ArrayView1, ArrayView2, Zip, concatenate, Axis}; //! # use egobox_doe::{Lhs, SamplingMethod}; -//! # use egobox_ego::{EgorBuilder, InfillStrategy, InfillOptimizer}; +//! # use egobox_ego::{EgorServiceBuilder, InfillStrategy, RegressionSpec, CorrelationSpec}; //! //! # use rand_xoshiro::Xoshiro256Plus; //! # use ndarray_rand::rand::SeedableRng; //! use argmin_testfunctions::rosenbrock; //! -//! // Rosenbrock test function: minimum y_opt = 0 at x_opt = (1, 1) -//! fn rosenb(x: &ArrayView2) -> Array2 { -//! let mut y: Array2 = Array2::zeros((x.nrows(), 1)); -//! Zip::from(y.rows_mut()) -//! .and(x.rows()) -//! .par_for_each(|mut yi, xi| yi.assign(&array![rosenbrock(&xi.to_vec(), 1., 100.)])); -//! y +//! fn xsinx(x: &ArrayView2) -> Array2 { +//! (x - 3.5) * ((x - 3.5) / std::f64::consts::PI).mapv(|v| v.sin()) //! } //! -//! let xlimits = array![[-2., 2.], [-2., 2.]]; -//! // TODO -//! //let res = EgorBuilder::optimize(rosenb) -//! // .min_within(&xlimits) -//! // .infill_strategy(InfillStrategy::EI) -//! // .n_doe(10) -//! // .target(1e-1) -//! // .n_iter(30) -//! // .run() -//! // .expect("Rosenbrock minimization"); -//! //println!("Rosenbrock min result = {:?}", res); +//! let egor = EgorServiceBuilder::optimize() +//! .configure(|conf| { +//! conf.regression_spec(RegressionSpec::ALL) +//! .correlation_spec(CorrelationSpec::ALL) +//! .infill_strategy(InfillStrategy::EI) +//! }) +//! .random_seed(42) +//! .min_within(&array![[0., 25.]]); +//! +//! let mut doe = array![[0.], [7.], [20.], [25.]]; +//! let mut y_doe = xsinx(&doe.view()); +//! +//! for _i in 0..10 { +//! // we tell function values and ask for next suggested optimum location +//! let x_suggested = egor.suggest(&doe, &y_doe); +//! +//! // we update the doe +//! doe = concatenate![Axis(0), doe, x_suggested]; +//! y_doe = xsinx(&doe.view()); +//! } +//! +//! println!("Rosenbrock min result = {:?}", doe); //! ``` //! use crate::egor_config::*; @@ -42,7 +50,7 @@ use ndarray_rand::rand::SeedableRng; use rand_xoshiro::Xoshiro256Plus; /// EGO optimizer service builder allowing to use Egor optimizer -/// with an ask-and-tell interface. +/// as a service. /// pub struct EgorServiceBuilder { config: EgorConfig, @@ -104,23 +112,20 @@ impl EgorServiceBuilder { }; EgorService { config: self.config.clone(), - solver: EgorSolver::new_with_xtypes(xtypes, rng), + solver: EgorSolver::new_with_xtypes(self.config, xtypes, rng), } } } -/// Egor optimizer structure used to parameterize the underlying `argmin::Solver` -/// and trigger the optimization using `argmin::Executor`. +/// Egor optimizer service. #[derive(Clone)] pub struct EgorService { - #[allow(dead_code)] config: EgorConfig, solver: EgorSolver, } impl EgorService { - #[allow(dead_code)] - fn configure EgorConfig>(mut self, init: F) -> Self { + pub fn configure EgorConfig>(mut self, init: F) -> Self { self.config = init(self.config); self } diff --git a/ego/src/egor_solver.rs b/ego/src/egor_solver.rs index 8c355490..8fcfcff6 100644 --- a/ego/src/egor_solver.rs +++ b/ego/src/egor_solver.rs @@ -8,7 +8,7 @@ //! ```no_run //! use ndarray::{array, Array2, ArrayView1, ArrayView2, Zip}; //! use egobox_doe::{Lhs, SamplingMethod}; -//! use egobox_ego::{EgorBuilder, InfillStrategy, InfillOptimizer, ObjFunc, EgorSolver}; +//! use egobox_ego::{EgorBuilder, EgorConfig, InfillStrategy, InfillOptimizer, ObjFunc, EgorSolver}; //! use egobox_moe::MoeParams; //! use rand_xoshiro::Xoshiro256Plus; //! use ndarray_rand::rand::SeedableRng; @@ -27,7 +27,8 @@ //! let rng = Xoshiro256Plus::seed_from_u64(42); //! let xlimits = array![[-2., 2.], [-2., 2.]]; //! let fobj = ObjFunc::new(rosenb); -//! let solver: EgorSolver> = EgorSolver::new(&xlimits, rng); +//! let config = EgorConfig::default(); +//! let solver: EgorSolver> = EgorSolver::new(config, &xlimits, rng); //! let res = Executor::new(fobj, solver) //! .configure(|state| state.max_iters(20)) //! .run() @@ -46,7 +47,7 @@ //! ```no_run //! use ndarray::{array, Array2, ArrayView1, ArrayView2, Zip}; //! use egobox_doe::{Lhs, SamplingMethod}; -//! use egobox_ego::{EgorBuilder, InfillStrategy, InfillOptimizer, ObjFunc, EgorSolver}; +//! use egobox_ego::{EgorBuilder, EgorConfig, InfillStrategy, InfillOptimizer, ObjFunc, EgorSolver}; //! use egobox_moe::MoeParams; //! use rand_xoshiro::Xoshiro256Plus; //! use ndarray_rand::rand::SeedableRng; @@ -84,14 +85,17 @@ //! let doe = Lhs::new(&xlimits).sample(10); //! //! let fobj = ObjFunc::new(f_g24); -//! let solver: EgorSolver> = -//! EgorSolver::new(&xlimits, rng) +//! +//! let config = EgorConfig::default() //! .n_cstr(2) //! .infill_strategy(InfillStrategy::EI) //! .infill_optimizer(InfillOptimizer::Cobyla) //! .doe(&doe) //! .target(-5.5080); //! +//! let solver: EgorSolver> = +//! EgorSolver::new(config, &xlimits, rng); +//! //! let res = Executor::new(fobj, solver) //! .configure(|state| state.max_iters(40)) //! .run() @@ -238,7 +242,7 @@ impl EgorSolver { /// /// The function `f` should return an objective but also constraint values if any. /// Design space is specified by a list of types for input variables `x` of `f` (see [`XType`]). - pub fn new_with_xtypes(xtypes: &[XType], rng: Xoshiro256Plus) -> Self { + pub fn new_with_xtypes(config: EgorConfig, xtypes: &[XType], rng: Xoshiro256Plus) -> Self { let env = Env::new().filter_or("EGOBOX_LOG", "info"); let mut builder = Builder::from_env(env); let builder = builder.target(env_logger::Target::Stdout); @@ -248,7 +252,7 @@ impl EgorSolver { config: EgorConfig { xtypes: Some(v_xtypes), no_discrete: no_discrete(xtypes), - ..EgorConfig::default() + ..config }, xlimits: unfold_xtypes_as_continuous_limits(xtypes), surrogate_builder: SB::new_with_xtypes_rng(xtypes), @@ -653,7 +657,6 @@ where lhs_optim, ) { Ok(xk) => { - println!(">>>>>>>>>> {}", self.config.n_cstr); match self.get_virtual_point(&xk, y_data, obj_model.as_ref(), cstr_models) { Ok(yk) => { y_dat = concatenate![ diff --git a/ego/src/lib.rs b/ego/src/lib.rs index 8ba7b5f3..8e8c5589 100644 --- a/ego/src/lib.rs +++ b/ego/src/lib.rs @@ -28,8 +28,8 @@ //! //! // We ask for 10 evaluations of the objective function to get the result //! let res = EgorBuilder::optimize(xsinx) +//! .configure(|config| config.n_iter(10)) //! .min_within(&array![[0.0, 25.0]]) -//! .n_iter(10) //! .run() //! .expect("xsinx minimized"); //! println!("Minimum found f(x) = {:?} at x = {:?}", res.x_opt, res.y_opt); @@ -100,11 +100,9 @@ //! approximating your objective function. //! //! ```no_run -//! # use egobox_ego::{EgorBuilder}; -//! # use ndarray::{array, Array2, ArrayView2}; -//! # fn fobj(x: &ArrayView2) -> Array2 { x.to_owned() } -//! # let egor = EgorBuilder::optimize(fobj).min_within(&array![[-1., 1.]]); -//! egor.n_doe(100); +//! # use egobox_ego::{EgorConfig}; +//! # let egor_config = EgorConfig::default(); +//! egor_config.n_doe(100); //! ``` //! //! You can also provide your initial doe though the `egor.doe(your_doe)` method. @@ -114,11 +112,8 @@ //! Gaussian process will be built using the `ndim` (usually 3 or 4) main components in the PLS projected space. //! //! ```no_run -//! # use egobox_ego::{EgorBuilder}; -//! # use ndarray::{array, Array2, ArrayView2}; -//! # fn fobj(x: &ArrayView2) -> Array2 { x.to_owned() } -//! # let egor = EgorBuilder::optimize(fobj).min_within(&array![[-1., 1.]]); -//! egor.kpls_dim(3); +//! # let egor_config = egobox_ego::EgorConfig::default(); +//! egor_config.kpls_dim(3); //! ``` //! //! * Specifications of constraints (expected to be negative at the end of the optimization) @@ -126,11 +121,8 @@ //! the objective function is expected to return an array '\[nsamples, 1 obj value + 2 const values\]'. //! //! ```no_run -//! # use egobox_ego::{EgorBuilder}; -//! # use ndarray::{array, Array2, ArrayView2}; -//! # fn fobj(x: &ArrayView2) -> Array2 { x.to_owned() } -//! # let egor = EgorBuilder::optimize(fobj).min_within(&array![[-1., 1.]]); -//! egor.n_cstr(2); +//! # let egor_config = egobox_ego::EgorConfig::default(); +//! egor_config.n_cstr(2); //! ``` //! //! * If the default infill strategy (WB2, Watson and Barnes 2nd criterion), @@ -138,11 +130,9 @@ //! See \[[Priem2019](#Priem2019)\] //! //! ```no_run -//! # use egobox_ego::{EgorBuilder, InfillStrategy}; -//! # use ndarray::{array, Array2, ArrayView2}; -//! # fn fobj(x: &ArrayView2) -> Array2 { x.to_owned() } -//! # let egor = EgorBuilder::optimize(fobj).min_within(&array![[-1., 1.]]); -//! egor.infill_strategy(InfillStrategy::EI); +//! # use egobox_ego::{EgorConfig, InfillStrategy}; +//! # let egor_config = EgorConfig::default(); +//! egor_config.infill_strategy(InfillStrategy::EI); //! ``` //! //! * The default gaussian process surrogate is parameterized with a constant trend and a squared exponential correlation kernel, also @@ -151,12 +141,10 @@ //! approximation (quality tested through cross validation). //! //! ```no_run -//! # use egobox_ego::{EgorBuilder, RegressionSpec, CorrelationSpec}; -//! # use ndarray::{array, Array2, ArrayView2}; -//! # fn fobj(x: &ArrayView2) -> Array2 { x.to_owned() } -//! # let egor = EgorBuilder::optimize(fobj).min_within(&array![[-1., 1.]]); -//! egor.regression_spec(RegressionSpec::CONSTANT | RegressionSpec::LINEAR) -//! .correlation_spec(CorrelationSpec::MATERN32 | CorrelationSpec::MATERN52); +//! # use egobox_ego::{EgorConfig, RegressionSpec, CorrelationSpec}; +//! # let egor_config = EgorConfig::default(); +//! egor_config.regression_spec(RegressionSpec::CONSTANT | RegressionSpec::LINEAR) +//! .correlation_spec(CorrelationSpec::MATERN32 | CorrelationSpec::MATERN52); //! ``` //! In the above example all GP with combinations of regression and correlation will be tested and the best combination for //! each modeled function will be retained. You can also simply specify `RegressionSpec::ALL` and `CorrelationSpec::ALL` to From 40ff9153c1367742fb26c96331ee8d7e081f135e Mon Sep 17 00:00:00 2001 From: relf Date: Sun, 19 Nov 2023 12:26:06 +0100 Subject: [PATCH 05/13] Move random_seed setting in configuration --- ego/examples/g24.rs | 3 +-- ego/src/egor.rs | 46 ++++++++++++++++++++--------------------- ego/src/egor_config.rs | 16 ++++++++------ ego/src/egor_service.rs | 17 ++++----------- ego/src/lib.rs | 4 ++-- src/egor.rs | 10 ++++----- 6 files changed, 44 insertions(+), 52 deletions(-) diff --git a/ego/examples/g24.rs b/ego/examples/g24.rs index bf4059f7..1632e28f 100644 --- a/ego/examples/g24.rs +++ b/ego/examples/g24.rs @@ -34,8 +34,7 @@ fn main() { // We use Egor optimizer as a service let egor = EgorServiceBuilder::optimize() - .configure(|config| config.n_cstr(2)) - .random_seed(42) + .configure(|config| config.n_cstr(2).random_seed(42)) .min_within(&xlimits); let mut y_doe = f_g24(&doe.view()); diff --git a/ego/src/egor.rs b/ego/src/egor.rs index a5f94254..da763ac8 100644 --- a/ego/src/egor.rs +++ b/ego/src/egor.rs @@ -109,7 +109,6 @@ use argmin::core::{Executor, State}; pub struct EgorBuilder { fobj: O, config: EgorConfig, - seed: Option, } impl EgorBuilder { @@ -122,7 +121,6 @@ impl EgorBuilder { EgorBuilder { fobj, config: EgorConfig::default(), - seed: None, } } @@ -131,13 +129,6 @@ impl EgorBuilder { self } - /// Allow to specify a seed for random number generator to allow - /// reproducible runs. - pub fn random_seed(mut self, seed: u64) -> Self { - self.seed = Some(seed); - self - } - /// Build an Egor optimizer to minimize the function within /// the continuous `xlimits` specified as [[lower, upper], ...] array where the /// number of rows gives the dimension of the inputs (continuous optimization) @@ -146,7 +137,7 @@ impl EgorBuilder { self, xlimits: &ArrayBase, Ix2>, ) -> Egor> { - let rng = if let Some(seed) = self.seed { + let rng = if let Some(seed) = self.config.seed { Xoshiro256Plus::seed_from_u64(seed) } else { Xoshiro256Plus::from_entropy() @@ -161,7 +152,7 @@ impl EgorBuilder { /// inputs specified with given xtypes where some of components may be /// discrete variables (mixed-integer optimization). pub fn min_within_mixint_space(self, xtypes: &[XType]) -> Egor { - let rng = if let Some(seed) = self.seed { + let rng = if let Some(seed) = self.config.seed { Xoshiro256Plus::seed_from_u64(seed) } else { Xoshiro256Plus::from_entropy() @@ -320,8 +311,13 @@ mod tests { let xlimits = array![[0.0, 25.0]]; let doe = Lhs::new(&xlimits).sample(10); let res = EgorBuilder::optimize(xsinx) - .configure(|config| config.n_iter(15).doe(&doe).outdir("target/tests")) - .random_seed(42) + .configure(|config| { + config + .n_iter(15) + .doe(&doe) + .outdir("target/tests") + .random_seed(42) + }) .min_within(&xlimits) .run() .expect("Minimize failure"); @@ -329,8 +325,13 @@ mod tests { assert_abs_diff_eq!(expected, res.x_opt, epsilon = 1e-1); let res = EgorBuilder::optimize(xsinx) - .configure(|config| config.n_iter(5).outdir("target/tests").hot_start(true)) - .random_seed(42) + .configure(|config| { + config + .n_iter(5) + .outdir("target/tests") + .hot_start(true) + .random_seed(42) + }) .min_within(&xlimits) .run() .expect("Egor should minimize xsinx"); @@ -362,8 +363,8 @@ mod tests { .regression_spec(RegressionSpec::ALL) .correlation_spec(CorrelationSpec::ALL) .target(1e-2) + .random_seed(42) }) - .random_seed(42) .min_within(&xlimits) .run() .expect("Minimize failure"); @@ -407,8 +408,7 @@ mod tests { .with_rng(Xoshiro256Plus::seed_from_u64(42)) .sample(3); let res = EgorBuilder::optimize(f_g24) - .configure(|config| config.n_cstr(2).doe(&doe).n_iter(20)) - .random_seed(42) + .configure(|config| config.n_cstr(2).doe(&doe).n_iter(20).random_seed(42)) .min_within(&xlimits) .run() .expect("Minimize failure"); @@ -436,8 +436,8 @@ mod tests { .doe(&doe) .target(-5.5030) .n_iter(30) + .random_seed(42) }) - .random_seed(42) .min_within(&xlimits) .run() .expect("Egor minimization"); @@ -470,8 +470,8 @@ mod tests { .n_iter(n_iter) .target(-15.1) .infill_strategy(InfillStrategy::EI) + .random_seed(42) }) - .random_seed(42) .min_within_mixint_space(&xtypes) .run() .unwrap(); @@ -492,8 +492,8 @@ mod tests { .n_iter(n_iter) .target(-15.1) .infill_strategy(InfillStrategy::EI) + .random_seed(42) }) - .random_seed(42) .min_within_mixint_space(&xtypes) .run() .unwrap(); @@ -512,8 +512,8 @@ mod tests { .regression_spec(egobox_moe::RegressionSpec::CONSTANT) .correlation_spec(egobox_moe::CorrelationSpec::SQUAREDEXPONENTIAL) .n_iter(n_iter) + .random_seed(42) }) - .random_seed(42) .min_within_mixint_space(&xtypes) .run() .unwrap(); @@ -563,8 +563,8 @@ mod tests { .regression_spec(egobox_moe::RegressionSpec::CONSTANT) .correlation_spec(egobox_moe::CorrelationSpec::SQUAREDEXPONENTIAL) .n_iter(n_iter) + .random_seed(42) }) - .random_seed(42) .min_within_mixint_space(&xtypes) .run() .unwrap(); diff --git a/ego/src/egor_config.rs b/ego/src/egor_config.rs index cef72536..3c2e7973 100644 --- a/ego/src/egor_config.rs +++ b/ego/src/egor_config.rs @@ -4,8 +4,6 @@ use crate::types::*; use egobox_moe::{CorrelationSpec, RegressionSpec}; use ndarray::Array1; use ndarray::Array2; -use rand_xoshiro::rand_core::SeedableRng; -use rand_xoshiro::Xoshiro256Plus; use serde::{Deserialize, Serialize}; @@ -59,9 +57,8 @@ pub struct EgorConfig { pub(crate) xtypes: Option>, /// Flag for discrete handling, true if mixed-integer type present in xtypes, otherwise false pub(crate) no_discrete: bool, - /// A random generator used to get reproductible results. - /// For instance: Xoshiro256Plus::from_u64_seed(42) for reproducibility - pub(crate) rng: Xoshiro256Plus, + /// A random generator seed used to get reproductible results. + pub(crate) seed: Option, } impl Default for EgorConfig { @@ -86,7 +83,7 @@ impl Default for EgorConfig { hot_start: false, xtypes: None, no_discrete: true, - rng: Xoshiro256Plus::from_entropy(), + seed: None, } } } @@ -233,4 +230,11 @@ impl EgorConfig { self.hot_start = hot_start; self } + + /// Allow to specify a seed for random number generator to allow + /// reproducible runs. + pub fn random_seed(mut self, seed: u64) -> Self { + self.seed = Some(seed); + self + } } diff --git a/ego/src/egor_service.rs b/ego/src/egor_service.rs index acdda956..1cf56bf1 100644 --- a/ego/src/egor_service.rs +++ b/ego/src/egor_service.rs @@ -20,8 +20,8 @@ //! conf.regression_spec(RegressionSpec::ALL) //! .correlation_spec(CorrelationSpec::ALL) //! .infill_strategy(InfillStrategy::EI) +//! .random_seed(42) //! }) -//! .random_seed(42) //! .min_within(&array![[0., 25.]]); //! //! let mut doe = array![[0.], [7.], [20.], [25.]]; @@ -54,7 +54,6 @@ use rand_xoshiro::Xoshiro256Plus; /// pub struct EgorServiceBuilder { config: EgorConfig, - seed: Option, } impl EgorServiceBuilder { @@ -66,7 +65,6 @@ impl EgorServiceBuilder { pub fn optimize() -> Self { EgorServiceBuilder { config: EgorConfig::default(), - seed: None, } } @@ -75,13 +73,6 @@ impl EgorServiceBuilder { self } - /// Allow to specify a seed for random number generator to allow - /// reproducible runs. - pub fn random_seed(mut self, seed: u64) -> Self { - self.seed = Some(seed); - self - } - /// Build an Egor optimizer to minimize the function within /// the continuous `xlimits` specified as [[lower, upper], ...] array where the /// number of rows gives the dimension of the inputs (continuous optimization) @@ -90,7 +81,7 @@ impl EgorServiceBuilder { self, xlimits: &ArrayBase, Ix2>, ) -> EgorService> { - let rng = if let Some(seed) = self.seed { + let rng = if let Some(seed) = self.config.seed { Xoshiro256Plus::seed_from_u64(seed) } else { Xoshiro256Plus::from_entropy() @@ -105,7 +96,7 @@ impl EgorServiceBuilder { /// inputs specified with given xtypes where some of components may be /// discrete variables (mixed-integer optimization). pub fn min_within_mixint_space(self, xtypes: &[XType]) -> EgorService { - let rng = if let Some(seed) = self.seed { + let rng = if let Some(seed) = self.config.seed { Xoshiro256Plus::seed_from_u64(seed) } else { Xoshiro256Plus::from_entropy() @@ -162,8 +153,8 @@ mod tests { conf.regression_spec(RegressionSpec::ALL) .correlation_spec(CorrelationSpec::ALL) .infill_strategy(InfillStrategy::EI) + .random_seed(42) }) - .random_seed(42) .min_within(&array![[0., 25.]]); let mut doe = array![[0.], [7.], [20.], [25.]]; diff --git a/ego/src/lib.rs b/ego/src/lib.rs index 8e8c5589..aad469b7 100644 --- a/ego/src/lib.rs +++ b/ego/src/lib.rs @@ -74,8 +74,8 @@ //! .configure(|config| //! config.doe(&doe) // we pass the initial doe //! .n_iter(n_iter) -//! .infill_strategy(InfillStrategy::EI)) -//! .random_seed(42) +//! .infill_strategy(InfillStrategy::EI) +//! .random_seed(42)) //! .min_within_mixint_space(&xtypes) // We build a mixed-integer optimizer //! .run() //! .expect("Egor minimization"); diff --git a/src/egor.rs b/src/egor.rs index c82aaf05..efdf2aa4 100644 --- a/src/egor.rs +++ b/src/egor.rs @@ -302,15 +302,10 @@ impl Egor { .collect(); println!("{:?}", xtypes); - let mut mixintegor_build = egobox_ego::EgorBuilder::optimize(obj); - if let Some(seed) = self.seed { - mixintegor_build = mixintegor_build.random_seed(seed); - }; - let cstr_tol = self.cstr_tol.clone().unwrap_or(vec![0.0; self.n_cstr]); let cstr_tol = Array1::from_vec(cstr_tol); - let mixintegor = mixintegor_build + let mixintegor = egobox_ego::EgorBuilder::optimize(obj) .configure(|config| { let mut config = config .n_cstr(self.n_cstr) @@ -342,6 +337,9 @@ impl Egor { if let Some(outdir) = self.outdir.as_ref().cloned() { config = config.outdir(outdir); }; + if let Some(seed) = self.seed { + config = config.random_seed(seed); + }; config }) .min_within_mixint_space(&xtypes); From 5f9b401aee60e53b1c8c7d28008daa32b1f030a6 Mon Sep 17 00:00:00 2001 From: relf Date: Sun, 19 Nov 2023 17:12:10 +0100 Subject: [PATCH 06/13] Fix poetry config --- poetry.lock | 138 +++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 5 +- 2 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 poetry.lock diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..bf1ae477 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,138 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.1.3" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "numpy" +version = "1.24.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, + {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, + {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, + {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, + {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, + {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, + {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, + {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, + {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, + {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, + {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "7.4.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.8" +content-hash = "58ee494648766d21d0334e3e00f5e84f3bf4a1f920ac7d20945af9e60fb88548" diff --git a/pyproject.toml b/pyproject.toml index 0842d4cd..e0cb69f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,10 +27,11 @@ name = "egobox" version = "0.12.0" description = "Python binding for egobox EGO optimizer written in Rust" authors = ["Rémi Lafage "] +packages = [{ include = "egobox", from = "python" }] [tool.poetry.dependencies] numpy = ">=1.18" -python = ">=3.7" +python = ">=3.8" [tool.poetry.dev-dependencies] -pytest = "^6.2" +pytest = ">=6" From 1ee73dfb9ba4a8a0760efefd73397f0c142156cd Mon Sep 17 00:00:00 2001 From: relf Date: Sun, 19 Nov 2023 18:27:23 +0100 Subject: [PATCH 07/13] Rename n_iter in max_iters --- doe/src/lhs.rs | 4 ++-- ego/examples/ackley.rs | 2 +- ego/examples/mopta08.rs | 4 ++-- ego/examples/rosenbrock.rs | 2 +- ego/src/egor.rs | 36 ++++++++++++++++++------------------ ego/src/egor_config.rs | 16 ++++++++-------- ego/src/egor_solver.rs | 2 +- ego/src/lib.rs | 6 +++--- src/egor.rs | 10 +++++----- 9 files changed, 41 insertions(+), 41 deletions(-) diff --git a/doe/src/lhs.rs b/doe/src/lhs.rs index 63f983c9..4eb1447e 100644 --- a/doe/src/lhs.rs +++ b/doe/src/lhs.rs @@ -271,10 +271,10 @@ impl Lhs { lhs.mapv(F::cast) } - fn _maximin_lhs(&self, ns: usize, centered: bool, n_iter: usize) -> Array2 { + fn _maximin_lhs(&self, ns: usize, centered: bool, max_iters: usize) -> Array2 { let mut max_dist = F::zero(); let mut lhs = self._classic_lhs(ns); - for _ in 0..n_iter { + for _ in 0..max_iters { if centered { lhs = self._centered_lhs(ns); } else { diff --git a/ego/examples/ackley.rs b/ego/examples/ackley.rs index 04920eab..42463c58 100644 --- a/ego/examples/ackley.rs +++ b/ego/examples/ackley.rs @@ -19,7 +19,7 @@ fn main() { .regression_spec(RegressionSpec::CONSTANT) .correlation_spec(CorrelationSpec::ABSOLUTEEXPONENTIAL) .infill_strategy(InfillStrategy::WB2S) - .n_iter(200) + .max_iters(200) .target(5e-1) }) .min_within(&xlimits) diff --git a/ego/examples/mopta08.rs b/ego/examples/mopta08.rs index 21354a1b..594d0dcf 100644 --- a/ego/examples/mopta08.rs +++ b/ego/examples/mopta08.rs @@ -253,7 +253,7 @@ fn main() -> anyhow::Result<()> { let dim = args.dim; let outdir = args.outdir; let n_doe = 2 * dim; - let n_iter = 2 * dim; + let max_iters = 2 * dim; const N_CSTR: usize = 68; let cstr_tol = Array1::from_elem(N_CSTR, 1e-4); let kpls_dim = 3; @@ -269,7 +269,7 @@ fn main() -> anyhow::Result<()> { .n_clusters(1) .n_start(50) .n_doe(n_doe) - .n_iter(n_iter) + .max_iters(max_iters) .regression_spec(RegressionSpec::CONSTANT) .correlation_spec(CorrelationSpec::SQUAREDEXPONENTIAL) .infill_optimizer(InfillOptimizer::Slsqp) diff --git a/ego/examples/rosenbrock.rs b/ego/examples/rosenbrock.rs index 2bf088bf..1922850c 100644 --- a/ego/examples/rosenbrock.rs +++ b/ego/examples/rosenbrock.rs @@ -19,7 +19,7 @@ fn rosenbrock(x: &ArrayView2) -> Array2 { fn main() { let xlimits = array![[-2., 2.], [-2., 2.]]; let res = EgorBuilder::optimize(rosenbrock) - .configure(|config| config.n_iter(100).target(1e-2)) + .configure(|config| config.max_iters(100).target(1e-2)) .min_within(&xlimits) .run() .expect("Minimize failure"); diff --git a/ego/src/egor.rs b/ego/src/egor.rs index da763ac8..910f6ab0 100644 --- a/ego/src/egor.rs +++ b/ego/src/egor.rs @@ -24,7 +24,7 @@ //! .infill_strategy(InfillStrategy::EI) //! .n_doe(10) //! .target(1e-1) -//! .n_iter(30)) +//! .max_iters(30)) //! .min_within(&xlimits) //! .run() //! .expect("Rosenbrock minimization"); @@ -81,7 +81,7 @@ //! .infill_strategy(InfillStrategy::EI) //! .infill_optimizer(InfillOptimizer::Cobyla) //! .doe(&doe) -//! .n_iter(40) +//! .max_iters(40) //! .target(-5.5080)) //! .min_within(&xlimits) //! .run() @@ -262,7 +262,7 @@ mod tests { cfg.infill_strategy(InfillStrategy::EI) .regression_spec(RegressionSpec::QUADRATIC) .correlation_spec(CorrelationSpec::ALL) - .n_iter(30) + .max_iters(30) .doe(&initial_doe) .target(-15.1) .outdir("target/tests") @@ -282,7 +282,7 @@ mod tests { let res = EgorBuilder::optimize(xsinx) .configure(|config| { config - .n_iter(20) + .max_iters(20) .regression_spec(RegressionSpec::ALL) .correlation_spec(CorrelationSpec::ALL) }) @@ -297,7 +297,7 @@ mod tests { #[serial] fn test_xsinx_auto_clustering_egor_builder() { let res = EgorBuilder::optimize(xsinx) - .configure(|config| config.n_clusters(0).n_iter(20)) + .configure(|config| config.n_clusters(0).max_iters(20)) .min_within(&array![[0.0, 25.0]]) .run() .expect("Egor with auto clustering should minimize xsinx"); @@ -313,7 +313,7 @@ mod tests { let res = EgorBuilder::optimize(xsinx) .configure(|config| { config - .n_iter(15) + .max_iters(15) .doe(&doe) .outdir("target/tests") .random_seed(42) @@ -327,7 +327,7 @@ mod tests { let res = EgorBuilder::optimize(xsinx) .configure(|config| { config - .n_iter(5) + .max_iters(5) .outdir("target/tests") .hot_start(true) .random_seed(42) @@ -359,7 +359,7 @@ mod tests { .configure(|config| { config .doe(&doe) - .n_iter(100) + .max_iters(100) .regression_spec(RegressionSpec::ALL) .correlation_spec(CorrelationSpec::ALL) .target(1e-2) @@ -408,7 +408,7 @@ mod tests { .with_rng(Xoshiro256Plus::seed_from_u64(42)) .sample(3); let res = EgorBuilder::optimize(f_g24) - .configure(|config| config.n_cstr(2).doe(&doe).n_iter(20).random_seed(42)) + .configure(|config| config.n_cstr(2).doe(&doe).max_iters(20).random_seed(42)) .min_within(&xlimits) .run() .expect("Minimize failure"); @@ -435,7 +435,7 @@ mod tests { .qei_strategy(QEiStrategy::KrigingBeliever) .doe(&doe) .target(-5.5030) - .n_iter(30) + .max_iters(30) .random_seed(42) }) .min_within(&xlimits) @@ -459,7 +459,7 @@ mod tests { #[test] #[serial] fn test_mixsinx_ei_mixint_egor_builder() { - let n_iter = 30; + let max_iters = 30; let doe = array![[0.], [7.], [25.]]; let xtypes = vec![XType::Int(0, 25)]; @@ -467,7 +467,7 @@ mod tests { .configure(|config| { config .doe(&doe) - .n_iter(n_iter) + .max_iters(max_iters) .target(-15.1) .infill_strategy(InfillStrategy::EI) .random_seed(42) @@ -481,7 +481,7 @@ mod tests { #[test] #[serial] fn test_mixsinx_reclustering_mixint_egor_builder() { - let n_iter = 30; + let max_iters = 30; let doe = array![[0.], [7.], [25.]]; let xtypes = vec![XType::Int(0, 25)]; @@ -489,7 +489,7 @@ mod tests { .configure(|config| { config .doe(&doe) - .n_iter(n_iter) + .max_iters(max_iters) .target(-15.1) .infill_strategy(InfillStrategy::EI) .random_seed(42) @@ -503,7 +503,7 @@ mod tests { #[test] #[serial] fn test_mixsinx_wb2_mixint_egor_builder() { - let n_iter = 30; + let max_iters = 30; let xtypes = vec![XType::Int(0, 25)]; let res = EgorBuilder::optimize(mixsinx) @@ -511,7 +511,7 @@ mod tests { config .regression_spec(egobox_moe::RegressionSpec::CONSTANT) .correlation_spec(egobox_moe::CorrelationSpec::SQUAREDEXPONENTIAL) - .n_iter(n_iter) + .max_iters(max_iters) .random_seed(42) }) .min_within_mixint_space(&xtypes) @@ -549,7 +549,7 @@ mod tests { let mut builder = env_logger::Builder::from_env(env); let builder = builder.target(env_logger::Target::Stdout); builder.try_init().ok(); - let n_iter = 10; + let max_iters = 10; let xtypes = vec![ XType::Cont(-5., 5.), XType::Enum(3), @@ -562,7 +562,7 @@ mod tests { config .regression_spec(egobox_moe::RegressionSpec::CONSTANT) .correlation_spec(egobox_moe::CorrelationSpec::SQUAREDEXPONENTIAL) - .n_iter(n_iter) + .max_iters(max_iters) .random_seed(42) }) .min_within_mixint_space(&xtypes) diff --git a/ego/src/egor_config.rs b/ego/src/egor_config.rs index 3c2e7973..006cbf96 100644 --- a/ego/src/egor_config.rs +++ b/ego/src/egor_config.rs @@ -10,11 +10,11 @@ use serde::{Deserialize, Serialize}; /// Egor optimizer configuration #[derive(Clone, Serialize, Deserialize)] pub struct EgorConfig { - /// Number of function iterations allocated to find the optimum (aka iteration budget) - /// Note 1 : The number of cost function evaluations is deduced using the following formula (n_doe + n_iter) - /// Note 2 : When q_points > 1, the number of cost function evaluations is (n_doe + n_iter * q_points) + /// Max number of function iterations allocated to find the optimum (aka iteration budget) + /// Note 1 : The number of cost function evaluations is deduced using the following formula (n_doe + max_iters) + /// Note 2 : When q_points > 1, the number of cost function evaluations is (n_doe + max_iters * q_points) /// is is an upper bounds as some points may be rejected as being to close to previous ones. - pub(crate) n_iter: usize, + pub(crate) max_iters: usize, /// Number of starts for multistart approach used for hyperparameters optimization pub(crate) n_start: usize, /// Number of points returned by EGO iteration (aka qEI Multipoint strategy) @@ -64,7 +64,7 @@ pub struct EgorConfig { impl Default for EgorConfig { fn default() -> Self { EgorConfig { - n_iter: 20, + max_iters: 20, n_start: 20, q_points: 1, n_doe: 0, @@ -94,9 +94,9 @@ impl EgorConfig { self } - /// Sets allowed number of evaluation of the function under optimization - pub fn n_iter(mut self, n_iter: usize) -> Self { - self.n_iter = n_iter; + /// Sets max number of iterations to optimize the objective function + pub fn max_iters(mut self, max_iters: usize) -> Self { + self.max_iters = max_iters; self } diff --git a/ego/src/egor_solver.rs b/ego/src/egor_solver.rs index 8fcfcff6..ccf9f0e7 100644 --- a/ego/src/egor_solver.rs +++ b/ego/src/egor_solver.rs @@ -375,7 +375,7 @@ where .clusterings(clusterings) .sampling(sampling); initial_state.doe_size = doe.nrows(); - initial_state.max_iters = self.config.n_iter as u64; + initial_state.max_iters = self.config.max_iters as u64; initial_state.added = doe.nrows(); initial_state.no_point_added_retries = no_point_added_retries; initial_state.cstr_tol = self diff --git a/ego/src/lib.rs b/ego/src/lib.rs index aad469b7..5916fcdd 100644 --- a/ego/src/lib.rs +++ b/ego/src/lib.rs @@ -28,7 +28,7 @@ //! //! // We ask for 10 evaluations of the objective function to get the result //! let res = EgorBuilder::optimize(xsinx) -//! .configure(|config| config.n_iter(10)) +//! .configure(|config| config.max_iters(10)) //! .min_within(&array![[0.0, 25.0]]) //! .run() //! .expect("xsinx minimized"); @@ -64,7 +64,7 @@ //! } //! } //! -//! let n_iter = 10; +//! let max_iters = 10; //! let doe = array![[0.], [7.], [25.]]; // the initial doe //! //! // We define input as being integer @@ -73,7 +73,7 @@ //! let res = EgorBuilder::optimize(mixsinx) //! .configure(|config| //! config.doe(&doe) // we pass the initial doe -//! .n_iter(n_iter) +//! .max_iters(max_iters) //! .infill_strategy(InfillStrategy::EI) //! .random_seed(42)) //! .min_within_mixint_space(&xtypes) // We build a mixed-integer optimizer diff --git a/src/egor.rs b/src/egor.rs index efdf2aa4..97fcc7b5 100644 --- a/src/egor.rs +++ b/src/egor.rs @@ -240,16 +240,16 @@ impl Egor { /// This function finds the minimum of a given function `fun` /// /// # Parameters - /// n_iter: - /// the iteration budget, number of fun calls is n_doe + q_points * n_iter. + /// max_iters: + /// the iteration budget, number of fun calls is n_doe + q_points * max_iters. /// /// # Returns /// optimization result /// x_opt (array[1, nx]): x value where fun is at its minimum subject to constraint /// y_opt (array[1, nx]): fun(x_opt) /// - #[pyo3(signature = (n_iter = 20))] - fn minimize(&self, py: Python, n_iter: usize) -> PyResult { + #[pyo3(signature = (max_iters = 20))] + fn minimize(&self, py: Python, max_iters: usize) -> PyResult { let fun = self.fun.to_object(py); let obj = move |x: &ArrayView2| -> Array2 { Python::with_gil(|py| { @@ -309,7 +309,7 @@ impl Egor { .configure(|config| { let mut config = config .n_cstr(self.n_cstr) - .n_iter(n_iter) + .max_iters(max_iters) .n_start(self.n_start) .n_doe(self.n_doe) .cstr_tol(&cstr_tol) From 36bafbdcf9caa2a82b08deba675a0cc887c30637 Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 20 Nov 2023 16:02:56 +0100 Subject: [PATCH 08/13] Rename n_iter in max_iters in Python code --- doc/Egor_Tutorial.ipynb | 1260 ++++++++++++------------ python/egobox/examples/optim_g24.py | 2 +- python/egobox/tests/test_egor.py | 20 +- python/egobox/tests/test_mixintegor.py | 4 +- 4 files changed, 643 insertions(+), 643 deletions(-) diff --git a/doc/Egor_Tutorial.ipynb b/doc/Egor_Tutorial.ipynb index 9000f4fc..766b8584 100644 --- a/doc/Egor_Tutorial.ipynb +++ b/doc/Egor_Tutorial.ipynb @@ -1,631 +1,631 @@ { - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "id": "72380b9b", - "metadata": {}, - "source": [ - "# Using _egobox_ optimizer _Egor_" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "31022791", - "metadata": {}, - "source": [ - "## Installation" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "1504e619-5775-42d3-8f48-7339272303ec", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: egobox in d:\\rlafage\\miniconda3\\lib\\site-packages (0.12.0)\n", - "Note: you may need to restart the kernel to use updated packages.\n" - ] - } - ], - "source": [ - "%pip install egobox" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "4c2757f5", - "metadata": {}, - "source": [ - "We import _egobox_ as _egx_ for short." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "0edaf00f", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import egobox as egx" - ] - }, - { - "cell_type": "markdown", - "id": "6c8c8c84", - "metadata": {}, - "source": [ - "You may setup the logging level to get optimization progress during the execution" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "af2d82be", - "metadata": {}, - "outputs": [], - "source": [ - "# To display optimization information (none by default)\n", - "# import logging\n", - "# logging.basicConfig(level=logging.INFO)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "22997c39", - "metadata": {}, - "source": [ - "## Example 1 : Continuous optimization" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "faae2555", - "metadata": {}, - "source": [ - "### Test functions" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "7f6da807", - "metadata": {}, - "outputs": [], - "source": [ - "xspecs_xsinx = egx.to_specs([[0., 25.]])\n", - "n_cstr_xsinx = 0\n", - "\n", - "def xsinx(x: np.ndarray) -> np.ndarray:\n", - " x = np.atleast_2d(x)\n", - " y = (x - 3.5) * np.sin((x - 3.5) / (np.pi))\n", - " return y" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "4c436437", - "metadata": {}, - "outputs": [], - "source": [ - "xspecs_g24 = egx.to_specs([[0., 3.], [0., 4.]])\n", - "n_cstr_g24 = 2\n", - "\n", - "# Objective\n", - "def G24(point):\n", - " \"\"\"\n", - " Function g24\n", - " 1 global optimum y_opt = -5.5080 at x_opt =(2.3295, 3.1785)\n", - " \"\"\"\n", - " p = np.atleast_2d(point)\n", - " return - p[:, 0] - p[:, 1]\n", - "\n", - "# Constraints < 0\n", - "def G24_c1(point):\n", - " p = np.atleast_2d(point)\n", - " return (- 2.0 * p[:, 0] ** 4.0\n", - " + 8.0 * p[:, 0] ** 3.0 \n", - " - 8.0 * p[:, 0] ** 2.0 \n", - " + p[:, 1] - 2.0)\n", - "\n", - "def G24_c2(point):\n", - " p = np.atleast_2d(point)\n", - " return (-4.0 * p[:, 0] ** 4.0\n", - " + 32.0 * p[:, 0] ** 3.0\n", - " - 88.0 * p[:, 0] ** 2.0\n", - " + 96.0 * p[:, 0]\n", - " + p[:, 1] - 36.0)\n", - "\n", - "# Grouped evaluation\n", - "def g24(point):\n", - " p = np.atleast_2d(point)\n", - " return np.array([G24(p), G24_c1(p), G24_c2(p)]).T\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "45641636", - "metadata": {}, - "source": [ - "### Continuous optimization with _Egor_" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "c8942031", - "metadata": {}, - "outputs": [], - "source": [ - "egor = egx.Egor(g24, xspecs_g24, \n", - " n_doe=10, \n", - " n_cstr=n_cstr_g24, \n", - " cstr_tol=[1e-3, 1e-3],\n", - " infill_strategy=egx.InfillStrategy.WB2,\n", - " target=-5.5,\n", - " # outdir=\"./out\",\n", - " # hot_start=True\n", - " ) # see help(egor) for options\n", - "\n", - "# Specify regression and/or correlation models used to build the surrogates of objective and constraints\n", - "#egor = egx.Egor(g24, xlimits_g24, n_cstr=n_cstr_g24, n_doe=10,\n", - "# regr_spec=egx.RegressionSpec.LINEAR,\n", - "# corr_spec=egx.CorrelationSpec.MATERN32 | egx.CorrelationSpec.MATERN52) " - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "c12b8e9d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Optimization f=[-5.50837809e+00 3.84933482e-04 3.56699512e-04] at [2.329518 3.17886009]\n", - "Optimization history: \n", - "Inputs = [[2.14941171 1.2022835 ]\n", - " [2.89591634 3.04421182]\n", - " [1.13129495 3.27983113]\n", - " [2.50882074 2.47459348]\n", - " [1.34917886 1.15978118]\n", - " [1.99449618 0.14570364]\n", - " [1.68323092 3.96900812]\n", - " [0.26740239 1.98981025]\n", - " [0.823056 0.52179224]\n", - " [0.34794318 2.23644392]\n", - " [2.3323044 3.09159185]\n", - " [2.32962545 3.17922313]\n", - " [2.329518 3.17886009]]\n", - "Outputs = [[-3.35169521e+00 -1.00398765e+00 -2.62111904e+00]\n", - " [-5.94012816e+00 -1.24186360e+01 2.88844914e+00]\n", - " [-4.41112609e+00 -6.51809729e-01 3.03904161e+00]\n", - " [-4.98341421e+00 -2.78451535e+00 2.77667993e-01]\n", - " [-2.50896004e+00 -2.38224715e+00 -1.69313518e-01]\n", - " [-2.14019982e+00 -1.85453736e+00 -3.85405403e+00]\n", - " [-5.65223904e+00 1.40041321e+00 7.31474755e-01]\n", - " [-2.25721264e+00 -4.39484898e-01 -1.40405159e+01]\n", - " [-1.34484824e+00 -3.35493157e+00 -7.17152437e-02]\n", - " [-2.58438710e+00 -4.24396527e-01 -9.72535570e+00]\n", - " [-5.42389625e+00 -1.09766687e-01 -7.37742400e-02]\n", - " [-5.50884858e+00 -1.29446921e-04 1.22477349e-03]\n", - " [-5.50837809e+00 3.84933482e-04 3.56699512e-04]]\n" - ] - } - ], - "source": [ - "res = egor.minimize(n_iter=30)\n", - "print(f\"Optimization f={res.y_opt} at {res.x_opt}\")\n", - "print(\"Optimization history: \")\n", - "print(f\"Inputs = {res.x_hist}\")\n", - "print(f\"Outputs = {res.y_hist}\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "fb8ddd3d", - "metadata": {}, - "source": [ - "## Example 2 : Mixed-integer optimization" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "d46259b3", - "metadata": {}, - "source": [ - "### Test function" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "6948efc1", - "metadata": {}, - "outputs": [], - "source": [ - "xspecs_mixint_xsinx = [egx.XSpec(egx.XType.INT, [0, 25])]\n", - "n_cstr_mixint_xsinx = 0\n", - "\n", - "def mixint_xsinx(x: np.ndarray) -> np.ndarray:\n", - " x = np.atleast_2d(x)\n", - " if (np.abs(np.linalg.norm(np.floor(x))-np.linalg.norm(x))< 1e-8):\n", - " y = (x - 3.5) * np.sin((x - 3.5) / (np.pi))\n", - " else:\n", - " raise ValueError(f\"Bad input: mixint_xsinx accepts integer only, got {x}\")\n", - " return y" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "67faa229", - "metadata": {}, - "source": [ - "### Mixed-integer optimization with _Egor_" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "928d1f38", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Optimization f=[-15.12161154] at [19.]\n", - "Optimization history: \n", - "Inputs = [[ 7.]\n", - " [24.]\n", - " [11.]\n", - " [ 6.]\n", - " [ 5.]\n", - " [ 4.]\n", - " [17.]\n", - " [18.]\n", - " [19.]]\n", - "Outputs = [[ 3.14127616]\n", - " [ 4.91604976]\n", - " [ 5.1356682 ]\n", - " [ 1.78601478]\n", - " [ 0.68929352]\n", - " [ 0.07924194]\n", - " [-12.35295142]\n", - " [-14.43198471]\n", - " [-15.12161154]]\n" - ] - } - ], - "source": [ - "egor = egx.Egor(mixint_xsinx, xspecs_mixint_xsinx, \n", - " n_doe=3, \n", - " infill_strategy=egx.InfillStrategy.EI,\n", - " target=-15.12,\n", - " ) # see help(egor) for options\n", - "res = egor.minimize(n_iter=30)\n", - "print(f\"Optimization f={res.y_opt} at {res.x_opt}\")\n", - "print(\"Optimization history: \")\n", - "print(f\"Inputs = {res.x_hist}\")\n", - "print(f\"Outputs = {res.y_hist}\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "b9747211", - "metadata": {}, - "source": [ - "## Example 3 : More mixed-integer optimization" - ] - }, - { - "cell_type": "markdown", - "id": "0fe3a862", - "metadata": {}, - "source": [ - "In the following example we see we can have other special integer type cases, where a component of x can take one value out of a list of ordered values (ORD type) or being like an enum value (ENUM type). Those types differ by the processing related to the continuous relaxation made behind the scene:\n", - "* For INT type, resulting float is rounded to the closest int value,\n", - "* For ORD type, resulting float is cast to closest value among the given valid ones,\n", - "* For ENUM type, one hot encoding is performed to give the resulting value. " - ] - }, - { - "cell_type": "markdown", - "id": "9c7d3511", - "metadata": {}, - "source": [ - "### Test function" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "f1615d5c", - "metadata": {}, - "outputs": [], - "source": [ - "# Objective function which takes [FLOAT, ENUM1, ENUM2, ORD] as input\n", - "# Note that ENUM values are passed as indice value eg either 0, 1 or 2 for a 3-sized enum \n", - "def mixobj(X):\n", - " # float\n", - " x1 = X[:, 0]\n", - " # ENUM 1\n", - " c1 = X[:, 1]\n", - " x2 = c1 == 0\n", - " x3 = c1 == 1\n", - " x4 = c1 == 2\n", - " # ENUM 2\n", - " c2 = X[:, 2]\n", - " x5 = c2 == 0\n", - " x6 = c2 == 1\n", - " # int\n", - " i = X[:, 3]\n", - "\n", - " y = (x2 + 2 * x3 + 3 * x4) * x5 * x1 + (x2 + 2 * x3 + 3 * x4) * x6 * 0.95 * x1 + i\n", - " return y.reshape(-1, 1)" - ] - }, - { - "cell_type": "markdown", - "id": "fa3c4223", - "metadata": {}, - "source": [ - "### Mixed-integer optimization with _Egor_" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "d14fff89", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Optimization f=[-14.25] at [-5. 2. 1. 0.]\n", - "Optimization history: \n", - "Inputs = [[-1.90197486 2. 1. 3. ]\n", - " [ 1.36933896 1. 0. 2. ]\n", - " [-0.10843099 1. 0. 0. ]\n", - " [-4.73477511 0. 0. 3. ]\n", - " [ 3.11266243 2. 1. 2. ]\n", - " [ 0.33069418 2. 1. 0. ]\n", - " [ 4.47594664 2. 1. 0. ]\n", - " [-3.26619512 0. 0. 2. ]\n", - " [-5. 2. 1. 2. ]\n", - " [-5. 2. 1. 0. ]\n", - " [-5. 2. 1. 0. ]\n", - " [-5. 2. 1. 0. ]\n", - " [-5. 2. 1. 0. ]\n", - " [-5. 1. 0. 0. ]\n", - " [-5. 2. 1. 0. ]\n", - " [-5. 2. 1. 0. ]\n", - " [-5. 2. 1. 0. ]\n", - " [-5. 2. 1. 0. ]]\n", - "Outputs = [[ -2.42062836]\n", - " [ 4.73867792]\n", - " [ -0.21686197]\n", - " [ -1.73477511]\n", - " [ 10.87108792]\n", - " [ 0.9424784 ]\n", - " [ 12.75644793]\n", - " [ -1.26619512]\n", - " [-12.25 ]\n", - " [-14.25 ]\n", - " [-14.25 ]\n", - " [-14.25 ]\n", - " [-14.25 ]\n", - " [-10. ]\n", - " [-14.25 ]\n", - " [-14.25 ]\n", - " [-14.25 ]\n", - " [-14.25 ]]\n" - ] - } - ], - "source": [ - "xtypes = [\n", - " egx.XSpec(egx.XType.FLOAT, [-5.0, 5.0]),\n", - " egx.XSpec(egx.XType.ENUM, tags=[\"blue\", \"red\", \"green\"]),\n", - " egx.XSpec(egx.XType.ENUM, xlimits=[2]),\n", - " egx.XSpec(egx.XType.ORD, [0, 2, 3]),\n", - "]\n", - "egor = egx.Egor(mixobj, xtypes, seed=42)\n", - "res = egor.minimize(n_iter=10)\n", - "print(f\"Optimization f={res.y_opt} at {res.x_opt}\")\n", - "print(\"Optimization history: \")\n", - "print(f\"Inputs = {res.x_hist}\")\n", - "print(f\"Outputs = {res.y_hist}\")" - ] - }, - { - "cell_type": "markdown", - "id": "705bf10d", - "metadata": {}, - "source": [ - "Note that `x_opt` result contains indices for corresponding optional tags list hence the second component should be read as 0=\"red\", 1=\"green\", 2=\"blue\", while the third component was unamed 0 correspond to first enum value and 1 to the second one." - ] - }, - { - "cell_type": "markdown", - "id": "3b9fadfa", - "metadata": {}, - "source": [ - "## Usage" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "b91f14f2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Help on Egor in module builtins object:\n", - "\n", - "class Egor(object)\n", - " | Egor(fun, xspecs, n_cstr=0, cstr_tol=None, n_start=20, n_doe=0, doe=None, regr_spec=Ellipsis, corr_spec=Ellipsis, infill_strategy=Ellipsis, q_points=1, par_infill_strategy=Ellipsis, infill_optimizer=Ellipsis, kpls_dim=None, n_clusters=1, target=Ellipsis, outdir=None, hot_start=False, seed=None)\n", - " | \n", - " | Optimizer constructor\n", - " | \n", - " | fun: array[n, nx]) -> array[n, ny]\n", - " | the function to be minimized\n", - " | fun(x) = [obj(x), cstr_1(x), ... cstr_k(x)] where\n", - " | obj is the objective function [n, nx] -> [n, 1]\n", - " | cstr_i is the ith constraint function [n, nx] -> [n, 1]\n", - " | an k the number of constraints (n_cstr)\n", - " | hence ny = 1 (obj) + k (cstrs)\n", - " | cstr functions are expected be negative (<=0) at the optimum.\n", - " | \n", - " | n_cstr (int):\n", - " | the number of constraint functions.\n", - " | \n", - " | cstr_tol (list(n_cstr,)):\n", - " | List of tolerances for constraints to be satisfied (cstr < tol), list size should be equal to n_cstr.\n", - " | None by default means zero tolerances.\n", - " | \n", - " | xspecs (list(XSpec)) where XSpec(xtype=FLOAT|INT|ORD|ENUM, xlimits=[] or tags=[strings]):\n", - " | Specifications of the nx components of the input x (eg. len(xspecs) == nx)\n", - " | Depending on the x type we get the following for xlimits:\n", - " | * when FLOAT: xlimits is [float lower_bound, float upper_bound],\n", - " | * when INT: xlimits is [int lower_bound, int upper_bound],\n", - " | * when ORD: xlimits is [float_1, float_2, ..., float_n],\n", - " | * when ENUM: xlimits is just the int size of the enumeration otherwise a list of tags is specified\n", - " | (eg xlimits=[3] or tags=[\"red\", \"green\", \"blue\"], tags are there for documention purpose but\n", - " | tags specific values themselves are not used only indices in the enum are used hence\n", - " | we can just specify the size of the enum, xlimits=[3]),\n", - " | \n", - " | n_start (int > 0):\n", - " | Number of runs of infill strategy optimizations (best result taken)\n", - " | \n", - " | n_doe (int >= 0):\n", - " | Number of samples of initial LHS sampling (used when DOE not provided by the user).\n", - " | When 0 a number of points is computed automatically regarding the number of input variables\n", - " | of the function under optimization.\n", - " | \n", - " | doe (array[ns, nt]):\n", - " | Initial DOE containing ns samples:\n", - " | either nt = nx then only x are specified and ns evals are done to get y doe values,\n", - " | or nt = nx + ny then x = doe[:, :nx] and y = doe[:, nx:] are specified \n", - " | \n", - " | regr_spec (RegressionSpec flags, an int in [1, 7]):\n", - " | Specification of regression models used in gaussian processes.\n", - " | Can be RegressionSpec.CONSTANT (1), RegressionSpec.LINEAR (2), RegressionSpec.QUADRATIC (4) or\n", - " | any bit-wise union of these values (e.g. RegressionSpec.CONSTANT | RegressionSpec.LINEAR)\n", - " | \n", - " | corr_spec (CorrelationSpec flags, an int in [1, 15]):\n", - " | Specification of correlation models used in gaussian processes.\n", - " | Can be CorrelationSpec.SQUARED_EXPONENTIAL (1), CorrelationSpec.ABSOLUTE_EXPONENTIAL (2),\n", - " | CorrelationSpec.MATERN32 (4), CorrelationSpec.MATERN52 (8) or\n", - " | any bit-wise union of these values (e.g. CorrelationSpec.MATERN32 | CorrelationSpec.MATERN52)\n", - " | \n", - " | infill_strategy (InfillStrategy enum)\n", - " | Infill criteria to decide best next promising point.\n", - " | Can be either InfillStrategy.EI, InfillStrategy.WB2 or InfillStrategy.WB2S.\n", - " | \n", - " | q_points (int > 0):\n", - " | Number of points to be evaluated to allow parallel evaluation of the function under optimization.\n", - " | \n", - " | par_infill_strategy (ParInfillStrategy enum)\n", - " | Parallel infill criteria (aka qEI) to get virtual next promising points in order to allow\n", - " | q parallel evaluations of the function under optimization.\n", - " | Can be either ParInfillStrategy.KB (Kriging Believer),\n", - " | ParInfillStrategy.KBLB (KB Lower Bound), ParInfillStrategy.KBUB (KB Upper Bound),\n", - " | ParInfillStrategy.CLMIN (Constant Liar Minimum)\n", - " | \n", - " | infill_optimizer (InfillOptimizer enum)\n", - " | Internal optimizer used to optimize infill criteria.\n", - " | Can be either InfillOptimizer.COBYLA or InfillOptimizer.SLSQP\n", - " | \n", - " | kpls_dim (0 < int < nx)\n", - " | Number of components to be used when PLS projection is used (a.k.a KPLS method).\n", - " | This is used to address high-dimensional problems typically when nx > 9.\n", - " | \n", - " | n_clusters (int >= 0)\n", - " | Number of clusters used by the mixture of surrogate experts.\n", - " | When set to 0, the number of cluster is determined automatically and refreshed every\n", - " | 10-points addition (should say 'tentative addition' because addition may fail for some points\n", - " | but it is counted anyway).\n", - " | \n", - " | target (float)\n", - " | Known optimum used as stopping criterion.\n", - " | \n", - " | outdir (String)\n", - " | Directory to write optimization history and used as search path for hot start doe\n", - " | \n", - " | hot_start (bool)\n", - " | Start by loading initial doe from directory\n", - " | \n", - " | seed (int >= 0)\n", - " | Random generator seed to allow computation reproducibility.\n", - " | \n", - " | Methods defined here:\n", - " | \n", - " | minimize(self, /, n_iter=20)\n", - " | This function finds the minimum of a given function `fun`\n", - " | \n", - " | # Parameters\n", - " | n_iter:\n", - " | the iteration budget, number of fun calls is n_doe + q_points * n_iter.\n", - " | \n", - " | # Returns\n", - " | optimization result\n", - " | x_opt (array[1, nx]): x value where fun is at its minimum subject to constraint\n", - " | y_opt (array[1, nx]): fun(x_opt)\n", - " | \n", - " | ----------------------------------------------------------------------\n", - " | Static methods defined here:\n", - " | \n", - " | __new__(*args, **kwargs) from builtins.type\n", - " | Create and return a new object. See help(type) for accurate signature.\n", - "\n" - ] - } - ], - "source": [ - "help(egor)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.5" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "72380b9b", + "metadata": {}, + "source": [ + "# Using _egobox_ optimizer _Egor_" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "31022791", + "metadata": {}, + "source": [ + "## Installation" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "1504e619-5775-42d3-8f48-7339272303ec", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: egobox in d:\\rlafage\\miniconda3\\lib\\site-packages (0.12.0)\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "%pip install egobox" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "4c2757f5", + "metadata": {}, + "source": [ + "We import _egobox_ as _egx_ for short." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "0edaf00f", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import egobox as egx" + ] + }, + { + "cell_type": "markdown", + "id": "6c8c8c84", + "metadata": {}, + "source": [ + "You may setup the logging level to get optimization progress during the execution" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "af2d82be", + "metadata": {}, + "outputs": [], + "source": [ + "# To display optimization information (none by default)\n", + "# import logging\n", + "# logging.basicConfig(level=logging.INFO)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "22997c39", + "metadata": {}, + "source": [ + "## Example 1 : Continuous optimization" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "faae2555", + "metadata": {}, + "source": [ + "### Test functions" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7f6da807", + "metadata": {}, + "outputs": [], + "source": [ + "xspecs_xsinx = egx.to_specs([[0., 25.]])\n", + "n_cstr_xsinx = 0\n", + "\n", + "def xsinx(x: np.ndarray) -> np.ndarray:\n", + " x = np.atleast_2d(x)\n", + " y = (x - 3.5) * np.sin((x - 3.5) / (np.pi))\n", + " return y" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "4c436437", + "metadata": {}, + "outputs": [], + "source": [ + "xspecs_g24 = egx.to_specs([[0., 3.], [0., 4.]])\n", + "n_cstr_g24 = 2\n", + "\n", + "# Objective\n", + "def G24(point):\n", + " \"\"\"\n", + " Function g24\n", + " 1 global optimum y_opt = -5.5080 at x_opt =(2.3295, 3.1785)\n", + " \"\"\"\n", + " p = np.atleast_2d(point)\n", + " return - p[:, 0] - p[:, 1]\n", + "\n", + "# Constraints < 0\n", + "def G24_c1(point):\n", + " p = np.atleast_2d(point)\n", + " return (- 2.0 * p[:, 0] ** 4.0\n", + " + 8.0 * p[:, 0] ** 3.0 \n", + " - 8.0 * p[:, 0] ** 2.0 \n", + " + p[:, 1] - 2.0)\n", + "\n", + "def G24_c2(point):\n", + " p = np.atleast_2d(point)\n", + " return (-4.0 * p[:, 0] ** 4.0\n", + " + 32.0 * p[:, 0] ** 3.0\n", + " - 88.0 * p[:, 0] ** 2.0\n", + " + 96.0 * p[:, 0]\n", + " + p[:, 1] - 36.0)\n", + "\n", + "# Grouped evaluation\n", + "def g24(point):\n", + " p = np.atleast_2d(point)\n", + " return np.array([G24(p), G24_c1(p), G24_c2(p)]).T\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "45641636", + "metadata": {}, + "source": [ + "### Continuous optimization with _Egor_" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "c8942031", + "metadata": {}, + "outputs": [], + "source": [ + "egor = egx.Egor(g24, xspecs_g24, \n", + " n_doe=10, \n", + " n_cstr=n_cstr_g24, \n", + " cstr_tol=[1e-3, 1e-3],\n", + " infill_strategy=egx.InfillStrategy.WB2,\n", + " target=-5.5,\n", + " # outdir=\"./out\",\n", + " # hot_start=True\n", + " ) # see help(egor) for options\n", + "\n", + "# Specify regression and/or correlation models used to build the surrogates of objective and constraints\n", + "#egor = egx.Egor(g24, xlimits_g24, n_cstr=n_cstr_g24, n_doe=10,\n", + "# regr_spec=egx.RegressionSpec.LINEAR,\n", + "# corr_spec=egx.CorrelationSpec.MATERN32 | egx.CorrelationSpec.MATERN52) " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "c12b8e9d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Optimization f=[-5.50837809e+00 3.84933482e-04 3.56699512e-04] at [2.329518 3.17886009]\n", + "Optimization history: \n", + "Inputs = [[2.14941171 1.2022835 ]\n", + " [2.89591634 3.04421182]\n", + " [1.13129495 3.27983113]\n", + " [2.50882074 2.47459348]\n", + " [1.34917886 1.15978118]\n", + " [1.99449618 0.14570364]\n", + " [1.68323092 3.96900812]\n", + " [0.26740239 1.98981025]\n", + " [0.823056 0.52179224]\n", + " [0.34794318 2.23644392]\n", + " [2.3323044 3.09159185]\n", + " [2.32962545 3.17922313]\n", + " [2.329518 3.17886009]]\n", + "Outputs = [[-3.35169521e+00 -1.00398765e+00 -2.62111904e+00]\n", + " [-5.94012816e+00 -1.24186360e+01 2.88844914e+00]\n", + " [-4.41112609e+00 -6.51809729e-01 3.03904161e+00]\n", + " [-4.98341421e+00 -2.78451535e+00 2.77667993e-01]\n", + " [-2.50896004e+00 -2.38224715e+00 -1.69313518e-01]\n", + " [-2.14019982e+00 -1.85453736e+00 -3.85405403e+00]\n", + " [-5.65223904e+00 1.40041321e+00 7.31474755e-01]\n", + " [-2.25721264e+00 -4.39484898e-01 -1.40405159e+01]\n", + " [-1.34484824e+00 -3.35493157e+00 -7.17152437e-02]\n", + " [-2.58438710e+00 -4.24396527e-01 -9.72535570e+00]\n", + " [-5.42389625e+00 -1.09766687e-01 -7.37742400e-02]\n", + " [-5.50884858e+00 -1.29446921e-04 1.22477349e-03]\n", + " [-5.50837809e+00 3.84933482e-04 3.56699512e-04]]\n" + ] + } + ], + "source": [ + "res = egor.minimize(max_iters=30)\n", + "print(f\"Optimization f={res.y_opt} at {res.x_opt}\")\n", + "print(\"Optimization history: \")\n", + "print(f\"Inputs = {res.x_hist}\")\n", + "print(f\"Outputs = {res.y_hist}\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "fb8ddd3d", + "metadata": {}, + "source": [ + "## Example 2 : Mixed-integer optimization" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "d46259b3", + "metadata": {}, + "source": [ + "### Test function" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "6948efc1", + "metadata": {}, + "outputs": [], + "source": [ + "xspecs_mixint_xsinx = [egx.XSpec(egx.XType.INT, [0, 25])]\n", + "n_cstr_mixint_xsinx = 0\n", + "\n", + "def mixint_xsinx(x: np.ndarray) -> np.ndarray:\n", + " x = np.atleast_2d(x)\n", + " if (np.abs(np.linalg.norm(np.floor(x))-np.linalg.norm(x))< 1e-8):\n", + " y = (x - 3.5) * np.sin((x - 3.5) / (np.pi))\n", + " else:\n", + " raise ValueError(f\"Bad input: mixint_xsinx accepts integer only, got {x}\")\n", + " return y" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "67faa229", + "metadata": {}, + "source": [ + "### Mixed-integer optimization with _Egor_" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "928d1f38", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Optimization f=[-15.12161154] at [19.]\n", + "Optimization history: \n", + "Inputs = [[ 7.]\n", + " [24.]\n", + " [11.]\n", + " [ 6.]\n", + " [ 5.]\n", + " [ 4.]\n", + " [17.]\n", + " [18.]\n", + " [19.]]\n", + "Outputs = [[ 3.14127616]\n", + " [ 4.91604976]\n", + " [ 5.1356682 ]\n", + " [ 1.78601478]\n", + " [ 0.68929352]\n", + " [ 0.07924194]\n", + " [-12.35295142]\n", + " [-14.43198471]\n", + " [-15.12161154]]\n" + ] + } + ], + "source": [ + "egor = egx.Egor(mixint_xsinx, xspecs_mixint_xsinx, \n", + " n_doe=3, \n", + " infill_strategy=egx.InfillStrategy.EI,\n", + " target=-15.12,\n", + " ) # see help(egor) for options\n", + "res = egor.minimize(max_iters=30)\n", + "print(f\"Optimization f={res.y_opt} at {res.x_opt}\")\n", + "print(\"Optimization history: \")\n", + "print(f\"Inputs = {res.x_hist}\")\n", + "print(f\"Outputs = {res.y_hist}\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "b9747211", + "metadata": {}, + "source": [ + "## Example 3 : More mixed-integer optimization" + ] + }, + { + "cell_type": "markdown", + "id": "0fe3a862", + "metadata": {}, + "source": [ + "In the following example we see we can have other special integer type cases, where a component of x can take one value out of a list of ordered values (ORD type) or being like an enum value (ENUM type). Those types differ by the processing related to the continuous relaxation made behind the scene:\n", + "* For INT type, resulting float is rounded to the closest int value,\n", + "* For ORD type, resulting float is cast to closest value among the given valid ones,\n", + "* For ENUM type, one hot encoding is performed to give the resulting value. " + ] + }, + { + "cell_type": "markdown", + "id": "9c7d3511", + "metadata": {}, + "source": [ + "### Test function" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "f1615d5c", + "metadata": {}, + "outputs": [], + "source": [ + "# Objective function which takes [FLOAT, ENUM1, ENUM2, ORD] as input\n", + "# Note that ENUM values are passed as indice value eg either 0, 1 or 2 for a 3-sized enum \n", + "def mixobj(X):\n", + " # float\n", + " x1 = X[:, 0]\n", + " # ENUM 1\n", + " c1 = X[:, 1]\n", + " x2 = c1 == 0\n", + " x3 = c1 == 1\n", + " x4 = c1 == 2\n", + " # ENUM 2\n", + " c2 = X[:, 2]\n", + " x5 = c2 == 0\n", + " x6 = c2 == 1\n", + " # int\n", + " i = X[:, 3]\n", + "\n", + " y = (x2 + 2 * x3 + 3 * x4) * x5 * x1 + (x2 + 2 * x3 + 3 * x4) * x6 * 0.95 * x1 + i\n", + " return y.reshape(-1, 1)" + ] + }, + { + "cell_type": "markdown", + "id": "fa3c4223", + "metadata": {}, + "source": [ + "### Mixed-integer optimization with _Egor_" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "d14fff89", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Optimization f=[-14.25] at [-5. 2. 1. 0.]\n", + "Optimization history: \n", + "Inputs = [[-1.90197486 2. 1. 3. ]\n", + " [ 1.36933896 1. 0. 2. ]\n", + " [-0.10843099 1. 0. 0. ]\n", + " [-4.73477511 0. 0. 3. ]\n", + " [ 3.11266243 2. 1. 2. ]\n", + " [ 0.33069418 2. 1. 0. ]\n", + " [ 4.47594664 2. 1. 0. ]\n", + " [-3.26619512 0. 0. 2. ]\n", + " [-5. 2. 1. 2. ]\n", + " [-5. 2. 1. 0. ]\n", + " [-5. 2. 1. 0. ]\n", + " [-5. 2. 1. 0. ]\n", + " [-5. 2. 1. 0. ]\n", + " [-5. 1. 0. 0. ]\n", + " [-5. 2. 1. 0. ]\n", + " [-5. 2. 1. 0. ]\n", + " [-5. 2. 1. 0. ]\n", + " [-5. 2. 1. 0. ]]\n", + "Outputs = [[ -2.42062836]\n", + " [ 4.73867792]\n", + " [ -0.21686197]\n", + " [ -1.73477511]\n", + " [ 10.87108792]\n", + " [ 0.9424784 ]\n", + " [ 12.75644793]\n", + " [ -1.26619512]\n", + " [-12.25 ]\n", + " [-14.25 ]\n", + " [-14.25 ]\n", + " [-14.25 ]\n", + " [-14.25 ]\n", + " [-10. ]\n", + " [-14.25 ]\n", + " [-14.25 ]\n", + " [-14.25 ]\n", + " [-14.25 ]]\n" + ] + } + ], + "source": [ + "xtypes = [\n", + " egx.XSpec(egx.XType.FLOAT, [-5.0, 5.0]),\n", + " egx.XSpec(egx.XType.ENUM, tags=[\"blue\", \"red\", \"green\"]),\n", + " egx.XSpec(egx.XType.ENUM, xlimits=[2]),\n", + " egx.XSpec(egx.XType.ORD, [0, 2, 3]),\n", + "]\n", + "egor = egx.Egor(mixobj, xtypes, seed=42)\n", + "res = egor.minimize(max_iters=10)\n", + "print(f\"Optimization f={res.y_opt} at {res.x_opt}\")\n", + "print(\"Optimization history: \")\n", + "print(f\"Inputs = {res.x_hist}\")\n", + "print(f\"Outputs = {res.y_hist}\")" + ] + }, + { + "cell_type": "markdown", + "id": "705bf10d", + "metadata": {}, + "source": [ + "Note that `x_opt` result contains indices for corresponding optional tags list hence the second component should be read as 0=\"red\", 1=\"green\", 2=\"blue\", while the third component was unamed 0 correspond to first enum value and 1 to the second one." + ] + }, + { + "cell_type": "markdown", + "id": "3b9fadfa", + "metadata": {}, + "source": [ + "## Usage" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "b91f14f2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on Egor in module builtins object:\n", + "\n", + "class Egor(object)\n", + " | Egor(fun, xspecs, n_cstr=0, cstr_tol=None, n_start=20, n_doe=0, doe=None, regr_spec=Ellipsis, corr_spec=Ellipsis, infill_strategy=Ellipsis, q_points=1, par_infill_strategy=Ellipsis, infill_optimizer=Ellipsis, kpls_dim=None, n_clusters=1, target=Ellipsis, outdir=None, hot_start=False, seed=None)\n", + " | \n", + " | Optimizer constructor\n", + " | \n", + " | fun: array[n, nx]) -> array[n, ny]\n", + " | the function to be minimized\n", + " | fun(x) = [obj(x), cstr_1(x), ... cstr_k(x)] where\n", + " | obj is the objective function [n, nx] -> [n, 1]\n", + " | cstr_i is the ith constraint function [n, nx] -> [n, 1]\n", + " | an k the number of constraints (n_cstr)\n", + " | hence ny = 1 (obj) + k (cstrs)\n", + " | cstr functions are expected be negative (<=0) at the optimum.\n", + " | \n", + " | n_cstr (int):\n", + " | the number of constraint functions.\n", + " | \n", + " | cstr_tol (list(n_cstr,)):\n", + " | List of tolerances for constraints to be satisfied (cstr < tol), list size should be equal to n_cstr.\n", + " | None by default means zero tolerances.\n", + " | \n", + " | xspecs (list(XSpec)) where XSpec(xtype=FLOAT|INT|ORD|ENUM, xlimits=[] or tags=[strings]):\n", + " | Specifications of the nx components of the input x (eg. len(xspecs) == nx)\n", + " | Depending on the x type we get the following for xlimits:\n", + " | * when FLOAT: xlimits is [float lower_bound, float upper_bound],\n", + " | * when INT: xlimits is [int lower_bound, int upper_bound],\n", + " | * when ORD: xlimits is [float_1, float_2, ..., float_n],\n", + " | * when ENUM: xlimits is just the int size of the enumeration otherwise a list of tags is specified\n", + " | (eg xlimits=[3] or tags=[\"red\", \"green\", \"blue\"], tags are there for documention purpose but\n", + " | tags specific values themselves are not used only indices in the enum are used hence\n", + " | we can just specify the size of the enum, xlimits=[3]),\n", + " | \n", + " | n_start (int > 0):\n", + " | Number of runs of infill strategy optimizations (best result taken)\n", + " | \n", + " | n_doe (int >= 0):\n", + " | Number of samples of initial LHS sampling (used when DOE not provided by the user).\n", + " | When 0 a number of points is computed automatically regarding the number of input variables\n", + " | of the function under optimization.\n", + " | \n", + " | doe (array[ns, nt]):\n", + " | Initial DOE containing ns samples:\n", + " | either nt = nx then only x are specified and ns evals are done to get y doe values,\n", + " | or nt = nx + ny then x = doe[:, :nx] and y = doe[:, nx:] are specified \n", + " | \n", + " | regr_spec (RegressionSpec flags, an int in [1, 7]):\n", + " | Specification of regression models used in gaussian processes.\n", + " | Can be RegressionSpec.CONSTANT (1), RegressionSpec.LINEAR (2), RegressionSpec.QUADRATIC (4) or\n", + " | any bit-wise union of these values (e.g. RegressionSpec.CONSTANT | RegressionSpec.LINEAR)\n", + " | \n", + " | corr_spec (CorrelationSpec flags, an int in [1, 15]):\n", + " | Specification of correlation models used in gaussian processes.\n", + " | Can be CorrelationSpec.SQUARED_EXPONENTIAL (1), CorrelationSpec.ABSOLUTE_EXPONENTIAL (2),\n", + " | CorrelationSpec.MATERN32 (4), CorrelationSpec.MATERN52 (8) or\n", + " | any bit-wise union of these values (e.g. CorrelationSpec.MATERN32 | CorrelationSpec.MATERN52)\n", + " | \n", + " | infill_strategy (InfillStrategy enum)\n", + " | Infill criteria to decide best next promising point.\n", + " | Can be either InfillStrategy.EI, InfillStrategy.WB2 or InfillStrategy.WB2S.\n", + " | \n", + " | q_points (int > 0):\n", + " | Number of points to be evaluated to allow parallel evaluation of the function under optimization.\n", + " | \n", + " | par_infill_strategy (ParInfillStrategy enum)\n", + " | Parallel infill criteria (aka qEI) to get virtual next promising points in order to allow\n", + " | q parallel evaluations of the function under optimization.\n", + " | Can be either ParInfillStrategy.KB (Kriging Believer),\n", + " | ParInfillStrategy.KBLB (KB Lower Bound), ParInfillStrategy.KBUB (KB Upper Bound),\n", + " | ParInfillStrategy.CLMIN (Constant Liar Minimum)\n", + " | \n", + " | infill_optimizer (InfillOptimizer enum)\n", + " | Internal optimizer used to optimize infill criteria.\n", + " | Can be either InfillOptimizer.COBYLA or InfillOptimizer.SLSQP\n", + " | \n", + " | kpls_dim (0 < int < nx)\n", + " | Number of components to be used when PLS projection is used (a.k.a KPLS method).\n", + " | This is used to address high-dimensional problems typically when nx > 9.\n", + " | \n", + " | n_clusters (int >= 0)\n", + " | Number of clusters used by the mixture of surrogate experts.\n", + " | When set to 0, the number of cluster is determined automatically and refreshed every\n", + " | 10-points addition (should say 'tentative addition' because addition may fail for some points\n", + " | but it is counted anyway).\n", + " | \n", + " | target (float)\n", + " | Known optimum used as stopping criterion.\n", + " | \n", + " | outdir (String)\n", + " | Directory to write optimization history and used as search path for hot start doe\n", + " | \n", + " | hot_start (bool)\n", + " | Start by loading initial doe from directory\n", + " | \n", + " | seed (int >= 0)\n", + " | Random generator seed to allow computation reproducibility.\n", + " | \n", + " | Methods defined here:\n", + " | \n", + " | minimize(self, /, max_iters=20)\n", + " | This function finds the minimum of a given function `fun`\n", + " | \n", + " | # Parameters\n", + " | max_iters:\n", + " | the iteration budget, number of fun calls is n_doe + q_points * max_iters.\n", + " | \n", + " | # Returns\n", + " | optimization result\n", + " | x_opt (array[1, nx]): x value where fun is at its minimum subject to constraint\n", + " | y_opt (array[1, nx]): fun(x_opt)\n", + " | \n", + " | ----------------------------------------------------------------------\n", + " | Static methods defined here:\n", + " | \n", + " | __new__(*args, **kwargs) from builtins.type\n", + " | Create and return a new object. See help(type) for accurate signature.\n", + "\n" + ] + } + ], + "source": [ + "help(egor)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/python/egobox/examples/optim_g24.py b/python/egobox/examples/optim_g24.py index 114c4898..d1c285b3 100644 --- a/python/egobox/examples/optim_g24.py +++ b/python/egobox/examples/optim_g24.py @@ -66,5 +66,5 @@ def g24(point): # regr_spec=egx.RegressionSpec.LINEAR, # corr_spec=egx.CorrelationSpec.MATERN32 | egx.CorrelationSpec.MATERN52) -res = egor.minimize(n_iter=30) +res = egor.minimize(max_iters=30) print(f"Optimization f={res.y_opt} at {res.x_opt}") diff --git a/python/egobox/tests/test_egor.py b/python/egobox/tests/test_egor.py index f3a94a8a..3ebee4b9 100644 --- a/python/egobox/tests/test_egor.py +++ b/python/egobox/tests/test_egor.py @@ -81,14 +81,14 @@ def six_humps(x): class TestOptimizer(unittest.TestCase): def test_xsinx(self): egor = egx.Egor(xsinx, egx.to_specs([[0.0, 25.0]]), seed=42) - res = egor.minimize(n_iter=20) + res = egor.minimize(max_iters=20) print(f"Optimization f={res.y_opt} at {res.x_opt}") self.assertAlmostEqual(-15.125, res.y_opt[0], delta=1e-3) self.assertAlmostEqual(18.935, res.x_opt[0], delta=1e-3) def test_xsinx_with_reclustering(self): egor = egx.Egor(xsinx, egx.to_specs([[0.0, 25.0]]), seed=42, n_clusters=0) - res = egor.minimize(n_iter=20) + res = egor.minimize(max_iters=20) print(f"Optimization f={res.y_opt} at {res.x_opt}") self.assertAlmostEqual(-15.125, res.y_opt[0], delta=1e-3) self.assertAlmostEqual(18.935, res.x_opt[0], delta=1e-3) @@ -101,13 +101,13 @@ def test_xsinx_with_hotstart(self): xlimits = egx.to_specs([[0.0, 25.0]]) doe = egx.lhs(xlimits, 10) egor = egx.Egor(xsinx, xlimits, doe=doe, seed=42, outdir="./test_dir") - res = egor.minimize(n_iter=15) + res = egor.minimize(max_iters=15) print(f"Optimization f={res.y_opt} at {res.x_opt}") self.assertAlmostEqual(-15.125, res.y_opt[0], delta=1e-3) self.assertAlmostEqual(18.935, res.x_opt[0], delta=1e-3) egor = egx.Egor(xsinx, xlimits, outdir="./test_dir", hot_start=True) - res = egor.minimize(n_iter=5) + res = egor.minimize(max_iters=5) print(f"Optimization f={res.y_opt} at {res.x_opt}") self.assertAlmostEqual(-15.125, res.y_opt[0], delta=1e-2) self.assertAlmostEqual(18.935, res.x_opt[0], delta=1e-2) @@ -119,7 +119,7 @@ def test_xsinx_with_hotstart(self): def test_g24(self): n_doe = 5 - n_iter = 20 + max_iters = 20 n_cstr = 2 egor = egx.Egor( g24, @@ -130,14 +130,14 @@ def test_g24(self): n_doe=n_doe, ) start = time.process_time() - res = egor.minimize(n_iter=n_iter) + res = egor.minimize(max_iters=max_iters) end = time.process_time() print(f"Optimization f={res.y_opt} at {res.x_opt} in {end-start}s") self.assertAlmostEqual(-5.5080, res.y_opt[0], delta=1e-2) self.assertAlmostEqual(2.3295, res.x_opt[0], delta=1e-2) self.assertAlmostEqual(3.1785, res.x_opt[1], delta=1e-2) - self.assertEqual((n_doe + n_iter, 2), res.x_hist.shape) - self.assertEqual((n_doe + n_iter, 1 + n_cstr), res.y_hist.shape) + self.assertEqual((n_doe + max_iters, 2), res.x_hist.shape) + self.assertEqual((n_doe + max_iters, 1 + n_cstr), res.y_hist.shape) def test_g24_kpls(self): egor = egx.Egor( @@ -151,7 +151,7 @@ def test_g24_kpls(self): seed=1, ) start = time.process_time() - res = egor.minimize(n_iter=20) + res = egor.minimize(max_iters=20) end = time.process_time() self.assertAlmostEqual(-5.5080, res.y_opt[0], delta=5e-1) print(f"Optimization f={res.y_opt} at {res.x_opt} in {end-start}s") @@ -164,7 +164,7 @@ def test_six_humps(self): seed=42, ) start = time.process_time() - res = egor.minimize(n_iter=45) + res = egor.minimize(max_iters=45) end = time.process_time() print(f"Optimization f={res.y_opt} at {res.x_opt} in {end-start}s") # 2 global optimum value =-1.0316 located at (0.089842, -0.712656) and (-0.089842, 0.712656) diff --git a/python/egobox/tests/test_mixintegor.py b/python/egobox/tests/test_mixintegor.py index 1f1dea40..9c2c28af 100644 --- a/python/egobox/tests/test_mixintegor.py +++ b/python/egobox/tests/test_mixintegor.py @@ -38,7 +38,7 @@ def test_int(self): xtypes = [egx.XSpec(egx.XType.INT, [0.0, 25.0])] egor = egx.Egor(xsinx, xtypes, seed=42, n_doe=3) - res = egor.minimize(n_iter=10) + res = egor.minimize(max_iters=10) print(f"Optimization f={res.y_opt} at {res.x_opt}") self.assertAlmostEqual(-15.125, res.y_opt[0], delta=5e-3) self.assertAlmostEqual(19, res.x_opt[0], delta=1) @@ -51,7 +51,7 @@ def test_ord_enum(self): egx.XSpec(egx.XType.ORD, [0, 2, 3]), ] egor = egx.Egor(mixobj, xtypes, seed=42) - res = egor.minimize(n_iter=10) + res = egor.minimize(max_iters=10) self.assertAlmostEqual(-14.25, res.y_opt[0]) self.assertAlmostEqual(-5, res.x_opt[0]) self.assertAlmostEqual(2, res.x_opt[1]) From 993e4a62bbe3f6d393bde4284d6229478f338b02 Mon Sep 17 00:00:00 2001 From: relf Date: Wed, 22 Nov 2023 17:41:44 +0100 Subject: [PATCH 09/13] Make Egor.suggest available in Python --- python/egobox/tests/test_egor.py | 12 +++ src/egor.rs | 125 ++++++++++++++++++++++++++++--- 2 files changed, 127 insertions(+), 10 deletions(-) diff --git a/python/egobox/tests/test_egor.py b/python/egobox/tests/test_egor.py index 3ebee4b9..5021939c 100644 --- a/python/egobox/tests/test_egor.py +++ b/python/egobox/tests/test_egor.py @@ -174,6 +174,18 @@ def test_constructor(self): self.assertRaises(TypeError, egx.Egor) egx.Egor(xsinx, egx.to_specs([[0.0, 25.0]]), 22, n_doe=10) + def test_egor_service(self): + xlimits = egx.to_specs([[0.0, 25.0]]) + egor = egx.Egor(xlimits, seed=42) + x_doe = egx.lhs(xlimits, 3, seed=42) + print(x_doe) + y_doe = xsinx(x_doe) + print(y_doe) + for _ in range(10): + x = egor.suggest(x_doe, y_doe) + x_doe = np.concatenate((x_doe, x)) + y_doe = np.concatenate((y_doe, xsinx(x))) + if __name__ == "__main__": unittest.main() diff --git a/src/egor.rs b/src/egor.rs index 97fcc7b5..81196626 100644 --- a/src/egor.rs +++ b/src/egor.rs @@ -11,7 +11,7 @@ //! use crate::types::*; -use ndarray::Array1; +use ndarray::{concatenate, Array1, Axis}; use numpy::ndarray::{Array2, ArrayView2}; use numpy::{IntoPyArray, PyArray1, PyArray2, PyReadonlyArray2}; use pyo3::exceptions::PyValueError; @@ -133,7 +133,6 @@ pub(crate) fn to_specs(py: Python, xlimits: Vec>) -> PyResult /// #[pyclass] pub(crate) struct Egor { - pub fun: PyObject, pub xspecs: PyObject, pub n_cstr: usize, pub cstr_tol: Option>, @@ -170,7 +169,6 @@ pub(crate) struct OptimResult { impl Egor { #[new] #[pyo3(signature = ( - fun, xspecs, n_cstr = 0, cstr_tol = None, @@ -192,8 +190,7 @@ impl Egor { ))] #[allow(clippy::too_many_arguments)] fn new( - py: Python, - fun: PyObject, + _py: Python, xspecs: PyObject, n_cstr: usize, cstr_tol: Option>, @@ -215,7 +212,6 @@ impl Egor { ) -> Self { let doe = doe.map(|x| x.to_owned_array()); Egor { - fun: fun.to_object(py), xspecs, n_cstr, cstr_tol, @@ -248,9 +244,9 @@ impl Egor { /// x_opt (array[1, nx]): x value where fun is at its minimum subject to constraint /// y_opt (array[1, nx]): fun(x_opt) /// - #[pyo3(signature = (max_iters = 20))] - fn minimize(&self, py: Python, max_iters: usize) -> PyResult { - let fun = self.fun.to_object(py); + #[pyo3(signature = (fun, max_iters = 20))] + fn minimize(&self, py: Python, fun: PyObject, max_iters: usize) -> PyResult { + let fun = fun.to_object(py); let obj = move |x: &ArrayView2| -> Array2 { Python::with_gil(|py| { let args = (x.to_owned().into_pyarray(py),); @@ -300,7 +296,6 @@ impl Egor { } }) .collect(); - println!("{:?}", xtypes); let cstr_tol = self.cstr_tol.clone().unwrap_or(vec![0.0; self.n_cstr]); let cstr_tol = Array1::from_vec(cstr_tol); @@ -360,4 +355,114 @@ impl Egor { y_hist, }) } + + /// This function gives the next best location where to evaluate the function + /// under optimization wrt to previous evaluations. + /// The function returns several point when multi point qEI strategy is used. + /// + /// # Parameters + /// x_doe (array[ns, nx]): ns samples where function has been evaluated + /// y_doe (array[ns, 1 + n_cstr]): ns values of objetcive and constraints + /// + /// + /// # Returns + /// (array[1, nx]): suggested location where to evaluate objective and constraints + /// + #[pyo3(signature = (x_doe, y_doe))] + fn suggest( + &self, + py: Python, + x_doe: PyReadonlyArray2, + y_doe: PyReadonlyArray2, + ) -> Py> { + let x_doe = x_doe.as_array(); + let y_doe = y_doe.as_array(); + + let doe = concatenate(Axis(1), &[x_doe.view(), y_doe.view()]).unwrap(); + + let infill_strategy = match self.infill_strategy { + InfillStrategy::Ei => egobox_ego::InfillStrategy::EI, + InfillStrategy::Wb2 => egobox_ego::InfillStrategy::WB2, + InfillStrategy::Wb2s => egobox_ego::InfillStrategy::WB2S, + }; + + let qei_strategy = match self.par_infill_strategy { + ParInfillStrategy::Kb => egobox_ego::QEiStrategy::KrigingBeliever, + ParInfillStrategy::Kblb => egobox_ego::QEiStrategy::KrigingBelieverLowerBound, + ParInfillStrategy::Kbub => egobox_ego::QEiStrategy::KrigingBelieverUpperBound, + ParInfillStrategy::Clmin => egobox_ego::QEiStrategy::ConstantLiarMinimum, + }; + + let infill_optimizer = match self.infill_optimizer { + InfillOptimizer::Cobyla => egobox_ego::InfillOptimizer::Cobyla, + InfillOptimizer::Slsqp => egobox_ego::InfillOptimizer::Slsqp, + }; + + let xspecs: Vec = self.xspecs.extract(py).expect("Error in xspecs conversion"); + if xspecs.is_empty() { + panic!("Error: xspecs argument cannot be empty") + } + + let xtypes: Vec = xspecs + .iter() + .map(|spec| match spec.xtype { + XType::Float => egobox_ego::XType::Cont(spec.xlimits[0], spec.xlimits[1]), + XType::Int => { + egobox_ego::XType::Int(spec.xlimits[0] as i32, spec.xlimits[1] as i32) + } + XType::Ord => egobox_ego::XType::Ord(spec.xlimits.clone()), + XType::Enum => { + if spec.tags.is_empty() { + egobox_ego::XType::Enum(spec.xlimits[0] as usize) + } else { + egobox_ego::XType::Enum(spec.tags.len()) + } + } + }) + .collect(); + + let cstr_tol = self.cstr_tol.clone().unwrap_or(vec![0.0; self.n_cstr]); + let cstr_tol = Array1::from_vec(cstr_tol); + + let mixintegor = egobox_ego::EgorServiceBuilder::optimize() + .configure(|config| { + let mut config = config + .n_cstr(self.n_cstr) + .n_start(self.n_start) + .doe(&doe) + .cstr_tol(&cstr_tol) + .regression_spec( + egobox_moe::RegressionSpec::from_bits(self.regression_spec.0).unwrap(), + ) + .correlation_spec( + egobox_moe::CorrelationSpec::from_bits(self.correlation_spec.0).unwrap(), + ) + .infill_strategy(infill_strategy) + .q_points(self.q_points) + .qei_strategy(qei_strategy) + .infill_optimizer(infill_optimizer) + .target(self.target) + .hot_start(false); // when used as a service no hotstart + if let Some(doe) = self.doe.as_ref() { + config = config.doe(doe); + }; + if let Some(kpls_dim) = self.kpls_dim { + config = config.kpls_dim(kpls_dim); + }; + if let Some(n_clusters) = self.n_clusters { + config = config.n_clusters(n_clusters); + }; + if let Some(outdir) = self.outdir.as_ref().cloned() { + config = config.outdir(outdir); + }; + if let Some(seed) = self.seed { + config = config.random_seed(seed); + }; + config + }) + .min_within_mixint_space(&xtypes); + + let x_suggested = py.allow_threads(|| mixintegor.suggest(&x_doe, &y_doe)); + x_suggested.into_pyarray(py).to_owned() + } } From fcfc96480e1a46e6d342e2e29054ab81c3d0101e Mon Sep 17 00:00:00 2001 From: relf Date: Thu, 23 Nov 2023 13:24:47 +0100 Subject: [PATCH 10/13] Adjust Python tests to new API --- python/egobox/tests/test_egor.py | 27 ++++++++++++-------------- python/egobox/tests/test_mixintegor.py | 8 ++++---- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/python/egobox/tests/test_egor.py b/python/egobox/tests/test_egor.py index 5021939c..7474d421 100644 --- a/python/egobox/tests/test_egor.py +++ b/python/egobox/tests/test_egor.py @@ -80,15 +80,15 @@ def six_humps(x): class TestOptimizer(unittest.TestCase): def test_xsinx(self): - egor = egx.Egor(xsinx, egx.to_specs([[0.0, 25.0]]), seed=42) - res = egor.minimize(max_iters=20) + egor = egx.Egor(egx.to_specs([[0.0, 25.0]]), seed=42) + res = egor.minimize(xsinx, max_iters=20) print(f"Optimization f={res.y_opt} at {res.x_opt}") self.assertAlmostEqual(-15.125, res.y_opt[0], delta=1e-3) self.assertAlmostEqual(18.935, res.x_opt[0], delta=1e-3) def test_xsinx_with_reclustering(self): - egor = egx.Egor(xsinx, egx.to_specs([[0.0, 25.0]]), seed=42, n_clusters=0) - res = egor.minimize(max_iters=20) + egor = egx.Egor(egx.to_specs([[0.0, 25.0]]), seed=42, n_clusters=0) + res = egor.minimize(xsinx, max_iters=20) print(f"Optimization f={res.y_opt} at {res.x_opt}") self.assertAlmostEqual(-15.125, res.y_opt[0], delta=1e-3) self.assertAlmostEqual(18.935, res.x_opt[0], delta=1e-3) @@ -100,14 +100,14 @@ def test_xsinx_with_hotstart(self): os.remove("./test_dir/egor_doe.npy") xlimits = egx.to_specs([[0.0, 25.0]]) doe = egx.lhs(xlimits, 10) - egor = egx.Egor(xsinx, xlimits, doe=doe, seed=42, outdir="./test_dir") - res = egor.minimize(max_iters=15) + egor = egx.Egor(xlimits, doe=doe, seed=42, outdir="./test_dir") + res = egor.minimize(xsinx, max_iters=15) print(f"Optimization f={res.y_opt} at {res.x_opt}") self.assertAlmostEqual(-15.125, res.y_opt[0], delta=1e-3) self.assertAlmostEqual(18.935, res.x_opt[0], delta=1e-3) - egor = egx.Egor(xsinx, xlimits, outdir="./test_dir", hot_start=True) - res = egor.minimize(max_iters=5) + egor = egx.Egor(xlimits, outdir="./test_dir", hot_start=True) + res = egor.minimize(xsinx, max_iters=5) print(f"Optimization f={res.y_opt} at {res.x_opt}") self.assertAlmostEqual(-15.125, res.y_opt[0], delta=1e-2) self.assertAlmostEqual(18.935, res.x_opt[0], delta=1e-2) @@ -122,7 +122,6 @@ def test_g24(self): max_iters = 20 n_cstr = 2 egor = egx.Egor( - g24, egx.to_specs([[0.0, 3.0], [0.0, 4.0]]), cstr_tol=np.array([1e-3, 1e-3]), n_cstr=n_cstr, @@ -130,7 +129,7 @@ def test_g24(self): n_doe=n_doe, ) start = time.process_time() - res = egor.minimize(max_iters=max_iters) + res = egor.minimize(g24, max_iters=max_iters) end = time.process_time() print(f"Optimization f={res.y_opt} at {res.x_opt} in {end-start}s") self.assertAlmostEqual(-5.5080, res.y_opt[0], delta=1e-2) @@ -141,7 +140,6 @@ def test_g24(self): def test_g24_kpls(self): egor = egx.Egor( - g24, egx.to_specs([[0.0, 3.0], [0.0, 4.0]]), n_cstr=2, cstr_tol=np.array([1e-3, 1e-3]), @@ -151,20 +149,19 @@ def test_g24_kpls(self): seed=1, ) start = time.process_time() - res = egor.minimize(max_iters=20) + res = egor.minimize(g24, max_iters=20) end = time.process_time() self.assertAlmostEqual(-5.5080, res.y_opt[0], delta=5e-1) print(f"Optimization f={res.y_opt} at {res.x_opt} in {end-start}s") def test_six_humps(self): egor = egx.Egor( - six_humps, egx.to_specs([[-3.0, 3.0], [-2.0, 2.0]]), infill_strategy=egx.InfillStrategy.WB2, seed=42, ) start = time.process_time() - res = egor.minimize(max_iters=45) + res = egor.minimize(six_humps, max_iters=45) end = time.process_time() print(f"Optimization f={res.y_opt} at {res.x_opt} in {end-start}s") # 2 global optimum value =-1.0316 located at (0.089842, -0.712656) and (-0.089842, 0.712656) @@ -172,7 +169,7 @@ def test_six_humps(self): def test_constructor(self): self.assertRaises(TypeError, egx.Egor) - egx.Egor(xsinx, egx.to_specs([[0.0, 25.0]]), 22, n_doe=10) + egx.Egor(egx.to_specs([[0.0, 25.0]]), 22, n_doe=10) def test_egor_service(self): xlimits = egx.to_specs([[0.0, 25.0]]) diff --git a/python/egobox/tests/test_mixintegor.py b/python/egobox/tests/test_mixintegor.py index 9c2c28af..ca00511d 100644 --- a/python/egobox/tests/test_mixintegor.py +++ b/python/egobox/tests/test_mixintegor.py @@ -37,8 +37,8 @@ class TestMixintEgx(unittest.TestCase): def test_int(self): xtypes = [egx.XSpec(egx.XType.INT, [0.0, 25.0])] - egor = egx.Egor(xsinx, xtypes, seed=42, n_doe=3) - res = egor.minimize(max_iters=10) + egor = egx.Egor(xtypes, seed=42, n_doe=3) + res = egor.minimize(xsinx, max_iters=10) print(f"Optimization f={res.y_opt} at {res.x_opt}") self.assertAlmostEqual(-15.125, res.y_opt[0], delta=5e-3) self.assertAlmostEqual(19, res.x_opt[0], delta=1) @@ -50,8 +50,8 @@ def test_ord_enum(self): egx.XSpec(egx.XType.ENUM, xlimits=[2]), egx.XSpec(egx.XType.ORD, [0, 2, 3]), ] - egor = egx.Egor(mixobj, xtypes, seed=42) - res = egor.minimize(max_iters=10) + egor = egx.Egor(xtypes, seed=42) + res = egor.minimize(mixobj, max_iters=10) self.assertAlmostEqual(-14.25, res.y_opt[0]) self.assertAlmostEqual(-5, res.x_opt[0]) self.assertAlmostEqual(2, res.x_opt[1]) From 3ab6851720525d65ab58ccd7e1ea983eda5c7db2 Mon Sep 17 00:00:00 2001 From: relf Date: Thu, 23 Nov 2023 22:07:15 +0100 Subject: [PATCH 11/13] Refactoring --- src/egor.rs | 200 +++++++++++++++++++--------------------------------- 1 file changed, 73 insertions(+), 127 deletions(-) diff --git a/src/egor.rs b/src/egor.rs index 81196626..5a66f67b 100644 --- a/src/egor.rs +++ b/src/egor.rs @@ -255,88 +255,10 @@ impl Egor { pyarray.to_owned_array() }) }; - - let infill_strategy = match self.infill_strategy { - InfillStrategy::Ei => egobox_ego::InfillStrategy::EI, - InfillStrategy::Wb2 => egobox_ego::InfillStrategy::WB2, - InfillStrategy::Wb2s => egobox_ego::InfillStrategy::WB2S, - }; - - let qei_strategy = match self.par_infill_strategy { - ParInfillStrategy::Kb => egobox_ego::QEiStrategy::KrigingBeliever, - ParInfillStrategy::Kblb => egobox_ego::QEiStrategy::KrigingBelieverLowerBound, - ParInfillStrategy::Kbub => egobox_ego::QEiStrategy::KrigingBelieverUpperBound, - ParInfillStrategy::Clmin => egobox_ego::QEiStrategy::ConstantLiarMinimum, - }; - - let infill_optimizer = match self.infill_optimizer { - InfillOptimizer::Cobyla => egobox_ego::InfillOptimizer::Cobyla, - InfillOptimizer::Slsqp => egobox_ego::InfillOptimizer::Slsqp, - }; - - let xspecs: Vec = self.xspecs.extract(py).expect("Error in xspecs conversion"); - if xspecs.is_empty() { - panic!("Error: xspecs argument cannot be empty") - } - - let xtypes: Vec = xspecs - .iter() - .map(|spec| match spec.xtype { - XType::Float => egobox_ego::XType::Cont(spec.xlimits[0], spec.xlimits[1]), - XType::Int => { - egobox_ego::XType::Int(spec.xlimits[0] as i32, spec.xlimits[1] as i32) - } - XType::Ord => egobox_ego::XType::Ord(spec.xlimits.clone()), - XType::Enum => { - if spec.tags.is_empty() { - egobox_ego::XType::Enum(spec.xlimits[0] as usize) - } else { - egobox_ego::XType::Enum(spec.tags.len()) - } - } - }) - .collect(); - - let cstr_tol = self.cstr_tol.clone().unwrap_or(vec![0.0; self.n_cstr]); - let cstr_tol = Array1::from_vec(cstr_tol); + let xtypes: Vec = self.xtypes(py); let mixintegor = egobox_ego::EgorBuilder::optimize(obj) - .configure(|config| { - let mut config = config - .n_cstr(self.n_cstr) - .max_iters(max_iters) - .n_start(self.n_start) - .n_doe(self.n_doe) - .cstr_tol(&cstr_tol) - .regression_spec( - egobox_moe::RegressionSpec::from_bits(self.regression_spec.0).unwrap(), - ) - .correlation_spec( - egobox_moe::CorrelationSpec::from_bits(self.correlation_spec.0).unwrap(), - ) - .infill_strategy(infill_strategy) - .q_points(self.q_points) - .qei_strategy(qei_strategy) - .infill_optimizer(infill_optimizer) - .target(self.target) - .hot_start(self.hot_start); - if let Some(doe) = self.doe.as_ref() { - config = config.doe(doe); - }; - if let Some(kpls_dim) = self.kpls_dim { - config = config.kpls_dim(kpls_dim); - }; - if let Some(n_clusters) = self.n_clusters { - config = config.n_clusters(n_clusters); - }; - if let Some(outdir) = self.outdir.as_ref().cloned() { - config = config.outdir(outdir); - }; - if let Some(seed) = self.seed { - config = config.random_seed(seed); - }; - config - }) + .configure(|config| self.apply_config(config, Some(max_iters), self.doe.as_ref())) .min_within_mixint_space(&xtypes); let res = py.allow_threads(|| { @@ -377,27 +299,44 @@ impl Egor { ) -> Py> { let x_doe = x_doe.as_array(); let y_doe = y_doe.as_array(); - let doe = concatenate(Axis(1), &[x_doe.view(), y_doe.view()]).unwrap(); + let xtypes: Vec = self.xtypes(py); + + let mixintegor = egobox_ego::EgorServiceBuilder::optimize() + .configure(|config| self.apply_config(config, Some(1), Some(&doe))) + .min_within_mixint_space(&xtypes); + + let x_suggested = py.allow_threads(|| mixintegor.suggest(&x_doe, &y_doe)); + x_suggested.into_pyarray(py).to_owned() + } +} - let infill_strategy = match self.infill_strategy { +impl Egor { + fn infill_strategy(&self) -> egobox_ego::InfillStrategy { + match self.infill_strategy { InfillStrategy::Ei => egobox_ego::InfillStrategy::EI, InfillStrategy::Wb2 => egobox_ego::InfillStrategy::WB2, InfillStrategy::Wb2s => egobox_ego::InfillStrategy::WB2S, - }; + } + } - let qei_strategy = match self.par_infill_strategy { + fn qei_strategy(&self) -> egobox_ego::QEiStrategy { + match self.par_infill_strategy { ParInfillStrategy::Kb => egobox_ego::QEiStrategy::KrigingBeliever, ParInfillStrategy::Kblb => egobox_ego::QEiStrategy::KrigingBelieverLowerBound, ParInfillStrategy::Kbub => egobox_ego::QEiStrategy::KrigingBelieverUpperBound, ParInfillStrategy::Clmin => egobox_ego::QEiStrategy::ConstantLiarMinimum, - }; + } + } - let infill_optimizer = match self.infill_optimizer { + fn infill_optimizer(&self) -> egobox_ego::InfillOptimizer { + match self.infill_optimizer { InfillOptimizer::Cobyla => egobox_ego::InfillOptimizer::Cobyla, InfillOptimizer::Slsqp => egobox_ego::InfillOptimizer::Slsqp, - }; + } + } + fn xtypes(&self, py: Python) -> Vec { let xspecs: Vec = self.xspecs.extract(py).expect("Error in xspecs conversion"); if xspecs.is_empty() { panic!("Error: xspecs argument cannot be empty") @@ -420,49 +359,56 @@ impl Egor { } }) .collect(); + xtypes + } + fn cstr_tol(&self) -> Array1 { let cstr_tol = self.cstr_tol.clone().unwrap_or(vec![0.0; self.n_cstr]); - let cstr_tol = Array1::from_vec(cstr_tol); + Array1::from_vec(cstr_tol) + } - let mixintegor = egobox_ego::EgorServiceBuilder::optimize() - .configure(|config| { - let mut config = config - .n_cstr(self.n_cstr) - .n_start(self.n_start) - .doe(&doe) - .cstr_tol(&cstr_tol) - .regression_spec( - egobox_moe::RegressionSpec::from_bits(self.regression_spec.0).unwrap(), - ) - .correlation_spec( - egobox_moe::CorrelationSpec::from_bits(self.correlation_spec.0).unwrap(), - ) - .infill_strategy(infill_strategy) - .q_points(self.q_points) - .qei_strategy(qei_strategy) - .infill_optimizer(infill_optimizer) - .target(self.target) - .hot_start(false); // when used as a service no hotstart - if let Some(doe) = self.doe.as_ref() { - config = config.doe(doe); - }; - if let Some(kpls_dim) = self.kpls_dim { - config = config.kpls_dim(kpls_dim); - }; - if let Some(n_clusters) = self.n_clusters { - config = config.n_clusters(n_clusters); - }; - if let Some(outdir) = self.outdir.as_ref().cloned() { - config = config.outdir(outdir); - }; - if let Some(seed) = self.seed { - config = config.random_seed(seed); - }; - config - }) - .min_within_mixint_space(&xtypes); + fn apply_config( + &self, + config: egobox_ego::EgorConfig, + max_iters: Option, + doe: Option<&Array2>, + ) -> egobox_ego::EgorConfig { + let infill_strategy = self.infill_strategy(); + let qei_strategy = self.qei_strategy(); + let infill_optimizer = self.infill_optimizer(); + let cstr_tol = self.cstr_tol(); - let x_suggested = py.allow_threads(|| mixintegor.suggest(&x_doe, &y_doe)); - x_suggested.into_pyarray(py).to_owned() + let mut config = config + .n_cstr(self.n_cstr) + .max_iters(max_iters.unwrap_or(1)) + .n_start(self.n_start) + .n_doe(self.n_doe) + .cstr_tol(&cstr_tol) + .regression_spec(egobox_moe::RegressionSpec::from_bits(self.regression_spec.0).unwrap()) + .correlation_spec( + egobox_moe::CorrelationSpec::from_bits(self.correlation_spec.0).unwrap(), + ) + .infill_strategy(infill_strategy) + .q_points(self.q_points) + .qei_strategy(qei_strategy) + .infill_optimizer(infill_optimizer) + .target(self.target) + .hot_start(self.hot_start); // when used as a service no hotstart + if let Some(doe) = doe { + config = config.doe(doe); + }; + if let Some(kpls_dim) = self.kpls_dim { + config = config.kpls_dim(kpls_dim); + }; + if let Some(n_clusters) = self.n_clusters { + config = config.n_clusters(n_clusters); + }; + if let Some(outdir) = self.outdir.as_ref().cloned() { + config = config.outdir(outdir); + }; + if let Some(seed) = self.seed { + config = config.random_seed(seed); + }; + config } } From 82be61fc21f3efbc4e014b582f932f637051ece2 Mon Sep 17 00:00:00 2001 From: relf Date: Thu, 30 Nov 2023 15:48:11 +0100 Subject: [PATCH 12/13] Add get_result for ask-and-tell interface --- python/egobox/tests/test_egor.py | 5 +-- src/egor.rs | 58 +++++++++++++++++++++++++++++--- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/python/egobox/tests/test_egor.py b/python/egobox/tests/test_egor.py index 7474d421..15af5fcd 100644 --- a/python/egobox/tests/test_egor.py +++ b/python/egobox/tests/test_egor.py @@ -175,13 +175,14 @@ def test_egor_service(self): xlimits = egx.to_specs([[0.0, 25.0]]) egor = egx.Egor(xlimits, seed=42) x_doe = egx.lhs(xlimits, 3, seed=42) - print(x_doe) y_doe = xsinx(x_doe) - print(y_doe) for _ in range(10): x = egor.suggest(x_doe, y_doe) x_doe = np.concatenate((x_doe, x)) y_doe = np.concatenate((y_doe, xsinx(x))) + res = egor.get_result(x_doe, y_doe) + self.assertAlmostEqual(-15.125, res.y_opt[0], delta=1e-3) + self.assertAlmostEqual(18.935, res.x_opt[0], delta=1e-3) if __name__ == "__main__": diff --git a/src/egor.rs b/src/egor.rs index 5a66f67b..ab363691 100644 --- a/src/egor.rs +++ b/src/egor.rs @@ -11,9 +11,10 @@ //! use crate::types::*; +use egobox_ego::find_best_result_index; use ndarray::{concatenate, Array1, Axis}; use numpy::ndarray::{Array2, ArrayView2}; -use numpy::{IntoPyArray, PyArray1, PyArray2, PyReadonlyArray2}; +use numpy::{IntoPyArray, PyArray1, PyArray2, PyReadonlyArray2, ToPyArray}; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; @@ -241,7 +242,7 @@ impl Egor { /// /// # Returns /// optimization result - /// x_opt (array[1, nx]): x value where fun is at its minimum subject to constraint + /// x_opt (array[1, nx]): x value where fun is at its minimum subject to constraints /// y_opt (array[1, nx]): fun(x_opt) /// #[pyo3(signature = (fun, max_iters = 20))] @@ -284,7 +285,7 @@ impl Egor { /// /// # Parameters /// x_doe (array[ns, nx]): ns samples where function has been evaluated - /// y_doe (array[ns, 1 + n_cstr]): ns values of objetcive and constraints + /// y_doe (array[ns, 1 + n_cstr]): ns values of objecctive and constraints /// /// /// # Returns @@ -307,7 +308,56 @@ impl Egor { .min_within_mixint_space(&xtypes); let x_suggested = py.allow_threads(|| mixintegor.suggest(&x_doe, &y_doe)); - x_suggested.into_pyarray(py).to_owned() + x_suggested.to_pyarray(py).into() + } + + /// This function gives the best evaluation index given the outputs + /// of the function (objective wrt constraints) under minimization. + /// + /// # Parameters + /// y_doe (array[ns, 1 + n_cstr]): ns values of objective and constraints + /// + /// # Returns + /// index in y_doe of the best evaluation + /// + #[pyo3(signature = (y_doe))] + fn get_result_index(&self, y_doe: PyReadonlyArray2) -> usize { + let y_doe = y_doe.as_array(); + find_best_result_index(&y_doe, &self.cstr_tol()) + } + + /// This function gives the best result given inputs and outputs + /// of the function (objective wrt constraints) under minimization. + /// + /// # Parameters + /// x_doe (array[ns, nx]): ns samples where function has been evaluated + /// y_doe (array[ns, 1 + n_cstr]): ns values of objective and constraints + /// + /// # Returns + /// optimization result + /// x_opt (array[1, nx]): x value where fun is at its minimum subject to constraints + /// y_opt (array[1, nx]): fun(x_opt) + /// + #[pyo3(signature = (x_doe, y_doe))] + fn get_result( + &self, + py: Python, + x_doe: PyReadonlyArray2, + y_doe: PyReadonlyArray2, + ) -> OptimResult { + let x_doe = x_doe.as_array(); + let y_doe = y_doe.as_array(); + let idx = find_best_result_index(&y_doe, &self.cstr_tol()); + let x_opt = x_doe.row(idx).to_pyarray(py).into(); + let y_opt = y_doe.row(idx).to_pyarray(py).into(); + let x_hist = x_doe.to_pyarray(py).into(); + let y_hist = y_doe.to_pyarray(py).into(); + OptimResult { + x_opt, + y_opt, + x_hist, + y_hist, + } } } From ec22cda48e7921ce93d28b44c2ac99cb7edebba9 Mon Sep 17 00:00:00 2001 From: relf Date: Thu, 30 Nov 2023 15:48:39 +0100 Subject: [PATCH 13/13] Add exmaple Egor as a service --- doc/Egor_Tutorial.ipynb | 264 ++++++++++++++++++++++++++++++---------- 1 file changed, 197 insertions(+), 67 deletions(-) diff --git a/doc/Egor_Tutorial.ipynb b/doc/Egor_Tutorial.ipynb index 766b8584..5f6180c0 100644 --- a/doc/Egor_Tutorial.ipynb +++ b/doc/Egor_Tutorial.ipynb @@ -158,25 +158,82 @@ "id": "45641636", "metadata": {}, "source": [ - "### Continuous optimization with _Egor_" + "### Continuous optimization" ] }, { "cell_type": "code", "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "egor = egx.Egor(xspecs_xsinx, n_cstr=n_cstr_xsinx) # see help(egor) for options" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Optimization f=[-15.12510323] at [18.93509877]\n", + "Optimization history: \n", + "Inputs = [[24.16632486]\n", + " [11.46890607]\n", + " [18.44757257]\n", + " [ 7.79926692]\n", + " [ 2.17759007]\n", + " [18.12777788]\n", + " [18.68945993]\n", + " [18.9361961 ]\n", + " [18.93481137]\n", + " [18.93489165]\n", + " [18.93509877]\n", + " [18.93564231]\n", + " [18.93490956]]\n", + "Outputs = [[ 6.01070335]\n", + " [ 4.53248411]\n", + " [-14.93205423]\n", + " [ 4.21159462]\n", + " [ 0.54035662]\n", + " [-14.60466485]\n", + " [-15.07551091]\n", + " [-15.12510243]\n", + " [-15.1251031 ]\n", + " [-15.12510315]\n", + " [-15.12510323]\n", + " [-15.12510308]\n", + " [-15.12510316]]\n" + ] + } + ], + "source": [ + "res = egor.minimize(xsinx, max_iters=8)\n", + "print(f\"Optimization f={res.y_opt} at {res.x_opt}\")\n", + "print(\"Optimization history: \")\n", + "print(f\"Inputs = {res.x_hist}\")\n", + "print(f\"Outputs = {res.y_hist}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, "id": "c8942031", "metadata": {}, "outputs": [], "source": [ - "egor = egx.Egor(g24, xspecs_g24, \n", - " n_doe=10, \n", - " n_cstr=n_cstr_g24, \n", - " cstr_tol=[1e-3, 1e-3],\n", - " infill_strategy=egx.InfillStrategy.WB2,\n", - " target=-5.5,\n", - " # outdir=\"./out\",\n", - " # hot_start=True\n", - " ) # see help(egor) for options\n", + "egor = egx.Egor(xspecs_g24, \n", + " n_doe=10, \n", + " n_cstr=n_cstr_g24, \n", + " cstr_tol=[1e-3, 1e-3],\n", + " infill_strategy=egx.InfillStrategy.WB2,\n", + " target=-5.5,\n", + " # outdir=\"./out\",\n", + " # hot_start=True\n", + " ) \n", "\n", "# Specify regression and/or correlation models used to build the surrogates of objective and constraints\n", "#egor = egx.Egor(g24, xlimits_g24, n_cstr=n_cstr_g24, n_doe=10,\n", @@ -186,7 +243,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 9, "id": "c12b8e9d", "metadata": {}, "outputs": [ @@ -194,45 +251,89 @@ "name": "stdout", "output_type": "stream", "text": [ - "Optimization f=[-5.50837809e+00 3.84933482e-04 3.56699512e-04] at [2.329518 3.17886009]\n", + "Optimization f=[-5.50857874e+00 -1.02345305e-04 8.35057110e-04] at [2.32959306 3.17898568]\n", "Optimization history: \n", - "Inputs = [[2.14941171 1.2022835 ]\n", - " [2.89591634 3.04421182]\n", - " [1.13129495 3.27983113]\n", - " [2.50882074 2.47459348]\n", - " [1.34917886 1.15978118]\n", - " [1.99449618 0.14570364]\n", - " [1.68323092 3.96900812]\n", - " [0.26740239 1.98981025]\n", - " [0.823056 0.52179224]\n", - " [0.34794318 2.23644392]\n", - " [2.3323044 3.09159185]\n", - " [2.32962545 3.17922313]\n", - " [2.329518 3.17886009]]\n", - "Outputs = [[-3.35169521e+00 -1.00398765e+00 -2.62111904e+00]\n", - " [-5.94012816e+00 -1.24186360e+01 2.88844914e+00]\n", - " [-4.41112609e+00 -6.51809729e-01 3.03904161e+00]\n", - " [-4.98341421e+00 -2.78451535e+00 2.77667993e-01]\n", - " [-2.50896004e+00 -2.38224715e+00 -1.69313518e-01]\n", - " [-2.14019982e+00 -1.85453736e+00 -3.85405403e+00]\n", - " [-5.65223904e+00 1.40041321e+00 7.31474755e-01]\n", - " [-2.25721264e+00 -4.39484898e-01 -1.40405159e+01]\n", - " [-1.34484824e+00 -3.35493157e+00 -7.17152437e-02]\n", - " [-2.58438710e+00 -4.24396527e-01 -9.72535570e+00]\n", - " [-5.42389625e+00 -1.09766687e-01 -7.37742400e-02]\n", - " [-5.50884858e+00 -1.29446921e-04 1.22477349e-03]\n", - " [-5.50837809e+00 3.84933482e-04 3.56699512e-04]]\n" + "Inputs = [[2.02750492 1.84783549]\n", + " [2.43770908 0.79253687]\n", + " [2.83077386 3.33954312]\n", + " [1.61764313 0.13863444]\n", + " [0.42165631 2.71057275]\n", + " [1.22945986 1.04947757]\n", + " [1.12529889 3.10581662]\n", + " [0.62145558 3.60508041]\n", + " [0.0868541 2.01094979]\n", + " [2.26033054 1.3874575 ]\n", + " [2.15407992 3.95417039]\n", + " [2.3260228 3.1921383 ]\n", + " [2.32959306 3.17898568]]\n", + "Outputs = [[-3.87534041e+00 -1.58384279e-01 -2.14611463e+00]\n", + " [-3.23024595e+00 -3.48447274e+00 -1.82157495e+00]\n", + " [-6.17031698e+00 -9.72175238e+00 2.95560230e+00]\n", + " [-1.75627757e+00 -2.62649202e+00 -2.77728535e+00]\n", + " [-3.13222905e+00 -1.75257198e-01 -6.18376493e+00]\n", + " [-2.27893743e+00 -2.74545955e+00 3.89263155e-01]\n", + " [-4.23111551e+00 -8.31877099e-01 2.88510841e+00]\n", + " [-4.22653599e+00 1.37196496e-01 3.62309923e-01]\n", + " [-2.09780388e+00 -4.42715310e-02 -2.62941582e+01]\n", + " [-3.64778804e+00 -1.30504948e+00 -2.08873873e+00]\n", + " [-6.10825031e+00 1.73385454e+00 1.41840902e-01]\n", + " [-5.51816110e+00 4.19898758e-02 -2.72575289e-03]\n", + " [-5.50857874e+00 -1.02345305e-04 8.35057110e-04]]\n" ] } ], "source": [ - "res = egor.minimize(max_iters=30)\n", + "res = egor.minimize(g24, max_iters=30)\n", "print(f\"Optimization f={res.y_opt} at {res.x_opt}\")\n", "print(\"Optimization history: \")\n", "print(f\"Inputs = {res.x_hist}\")\n", "print(f\"Outputs = {res.y_hist}\")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Egor as a service: ask-and-tell interface" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When the user needs to be in control of the optimization loop, `Egor` can be used as a service. \n", + "\n", + "For instance with the `xsinx` objective function, we can do:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Optimization f=[-15.12510323] at [18.93528147]\n" + ] + } + ], + "source": [ + "xlimits = egx.to_specs([[0.0, 25.0]])\n", + "egor = egx.Egor(xlimits, seed=42) \n", + "\n", + "# initial doe\n", + "x_doe = egx.lhs(xlimits, 3, seed=42)\n", + "y_doe = xsinx(x_doe)\n", + "for _ in range(10): # run for 10 iterations\n", + " x = egor.suggest(x_doe, y_doe) # ask for best location\n", + " x_doe = np.concatenate((x_doe, x))\n", + " y_doe = np.concatenate((y_doe, xsinx(x))) \n", + "res = egor.get_result(x_doe, y_doe)\n", + "print(f\"Optimization f={res.y_opt} at {res.x_opt}\")" + ] + }, { "attachments": {}, "cell_type": "markdown", @@ -253,7 +354,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 11, "id": "6948efc1", "metadata": {}, "outputs": [], @@ -281,7 +382,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 12, "id": "928d1f38", "metadata": {}, "outputs": [ @@ -291,34 +392,28 @@ "text": [ "Optimization f=[-15.12161154] at [19.]\n", "Optimization history: \n", - "Inputs = [[ 7.]\n", - " [24.]\n", - " [11.]\n", - " [ 6.]\n", - " [ 5.]\n", - " [ 4.]\n", - " [17.]\n", + "Inputs = [[23.]\n", + " [ 8.]\n", + " [ 9.]\n", + " [20.]\n", " [18.]\n", " [19.]]\n", - "Outputs = [[ 3.14127616]\n", - " [ 4.91604976]\n", - " [ 5.1356682 ]\n", - " [ 1.78601478]\n", - " [ 0.68929352]\n", - " [ 0.07924194]\n", - " [-12.35295142]\n", + "Outputs = [[ -1.48334497]\n", + " [ 4.45696985]\n", + " [ 5.41123083]\n", + " [-14.15453288]\n", " [-14.43198471]\n", " [-15.12161154]]\n" ] } ], "source": [ - "egor = egx.Egor(mixint_xsinx, xspecs_mixint_xsinx, \n", + "egor = egx.Egor(xspecs_mixint_xsinx, \n", " n_doe=3, \n", " infill_strategy=egx.InfillStrategy.EI,\n", " target=-15.12,\n", " ) # see help(egor) for options\n", - "res = egor.minimize(max_iters=30)\n", + "res = egor.minimize(mixint_xsinx, max_iters=30)\n", "print(f\"Optimization f={res.y_opt} at {res.x_opt}\")\n", "print(\"Optimization history: \")\n", "print(f\"Inputs = {res.x_hist}\")\n", @@ -355,7 +450,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 13, "id": "f1615d5c", "metadata": {}, "outputs": [], @@ -391,7 +486,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 14, "id": "d14fff89", "metadata": {}, "outputs": [ @@ -447,8 +542,8 @@ " egx.XSpec(egx.XType.ENUM, xlimits=[2]),\n", " egx.XSpec(egx.XType.ORD, [0, 2, 3]),\n", "]\n", - "egor = egx.Egor(mixobj, xtypes, seed=42)\n", - "res = egor.minimize(max_iters=10)\n", + "egor = egx.Egor(xtypes, seed=42)\n", + "res = egor.minimize(mixobj, max_iters=10)\n", "print(f\"Optimization f={res.y_opt} at {res.x_opt}\")\n", "print(\"Optimization history: \")\n", "print(f\"Inputs = {res.x_hist}\")\n", @@ -465,7 +560,6 @@ }, { "cell_type": "markdown", - "id": "3b9fadfa", "metadata": {}, "source": [ "## Usage" @@ -473,7 +567,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 15, "id": "b91f14f2", "metadata": {}, "outputs": [ @@ -484,7 +578,7 @@ "Help on Egor in module builtins object:\n", "\n", "class Egor(object)\n", - " | Egor(fun, xspecs, n_cstr=0, cstr_tol=None, n_start=20, n_doe=0, doe=None, regr_spec=Ellipsis, corr_spec=Ellipsis, infill_strategy=Ellipsis, q_points=1, par_infill_strategy=Ellipsis, infill_optimizer=Ellipsis, kpls_dim=None, n_clusters=1, target=Ellipsis, outdir=None, hot_start=False, seed=None)\n", + " | Egor(xspecs, n_cstr=0, cstr_tol=None, n_start=20, n_doe=0, doe=None, regr_spec=Ellipsis, corr_spec=Ellipsis, infill_strategy=Ellipsis, q_points=1, par_infill_strategy=Ellipsis, infill_optimizer=Ellipsis, kpls_dim=None, n_clusters=1, target=Ellipsis, outdir=None, hot_start=False, seed=None)\n", " | \n", " | Optimizer constructor\n", " | \n", @@ -581,7 +675,30 @@ " | \n", " | Methods defined here:\n", " | \n", - " | minimize(self, /, max_iters=20)\n", + " | get_result(self, /, x_doe, y_doe)\n", + " | This function gives the best result given inputs and outputs\n", + " | of the function (objective wrt constraints) under minimization.\n", + " | \n", + " | # Parameters\n", + " | x_doe (array[ns, nx]): ns samples where function has been evaluated\n", + " | y_doe (array[ns, 1 + n_cstr]): ns values of objective and constraints\n", + " | \n", + " | # Returns\n", + " | optimization result\n", + " | x_opt (array[1, nx]): x value where fun is at its minimum subject to constraints\n", + " | y_opt (array[1, nx]): fun(x_opt)\n", + " | \n", + " | get_result_index(self, /, y_doe)\n", + " | This function gives the best evaluation index given the outputs\n", + " | of the function (objective wrt constraints) under minimization.\n", + " | \n", + " | # Parameters\n", + " | y_doe (array[ns, 1 + n_cstr]): ns values of objective and constraints\n", + " | \n", + " | # Returns\n", + " | index in y_doe of the best evaluation\n", + " | \n", + " | minimize(self, /, fun, max_iters=20)\n", " | This function finds the minimum of a given function `fun`\n", " | \n", " | # Parameters\n", @@ -590,9 +707,22 @@ " | \n", " | # Returns\n", " | optimization result\n", - " | x_opt (array[1, nx]): x value where fun is at its minimum subject to constraint\n", + " | x_opt (array[1, nx]): x value where fun is at its minimum subject to constraints\n", " | y_opt (array[1, nx]): fun(x_opt)\n", " | \n", + " | suggest(self, /, x_doe, y_doe)\n", + " | This function gives the next best location where to evaluate the function\n", + " | under optimization wrt to previous evaluations.\n", + " | The function returns several point when multi point qEI strategy is used.\n", + " | \n", + " | # Parameters\n", + " | x_doe (array[ns, nx]): ns samples where function has been evaluated\n", + " | y_doe (array[ns, 1 + n_cstr]): ns values of objecctive and constraints\n", + " | \n", + " | \n", + " | # Returns\n", + " | (array[1, nx]): suggested location where to evaluate objective and constraints\n", + " | \n", " | ----------------------------------------------------------------------\n", " | Static methods defined here:\n", " | \n", @@ -628,4 +758,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +}