1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
use crate::types::*;
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::{PyAny, PyDict};
use pyo3_stub_gen::derive::gen_stub_pyclass;
/// GP configuration used by `Egor` and `GpMix`
#[gen_stub_pyclass]
#[pyclass(skip_from_py_object)]
#[derive(Clone, Debug)]
pub(crate) struct GpConfig {
/// (RegressionSpec flags, an int in [1, 7])
/// Specification of regression models used in mixture.
/// Can be RegressionSpec.CONSTANT (1), RegressionSpec.LINEAR (2), RegressionSpec.QUADRATIC (4) or
/// any bit-wise union of these values (e.g. RegressionSpec.CONSTANT | RegressionSpec.LINEAR)
#[pyo3(get, set)]
pub regr_spec: u8,
/// (CorrelationSpec flags, an int in [1, 15])
/// Specification of correlation models used in mixture.
/// Can be CorrelationSpec.SQUARED_EXPONENTIAL (1), CorrelationSpec.ABSOLUTE_EXPONENTIAL (2),
/// CorrelationSpec.MATERN32 (4), CorrelationSpec.MATERN52 (8) or
/// any bit-wise union of these values (e.g. CorrelationSpec.MATERN32 | CorrelationSpec.MATERN52)
#[pyo3(get, set)]
pub corr_spec: u8,
/// (0 < int < nx where nx is the dimension of inputs x)
/// Number of components to be used when PLS projection is used (a.k.a KPLS method).
/// This is used to address high-dimensional problems typically when nx > 9.
#[pyo3(get, set)]
pub kpls_dim: Option<usize>,
/// (int)
/// Number of clusters used by the mixture of surrogate experts (default is 1).
/// When set to 0, the number of cluster is determined automatically and refreshed every
/// 10-points addition (should say 'tentative addition' because addition may fail for some points
/// but it is counted anyway).
/// When set to negative number -n, the number of clusters is determined automatically in [1, n]
/// this is used to limit the number of trials hence the execution time.
#[pyo3(get, set)]
pub n_clusters: isize,
/// (Recombination.Smooth or Recombination.Hard (default))
/// Specify how the various experts predictions are recombined
/// * Smooth: prediction is a combination of experts prediction wrt their responsabilities,
/// the heaviside factor which controls steepness of the change between experts regions is optimized
/// to get best mixture quality.
/// * Hard: prediction is taken from the expert with highest responsability
/// resulting in a model with discontinuities.
#[pyo3(get, set)]
pub recombination: Recombination,
/// ([nx] where nx is the dimension of inputs x)
/// Initial guess for GP theta hyperparameters.
/// When None the default is 1e-1 for all components
#[pyo3(get, set)]
pub theta_init: Option<Vec<f64>>,
/// ([[lower_1, upper_1], ..., [lower_nx, upper_nx]] where nx is the dimension of inputs x)
/// Space search when optimizing theta GP hyperparameters
/// When None the default is [1e-2, 1e1] for all components.
/// Note: `Egor` may adapt these bounds automatically for high-dimensional inputs.
#[pyo3(get, set)]
pub theta_bounds: Option<Vec<Vec<f64>>>,
/// (int >= 0)
/// Number of internal GP hyperpameters optimization restart (multistart)
/// When zero, optimization is disabled and theta init value is used as is.
#[pyo3(get, set)]
pub n_start: usize,
/// (int >= 0)
/// Max number of likelihood evaluations during GP hyperparameters optimization
#[pyo3(get, set)]
pub max_eval: usize,
}
impl<'a, 'py> FromPyObject<'a, 'py> for GpConfig {
type Error = PyErr;
fn extract(obj: pyo3::Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
if let Ok(cfg) = obj.extract::<PyRef<'py, Self>>() {
return Ok(cfg.clone());
}
let dict = obj.cast::<PyDict>()?;
let mut cfg = GpConfig::default();
for key_any in dict.keys().iter() {
let key = key_any.extract::<String>()?;
match key.as_str() {
"regr_spec" => cfg.regr_spec = dict.get_item("regr_spec")?.unwrap().extract()?,
"corr_spec" => cfg.corr_spec = dict.get_item("corr_spec")?.unwrap().extract()?,
"kpls_dim" => cfg.kpls_dim = dict.get_item("kpls_dim")?.unwrap().extract()?,
"n_clusters" => cfg.n_clusters = dict.get_item("n_clusters")?.unwrap().extract()?,
"recombination" => {
cfg.recombination = dict.get_item("recombination")?.unwrap().extract()?
}
"theta_init" => cfg.theta_init = dict.get_item("theta_init")?.unwrap().extract()?,
"theta_bounds" => {
cfg.theta_bounds = dict.get_item("theta_bounds")?.unwrap().extract()?
}
"n_start" => cfg.n_start = dict.get_item("n_start")?.unwrap().extract()?,
"max_eval" => cfg.max_eval = dict.get_item("max_eval")?.unwrap().extract()?,
_ => {
return Err(PyValueError::new_err(format!(
"unknown gp_config key '{key}'"
)));
}
}
}
Ok(cfg)
}
}
impl Default for GpConfig {
fn default() -> Self {
GpConfig::new(
RegressionSpec::CONSTANT,
CorrelationSpec::SQUARED_EXPONENTIAL,
None,
1,
Recombination::Hard,
None,
None,
egobox_ego::EGO_GP_OPTIM_N_START,
egobox_ego::EGO_GP_OPTIM_MAX_EVAL,
)
}
}
#[pymethods]
impl GpConfig {
#[new]
#[pyo3(signature = (
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,
))]
#[allow(clippy::too_many_arguments)]
pub fn new(
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,
) -> Self {
GpConfig {
regr_spec,
corr_spec,
kpls_dim,
n_clusters,
recombination,
theta_init,
theta_bounds,
n_start,
max_eval,
}
}
}