1use scirs2_core::ndarray::{Array1, Array2};
61use scirs2_core::random::rngs::StdRng;
62use scirs2_core::random::{Rng, RngExt, SeedableRng};
63
64use crate::error::{OptimizeError, OptimizeResult};
65
66use super::gp::{GpSurrogate, GpSurrogateConfig, RbfKernel};
67use super::sampling::{generate_samples, SamplingConfig, SamplingStrategy};
68
69#[derive(Debug, Clone)]
75pub struct FidelityLevel {
76 pub cost: f64,
78 pub noise: f64,
80 pub correlation: f64,
83}
84
85pub struct AutoRegressiveGp {
94 gps: Vec<GpSurrogate>,
96 rhos: Vec<f64>,
98 data: Vec<(Array2<f64>, Array1<f64>)>,
100 levels: Vec<FidelityLevel>,
102}
103
104impl AutoRegressiveGp {
105 pub fn new(levels: &[FidelityLevel]) -> Self {
107 let n = levels.len();
108 let gps = (0..n)
109 .map(|i| {
110 let noise = levels[i].noise.max(1e-8);
111 let gp_config = GpSurrogateConfig {
112 noise_variance: noise,
113 optimize_hyperparams: true,
114 n_restarts: 2,
115 max_opt_iters: 30,
116 };
117 GpSurrogate::new(Box::new(RbfKernel::default()), gp_config)
118 })
119 .collect();
120
121 let rhos = levels
122 .iter()
123 .map(|l| l.correlation.clamp(-5.0, 5.0))
124 .collect();
125
126 Self {
127 gps,
128 rhos,
129 data: vec![(Array2::zeros((0, 1)), Array1::zeros(0)); n],
130 levels: levels.to_vec(),
131 }
132 }
133
134 pub fn fit(&mut self, x_list: &[Array2<f64>], y_list: &[Array1<f64>]) -> OptimizeResult<()> {
139 if x_list.len() != self.levels.len() || y_list.len() != self.levels.len() {
140 return Err(OptimizeError::InvalidInput(format!(
141 "Expected {} fidelity levels, got x_list={} y_list={}",
142 self.levels.len(),
143 x_list.len(),
144 y_list.len()
145 )));
146 }
147
148 for (l, (xl, yl)) in x_list.iter().zip(y_list.iter()).enumerate() {
149 if xl.nrows() != yl.len() {
150 return Err(OptimizeError::InvalidInput(format!(
151 "Level {}: x has {} rows but y has {} elements",
152 l,
153 xl.nrows(),
154 yl.len()
155 )));
156 }
157 self.data[l] = (xl.clone(), yl.clone());
158 }
159
160 if !x_list[0].is_empty() {
162 self.gps[0].fit(&x_list[0], &y_list[0])?;
163 }
164
165 for l in 1..self.levels.len() {
167 let (xl, yl) = (&x_list[l], &y_list[l]);
168 if xl.is_empty() {
169 continue;
170 }
171
172 let rho = self.rhos[l];
174 let mut residuals = Array1::zeros(yl.len());
175 for i in 0..xl.nrows() {
176 let x_row = xl.row(i).to_owned();
177 let x_mat = x_row
178 .into_shape_with_order((1, xl.ncols()))
179 .map_err(|e| OptimizeError::ComputationError(format!("Shape: {}", e)))?;
180 let mu_prev = self.predict_mean_level(l - 1, &x_mat)?;
181 residuals[i] = yl[i] - rho * mu_prev[0];
182 }
183
184 self.gps[l].fit(xl, &residuals)?;
185 }
186
187 Ok(())
188 }
189
190 pub fn update(&mut self, fidelity: usize, x: Array1<f64>, y: f64) -> OptimizeResult<()> {
192 if fidelity >= self.levels.len() {
193 return Err(OptimizeError::InvalidInput(format!(
194 "Fidelity {} out of range (max {})",
195 fidelity,
196 self.levels.len() - 1
197 )));
198 }
199 let n_dims = x.len();
200 let (ref mut xs, ref mut ys) = self.data[fidelity];
201 let new_n = xs.nrows() + 1;
203 let mut new_x = Array2::zeros((new_n, n_dims));
204 for i in 0..xs.nrows() {
205 for j in 0..n_dims {
206 new_x[[i, j]] = xs[[i, j]];
207 }
208 }
209 for j in 0..n_dims {
210 new_x[[xs.nrows(), j]] = x[j];
211 }
212 let mut new_y = Array1::zeros(new_n);
213 for i in 0..ys.len() {
214 new_y[i] = ys[i];
215 }
216 new_y[ys.len()] = y;
217
218 *xs = new_x;
219 *ys = new_y;
220
221 let x_list: Vec<Array2<f64>> = self.data.iter().map(|(x, _)| x.clone()).collect();
223 let y_list: Vec<Array1<f64>> = self.data.iter().map(|(_, y)| y.clone()).collect();
224
225 self.fit(&x_list, &y_list)
226 }
227
228 pub fn predict(
233 &self,
234 x: &Array2<f64>,
235 fidelity: usize,
236 ) -> OptimizeResult<(Array1<f64>, Array1<f64>)> {
237 if fidelity >= self.levels.len() {
238 return Err(OptimizeError::InvalidInput(format!(
239 "Fidelity {} out of range",
240 fidelity
241 )));
242 }
243
244 let (mut mean, mut var) = self.predict_gp_level(0, x)?;
246
247 for l in 1..=fidelity {
248 let rho = self.rhos[l];
249 if self.gps[l].n_train() == 0 {
250 mean = mean.mapv(|m| rho * m);
252 var = var.mapv(|v| rho * rho * v);
253 continue;
254 }
255 let (delta_mean, delta_var) = self.predict_gp_level(l, x)?;
256 mean = mean.mapv(|m| rho * m) + &delta_mean;
257 var = var.mapv(|v| rho * rho * v) + &delta_var;
258 }
259
260 Ok((mean, var))
261 }
262
263 fn predict_gp_level(
269 &self,
270 level: usize,
271 x: &Array2<f64>,
272 ) -> OptimizeResult<(Array1<f64>, Array1<f64>)> {
273 if self.gps[level].n_train() == 0 {
274 let n = x.nrows();
275 return Ok((Array1::zeros(n), Array1::ones(n)));
276 }
277 self.gps[level].predict(x)
278 }
279
280 fn predict_mean_level(&self, level: usize, x: &Array2<f64>) -> OptimizeResult<Array1<f64>> {
282 let (mean, _) = self.predict_gp_level(level, x)?;
283 Ok(mean)
284 }
285}
286
287pub fn extended_ei(
299 x: &Array2<f64>,
300 model: &AutoRegressiveGp,
301 fidelity: usize,
302 xi: f64,
303 best_y: f64,
304) -> OptimizeResult<f64> {
305 if fidelity >= model.levels.len() {
306 return Err(OptimizeError::InvalidInput(format!(
307 "Fidelity {} out of range",
308 fidelity
309 )));
310 }
311 let cost = model.levels[fidelity].cost.max(1e-12);
312
313 let (mean, var) = model.predict(x, fidelity)?;
314 let mu = mean[0];
315 let sigma = var[0].max(0.0).sqrt();
316
317 if sigma < 1e-12 {
318 return Ok(0.0);
319 }
320
321 let z = (best_y - mu - xi) / sigma;
322 let ei = (best_y - mu - xi) * norm_cdf(z) + sigma * norm_pdf(z);
323 let ei_normalized = ei.max(0.0) / cost;
324
325 Ok(ei_normalized)
326}
327
328#[derive(Debug, Clone)]
334pub struct MultiFidelityConfig {
335 pub n_initial: usize,
337 pub xi: f64,
339 pub n_candidates: usize,
341 pub seed: Option<u64>,
343 pub verbose: usize,
345}
346
347impl Default for MultiFidelityConfig {
348 fn default() -> Self {
349 Self {
350 n_initial: 8,
351 xi: 0.01,
352 n_candidates: 300,
353 seed: None,
354 verbose: 0,
355 }
356 }
357}
358
359#[derive(Debug, Clone)]
361pub struct MultiFidelityResult {
362 pub x_best: Array1<f64>,
364 pub f_best: f64,
366 pub n_evals_per_level: Vec<usize>,
368 pub budget_spent: f64,
370 pub history: Vec<(f64, usize, f64)>,
372}
373
374pub struct MultiFidelityBo {
383 levels: Vec<FidelityLevel>,
384 bounds: Vec<(f64, f64)>,
385 config: MultiFidelityConfig,
386}
387
388impl MultiFidelityBo {
389 pub fn new(
391 levels: Vec<FidelityLevel>,
392 bounds: Vec<(f64, f64)>,
393 config: MultiFidelityConfig,
394 ) -> OptimizeResult<Self> {
395 if levels.is_empty() {
396 return Err(OptimizeError::InvalidInput(
397 "At least one fidelity level is required".to_string(),
398 ));
399 }
400 if bounds.is_empty() {
401 return Err(OptimizeError::InvalidInput(
402 "Search space bounds must not be empty".to_string(),
403 ));
404 }
405 for (i, l) in levels.iter().enumerate() {
406 if l.cost <= 0.0 {
407 return Err(OptimizeError::InvalidInput(format!(
408 "Level {} has non-positive cost {}",
409 i, l.cost
410 )));
411 }
412 }
413 Ok(Self {
414 levels,
415 bounds,
416 config,
417 })
418 }
419
420 pub fn optimize(
425 &mut self,
426 objectives: &[Box<dyn Fn(&[f64]) -> f64>],
427 budget: f64,
428 ) -> OptimizeResult<MultiFidelityResult> {
429 if objectives.len() != self.levels.len() {
430 return Err(OptimizeError::InvalidInput(format!(
431 "Expected {} objective functions, got {}",
432 self.levels.len(),
433 objectives.len()
434 )));
435 }
436 if budget <= 0.0 {
437 return Err(OptimizeError::InvalidInput(
438 "Budget must be positive".to_string(),
439 ));
440 }
441
442 let n_levels = self.levels.len();
443 let highest = n_levels - 1;
444 let mut model = AutoRegressiveGp::new(&self.levels);
445
446 let seed = self.config.seed.unwrap_or(42);
447 let mut rng = StdRng::seed_from_u64(seed);
448
449 let mut budget_spent = 0.0;
450 let mut n_evals_per_level = vec![0usize; n_levels];
451 let mut history: Vec<(f64, usize, f64)> = Vec::new();
452 let mut best_y = f64::INFINITY;
453 let mut best_x: Option<Array1<f64>> = None;
454
455 let n_initial = self.config.n_initial.max(2);
459 let lhs_cfg = SamplingConfig {
460 seed: Some(seed),
461 ..SamplingConfig::default()
462 };
463 let x_init = generate_samples(
464 n_initial,
465 &self.bounds,
466 SamplingStrategy::LatinHypercube,
467 Some(lhs_cfg),
468 )?;
469
470 let mut init_data: Vec<(Array2<f64>, Array1<f64>)> = (0..n_levels)
471 .map(|_| (Array2::zeros((0, self.bounds.len())), Array1::zeros(0)))
472 .collect();
473
474 for i in 0..x_init.nrows() {
475 let xi = x_init.row(i).to_owned();
476 let x_slice: Vec<f64> = xi.iter().copied().collect();
477
478 let y0 = (objectives[0])(&x_slice);
480 let cost0 = self.levels[0].cost;
481 budget_spent += cost0;
482 n_evals_per_level[0] += 1;
483
484 let (ref mut xs0, ref mut ys0) = init_data[0];
486 let old_n = xs0.nrows();
487 let n_dims = self.bounds.len();
488 let mut new_xs = Array2::zeros((old_n + 1, n_dims));
489 for r in 0..old_n {
490 for c in 0..n_dims {
491 new_xs[[r, c]] = xs0[[r, c]];
492 }
493 }
494 for c in 0..n_dims {
495 new_xs[[old_n, c]] = xi[c];
496 }
497 let mut new_ys = Array1::zeros(old_n + 1);
498 for r in 0..old_n {
499 new_ys[r] = ys0[r];
500 }
501 new_ys[old_n] = y0;
502 *xs0 = new_xs;
503 *ys0 = new_ys;
504
505 history.push((budget_spent, 0, y0));
506
507 if budget_spent >= budget {
508 break;
509 }
510 }
511
512 let n_init_hf = (n_initial / 2).max(2);
514 let lhs_cfg2 = SamplingConfig {
515 seed: Some(seed + 1),
516 ..SamplingConfig::default()
517 };
518 let x_init_hf = generate_samples(
519 n_init_hf,
520 &self.bounds,
521 SamplingStrategy::LatinHypercube,
522 Some(lhs_cfg2),
523 )?;
524
525 for i in 0..x_init_hf.nrows() {
526 if budget_spent >= budget {
527 break;
528 }
529 let xi = x_init_hf.row(i).to_owned();
530 let x_slice: Vec<f64> = xi.iter().copied().collect();
531 let y = (objectives[highest])(&x_slice);
532 let cost = self.levels[highest].cost;
533 budget_spent += cost;
534 n_evals_per_level[highest] += 1;
535
536 let (ref mut xs, ref mut ys) = init_data[highest];
537 let old_n = xs.nrows();
538 let n_dims = self.bounds.len();
539 let mut new_xs = Array2::zeros((old_n + 1, n_dims));
540 for r in 0..old_n {
541 for c in 0..n_dims {
542 new_xs[[r, c]] = xs[[r, c]];
543 }
544 }
545 for c in 0..n_dims {
546 new_xs[[old_n, c]] = xi[c];
547 }
548 let mut new_ys = Array1::zeros(old_n + 1);
549 for r in 0..old_n {
550 new_ys[r] = ys[r];
551 }
552 new_ys[old_n] = y;
553 *xs = new_xs;
554 *ys = new_ys;
555
556 history.push((budget_spent, highest, y));
557
558 if y < best_y {
559 best_y = y;
560 best_x = Some(xi);
561 }
562 }
563
564 let x_list: Vec<Array2<f64>> = init_data.iter().map(|(x, _)| x.clone()).collect();
566 let y_list: Vec<Array1<f64>> = init_data.iter().map(|(_, y)| y.clone()).collect();
567
568 if !x_list[0].is_empty() {
570 model.fit(&x_list, &y_list)?;
571 }
572
573 for i in 0..init_data[highest].0.nrows() {
575 let y = init_data[highest].1[i];
576 if y < best_y {
577 best_y = y;
578 best_x = Some(init_data[highest].0.row(i).to_owned());
579 }
580 }
581
582 while budget_spent < budget {
586 let n_dims = self.bounds.len();
587 let n_cands = self.config.n_candidates;
588
589 let mut candidates = Array2::zeros((n_cands, n_dims));
591 for r in 0..n_cands {
592 for c in 0..n_dims {
593 let lo = self.bounds[c].0;
594 let hi = self.bounds[c].1;
595 candidates[[r, c]] = lo + rng.random::<f64>() * (hi - lo);
596 }
597 }
598
599 let mut best_acq = f64::NEG_INFINITY;
601 let mut best_row = 0;
602 let mut best_level = highest;
603
604 let current_best = if best_y.is_finite() { best_y } else { 1.0 };
605
606 for r in 0..n_cands {
607 let x_cand = candidates.row(r).to_owned();
608 let x_mat = match x_cand.into_shape_with_order((1, n_dims)) {
609 Ok(m) => m,
610 Err(_) => continue,
611 };
612
613 for l in 0..n_levels {
614 let remaining = budget - budget_spent;
616 if self.levels[l].cost > remaining + 1e-9 {
617 continue;
618 }
619
620 let acq = match extended_ei(&x_mat, &model, l, self.config.xi, current_best) {
621 Ok(v) => v,
622 Err(_) => continue,
623 };
624
625 if acq > best_acq {
626 best_acq = acq;
627 best_row = r;
628 best_level = l;
629 }
630 }
631 }
632
633 let x_next = candidates.row(best_row).to_owned();
635 let x_slice: Vec<f64> = x_next.iter().copied().collect();
636 let cost = self.levels[best_level].cost;
637
638 if budget_spent + cost > budget + 1e-9 {
639 break;
640 }
641
642 let y_next = (objectives[best_level])(&x_slice);
643 budget_spent += cost;
644 n_evals_per_level[best_level] += 1;
645 history.push((budget_spent, best_level, y_next));
646
647 if self.config.verbose >= 2 {
648 println!(
649 "[MFBO] budget={:.1}/{:.1} level={} f={:.6}",
650 budget_spent, budget, best_level, y_next
651 );
652 }
653
654 model.update(best_level, x_next.clone(), y_next)?;
656
657 if best_level == highest && y_next < best_y {
659 best_y = y_next;
660 best_x = Some(x_next);
661 }
662 }
663
664 if self.config.verbose >= 1 {
665 println!(
666 "[MFBO] Done. budget_spent={:.2} best_f={:.6}",
667 budget_spent, best_y
668 );
669 }
670
671 let x_best = best_x.unwrap_or_else(|| Array1::zeros(self.bounds.len()));
672
673 Ok(MultiFidelityResult {
674 x_best,
675 f_best: best_y,
676 n_evals_per_level,
677 budget_spent,
678 history,
679 })
680 }
681}
682
683pub fn mfbo_optimize(
691 objectives: Vec<Box<dyn Fn(&[f64]) -> f64>>,
692 levels: Vec<FidelityLevel>,
693 bounds: Vec<(f64, f64)>,
694 budget: f64,
695 config: Option<MultiFidelityConfig>,
696) -> OptimizeResult<MultiFidelityResult> {
697 let cfg = config.unwrap_or_default();
698 let mut optimizer = MultiFidelityBo::new(levels, bounds, cfg)?;
699 optimizer.optimize(&objectives, budget)
700}
701
702fn erf_approx(x: f64) -> f64 {
707 let p = 0.3275911_f64;
708 let (a1, a2, a3, a4, a5) = (
709 0.254829592_f64,
710 -0.284496736_f64,
711 1.421413741_f64,
712 -1.453152027_f64,
713 1.061405429_f64,
714 );
715 let sign = if x < 0.0 { -1.0 } else { 1.0 };
716 let xa = x.abs();
717 let t = 1.0 / (1.0 + p * xa);
718 let poly = (((a5 * t + a4) * t + a3) * t + a2) * t + a1;
719 sign * (1.0 - poly * t * (-xa * xa).exp())
720}
721
722fn norm_cdf(z: f64) -> f64 {
723 0.5 * (1.0 + erf_approx(z / std::f64::consts::SQRT_2))
724}
725
726fn norm_pdf(z: f64) -> f64 {
727 (-0.5 * z * z).exp() / (2.0 * std::f64::consts::PI).sqrt()
728}
729
730#[cfg(test)]
735mod tests {
736 use super::*;
737 use scirs2_core::ndarray::array;
738
739 fn make_two_level_fidelities() -> Vec<FidelityLevel> {
740 vec![
741 FidelityLevel {
742 cost: 1.0,
743 noise: 0.05,
744 correlation: 1.0,
745 },
746 FidelityLevel {
747 cost: 5.0,
748 noise: 0.005,
749 correlation: 0.9,
750 },
751 ]
752 }
753
754 #[test]
755 fn test_ar_gp_fit_predict() {
756 let levels = make_two_level_fidelities();
757 let mut ar_gp = AutoRegressiveGp::new(&levels);
758
759 let x0 = Array2::from_shape_vec((4, 1), vec![0.0, 1.0, 2.0, 3.0]).expect("shape");
761 let y0 = array![0.0, 1.0, 4.0, 9.0];
762
763 let x1 = Array2::from_shape_vec((3, 1), vec![0.5, 1.5, 2.5]).expect("shape");
765 let y1 = array![0.25, 2.25, 6.25];
766
767 ar_gp.fit(&[x0, x1], &[y0, y1]).expect("fit");
768
769 let x_test = Array2::from_shape_vec((1, 1), vec![1.0]).expect("shape");
771 let (mean, var) = ar_gp.predict(&x_test, 1).expect("predict");
772 assert!(mean[0].is_finite(), "mean must be finite: {}", mean[0]);
773 assert!(var[0] >= 0.0, "variance must be non-negative: {}", var[0]);
774 }
775
776 #[test]
777 fn test_ar_gp_update() {
778 let levels = make_two_level_fidelities();
779 let mut ar_gp = AutoRegressiveGp::new(&levels);
780
781 let x0 = Array2::from_shape_vec((3, 1), vec![0.0, 1.0, 2.0]).expect("shape");
782 let y0 = array![0.0, 1.0, 4.0];
783 let x1 = Array2::from_shape_vec((2, 1), vec![0.5, 1.5]).expect("shape");
784 let y1 = array![0.25, 2.25];
785
786 ar_gp.fit(&[x0, x1], &[y0, y1]).expect("fit");
787 ar_gp.update(1, array![3.0], 9.0).expect("update");
789
790 let x_test = Array2::from_shape_vec((1, 1), vec![3.0]).expect("shape");
791 let (mean, _) = ar_gp.predict(&x_test, 1).expect("predict");
792 assert!(mean[0].is_finite());
793 }
794
795 #[test]
796 fn test_extended_ei_basics() {
797 let levels = make_two_level_fidelities();
798 let mut ar_gp = AutoRegressiveGp::new(&levels);
799
800 let x0 = Array2::from_shape_vec((4, 1), vec![0.0, 1.0, 2.0, 3.0]).expect("shape");
801 let y0 = array![0.0, 1.0, 4.0, 9.0];
802 let x1 = Array2::from_shape_vec((2, 1), vec![1.0, 2.0]).expect("shape");
803 let y1 = array![1.0, 4.0];
804 ar_gp.fit(&[x0, x1], &[y0, y1]).expect("fit");
805
806 let x_cand = Array2::from_shape_vec((1, 1), vec![0.5]).expect("shape");
807 let ei = extended_ei(&x_cand, &ar_gp, 1, 0.01, 0.5).expect("ei");
808 assert!(
809 ei.is_finite() && ei >= 0.0,
810 "EI must be non-negative: {}",
811 ei
812 );
813 }
814
815 #[test]
816 fn test_mfbo_optimizes_simple_function() {
817 let levels = vec![
819 FidelityLevel {
820 cost: 1.0,
821 noise: 0.05,
822 correlation: 1.0,
823 },
824 FidelityLevel {
825 cost: 3.0,
826 noise: 0.001,
827 correlation: 0.95,
828 },
829 ];
830 let bounds = vec![(0.0_f64, 3.0_f64)];
831 let config = MultiFidelityConfig {
832 n_initial: 4,
833 n_candidates: 50,
834 seed: Some(123),
835 ..Default::default()
836 };
837
838 let objectives: Vec<Box<dyn Fn(&[f64]) -> f64>> = vec![
839 Box::new(|x: &[f64]| (x[0] - 1.0).powi(2) + 0.3),
840 Box::new(|x: &[f64]| (x[0] - 1.0).powi(2)),
841 ];
842
843 let result =
844 mfbo_optimize(objectives, levels, bounds, 20.0, Some(config)).expect("optimize");
845
846 assert!(result.f_best.is_finite());
847 assert!(
849 result.f_best < 1.5,
850 "Expected f_best < 1.5, got {}",
851 result.f_best
852 );
853 assert!(result.budget_spent <= 20.5); }
855}