#![allow(clippy::useless_conversion)]
use crate::domain::*;
use crate::gp_config::*;
use crate::types::*;
use egobox_ego::{find_best_result_index, CoegoStatus, InfillObjData};
use egobox_gp::ThetaTuning;
use egobox_moe::NbClusters;
use ndarray::{array, concatenate, Array1, Array2, ArrayView2, Axis};
use numpy::{IntoPyArray, PyArray1, PyArray2, PyArrayMethods, PyReadonlyArray2, ToPyArray};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pyfunction, gen_stub_pymethods};
use std::cmp::Ordering;
#[gen_stub_pyfunction]
#[pyfunction]
pub(crate) fn to_specs(py: Python, xlimits: Vec<Vec<f64>>) -> PyResult<Bound<PyAny>> {
if xlimits.is_empty() || xlimits[0].is_empty() {
let err = "Error: xspecs argument cannot be empty";
return Err(PyValueError::new_err(err.to_string()));
}
xlimits
.iter()
.map(|xlimit| XSpec::new(XType::Float, xlimit.clone(), vec![]))
.collect::<Vec<XSpec>>()
.into_pyobject(py)
}
#[gen_stub_pyclass]
#[pyclass]
pub(crate) struct Egor {
pub xspecs: PyObject,
pub gp_config: GpConfig,
pub n_cstr: usize,
pub cstr_tol: Option<Vec<f64>>,
pub n_start: usize,
pub n_doe: usize,
pub doe: Option<Array2<f64>>,
pub infill_strategy: InfillStrategy,
pub cstr_infill: bool,
pub cstr_strategy: ConstraintStrategy,
pub q_points: usize,
pub q_infill_strategy: QInfillStrategy,
pub infill_optimizer: InfillOptimizer,
pub trego: bool,
pub coego_n_coop: usize,
pub q_optmod: usize,
pub target: f64,
pub outdir: Option<String>,
pub warm_start: bool,
pub hot_start: Option<u64>,
pub seed: Option<u64>,
}
#[gen_stub_pymethods]
#[pymethods]
impl Egor {
#[new]
#[pyo3(signature = (
xspecs,
gp_config = GpConfig::default(),
n_cstr = 0,
cstr_tol = None,
n_start = 20,
n_doe = 0,
doe = None,
infill_strategy = InfillStrategy::Wb2,
cstr_infill = false,
cstr_strategy = ConstraintStrategy::Mc,
q_points = 1,
q_infill_strategy = QInfillStrategy::Kb,
infill_optimizer = InfillOptimizer::Cobyla,
trego = false,
coego_n_coop = 0,
q_optmod = 1,
target = f64::NEG_INFINITY,
outdir = None,
warm_start = false,
hot_start = None,
seed = None
))]
#[allow(clippy::too_many_arguments)]
fn new(
_py: Python,
xspecs: PyObject,
gp_config: GpConfig,
n_cstr: usize,
cstr_tol: Option<Vec<f64>>,
n_start: usize,
n_doe: usize,
doe: Option<PyReadonlyArray2<f64>>,
infill_strategy: InfillStrategy,
cstr_infill: bool,
cstr_strategy: ConstraintStrategy,
q_points: usize,
q_infill_strategy: QInfillStrategy,
infill_optimizer: InfillOptimizer,
trego: bool,
coego_n_coop: usize,
q_optmod: usize,
target: f64,
outdir: Option<String>,
warm_start: bool,
hot_start: Option<u64>,
seed: Option<u64>,
) -> Self {
let doe = doe.map(|x| x.to_owned_array());
Egor {
xspecs,
gp_config,
n_cstr,
cstr_tol,
n_start,
n_doe,
doe,
infill_strategy,
cstr_infill,
cstr_strategy,
q_points,
q_infill_strategy,
infill_optimizer,
trego,
coego_n_coop,
q_optmod,
target,
outdir,
warm_start,
hot_start,
seed,
}
}
#[pyo3(signature = (fun, fcstrs=vec![], max_iters = 20))]
fn minimize(
&self,
py: Python,
fun: PyObject,
fcstrs: Vec<PyObject>,
max_iters: usize,
) -> PyResult<OptimResult> {
let obj = |x: &ArrayView2<f64>| -> Array2<f64> {
Python::with_gil(|py| {
let args = (x.to_owned().into_pyarray(py),);
let res = fun.bind(py).call1(args).unwrap();
let pyarray = res.downcast_into::<PyArray2<f64>>().unwrap();
pyarray.to_owned_array()
})
};
let n_fcstr = fcstrs.len();
let fcstrs = fcstrs
.iter()
.map(|cstr| {
let cstr = |x: &[f64], g: Option<&mut [f64]>, _u: &mut InfillObjData<f64>| -> f64 {
Python::with_gil(|py| {
if let Some(g) = g {
let args = (Array1::from(x.to_vec()).into_pyarray(py), true);
let grad = cstr.bind(py).call1(args).unwrap();
let grad = grad.downcast_into::<PyArray1<f64>>().unwrap().readonly();
g.copy_from_slice(grad.as_slice().unwrap())
}
let args = (Array1::from(x.to_vec()).into_pyarray(py), false);
let res = cstr.bind(py).call1(args).unwrap().extract().unwrap();
res
})
};
cstr
})
.collect::<Vec<_>>();
let xtypes: Vec<egobox_ego::XType> = parse(py, self.xspecs.clone_ref(py));
let mixintegor = egobox_ego::EgorFactory::optimize(obj)
.subject_to(fcstrs)
.configure(|config| {
self.apply_config(config, Some(max_iters), n_fcstr, self.doe.as_ref())
})
.min_within_mixint_space(&xtypes);
let res = py.allow_threads(|| {
mixintegor
.run()
.expect("Egor should optimize the objective function")
});
let x_opt = res.x_opt.into_pyarray(py).to_owned();
let y_opt = res.y_opt.into_pyarray(py).to_owned();
let x_doe = res.x_doe.into_pyarray(py).to_owned();
let y_doe = res.y_doe.into_pyarray(py).to_owned();
Ok(OptimResult {
x_opt: x_opt.into(),
y_opt: y_opt.into(),
x_doe: x_doe.into(),
y_doe: y_doe.into(),
})
}
#[pyo3(signature = (x_doe, y_doe))]
fn suggest(
&self,
py: Python,
x_doe: PyReadonlyArray2<f64>,
y_doe: PyReadonlyArray2<f64>,
) -> Py<PyArray2<f64>> {
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<egobox_ego::XType> = parse(py, self.xspecs.clone_ref(py));
let mixintegor = egobox_ego::EgorServiceBuilder::optimize()
.configure(|config| self.apply_config(config, Some(1), 0, Some(&doe)))
.min_within_mixint_space(&xtypes);
let x_suggested = py.allow_threads(|| mixintegor.suggest(&x_doe, &y_doe));
x_suggested.to_pyarray(py).into()
}
#[pyo3(signature = (y_doe))]
fn get_result_index(&self, y_doe: PyReadonlyArray2<f64>) -> usize {
let y_doe = y_doe.as_array();
let n_fcstrs = 0;
let c_doe = Array2::zeros((y_doe.ncols(), n_fcstrs));
find_best_result_index(&y_doe, &c_doe, &self.cstr_tol(n_fcstrs))
}
#[pyo3(signature = (x_doe, y_doe))]
fn get_result(
&self,
py: Python,
x_doe: PyReadonlyArray2<f64>,
y_doe: PyReadonlyArray2<f64>,
) -> OptimResult {
let x_doe = x_doe.as_array();
let y_doe = y_doe.as_array();
let n_fcstrs = 0;
let c_doe = Array2::zeros((y_doe.ncols(), n_fcstrs));
let idx = find_best_result_index(&y_doe, &c_doe, &self.cstr_tol(n_fcstrs));
let x_opt = x_doe.row(idx).to_pyarray(py).into();
let y_opt = y_doe.row(idx).to_pyarray(py).into();
let x_doe = x_doe.to_pyarray(py).into();
let y_doe = y_doe.to_pyarray(py).into();
OptimResult {
x_opt,
y_opt,
x_doe,
y_doe,
}
}
}
impl Egor {
fn n_clusters(&self) -> NbClusters {
match self.gp_config.n_clusters.cmp(&0) {
Ordering::Greater => NbClusters::fixed(self.gp_config.n_clusters as usize),
Ordering::Equal => NbClusters::auto(),
Ordering::Less => NbClusters::automax(-self.gp_config.n_clusters as usize),
}
}
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,
InfillStrategy::LogEi => egobox_ego::InfillStrategy::LogEI,
}
}
fn cstr_strategy(&self) -> egobox_ego::ConstraintStrategy {
match self.cstr_strategy {
ConstraintStrategy::Mc => egobox_ego::ConstraintStrategy::MeanConstraint,
ConstraintStrategy::Utb => egobox_ego::ConstraintStrategy::UpperTrustBound,
}
}
fn qei_strategy(&self) -> egobox_ego::QEiStrategy {
match self.q_infill_strategy {
QInfillStrategy::Kb => egobox_ego::QEiStrategy::KrigingBeliever,
QInfillStrategy::Kblb => egobox_ego::QEiStrategy::KrigingBelieverLowerBound,
QInfillStrategy::Kbub => egobox_ego::QEiStrategy::KrigingBelieverUpperBound,
QInfillStrategy::Clmin => egobox_ego::QEiStrategy::ConstantLiarMinimum,
}
}
fn infill_optimizer(&self) -> egobox_ego::InfillOptimizer {
match self.infill_optimizer {
InfillOptimizer::Cobyla => egobox_ego::InfillOptimizer::Cobyla,
InfillOptimizer::Slsqp => egobox_ego::InfillOptimizer::Slsqp,
}
}
fn cstr_tol(&self, n_fcstr: usize) -> Array1<f64> {
let cstr_tol = self
.cstr_tol
.clone()
.unwrap_or(vec![egobox_ego::DEFAULT_CSTR_TOL; self.n_cstr + n_fcstr]);
Array1::from_vec(cstr_tol)
}
fn recombination(&self) -> egobox_moe::Recombination<f64> {
match self.gp_config.recombination {
Recombination::Hard => egobox_moe::Recombination::Hard,
Recombination::Smooth => egobox_moe::Recombination::Smooth(Some(1.0)),
}
}
fn theta_tuning(&self) -> ThetaTuning<f64> {
let mut theta_tuning = ThetaTuning::<f64>::default();
if let Some(init) = self.gp_config.theta_init.as_ref() {
theta_tuning = ThetaTuning::Full {
init: Array1::from_vec(init.to_vec()),
bounds: array![ThetaTuning::<f64>::DEFAULT_BOUNDS],
}
}
if let Some(bounds) = self.gp_config.theta_bounds.as_ref() {
theta_tuning = ThetaTuning::Full {
init: theta_tuning.init().to_owned(),
bounds: bounds.iter().map(|v| (v[0], v[1])).collect(),
}
}
theta_tuning
}
fn apply_config(
&self,
config: egobox_ego::EgorConfig,
max_iters: Option<usize>,
n_fcstr: usize,
doe: Option<&Array2<f64>>,
) -> egobox_ego::EgorConfig {
let infill_strategy = self.infill_strategy();
let cstr_strategy = self.cstr_strategy();
let qei_strategy = self.qei_strategy();
let infill_optimizer = self.infill_optimizer();
let coego_status = if self.coego_n_coop == 0 {
CoegoStatus::Disabled
} else {
CoegoStatus::Enabled(self.coego_n_coop)
};
let cstr_tol = self.cstr_tol(n_fcstr);
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)
.configure_gp(|gp| {
let regr = RegressionSpec(self.gp_config.regr_spec);
let corr = CorrelationSpec(self.gp_config.corr_spec);
let n_start = self.gp_config.n_start.max(0) as usize;
gp.regression_spec(egobox_moe::RegressionSpec::from_bits(regr.0).unwrap())
.correlation_spec(egobox_moe::CorrelationSpec::from_bits(corr.0).unwrap())
.kpls_dim(self.gp_config.kpls_dim)
.n_clusters(self.n_clusters())
.recombination(self.recombination())
.theta_tuning(self.theta_tuning())
.n_start(n_start)
.max_eval(self.gp_config.max_eval)
})
.infill_strategy(infill_strategy)
.cstr_infill(self.cstr_infill)
.cstr_strategy(cstr_strategy)
.q_points(self.q_points)
.qei_strategy(qei_strategy)
.infill_optimizer(infill_optimizer)
.trego(self.trego)
.coego(coego_status)
.q_optmod(self.q_optmod)
.target(self.target)
.warm_start(self.warm_start)
.hot_start(self.hot_start.into());
if let Some(doe) = doe {
config = config.doe(doe);
};
if let Some(outdir) = self.outdir.as_ref().cloned() {
config = config.outdir(outdir);
};
if let Some(seed) = self.seed {
config = config.seed(seed);
};
config
}
}