Repository: relf/egobox (language: python)

Issue:
Relation nb of iterations vs nb of added points
Depending on the settings of the Egor algorithm (specially the use of q parallel points or the use of TREGO method) the relation between iterations and added points is not one to one. At the end when DOE points file saving is enabled, it (ie also called DOE here) does not allow to retrieve the number of iteration as well as the iteration number of a specific added point.

This information is required to display accurate minimum vs iteration graph as a postprocessing.


Discussion / hints:

Fix patch:
diff --git a/ego/src/egor.rs b/ego/src/egor.rs
index 5a4fd658..9137f6ce 100644
--- a/ego/src/egor.rs
+++ b/ego/src/egor.rs
@@ -246,7 +246,7 @@ mod tests {
     use serial_test::serial;
     use std::time::Instant;
 
-    use crate::{gpmix::spec::*, DOE_FILE, DOE_INITIAL_FILE};
+    use crate::{gpmix::spec::*, DOE_FILE};
 
     #[cfg(not(feature = "blas"))]
     use linfa_linalg::norm::*;
@@ -261,7 +261,7 @@ mod tests {
     #[serial]
     fn test_xsinx_ei_quadratic_egor_builder() {
         let outdir = "target/test_egor_builder_01";
-        let outfile = format!("{outdir}/{DOE_INITIAL_FILE}");
+        let outfile = format!("{outdir}/{DOE_FILE}");
         let _ = std::fs::remove_file(&outfile);
         let initial_doe = array![[0.], [7.], [25.]];
         let res = EgorBuilder::optimize(xsinx)
@@ -281,7 +281,7 @@ mod tests {
         assert_abs_diff_eq!(expected, res.y_opt, epsilon = 0.5);
         let saved_doe: Array2<f64> = read_npy(&outfile).unwrap();
         let _ = std::fs::remove_file(&outfile);
-        assert_abs_diff_eq!(initial_doe, saved_doe.slice(s![..3, ..1]), epsilon = 1e-6);
+        assert_abs_diff_eq!(initial_doe, saved_doe.slice(s![..3, 1..2]), epsilon = 1e-6);
     }
 
     #[test]
@@ -330,7 +330,6 @@ mod tests {
     #[serial]
     fn test_xsinx_with_hotstart_egor_builder() {
         let outdir = "target/test_hotstart_01";
-        let _ = std::fs::remove_file(format!("{outdir}/{DOE_INITIAL_FILE}"));
         let _ = std::fs::remove_file(format!("{outdir}/{DOE_FILE}"));
         let xlimits = array![[0.0, 25.0]];
         let doe = array![[0.], [7.], [23.]];
@@ -352,7 +351,6 @@ mod tests {
         let doe2: Array2<f64> = read_npy(&filepath).expect("file read");
         assert_eq!(doe2.nrows(), doe.nrows() + 3);
 
-        let _ = std::fs::remove_file(format!("{outdir}/{DOE_INITIAL_FILE}"));
         let _ = std::fs::remove_file(format!("{outdir}/{DOE_FILE}"));
         let expected = array![18.9];
         assert_abs_diff_eq!(expected, res.x_opt, epsilon = 1e-1);
@@ -397,7 +395,7 @@ mod tests {
     #[serial]
     fn test_rosenbrock_2d_no_trego_egor_builder() {
         let outdir = "target/test_trego";
-        let _ = std::fs::remove_file(format!("{outdir}/{DOE_INITIAL_FILE}"));
+
         let _ = std::fs::remove_file(format!("{outdir}/{DOE_FILE}"));
         let now = Instant::now();
         let xlimits = array![[-2., 2.], [-2., 2.]];
@@ -642,9 +640,8 @@ mod tests {
         builder.try_init().ok();
 
         let outdir = "target/test_hotstart_02";
-        let outfile = format!("{outdir}/{DOE_INITIAL_FILE}");
-        let _ = std::fs::remove_file(format!("{outdir}/{DOE_INITIAL_FILE}"));
-        let _ = std::fs::remove_file(format!("{outdir}/{DOE_FILE}"));
+        let outfile = format!("{outdir}/{DOE_FILE}");
+        let _ = std::fs::remove_file(&outfile);
 
         let xtypes = vec![
             XType::Cont(-5., 5.),
@@ -662,7 +659,7 @@ mod tests {
 
         // Check saved DOE has continuous (unfolded) dimension + one output
         let saved_doe: Array2<f64> = read_npy(outfile).unwrap();
-        assert_eq!(saved_doe.shape()[1], xlimits.nrows() + 1);
+        assert_eq!(saved_doe.shape()[1], xlimits.nrows() + 2); // row: [n_iter, x, y]
 
         // Check that with no iteration, obj function is never called
         // as the DOE does not need to be evaluated!
diff --git a/ego/src/solver/egor_solver.rs b/ego/src/solver/egor_solver.rs
index 514fbbfb..ac39d6a7 100644
--- a/ego/src/solver/egor_solver.rs
+++ b/ego/src/solver/egor_solver.rs
@@ -123,8 +123,6 @@ use rand_xoshiro::Xoshiro256Plus;
 use serde::{Deserialize, Serialize};
 use std::time::Instant;
 
-/// Numpy filename for initial DOE dump
-pub const DOE_INITIAL_FILE: &str = "egor_initial_doe.npy";
 /// Numpy Filename for current DOE dump
 pub const DOE_FILE: &str = "egor_doe.npy";
 
@@ -179,11 +177,11 @@ where
                 let filepath = std::path::Path::new(&path).join(DOE_FILE);
                 if filepath.is_file() {
                     info!("Reading DOE from {:?}", filepath);
-                    Some(read_npy(filepath)?)
-                } else if std::path::Path::new(&path).join(DOE_INITIAL_FILE).is_file() {
-                    let filepath = std::path::Path::new(&path).join(DOE_INITIAL_FILE);
-                    info!("Reading DOE from {:?}", filepath);
-                    Some(read_npy(filepath)?)
+                    Some(
+                        read_npy::<_, Array2<f64>>(filepath)?
+                            .slice(s![.., 1..]) // remove iternb first column
+                            .to_owned(),
+                    )
                 } else {
                     None
                 }
@@ -194,7 +192,9 @@ where
         let doe = hstart_doe.as_ref().or(self.config.doe.as_ref());
 
         let (y_data, x_data) = if let Some(doe) = doe {
-            if doe.ncols() == self.xlimits.nrows() {
+            let xend = self.xlimits.nrows(); // x index end
+            println!("DOE={}", doe);
+            if doe.ncols() == xend {
                 // only x are specified
                 info!("Compute initial DOE on specified {} points", doe.nrows());
                 (self.eval_obj(problem, doe), doe.to_owned())
@@ -202,8 +202,8 @@ where
                 // split doe in x and y
                 info!("Use specified DOE {} samples", doe.nrows());
                 (
-                    doe.slice(s![.., self.xlimits.nrows()..]).to_owned(),
-                    doe.slice(s![.., ..self.xlimits.nrows()]).to_owned(),
+                    doe.slice(s![.., xend..]).to_owned(),
+                    doe.slice(s![.., ..xend]).to_owned(),
                 )
             }
         } else {
@@ -216,11 +216,15 @@ where
             let x = sampling.sample(n_doe);
             (self.eval_obj(problem, &x), x)
         };
-        let doe = concatenate![Axis(1), x_data, y_data];
+
+        // DOE row format: [iter, x, y] where y = f_obj, cstr1, ..., cstrn
+        let iters = Array2::zeros((x_data.nrows(), 1));
+        let doe = concatenate![Axis(1), iters, x_data, y_data];
+
         if self.config.outdir.is_some() {
             let path = self.config.outdir.as_ref().unwrap();
             std::fs::create_dir_all(path)?;
-            let filepath = std::path::Path::new(path).join(DOE_INITIAL_FILE);
+            let filepath = std::path::Path::new(path).join(DOE_FILE);
             info!("Save initial doe shape {:?} in {:?}", doe.shape(), filepath);
             write_npy(filepath, &doe).expect("Write initial doe");
         }
@@ -272,8 +276,22 @@ where
         let (x_data, y_data) = res.0.data.clone().unwrap();
 
         if self.config.outdir.is_some() {
-            let doe = concatenate![Axis(1), x_data, y_data];
-            let path = self.config.outdir.as_ref().unwrap();
+            // DOE row format: [iter, x, y] where y = f_obj, cstr1, ..., cstrn
+            let path: &String = self.config.outdir.as_ref().unwrap();
+            let filepath = std::path::Path::new(&path).join(DOE_FILE);
+            let prev_doe: Array2<f64> = read_npy(filepath)?;
+
+            let mut iters = Array1::zeros(x_data.nrows());
+            let added = x_data.nrows() - prev_doe.nrows();
+            iters
+                .slice_mut(s![..prev_doe.nrows()])
+                .assign(&prev_doe.column(0));
+            let new_iters = Array1::from_vec(vec![(res.0.get_iter() + 1) as f64; added]);
+            iters.slice_mut(s![prev_doe.nrows()..]).assign(&new_iters);
+            let iters = iters.into_shape((x_data.nrows(), 1)).unwrap();
+
+            let doe = concatenate![Axis(1), iters, x_data, y_data];
+            //let path = self.config.outdir.as_ref().unwrap();
             std::fs::create_dir_all(path)?;
             let filepath = std::path::Path::new(path).join(DOE_FILE);
             info!("Save doe shape {:?} in {:?}", doe.shape(), filepath);


Test patch:
diff --git a/python/egobox/tests/test_egor.py b/python/egobox/tests/test_egor.py
index 16021186..0fbada44 100644
--- a/python/egobox/tests/test_egor.py
+++ b/python/egobox/tests/test_egor.py
@@ -87,8 +87,6 @@ def test_xsinx_with_reclustering(self):
         self.assertAlmostEqual(18.935, res.x_opt[0], delta=1e-3)
 
     def test_xsinx_with_hotstart(self):
-        if os.path.exists("./test_dir/egor_initial_doe.npy"):
-            os.remove("./test_dir/egor_initial_doe.npy")
         if os.path.exists("./test_dir/egor_doe.npy"):
             os.remove("./test_dir/egor_doe.npy")
         xlimits = egx.to_specs([[0.0, 25.0]])
@@ -105,8 +103,6 @@ def test_xsinx_with_hotstart(self):
         self.assertAlmostEqual(-15.125, res.y_opt[0], delta=1e-2)
         self.assertAlmostEqual(18.935, res.x_opt[0], delta=1e-2)
 
-        self.assertTrue(os.path.exists("./test_dir/egor_initial_doe.npy"))
-        os.remove("./test_dir/egor_initial_doe.npy")
         self.assertTrue(os.path.exists("./test_dir/egor_doe.npy"))
         os.remove("./test_dir/egor_doe.npy")