use std::{cmp::Ordering, path::Path};
use crate::logging::init_logger;
use crate::types::*;
use crate::{domain::parse, gp_config::GpConfig};
use egobox_ego::{EGO_GP_OPTIM_MAX_EVAL, EGO_GP_OPTIM_N_START};
#[allow(unused_imports)] use egobox_moe::{GpMixture, GpSurrogate, GpSurrogateExt};
use egobox_moe::{MixintGpMixture, MixtureGpSurrogate, NbClusters, ThetaTuning};
use linfa::{Dataset, traits::Fit};
use log::error;
use ndarray::{Array1, Array2, Axis, Ix1, Ix2, Zip, array};
use ndarray_rand::rand::SeedableRng;
use numpy::{IntoPyArray, PyArray1, PyArray2, PyReadonlyArray2, PyReadonlyArrayDyn};
use pyo3::prelude::*;
use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
use rand_xoshiro::Xoshiro256Plus;
#[gen_stub_pyclass]
#[pyclass(skip_from_py_object)]
pub(crate) struct GpMix {
gp_config: GpConfig,
xtypes: Option<Vec<egobox_moe::XType>>,
seed: Option<u64>,
}
#[gen_stub_pymethods]
#[pymethods]
impl GpMix {
#[new]
#[pyo3(signature = (
xspecs=None,
regr_spec=RegressionSpec::CONSTANT,
corr_spec=CorrelationSpec::SQUARED_EXPONENTIAL,
kpls_dim=None,
n_clusters=1,
recombination=Recombination::Hard,
theta_init=None,
theta_bounds=None,
n_start=EGO_GP_OPTIM_N_START,
max_eval=EGO_GP_OPTIM_MAX_EVAL,
seed=None,
verbose=None
))]
#[allow(clippy::too_many_arguments)]
fn new(
py: Python,
xspecs: Option<Py<PyAny>>,
regr_spec: u8,
corr_spec: u8,
kpls_dim: Option<usize>,
n_clusters: isize,
recombination: Recombination,
theta_init: Option<Vec<f64>>,
theta_bounds: Option<Vec<Vec<f64>>>,
n_start: usize,
max_eval: usize,
seed: Option<u64>,
verbose: Option<Py<PyAny>>,
) -> Self {
init_logger(py, verbose);
let xtypes = xspecs
.as_ref()
.map(|xspecs| parse(py, xspecs.clone_ref(py)));
GpMix {
gp_config: GpConfig::new(
regr_spec,
corr_spec,
kpls_dim,
n_clusters,
recombination,
theta_init,
theta_bounds,
n_start,
max_eval,
),
xtypes,
seed,
}
}
fn fit(&mut self, py: Python, xt: PyReadonlyArrayDyn<f64>, yt: PyReadonlyArrayDyn<f64>) -> Gpx {
let xt = xt.as_array();
let xt = match xt.to_owned().into_dimensionality::<Ix2>() {
Ok(xt) => xt,
Err(_) => match xt.into_dimensionality::<Ix1>() {
Ok(xt) => xt.insert_axis(Axis(1)).to_owned(),
_ => {
error!("Training input has to be an [nsamples, nx] array");
panic!("Bad training input data");
}
},
};
let yt = yt.as_array();
let yt = match yt.to_owned().into_dimensionality::<Ix1>() {
Ok(yt) => yt,
Err(_) => match yt.into_dimensionality::<Ix2>() {
Ok(yt) => {
if yt.dim().1 == 1 {
yt.to_owned().remove_axis(Axis(1))
} else {
error!("Training output has to be one dimensional");
panic!("Bad training output data");
}
}
Err(_) => {
error!("Training output has to be one dimensional");
panic!("Bad training output data");
}
},
};
let dataset = Dataset::new(xt, yt);
let recomb = match self.gp_config.recombination {
Recombination::Hard => egobox_moe::Recombination::Hard,
Recombination::Smooth => egobox_moe::Recombination::Smooth(None),
};
let rng = if let Some(seed) = self.seed {
Xoshiro256Plus::seed_from_u64(seed)
} else {
Xoshiro256Plus::from_entropy()
};
let mut theta_tuning = ThetaTuning::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(),
}
}
let n_clusters = 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),
};
let n_start: usize = if self.gp_config.n_start == 0 {
theta_tuning = ThetaTuning::Fixed(theta_tuning.init().to_owned());
0 } else {
self.gp_config.n_start
};
let theta_tunings = if let NbClusters::Fixed { nb } = n_clusters {
vec![theta_tuning; nb]
} else {
vec![theta_tuning; 1] };
if let Err(ctrlc::Error::MultipleHandlers) = ctrlc::set_handler(|| std::process::exit(2)) {
};
let moe = py.detach(|| {
let regr = RegressionSpec(self.gp_config.regr_spec);
let corr = CorrelationSpec(self.gp_config.corr_spec);
if let Some(xtypes) = self.xtypes.as_ref() {
Box::new(
MixintGpMixture::params(xtypes)
.n_clusters(n_clusters)
.recombination(recomb)
.regression_spec(egobox_moe::RegressionSpec::from_bits(regr.0).unwrap())
.correlation_spec(egobox_moe::CorrelationSpec::from_bits(corr.0).unwrap())
.theta_tunings(&theta_tunings)
.kpls_dim(self.gp_config.kpls_dim)
.n_start(n_start)
.with_rng(rng)
.fit(&dataset)
.expect("MoE model training"),
) as Box<dyn MixtureGpSurrogate>
} else {
Box::new(
GpMixture::params()
.n_clusters(n_clusters)
.recombination(recomb)
.regression_spec(egobox_moe::RegressionSpec::from_bits(regr.0).unwrap())
.correlation_spec(egobox_moe::CorrelationSpec::from_bits(corr.0).unwrap())
.theta_tunings(&theta_tunings)
.kpls_dim(self.gp_config.kpls_dim)
.n_start(n_start)
.with_rng(rng)
.fit(&dataset)
.expect("MoE model training"),
) as Box<dyn MixtureGpSurrogate>
}
});
Gpx(moe)
}
}
#[gen_stub_pyclass]
#[pyclass(skip_from_py_object)]
pub(crate) struct Gpx(Box<dyn MixtureGpSurrogate>);
#[gen_stub_pymethods]
#[pymethods]
impl Gpx {
#[staticmethod]
#[pyo3(signature = (
xspecs=None,
regr_spec=GpConfig::default().regr_spec,
corr_spec=GpConfig::default().corr_spec,
kpls_dim=GpConfig::default().kpls_dim,
n_clusters=GpConfig::default().n_clusters,
recombination=GpConfig::default().recombination,
theta_init=GpConfig::default().theta_init,
theta_bounds=GpConfig::default().theta_bounds,
n_start=GpConfig::default().n_start,
max_eval=GpConfig::default().max_eval,
seed = None,
verbose=None
))]
#[allow(clippy::too_many_arguments)]
fn builder(
py: Python,
xspecs: Option<Py<PyAny>>,
regr_spec: u8,
corr_spec: u8,
kpls_dim: Option<usize>,
n_clusters: isize,
recombination: Recombination,
theta_init: Option<Vec<f64>>,
theta_bounds: Option<Vec<Vec<f64>>>,
n_start: usize,
max_eval: usize,
seed: Option<u64>,
verbose: Option<Py<PyAny>>,
) -> GpMix {
GpMix::new(
py,
xspecs,
regr_spec,
corr_spec,
kpls_dim,
n_clusters,
recombination,
theta_init,
theta_bounds,
n_start,
max_eval,
seed,
verbose,
)
}
fn __repr__(&self) -> String {
serde_json::to_string(&self.0).unwrap()
}
fn __str__(&self) -> String {
self.0.to_string()
}
fn save(&self, filename: String) -> bool {
let format = match Path::new(&filename).extension().unwrap().to_str().unwrap() {
"json" => egobox_moe::GpFileFormat::Json,
_ => egobox_moe::GpFileFormat::Binary,
};
self.0.save(&filename, format).is_ok()
}
#[staticmethod]
fn load(filename: String) -> Gpx {
let format = match Path::new(&filename).extension().unwrap().to_str().unwrap() {
"json" => egobox_moe::GpFileFormat::Json,
_ => egobox_moe::GpFileFormat::Binary,
};
Gpx(GpMixture::load(&filename, format).unwrap())
}
fn predict<'py>(&self, py: Python<'py>, x: PyReadonlyArray2<f64>) -> Bound<'py, PyArray1<f64>> {
self.0.predict(&x.as_array()).unwrap().into_pyarray(py)
}
fn predict_var<'py>(
&self,
py: Python<'py>,
x: PyReadonlyArray2<f64>,
) -> Bound<'py, PyArray1<f64>> {
self.0.predict_var(&x.as_array()).unwrap().into_pyarray(py)
}
fn predict_gradients<'py>(
&self,
py: Python<'py>,
x: PyReadonlyArray2<f64>,
) -> Bound<'py, PyArray2<f64>> {
self.0
.predict_gradients(&x.as_array())
.unwrap()
.into_pyarray(py)
}
fn predict_var_gradients<'py>(
&self,
py: Python<'py>,
x: PyReadonlyArray2<f64>,
) -> Bound<'py, PyArray2<f64>> {
self.0
.predict_var_gradients(&x.as_array())
.unwrap()
.into_pyarray(py)
}
fn sample<'py>(
&self,
py: Python<'py>,
x: PyReadonlyArray2<f64>,
n_traj: usize,
) -> Bound<'py, PyArray2<f64>> {
self.0
.sample(&x.as_array(), n_traj)
.unwrap()
.into_pyarray(py)
}
fn dims(&self) -> (usize, usize) {
self.0.dims()
}
fn training_data<'py>(
&self,
py: Python<'py>,
) -> (Bound<'py, PyArray2<f64>>, Bound<'py, PyArray1<f64>>) {
let (xdata, ydata) = self.0.training_data();
(
xdata.to_owned().into_pyarray(py),
ydata.to_owned().into_pyarray(py),
)
}
fn thetas<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray2<f64>> {
let experts = self.0.experts();
let proto = experts.first().expect("Mixture should contain an expert");
let mut thetas = Array2::zeros((self.0.n_clusters(), proto.theta().len()));
Zip::from(thetas.rows_mut())
.and(experts)
.for_each(|mut theta, expert| theta.assign(expert.theta()));
thetas.into_pyarray(py)
}
fn variances<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<f64>> {
let experts = self.0.experts();
let mut variances = Array1::zeros(self.0.n_clusters());
Zip::from(&mut variances)
.and(experts)
.for_each(|var, expert| *var = expert.variance());
variances.into_pyarray(py)
}
fn likelihoods<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<f64>> {
let experts = self.0.experts();
let mut likelihoods = Array1::zeros(self.0.n_clusters());
Zip::from(&mut likelihoods)
.and(experts)
.for_each(|lkh, expert| *lkh = expert.likelihood());
likelihoods.into_pyarray(py)
}
}
impl From<Box<dyn MixtureGpSurrogate>> for Gpx {
fn from(moe: Box<dyn MixtureGpSurrogate>) -> Self {
Gpx(moe)
}
}