Skip to main content

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