Skip to content

Commit 0116af0

Browse files
authored
Fix Egor service in presence of discrete variables (#119)
* Check MoeParams not used with discrete * Make unfold function public * Add doc comment * Fix bug in unfold_with_enum_mask * Make suggest work in folded space * Manage LHS optim failure due to bad surrogate * Rename new_with_xtypes * Improve comments * Renaming to_xtypes * Keep only EgorSolver::new constructor with xtypes * Refactor no_discrete * Remove duplication
1 parent e97be19 commit 0116af0

8 files changed

+99
-89
lines changed

ego/src/egor.rs

+11-5
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,13 @@ impl<O: GroupFunc> EgorBuilder<O> {
142142
} else {
143143
Xoshiro256Plus::from_entropy()
144144
};
145+
let config = EgorConfig {
146+
xtypes: to_xtypes(xlimits),
147+
..self.config.clone()
148+
};
145149
Egor {
146150
fobj: ObjFunc::new(self.fobj),
147-
solver: EgorSolver::new(self.config, xlimits, rng),
151+
solver: EgorSolver::new(config, rng),
148152
}
149153
}
150154

@@ -157,9 +161,13 @@ impl<O: GroupFunc> EgorBuilder<O> {
157161
} else {
158162
Xoshiro256Plus::from_entropy()
159163
};
164+
let config = EgorConfig {
165+
xtypes: xtypes.into(),
166+
..self.config.clone()
167+
};
160168
Egor {
161169
fobj: ObjFunc::new(self.fobj),
162-
solver: EgorSolver::new_with_xtypes(self.config, xtypes, rng),
170+
solver: EgorSolver::new(config, rng),
163171
}
164172
}
165173
}
@@ -189,14 +197,13 @@ impl<O: GroupFunc, SB: SurrogateBuilder> Egor<O, SB> {
189197

190198
/// Runs the (constrained) optimization of the objective function.
191199
pub fn run(&self) -> Result<OptimResult<f64>> {
192-
let no_discrete = self.solver.config.no_discrete;
193200
let xtypes = self.solver.config.xtypes.clone();
194201

195202
let result = Executor::new(self.fobj.clone(), self.solver.clone()).run()?;
196203
info!("{}", result);
197204
let (x_data, y_data) = result.state().clone().take_data().unwrap();
198205

199-
let res = if no_discrete {
206+
let res = if !self.solver.config.discrete() {
200207
info!("History: \n{}", concatenate![Axis(1), x_data, y_data]);
201208
OptimResult {
202209
x_opt: result.state.get_best_param().unwrap().to_owned(),
@@ -205,7 +212,6 @@ impl<O: GroupFunc, SB: SurrogateBuilder> Egor<O, SB> {
205212
y_hist: y_data,
206213
}
207214
} else {
208-
let xtypes = xtypes.unwrap(); // !no_discrete
209215
let x_data = cast_to_discrete_values(&xtypes, &x_data);
210216
let x_data = fold_with_enum_index(&xtypes, &x_data.view());
211217
info!("History: \n{}", concatenate![Axis(1), x_data, y_data]);

ego/src/egor_config.rs

+13-5
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,7 @@ pub struct EgorConfig {
5454
/// If true use `outdir` to retrieve and start from previous results
5555
pub(crate) hot_start: bool,
5656
/// List of x types allowing the handling of discrete input variables
57-
pub(crate) xtypes: Option<Vec<XType>>,
58-
/// Flag for discrete handling, true if mixed-integer type present in xtypes, otherwise false
59-
pub(crate) no_discrete: bool,
57+
pub(crate) xtypes: Vec<XType>,
6058
/// A random generator seed used to get reproductible results.
6159
pub(crate) seed: Option<u64>,
6260
}
@@ -81,8 +79,7 @@ impl Default for EgorConfig {
8179
target: f64::NEG_INFINITY,
8280
outdir: None,
8381
hot_start: false,
84-
xtypes: None,
85-
no_discrete: true,
82+
xtypes: vec![],
8683
seed: None,
8784
}
8885
}
@@ -237,4 +234,15 @@ impl EgorConfig {
237234
self.seed = Some(seed);
238235
self
239236
}
237+
238+
/// Define design space with given x types
239+
pub fn xtypes(mut self, xtypes: &[XType]) -> Self {
240+
self.xtypes = xtypes.into();
241+
self
242+
}
243+
244+
/// Check whether we are in a discrete optimization context
245+
pub fn discrete(&self) -> bool {
246+
crate::utils::discrete(&self.xtypes)
247+
}
240248
}

ego/src/egor_service.rs

+15-11
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,12 @@ impl EgorServiceBuilder {
8686
} else {
8787
Xoshiro256Plus::from_entropy()
8888
};
89+
let config = EgorConfig {
90+
xtypes: to_xtypes(xlimits),
91+
..self.config.clone()
92+
};
8993
EgorService {
90-
config: self.config.clone(),
91-
solver: EgorSolver::new(self.config, xlimits, rng),
94+
solver: EgorSolver::new(config, rng),
9295
}
9396
}
9497

@@ -101,26 +104,23 @@ impl EgorServiceBuilder {
101104
} else {
102105
Xoshiro256Plus::from_entropy()
103106
};
107+
let config = EgorConfig {
108+
xtypes: xtypes.to_vec(),
109+
..self.config.clone()
110+
};
104111
EgorService {
105-
config: self.config.clone(),
106-
solver: EgorSolver::new_with_xtypes(self.config, xtypes, rng),
112+
solver: EgorSolver::new(config, rng),
107113
}
108114
}
109115
}
110116

111117
/// Egor optimizer service.
112118
#[derive(Clone)]
113119
pub struct EgorService<SB: SurrogateBuilder> {
114-
config: EgorConfig,
115120
solver: EgorSolver<SB>,
116121
}
117122

118123
impl<SB: SurrogateBuilder> EgorService<SB> {
119-
pub fn configure<F: FnOnce(EgorConfig) -> EgorConfig>(mut self, init: F) -> Self {
120-
self.config = init(self.config);
121-
self
122-
}
123-
124124
/// Given an evaluated doe (x, y) data, return the next promising x point
125125
/// where optimum may occurs regarding the infill criterium.
126126
/// This function inverse the control of the optimization and can used
@@ -130,7 +130,11 @@ impl<SB: SurrogateBuilder> EgorService<SB> {
130130
x_data: &ArrayBase<impl Data<Elem = f64>, Ix2>,
131131
y_data: &ArrayBase<impl Data<Elem = f64>, Ix2>,
132132
) -> Array2<f64> {
133-
self.solver.suggest(x_data, y_data)
133+
let xtypes = &self.solver.config.xtypes;
134+
let x_data = unfold_with_enum_mask(xtypes, x_data);
135+
let x = self.solver.suggest(&x_data, y_data);
136+
let x = cast_to_discrete_values(xtypes, &x);
137+
fold_with_enum_index(xtypes, &x).to_owned()
134138
}
135139
}
136140

ego/src/egor_solver.rs

+29-49
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
//! ```no_run
99
//! use ndarray::{array, Array2, ArrayView1, ArrayView2, Zip};
1010
//! use egobox_doe::{Lhs, SamplingMethod};
11-
//! use egobox_ego::{EgorBuilder, EgorConfig, InfillStrategy, InfillOptimizer, ObjFunc, EgorSolver};
11+
//! use egobox_ego::{EgorBuilder, EgorConfig, InfillStrategy, InfillOptimizer, ObjFunc, EgorSolver, to_xtypes};
1212
//! use egobox_moe::MoeParams;
1313
//! use rand_xoshiro::Xoshiro256Plus;
1414
//! use ndarray_rand::rand::SeedableRng;
@@ -25,10 +25,10 @@
2525
//! y
2626
//! }
2727
//! let rng = Xoshiro256Plus::seed_from_u64(42);
28-
//! let xlimits = array![[-2., 2.], [-2., 2.]];
28+
//! let xtypes = to_xtypes(&array![[-2., 2.], [-2., 2.]]);
2929
//! let fobj = ObjFunc::new(rosenb);
30-
//! let config = EgorConfig::default();
31-
//! let solver: EgorSolver<MoeParams<f64, Xoshiro256Plus>> = EgorSolver::new(config, &xlimits, rng);
30+
//! let config = EgorConfig::default().xtypes(&xtypes);
31+
//! let solver: EgorSolver<MoeParams<f64, Xoshiro256Plus>> = EgorSolver::new(config, rng);
3232
//! let res = Executor::new(fobj, solver)
3333
//! .configure(|state| state.max_iters(20))
3434
//! .run()
@@ -47,7 +47,7 @@
4747
//! ```no_run
4848
//! use ndarray::{array, Array2, ArrayView1, ArrayView2, Zip};
4949
//! use egobox_doe::{Lhs, SamplingMethod};
50-
//! use egobox_ego::{EgorBuilder, EgorConfig, InfillStrategy, InfillOptimizer, ObjFunc, EgorSolver};
50+
//! use egobox_ego::{EgorBuilder, EgorConfig, InfillStrategy, InfillOptimizer, ObjFunc, EgorSolver, to_xtypes};
5151
//! use egobox_moe::MoeParams;
5252
//! use rand_xoshiro::Xoshiro256Plus;
5353
//! use ndarray_rand::rand::SeedableRng;
@@ -83,18 +83,20 @@
8383
//! let rng = Xoshiro256Plus::seed_from_u64(42);
8484
//! let xlimits = array![[0., 3.], [0., 4.]];
8585
//! let doe = Lhs::new(&xlimits).sample(10);
86+
//! let xtypes = to_xtypes(&xlimits);
8687
//!
8788
//! let fobj = ObjFunc::new(f_g24);
8889
//!
8990
//! let config = EgorConfig::default()
91+
//! .xtypes(&xtypes)
9092
//! .n_cstr(2)
9193
//! .infill_strategy(InfillStrategy::EI)
9294
//! .infill_optimizer(InfillOptimizer::Cobyla)
9395
//! .doe(&doe)
9496
//! .target(-5.5080);
9597
//!
9698
//! let solver: EgorSolver<MoeParams<f64, Xoshiro256Plus>> =
97-
//! EgorSolver::new(config, &xlimits, rng);
99+
//! EgorSolver::new(config, rng);
98100
//!
99101
//! let res = Executor::new(fobj, solver)
100102
//! .configure(|state| state.max_iters(40))
@@ -112,7 +114,7 @@ use crate::mixint::*;
112114
use crate::optimizer::*;
113115

114116
use crate::types::*;
115-
use crate::utils::{compute_cstr_scales, no_discrete, update_data};
117+
use crate::utils::{compute_cstr_scales, update_data};
116118

117119
use egobox_doe::{Lhs, LhsKind, SamplingMethod};
118120
use egobox_moe::{ClusteredSurrogate, Clustering, CorrelationSpec, MoeParams, RegressionSpec};
@@ -163,7 +165,12 @@ pub struct EgorSolver<SB: SurrogateBuilder> {
163165
}
164166

165167
impl SurrogateBuilder for MoeParams<f64, Xoshiro256Plus> {
166-
fn new_with_xtypes_rng(_xtypes: &[XType]) -> Self {
168+
/// Constructor from domain space specified with types
169+
/// **panic** if xtypes contains other types than continuous type `Float`
170+
fn new_with_xtypes(xtypes: &[XType]) -> Self {
171+
if crate::utils::discrete(xtypes) {
172+
panic!("MoeParams cannot be created with discrete types!");
173+
}
167174
MoeParams::new()
168175
}
169176

@@ -213,49 +220,18 @@ impl<SB: SurrogateBuilder> EgorSolver<SB> {
213220
/// Constructor of the optimization of the function `f` with specified random generator
214221
/// to get reproducibility.
215222
///
216-
/// The function `f` should return an objective value but also constraint values if any.
217-
/// Design space is specified by the matrix `xlimits` which is `[nx, 2]`-shaped
218-
/// the ith row contains lower and upper bounds of the ith component of `x`.
219-
pub fn new(
220-
config: EgorConfig,
221-
xlimits: &ArrayBase<impl Data<Elem = f64>, Ix2>,
222-
rng: Xoshiro256Plus,
223-
) -> Self {
224-
let env = Env::new().filter_or("EGOBOX_LOG", "info");
225-
let mut builder = Builder::from_env(env);
226-
let builder = builder.target(env_logger::Target::Stdout);
227-
builder.try_init().ok();
228-
EgorSolver {
229-
config: EgorConfig {
230-
xtypes: Some(continuous_xlimits_to_xtypes(xlimits)), // align xlimits and xtypes
231-
..config
232-
},
233-
xlimits: xlimits.to_owned(),
234-
surrogate_builder: SB::new_with_xtypes_rng(&continuous_xlimits_to_xtypes(xlimits)),
235-
rng,
236-
}
237-
}
238-
239-
/// Constructor of the optimization of the function `f` with specified random generator
240-
/// to get reproducibility. This constructor is used for mixed-integer optimization
241-
/// when `f` has discrete inputs to be specified with list of xtypes.
242-
///
243223
/// The function `f` should return an objective but also constraint values if any.
244224
/// Design space is specified by a list of types for input variables `x` of `f` (see [`XType`]).
245-
pub fn new_with_xtypes(config: EgorConfig, xtypes: &[XType], rng: Xoshiro256Plus) -> Self {
225+
pub fn new(config: EgorConfig, rng: Xoshiro256Plus) -> Self {
246226
let env = Env::new().filter_or("EGOBOX_LOG", "info");
247227
let mut builder = Builder::from_env(env);
248228
let builder = builder.target(env_logger::Target::Stdout);
249229
builder.try_init().ok();
250-
let v_xtypes = xtypes.to_vec();
230+
let xtypes = config.xtypes.clone();
251231
EgorSolver {
252-
config: EgorConfig {
253-
xtypes: Some(v_xtypes),
254-
no_discrete: no_discrete(xtypes),
255-
..config
256-
},
257-
xlimits: unfold_xtypes_as_continuous_limits(xtypes),
258-
surrogate_builder: SB::new_with_xtypes_rng(xtypes),
232+
config,
233+
xlimits: unfold_xtypes_as_continuous_limits(&xtypes),
234+
surrogate_builder: SB::new_with_xtypes(&xtypes),
259235
rng,
260236
}
261237
}
@@ -294,7 +270,7 @@ impl<SB: SurrogateBuilder> EgorSolver<SB> {
294270
/// Build `xtypes` from simple float bounds of `x` input components when x belongs to R^n.
295271
/// xlimits are bounds of the x components expressed a matrix (dim, 2) where dim is the dimension of x
296272
/// the ith row is the bounds interval [lower, upper] of the ith comonent of `x`.
297-
fn continuous_xlimits_to_xtypes(xlimits: &ArrayBase<impl Data<Elem = f64>, Ix2>) -> Vec<XType> {
273+
pub fn to_xtypes(xlimits: &ArrayBase<impl Data<Elem = f64>, Ix2>) -> Vec<XType> {
298274
let mut xtypes: Vec<XType> = vec![];
299275
Zip::from(xlimits.rows()).for_each(|limits| xtypes.push(XType::Cont(limits[0], limits[1])));
300276
xtypes
@@ -336,10 +312,12 @@ where
336312
let doe = hstart_doe.as_ref().or(self.config.doe.as_ref());
337313

338314
let (y_data, x_data) = if let Some(doe) = doe {
315+
let doe = unfold_with_enum_mask(&self.config.xtypes, doe);
316+
339317
if doe.ncols() == self.xlimits.nrows() {
340318
// only x are specified
341319
info!("Compute initial DOE on specified {} points", doe.nrows());
342-
(self.eval_obj(problem, doe), doe.to_owned())
320+
(self.eval_obj(problem, &doe), doe.to_owned())
343321
} else {
344322
// split doe in x and y
345323
info!("Use specified DOE {} samples", doe.nrows());
@@ -991,9 +969,11 @@ where
991969
pb: &mut Problem<O>,
992970
x: &Array2<f64>,
993971
) -> Array2<f64> {
994-
let params = if let Some(xtypes) = &self.config.xtypes {
995-
let xcast = cast_to_discrete_values(xtypes, x);
996-
fold_with_enum_index(xtypes, &xcast.view())
972+
let params = if self.config.discrete() {
973+
// When xtypes is specified, we have to cast x to folded space
974+
// as EgorSolver works internally in the continuous space
975+
let xcast = cast_to_discrete_values(&self.config.xtypes, x);
976+
fold_with_enum_index(&self.config.xtypes, &xcast.view())
997977
} else {
998978
x.to_owned()
999979
};

ego/src/lhs_optimizer.rs

+12-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use egobox_doe::{Lhs, LhsKind, SamplingMethod};
33
use ndarray::{Array1, Array2, Axis, Zip};
44
use ndarray_rand::rand::{Rng, SeedableRng};
55
use rand_xoshiro::Xoshiro256Plus;
6-
use rayon::prelude::*;
6+
//use rayon::prelude::*;
77

88
#[cfg(not(feature = "blas"))]
99
use linfa_linalg::norm::*;
@@ -114,9 +114,17 @@ impl<'a, R: Rng + Clone + Sync + Send> LhsOptimizer<'a, R> {
114114
})
115115
.collect();
116116
let values = Array1::from_vec(vals.iter().map(|(_, y, _)| *y).collect());
117-
let index_min = values
118-
.argmin()
119-
.unwrap_or_else(|err| panic!("Cannot find min in {}: {:?}", values, err));
117+
let index_min = values.argmin().unwrap_or_else(|err| {
118+
log::error!(
119+
"LHS optimization failed! Cannot find minimum in {} (Error: {})",
120+
values,
121+
err
122+
);
123+
if values.is_empty() {
124+
log::error!("No valid output value maybe due to ill-formed surrogate models.");
125+
}
126+
panic!("Optimization Aborted!")
127+
});
120128
(
121129
true,
122130
vals[index_min].0.to_owned(),
@@ -142,7 +150,6 @@ impl<'a, R: Rng + Clone + Sync + Send> LhsOptimizer<'a, R> {
142150

143151
// Make n_start optim
144152
let x_optims = (0..self.n_start)
145-
.into_par_iter()
146153
.map(|_| self.find_lhs_min(lhs.clone()))
147154
.collect::<Vec<_>>();
148155

0 commit comments

Comments
 (0)