use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
use scirs2_core::random::rngs::StdRng;
use scirs2_core::random::{Rng, RngExt, SeedableRng};
use crate::error::{OptimizeError, OptimizeResult};
use super::gp::GpSurrogate;
pub trait AcquisitionFn: Send + Sync {
fn evaluate(&self, x: &ArrayView1<f64>, surrogate: &GpSurrogate) -> OptimizeResult<f64>;
fn evaluate_batch(
&self,
x: &Array2<f64>,
surrogate: &GpSurrogate,
) -> OptimizeResult<Array1<f64>> {
let n = x.nrows();
let mut values = Array1::zeros(n);
for i in 0..n {
values[i] = self.evaluate(&x.row(i), surrogate)?;
}
Ok(values)
}
fn name(&self) -> &str;
}
fn erf_approx(x: f64) -> f64 {
let a1 = 0.254829592_f64;
let a2 = -0.284496736_f64;
let a3 = 1.421413741_f64;
let a4 = -1.453152027_f64;
let a5 = 1.061405429_f64;
let p = 0.3275911_f64;
let sign = if x < 0.0 { -1.0 } else { 1.0 };
let x_abs = x.abs();
let t = 1.0 / (1.0 + p * x_abs);
let poly = (((a5 * t + a4) * t + a3) * t + a2) * t + a1;
sign * (1.0 - poly * t * (-x_abs * x_abs).exp())
}
fn norm_cdf(z: f64) -> f64 {
0.5 * (1.0 + erf_approx(z / std::f64::consts::SQRT_2))
}
fn norm_pdf(z: f64) -> f64 {
(-0.5 * z * z).exp() / (2.0 * std::f64::consts::PI).sqrt()
}
#[derive(Debug, Clone)]
pub struct ExpectedImprovement {
pub f_best: f64,
pub xi: f64,
}
impl ExpectedImprovement {
pub fn new(f_best: f64, xi: f64) -> Self {
Self {
f_best,
xi: xi.max(0.0),
}
}
}
impl AcquisitionFn for ExpectedImprovement {
fn evaluate(&self, x: &ArrayView1<f64>, surrogate: &GpSurrogate) -> OptimizeResult<f64> {
let (mu, sigma) = surrogate.predict_single(x)?;
if sigma < 1e-12 {
return Ok((self.f_best - mu - self.xi).max(0.0));
}
let z = (self.f_best - mu - self.xi) / sigma;
let ei = (self.f_best - mu - self.xi) * norm_cdf(z) + sigma * norm_pdf(z);
Ok(ei.max(0.0))
}
fn name(&self) -> &str {
"ExpectedImprovement"
}
}
#[derive(Debug, Clone)]
pub struct ProbabilityOfImprovement {
pub f_best: f64,
pub xi: f64,
}
impl ProbabilityOfImprovement {
pub fn new(f_best: f64, xi: f64) -> Self {
Self {
f_best,
xi: xi.max(0.0),
}
}
}
impl AcquisitionFn for ProbabilityOfImprovement {
fn evaluate(&self, x: &ArrayView1<f64>, surrogate: &GpSurrogate) -> OptimizeResult<f64> {
let (mu, sigma) = surrogate.predict_single(x)?;
if sigma < 1e-12 {
return Ok(if mu < self.f_best - self.xi { 1.0 } else { 0.0 });
}
let z = (self.f_best - mu - self.xi) / sigma;
Ok(norm_cdf(z))
}
fn name(&self) -> &str {
"ProbabilityOfImprovement"
}
}
#[derive(Debug, Clone)]
pub struct UpperConfidenceBound {
pub kappa: f64,
}
impl UpperConfidenceBound {
pub fn new(kappa: f64) -> Self {
Self {
kappa: kappa.max(0.0),
}
}
}
impl Default for UpperConfidenceBound {
fn default() -> Self {
Self::new(2.0)
}
}
impl AcquisitionFn for UpperConfidenceBound {
fn evaluate(&self, x: &ArrayView1<f64>, surrogate: &GpSurrogate) -> OptimizeResult<f64> {
let (mu, sigma) = surrogate.predict_single(x)?;
Ok(-(mu - self.kappa * sigma))
}
fn name(&self) -> &str {
"UpperConfidenceBound"
}
}
#[derive(Debug, Clone)]
pub struct KnowledgeGradient {
reference_points: Array2<f64>,
n_fantasies: usize,
seed: u64,
}
impl KnowledgeGradient {
pub fn new(reference_points: Array2<f64>, n_fantasies: usize, seed: u64) -> Self {
Self {
reference_points,
n_fantasies: n_fantasies.max(1),
seed,
}
}
}
impl AcquisitionFn for KnowledgeGradient {
fn evaluate(&self, x: &ArrayView1<f64>, surrogate: &GpSurrogate) -> OptimizeResult<f64> {
let (mu_x, sigma_x) = surrogate.predict_single(x)?;
let ref_means = surrogate.predict_mean(&self.reference_points)?;
let current_best = ref_means.iter().copied().fold(f64::INFINITY, f64::min);
if sigma_x < 1e-12 {
return Ok(0.0);
}
let mut rng = StdRng::seed_from_u64(self.seed);
let mut kg_sum = 0.0;
for _ in 0..self.n_fantasies {
let z: f64 = standard_normal(&mut rng);
let _y_fantasy = mu_x + sigma_x * z;
let x_mat = x
.to_owned()
.into_shape_with_order((1, x.len()))
.map_err(|e| OptimizeError::ComputationError(format!("Shape error: {}", e)))?;
let (_, ref_var) = surrogate.predict(&self.reference_points)?;
let mut min_updated = f64::INFINITY;
for i in 0..self.reference_points.nrows() {
let sigma_ref = ref_var[i].max(0.0).sqrt();
let k_xr = surrogate
.kernel()
.eval(&x.view(), &self.reference_points.row(i));
let k_xx = surrogate.kernel().eval(&x.view(), &x.view());
let rho = if k_xx > 1e-12 && sigma_ref > 1e-12 {
k_xr / (k_xx.sqrt() * sigma_ref)
} else {
0.0
};
let updated_mean = ref_means[i] + rho * sigma_ref * z;
if updated_mean < min_updated {
min_updated = updated_mean;
}
}
let improvement = (current_best - min_updated).max(0.0);
kg_sum += improvement;
}
Ok(kg_sum / self.n_fantasies as f64)
}
fn name(&self) -> &str {
"KnowledgeGradient"
}
}
#[derive(Debug, Clone)]
pub struct ThompsonSampling {
seed: u64,
}
impl ThompsonSampling {
pub fn new(seed: u64) -> Self {
Self { seed }
}
}
impl AcquisitionFn for ThompsonSampling {
fn evaluate(&self, x: &ArrayView1<f64>, surrogate: &GpSurrogate) -> OptimizeResult<f64> {
let (mu, sigma) = surrogate.predict_single(x)?;
let mut rng = StdRng::seed_from_u64(self.seed.wrapping_add(hash_array_view(x)));
let z = standard_normal(&mut rng);
let sample = mu + sigma * z;
Ok(-sample)
}
fn name(&self) -> &str {
"ThompsonSampling"
}
}
#[derive(Debug, Clone)]
pub struct BatchExpectedImprovement {
pub batch_size: usize,
pub xi: f64,
}
impl BatchExpectedImprovement {
pub fn new(batch_size: usize, xi: f64) -> Self {
Self {
batch_size: batch_size.max(1),
xi: xi.max(0.0),
}
}
}
impl AcquisitionFn for BatchExpectedImprovement {
fn evaluate(&self, x: &ArrayView1<f64>, surrogate: &GpSurrogate) -> OptimizeResult<f64> {
let ei = ExpectedImprovement::new(get_f_best(surrogate)?, self.xi);
ei.evaluate(x, surrogate)
}
fn name(&self) -> &str {
"BatchExpectedImprovement"
}
}
#[derive(Debug, Clone)]
pub struct BatchUpperConfidenceBound {
pub batch_size: usize,
pub kappa: f64,
}
impl BatchUpperConfidenceBound {
pub fn new(batch_size: usize, kappa: f64) -> Self {
Self {
batch_size: batch_size.max(1),
kappa: kappa.max(0.0),
}
}
}
impl AcquisitionFn for BatchUpperConfidenceBound {
fn evaluate(&self, x: &ArrayView1<f64>, surrogate: &GpSurrogate) -> OptimizeResult<f64> {
let ucb = UpperConfidenceBound::new(self.kappa);
ucb.evaluate(x, surrogate)
}
fn name(&self) -> &str {
"BatchUpperConfidenceBound"
}
}
#[derive(Debug, Clone)]
pub enum AcquisitionType {
EI { xi: f64 },
PI { xi: f64 },
UCB { kappa: f64 },
KG { n_fantasies: usize, seed: u64 },
Thompson { seed: u64 },
BatchEI { batch_size: usize, xi: f64 },
BatchUCB { batch_size: usize, kappa: f64 },
}
impl Default for AcquisitionType {
fn default() -> Self {
Self::EI { xi: 0.01 }
}
}
impl AcquisitionType {
pub fn build(
&self,
f_best: f64,
reference_points: Option<&Array2<f64>>,
) -> Box<dyn AcquisitionFn> {
match self {
AcquisitionType::EI { xi } => Box::new(ExpectedImprovement::new(f_best, *xi)),
AcquisitionType::PI { xi } => Box::new(ProbabilityOfImprovement::new(f_best, *xi)),
AcquisitionType::UCB { kappa } => Box::new(UpperConfidenceBound::new(*kappa)),
AcquisitionType::KG { n_fantasies, seed } => {
let ref_pts = reference_points
.cloned()
.unwrap_or_else(|| Array2::zeros((0, 1)));
Box::new(KnowledgeGradient::new(ref_pts, *n_fantasies, *seed))
}
AcquisitionType::Thompson { seed } => Box::new(ThompsonSampling::new(*seed)),
AcquisitionType::BatchEI { batch_size, xi } => {
Box::new(BatchExpectedImprovement::new(*batch_size, *xi))
}
AcquisitionType::BatchUCB { batch_size, kappa } => {
Box::new(BatchUpperConfidenceBound::new(*batch_size, *kappa))
}
}
}
pub fn batch_size(&self) -> usize {
match self {
AcquisitionType::BatchEI { batch_size, .. } => *batch_size,
AcquisitionType::BatchUCB { batch_size, .. } => *batch_size,
_ => 1,
}
}
}
fn get_f_best(surrogate: &GpSurrogate) -> OptimizeResult<f64> {
let n = surrogate.n_train();
if n == 0 {
return Err(OptimizeError::ComputationError(
"No training data to compute f_best".to_string(),
));
}
Ok(f64::NEG_INFINITY) }
fn standard_normal(rng: &mut StdRng) -> f64 {
let u1: f64 = rng.random_range(1e-10..1.0); let u2: f64 = rng.random_range(0.0..1.0);
(-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
}
fn hash_array_view(x: &ArrayView1<f64>) -> u64 {
let mut h: u64 = 0xcbf29ce484222325; for &v in x.iter() {
let bits = v.to_bits();
h ^= bits;
h = h.wrapping_mul(0x100000001b3); }
h
}
#[cfg(test)]
mod tests {
use super::super::gp::{GpSurrogate, GpSurrogateConfig, RbfKernel};
use super::*;
use scirs2_core::ndarray::{array, Array2};
fn fitted_gp() -> GpSurrogate {
let x = Array2::from_shape_vec((5, 1), vec![0.0, 1.0, 2.0, 3.0, 4.0]).expect("shape ok");
let y = array![1.0, 0.5, 0.2, 0.3, 0.8];
let mut gp = GpSurrogate::new(
Box::new(RbfKernel::default()),
GpSurrogateConfig {
optimize_hyperparams: false,
noise_variance: 1e-4,
..Default::default()
},
);
gp.fit(&x, &y).expect("fit ok");
gp
}
#[test]
fn test_ei_positive() {
let gp = fitted_gp();
let ei = ExpectedImprovement::new(0.2, 0.01);
let x = array![5.0];
let val = ei.evaluate(&x.view(), &gp).expect("eval ok");
assert!(val >= 0.0, "EI should be non-negative, got {}", val);
}
#[test]
fn test_ei_at_best_near_zero() {
let gp = fitted_gp();
let ei = ExpectedImprovement::new(0.2, 0.0);
let x = array![2.0];
let val = ei.evaluate(&x.view(), &gp).expect("eval ok");
assert!(val < 0.1, "EI at best point should be small, got {}", val);
}
#[test]
fn test_pi_bounded() {
let gp = fitted_gp();
let pi = ProbabilityOfImprovement::new(0.2, 0.01);
let x = array![1.5];
let val = pi.evaluate(&x.view(), &gp).expect("eval ok");
assert!(
val >= 0.0 && val <= 1.0,
"PI should be in [0,1], got {}",
val
);
}
#[test]
fn test_ucb_finite() {
let gp = fitted_gp();
let ucb = UpperConfidenceBound::new(2.0);
let x = array![1.5];
let val = ucb.evaluate(&x.view(), &gp).expect("eval ok");
assert!(val.is_finite(), "UCB should be finite, got {}", val);
}
#[test]
fn test_ucb_exploration_increases_with_kappa() {
let gp = fitted_gp();
let ucb_low = UpperConfidenceBound::new(0.1);
let ucb_high = UpperConfidenceBound::new(5.0);
let x = array![10.0];
let val_low = ucb_low.evaluate(&x.view(), &gp).expect("eval ok");
let val_high = ucb_high.evaluate(&x.view(), &gp).expect("eval ok");
assert!(
val_high > val_low,
"Higher kappa should give higher UCB at uncertain point: {} vs {}",
val_high,
val_low
);
}
#[test]
fn test_thompson_sampling() {
let gp = fitted_gp();
let ts = ThompsonSampling::new(42);
let x = array![1.5];
let val = ts.evaluate(&x.view(), &gp).expect("eval ok");
assert!(
val.is_finite(),
"TS should produce finite value, got {}",
val
);
}
#[test]
fn test_thompson_sampling_different_seeds() {
let gp = fitted_gp();
let ts1 = ThompsonSampling::new(42);
let ts2 = ThompsonSampling::new(43);
let x = array![1.5];
let val1 = ts1.evaluate(&x.view(), &gp).expect("eval ok");
let val2 = ts2.evaluate(&x.view(), &gp).expect("eval ok");
assert!(val1.is_finite());
assert!(val2.is_finite());
}
#[test]
fn test_knowledge_gradient() {
let gp = fitted_gp();
let ref_pts = Array2::from_shape_vec((3, 1), vec![0.0, 2.0, 4.0]).expect("shape ok");
let kg = KnowledgeGradient::new(ref_pts, 10, 42);
let x = array![1.5];
let val = kg.evaluate(&x.view(), &gp).expect("eval ok");
assert!(val >= 0.0, "KG should be non-negative, got {}", val);
assert!(val.is_finite(), "KG should be finite, got {}", val);
}
#[test]
fn test_batch_ei() {
let gp = fitted_gp();
let bei = BatchExpectedImprovement::new(3, 0.01);
let x = array![1.5];
let val = bei.evaluate(&x.view(), &gp).expect("eval ok");
assert!(val.is_finite());
}
#[test]
fn test_batch_ucb() {
let gp = fitted_gp();
let bucb = BatchUpperConfidenceBound::new(3, 2.0);
let x = array![1.5];
let val = bucb.evaluate(&x.view(), &gp).expect("eval ok");
assert!(val.is_finite());
}
#[test]
fn test_acquisition_type_build_all() {
let f_best = 0.2;
let ref_pts = Array2::from_shape_vec((3, 1), vec![0.0, 2.0, 4.0]).expect("shape ok");
let types = vec![
AcquisitionType::EI { xi: 0.01 },
AcquisitionType::PI { xi: 0.01 },
AcquisitionType::UCB { kappa: 2.0 },
AcquisitionType::KG {
n_fantasies: 5,
seed: 42,
},
AcquisitionType::Thompson { seed: 42 },
AcquisitionType::BatchEI {
batch_size: 3,
xi: 0.01,
},
AcquisitionType::BatchUCB {
batch_size: 3,
kappa: 2.0,
},
];
let gp = fitted_gp();
for acq_type in &types {
let acq = acq_type.build(f_best, Some(&ref_pts));
let x = array![1.5];
let val = acq.evaluate(&x.view(), &gp).expect("eval should succeed");
assert!(
val.is_finite(),
"{} produced non-finite value: {}",
acq.name(),
val
);
}
}
#[test]
fn test_evaluate_batch() {
let gp = fitted_gp();
let ei = ExpectedImprovement::new(0.2, 0.01);
let x_batch = Array2::from_shape_vec((3, 1), vec![1.0, 2.5, 4.5]).expect("shape ok");
let values = ei.evaluate_batch(&x_batch, &gp).expect("eval batch ok");
assert_eq!(values.len(), 3);
for &v in values.iter() {
assert!(v >= 0.0 && v.is_finite());
}
}
#[test]
fn test_norm_cdf_pdf() {
assert!((norm_cdf(0.0) - 0.5).abs() < 1e-6);
assert!(norm_cdf(-10.0) < 1e-10);
assert!((norm_cdf(10.0) - 1.0).abs() < 1e-10);
assert!((norm_pdf(0.0) - 1.0 / (2.0 * std::f64::consts::PI).sqrt()).abs() < 1e-10);
}
#[test]
fn test_acquisition_type_batch_size() {
assert_eq!(AcquisitionType::EI { xi: 0.01 }.batch_size(), 1);
assert_eq!(
AcquisitionType::BatchEI {
batch_size: 5,
xi: 0.01
}
.batch_size(),
5
);
}
}