quantrs2_anneal/bayesian_hyperopt/
config.rs

1//! Bayesian Optimization Configuration Types
2
3use super::gaussian_process::{GaussianProcessSurrogate, KernelFunction};
4use crate::ising::IsingError;
5use scirs2_core::random::ChaCha8Rng;
6use scirs2_core::random::Rng;
7use thiserror::Error;
8
9/// Errors that can occur in Bayesian optimization
10#[derive(Error, Debug)]
11pub enum BayesianOptError {
12    /// Ising model error
13    #[error("Ising error: {0}")]
14    IsingError(#[from] IsingError),
15
16    /// Optimization error
17    #[error("Optimization error: {0}")]
18    OptimizationError(String),
19
20    /// Configuration error
21    #[error("Configuration error: {0}")]
22    ConfigurationError(String),
23
24    /// Gaussian process error
25    #[error("Gaussian process error: {0}")]
26    GaussianProcessError(String),
27
28    /// Acquisition function error
29    #[error("Acquisition function error: {0}")]
30    AcquisitionFunctionError(String),
31
32    /// Constraint handling error
33    #[error("Constraint handling error: {0}")]
34    ConstraintError(String),
35
36    /// Transfer learning error
37    #[error("Transfer learning error: {0}")]
38    TransferLearningError(String),
39
40    /// Convergence error
41    #[error("Convergence error: {0}")]
42    ConvergenceError(String),
43}
44
45/// Result type for Bayesian optimization operations
46pub type BayesianOptResult<T> = Result<T, BayesianOptError>;
47
48/// Parameter types
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum ParameterType {
51    Continuous,
52    Discrete,
53    Categorical,
54}
55
56/// Parameter value
57#[derive(Debug, Clone, PartialEq)]
58pub enum ParameterValue {
59    Continuous(f64),
60    Discrete(i64),
61    Categorical(String),
62}
63
64/// Parameter definition
65#[derive(Debug, Clone)]
66pub struct Parameter {
67    pub name: String,
68    pub param_type: ParameterType,
69    pub bounds: ParameterBounds,
70}
71
72/// Parameter bounds
73#[derive(Debug, Clone)]
74pub enum ParameterBounds {
75    Continuous { min: f64, max: f64 },
76    Discrete { min: i64, max: i64 },
77    Categorical { values: Vec<String> },
78}
79
80/// Parameter space definition
81#[derive(Debug, Clone)]
82pub struct ParameterSpace {
83    pub parameters: Vec<Parameter>,
84}
85
86impl Default for ParameterSpace {
87    fn default() -> Self {
88        Self {
89            parameters: Vec::new(),
90        }
91    }
92}
93
94/// Constraint handling methods (re-exported from constraints module)
95pub use super::constraints::ConstraintHandlingMethod;
96
97/// Scalarization methods (re-exported from `multi_objective` module)
98pub use super::multi_objective::ScalarizationMethod;
99
100/// Optimization history
101#[derive(Debug, Clone)]
102pub struct OptimizationHistory {
103    pub evaluations: Vec<(Vec<f64>, f64)>,
104    pub best_values: Vec<f64>,
105    pub iteration_times: Vec<f64>,
106}
107
108impl Default for OptimizationHistory {
109    fn default() -> Self {
110        Self {
111            evaluations: Vec::new(),
112            best_values: Vec::new(),
113            iteration_times: Vec::new(),
114        }
115    }
116}
117
118/// Objective function trait
119pub trait ObjectiveFunction {
120    fn evaluate(&self, parameters: &[f64]) -> f64;
121    fn get_bounds(&self) -> Vec<(f64, f64)>;
122}
123
124/// Bayesian optimization metrics
125#[derive(Debug, Clone)]
126pub struct BayesianOptMetrics {
127    pub convergence_rate: f64,
128    pub regret: Vec<f64>,
129    pub acquisition_time: f64,
130    pub gp_training_time: f64,
131}
132
133impl Default for BayesianOptMetrics {
134    fn default() -> Self {
135        Self {
136            convergence_rate: 0.0,
137            regret: Vec::new(),
138            acquisition_time: 0.0,
139            gp_training_time: 0.0,
140        }
141    }
142}
143
144/// Main Bayesian hyperoptimizer
145#[derive(Debug)]
146pub struct BayesianHyperoptimizer {
147    pub config: BayesianOptConfig,
148    pub parameter_space: ParameterSpace,
149    pub history: OptimizationHistory,
150    pub gp_model: Option<GaussianProcessSurrogate>,
151    pub current_best_value: f64,
152    pub metrics: BayesianOptMetrics,
153}
154
155impl Default for BayesianHyperoptimizer {
156    fn default() -> Self {
157        Self {
158            config: BayesianOptConfig::default(),
159            parameter_space: ParameterSpace::default(),
160            history: OptimizationHistory::default(),
161            gp_model: None,
162            current_best_value: f64::INFINITY,
163            metrics: BayesianOptMetrics::default(),
164        }
165    }
166}
167
168impl BayesianHyperoptimizer {
169    /// Create new Bayesian hyperoptimizer with configuration
170    #[must_use]
171    pub fn new(config: BayesianOptConfig, parameter_space: ParameterSpace) -> Self {
172        Self {
173            config,
174            parameter_space,
175            history: OptimizationHistory::default(),
176            gp_model: None,
177            current_best_value: f64::INFINITY,
178            metrics: BayesianOptMetrics::default(),
179        }
180    }
181
182    /// Run Bayesian optimization
183    pub fn optimize<F>(&mut self, objective_function: F) -> BayesianOptResult<Vec<f64>>
184    where
185        F: Fn(&[f64]) -> f64,
186    {
187        use scirs2_core::random::prelude::*;
188        use scirs2_core::random::ChaCha8Rng;
189
190        let mut rng = if let Some(seed) = self.config.seed {
191            ChaCha8Rng::seed_from_u64(seed)
192        } else {
193            ChaCha8Rng::from_rng(&mut thread_rng())
194        };
195
196        let start_time = std::time::Instant::now();
197
198        // Step 1: Generate initial random samples
199        self.generate_initial_samples(&mut rng, &objective_function)?;
200
201        // Step 2: Main optimization loop
202        for iteration in 0..self.config.max_iterations {
203            let iter_start = std::time::Instant::now();
204
205            // Update Gaussian Process model
206            self.update_gp_model()?;
207
208            // Find next point to evaluate using acquisition function
209            let next_point = self.suggest_next_point(&mut rng)?;
210
211            // Evaluate objective function
212            let value = objective_function(&next_point);
213
214            // Update history
215            self.history.evaluations.push((next_point, value));
216
217            // Update best value
218            if value < self.current_best_value {
219                self.current_best_value = value;
220            }
221            self.history.best_values.push(self.current_best_value);
222
223            // Record iteration time
224            let iter_time = iter_start.elapsed().as_secs_f64();
225            self.history.iteration_times.push(iter_time);
226
227            // Check convergence
228            if self.check_convergence()? {
229                println!(
230                    "Bayesian optimization converged after {} iterations",
231                    iteration + 1
232                );
233                break;
234            }
235        }
236
237        // Update final metrics
238        self.metrics.convergence_rate = self.calculate_convergence_rate();
239        self.metrics.regret = self.calculate_regret();
240
241        // Return best parameter values found
242        self.get_best_parameters()
243    }
244
245    /// Generate initial random samples
246    fn generate_initial_samples<F>(
247        &mut self,
248        rng: &mut ChaCha8Rng,
249        objective_function: &F,
250    ) -> BayesianOptResult<()>
251    where
252        F: Fn(&[f64]) -> f64,
253    {
254        for _ in 0..self.config.initial_samples {
255            let sample = self.sample_random_point(rng)?;
256            let value = objective_function(&sample);
257
258            self.history.evaluations.push((sample, value));
259
260            if value < self.current_best_value {
261                self.current_best_value = value;
262            }
263            self.history.best_values.push(self.current_best_value);
264        }
265
266        Ok(())
267    }
268
269    /// Sample a random point from the parameter space
270    fn sample_random_point(&self, rng: &mut ChaCha8Rng) -> BayesianOptResult<Vec<f64>> {
271        let mut point = Vec::new();
272
273        for param in &self.parameter_space.parameters {
274            match &param.bounds {
275                ParameterBounds::Continuous { min, max } => {
276                    let value = rng.gen_range(*min..*max);
277                    point.push(value);
278                }
279                ParameterBounds::Discrete { min, max } => {
280                    let value = rng.gen_range(*min..*max) as f64;
281                    point.push(value);
282                }
283                ParameterBounds::Categorical { values } => {
284                    let index = rng.gen_range(0..values.len()) as f64;
285                    point.push(index);
286                }
287            }
288        }
289
290        Ok(point)
291    }
292
293    /// Update Gaussian Process model with current data
294    fn update_gp_model(&mut self) -> BayesianOptResult<()> {
295        if self.history.evaluations.is_empty() {
296            return Err(BayesianOptError::GaussianProcessError(
297                "No data available for GP model".to_string(),
298            ));
299        }
300
301        let gp_start = std::time::Instant::now();
302
303        // Extract training data
304        let x_data: Vec<Vec<f64>> = self
305            .history
306            .evaluations
307            .iter()
308            .map(|(x, _)| x.clone())
309            .collect();
310        let y_data: Vec<f64> = self.history.evaluations.iter().map(|(_, y)| *y).collect();
311
312        // Create or update GP model
313        let model = GaussianProcessSurrogate {
314            kernel: KernelFunction::RBF,
315            noise_variance: 1e-6,
316            mean_function: super::gaussian_process::MeanFunction::Zero,
317        };
318
319        self.gp_model = Some(model);
320        self.metrics.gp_training_time = gp_start.elapsed().as_secs_f64();
321
322        Ok(())
323    }
324
325    /// Suggest next point to evaluate using acquisition function
326    fn suggest_next_point(&mut self, rng: &mut ChaCha8Rng) -> BayesianOptResult<Vec<f64>> {
327        let acq_start = std::time::Instant::now();
328
329        let gp_model = self.gp_model.as_ref().ok_or_else(|| {
330            BayesianOptError::GaussianProcessError("GP model not initialized".to_string())
331        })?;
332
333        let mut best_point = self.sample_random_point(rng)?;
334        let mut best_acquisition_value = f64::NEG_INFINITY;
335
336        // Random search for acquisition function optimization
337        // In practice, would use more sophisticated optimization
338        for _ in 0..self.config.acquisition_config.num_restarts * 10 {
339            let candidate = self.sample_random_point(rng)?;
340            let acquisition_value = self.evaluate_acquisition_function(&candidate, gp_model)?;
341
342            if acquisition_value > best_acquisition_value {
343                best_acquisition_value = acquisition_value;
344                best_point = candidate;
345            }
346        }
347
348        // Update acquisition time metric
349        let acq_time = acq_start.elapsed().as_secs_f64();
350        // Note: This overwrites the previous value. In practice, you might want to accumulate or average
351        self.metrics.acquisition_time = acq_time;
352
353        Ok(best_point)
354    }
355
356    /// Evaluate acquisition function at given point
357    fn evaluate_acquisition_function(
358        &self,
359        point: &[f64],
360        gp_model: &GaussianProcessSurrogate,
361    ) -> BayesianOptResult<f64> {
362        let (mean, variance) = gp_model.predict(point)?;
363        let std_dev = variance.sqrt();
364
365        match self.config.acquisition_config.function_type {
366            super::AcquisitionFunctionType::ExpectedImprovement => {
367                self.expected_improvement(mean, std_dev)
368            }
369            super::AcquisitionFunctionType::UpperConfidenceBound => {
370                self.upper_confidence_bound(mean, std_dev)
371            }
372            super::AcquisitionFunctionType::ProbabilityOfImprovement => {
373                self.probability_of_improvement(mean, std_dev)
374            }
375            _ => {
376                // Fallback to Expected Improvement for unimplemented functions
377                self.expected_improvement(mean, std_dev)
378            }
379        }
380    }
381
382    /// Expected Improvement acquisition function
383    fn expected_improvement(&self, mean: f64, std_dev: f64) -> BayesianOptResult<f64> {
384        if std_dev <= 1e-10 {
385            return Ok(0.0);
386        }
387
388        let improvement = self.current_best_value - mean;
389        let z = improvement / std_dev;
390
391        // Approximation of normal CDF and PDF
392        // Using approximation for erf
393        let a1 = 0.254_829_592;
394        let a2 = -0.284_496_736;
395        let a3 = 1.421_413_741;
396        let a4 = -1.453_152_027;
397        let a5 = 1.061_405_429;
398        let p = 0.3_275_911;
399        let sign = if z < 0.0 { -1.0 } else { 1.0 };
400        let z_abs = z.abs() / std::f64::consts::SQRT_2;
401        let t = 1.0 / (1.0 + p * z_abs);
402        let erf = sign
403            * ((a5 * t + a4).mul_add(t, a3).mul_add(t, a2).mul_add(t, a1) * t)
404                .mul_add(-(-z_abs * z_abs).exp(), 1.0);
405        let phi = 0.5 * (1.0 + erf);
406        let pdf = (1.0 / (std::f64::consts::TAU.sqrt())) * (-0.5 * z * z).exp();
407
408        let ei = improvement.mul_add(phi, std_dev * pdf);
409        Ok(ei.max(0.0))
410    }
411
412    /// Upper Confidence Bound acquisition function
413    fn upper_confidence_bound(&self, mean: f64, std_dev: f64) -> BayesianOptResult<f64> {
414        let beta = self.config.acquisition_config.exploration_factor;
415        Ok(beta.mul_add(std_dev, -mean)) // Negative because we're minimizing
416    }
417
418    /// Probability of Improvement acquisition function
419    fn probability_of_improvement(&self, mean: f64, std_dev: f64) -> BayesianOptResult<f64> {
420        if std_dev <= 1e-10 {
421            return Ok(0.0);
422        }
423
424        let z = (self.current_best_value - mean) / std_dev;
425        // Using approximation for erf (same as above)
426        let a1 = 0.254_829_592;
427        let a2 = -0.284_496_736;
428        let a3 = 1.421_413_741;
429        let a4 = -1.453_152_027;
430        let a5 = 1.061_405_429;
431        let p = 0.3_275_911;
432        let sign = if z < 0.0 { -1.0 } else { 1.0 };
433        let z_abs = z.abs() / std::f64::consts::SQRT_2;
434        let t = 1.0 / (1.0 + p * z_abs);
435        let erf = sign
436            * ((a5 * t + a4).mul_add(t, a3).mul_add(t, a2).mul_add(t, a1) * t)
437                .mul_add(-(-z_abs * z_abs).exp(), 1.0);
438        let pi = 0.5 * (1.0 + erf);
439        Ok(pi)
440    }
441
442    /// Check convergence criteria
443    fn check_convergence(&self) -> BayesianOptResult<bool> {
444        if self.history.best_values.len() < 2 {
445            return Ok(false);
446        }
447
448        // Simple convergence check: improvement in last few iterations
449        let recent_window = 5.min(self.history.best_values.len());
450        let recent_best =
451            self.history.best_values[self.history.best_values.len() - recent_window..].to_vec();
452
453        let improvement = recent_best.first().unwrap_or(&0.0) - recent_best.last().unwrap_or(&0.0);
454        let relative_improvement =
455            improvement.abs() / (recent_best.first().unwrap_or(&0.0).abs() + 1e-10);
456
457        Ok(relative_improvement < 1e-6)
458    }
459
460    /// Calculate convergence rate
461    fn calculate_convergence_rate(&self) -> f64 {
462        if self.history.best_values.len() < 2 {
463            return 0.0;
464        }
465
466        let initial = self.history.best_values[0];
467        let final_val = *self.history.best_values.last().unwrap_or(&0.0);
468
469        if initial.abs() < 1e-10 {
470            return 0.0;
471        }
472
473        (initial - final_val) / initial.abs()
474    }
475
476    /// Calculate regret over time
477    fn calculate_regret(&self) -> Vec<f64> {
478        if self.history.best_values.is_empty() {
479            return Vec::new();
480        }
481
482        let global_best = *self.history.best_values.last().unwrap_or(&0.0);
483        self.history
484            .best_values
485            .iter()
486            .map(|&v| v - global_best)
487            .collect()
488    }
489
490    /// Get best parameters found during optimization
491    fn get_best_parameters(&self) -> BayesianOptResult<Vec<f64>> {
492        self.history
493            .evaluations
494            .iter()
495            .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
496            .map(|(params, _)| params.clone())
497            .ok_or_else(|| BayesianOptError::OptimizationError("No evaluations found".to_string()))
498    }
499
500    /// Get optimization metrics
501    #[must_use]
502    pub const fn get_metrics(&self) -> &BayesianOptMetrics {
503        &self.metrics
504    }
505
506    /// Get optimization history
507    #[must_use]
508    pub const fn get_history(&self) -> &OptimizationHistory {
509        &self.history
510    }
511}
512
513/// Configuration for Bayesian optimization
514#[derive(Debug, Clone)]
515pub struct BayesianOptConfig {
516    /// Number of optimization iterations
517    pub max_iterations: usize,
518    /// Number of initial random samples
519    pub initial_samples: usize,
520    /// Acquisition function configuration
521    pub acquisition_config: AcquisitionConfig,
522    /// Gaussian process configuration
523    pub gp_config: GaussianProcessConfig,
524    /// Multi-objective configuration
525    pub multi_objective_config: MultiObjectiveConfig,
526    /// Constraint handling configuration
527    pub constraint_config: ConstraintConfig,
528    /// Convergence criteria
529    pub convergence_config: ConvergenceConfig,
530    /// Parallel optimization settings
531    pub parallel_config: ParallelConfig,
532    /// Transfer learning settings
533    pub transfer_config: TransferConfig,
534    /// Random seed
535    pub seed: Option<u64>,
536}
537
538impl Default for BayesianOptConfig {
539    fn default() -> Self {
540        Self {
541            max_iterations: 100,
542            initial_samples: 10,
543            acquisition_config: AcquisitionConfig::default(),
544            gp_config: GaussianProcessConfig::default(),
545            multi_objective_config: MultiObjectiveConfig::default(),
546            constraint_config: ConstraintConfig::default(),
547            convergence_config: ConvergenceConfig::default(),
548            parallel_config: ParallelConfig::default(),
549            transfer_config: TransferConfig::default(),
550            seed: None,
551        }
552    }
553}
554
555// Forward declarations for types that will be defined in other modules
556use super::{
557    acquisition::AcquisitionConfig, constraints::ConstraintConfig, convergence::ConvergenceConfig,
558    gaussian_process::GaussianProcessConfig, multi_objective::MultiObjectiveConfig,
559    parallel::ParallelConfig, transfer::TransferConfig,
560};