Skip to main content

quantrs2_circuit/
scirs2_optimization.rs

1//! `SciRS2` optimization integration for parameter tuning
2//!
3//! This module integrates `SciRS2`'s advanced optimization capabilities for quantum circuit
4//! parameter optimization, variational algorithms, and machine learning-enhanced optimization.
5
6use crate::builder::Circuit;
7use crate::scirs2_matrices::SparseMatrix;
8use quantrs2_core::{
9    error::{QuantRS2Error, QuantRS2Result},
10    gate::GateOp,
11    qubit::QubitId,
12};
13use scirs2_core::Complex64;
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::sync::{Arc, Mutex};
17
18// Placeholder types representing SciRS2 optimization interface
19// In the real implementation, these would be imported from SciRS2
20
21/// Optimization objective function
22pub trait ObjectiveFunction: Send + Sync {
23    /// Evaluate the objective at given parameters
24    fn evaluate(&self, parameters: &[f64]) -> f64;
25
26    /// Compute gradient if available
27    fn gradient(&self, parameters: &[f64]) -> Option<Vec<f64>> {
28        None
29    }
30
31    /// Compute Hessian if available
32    fn hessian(&self, parameters: &[f64]) -> Option<Vec<Vec<f64>>> {
33        None
34    }
35
36    /// Get parameter bounds
37    fn bounds(&self) -> Vec<(f64, f64)>;
38
39    /// Get objective name
40    fn name(&self) -> &str;
41}
42
43/// `SciRS2` optimization algorithms
44#[derive(Debug, Clone, PartialEq)]
45pub enum OptimizationAlgorithm {
46    /// Gradient descent variants
47    GradientDescent { learning_rate: f64, momentum: f64 },
48    /// Adam optimizer
49    Adam {
50        learning_rate: f64,
51        beta1: f64,
52        beta2: f64,
53        epsilon: f64,
54    },
55    /// L-BFGS-B
56    LBFGSB {
57        max_iterations: usize,
58        tolerance: f64,
59    },
60    /// Nelder-Mead simplex
61    NelderMead {
62        max_iterations: usize,
63        tolerance: f64,
64    },
65    /// Simulated annealing
66    SimulatedAnnealing {
67        initial_temperature: f64,
68        cooling_rate: f64,
69        min_temperature: f64,
70    },
71    /// Genetic algorithm
72    GeneticAlgorithm {
73        population_size: usize,
74        mutation_rate: f64,
75        crossover_rate: f64,
76    },
77    /// Particle swarm optimization
78    ParticleSwarm {
79        num_particles: usize,
80        inertia_weight: f64,
81        cognitive_weight: f64,
82        social_weight: f64,
83    },
84    /// Bayesian optimization
85    BayesianOptimization {
86        acquisition_function: AcquisitionFunction,
87        kernel: KernelType,
88        num_initial_samples: usize,
89    },
90    /// Quantum approximate optimization algorithm (QAOA)
91    QAOA {
92        num_layers: usize,
93        classical_optimizer: Box<Self>,
94    },
95}
96
97/// Acquisition functions for Bayesian optimization
98#[derive(Debug, Clone, PartialEq)]
99pub enum AcquisitionFunction {
100    ExpectedImprovement,
101    ProbabilityOfImprovement,
102    UpperConfidenceBound { kappa: f64 },
103    Thompson,
104}
105
106/// Kernel types for Gaussian processes
107#[derive(Debug, Clone, PartialEq)]
108pub enum KernelType {
109    RBF { length_scale: f64 },
110    Matern { nu: f64, length_scale: f64 },
111    Linear { variance: f64 },
112    Periodic { period: f64, length_scale: f64 },
113}
114
115/// Optimization configuration
116pub struct OptimizationConfig {
117    /// Optimization algorithm
118    pub algorithm: OptimizationAlgorithm,
119    /// Maximum number of function evaluations
120    pub max_evaluations: usize,
121    /// Convergence tolerance
122    pub tolerance: f64,
123    /// Random seed for reproducibility
124    pub seed: Option<u64>,
125    /// Parallel evaluation of objective
126    pub parallel: bool,
127    /// Number of threads for parallel evaluation
128    pub num_threads: Option<usize>,
129    /// Progress callback
130    pub progress_callback: Option<Box<dyn Fn(usize, f64) + Send + Sync>>,
131    /// Early stopping criteria
132    pub early_stopping: Option<EarlyStoppingCriteria>,
133}
134
135impl std::fmt::Debug for OptimizationConfig {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        f.debug_struct("OptimizationConfig")
138            .field("algorithm", &self.algorithm)
139            .field("max_evaluations", &self.max_evaluations)
140            .field("tolerance", &self.tolerance)
141            .field("seed", &self.seed)
142            .field("parallel", &self.parallel)
143            .field("num_threads", &self.num_threads)
144            .field(
145                "progress_callback",
146                &self.progress_callback.as_ref().map(|_| "Some(callback)"),
147            )
148            .field("early_stopping", &self.early_stopping)
149            .finish()
150    }
151}
152
153impl Clone for OptimizationConfig {
154    fn clone(&self) -> Self {
155        Self {
156            algorithm: self.algorithm.clone(),
157            max_evaluations: self.max_evaluations,
158            tolerance: self.tolerance,
159            seed: self.seed,
160            parallel: self.parallel,
161            num_threads: self.num_threads,
162            progress_callback: None, // Function pointers can't be cloned
163            early_stopping: self.early_stopping.clone(),
164        }
165    }
166}
167
168/// Early stopping criteria
169#[derive(Debug, Clone)]
170pub struct EarlyStoppingCriteria {
171    /// Patience (number of iterations without improvement)
172    pub patience: usize,
173    /// Minimum change to be considered an improvement
174    pub min_delta: f64,
175    /// Monitor best value or last value
176    pub monitor_best: bool,
177}
178
179/// Optimization result
180#[derive(Debug, Clone)]
181pub struct OptimizationResult {
182    /// Optimal parameters
183    pub optimal_parameters: Vec<f64>,
184    /// Optimal objective value
185    pub optimal_value: f64,
186    /// Number of function evaluations
187    pub num_evaluations: usize,
188    /// Convergence status
189    pub converged: bool,
190    /// Optimization history
191    pub history: OptimizationHistory,
192    /// Additional algorithm-specific information
193    pub algorithm_info: HashMap<String, String>,
194    /// Total optimization time
195    pub optimization_time: std::time::Duration,
196}
197
198/// Optimization history tracking
199#[derive(Debug, Clone)]
200pub struct OptimizationHistory {
201    /// Parameter values at each iteration
202    pub parameters: Vec<Vec<f64>>,
203    /// Objective values at each iteration
204    pub objective_values: Vec<f64>,
205    /// Gradient norms (if available)
206    pub gradient_norms: Vec<f64>,
207    /// Step sizes
208    pub step_sizes: Vec<f64>,
209    /// Timestamps
210    pub timestamps: Vec<std::time::Instant>,
211}
212
213/// Quantum circuit parameter optimizer using `SciRS2`
214pub struct QuantumCircuitOptimizer {
215    /// Current circuit template
216    circuit_template: CircuitTemplate,
217    /// Optimization configuration
218    config: OptimizationConfig,
219    /// Parameter history
220    history: Arc<Mutex<OptimizationHistory>>,
221    /// Best parameters found so far
222    best_parameters: Arc<Mutex<Option<Vec<f64>>>>,
223    /// Best objective value
224    best_value: Arc<Mutex<f64>>,
225}
226
227/// Parameterized circuit template
228#[derive(Debug, Clone)]
229pub struct CircuitTemplate {
230    /// Circuit structure with parameter placeholders
231    pub structure: Vec<ParameterizedGate>,
232    /// Parameter names and bounds
233    pub parameters: Vec<Parameter>,
234    /// Number of qubits
235    pub num_qubits: usize,
236}
237
238/// Parameterized gate in circuit template
239#[derive(Debug, Clone)]
240pub struct ParameterizedGate {
241    /// Gate name
242    pub gate_name: String,
243    /// Qubits the gate acts on
244    pub qubits: Vec<usize>,
245    /// Parameter indices
246    pub parameter_indices: Vec<usize>,
247    /// Fixed parameters (if any)
248    pub fixed_parameters: Vec<f64>,
249}
250
251/// Parameter definition
252#[derive(Debug, Clone)]
253pub struct Parameter {
254    /// Parameter name
255    pub name: String,
256    /// Lower bound
257    pub lower_bound: f64,
258    /// Upper bound
259    pub upper_bound: f64,
260    /// Initial value
261    pub initial_value: f64,
262    /// Whether parameter is discrete
263    pub discrete: bool,
264}
265
266impl QuantumCircuitOptimizer {
267    /// Create a new quantum circuit optimizer
268    #[must_use]
269    pub fn new(template: CircuitTemplate, config: OptimizationConfig) -> Self {
270        Self {
271            circuit_template: template,
272            config,
273            history: Arc::new(Mutex::new(OptimizationHistory {
274                parameters: Vec::new(),
275                objective_values: Vec::new(),
276                gradient_norms: Vec::new(),
277                step_sizes: Vec::new(),
278                timestamps: Vec::new(),
279            })),
280            best_parameters: Arc::new(Mutex::new(None)),
281            best_value: Arc::new(Mutex::new(f64::INFINITY)),
282        }
283    }
284
285    /// Optimize circuit parameters
286    pub fn optimize(
287        &mut self,
288        objective: Arc<dyn ObjectiveFunction>,
289    ) -> QuantRS2Result<OptimizationResult> {
290        let start_time = std::time::Instant::now();
291
292        // Get initial parameters
293        let initial_params: Vec<f64> = self
294            .circuit_template
295            .parameters
296            .iter()
297            .map(|p| p.initial_value)
298            .collect();
299
300        // Validate parameter bounds
301        let bounds = objective.bounds();
302        if bounds.len() != initial_params.len() {
303            return Err(QuantRS2Error::InvalidInput(
304                "Parameter count mismatch with bounds".to_string(),
305            ));
306        }
307
308        // Run optimization based on algorithm
309        let result = match &self.config.algorithm {
310            OptimizationAlgorithm::GradientDescent {
311                learning_rate,
312                momentum,
313            } => self.optimize_gradient_descent(
314                objective,
315                &initial_params,
316                *learning_rate,
317                *momentum,
318            ),
319            OptimizationAlgorithm::Adam {
320                learning_rate,
321                beta1,
322                beta2,
323                epsilon,
324            } => self.optimize_adam(
325                objective,
326                &initial_params,
327                *learning_rate,
328                *beta1,
329                *beta2,
330                *epsilon,
331            ),
332            OptimizationAlgorithm::LBFGSB {
333                max_iterations,
334                tolerance,
335            } => self.optimize_lbfgs(objective, &initial_params, *max_iterations, *tolerance),
336            OptimizationAlgorithm::NelderMead {
337                max_iterations,
338                tolerance,
339            } => self.optimize_nelder_mead(objective, &initial_params, *max_iterations, *tolerance),
340            OptimizationAlgorithm::SimulatedAnnealing {
341                initial_temperature,
342                cooling_rate,
343                min_temperature,
344            } => self.optimize_simulated_annealing(
345                objective,
346                &initial_params,
347                *initial_temperature,
348                *cooling_rate,
349                *min_temperature,
350            ),
351            OptimizationAlgorithm::BayesianOptimization {
352                acquisition_function,
353                kernel,
354                num_initial_samples,
355            } => self.optimize_bayesian(
356                objective,
357                &initial_params,
358                acquisition_function,
359                kernel,
360                *num_initial_samples,
361            ),
362            _ => Err(QuantRS2Error::InvalidInput(
363                "Algorithm not yet implemented".to_string(),
364            )),
365        }?;
366
367        let history = self
368            .history
369            .lock()
370            .map_err(|e| QuantRS2Error::RuntimeError(format!("Failed to lock history: {}", e)))?
371            .clone();
372
373        Ok(OptimizationResult {
374            optimal_parameters: result.0,
375            optimal_value: result.1,
376            num_evaluations: result.2,
377            converged: result.3,
378            history,
379            algorithm_info: HashMap::new(),
380            optimization_time: start_time.elapsed(),
381        })
382    }
383
384    /// Gradient descent optimization
385    fn optimize_gradient_descent(
386        &self,
387        objective: Arc<dyn ObjectiveFunction>,
388        initial_params: &[f64],
389        learning_rate: f64,
390        momentum: f64,
391    ) -> QuantRS2Result<(Vec<f64>, f64, usize, bool)> {
392        let mut params = initial_params.to_vec();
393        let mut velocity = vec![0.0; params.len()];
394        let mut evaluations = 0;
395        let mut best_value = f64::INFINITY;
396        // The Euclidean norm of the previous iteration's parameter update;
397        // this is what `record_iteration` reports as this iteration's
398        // "step size" (there is no step yet before the first update).
399        let mut last_step_size = 0.0;
400
401        for iteration in 0..self.config.max_evaluations {
402            // Evaluate objective
403            let value = objective.evaluate(&params);
404            evaluations += 1;
405
406            // Update best
407            if value < best_value {
408                best_value = value;
409                if let Ok(mut guard) = self.best_parameters.lock() {
410                    *guard = Some(params.clone());
411                }
412                if let Ok(mut guard) = self.best_value.lock() {
413                    *guard = best_value;
414                }
415            }
416
417            // Compute gradient (numerical if not available) *before* recording
418            // history, so `gradient_norms` holds the real gradient driving this
419            // iteration rather than a placeholder.
420            let gradient = if let Some(grad) = objective.gradient(&params) {
421                grad
422            } else {
423                self.numerical_gradient(&*objective, &params)?
424            };
425            let gradient_norm = gradient.iter().map(|g| g * g).sum::<f64>().sqrt();
426
427            // Record history
428            self.record_iteration(&params, value, iteration, gradient_norm, last_step_size);
429
430            // Check convergence
431            if iteration > 0 {
432                let prev_value = self
433                    .history
434                    .lock()
435                    .ok()
436                    .and_then(|h| h.objective_values.get(iteration - 1).copied())
437                    .unwrap_or(value);
438                if (prev_value - value).abs() < self.config.tolerance {
439                    return Ok((params, best_value, evaluations, true));
440                }
441            }
442
443            // Update parameters with momentum
444            for i in 0..params.len() {
445                velocity[i] = momentum.mul_add(velocity[i], -(learning_rate * gradient[i]));
446                params[i] += velocity[i];
447
448                // Apply bounds
449                let bounds = objective.bounds();
450                params[i] = params[i].max(bounds[i].0).min(bounds[i].1);
451            }
452            last_step_size = velocity.iter().map(|v| v * v).sum::<f64>().sqrt();
453
454            // Progress callback
455            if let Some(callback) = &self.config.progress_callback {
456                callback(iteration, value);
457            }
458        }
459
460        Ok((params, best_value, evaluations, false))
461    }
462
463    /// Adam optimization algorithm
464    fn optimize_adam(
465        &self,
466        objective: Arc<dyn ObjectiveFunction>,
467        initial_params: &[f64],
468        learning_rate: f64,
469        beta1: f64,
470        beta2: f64,
471        epsilon: f64,
472    ) -> QuantRS2Result<(Vec<f64>, f64, usize, bool)> {
473        let mut params = initial_params.to_vec();
474        let mut m = vec![0.0; params.len()]; // First moment
475        let mut v = vec![0.0; params.len()]; // Second moment
476        let mut evaluations = 0;
477        let mut best_value = f64::INFINITY;
478        let mut last_step_size = 0.0;
479
480        for iteration in 0..self.config.max_evaluations {
481            let t = iteration + 1;
482
483            // Evaluate objective
484            let value = objective.evaluate(&params);
485            evaluations += 1;
486
487            // Update best
488            if value < best_value {
489                best_value = value;
490                if let Ok(mut guard) = self.best_parameters.lock() {
491                    *guard = Some(params.clone());
492                }
493                if let Ok(mut guard) = self.best_value.lock() {
494                    *guard = best_value;
495                }
496            }
497
498            // Compute gradient before recording so `gradient_norms` holds the
499            // real value driving this iteration's update.
500            let gradient = if let Some(grad) = objective.gradient(&params) {
501                grad
502            } else {
503                self.numerical_gradient(&*objective, &params)?
504            };
505            let gradient_norm = gradient.iter().map(|g| g * g).sum::<f64>().sqrt();
506
507            // Record history
508            self.record_iteration(&params, value, iteration, gradient_norm, last_step_size);
509
510            // Check convergence
511            if iteration > 0 {
512                let prev_value = self
513                    .history
514                    .lock()
515                    .ok()
516                    .and_then(|h| h.objective_values.get(iteration - 1).copied())
517                    .unwrap_or(value);
518                if (prev_value - value).abs() < self.config.tolerance {
519                    return Ok((params, best_value, evaluations, true));
520                }
521            }
522
523            // Update biased first and second moment estimates
524            let mut step_sq_norm = 0.0;
525            for i in 0..params.len() {
526                m[i] = beta1.mul_add(m[i], (1.0 - beta1) * gradient[i]);
527                v[i] = beta2.mul_add(v[i], (1.0 - beta2) * gradient[i] * gradient[i]);
528
529                // Bias correction
530                let m_hat = m[i] / (1.0 - beta1.powi(t as i32));
531                let v_hat = v[i] / (1.0 - beta2.powi(t as i32));
532
533                // Update parameters
534                let step = learning_rate * m_hat / (v_hat.sqrt() + epsilon);
535                params[i] -= step;
536                step_sq_norm += step * step;
537
538                // Apply bounds
539                let bounds = objective.bounds();
540                params[i] = params[i].max(bounds[i].0).min(bounds[i].1);
541            }
542            last_step_size = step_sq_norm.sqrt();
543
544            // Progress callback
545            if let Some(callback) = &self.config.progress_callback {
546                callback(iteration, value);
547            }
548        }
549
550        Ok((params, best_value, evaluations, false))
551    }
552
553    /// L-BFGS-B: limited-memory BFGS quasi-Newton optimization with box
554    /// (bound) constraints.
555    ///
556    /// This is a genuine L-BFGS implementation, not an alias for gradient
557    /// descent: it maintains the last `memory` `(s, y)` curvature pairs and
558    /// uses the standard *two-loop recursion* (Nocedal & Wright, Algorithm
559    /// 7.4) to form the quasi-Newton search direction `d = -H_k ∇f(x_k)`
560    /// without ever materializing the dense inverse-Hessian approximation
561    /// `H_k`. A backtracking Armijo line search picks the step length, and
562    /// bounds are enforced by elementwise clamping of the trial point
563    /// ("gradient projection"), a standard practical technique for bound
564    /// handling. This is not the full generalized Cauchy-point/active-set
565    /// method of the original L-BFGS-B paper, but it is a real quasi-Newton
566    /// method with genuine curvature memory, superlinear convergence on
567    /// smooth problems, and honest bound support -- not the `learning_rate =
568    /// 0.01, momentum = 0.9` gradient descent this used to silently alias.
569    fn optimize_lbfgs(
570        &self,
571        objective: Arc<dyn ObjectiveFunction>,
572        initial_params: &[f64],
573        max_iterations: usize,
574        tolerance: f64,
575    ) -> QuantRS2Result<(Vec<f64>, f64, usize, bool)> {
576        let bounds = objective.bounds();
577        let n = initial_params.len();
578        // Limited memory: how many (s, y) curvature pairs to retain.
579        let memory = 10.min(max_iterations.max(1));
580
581        let clamp = |v: &mut [f64]| {
582            for (vi, &(lo, hi)) in v.iter_mut().zip(bounds.iter()) {
583                *vi = vi.max(lo).min(hi);
584            }
585        };
586
587        let gradient_at = |x: &[f64]| -> QuantRS2Result<Vec<f64>> {
588            if let Some(g) = objective.gradient(x) {
589                Ok(g)
590            } else {
591                self.numerical_gradient(&*objective, x)
592            }
593        };
594
595        let mut x = initial_params.to_vec();
596        clamp(&mut x);
597        let mut evaluations = 0usize;
598        let mut value = objective.evaluate(&x);
599        evaluations += 1;
600        let mut grad = gradient_at(&x)?;
601
602        let mut best_params = x.clone();
603        let mut best_value = value;
604
605        let mut s_history: Vec<Vec<f64>> = Vec::with_capacity(memory);
606        let mut y_history: Vec<Vec<f64>> = Vec::with_capacity(memory);
607        let mut rho_history: Vec<f64> = Vec::with_capacity(memory);
608
609        for iteration in 0..max_iterations {
610            let grad_norm = dot(&grad, &grad).sqrt();
611
612            if grad_norm < tolerance {
613                self.record_iteration(&x, value, iteration, grad_norm, 0.0);
614                return Ok((best_params, best_value, evaluations, true));
615            }
616
617            // Two-loop recursion: d = -H_k * grad, using only the stored
618            // (s, y, rho) curvature triples (Nocedal & Wright, Alg. 7.4).
619            let k = s_history.len();
620            let mut q = grad.clone();
621            let mut alpha_i = vec![0.0; k];
622            for i in (0..k).rev() {
623                let a = rho_history[i] * dot(&s_history[i], &q);
624                alpha_i[i] = a;
625                for j in 0..n {
626                    q[j] -= a * y_history[i][j];
627                }
628            }
629            let gamma = if k > 0 {
630                let s = &s_history[k - 1];
631                let y = &y_history[k - 1];
632                dot(s, y) / dot(y, y).max(1e-12)
633            } else {
634                1.0
635            };
636            for qj in &mut q {
637                *qj *= gamma;
638            }
639            for i in 0..k {
640                let beta = rho_history[i] * dot(&y_history[i], &q);
641                for j in 0..n {
642                    q[j] += s_history[i][j] * (alpha_i[i] - beta);
643                }
644            }
645            let direction: Vec<f64> = q.iter().map(|v| -v).collect();
646
647            // Backtracking Armijo line search with elementwise bound clamping.
648            let directional_derivative = dot(&grad, &direction);
649            let c1 = 1e-4;
650            let mut step_length = 1.0;
651            let mut new_x = x.clone();
652            let mut new_value = value;
653            let mut accepted = false;
654            for _ in 0..20 {
655                for j in 0..n {
656                    new_x[j] = x[j] + step_length * direction[j];
657                }
658                clamp(&mut new_x);
659                new_value = objective.evaluate(&new_x);
660                evaluations += 1;
661                if new_value <= step_length.mul_add(c1 * directional_derivative, value) {
662                    accepted = true;
663                    break;
664                }
665                step_length *= 0.5;
666            }
667
668            if !accepted {
669                // No descent direction respecting the bounds: a local
670                // (possibly boundary) minimum under the current curvature
671                // model, which is a legitimate convergence condition.
672                self.record_iteration(&x, value, iteration, grad_norm, 0.0);
673                return Ok((best_params, best_value, evaluations, true));
674            }
675
676            let step_vec: Vec<f64> = (0..n).map(|j| new_x[j] - x[j]).collect();
677            let step_size = dot(&step_vec, &step_vec).sqrt();
678            self.record_iteration(&x, value, iteration, grad_norm, step_size);
679
680            let new_grad = gradient_at(&new_x)?;
681            let y_vec: Vec<f64> = (0..n).map(|j| new_grad[j] - grad[j]).collect();
682            let sy = dot(&step_vec, &y_vec);
683            // Skip the curvature update if the curvature condition `s^T y >
684            // 0` fails (a standard L-BFGS safeguard against indefinite
685            // updates near the bounds or on non-convex regions).
686            if sy > 1e-10 {
687                if s_history.len() == memory {
688                    s_history.remove(0);
689                    y_history.remove(0);
690                    rho_history.remove(0);
691                }
692                s_history.push(step_vec);
693                y_history.push(y_vec);
694                rho_history.push(1.0 / sy);
695            }
696
697            x = new_x;
698            value = new_value;
699            grad = new_grad;
700
701            if value < best_value {
702                best_value = value;
703                best_params.clone_from(&x);
704                if let Ok(mut guard) = self.best_parameters.lock() {
705                    *guard = Some(best_params.clone());
706                }
707                if let Ok(mut guard) = self.best_value.lock() {
708                    *guard = best_value;
709                }
710            }
711
712            if let Some(callback) = &self.config.progress_callback {
713                callback(iteration, value);
714            }
715        }
716
717        Ok((best_params, best_value, evaluations, false))
718    }
719
720    /// Nelder-Mead simplex optimization
721    fn optimize_nelder_mead(
722        &self,
723        objective: Arc<dyn ObjectiveFunction>,
724        initial_params: &[f64],
725        max_iterations: usize,
726        tolerance: f64,
727    ) -> QuantRS2Result<(Vec<f64>, f64, usize, bool)> {
728        let n = initial_params.len();
729        let mut simplex = Vec::new();
730        let mut evaluations = 0;
731
732        // Initialize simplex
733        simplex.push(initial_params.to_vec());
734        for i in 0..n {
735            let mut vertex = initial_params.to_vec();
736            vertex[i] += if vertex[i] == 0.0 {
737                0.00025
738            } else {
739                vertex[i] * 0.05
740            };
741            simplex.push(vertex);
742        }
743
744        // Evaluate initial simplex
745        let mut values: Vec<f64> = simplex
746            .iter()
747            .map(|params| {
748                evaluations += 1;
749                objective.evaluate(params)
750            })
751            .collect();
752
753        // Nelder-Mead is derivative-free, so `gradient_norms` is honestly
754        // `0.0`; `step_sizes` tracks the real distance the best vertex moves
755        // between iterations, seeded from the initial simplex's best vertex.
756        let mut previous_best_point = simplex[0].clone();
757
758        for iteration in 0..max_iterations {
759            // Sort simplex by objective values
760            let mut indices: Vec<usize> = (0..simplex.len()).collect();
761            indices.sort_by(|&i, &j| {
762                values[i]
763                    .partial_cmp(&values[j])
764                    .unwrap_or(std::cmp::Ordering::Equal)
765            });
766
767            let best_value = values[indices[0]];
768            let worst_idx = indices[n];
769            let second_worst_idx = indices[n - 1];
770
771            let best_step_size = dot_diff(&simplex[indices[0]], &previous_best_point).sqrt();
772            previous_best_point.clone_from(&simplex[indices[0]]);
773
774            // Record best iteration
775            self.record_iteration(
776                &simplex[indices[0]],
777                best_value,
778                iteration,
779                0.0,
780                best_step_size,
781            );
782
783            // Check convergence
784            let range = values[worst_idx] - values[indices[0]];
785            if range < tolerance {
786                return Ok((simplex[indices[0]].clone(), best_value, evaluations, true));
787            }
788
789            // Compute centroid (excluding worst point)
790            let mut centroid = vec![0.0; n];
791            for i in 0..n {
792                for j in 0..n {
793                    centroid[j] += simplex[indices[i]][j];
794                }
795            }
796            for j in 0..n {
797                centroid[j] /= n as f64;
798            }
799
800            // Reflection
801            let alpha = 1.0;
802            let mut reflected = vec![0.0; n];
803            for j in 0..n {
804                reflected[j] = centroid[j] + alpha * (centroid[j] - simplex[worst_idx][j]);
805            }
806
807            // Apply bounds
808            let bounds = objective.bounds();
809            for j in 0..n {
810                reflected[j] = reflected[j].max(bounds[j].0).min(bounds[j].1);
811            }
812
813            let reflected_value = objective.evaluate(&reflected);
814            evaluations += 1;
815
816            if values[indices[0]] <= reflected_value && reflected_value < values[second_worst_idx] {
817                // Accept reflection
818                simplex[worst_idx] = reflected;
819                values[worst_idx] = reflected_value;
820            } else if reflected_value < values[indices[0]] {
821                // Expansion
822                let gamma = 2.0;
823                let mut expanded = vec![0.0; n];
824                for j in 0..n {
825                    expanded[j] = centroid[j] + gamma * (reflected[j] - centroid[j]);
826                    expanded[j] = expanded[j].max(bounds[j].0).min(bounds[j].1);
827                }
828
829                let expanded_value = objective.evaluate(&expanded);
830                evaluations += 1;
831
832                if expanded_value < reflected_value {
833                    simplex[worst_idx] = expanded;
834                    values[worst_idx] = expanded_value;
835                } else {
836                    simplex[worst_idx] = reflected;
837                    values[worst_idx] = reflected_value;
838                }
839            } else {
840                // Contraction
841                let rho = 0.5;
842                let mut contracted = vec![0.0; n];
843                for j in 0..n {
844                    contracted[j] = centroid[j] + rho * (simplex[worst_idx][j] - centroid[j]);
845                    contracted[j] = contracted[j].max(bounds[j].0).min(bounds[j].1);
846                }
847
848                let contracted_value = objective.evaluate(&contracted);
849                evaluations += 1;
850
851                if contracted_value < values[worst_idx] {
852                    simplex[worst_idx] = contracted;
853                    values[worst_idx] = contracted_value;
854                } else {
855                    // Shrink
856                    let sigma = 0.5;
857                    for i in 1..=n {
858                        for j in 0..n {
859                            simplex[i][j] = simplex[indices[0]][j]
860                                + sigma * (simplex[i][j] - simplex[indices[0]][j]);
861                            simplex[i][j] = simplex[i][j].max(bounds[j].0).min(bounds[j].1);
862                        }
863                        values[i] = objective.evaluate(&simplex[i]);
864                        evaluations += 1;
865                    }
866                }
867            }
868
869            // Progress callback
870            if let Some(callback) = &self.config.progress_callback {
871                callback(iteration, best_value);
872            }
873        }
874
875        // Find best point
876        let mut best_idx = 0;
877        let mut best_value = values[0];
878        for i in 1..values.len() {
879            if values[i] < best_value {
880                best_value = values[i];
881                best_idx = i;
882            }
883        }
884
885        Ok((simplex[best_idx].clone(), best_value, evaluations, false))
886    }
887
888    /// Simulated annealing optimization
889    fn optimize_simulated_annealing(
890        &self,
891        objective: Arc<dyn ObjectiveFunction>,
892        initial_params: &[f64],
893        initial_temperature: f64,
894        cooling_rate: f64,
895        min_temperature: f64,
896    ) -> QuantRS2Result<(Vec<f64>, f64, usize, bool)> {
897        use scirs2_core::random::prelude::*;
898        let mut rng = thread_rng();
899
900        let mut current_params = initial_params.to_vec();
901        let mut current_value = objective.evaluate(&current_params);
902        let mut best_params = current_params.clone();
903        let mut best_value = current_value;
904        let mut temperature = initial_temperature;
905        let mut evaluations = 1;
906
907        let bounds = objective.bounds();
908
909        for iteration in 0..self.config.max_evaluations {
910            if temperature < min_temperature {
911                break;
912            }
913
914            // Generate neighbor solution
915            let mut neighbor_params = current_params.clone();
916            let mut proposed_step_sq_norm = 0.0;
917            for i in 0..neighbor_params.len() {
918                let range = bounds[i].1 - bounds[i].0;
919                let step = rng.random_range(-0.1..0.1) * range * temperature / initial_temperature;
920                neighbor_params[i] = (neighbor_params[i] + step)
921                    .max(bounds[i].0)
922                    .min(bounds[i].1);
923                let applied_step = neighbor_params[i] - current_params[i];
924                proposed_step_sq_norm += applied_step * applied_step;
925            }
926            let metropolis_step_size = proposed_step_sq_norm.sqrt();
927
928            let neighbor_value = objective.evaluate(&neighbor_params);
929            evaluations += 1;
930
931            // Accept or reject based on Metropolis criterion
932            let delta = neighbor_value - current_value;
933            if delta < 0.0 || rng.random::<f64>() < (-delta / temperature).exp() {
934                current_params = neighbor_params;
935                current_value = neighbor_value;
936
937                if current_value < best_value {
938                    best_params.clone_from(&current_params);
939                    best_value = current_value;
940                }
941            }
942
943            // Record iteration. Simulated annealing has no gradient, so
944            // `gradient_norms` is honestly `0.0`; `step_sizes` is the real
945            // Metropolis proposal size explored this iteration (after
946            // clamping to bounds), whether or not it was accepted.
947            self.record_iteration(
948                &current_params,
949                current_value,
950                iteration,
951                0.0,
952                metropolis_step_size,
953            );
954
955            // Cool down
956            temperature *= cooling_rate;
957
958            // Progress callback
959            if let Some(callback) = &self.config.progress_callback {
960                callback(iteration, best_value);
961            }
962        }
963
964        Ok((
965            best_params,
966            best_value,
967            evaluations,
968            temperature < min_temperature,
969        ))
970    }
971
972    /// Bayesian optimization (simplified implementation)
973    /// Bayesian optimization with a genuine Gaussian-process (GP) surrogate.
974    ///
975    /// Unlike the former alias to Nelder-Mead, this actually uses
976    /// `acquisition_function`, `kernel`, and `num_initial_samples`:
977    ///
978    /// 1. Draw `num_initial_samples` design points uniformly within the
979    ///    bounds (plus the given `initial_params`) and evaluate the true
980    ///    objective at each -- the GP's training data.
981    /// 2. Fit an exact GP posterior: build the `kernel`-induced covariance
982    ///    matrix over every observed point, Cholesky-factorize it, and solve
983    ///    for the (constant-mean) GP regression weights.
984    /// 3. Repeatedly maximize `acquisition_function` (Expected Improvement /
985    ///    Probability of Improvement / Upper-Confidence-Bound / Thompson
986    ///    sampling) over a candidate pool (uniform samples plus local
987    ///    perturbations around the current best) using the GP posterior
988    ///    mean/std at each candidate, evaluate the true objective at the
989    ///    winner, fold it into the training set, and refit the GP.
990    ///
991    /// The loop runs until `self.config.max_evaluations` true objective
992    /// evaluations have been spent.
993    fn optimize_bayesian(
994        &self,
995        objective: Arc<dyn ObjectiveFunction>,
996        initial_params: &[f64],
997        acquisition_function: &AcquisitionFunction,
998        kernel: &KernelType,
999        num_initial_samples: usize,
1000    ) -> QuantRS2Result<(Vec<f64>, f64, usize, bool)> {
1001        use scirs2_core::random::prelude::*;
1002        let mut rng = thread_rng();
1003
1004        let bounds = objective.bounds();
1005        let n_dims = initial_params.len();
1006        let max_evaluations = self.config.max_evaluations.max(1);
1007
1008        let mut points: Vec<Vec<f64>> = Vec::new();
1009        let mut values: Vec<f64> = Vec::new();
1010        let mut evaluations = 0usize;
1011
1012        let initial_value = objective.evaluate(initial_params);
1013        points.push(initial_params.to_vec());
1014        values.push(initial_value);
1015        evaluations += 1;
1016
1017        let num_random_samples = num_initial_samples
1018            .saturating_sub(1)
1019            .min(max_evaluations.saturating_sub(evaluations));
1020        for _ in 0..num_random_samples {
1021            let candidate: Vec<f64> = bounds
1022                .iter()
1023                .map(|&(lo, hi)| lo + rng.random::<f64>() * (hi - lo))
1024                .collect();
1025            let value = objective.evaluate(&candidate);
1026            points.push(candidate);
1027            values.push(value);
1028            evaluations += 1;
1029        }
1030
1031        let mut best_idx = 0;
1032        for i in 1..values.len() {
1033            if values[i] < values[best_idx] {
1034                best_idx = i;
1035            }
1036        }
1037        let mut best_params = points[best_idx].clone();
1038        let mut best_value = values[best_idx];
1039        if let Ok(mut guard) = self.best_parameters.lock() {
1040            *guard = Some(best_params.clone());
1041        }
1042        if let Ok(mut guard) = self.best_value.lock() {
1043            *guard = best_value;
1044        }
1045
1046        const JITTER: f64 = 1e-6;
1047        let num_candidates = (4 * n_dims).max(32);
1048        let mut iteration = 0usize;
1049        let mut converged = false;
1050
1051        while evaluations < max_evaluations {
1052            let covariance = build_covariance(kernel, &points, JITTER);
1053            let Ok(chol) = cholesky(&covariance) else {
1054                // The observed points became numerically degenerate for this
1055                // kernel (e.g. near-duplicate samples); stop honestly with
1056                // whatever has been found so far rather than fabricating a
1057                // posterior from an unfactorizable covariance.
1058                break;
1059            };
1060            let y_mean = values.iter().sum::<f64>() / values.len() as f64;
1061            let y_centered: Vec<f64> = values.iter().map(|v| v - y_mean).collect();
1062            let z = forward_substitution(&chol, &y_centered);
1063            let alpha = backward_substitution_transpose(&chol, &z);
1064
1065            let mut best_candidate: Option<Vec<f64>> = None;
1066            let mut best_acquisition = f64::NEG_INFINITY;
1067            for c in 0..num_candidates {
1068                let candidate: Vec<f64> = if c < num_candidates / 2 {
1069                    bounds
1070                        .iter()
1071                        .map(|&(lo, hi)| lo + rng.random::<f64>() * (hi - lo))
1072                        .collect()
1073                } else {
1074                    best_params
1075                        .iter()
1076                        .zip(bounds.iter())
1077                        .map(|(&v, &(lo, hi))| {
1078                            let scale = (hi - lo) * 0.1;
1079                            (v + (rng.random::<f64>() - 0.5) * 2.0 * scale).clamp(lo, hi)
1080                        })
1081                        .collect()
1082                };
1083
1084                let (mean_c, std_c) =
1085                    gp_posterior(kernel, &points, &alpha, &chol, &candidate, y_mean, JITTER);
1086
1087                let acquisition_score = match acquisition_function {
1088                    AcquisitionFunction::Thompson => {
1089                        // Independent Thompson sampling: draw one posterior
1090                        // sample per candidate via Box-Muller.
1091                        let u1 = rng.random::<f64>().max(1e-12);
1092                        let u2 = rng.random::<f64>();
1093                        let z = (-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos();
1094                        let sampled_value = mean_c + std_c * z;
1095                        -sampled_value // maximize acquisition == minimize sampled objective
1096                    }
1097                    other => acquisition_value(other, mean_c, std_c, best_value),
1098                };
1099
1100                if acquisition_score > best_acquisition {
1101                    best_acquisition = acquisition_score;
1102                    best_candidate = Some(candidate);
1103                }
1104            }
1105
1106            let Some(next_point) = best_candidate else {
1107                break;
1108            };
1109            let next_value = objective.evaluate(&next_point);
1110            evaluations += 1;
1111
1112            let step_size = dot_diff(&next_point, &best_params).sqrt();
1113
1114            if next_value < best_value {
1115                best_value = next_value;
1116                best_params = next_point.clone();
1117                if let Ok(mut guard) = self.best_parameters.lock() {
1118                    *guard = Some(best_params.clone());
1119                }
1120                if let Ok(mut guard) = self.best_value.lock() {
1121                    *guard = best_value;
1122                }
1123            }
1124
1125            points.push(next_point);
1126            values.push(next_value);
1127
1128            // Bayesian optimization has no parameter-space gradient, so
1129            // `gradient_norms` is honestly `0.0`; `step_sizes` is the real
1130            // distance from the previous best point to the newly sampled one.
1131            self.record_iteration(&best_params, best_value, iteration, 0.0, step_size);
1132
1133            if let Some(callback) = &self.config.progress_callback {
1134                callback(iteration, best_value);
1135            }
1136
1137            // Converge once the acquisition function itself reports
1138            // negligible expected benefit and the sampled step is tiny --
1139            // there is nothing more to gain from further sampling.
1140            if best_acquisition.abs() < self.config.tolerance && step_size < self.config.tolerance {
1141                converged = true;
1142                iteration += 1;
1143                break;
1144            }
1145            iteration += 1;
1146        }
1147
1148        Ok((best_params, best_value, evaluations, converged))
1149    }
1150
1151    /// Compute numerical gradient
1152    fn numerical_gradient(
1153        &self,
1154        objective: &dyn ObjectiveFunction,
1155        params: &[f64],
1156    ) -> QuantRS2Result<Vec<f64>> {
1157        let epsilon = 1e-8;
1158        let mut gradient = vec![0.0; params.len()];
1159
1160        for i in 0..params.len() {
1161            let mut params_plus = params.to_vec();
1162            let mut params_minus = params.to_vec();
1163
1164            params_plus[i] += epsilon;
1165            params_minus[i] -= epsilon;
1166
1167            let f_plus = objective.evaluate(&params_plus);
1168            let f_minus = objective.evaluate(&params_minus);
1169
1170            gradient[i] = (f_plus - f_minus) / (2.0 * epsilon);
1171        }
1172
1173        Ok(gradient)
1174    }
1175
1176    /// Record one optimization iteration.
1177    ///
1178    /// `gradient_norm` and `step_size` are the *real* diagnostics the calling
1179    /// algorithm already computed for this iteration (the parameter-space
1180    /// gradient's Euclidean norm and the Euclidean norm of the update /
1181    /// perturbation actually explored), not placeholders: every call site
1182    /// passes `0.0` only where the algorithm genuinely has no such quantity
1183    /// (e.g. no gradient exists for the derivative-free Nelder-Mead simplex
1184    /// or simulated annealing methods).
1185    fn record_iteration(
1186        &self,
1187        params: &[f64],
1188        value: f64,
1189        _iteration: usize,
1190        gradient_norm: f64,
1191        step_size: f64,
1192    ) {
1193        if let Ok(mut history) = self.history.lock() {
1194            history.parameters.push(params.to_vec());
1195            history.objective_values.push(value);
1196            history.gradient_norms.push(gradient_norm);
1197            history.step_sizes.push(step_size);
1198            history.timestamps.push(std::time::Instant::now());
1199        }
1200    }
1201
1202    /// Get current best parameters
1203    #[must_use]
1204    pub fn get_best_parameters(&self) -> Option<Vec<f64>> {
1205        self.best_parameters.lock().ok().and_then(|g| g.clone())
1206    }
1207
1208    /// Get current best value
1209    #[must_use]
1210    pub fn get_best_value(&self) -> f64 {
1211        self.best_value.lock().ok().map_or(f64::INFINITY, |g| *g)
1212    }
1213
1214    /// Build a concrete circuit from the template, binding the current
1215    /// `parameters` into each parameterized gate.
1216    ///
1217    /// Each [`ParameterizedGate`] names a gate (`"H"`, `"RX"`, `"CNOT"`, …),
1218    /// the qubits it acts on, and the indices into `parameters` supplying its
1219    /// angles.  The gate is materialized via the circuit builder so the result
1220    /// is a fully-formed circuit (previously this returned an empty circuit,
1221    /// silently discarding the entire template).
1222    pub fn build_circuit(&self, parameters: &[f64]) -> QuantRS2Result<Circuit<32>> {
1223        if parameters.len() != self.circuit_template.parameters.len() {
1224            return Err(QuantRS2Error::InvalidInput(
1225                "Parameter count mismatch".to_string(),
1226            ));
1227        }
1228
1229        let mut circuit = Circuit::<32>::new();
1230        for gate_template in &self.circuit_template.structure {
1231            apply_template_gate(&mut circuit, gate_template, parameters)?;
1232        }
1233
1234        Ok(circuit)
1235    }
1236}
1237
1238/// Euclidean inner product `a · b`.
1239fn dot(a: &[f64], b: &[f64]) -> f64 {
1240    a.iter().zip(b).map(|(x, y)| x * y).sum()
1241}
1242
1243/// Squared Euclidean distance `‖a − b‖²`.
1244fn dot_diff(a: &[f64], b: &[f64]) -> f64 {
1245    a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum()
1246}
1247
1248/// Gaussian-process kernel `k(a, b)` for the requested [`KernelType`].
1249fn kernel_value(kernel: &KernelType, a: &[f64], b: &[f64]) -> f64 {
1250    match kernel {
1251        KernelType::RBF { length_scale } => {
1252            let denom = (2.0 * length_scale * length_scale).max(1e-12);
1253            (-dot_diff(a, b) / denom).exp()
1254        }
1255        KernelType::Matern { nu, length_scale } => {
1256            matern_kernel(*nu, *length_scale, dot_diff(a, b).sqrt())
1257        }
1258        KernelType::Linear { variance } => variance * dot(a, b),
1259        KernelType::Periodic {
1260            period,
1261            length_scale,
1262        } => {
1263            let dist = dot_diff(a, b).sqrt();
1264            let s = (std::f64::consts::PI * dist / period.max(1e-12)).sin();
1265            (-2.0 * s * s / (length_scale * length_scale).max(1e-12)).exp()
1266        }
1267    }
1268}
1269
1270/// Matérn kernel value for separation `dist` (already divided is done inside).
1271///
1272/// Implements the three half-integer orders used in practice in closed form
1273/// (`ν = 1/2, 3/2, 5/2`; Rasmussen & Williams, *Gaussian Processes for Machine
1274/// Learning*, eq. 4.16); any other requested `ν` snaps to the nearest of
1275/// these, since the fully general case needs a modified Bessel function of
1276/// the second kind that this crate does not implement.
1277fn matern_kernel(nu: f64, length_scale: f64, dist: f64) -> f64 {
1278    let length_scale = length_scale.max(1e-12);
1279    let r = dist / length_scale;
1280    if nu <= 1.0 {
1281        (-r).exp()
1282    } else if nu <= 2.0 {
1283        let root3 = 3f64.sqrt();
1284        (1.0 + root3 * r) * (-root3 * r).exp()
1285    } else {
1286        let root5 = 5f64.sqrt();
1287        (1.0 + root5 * r + 5.0 * r * r / 3.0) * (-root5 * r).exp()
1288    }
1289}
1290
1291/// Build the `n×n` covariance matrix `K_ij = k(points_i, points_j) + jitter·δ_ij`.
1292///
1293/// The jitter term is a small ridge added to the diagonal for numerical
1294/// stability of the subsequent Cholesky factorization, standard practice for
1295/// exact GP regression.
1296fn build_covariance(kernel: &KernelType, points: &[Vec<f64>], jitter: f64) -> Vec<Vec<f64>> {
1297    let n = points.len();
1298    let mut cov = vec![vec![0.0; n]; n];
1299    for i in 0..n {
1300        for j in i..n {
1301            let value =
1302                kernel_value(kernel, &points[i], &points[j]) + if i == j { jitter } else { 0.0 };
1303            cov[i][j] = value;
1304            cov[j][i] = value;
1305        }
1306    }
1307    cov
1308}
1309
1310/// Cholesky factorization `L` (lower-triangular) of a symmetric positive
1311/// definite matrix, such that `L·Lᵀ = matrix`.
1312fn cholesky(matrix: &[Vec<f64>]) -> QuantRS2Result<Vec<Vec<f64>>> {
1313    let n = matrix.len();
1314    let mut l = vec![vec![0.0; n]; n];
1315    for i in 0..n {
1316        for j in 0..=i {
1317            let mut sum = matrix[i][j];
1318            for k in 0..j {
1319                sum -= l[i][k] * l[j][k];
1320            }
1321            if i == j {
1322                if sum <= 0.0 {
1323                    return Err(QuantRS2Error::ComputationError(
1324                        "GP covariance matrix is not positive definite".to_string(),
1325                    ));
1326                }
1327                l[i][j] = sum.sqrt();
1328            } else {
1329                l[i][j] = sum / l[j][j];
1330            }
1331        }
1332    }
1333    Ok(l)
1334}
1335
1336/// Solve `L·y = b` for lower-triangular `L` (forward substitution).
1337fn forward_substitution(l: &[Vec<f64>], b: &[f64]) -> Vec<f64> {
1338    let n = l.len();
1339    let mut y = vec![0.0; n];
1340    for i in 0..n {
1341        let mut sum = b[i];
1342        for k in 0..i {
1343            sum -= l[i][k] * y[k];
1344        }
1345        y[i] = sum / l[i][i];
1346    }
1347    y
1348}
1349
1350/// Solve `Lᵀ·x = y` for lower-triangular `L` (back substitution against its
1351/// transpose).
1352fn backward_substitution_transpose(l: &[Vec<f64>], y: &[f64]) -> Vec<f64> {
1353    let n = l.len();
1354    let mut x = vec![0.0; n];
1355    for i in (0..n).rev() {
1356        let mut sum = y[i];
1357        for k in (i + 1)..n {
1358            sum -= l[k][i] * x[k];
1359        }
1360        x[i] = sum / l[i][i];
1361    }
1362    x
1363}
1364
1365/// GP posterior mean and standard deviation at `x`, given the training
1366/// `points`, the precomputed regression weights `alpha = K⁻¹(y − y_mean)`,
1367/// and the Cholesky factor `l` of the training covariance.
1368fn gp_posterior(
1369    kernel: &KernelType,
1370    points: &[Vec<f64>],
1371    alpha: &[f64],
1372    l: &[Vec<f64>],
1373    x: &[f64],
1374    y_mean: f64,
1375    jitter: f64,
1376) -> (f64, f64) {
1377    let k_star: Vec<f64> = points.iter().map(|p| kernel_value(kernel, p, x)).collect();
1378    let mean = y_mean + dot(&k_star, alpha);
1379    let v = forward_substitution(l, &k_star);
1380    let k_xx = kernel_value(kernel, x, x) + jitter;
1381    let variance = (k_xx - dot(&v, &v)).max(0.0);
1382    (mean, variance.sqrt())
1383}
1384
1385/// Standard normal CDF via the Abramowitz & Stegun 7.1.26 `erf` approximation
1386/// (max absolute error ≈ 1.5×10⁻⁷).
1387fn normal_cdf(z: f64) -> f64 {
1388    0.5 * (1.0 + erf(z / std::f64::consts::SQRT_2))
1389}
1390
1391/// Standard normal PDF.
1392fn normal_pdf(z: f64) -> f64 {
1393    (-(z * z) / 2.0).exp() / (2.0 * std::f64::consts::PI).sqrt()
1394}
1395
1396/// Error function approximation (Abramowitz & Stegun, formula 7.1.26).
1397fn erf(x: f64) -> f64 {
1398    let sign = if x < 0.0 { -1.0 } else { 1.0 };
1399    let x = x.abs();
1400    let a1 = 0.254_829_592;
1401    let a2 = -0.284_496_736;
1402    let a3 = 1.421_413_741;
1403    let a4 = -1.453_152_027;
1404    let a5 = 1.061_405_429;
1405    let p: f64 = 0.327_591_1;
1406    let t = 1.0 / p.mul_add(x, 1.0);
1407    let y = 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * (-x * x).exp();
1408    sign * y
1409}
1410
1411/// Value of `acquisition` (for minimization) given the GP posterior
1412/// `(mean, std)` at a candidate point and the best objective value observed
1413/// so far. Larger is better; the caller picks the candidate that maximizes
1414/// this. [`AcquisitionFunction::Thompson`] is handled by the caller via
1415/// direct posterior sampling and never reaches this function.
1416fn acquisition_value(
1417    acquisition: &AcquisitionFunction,
1418    mean: f64,
1419    std: f64,
1420    best_so_far: f64,
1421) -> f64 {
1422    let std = std.max(1e-9);
1423    match acquisition {
1424        AcquisitionFunction::ExpectedImprovement => {
1425            let improvement = best_so_far - mean;
1426            let z = improvement / std;
1427            improvement * normal_cdf(z) + std * normal_pdf(z)
1428        }
1429        AcquisitionFunction::ProbabilityOfImprovement => {
1430            let z = (best_so_far - mean) / std;
1431            normal_cdf(z)
1432        }
1433        AcquisitionFunction::UpperConfidenceBound { kappa } => {
1434            // Minimization convention: prefer low mean, high uncertainty.
1435            kappa * std - mean
1436        }
1437        AcquisitionFunction::Thompson => {
1438            unreachable!("Thompson sampling is handled by the caller, not acquisition_value")
1439        }
1440    }
1441}
1442
1443/// Resolve the angle for the `k`-th parameter slot of a template gate.
1444///
1445/// `parameter_indices` give the positions in the optimizer's parameter vector;
1446/// any leftover slots fall back to `fixed_parameters`.
1447fn template_gate_angle(
1448    gate: &ParameterizedGate,
1449    parameters: &[f64],
1450    slot: usize,
1451) -> QuantRS2Result<f64> {
1452    if let Some(&idx) = gate.parameter_indices.get(slot) {
1453        parameters.get(idx).copied().ok_or_else(|| {
1454            QuantRS2Error::InvalidInput(format!(
1455                "Gate '{}' references parameter index {idx} which is out of range",
1456                gate.gate_name
1457            ))
1458        })
1459    } else if let Some(&fixed) = gate
1460        .fixed_parameters
1461        .get(slot - gate.parameter_indices.len())
1462    {
1463        Ok(fixed)
1464    } else {
1465        Err(QuantRS2Error::InvalidInput(format!(
1466            "Gate '{}' is missing angle #{slot}",
1467            gate.gate_name
1468        )))
1469    }
1470}
1471
1472/// Materialize a single template gate onto `circuit`.
1473///
1474/// Returns an honest error for an unknown gate name or a malformed qubit list
1475/// rather than silently skipping it.
1476fn apply_template_gate<const N: usize>(
1477    circuit: &mut Circuit<N>,
1478    gate: &ParameterizedGate,
1479    parameters: &[f64],
1480) -> QuantRS2Result<()> {
1481    let q = |i: usize| -> QuantRS2Result<QubitId> {
1482        gate.qubits
1483            .get(i)
1484            .map(|&qb| QubitId(qb as u32))
1485            .ok_or_else(|| {
1486                QuantRS2Error::InvalidInput(format!(
1487                    "Gate '{}' expects at least {} qubit(s)",
1488                    gate.gate_name,
1489                    i + 1
1490                ))
1491            })
1492    };
1493
1494    match gate.gate_name.as_str() {
1495        "H" | "h" => {
1496            circuit.h(q(0)?)?;
1497        }
1498        "X" | "x" => {
1499            circuit.x(q(0)?)?;
1500        }
1501        "Y" | "y" => {
1502            circuit.y(q(0)?)?;
1503        }
1504        "Z" | "z" => {
1505            circuit.z(q(0)?)?;
1506        }
1507        "S" | "s" => {
1508            circuit.s(q(0)?)?;
1509        }
1510        "T" | "t" => {
1511            circuit.t(q(0)?)?;
1512        }
1513        "RX" | "rx" => {
1514            circuit.rx(q(0)?, template_gate_angle(gate, parameters, 0)?)?;
1515        }
1516        "RY" | "ry" => {
1517            circuit.ry(q(0)?, template_gate_angle(gate, parameters, 0)?)?;
1518        }
1519        "RZ" | "rz" => {
1520            circuit.rz(q(0)?, template_gate_angle(gate, parameters, 0)?)?;
1521        }
1522        "P" | "p" | "PHASE" => {
1523            circuit.p(q(0)?, template_gate_angle(gate, parameters, 0)?)?;
1524        }
1525        "CNOT" | "cnot" | "CX" | "cx" => {
1526            circuit.cnot(q(0)?, q(1)?)?;
1527        }
1528        "CZ" | "cz" => {
1529            circuit.cz(q(0)?, q(1)?)?;
1530        }
1531        other => {
1532            return Err(QuantRS2Error::UnsupportedOperation(format!(
1533                "Template gate '{other}' is not supported by the circuit builder"
1534            )));
1535        }
1536    }
1537    Ok(())
1538}
1539
1540/// Variational quantum eigensolver (VQE) objective
1541pub struct VQEObjective {
1542    /// Hamiltonian matrix
1543    hamiltonian: SparseMatrix,
1544    /// Circuit template
1545    circuit_template: CircuitTemplate,
1546    /// Parameter bounds
1547    bounds: Vec<(f64, f64)>,
1548}
1549
1550impl VQEObjective {
1551    /// Create new VQE objective
1552    #[must_use]
1553    pub fn new(hamiltonian: SparseMatrix, circuit_template: CircuitTemplate) -> Self {
1554        let bounds = circuit_template
1555            .parameters
1556            .iter()
1557            .map(|p| (p.lower_bound, p.upper_bound))
1558            .collect();
1559
1560        Self {
1561            hamiltonian,
1562            circuit_template,
1563            bounds,
1564        }
1565    }
1566}
1567
1568impl VQEObjective {
1569    /// Fallible core of [`ObjectiveFunction::evaluate`]: build the ansatz state
1570    /// `|ψ(θ)⟩`, then compute `⟨ψ|H|ψ⟩` directly from the sparse Hamiltonian's
1571    /// COO triplets.
1572    fn try_evaluate(&self, parameters: &[f64]) -> QuantRS2Result<f64> {
1573        let num_qubits = self.circuit_template.num_qubits;
1574        let state = statevector::simulate_template(
1575            num_qubits,
1576            &self.circuit_template.structure,
1577            parameters,
1578        )?;
1579
1580        let dim = 1usize << num_qubits;
1581        if self.hamiltonian.shape.0 != dim || self.hamiltonian.shape.1 != dim {
1582            return Err(QuantRS2Error::InvalidInput(format!(
1583                "Hamiltonian is {}x{} but a {}-qubit circuit needs {dim}x{dim}",
1584                self.hamiltonian.shape.0, self.hamiltonian.shape.1, num_qubits
1585            )));
1586        }
1587
1588        let expectation = statevector::sparse_expectation(self.hamiltonian.triplets(), &state);
1589        if expectation.im.abs() > 1e-6 {
1590            return Err(QuantRS2Error::ComputationError(format!(
1591                "VQE energy has non-negligible imaginary part ({:.3e}); Hamiltonian is not Hermitian",
1592                expectation.im
1593            )));
1594        }
1595        Ok(expectation.re)
1596    }
1597}
1598
1599impl ObjectiveFunction for VQEObjective {
1600    /// Energy `⟨ψ(θ)|H|ψ(θ)⟩` of the ansatz state produced by the circuit
1601    /// template at the given parameters.
1602    ///
1603    /// On a malformed configuration (dimension mismatch, unsupported gate, …)
1604    /// this returns `f64::INFINITY` — the worst possible value for the
1605    /// minimizer, so a broken term is rejected rather than silently accepted as
1606    /// a plausible energy.
1607    fn evaluate(&self, parameters: &[f64]) -> f64 {
1608        self.try_evaluate(parameters).unwrap_or(f64::INFINITY)
1609    }
1610
1611    fn bounds(&self) -> Vec<(f64, f64)> {
1612        self.bounds.clone()
1613    }
1614
1615    fn name(&self) -> &'static str {
1616        "VQE"
1617    }
1618}
1619
1620/// Quantum Approximate Optimization Algorithm (QAOA) objective
1621pub struct QAOAObjective {
1622    /// Problem Hamiltonian
1623    problem_hamiltonian: SparseMatrix,
1624    /// Mixer Hamiltonian
1625    mixer_hamiltonian: SparseMatrix,
1626    /// Number of QAOA layers
1627    num_layers: usize,
1628    /// Parameter bounds
1629    bounds: Vec<(f64, f64)>,
1630}
1631
1632impl QAOAObjective {
1633    /// Create new QAOA objective
1634    #[must_use]
1635    pub fn new(
1636        problem_hamiltonian: SparseMatrix,
1637        mixer_hamiltonian: SparseMatrix,
1638        num_layers: usize,
1639    ) -> Self {
1640        // Beta and gamma parameters for each layer
1641        let bounds = vec![(0.0, 2.0 * std::f64::consts::PI); 2 * num_layers];
1642
1643        Self {
1644            problem_hamiltonian,
1645            mixer_hamiltonian,
1646            num_layers,
1647            bounds,
1648        }
1649    }
1650}
1651
1652impl QAOAObjective {
1653    /// Fallible core of [`ObjectiveFunction::evaluate`].
1654    ///
1655    /// Prepares `|+⟩^⊗n`, applies the alternating QAOA layers
1656    /// `U(β,γ) = ∏_p exp(-iβ_p H_mixer) exp(-iγ_p H_problem)` and returns the
1657    /// problem-Hamiltonian energy `⟨ψ|H_problem|ψ⟩`.  Each layer's parameters
1658    /// are taken as `[γ_0, β_0, γ_1, β_1, …]`.
1659    fn try_evaluate(&self, parameters: &[f64]) -> QuantRS2Result<f64> {
1660        if parameters.len() != 2 * self.num_layers {
1661            return Err(QuantRS2Error::InvalidInput(format!(
1662                "QAOA with {} layers needs {} parameters, got {}",
1663                self.num_layers,
1664                2 * self.num_layers,
1665                parameters.len()
1666            )));
1667        }
1668
1669        let dim = self.problem_hamiltonian.shape.0;
1670        if !dim.is_power_of_two() || self.problem_hamiltonian.shape.1 != dim {
1671            return Err(QuantRS2Error::InvalidInput(
1672                "QAOA problem Hamiltonian must be a square 2^n matrix".to_string(),
1673            ));
1674        }
1675        if self.mixer_hamiltonian.shape != self.problem_hamiltonian.shape {
1676            return Err(QuantRS2Error::InvalidInput(
1677                "QAOA mixer and problem Hamiltonians must have the same shape".to_string(),
1678            ));
1679        }
1680        let num_qubits = dim.trailing_zeros() as usize;
1681
1682        // Dense complex operators for exact exponentiation (QAOA Hamiltonians
1683        // are small).  exp(-iθH) is applied via scaling-and-squaring Taylor expm.
1684        let problem_dense =
1685            statevector::dense_from_triplets(self.problem_hamiltonian.triplets(), dim);
1686        let mixer_dense = statevector::dense_from_triplets(self.mixer_hamiltonian.triplets(), dim);
1687
1688        // |+⟩^⊗n = uniform superposition.
1689        let amp = Complex64::new(1.0 / (dim as f64).sqrt(), 0.0);
1690        let mut state = vec![amp; dim];
1691
1692        for layer in 0..self.num_layers {
1693            let gamma = parameters[2 * layer];
1694            let beta = parameters[2 * layer + 1];
1695            // exp(-iγ H_problem)
1696            let u_problem =
1697                statevector::expm_scaled(&problem_dense, dim, Complex64::new(0.0, -gamma))?;
1698            state = statevector::matvec(&u_problem, dim, &state);
1699            // exp(-iβ H_mixer)
1700            let u_mixer = statevector::expm_scaled(&mixer_dense, dim, Complex64::new(0.0, -beta))?;
1701            state = statevector::matvec(&u_mixer, dim, &state);
1702        }
1703
1704        let _ = num_qubits; // documented above; retained for clarity of intent
1705        let expectation =
1706            statevector::sparse_expectation(self.problem_hamiltonian.triplets(), &state);
1707        if expectation.im.abs() > 1e-6 {
1708            return Err(QuantRS2Error::ComputationError(format!(
1709                "QAOA energy has non-negligible imaginary part ({:.3e}); problem Hamiltonian is not Hermitian",
1710                expectation.im
1711            )));
1712        }
1713        Ok(expectation.re)
1714    }
1715}
1716
1717impl ObjectiveFunction for QAOAObjective {
1718    /// Problem-Hamiltonian energy of the QAOA state at the given `[γ,β,…]`.
1719    ///
1720    /// On a malformed configuration this returns `f64::INFINITY` (worst value
1721    /// for a minimizer) rather than a fabricated plausible energy.
1722    fn evaluate(&self, parameters: &[f64]) -> f64 {
1723        self.try_evaluate(parameters).unwrap_or(f64::INFINITY)
1724    }
1725
1726    fn bounds(&self) -> Vec<(f64, f64)> {
1727        self.bounds.clone()
1728    }
1729
1730    fn name(&self) -> &'static str {
1731        "QAOA"
1732    }
1733}
1734
1735impl Default for OptimizationConfig {
1736    fn default() -> Self {
1737        Self {
1738            algorithm: OptimizationAlgorithm::Adam {
1739                learning_rate: 0.01,
1740                beta1: 0.9,
1741                beta2: 0.999,
1742                epsilon: 1e-8,
1743            },
1744            max_evaluations: 1000,
1745            tolerance: 1e-6,
1746            seed: None,
1747            parallel: false,
1748            num_threads: None,
1749            progress_callback: None,
1750            early_stopping: None,
1751        }
1752    }
1753}
1754
1755/// Dense state-vector simulation helpers for the variational objectives.
1756///
1757/// `quantrs2-circuit` is a dependency of `quantrs2-sim`, so it cannot use the
1758/// simulator crate (dependency cycle).  These routines provide a small,
1759/// self-contained exact engine: gate application via the generic
1760/// [`GateOp::matrix`] interface, sparse `⟨ψ|H|ψ⟩`, and a dense complex
1761/// matrix exponential (scaling-and-squaring + Taylor) used by QAOA's
1762/// `exp(-iθH)` layers.
1763mod statevector {
1764    use super::{ParameterizedGate, QuantRS2Error, QuantRS2Result};
1765    use quantrs2_core::gate::{
1766        single::{Phase, RotationX, RotationY, RotationZ, T},
1767        GateOp,
1768    };
1769    use quantrs2_core::qubit::QubitId;
1770    use scirs2_core::Complex64;
1771
1772    /// Simulate a circuit template on `2^num_qubits` amplitudes from `|0…0⟩`.
1773    pub fn simulate_template(
1774        num_qubits: usize,
1775        structure: &[ParameterizedGate],
1776        parameters: &[f64],
1777    ) -> QuantRS2Result<Vec<Complex64>> {
1778        let dim = 1usize << num_qubits;
1779        let mut state = vec![Complex64::new(0.0, 0.0); dim];
1780        state[0] = Complex64::new(1.0, 0.0);
1781
1782        for gate in structure {
1783            let boxed = super::statevector::boxed_template_gate(gate, parameters)?;
1784            apply_gate(&mut state, num_qubits, boxed.as_ref())?;
1785        }
1786        Ok(state)
1787    }
1788
1789    /// Build the concrete `GateOp` for a template gate, binding its parameters.
1790    ///
1791    /// Only parameterized rotations need a boxed `core` gate here; the
1792    /// non-parameterized Clifford+T gates are produced via their builder-less
1793    /// constructors.  Multi-qubit gates (CNOT/CZ) are handled in
1794    /// [`apply_gate`] through the matrix interface after construction, so they
1795    /// are constructed here too.
1796    fn boxed_template_gate(
1797        gate: &ParameterizedGate,
1798        parameters: &[f64],
1799    ) -> QuantRS2Result<Box<dyn GateOp>> {
1800        use quantrs2_core::gate::multi::{CNOT, CZ};
1801        use quantrs2_core::gate::single::{Hadamard, PauliX, PauliY, PauliZ};
1802
1803        let target = |i: usize| -> QuantRS2Result<QubitId> {
1804            gate.qubits
1805                .get(i)
1806                .map(|&q| QubitId(q as u32))
1807                .ok_or_else(|| {
1808                    QuantRS2Error::InvalidInput(format!(
1809                        "Gate '{}' expects at least {} qubit(s)",
1810                        gate.gate_name,
1811                        i + 1
1812                    ))
1813                })
1814        };
1815        let angle = |slot: usize| super::template_gate_angle(gate, parameters, slot);
1816
1817        let boxed: Box<dyn GateOp> = match gate.gate_name.as_str() {
1818            "H" | "h" => Box::new(Hadamard { target: target(0)? }),
1819            "X" | "x" => Box::new(PauliX { target: target(0)? }),
1820            "Y" | "y" => Box::new(PauliY { target: target(0)? }),
1821            "Z" | "z" => Box::new(PauliZ { target: target(0)? }),
1822            "S" | "s" => Box::new(Phase { target: target(0)? }),
1823            "T" | "t" => Box::new(T { target: target(0)? }),
1824            "RX" | "rx" => Box::new(RotationX {
1825                target: target(0)?,
1826                theta: angle(0)?,
1827            }),
1828            "RY" | "ry" => Box::new(RotationY {
1829                target: target(0)?,
1830                theta: angle(0)?,
1831            }),
1832            "RZ" | "rz" => Box::new(RotationZ {
1833                target: target(0)?,
1834                theta: angle(0)?,
1835            }),
1836            "CNOT" | "cnot" | "CX" | "cx" => Box::new(CNOT {
1837                control: target(0)?,
1838                target: target(1)?,
1839            }),
1840            "CZ" | "cz" => Box::new(CZ {
1841                control: target(0)?,
1842                target: target(1)?,
1843            }),
1844            other => {
1845                return Err(QuantRS2Error::UnsupportedOperation(format!(
1846                    "Template gate '{other}' is not supported by the state-vector engine"
1847                )))
1848            }
1849        };
1850        Ok(boxed)
1851    }
1852
1853    /// Apply a (possibly multi-qubit) gate in place, MSB-first convention
1854    /// (the first qubit of `qubits()` is the high bit of the gate block).
1855    pub fn apply_gate(
1856        state: &mut [Complex64],
1857        num_qubits: usize,
1858        gate: &dyn GateOp,
1859    ) -> QuantRS2Result<()> {
1860        let targets: Vec<usize> = gate.qubits().iter().map(|q| q.id() as usize).collect();
1861        let k = targets.len();
1862        if k == 0 {
1863            return Ok(());
1864        }
1865        for &t in &targets {
1866            if t >= num_qubits {
1867                return Err(QuantRS2Error::InvalidInput(format!(
1868                    "Gate '{}' acts on qubit {t} but only {num_qubits} qubits are available",
1869                    gate.name()
1870                )));
1871            }
1872        }
1873        let matrix = gate.matrix()?;
1874        let side = 1usize << k;
1875        if matrix.len() != side * side {
1876            return Err(QuantRS2Error::InvalidInput(format!(
1877                "Gate '{}' returned {} matrix elements, expected {}",
1878                gate.name(),
1879                matrix.len(),
1880                side * side
1881            )));
1882        }
1883
1884        // local bit p (LSB=0) -> targets[k-1-p]  (first qubit = MSB)
1885        let bit_masks: Vec<usize> = (0..k).map(|p| 1usize << targets[k - 1 - p]).collect();
1886        let mut fixed_mask = 0usize;
1887        for &m in &bit_masks {
1888            fixed_mask |= m;
1889        }
1890        let dim = state.len();
1891        let mut visited = vec![false; dim];
1892        let mut amplitudes = vec![Complex64::new(0.0, 0.0); side];
1893        let mut indices = vec![0usize; side];
1894
1895        for base in 0..dim {
1896            if visited[base] || (base & fixed_mask) != 0 {
1897                continue;
1898            }
1899            for (local, slot) in indices.iter_mut().enumerate() {
1900                let mut idx = base;
1901                for (bit, &mask) in bit_masks.iter().enumerate() {
1902                    if (local >> bit) & 1 == 1 {
1903                        idx |= mask;
1904                    }
1905                }
1906                *slot = idx;
1907                amplitudes[local] = state[idx];
1908                visited[idx] = true;
1909            }
1910            for r in 0..side {
1911                let mut acc = Complex64::new(0.0, 0.0);
1912                let row = r * side;
1913                for (c, amp) in amplitudes.iter().enumerate() {
1914                    acc += matrix[row + c] * amp;
1915                }
1916                state[indices[r]] = acc;
1917            }
1918        }
1919        Ok(())
1920    }
1921
1922    /// `⟨ψ|H|ψ⟩` from a sparse Hamiltonian given as `(row, col, value)` triplets.
1923    pub fn sparse_expectation(
1924        triplets: &[(usize, usize, Complex64)],
1925        state: &[Complex64],
1926    ) -> Complex64 {
1927        let mut acc = Complex64::new(0.0, 0.0);
1928        for &(row, col, value) in triplets {
1929            if row < state.len() && col < state.len() {
1930                acc += state[row].conj() * value * state[col];
1931            }
1932        }
1933        acc
1934    }
1935
1936    /// Materialize a dense `dim × dim` (row-major) complex matrix from triplets.
1937    pub fn dense_from_triplets(
1938        triplets: &[(usize, usize, Complex64)],
1939        dim: usize,
1940    ) -> Vec<Complex64> {
1941        let mut dense = vec![Complex64::new(0.0, 0.0); dim * dim];
1942        for &(row, col, value) in triplets {
1943            if row < dim && col < dim {
1944                dense[row * dim + col] += value;
1945            }
1946        }
1947        dense
1948    }
1949
1950    /// Dense matrix-vector product `y = M x` (M row-major `dim × dim`).
1951    pub fn matvec(matrix: &[Complex64], dim: usize, x: &[Complex64]) -> Vec<Complex64> {
1952        let mut y = vec![Complex64::new(0.0, 0.0); dim];
1953        for (row, y_row) in y.iter_mut().enumerate() {
1954            let base = row * dim;
1955            let mut acc = Complex64::new(0.0, 0.0);
1956            for (col, x_col) in x.iter().enumerate() {
1957                acc += matrix[base + col] * x_col;
1958            }
1959            *y_row = acc;
1960        }
1961        y
1962    }
1963
1964    /// Compute `exp(scale · H)` for a dense `dim × dim` complex matrix `H` using
1965    /// scaling-and-squaring with a truncated Taylor series.
1966    ///
1967    /// This is the standard `expm` algorithm and is exact to machine precision
1968    /// for the small dimensions used by the variational objectives.  For
1969    /// `exp(-iθH)` pass `scale = Complex64::new(0.0, -θ)`.
1970    pub fn expm_scaled(
1971        h: &[Complex64],
1972        dim: usize,
1973        scale: Complex64,
1974    ) -> QuantRS2Result<Vec<Complex64>> {
1975        // A = scale * H
1976        let a: Vec<Complex64> = h.iter().map(|&v| v * scale).collect();
1977
1978        // Choose squaring count s so that ||A/2^s|| is small (≤ 0.5).
1979        let norm = matrix_inf_norm(&a, dim);
1980        let s = if norm <= 0.5 {
1981            0u32
1982        } else {
1983            (norm.log2().ceil().max(0.0) as u32) + 1
1984        };
1985        let scaling = Complex64::new(f64::from(1u32 << s).recip(), 0.0);
1986        let a_scaled: Vec<Complex64> = a.iter().map(|&v| v * scaling).collect();
1987
1988        // Taylor series: exp(B) = Σ B^k / k!, B = a_scaled.
1989        let mut result = identity(dim);
1990        let mut term = identity(dim);
1991        for k in 1..=18u32 {
1992            term = matmul(&term, &a_scaled, dim);
1993            let inv_factorial = Complex64::new(1.0_f64 / factorial(k), 0.0);
1994            for (r, t) in result.iter_mut().zip(term.iter()) {
1995                *r += *t * inv_factorial;
1996            }
1997        }
1998
1999        // Square s times.
2000        for _ in 0..s {
2001            result = matmul(&result, &result, dim);
2002        }
2003        Ok(result)
2004    }
2005
2006    fn factorial(n: u32) -> f64 {
2007        (1..=n).fold(1.0_f64, |acc, x| acc * f64::from(x))
2008    }
2009
2010    fn identity(dim: usize) -> Vec<Complex64> {
2011        let mut m = vec![Complex64::new(0.0, 0.0); dim * dim];
2012        for i in 0..dim {
2013            m[i * dim + i] = Complex64::new(1.0, 0.0);
2014        }
2015        m
2016    }
2017
2018    fn matmul(a: &[Complex64], b: &[Complex64], dim: usize) -> Vec<Complex64> {
2019        let mut c = vec![Complex64::new(0.0, 0.0); dim * dim];
2020        for i in 0..dim {
2021            for k in 0..dim {
2022                let a_ik = a[i * dim + k];
2023                if a_ik.norm_sqr() == 0.0 {
2024                    continue;
2025                }
2026                let brow = k * dim;
2027                let crow = i * dim;
2028                for j in 0..dim {
2029                    c[crow + j] += a_ik * b[brow + j];
2030                }
2031            }
2032        }
2033        c
2034    }
2035
2036    fn matrix_inf_norm(m: &[Complex64], dim: usize) -> f64 {
2037        let mut max_row = 0.0_f64;
2038        for i in 0..dim {
2039            let base = i * dim;
2040            let row_sum: f64 = (0..dim).map(|j| m[base + j].norm()).sum();
2041            if row_sum > max_row {
2042                max_row = row_sum;
2043            }
2044        }
2045        max_row
2046    }
2047}
2048
2049#[cfg(test)]
2050mod tests {
2051    use super::*;
2052
2053    #[test]
2054    fn test_optimization_config_creation() {
2055        let config = OptimizationConfig::default();
2056        assert_eq!(config.max_evaluations, 1000);
2057        assert_eq!(config.tolerance, 1e-6);
2058    }
2059
2060    #[test]
2061    fn test_vqe_objective() {
2062        // ⟨ψ|I|ψ⟩ = 1 for any normalized state, regardless of parameters.
2063        let hamiltonian = SparseMatrix::identity(4);
2064        let template = CircuitTemplate {
2065            structure: vec![ParameterizedGate {
2066                gate_name: "RY".to_string(),
2067                qubits: vec![0],
2068                parameter_indices: vec![0],
2069                fixed_parameters: Vec::new(),
2070            }],
2071            parameters: vec![Parameter {
2072                name: "theta".to_string(),
2073                lower_bound: 0.0,
2074                upper_bound: 2.0 * std::f64::consts::PI,
2075                initial_value: 0.5,
2076                discrete: false,
2077            }],
2078            num_qubits: 2,
2079        };
2080
2081        let objective = VQEObjective::new(hamiltonian, template);
2082        let value = objective.evaluate(&[0.5]);
2083        assert!((value - 1.0).abs() < 1e-9, "⟨I⟩ must equal 1, got {value}");
2084    }
2085
2086    /// `⟨0|RY(θ)† Z RY(θ)|0⟩ = cos θ` — pins the VQE objective to an analytic
2087    /// value, killing the former `Σ x²` fabrication.
2088    #[test]
2089    fn test_vqe_objective_matches_cos() {
2090        use std::f64::consts::PI;
2091        // H = Z on a single qubit: diagonal(+1, -1).
2092        let mut hamiltonian = SparseMatrix::zeros(2, 2);
2093        hamiltonian.insert(0, 0, Complex64::new(1.0, 0.0));
2094        hamiltonian.insert(1, 1, Complex64::new(-1.0, 0.0));
2095
2096        let template = CircuitTemplate {
2097            structure: vec![ParameterizedGate {
2098                gate_name: "RY".to_string(),
2099                qubits: vec![0],
2100                parameter_indices: vec![0],
2101                fixed_parameters: Vec::new(),
2102            }],
2103            parameters: vec![Parameter {
2104                name: "theta".to_string(),
2105                lower_bound: 0.0,
2106                upper_bound: 2.0 * PI,
2107                initial_value: 0.0,
2108                discrete: false,
2109            }],
2110            num_qubits: 1,
2111        };
2112        let objective = VQEObjective::new(hamiltonian, template);
2113
2114        for &theta in &[0.0, PI / 4.0, PI / 2.0, PI, 3.0 * PI / 2.0] {
2115            let value = objective.evaluate(&[theta]);
2116            assert!(
2117                (value - theta.cos()).abs() < 1e-9,
2118                "θ={theta}: got {value}, expected {}",
2119                theta.cos()
2120            );
2121        }
2122    }
2123
2124    #[test]
2125    fn test_qaoa_objective() {
2126        // For H_problem = I, ⟨ψ|I|ψ⟩ = 1 for the normalized QAOA state.
2127        let problem_h = SparseMatrix::identity(4);
2128        let mixer_h = SparseMatrix::identity(4);
2129
2130        let objective = QAOAObjective::new(problem_h, mixer_h, 2);
2131        assert_eq!(objective.bounds().len(), 4); // 2 parameters per layer
2132
2133        let value = objective.evaluate(&[0.5, 1.0, 1.5, 2.0]);
2134        assert!(
2135            (value - 1.0).abs() < 1e-9,
2136            "⟨I⟩ for QAOA state must equal 1, got {value}"
2137        );
2138    }
2139
2140    /// QAOA with a diagonal problem Hamiltonian H = diag(0,1,1,2) (a 2-qubit
2141    /// "number operator" Z-cost): the |+⟩^2 start has expectation = mean of the
2142    /// diagonal = 1.  With γ=β=0 the layers are identity, so the energy must be
2143    /// exactly that mean — would fail for the old `Σ sin² x` fabrication.
2144    #[test]
2145    fn test_qaoa_objective_zero_angles_is_diagonal_mean() {
2146        let mut problem_h = SparseMatrix::zeros(4, 4);
2147        problem_h.insert(0, 0, Complex64::new(0.0, 0.0));
2148        problem_h.insert(1, 1, Complex64::new(1.0, 0.0));
2149        problem_h.insert(2, 2, Complex64::new(1.0, 0.0));
2150        problem_h.insert(3, 3, Complex64::new(2.0, 0.0));
2151        let mixer_h = SparseMatrix::identity(4);
2152
2153        let objective = QAOAObjective::new(problem_h, mixer_h, 1);
2154        // γ=0, β=0 → identity layers → ⟨+⊗+|H|+⊗+⟩ = mean(diag) = 1.
2155        let value = objective.evaluate(&[0.0, 0.0]);
2156        assert!(
2157            (value - 1.0).abs() < 1e-9,
2158            "QAOA zero-angle energy should be diagonal mean 1, got {value}"
2159        );
2160    }
2161
2162    /// `exp(-iθH)` for a Pauli-X generator must reproduce an `RX(2θ)` rotation,
2163    /// validating the dense matrix-exponential used by QAOA.
2164    #[test]
2165    fn test_expm_matches_rx_rotation() {
2166        use std::f64::consts::PI;
2167        // H = X = [[0,1],[1,0]].
2168        let x = vec![
2169            Complex64::new(0.0, 0.0),
2170            Complex64::new(1.0, 0.0),
2171            Complex64::new(1.0, 0.0),
2172            Complex64::new(0.0, 0.0),
2173        ];
2174        let theta = PI / 3.0;
2175        // exp(-iθX) = cos θ I - i sin θ X.
2176        let u = statevector::expm_scaled(&x, 2, Complex64::new(0.0, -theta)).expect("expm");
2177        let c = theta.cos();
2178        let s = theta.sin();
2179        let expected = [
2180            Complex64::new(c, 0.0),
2181            Complex64::new(0.0, -s),
2182            Complex64::new(0.0, -s),
2183            Complex64::new(c, 0.0),
2184        ];
2185        for (got, want) in u.iter().zip(expected.iter()) {
2186            assert!((got - want).norm() < 1e-10, "expm element {got} vs {want}");
2187        }
2188    }
2189
2190    #[test]
2191    fn test_build_circuit_emits_gates() {
2192        let template = CircuitTemplate {
2193            structure: vec![
2194                ParameterizedGate {
2195                    gate_name: "RY".to_string(),
2196                    qubits: vec![0],
2197                    parameter_indices: vec![0],
2198                    fixed_parameters: Vec::new(),
2199                },
2200                ParameterizedGate {
2201                    gate_name: "CNOT".to_string(),
2202                    qubits: vec![0, 1],
2203                    parameter_indices: Vec::new(),
2204                    fixed_parameters: Vec::new(),
2205                },
2206            ],
2207            parameters: vec![Parameter {
2208                name: "theta".to_string(),
2209                lower_bound: 0.0,
2210                upper_bound: 2.0 * std::f64::consts::PI,
2211                initial_value: 0.3,
2212                discrete: false,
2213            }],
2214            num_qubits: 2,
2215        };
2216        let config = OptimizationConfig::default();
2217        let optimizer = QuantumCircuitOptimizer::new(template, config);
2218        let circuit = optimizer.build_circuit(&[0.3]).expect("build");
2219        // Previously returned an empty circuit; must now contain both gates.
2220        assert_eq!(circuit.gates().len(), 2);
2221    }
2222
2223    #[test]
2224    fn test_circuit_template() {
2225        let template = CircuitTemplate {
2226            structure: vec![ParameterizedGate {
2227                gate_name: "RY".to_string(),
2228                qubits: vec![0],
2229                parameter_indices: vec![0],
2230                fixed_parameters: Vec::new(),
2231            }],
2232            parameters: vec![Parameter {
2233                name: "theta".to_string(),
2234                lower_bound: 0.0,
2235                upper_bound: 2.0 * std::f64::consts::PI,
2236                initial_value: 0.0,
2237                discrete: false,
2238            }],
2239            num_qubits: 1,
2240        };
2241
2242        assert_eq!(template.parameters.len(), 1);
2243        assert_eq!(template.structure.len(), 1);
2244    }
2245
2246    struct TestObjective;
2247
2248    impl ObjectiveFunction for TestObjective {
2249        fn evaluate(&self, parameters: &[f64]) -> f64 {
2250            parameters.iter().map(|x| (x - 1.0).powi(2)).sum()
2251        }
2252
2253        fn bounds(&self) -> Vec<(f64, f64)> {
2254            vec![(-5.0, 5.0); 2]
2255        }
2256
2257        fn name(&self) -> &'static str {
2258            "test"
2259        }
2260    }
2261
2262    #[test]
2263    fn test_optimizer_creation() {
2264        let template = CircuitTemplate {
2265            structure: Vec::new(),
2266            parameters: vec![
2267                Parameter {
2268                    name: "x1".to_string(),
2269                    lower_bound: -5.0,
2270                    upper_bound: 5.0,
2271                    initial_value: 0.0,
2272                    discrete: false,
2273                },
2274                Parameter {
2275                    name: "x2".to_string(),
2276                    lower_bound: -5.0,
2277                    upper_bound: 5.0,
2278                    initial_value: 0.0,
2279                    discrete: false,
2280                },
2281            ],
2282            num_qubits: 1,
2283        };
2284
2285        let config = OptimizationConfig::default();
2286        let optimizer = QuantumCircuitOptimizer::new(template, config);
2287
2288        assert_eq!(optimizer.circuit_template.parameters.len(), 2);
2289    }
2290}