use crate::{OptimizerError, OptimizerResult};
use scirs2_core::RngExt;
use std::collections::HashMap;
use std::sync::Arc;
use torsh_core::device::CpuDevice;
pub trait ObjectiveFunction: Send + Sync {
fn evaluate(&self, parameters: &[f32]) -> OptimizerResult<f32>;
fn dimension(&self) -> usize;
fn bounds(&self) -> (Vec<f32>, Vec<f32>);
fn name(&self) -> &str {
"Unknown"
}
fn is_noisy(&self) -> bool {
false
}
}
#[derive(Debug, Clone)]
pub struct BayesianOptConfig {
pub max_evaluations: usize,
pub initial_random_evaluations: usize,
pub acquisition_function: AcquisitionFunction,
pub acquisition_restarts: usize,
pub gp_config: GaussianProcessConfig,
pub seed: Option<u64>,
pub device: Arc<CpuDevice>,
pub verbose: bool,
}
impl Default for BayesianOptConfig {
fn default() -> Self {
Self {
max_evaluations: 100,
initial_random_evaluations: 10,
acquisition_function: AcquisitionFunction::ExpectedImprovement { xi: 0.01 },
acquisition_restarts: 10,
gp_config: GaussianProcessConfig::default(),
seed: None,
device: Arc::new(CpuDevice::new()),
verbose: false,
}
}
}
#[derive(Debug, Clone)]
pub enum AcquisitionFunction {
ExpectedImprovement { xi: f32 },
ProbabilityOfImprovement { xi: f32 },
UpperConfidenceBound { kappa: f32 },
ThompsonSampling,
EntropySearch,
}
#[derive(Debug, Clone)]
pub struct GaussianProcessConfig {
pub kernel: KernelType,
pub noise_level: f32,
pub length_scale_bounds: (f32, f32),
pub signal_variance_bounds: (f32, f32),
pub optimize_hyperparameters: bool,
}
impl Default for GaussianProcessConfig {
fn default() -> Self {
Self {
kernel: KernelType::RBF { length_scale: 1.0 },
noise_level: 1e-6,
length_scale_bounds: (1e-3, 1e3),
signal_variance_bounds: (1e-3, 1e3),
optimize_hyperparameters: true,
}
}
}
#[derive(Debug, Clone)]
pub enum KernelType {
RBF { length_scale: f32 },
Matern15 { length_scale: f32 },
Matern25 { length_scale: f32 },
Linear { variance: f32 },
RationalQuadratic { length_scale: f32, alpha: f32 },
}
#[derive(Debug, Clone)]
pub struct DataPoint {
pub parameters: Vec<f32>,
pub value: f32,
pub evaluation_time: Option<std::time::Duration>,
}
#[derive(Debug, Clone)]
pub struct BayesianOptResult {
pub best_point: DataPoint,
pub history: Vec<DataPoint>,
pub evaluations: usize,
pub converged: bool,
pub convergence_reason: String,
pub acquisition_history: Vec<f32>,
}
#[derive(Debug)]
pub struct GaussianProcess {
config: GaussianProcessConfig,
x_train: Vec<Vec<f32>>,
y_train: Vec<f32>,
gram_matrix: Vec<Vec<f32>>,
chol_gram: Vec<Vec<f32>>,
alpha: Vec<f32>,
y_mean: f32,
y_std: f32,
fitted: bool,
}
impl GaussianProcess {
pub fn new(config: GaussianProcessConfig) -> Self {
Self {
config,
x_train: Vec::new(),
y_train: Vec::new(),
gram_matrix: Vec::new(),
chol_gram: Vec::new(),
alpha: Vec::new(),
y_mean: 0.0,
y_std: 1.0,
fitted: false,
}
}
pub fn fit(&mut self, x_train: Vec<Vec<f32>>, y_train: Vec<f32>) -> OptimizerResult<()> {
if x_train.is_empty() || y_train.is_empty() || x_train.len() != y_train.len() {
return Err(OptimizerError::InvalidInput(
"Invalid training data".to_string(),
));
}
self.x_train = x_train;
self.y_train = y_train;
self.y_mean = self.y_train.iter().sum::<f32>() / self.y_train.len() as f32;
let var = self
.y_train
.iter()
.map(|&y| (y - self.y_mean).powi(2))
.sum::<f32>()
/ self.y_train.len() as f32;
self.y_std = var.sqrt().max(1e-6);
let y_normalized: Vec<f32> = self
.y_train
.iter()
.map(|&y| (y - self.y_mean) / self.y_std)
.collect();
let n = self.x_train.len();
self.gram_matrix = vec![vec![0.0; n]; n];
for i in 0..n {
for j in 0..n {
self.gram_matrix[i][j] = self.kernel(&self.x_train[i], &self.x_train[j]);
if i == j {
self.gram_matrix[i][j] += self.config.noise_level;
}
}
}
self.chol_gram = self.cholesky_decomposition(&self.gram_matrix)?;
self.alpha = self.solve_triangular(&self.chol_gram, &y_normalized)?;
self.fitted = true;
Ok(())
}
pub fn predict(&self, x_test: &[f32]) -> OptimizerResult<(f32, f32)> {
if !self.fitted {
return Err(OptimizerError::InvalidInput("Model not fitted".to_string()));
}
let k_star: Vec<f32> = self
.x_train
.iter()
.map(|x_train| self.kernel(x_test, x_train))
.collect();
let mut mean = 0.0;
for i in 0..self.alpha.len() {
mean += self.alpha[i] * k_star[i];
}
mean = mean * self.y_std + self.y_mean;
let k_star_star = self.kernel(x_test, x_test);
let v = self.solve_triangular(&self.chol_gram, &k_star)?;
let var = k_star_star - v.iter().map(|x| x * x).sum::<f32>();
let std = (var.max(0.0).sqrt() * self.y_std).max(1e-6);
Ok((mean, std))
}
fn kernel(&self, x1: &[f32], x2: &[f32]) -> f32 {
match &self.config.kernel {
KernelType::RBF { length_scale } => {
let dist_sq = x1
.iter()
.zip(x2.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f32>();
(-0.5 * dist_sq / length_scale.powi(2)).exp()
}
KernelType::Matern15 { length_scale } => {
let dist = x1
.iter()
.zip(x2.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f32>()
.sqrt();
let scaled_dist = dist * 3.0_f32.sqrt() / length_scale;
(1.0 + scaled_dist) * (-scaled_dist).exp()
}
KernelType::Matern25 { length_scale } => {
let dist = x1
.iter()
.zip(x2.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f32>()
.sqrt();
let scaled_dist = dist * 5.0_f32.sqrt() / length_scale;
(1.0 + scaled_dist + scaled_dist.powi(2) / 3.0) * (-scaled_dist).exp()
}
KernelType::Linear { variance } => {
variance * x1.iter().zip(x2.iter()).map(|(a, b)| a * b).sum::<f32>()
}
KernelType::RationalQuadratic {
length_scale,
alpha,
} => {
let dist_sq = x1
.iter()
.zip(x2.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f32>();
(1.0 + dist_sq / (2.0 * alpha * length_scale.powi(2))).powf(-alpha)
}
}
}
fn cholesky_decomposition(&self, matrix: &[Vec<f32>]) -> OptimizerResult<Vec<Vec<f32>>> {
let n = matrix.len();
let mut l = vec![vec![0.0; n]; n];
for i in 0..n {
for j in 0..=i {
if i == j {
let sum: f32 = (0..j).map(|k| (l[j][k] as f32).powi(2)).sum();
let val = matrix[j][j] - sum;
if val <= 0.0 {
return Err(OptimizerError::NumericalError(
"Matrix not positive definite".to_string(),
));
}
l[j][j] = val.sqrt();
} else {
let sum: f32 = (0..j).map(|k| l[i][k] * l[j][k]).sum();
l[i][j] = (matrix[i][j] - sum) / l[j][j];
}
}
}
Ok(l)
}
fn solve_triangular(&self, l: &[Vec<f32>], b: &[f32]) -> OptimizerResult<Vec<f32>> {
let n = l.len();
let mut x = vec![0.0; n];
for i in 0..n {
let sum: f32 = (0..i).map(|j| l[i][j] * x[j]).sum();
x[i] = (b[i] - sum) / l[i][i];
}
for i in (0..n).rev() {
let sum: f32 = ((i + 1)..n).map(|j| l[j][i] * x[j]).sum();
x[i] = (x[i] - sum) / l[i][i];
}
Ok(x)
}
}
#[derive(Debug)]
pub struct BayesianOptimizer {
config: BayesianOptConfig,
gp: GaussianProcess,
history: Vec<DataPoint>,
best_point: Option<DataPoint>,
}
impl BayesianOptimizer {
pub fn new(config: BayesianOptConfig) -> Self {
let gp = GaussianProcess::new(config.gp_config.clone());
Self {
config,
gp,
history: Vec::new(),
best_point: None,
}
}
pub fn optimize<F: ObjectiveFunction>(
&mut self,
objective: &F,
) -> OptimizerResult<BayesianOptResult> {
let dimension = objective.dimension();
let (lower_bounds, upper_bounds) = objective.bounds();
if lower_bounds.len() != dimension || upper_bounds.len() != dimension {
return Err(OptimizerError::InvalidInput(
"Bounds dimension mismatch".to_string(),
));
}
use scirs2_core::random::{Random, Rng};
let mut rng = if let Some(seed) = self.config.seed {
Random::seed(seed)
} else {
Random::seed(0)
};
let mut acquisition_history = Vec::new();
for i in 0..self
.config
.initial_random_evaluations
.min(self.config.max_evaluations)
{
let params = self.sample_random_point(&mut rng, &lower_bounds, &upper_bounds);
let start_time = std::time::Instant::now();
let value = objective.evaluate(¶ms)?;
let evaluation_time = start_time.elapsed();
let point = DataPoint {
parameters: params,
value,
evaluation_time: Some(evaluation_time),
};
self.add_observation(point.clone());
if self.config.verbose {
println!(
"Initial evaluation {}: f({:?}) = {:.6e}",
i + 1,
point.parameters,
point.value
);
}
}
let mut evaluations = self.config.initial_random_evaluations;
while evaluations < self.config.max_evaluations {
let x_train: Vec<Vec<f32>> =
self.history.iter().map(|p| p.parameters.clone()).collect();
let y_train: Vec<f32> = self.history.iter().map(|p| p.value).collect();
self.gp.fit(x_train, y_train)?;
let next_point = self.optimize_acquisition(&mut rng, &lower_bounds, &upper_bounds)?;
let start_time = std::time::Instant::now();
let value = objective.evaluate(&next_point)?;
let evaluation_time = start_time.elapsed();
let point = DataPoint {
parameters: next_point,
value,
evaluation_time: Some(evaluation_time),
};
let acq_value = self.compute_acquisition(&point.parameters)?;
acquisition_history.push(acq_value);
self.add_observation(point.clone());
evaluations += 1;
if self.config.verbose {
println!(
"Iteration {}: f({:?}) = {:.6e}, Best = {:.6e}",
evaluations,
point.parameters,
point.value,
self.best_point
.as_ref()
.expect("best_point should exist after add_observation")
.value
);
}
}
Ok(BayesianOptResult {
best_point: self
.best_point
.clone()
.expect("best_point should exist after optimization"),
history: self.history.clone(),
evaluations,
converged: false, convergence_reason: "Maximum evaluations reached".to_string(),
acquisition_history,
})
}
fn add_observation(&mut self, point: DataPoint) {
if self.best_point.is_none()
|| point.value
< self
.best_point
.as_ref()
.expect("best_point should exist after is_none check")
.value
{
self.best_point = Some(point.clone());
}
self.history.push(point);
}
fn sample_random_point<R: scirs2_core::random::Rng>(
&self,
rng: &mut R,
lower: &[f32],
upper: &[f32],
) -> Vec<f32> {
(0..lower.len())
.map(|i| rng.random::<f32>() * (upper[i] - lower[i]) + lower[i])
.collect()
}
fn optimize_acquisition<R: scirs2_core::random::Rng>(
&self,
rng: &mut R,
lower: &[f32],
upper: &[f32],
) -> OptimizerResult<Vec<f32>> {
let mut best_point = Vec::new();
let mut best_acquisition = f32::NEG_INFINITY;
for _ in 0..self.config.acquisition_restarts {
let start_point = self.sample_random_point(rng, lower, upper);
let optimized_point = self.local_optimize_acquisition(start_point, lower, upper)?;
let acq_value = self.compute_acquisition(&optimized_point)?;
if acq_value > best_acquisition {
best_acquisition = acq_value;
best_point = optimized_point;
}
}
if best_point.is_empty() {
best_point = self.sample_random_point(rng, lower, upper);
}
Ok(best_point)
}
fn local_optimize_acquisition(
&self,
start_point: Vec<f32>,
lower: &[f32],
upper: &[f32],
) -> OptimizerResult<Vec<f32>> {
let mut best_point = start_point;
let mut best_value = self.compute_acquisition(&best_point)?;
use scirs2_core::random::{Random, Rng};
let mut rng = Random::seed(0);
let step_size = 0.1;
for _ in 0..100 {
let mut candidate = best_point.clone();
for i in 0..candidate.len() {
let perturbation =
(rng.random::<f32>() * 2.0 - 1.0) * step_size * (upper[i] - lower[i]);
candidate[i] = (candidate[i] + perturbation).max(lower[i]).min(upper[i]);
}
let value = self.compute_acquisition(&candidate)?;
if value > best_value {
best_value = value;
best_point = candidate;
}
}
Ok(best_point)
}
fn compute_acquisition(&self, point: &[f32]) -> OptimizerResult<f32> {
let (mean, std) = self.gp.predict(point)?;
let best_value = self
.best_point
.as_ref()
.expect("best_point should exist for acquisition computation")
.value;
match &self.config.acquisition_function {
AcquisitionFunction::ExpectedImprovement { xi } => {
let improvement = best_value - mean - xi;
if std <= 0.0 {
return Ok(0.0);
}
let z = improvement / std;
Ok(improvement * self.normal_cdf(z) + std * self.normal_pdf(z))
}
AcquisitionFunction::ProbabilityOfImprovement { xi } => {
let improvement = best_value - mean - xi;
if std <= 0.0 {
return Ok(0.0);
}
let z = improvement / std;
Ok(self.normal_cdf(z))
}
AcquisitionFunction::UpperConfidenceBound { kappa } => {
Ok(-(mean - kappa * std)) }
_ => {
Ok(-(mean - std)) }
}
}
fn normal_cdf(&self, x: f32) -> f32 {
0.5 * (1.0 + self.erf(x / 2.0_f32.sqrt()))
}
fn normal_pdf(&self, x: f32) -> f32 {
(2.0 * std::f32::consts::PI).sqrt().recip() * (-0.5 * x * x).exp()
}
fn erf(&self, x: f32) -> f32 {
let a1 = 0.254829592;
let a2 = -0.284496736;
let a3 = 1.421413741;
let a4 = -1.453152027;
let a5 = 1.061405429;
let p = 0.3275911;
let sign = if x < 0.0 { -1.0 } else { 1.0 };
let x = x.abs();
let t = 1.0 / (1.0 + p * x);
let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-x * x).exp();
sign * y
}
}
pub mod test_functions {
use super::*;
pub struct Quadratic1D;
impl ObjectiveFunction for Quadratic1D {
fn evaluate(&self, parameters: &[f32]) -> OptimizerResult<f32> {
if parameters.len() != 1 {
return Err(OptimizerError::InvalidInput(
"Expected 1D input".to_string(),
));
}
Ok((parameters[0] - 0.5).powi(2))
}
fn dimension(&self) -> usize {
1
}
fn bounds(&self) -> (Vec<f32>, Vec<f32>) {
(vec![0.0], vec![1.0])
}
fn name(&self) -> &str {
"Quadratic 1D"
}
}
pub struct Branin;
impl ObjectiveFunction for Branin {
fn evaluate(&self, parameters: &[f32]) -> OptimizerResult<f32> {
if parameters.len() != 2 {
return Err(OptimizerError::InvalidInput(
"Expected 2D input".to_string(),
));
}
let x1 = parameters[0];
let x2 = parameters[1];
let a = 1.0;
let b = 5.1 / (4.0 * std::f32::consts::PI.powi(2));
let c = 5.0 / std::f32::consts::PI;
let r = 6.0;
let s = 10.0;
let t = 1.0 / (8.0 * std::f32::consts::PI);
let term1 = a * (x2 - b * x1.powi(2) + c * x1 - r).powi(2);
let term2 = s * (1.0 - t) * x1.cos();
let term3 = s;
Ok(term1 + term2 + term3)
}
fn dimension(&self) -> usize {
2
}
fn bounds(&self) -> (Vec<f32>, Vec<f32>) {
(vec![-5.0, 0.0], vec![10.0, 15.0])
}
fn name(&self) -> &str {
"Branin"
}
}
}
#[cfg(test)]
mod tests {
use super::test_functions::*;
use super::*;
#[test]
fn test_gaussian_process_fit_predict() {
let config = GaussianProcessConfig::default();
let mut gp = GaussianProcess::new(config);
let x_train = vec![vec![0.0], vec![0.5], vec![1.0]];
let y_train = vec![0.0, 0.25, 1.0];
gp.fit(x_train, y_train).unwrap();
let (mean, std) = gp.predict(&[0.75]).unwrap();
assert!(mean >= 0.0 && mean <= 1.0);
assert!(std > 0.0);
}
#[test]
fn test_bayesian_optimization_quadratic() {
let config = BayesianOptConfig {
max_evaluations: 20,
initial_random_evaluations: 5,
acquisition_function: AcquisitionFunction::ExpectedImprovement { xi: 0.01 },
seed: Some(42),
verbose: false,
..Default::default()
};
let mut optimizer = BayesianOptimizer::new(config);
let objective = Quadratic1D;
let result = optimizer.optimize(&objective).unwrap();
assert!(result.best_point.parameters[0] > 0.3 && result.best_point.parameters[0] < 0.7);
assert!(result.best_point.value < 0.1);
assert_eq!(result.evaluations, 20);
}
#[test]
fn test_bayesian_optimization_branin() {
let config = BayesianOptConfig {
max_evaluations: 30,
initial_random_evaluations: 10,
acquisition_function: AcquisitionFunction::UpperConfidenceBound { kappa: 2.576 },
seed: Some(42),
verbose: false,
..Default::default()
};
let mut optimizer = BayesianOptimizer::new(config);
let objective = Branin;
let result = optimizer.optimize(&objective).unwrap();
assert!(result.best_point.value < 5.0); assert_eq!(result.evaluations, 30);
}
}