quantrs2_anneal/bayesian_hyperopt/
config.rs1use super::gaussian_process::GaussianProcessModel;
4use crate::ising::IsingError;
5use scirs2_core::random::ChaCha8Rng;
6use scirs2_core::random::Rng;
7use scirs2_core::RngExt;
8use thiserror::Error;
9
10#[derive(Error, Debug)]
12pub enum BayesianOptError {
13 #[error("Ising error: {0}")]
15 IsingError(#[from] IsingError),
16
17 #[error("Optimization error: {0}")]
19 OptimizationError(String),
20
21 #[error("Configuration error: {0}")]
23 ConfigurationError(String),
24
25 #[error("Gaussian process error: {0}")]
27 GaussianProcessError(String),
28
29 #[error("Acquisition function error: {0}")]
31 AcquisitionFunctionError(String),
32
33 #[error("Constraint handling error: {0}")]
35 ConstraintError(String),
36
37 #[error("Transfer learning error: {0}")]
39 TransferLearningError(String),
40
41 #[error("Convergence error: {0}")]
43 ConvergenceError(String),
44}
45
46pub type BayesianOptResult<T> = Result<T, BayesianOptError>;
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum ParameterType {
52 Continuous,
53 Discrete,
54 Categorical,
55}
56
57#[derive(Debug, Clone, PartialEq)]
59pub enum ParameterValue {
60 Continuous(f64),
61 Discrete(i64),
62 Categorical(String),
63}
64
65#[derive(Debug, Clone)]
67pub struct Parameter {
68 pub name: String,
69 pub param_type: ParameterType,
70 pub bounds: ParameterBounds,
71}
72
73#[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#[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
95pub use super::constraints::ConstraintHandlingMethod;
97
98pub use super::multi_objective::ScalarizationMethod;
100
101#[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
119pub trait ObjectiveFunction {
121 fn evaluate(&self, parameters: &[f64]) -> f64;
122 fn get_bounds(&self) -> Vec<(f64, f64)>;
123}
124
125#[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#[derive(Debug)]
147pub struct BayesianHyperoptimizer {
148 pub config: BayesianOptConfig,
149 pub parameter_space: ParameterSpace,
150 pub history: OptimizationHistory,
151 pub gp_model: Option<GaussianProcessModel>,
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 #[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 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 self.generate_initial_samples(&mut rng, &objective_function)?;
201
202 for iteration in 0..self.config.max_iterations {
204 let iter_start = std::time::Instant::now();
205
206 self.update_gp_model()?;
208
209 let next_point = self.suggest_next_point(&mut rng)?;
211
212 let value = objective_function(&next_point);
214
215 self.history.evaluations.push((next_point, value));
217
218 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 let iter_time = iter_start.elapsed().as_secs_f64();
226 self.history.iteration_times.push(iter_time);
227
228 if self.check_convergence()? {
230 println!(
231 "Bayesian optimization converged after {} iterations",
232 iteration + 1
233 );
234 break;
235 }
236 }
237
238 self.metrics.convergence_rate = self.calculate_convergence_rate();
240 self.metrics.regret = self.calculate_regret();
241
242 self.get_best_parameters()
244 }
245
246 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 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 ¶m.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 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 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 let model = GaussianProcessModel::new(x_data, y_data, self.config.gp_config.clone())?;
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 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 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 let acq_time = acq_start.elapsed().as_secs_f64();
350 self.metrics.acquisition_time = acq_time;
352
353 Ok(best_point)
354 }
355
356 fn evaluate_acquisition_function(
358 &self,
359 point: &[f64],
360 gp_model: &GaussianProcessModel,
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 self.expected_improvement(mean, std_dev)
378 }
379 }
380 }
381
382 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 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 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)) }
417
418 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 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 fn check_convergence(&self) -> BayesianOptResult<bool> {
444 if self.history.best_values.len() < 2 {
445 return Ok(false);
446 }
447
448 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 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 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 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 #[must_use]
502 pub const fn get_metrics(&self) -> &BayesianOptMetrics {
503 &self.metrics
504 }
505
506 #[must_use]
508 pub const fn get_history(&self) -> &OptimizationHistory {
509 &self.history
510 }
511}
512
513#[derive(Debug, Clone)]
515pub struct BayesianOptConfig {
516 pub max_iterations: usize,
518 pub initial_samples: usize,
520 pub acquisition_config: AcquisitionConfig,
522 pub gp_config: GaussianProcessConfig,
524 pub multi_objective_config: MultiObjectiveConfig,
526 pub constraint_config: ConstraintConfig,
528 pub convergence_config: ConvergenceConfig,
530 pub parallel_config: ParallelConfig,
532 pub transfer_config: TransferConfig,
534 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
555use super::{
557 acquisition::AcquisitionConfig, constraints::ConstraintConfig, convergence::ConvergenceConfig,
558 gaussian_process::GaussianProcessConfig, multi_objective::MultiObjectiveConfig,
559 parallel::ParallelConfig, transfer::TransferConfig,
560};