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 thiserror::Error;
8
9#[derive(Error, Debug)]
11pub enum BayesianOptError {
12 #[error("Ising error: {0}")]
14 IsingError(#[from] IsingError),
15
16 #[error("Optimization error: {0}")]
18 OptimizationError(String),
19
20 #[error("Configuration error: {0}")]
22 ConfigurationError(String),
23
24 #[error("Gaussian process error: {0}")]
26 GaussianProcessError(String),
27
28 #[error("Acquisition function error: {0}")]
30 AcquisitionFunctionError(String),
31
32 #[error("Constraint handling error: {0}")]
34 ConstraintError(String),
35
36 #[error("Transfer learning error: {0}")]
38 TransferLearningError(String),
39
40 #[error("Convergence error: {0}")]
42 ConvergenceError(String),
43}
44
45pub type BayesianOptResult<T> = Result<T, BayesianOptError>;
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum ParameterType {
51 Continuous,
52 Discrete,
53 Categorical,
54}
55
56#[derive(Debug, Clone, PartialEq)]
58pub enum ParameterValue {
59 Continuous(f64),
60 Discrete(i64),
61 Categorical(String),
62}
63
64#[derive(Debug, Clone)]
66pub struct Parameter {
67 pub name: String,
68 pub param_type: ParameterType,
69 pub bounds: ParameterBounds,
70}
71
72#[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#[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
94pub use super::constraints::ConstraintHandlingMethod;
96
97pub use super::multi_objective::ScalarizationMethod;
99
100#[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
118pub trait ObjectiveFunction {
120 fn evaluate(&self, parameters: &[f64]) -> f64;
121 fn get_bounds(&self) -> Vec<(f64, f64)>;
122}
123
124#[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#[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 #[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 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 self.generate_initial_samples(&mut rng, &objective_function)?;
200
201 for iteration in 0..self.config.max_iterations {
203 let iter_start = std::time::Instant::now();
204
205 self.update_gp_model()?;
207
208 let next_point = self.suggest_next_point(&mut rng)?;
210
211 let value = objective_function(&next_point);
213
214 self.history.evaluations.push((next_point, value));
216
217 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 let iter_time = iter_start.elapsed().as_secs_f64();
225 self.history.iteration_times.push(iter_time);
226
227 if self.check_convergence()? {
229 println!(
230 "Bayesian optimization converged after {} iterations",
231 iteration + 1
232 );
233 break;
234 }
235 }
236
237 self.metrics.convergence_rate = self.calculate_convergence_rate();
239 self.metrics.regret = self.calculate_regret();
240
241 self.get_best_parameters()
243 }
244
245 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 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 ¶m.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 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 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 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 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: &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 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};