quantrs2_anneal/bayesian_hyperopt/
config.rs1use 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#[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<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 #[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 = 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 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 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 let acq_time = acq_start.elapsed().as_secs_f64();
351 self.metrics.acquisition_time = acq_time;
353
354 Ok(best_point)
355 }
356
357 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 self.expected_improvement(mean, std_dev)
379 }
380 }
381 }
382
383 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 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 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)) }
418
419 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 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 fn check_convergence(&self) -> BayesianOptResult<bool> {
445 if self.history.best_values.len() < 2 {
446 return Ok(false);
447 }
448
449 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 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 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 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 #[must_use]
503 pub const fn get_metrics(&self) -> &BayesianOptMetrics {
504 &self.metrics
505 }
506
507 #[must_use]
509 pub const fn get_history(&self) -> &OptimizationHistory {
510 &self.history
511 }
512}
513
514#[derive(Debug, Clone)]
516pub struct BayesianOptConfig {
517 pub max_iterations: usize,
519 pub initial_samples: usize,
521 pub acquisition_config: AcquisitionConfig,
523 pub gp_config: GaussianProcessConfig,
525 pub multi_objective_config: MultiObjectiveConfig,
527 pub constraint_config: ConstraintConfig,
529 pub convergence_config: ConvergenceConfig,
531 pub parallel_config: ParallelConfig,
533 pub transfer_config: TransferConfig,
535 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
556use super::{
558 acquisition::AcquisitionConfig, constraints::ConstraintConfig, convergence::ConvergenceConfig,
559 gaussian_process::GaussianProcessConfig, multi_objective::MultiObjectiveConfig,
560 parallel::ParallelConfig, transfer::TransferConfig,
561};