use std::path::Path;
use crate::types::*;
use egobox_moe::{
Clustered, GpMixture, GpSurrogate, GpType, Inducings, MixtureGpSurrogate, ThetaTuning,
};
use linfa::{traits::Fit, Dataset};
use log::error;
use ndarray::{array, Array1, Array2, Axis, Ix1, Ix2, Zip};
use ndarray_rand::rand::SeedableRng;
use numpy::{IntoPyArray, PyArray1, PyArray2, PyReadonlyArray2};
use pyo3::prelude::*;
use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
use rand_xoshiro::Xoshiro256Plus;
#[gen_stub_pyclass]
#[pyclass]
pub(crate) struct SparseGpMix {
pub correlation_spec: CorrelationSpec,
pub theta_init: Option<Vec<f64>>,
pub theta_bounds: Option<Vec<Vec<f64>>>,
pub kpls_dim: Option<usize>,
pub n_start: usize,
pub nz: Option<usize>,
pub z: Option<Array2<f64>>,
pub method: SparseMethod,
pub seed: Option<u64>,
}
#[gen_stub_pymethods]
#[pymethods]
impl SparseGpMix {
#[new]
#[pyo3(signature = (
corr_spec = CorrelationSpec::SQUARED_EXPONENTIAL,
theta_init = None,
theta_bounds = None,
kpls_dim = None,
n_start = 10,
nz = None,
z = None,
method = SparseMethod::Fitc,
seed = None
))]
#[allow(clippy::too_many_arguments)]
fn new(
corr_spec: u8,
theta_init: Option<Vec<f64>>,
theta_bounds: Option<Vec<Vec<f64>>>,
kpls_dim: Option<usize>,
n_start: usize,
nz: Option<usize>,
z: Option<PyReadonlyArray2<f64>>,
method: SparseMethod,
seed: Option<u64>,
) -> Self {
SparseGpMix {
correlation_spec: CorrelationSpec(corr_spec),
theta_init,
theta_bounds,
kpls_dim,
n_start,
nz,
z: z.map(|z| z.as_array().to_owned()),
method,
seed,
}
}
fn fit(
&mut self,
py: Python,
xt: PyReadonlyArray2<f64>,
yt: PyReadonlyArray2<f64>,
) -> SparseGpx {
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 rng = if let Some(seed) = self.seed {
Xoshiro256Plus::seed_from_u64(seed)
} else {
Xoshiro256Plus::from_entropy()
};
let inducings = if let Some(z) = self.z.as_ref() {
Inducings::Located(z.clone())
} else if let Some(nz) = self.nz {
Inducings::Randomized(nz)
} else {
panic!("You must specify inducing points")
};
let method = match self.method {
SparseMethod::Fitc => egobox_gp::SparseMethod::Fitc,
SparseMethod::Vfe => egobox_gp::SparseMethod::Vfe,
};
let mut theta_tuning = ThetaTuning::default();
if let Some(init) = self.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.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 theta_tunings = vec![theta_tuning];
if let Err(ctrlc::Error::MultipleHandlers) = ctrlc::set_handler(|| std::process::exit(2)) {
};
let sgp = py.allow_threads(|| {
GpMixture::params()
.gp_type(GpType::SparseGp {
sparse_method: method,
inducings,
})
.correlation_spec(
egobox_moe::CorrelationSpec::from_bits(self.correlation_spec.0).unwrap(),
)
.theta_tunings(&theta_tunings)
.kpls_dim(self.kpls_dim)
.n_start(self.n_start)
.with_rng(rng)
.fit(&dataset)
.expect("Sgp model training")
});
SparseGpx(Box::new(sgp))
}
}
#[gen_stub_pyclass]
#[pyclass]
pub(crate) struct SparseGpx(Box<GpMixture>);
#[gen_stub_pymethods]
#[pymethods]
impl SparseGpx {
#[staticmethod]
#[pyo3(signature = (
corr_spec = CorrelationSpec::SQUARED_EXPONENTIAL,
theta_init = None,
theta_bounds = None,
kpls_dim = None,
n_start = 10,
nz = None,
z = None,
method = SparseMethod::Fitc,
seed = None
))]
#[allow(clippy::too_many_arguments)]
fn builder(
corr_spec: u8,
theta_init: Option<Vec<f64>>,
theta_bounds: Option<Vec<Vec<f64>>>,
kpls_dim: Option<usize>,
n_start: usize,
nz: Option<usize>,
z: Option<PyReadonlyArray2<f64>>,
method: SparseMethod,
seed: Option<u64>,
) -> SparseGpMix {
SparseGpMix::new(
corr_spec,
theta_init,
theta_bounds,
kpls_dim,
n_start,
nz,
z,
method,
seed,
)
}
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) -> SparseGpx {
let format = match Path::new(&filename).extension().unwrap().to_str().unwrap() {
"json" => egobox_moe::GpFileFormat::Json,
_ => egobox_moe::GpFileFormat::Binary,
};
SparseGpx(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().to_owned())
.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 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)
}
}