1use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
20use scirs2_core::random::rngs::StdRng;
21use scirs2_core::random::{Rng, RngExt, SeedableRng};
22
23use crate::error::{OptimizeError, OptimizeResult};
24
25use super::gp::GpSurrogate;
26
27pub trait AcquisitionFn: Send + Sync {
33 fn evaluate(&self, x: &ArrayView1<f64>, surrogate: &GpSurrogate) -> OptimizeResult<f64>;
37
38 fn evaluate_batch(
40 &self,
41 x: &Array2<f64>,
42 surrogate: &GpSurrogate,
43 ) -> OptimizeResult<Array1<f64>> {
44 let n = x.nrows();
45 let mut values = Array1::zeros(n);
46 for i in 0..n {
47 values[i] = self.evaluate(&x.row(i), surrogate)?;
48 }
49 Ok(values)
50 }
51
52 fn name(&self) -> &str;
54}
55
56fn erf_approx(x: f64) -> f64 {
62 let a1 = 0.254829592_f64;
63 let a2 = -0.284496736_f64;
64 let a3 = 1.421413741_f64;
65 let a4 = -1.453152027_f64;
66 let a5 = 1.061405429_f64;
67 let p = 0.3275911_f64;
68
69 let sign = if x < 0.0 { -1.0 } else { 1.0 };
70 let x_abs = x.abs();
71 let t = 1.0 / (1.0 + p * x_abs);
72 let poly = (((a5 * t + a4) * t + a3) * t + a2) * t + a1;
73 sign * (1.0 - poly * t * (-x_abs * x_abs).exp())
74}
75
76fn norm_cdf(z: f64) -> f64 {
78 0.5 * (1.0 + erf_approx(z / std::f64::consts::SQRT_2))
79}
80
81fn norm_pdf(z: f64) -> f64 {
83 (-0.5 * z * z).exp() / (2.0 * std::f64::consts::PI).sqrt()
84}
85
86#[derive(Debug, Clone)]
101pub struct ExpectedImprovement {
102 pub f_best: f64,
104 pub xi: f64,
106}
107
108impl ExpectedImprovement {
109 pub fn new(f_best: f64, xi: f64) -> Self {
111 Self {
112 f_best,
113 xi: xi.max(0.0),
114 }
115 }
116}
117
118impl AcquisitionFn for ExpectedImprovement {
119 fn evaluate(&self, x: &ArrayView1<f64>, surrogate: &GpSurrogate) -> OptimizeResult<f64> {
120 let (mu, sigma) = surrogate.predict_single(x)?;
121
122 if sigma < 1e-12 {
123 return Ok((self.f_best - mu - self.xi).max(0.0));
125 }
126
127 let z = (self.f_best - mu - self.xi) / sigma;
128 let ei = (self.f_best - mu - self.xi) * norm_cdf(z) + sigma * norm_pdf(z);
129
130 Ok(ei.max(0.0))
131 }
132
133 fn name(&self) -> &str {
134 "ExpectedImprovement"
135 }
136}
137
138#[derive(Debug, Clone)]
146pub struct ProbabilityOfImprovement {
147 pub f_best: f64,
149 pub xi: f64,
151}
152
153impl ProbabilityOfImprovement {
154 pub fn new(f_best: f64, xi: f64) -> Self {
155 Self {
156 f_best,
157 xi: xi.max(0.0),
158 }
159 }
160}
161
162impl AcquisitionFn for ProbabilityOfImprovement {
163 fn evaluate(&self, x: &ArrayView1<f64>, surrogate: &GpSurrogate) -> OptimizeResult<f64> {
164 let (mu, sigma) = surrogate.predict_single(x)?;
165
166 if sigma < 1e-12 {
167 return Ok(if mu < self.f_best - self.xi { 1.0 } else { 0.0 });
168 }
169
170 let z = (self.f_best - mu - self.xi) / sigma;
171 Ok(norm_cdf(z))
172 }
173
174 fn name(&self) -> &str {
175 "ProbabilityOfImprovement"
176 }
177}
178
179#[derive(Debug, Clone)]
197pub struct UpperConfidenceBound {
198 pub kappa: f64,
200}
201
202impl UpperConfidenceBound {
203 pub fn new(kappa: f64) -> Self {
204 Self {
205 kappa: kappa.max(0.0),
206 }
207 }
208}
209
210impl Default for UpperConfidenceBound {
211 fn default() -> Self {
212 Self::new(2.0)
213 }
214}
215
216impl AcquisitionFn for UpperConfidenceBound {
217 fn evaluate(&self, x: &ArrayView1<f64>, surrogate: &GpSurrogate) -> OptimizeResult<f64> {
218 let (mu, sigma) = surrogate.predict_single(x)?;
219 Ok(-(mu - self.kappa * sigma))
221 }
222
223 fn name(&self) -> &str {
224 "UpperConfidenceBound"
225 }
226}
227
228#[derive(Debug, Clone)]
242pub struct KnowledgeGradient {
243 reference_points: Array2<f64>,
245 n_fantasies: usize,
247 seed: u64,
249}
250
251impl KnowledgeGradient {
252 pub fn new(reference_points: Array2<f64>, n_fantasies: usize, seed: u64) -> Self {
258 Self {
259 reference_points,
260 n_fantasies: n_fantasies.max(1),
261 seed,
262 }
263 }
264}
265
266impl AcquisitionFn for KnowledgeGradient {
267 fn evaluate(&self, x: &ArrayView1<f64>, surrogate: &GpSurrogate) -> OptimizeResult<f64> {
268 let (mu_x, sigma_x) = surrogate.predict_single(x)?;
269
270 let ref_means = surrogate.predict_mean(&self.reference_points)?;
272 let current_best = ref_means.iter().copied().fold(f64::INFINITY, f64::min);
273
274 if sigma_x < 1e-12 {
275 return Ok(0.0);
276 }
277
278 let mut rng = StdRng::seed_from_u64(self.seed);
280 let mut kg_sum = 0.0;
281
282 for _ in 0..self.n_fantasies {
283 let z: f64 = standard_normal(&mut rng);
285 let _y_fantasy = mu_x + sigma_x * z;
286
287 let x_mat = x
296 .to_owned()
297 .into_shape_with_order((1, x.len()))
298 .map_err(|e| OptimizeError::ComputationError(format!("Shape error: {}", e)))?;
299 let (_, ref_var) = surrogate.predict(&self.reference_points)?;
300
301 let mut min_updated = f64::INFINITY;
303 for i in 0..self.reference_points.nrows() {
304 let sigma_ref = ref_var[i].max(0.0).sqrt();
306 let k_xr = surrogate
307 .kernel()
308 .eval(&x.view(), &self.reference_points.row(i));
309 let k_xx = surrogate.kernel().eval(&x.view(), &x.view());
310
311 let rho = if k_xx > 1e-12 && sigma_ref > 1e-12 {
312 k_xr / (k_xx.sqrt() * sigma_ref)
313 } else {
314 0.0
315 };
316
317 let updated_mean = ref_means[i] + rho * sigma_ref * z;
318 if updated_mean < min_updated {
319 min_updated = updated_mean;
320 }
321 }
322
323 let improvement = (current_best - min_updated).max(0.0);
324 kg_sum += improvement;
325 }
326
327 Ok(kg_sum / self.n_fantasies as f64)
328 }
329
330 fn name(&self) -> &str {
331 "KnowledgeGradient"
332 }
333}
334
335#[derive(Debug, Clone)]
349pub struct ThompsonSampling {
350 seed: u64,
352}
353
354impl ThompsonSampling {
355 pub fn new(seed: u64) -> Self {
356 Self { seed }
357 }
358}
359
360impl AcquisitionFn for ThompsonSampling {
361 fn evaluate(&self, x: &ArrayView1<f64>, surrogate: &GpSurrogate) -> OptimizeResult<f64> {
362 let (mu, sigma) = surrogate.predict_single(x)?;
363
364 let mut rng = StdRng::seed_from_u64(self.seed.wrapping_add(hash_array_view(x)));
366 let z = standard_normal(&mut rng);
367 let sample = mu + sigma * z;
368
369 Ok(-sample)
371 }
372
373 fn name(&self) -> &str {
374 "ThompsonSampling"
375 }
376}
377
378#[derive(Debug, Clone)]
389pub struct BatchExpectedImprovement {
390 pub batch_size: usize,
392 pub xi: f64,
394}
395
396impl BatchExpectedImprovement {
397 pub fn new(batch_size: usize, xi: f64) -> Self {
398 Self {
399 batch_size: batch_size.max(1),
400 xi: xi.max(0.0),
401 }
402 }
403}
404
405impl AcquisitionFn for BatchExpectedImprovement {
406 fn evaluate(&self, x: &ArrayView1<f64>, surrogate: &GpSurrogate) -> OptimizeResult<f64> {
407 let ei = ExpectedImprovement::new(get_f_best(surrogate)?, self.xi);
412 ei.evaluate(x, surrogate)
413 }
414
415 fn name(&self) -> &str {
416 "BatchExpectedImprovement"
417 }
418}
419
420#[derive(Debug, Clone)]
428pub struct BatchUpperConfidenceBound {
429 pub batch_size: usize,
431 pub kappa: f64,
433}
434
435impl BatchUpperConfidenceBound {
436 pub fn new(batch_size: usize, kappa: f64) -> Self {
437 Self {
438 batch_size: batch_size.max(1),
439 kappa: kappa.max(0.0),
440 }
441 }
442}
443
444impl AcquisitionFn for BatchUpperConfidenceBound {
445 fn evaluate(&self, x: &ArrayView1<f64>, surrogate: &GpSurrogate) -> OptimizeResult<f64> {
446 let ucb = UpperConfidenceBound::new(self.kappa);
447 ucb.evaluate(x, surrogate)
448 }
449
450 fn name(&self) -> &str {
451 "BatchUpperConfidenceBound"
452 }
453}
454
455#[derive(Debug, Clone)]
461pub enum AcquisitionType {
462 EI { xi: f64 },
464 PI { xi: f64 },
466 UCB { kappa: f64 },
468 KG { n_fantasies: usize, seed: u64 },
470 Thompson { seed: u64 },
472 BatchEI { batch_size: usize, xi: f64 },
474 BatchUCB { batch_size: usize, kappa: f64 },
476}
477
478impl Default for AcquisitionType {
479 fn default() -> Self {
480 Self::EI { xi: 0.01 }
481 }
482}
483
484impl AcquisitionType {
485 pub fn build(
491 &self,
492 f_best: f64,
493 reference_points: Option<&Array2<f64>>,
494 ) -> Box<dyn AcquisitionFn> {
495 match self {
496 AcquisitionType::EI { xi } => Box::new(ExpectedImprovement::new(f_best, *xi)),
497 AcquisitionType::PI { xi } => Box::new(ProbabilityOfImprovement::new(f_best, *xi)),
498 AcquisitionType::UCB { kappa } => Box::new(UpperConfidenceBound::new(*kappa)),
499 AcquisitionType::KG { n_fantasies, seed } => {
500 let ref_pts = reference_points
501 .cloned()
502 .unwrap_or_else(|| Array2::zeros((0, 1)));
503 Box::new(KnowledgeGradient::new(ref_pts, *n_fantasies, *seed))
504 }
505 AcquisitionType::Thompson { seed } => Box::new(ThompsonSampling::new(*seed)),
506 AcquisitionType::BatchEI { batch_size, xi } => {
507 Box::new(BatchExpectedImprovement::new(*batch_size, *xi))
508 }
509 AcquisitionType::BatchUCB { batch_size, kappa } => {
510 Box::new(BatchUpperConfidenceBound::new(*batch_size, *kappa))
511 }
512 }
513 }
514
515 pub fn batch_size(&self) -> usize {
517 match self {
518 AcquisitionType::BatchEI { batch_size, .. } => *batch_size,
519 AcquisitionType::BatchUCB { batch_size, .. } => *batch_size,
520 _ => 1,
521 }
522 }
523}
524
525fn get_f_best(surrogate: &GpSurrogate) -> OptimizeResult<f64> {
531 let n = surrogate.n_train();
534 if n == 0 {
535 return Err(OptimizeError::ComputationError(
536 "No training data to compute f_best".to_string(),
537 ));
538 }
539 Ok(f64::NEG_INFINITY) }
548
549fn standard_normal(rng: &mut StdRng) -> f64 {
551 let u1: f64 = rng.random_range(1e-10..1.0); let u2: f64 = rng.random_range(0.0..1.0);
553 (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
554}
555
556fn hash_array_view(x: &ArrayView1<f64>) -> u64 {
558 let mut h: u64 = 0xcbf29ce484222325; for &v in x.iter() {
560 let bits = v.to_bits();
561 h ^= bits;
562 h = h.wrapping_mul(0x100000001b3); }
564 h
565}
566
567#[cfg(test)]
572mod tests {
573 use super::super::gp::{GpSurrogate, GpSurrogateConfig, RbfKernel};
574 use super::*;
575 use scirs2_core::ndarray::{array, Array2};
576
577 fn fitted_gp() -> GpSurrogate {
578 let x = Array2::from_shape_vec((5, 1), vec![0.0, 1.0, 2.0, 3.0, 4.0]).expect("shape ok");
579 let y = array![1.0, 0.5, 0.2, 0.3, 0.8];
580 let mut gp = GpSurrogate::new(
581 Box::new(RbfKernel::default()),
582 GpSurrogateConfig {
583 optimize_hyperparams: false,
584 noise_variance: 1e-4,
585 ..Default::default()
586 },
587 );
588 gp.fit(&x, &y).expect("fit ok");
589 gp
590 }
591
592 #[test]
593 fn test_ei_positive() {
594 let gp = fitted_gp();
595 let ei = ExpectedImprovement::new(0.2, 0.01);
596
597 let x = array![5.0];
599 let val = ei.evaluate(&x.view(), &gp).expect("eval ok");
600 assert!(val >= 0.0, "EI should be non-negative, got {}", val);
601 }
602
603 #[test]
604 fn test_ei_at_best_near_zero() {
605 let gp = fitted_gp();
606 let ei = ExpectedImprovement::new(0.2, 0.0);
608
609 let x = array![2.0];
610 let val = ei.evaluate(&x.view(), &gp).expect("eval ok");
611 assert!(val < 0.1, "EI at best point should be small, got {}", val);
613 }
614
615 #[test]
616 fn test_pi_bounded() {
617 let gp = fitted_gp();
618 let pi = ProbabilityOfImprovement::new(0.2, 0.01);
619
620 let x = array![1.5];
621 let val = pi.evaluate(&x.view(), &gp).expect("eval ok");
622 assert!(
623 val >= 0.0 && val <= 1.0,
624 "PI should be in [0,1], got {}",
625 val
626 );
627 }
628
629 #[test]
630 fn test_ucb_finite() {
631 let gp = fitted_gp();
632 let ucb = UpperConfidenceBound::new(2.0);
633
634 let x = array![1.5];
635 let val = ucb.evaluate(&x.view(), &gp).expect("eval ok");
636 assert!(val.is_finite(), "UCB should be finite, got {}", val);
637 }
638
639 #[test]
640 fn test_ucb_exploration_increases_with_kappa() {
641 let gp = fitted_gp();
642 let ucb_low = UpperConfidenceBound::new(0.1);
643 let ucb_high = UpperConfidenceBound::new(5.0);
644
645 let x = array![10.0];
647 let val_low = ucb_low.evaluate(&x.view(), &gp).expect("eval ok");
648 let val_high = ucb_high.evaluate(&x.view(), &gp).expect("eval ok");
649
650 assert!(
652 val_high > val_low,
653 "Higher kappa should give higher UCB at uncertain point: {} vs {}",
654 val_high,
655 val_low
656 );
657 }
658
659 #[test]
660 fn test_thompson_sampling() {
661 let gp = fitted_gp();
662 let ts = ThompsonSampling::new(42);
663
664 let x = array![1.5];
665 let val = ts.evaluate(&x.view(), &gp).expect("eval ok");
666 assert!(
667 val.is_finite(),
668 "TS should produce finite value, got {}",
669 val
670 );
671 }
672
673 #[test]
674 fn test_thompson_sampling_different_seeds() {
675 let gp = fitted_gp();
676 let ts1 = ThompsonSampling::new(42);
677 let ts2 = ThompsonSampling::new(43);
678
679 let x = array![1.5];
680 let val1 = ts1.evaluate(&x.view(), &gp).expect("eval ok");
681 let val2 = ts2.evaluate(&x.view(), &gp).expect("eval ok");
682
683 assert!(val1.is_finite());
686 assert!(val2.is_finite());
687 }
688
689 #[test]
690 fn test_knowledge_gradient() {
691 let gp = fitted_gp();
692 let ref_pts = Array2::from_shape_vec((3, 1), vec![0.0, 2.0, 4.0]).expect("shape ok");
693 let kg = KnowledgeGradient::new(ref_pts, 10, 42);
694
695 let x = array![1.5];
696 let val = kg.evaluate(&x.view(), &gp).expect("eval ok");
697 assert!(val >= 0.0, "KG should be non-negative, got {}", val);
698 assert!(val.is_finite(), "KG should be finite, got {}", val);
699 }
700
701 #[test]
702 fn test_batch_ei() {
703 let gp = fitted_gp();
704 let bei = BatchExpectedImprovement::new(3, 0.01);
705
706 let x = array![1.5];
707 let val = bei.evaluate(&x.view(), &gp).expect("eval ok");
708 assert!(val.is_finite());
709 }
710
711 #[test]
712 fn test_batch_ucb() {
713 let gp = fitted_gp();
714 let bucb = BatchUpperConfidenceBound::new(3, 2.0);
715
716 let x = array![1.5];
717 let val = bucb.evaluate(&x.view(), &gp).expect("eval ok");
718 assert!(val.is_finite());
719 }
720
721 #[test]
722 fn test_acquisition_type_build_all() {
723 let f_best = 0.2;
724 let ref_pts = Array2::from_shape_vec((3, 1), vec![0.0, 2.0, 4.0]).expect("shape ok");
725
726 let types = vec![
727 AcquisitionType::EI { xi: 0.01 },
728 AcquisitionType::PI { xi: 0.01 },
729 AcquisitionType::UCB { kappa: 2.0 },
730 AcquisitionType::KG {
731 n_fantasies: 5,
732 seed: 42,
733 },
734 AcquisitionType::Thompson { seed: 42 },
735 AcquisitionType::BatchEI {
736 batch_size: 3,
737 xi: 0.01,
738 },
739 AcquisitionType::BatchUCB {
740 batch_size: 3,
741 kappa: 2.0,
742 },
743 ];
744
745 let gp = fitted_gp();
746
747 for acq_type in &types {
748 let acq = acq_type.build(f_best, Some(&ref_pts));
749 let x = array![1.5];
750 let val = acq.evaluate(&x.view(), &gp).expect("eval should succeed");
751 assert!(
752 val.is_finite(),
753 "{} produced non-finite value: {}",
754 acq.name(),
755 val
756 );
757 }
758 }
759
760 #[test]
761 fn test_evaluate_batch() {
762 let gp = fitted_gp();
763 let ei = ExpectedImprovement::new(0.2, 0.01);
764
765 let x_batch = Array2::from_shape_vec((3, 1), vec![1.0, 2.5, 4.5]).expect("shape ok");
766 let values = ei.evaluate_batch(&x_batch, &gp).expect("eval batch ok");
767 assert_eq!(values.len(), 3);
768 for &v in values.iter() {
769 assert!(v >= 0.0 && v.is_finite());
770 }
771 }
772
773 #[test]
774 fn test_norm_cdf_pdf() {
775 assert!((norm_cdf(0.0) - 0.5).abs() < 1e-6);
777 assert!(norm_cdf(-10.0) < 1e-10);
779 assert!((norm_cdf(10.0) - 1.0).abs() < 1e-10);
781 assert!((norm_pdf(0.0) - 1.0 / (2.0 * std::f64::consts::PI).sqrt()).abs() < 1e-10);
783 }
784
785 #[test]
786 fn test_acquisition_type_batch_size() {
787 assert_eq!(AcquisitionType::EI { xi: 0.01 }.batch_size(), 1);
788 assert_eq!(
789 AcquisitionType::BatchEI {
790 batch_size: 5,
791 xi: 0.01
792 }
793 .batch_size(),
794 5
795 );
796 }
797}