1use scirs2_core::ndarray::Array1;
7use std::collections::HashMap;
8
9use super::circuit::ParameterizedQuantumCircuit;
10use super::config::{HardwareArchitecture, QMLAlgorithmType, QMLConfig};
11use super::trainer::QuantumMLTrainer;
12use crate::circuit_interfaces::InterfaceCircuit;
13use crate::error::Result;
14
15pub fn benchmark_quantum_ml_algorithms() -> Result<HashMap<String, f64>> {
17 let mut results = HashMap::new();
18
19 let algorithms = vec![
21 QMLAlgorithmType::VQE,
22 QMLAlgorithmType::QAOA,
23 QMLAlgorithmType::QCNN,
24 QMLAlgorithmType::QSVM,
25 ];
26
27 let hardware_archs = vec![
28 HardwareArchitecture::NISQ,
29 HardwareArchitecture::Superconducting,
30 HardwareArchitecture::TrappedIon,
31 ];
32
33 for &algorithm in &algorithms {
34 for &hardware in &hardware_archs {
35 let benchmark_time = benchmark_algorithm_hardware_combination(algorithm, hardware)?;
36 results.insert(format!("{algorithm:?}_{hardware:?}"), benchmark_time);
37 }
38 }
39
40 Ok(results)
41}
42
43fn benchmark_algorithm_hardware_combination(
45 algorithm: QMLAlgorithmType,
46 hardware: HardwareArchitecture,
47) -> Result<f64> {
48 let start = std::time::Instant::now();
49
50 let config = QMLConfig {
51 algorithm_type: algorithm,
52 hardware_architecture: hardware,
53 num_qubits: 4,
54 circuit_depth: 2,
55 num_parameters: 8,
56 max_epochs: 5,
57 batch_size: 4,
58 ..Default::default()
59 };
60
61 let circuit = create_test_circuit(config.num_qubits)?;
63 let parameters = Array1::from_vec(vec![0.1; config.num_parameters]);
64 let parameter_names = (0..config.num_parameters)
65 .map(|i| format!("param_{i}"))
66 .collect();
67
68 let pqc = ParameterizedQuantumCircuit::new(circuit, parameters, parameter_names, hardware);
69
70 let mut trainer = QuantumMLTrainer::new(config, pqc, None)?;
71
72 let loss_fn = |params: &Array1<f64>| -> Result<f64> {
74 Ok(params.iter().map(|&x| x * x).sum::<f64>())
76 };
77
78 let _result = trainer.train(loss_fn)?;
79
80 Ok(start.elapsed().as_secs_f64() * 1000.0)
81}
82
83fn create_test_circuit(num_qubits: usize) -> Result<InterfaceCircuit> {
85 let circuit = InterfaceCircuit::new(num_qubits, 0);
88 Ok(circuit)
89}
90
91pub fn benchmark_gradient_methods() -> Result<HashMap<String, f64>> {
93 let mut results = HashMap::new();
94
95 let methods = vec![
96 "parameter_shift",
97 "finite_differences",
98 "automatic_differentiation",
99 "natural_gradients",
100 ];
101
102 for method in methods {
103 let benchmark_time = benchmark_gradient_method(method)?;
104 results.insert(method.to_string(), benchmark_time);
105 }
106
107 Ok(results)
108}
109
110fn benchmark_gradient_method(method: &str) -> Result<f64> {
112 let start = std::time::Instant::now();
113
114 let test_function = |params: &Array1<f64>| -> Result<f64> {
116 Ok(params
117 .iter()
118 .enumerate()
119 .map(|(i, &x)| (i as f64 + 1.0) * x * x)
120 .sum::<f64>())
121 };
122
123 let test_params = Array1::from_vec(vec![0.1, 0.2, 0.3, 0.4]);
124
125 let test_function_dual = |params: &[Dual64]| -> Dual64 {
130 params
131 .iter()
132 .enumerate()
133 .fold(Dual64::constant(0.0), |acc, (i, &x)| {
134 acc + (x * x) * (i as f64 + 1.0)
135 })
136 };
137
138 match method {
140 "parameter_shift" => {
141 compute_parameter_shift_gradient(&test_function, &test_params)?;
142 }
143 "finite_differences" => {
144 compute_finite_difference_gradient(&test_function, &test_params)?;
145 }
146 "automatic_differentiation" => {
147 compute_autodiff_gradient(&test_function_dual, &test_params)?;
148 }
149 "natural_gradients" => {
150 compute_natural_gradient(&test_function, &test_params)?;
151 }
152 _ => {
153 return Err(crate::error::SimulatorError::InvalidInput(format!(
154 "Unknown gradient method: {method}"
155 )))
156 }
157 }
158
159 Ok(start.elapsed().as_secs_f64() * 1000.0)
160}
161
162fn compute_parameter_shift_gradient<F>(
164 function: &F,
165 parameters: &Array1<f64>,
166) -> Result<Array1<f64>>
167where
168 F: Fn(&Array1<f64>) -> Result<f64>,
169{
170 let num_params = parameters.len();
171 let mut gradient = Array1::zeros(num_params);
172 let shift = std::f64::consts::PI / 2.0;
173
174 for i in 0..num_params {
175 let mut params_plus = parameters.clone();
176 let mut params_minus = parameters.clone();
177
178 params_plus[i] += shift;
179 params_minus[i] -= shift;
180
181 let loss_plus = function(¶ms_plus)?;
182 let loss_minus = function(¶ms_minus)?;
183
184 gradient[i] = (loss_plus - loss_minus) / 2.0;
185 }
186
187 Ok(gradient)
188}
189
190fn compute_finite_difference_gradient<F>(
192 function: &F,
193 parameters: &Array1<f64>,
194) -> Result<Array1<f64>>
195where
196 F: Fn(&Array1<f64>) -> Result<f64>,
197{
198 let num_params = parameters.len();
199 let mut gradient = Array1::zeros(num_params);
200 let eps = 1e-8;
201
202 for i in 0..num_params {
203 let mut params_plus = parameters.clone();
204 params_plus[i] += eps;
205
206 let loss_plus = function(¶ms_plus)?;
207 let loss_current = function(parameters)?;
208
209 gradient[i] = (loss_plus - loss_current) / eps;
210 }
211
212 Ok(gradient)
213}
214
215#[derive(Debug, Clone, Copy)]
225struct Dual64 {
226 val: f64,
227 deriv: f64,
228}
229
230impl Dual64 {
231 const fn constant(val: f64) -> Self {
234 Self { val, deriv: 0.0 }
235 }
236
237 const fn variable(val: f64) -> Self {
240 Self { val, deriv: 1.0 }
241 }
242}
243
244impl std::ops::Add for Dual64 {
245 type Output = Self;
246 fn add(self, rhs: Self) -> Self {
247 Self {
248 val: self.val + rhs.val,
249 deriv: self.deriv + rhs.deriv,
250 }
251 }
252}
253
254impl std::ops::Mul for Dual64 {
255 type Output = Self;
256 fn mul(self, rhs: Self) -> Self {
257 Self {
259 val: self.val * rhs.val,
260 deriv: self.deriv * rhs.val + self.val * rhs.deriv,
261 }
262 }
263}
264
265impl std::ops::Mul<f64> for Dual64 {
266 type Output = Self;
267 fn mul(self, rhs: f64) -> Self {
268 Self {
269 val: self.val * rhs,
270 deriv: self.deriv * rhs,
271 }
272 }
273}
274
275fn compute_autodiff_gradient<G>(function: &G, parameters: &Array1<f64>) -> Result<Array1<f64>>
285where
286 G: Fn(&[Dual64]) -> Dual64,
287{
288 let num_params = parameters.len();
289 let mut gradient = Array1::zeros(num_params);
290 let mut duals: Vec<Dual64> = parameters.iter().map(|&p| Dual64::constant(p)).collect();
291
292 for i in 0..num_params {
293 duals[i] = Dual64::variable(parameters[i]);
294 gradient[i] = function(&duals).deriv;
295 duals[i] = Dual64::constant(parameters[i]);
296 }
297
298 Ok(gradient)
299}
300
301fn compute_natural_gradient<F>(function: &F, parameters: &Array1<f64>) -> Result<Array1<f64>>
313where
314 F: Fn(&Array1<f64>) -> Result<f64>,
315{
316 let gradient = compute_parameter_shift_gradient(function, parameters)?;
317 let damping = 1e-4;
318 let natural_gradient = gradient.mapv(|g| g / g.mul_add(g, damping));
319 Ok(natural_gradient)
320}
321
322pub fn benchmark_optimizers() -> Result<HashMap<String, f64>> {
324 let mut results = HashMap::new();
325
326 let optimizers = vec!["adam", "sgd", "rmsprop", "lbfgs"];
327
328 for optimizer in optimizers {
329 let benchmark_time = benchmark_optimizer(optimizer)?;
330 results.insert(optimizer.to_string(), benchmark_time);
331 }
332
333 Ok(results)
334}
335
336fn benchmark_optimizer(optimizer: &str) -> Result<f64> {
351 let start = std::time::Instant::now();
352
353 let mut params = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
357 let target = Array1::<f64>::zeros(4);
358 let lr = 0.1;
359 let num_params = params.len();
360
361 match optimizer {
362 "sgd" => {
363 for _iteration in 0..100 {
364 let gradient = ¶ms - ⌖
365 params = ¶ms - lr * &gradient;
366 }
367 }
368 "adam" => {
369 let beta1 = 0.9;
370 let beta2 = 0.999;
371 let eps = 1e-8;
372 let mut m = Array1::<f64>::zeros(num_params);
373 let mut v = Array1::<f64>::zeros(num_params);
374
375 for iteration in 1..=100 {
376 let gradient = ¶ms - ⌖
377 m = beta1 * &m + (1.0 - beta1) * &gradient;
378 v = beta2 * &v + (1.0 - beta2) * gradient.mapv(|g| g * g);
379
380 let bias_correction1 = 1.0 - beta1.powi(iteration);
381 let bias_correction2 = 1.0 - beta2.powi(iteration);
382 let m_hat = &m / bias_correction1;
383 let v_hat = &v / bias_correction2;
384
385 let update = &m_hat / (v_hat.mapv(f64::sqrt) + eps);
386 params = ¶ms - lr * &update;
387 }
388 }
389 "rmsprop" => {
390 let decay = 0.9;
391 let eps = 1e-8;
392 let mut mean_square = Array1::<f64>::zeros(num_params);
393
394 for _iteration in 0..100 {
395 let gradient = ¶ms - ⌖
396 mean_square = decay * &mean_square + (1.0 - decay) * gradient.mapv(|g| g * g);
397
398 let update = &gradient / (mean_square.mapv(f64::sqrt) + eps);
399 params = ¶ms - lr * &update;
400 }
401 }
402 "lbfgs" => {
403 const HISTORY_SIZE: usize = 5;
404 let mut s_history: Vec<Array1<f64>> = Vec::new();
405 let mut y_history: Vec<Array1<f64>> = Vec::new();
406 let mut prev_params: Option<Array1<f64>> = None;
407 let mut prev_gradient: Option<Array1<f64>> = None;
408
409 for _iteration in 0..100 {
410 let gradient = ¶ms - ⌖
411
412 if let (Some(pp), Some(pg)) = (&prev_params, &prev_gradient) {
413 let s = ¶ms - pp;
414 let y = &gradient - pg;
415 if y.dot(&y) > 1e-14 {
416 s_history.push(s);
417 y_history.push(y);
418 if s_history.len() > HISTORY_SIZE {
419 s_history.remove(0);
420 y_history.remove(0);
421 }
422 }
423 }
424
425 let mut q = gradient.clone();
428 let mut alphas = vec![0.0; s_history.len()];
429 let mut rhos = vec![0.0; s_history.len()];
430 for i in (0..s_history.len()).rev() {
431 let rho = 1.0 / y_history[i].dot(&s_history[i]);
432 let alpha = rho * s_history[i].dot(&q);
433 q = &q - alpha * &y_history[i];
434 alphas[i] = alpha;
435 rhos[i] = rho;
436 }
437
438 let gamma = match (s_history.last(), y_history.last()) {
439 (Some(s), Some(y)) => s.dot(y) / y.dot(y),
440 _ => 1.0,
441 };
442 let mut z = &q * gamma;
443 for i in 0..s_history.len() {
444 let beta = rhos[i] * y_history[i].dot(&z);
445 z = &z + (alphas[i] - beta) * &s_history[i];
446 }
447
448 prev_params = Some(params.clone());
449 prev_gradient = Some(gradient);
450 params = ¶ms - lr * &z;
451 }
452 }
453 _ => {
454 return Err(crate::error::SimulatorError::InvalidInput(format!(
455 "Unknown optimizer: {optimizer}"
456 )))
457 }
458 }
459
460 Ok(start.elapsed().as_secs_f64() * 1000.0)
461}
462
463pub fn run_comprehensive_benchmarks() -> Result<HashMap<String, HashMap<String, f64>>> {
465 let mut all_results = HashMap::new();
466
467 all_results.insert("algorithms".to_string(), benchmark_quantum_ml_algorithms()?);
468 all_results.insert("gradients".to_string(), benchmark_gradient_methods()?);
469 all_results.insert("optimizers".to_string(), benchmark_optimizers()?);
470
471 Ok(all_results)
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477
478 #[test]
485 fn test_autodiff_gradient_is_exact_and_distinct_from_parameter_shift() {
486 let test_function_dual = |params: &[Dual64]| -> Dual64 {
487 params
488 .iter()
489 .enumerate()
490 .fold(Dual64::constant(0.0), |acc, (i, &x)| {
491 acc + (x * x) * (i as f64 + 1.0)
492 })
493 };
494 let test_function = |params: &Array1<f64>| -> Result<f64> {
495 Ok(params
496 .iter()
497 .enumerate()
498 .map(|(i, &x)| (i as f64 + 1.0) * x * x)
499 .sum::<f64>())
500 };
501
502 let params = Array1::from_vec(vec![0.1, 0.2, 0.3, 0.4]);
503
504 let autodiff_grad = compute_autodiff_gradient(&test_function_dual, ¶ms)
505 .expect("autodiff gradient should succeed");
506 let parameter_shift_grad = compute_parameter_shift_gradient(&test_function, ¶ms)
507 .expect("parameter-shift gradient should succeed");
508
509 for (i, &x) in params.iter().enumerate() {
510 let expected = 2.0 * (i as f64 + 1.0) * x;
511 assert!(
512 (autodiff_grad[i] - expected).abs() < 1e-10,
513 "autodiff gradient[{i}] = {}, expected exact {expected}",
514 autodiff_grad[i]
515 );
516 }
517
518 let mut any_differs = false;
522 for i in 0..params.len() {
523 if (autodiff_grad[i] - parameter_shift_grad[i]).abs() > 1e-6 {
524 any_differs = true;
525 }
526 }
527 assert!(
528 any_differs,
529 "autodiff and parameter-shift must not be the same algorithm in disguise"
530 );
531 }
532
533 #[test]
538 fn test_natural_gradient_differs_from_parameter_shift() {
539 let test_function = |params: &Array1<f64>| -> Result<f64> {
540 Ok(params
541 .iter()
542 .enumerate()
543 .map(|(i, &x)| (i as f64 + 1.0) * x * x)
544 .sum::<f64>())
545 };
546 let params = Array1::from_vec(vec![0.1, 0.2, 0.3, 0.4]);
547
548 let natural_grad = compute_natural_gradient(&test_function, ¶ms)
549 .expect("natural gradient should succeed");
550 let parameter_shift_grad = compute_parameter_shift_gradient(&test_function, ¶ms)
551 .expect("parameter-shift gradient should succeed");
552
553 let mut any_differs = false;
554 for i in 0..params.len() {
555 if (natural_grad[i] - parameter_shift_grad[i]).abs() > 1e-6 {
556 any_differs = true;
557 }
558 }
559 assert!(
560 any_differs,
561 "natural gradient must apply real Fisher preconditioning, not alias parameter-shift"
562 );
563 }
564
565 #[test]
570 fn test_optimizers_are_genuinely_distinct_update_rules() {
571 fn run(optimizer: &str, iterations: usize) -> Array1<f64> {
572 let mut params = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
573 let target = Array1::<f64>::zeros(4);
574 let lr = 0.1;
575 let num_params = params.len();
576
577 match optimizer {
578 "sgd" => {
579 for _ in 0..iterations {
580 let gradient = ¶ms - ⌖
581 params = ¶ms - lr * &gradient;
582 }
583 }
584 "adam" => {
585 let (beta1, beta2, eps) = (0.9, 0.999, 1e-8);
586 let mut m = Array1::<f64>::zeros(num_params);
587 let mut v = Array1::<f64>::zeros(num_params);
588 for t in 1..=iterations {
589 let gradient = ¶ms - ⌖
590 m = beta1 * &m + (1.0 - beta1) * &gradient;
591 v = beta2 * &v + (1.0 - beta2) * gradient.mapv(|g| g * g);
592 let m_hat = &m / (1.0 - beta1.powi(t as i32));
593 let v_hat = &v / (1.0 - beta2.powi(t as i32));
594 let update = &m_hat / (v_hat.mapv(f64::sqrt) + eps);
595 params = ¶ms - lr * &update;
596 }
597 }
598 "rmsprop" => {
599 let (decay, eps) = (0.9, 1e-8);
600 let mut mean_square = Array1::<f64>::zeros(num_params);
601 for _ in 0..iterations {
602 let gradient = ¶ms - ⌖
603 mean_square =
604 decay * &mean_square + (1.0 - decay) * gradient.mapv(|g| g * g);
605 let update = &gradient / (mean_square.mapv(f64::sqrt) + eps);
606 params = ¶ms - lr * &update;
607 }
608 }
609 _ => unreachable!(),
610 }
611 params
612 }
613
614 let sgd_result = run("sgd", 10);
615 let adam_result = run("adam", 10);
616 let rmsprop_result = run("rmsprop", 10);
617
618 assert!(
619 (0..sgd_result.len()).any(|i| (sgd_result[i] - adam_result[i]).abs() > 1e-6),
620 "adam must diverge from plain sgd once its moment estimates kick in"
621 );
622 assert!(
623 (0..sgd_result.len()).any(|i| (sgd_result[i] - rmsprop_result[i]).abs() > 1e-6),
624 "rmsprop must diverge from plain sgd once its running average kicks in"
625 );
626 assert!(
627 (0..adam_result.len()).any(|i| (adam_result[i] - rmsprop_result[i]).abs() > 1e-6),
628 "adam and rmsprop must be genuinely different update rules"
629 );
630 }
631
632 #[test]
636 fn test_benchmark_optimizer_runs_all_named_optimizers() {
637 for name in ["sgd", "adam", "rmsprop", "lbfgs"] {
638 let elapsed_ms = benchmark_optimizer(name)
639 .unwrap_or_else(|e| panic!("benchmark_optimizer({name}) failed: {e}"));
640 assert!(elapsed_ms >= 0.0);
641 }
642 }
643
644 #[test]
645 fn test_benchmark_optimizer_unknown_name_errors() {
646 let result = benchmark_optimizer("not_a_real_optimizer");
647 assert!(result.is_err());
648 }
649}