Skip to main content

quantrs2_circuit/
qc_co_optimization.rs

1//! Quantum-Classical Co-optimization Framework
2//!
3//! This module provides tools for optimizing hybrid quantum-classical algorithms
4//! where quantum circuits and classical processing are interleaved and optimized together.
5
6use crate::builder::Circuit;
7use quantrs2_core::{
8    error::{QuantRS2Error, QuantRS2Result},
9    gate::single::{RotationX, RotationY, RotationZ},
10    gate::GateOp,
11    qubit::QubitId,
12};
13use scirs2_core::parallel_ops::{IntoParallelRefIterator, ParallelIterator};
14use scirs2_core::Complex64;
15use std::collections::{HashMap, HashSet};
16
17/// A hybrid quantum-classical optimization problem
18///
19/// This combines quantum circuits with classical processing steps,
20/// allowing for co-optimization of both quantum parameters and classical algorithms.
21#[derive(Debug, Clone)]
22pub struct HybridOptimizationProblem<const N: usize> {
23    /// Quantum circuit components
24    pub quantum_circuits: Vec<ParameterizedQuantumComponent<N>>,
25    /// Classical processing steps
26    pub classical_steps: Vec<ClassicalProcessingStep>,
27    /// Data flow between quantum and classical components
28    pub data_flow: DataFlowGraph,
29    /// Global optimization parameters
30    pub global_parameters: Vec<f64>,
31    /// Objective function for optimization
32    pub objective: ObjectiveFunction,
33}
34
35/// A parameterized quantum circuit component
36#[derive(Debug, Clone)]
37pub struct ParameterizedQuantumComponent<const N: usize> {
38    /// The quantum circuit
39    pub circuit: Circuit<N>,
40    /// Parameter indices in the global parameter vector
41    pub parameter_indices: Vec<usize>,
42    /// Input data from classical components
43    pub classical_inputs: Vec<String>,
44    /// Output measurements to classical components
45    pub quantum_outputs: Vec<String>,
46    /// Component identifier
47    pub id: String,
48}
49
50/// A classical processing step in the hybrid algorithm
51#[derive(Debug, Clone)]
52pub struct ClassicalProcessingStep {
53    /// Step identifier
54    pub id: String,
55    /// Type of classical processing
56    pub step_type: ClassicalStepType,
57    /// Input data sources
58    pub inputs: Vec<String>,
59    /// Output data destinations
60    pub outputs: Vec<String>,
61    /// Parameters for this processing step
62    pub parameters: HashMap<String, f64>,
63}
64
65/// Types of classical processing steps
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum ClassicalStepType {
68    /// Linear algebra operations
69    LinearAlgebra(LinearAlgebraOp),
70    /// Machine learning model inference
71    MachineLearning(MLModelType),
72    /// Optimization subroutine
73    Optimization(OptimizationMethod),
74    /// Data preprocessing
75    DataProcessing(DataProcessingOp),
76    /// Control flow decision
77    ControlFlow(ControlFlowType),
78    /// Parameter update rule
79    ParameterUpdate(UpdateRule),
80    /// Custom processing function
81    Custom(String),
82}
83
84/// Linear algebra operations
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum LinearAlgebraOp {
87    MatrixMultiplication,
88    Eigendecomposition,
89    SVD,
90    LeastSquares,
91    LinearSolve,
92    TensorContraction,
93}
94
95/// Machine learning model types
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub enum MLModelType {
98    NeuralNetwork,
99    SupportVectorMachine,
100    RandomForest,
101    GaussianProcess,
102    LinearRegression,
103    LogisticRegression,
104}
105
106/// Optimization methods for classical subroutines
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub enum OptimizationMethod {
109    GradientDescent,
110    BFGS,
111    NelderMead,
112    SimulatedAnnealing,
113    GeneticAlgorithm,
114    BayesianOptimization,
115}
116
117/// Data preprocessing operations
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub enum DataProcessingOp {
120    Normalization,
121    Standardization,
122    PCA,
123    FeatureSelection,
124    DataAugmentation,
125    OutlierRemoval,
126}
127
128/// Control flow types
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub enum ControlFlowType {
131    Conditional,
132    Loop,
133    Parallel,
134    Adaptive,
135}
136
137/// Parameter update rules
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub enum UpdateRule {
140    GradientBased,
141    MomentumBased,
142    AdamOptimizer,
143    AdaGrad,
144    RMSProp,
145    Custom(String),
146}
147
148/// Data flow graph representing connections between components
149#[derive(Debug, Clone)]
150pub struct DataFlowGraph {
151    /// Nodes in the graph (component IDs)
152    pub nodes: Vec<String>,
153    /// Edges representing data flow (source, target, `data_type`)
154    pub edges: Vec<(String, String, DataType)>,
155    /// Execution order constraints
156    pub execution_order: Vec<Vec<String>>,
157}
158
159/// Types of data flowing between components
160#[derive(Debug, Clone, PartialEq)]
161pub enum DataType {
162    /// Quantum measurement results
163    Measurements(Vec<f64>),
164    /// Probability distributions
165    Probabilities(Vec<f64>),
166    /// Classical vectors/matrices
167    Matrix(Vec<Vec<f64>>),
168    /// Scalar values
169    Scalar(f64),
170    /// Parameter vectors
171    Parameters(Vec<f64>),
172    /// Boolean control signals
173    Control(bool),
174    /// Custom data format
175    Custom(String),
176}
177
178/// Objective function for hybrid optimization
179#[derive(Debug, Clone)]
180pub struct ObjectiveFunction {
181    /// Function type
182    pub function_type: ObjectiveFunctionType,
183    /// Target value (for minimization/maximization)
184    pub target: Option<f64>,
185    /// Weights for multi-objective optimization
186    pub weights: Vec<f64>,
187    /// Regularization terms
188    pub regularization: Vec<RegularizationTerm>,
189}
190
191/// Types of objective functions
192#[derive(Debug, Clone, PartialEq)]
193pub enum ObjectiveFunctionType {
194    /// Minimize expectation value
195    ExpectationValue,
196    /// Maximize fidelity
197    Fidelity,
198    /// Minimize cost function
199    CostFunction,
200    /// Multi-objective optimization
201    MultiObjective(Vec<Self>),
202    /// Custom objective
203    Custom(String),
204}
205
206/// Regularization terms for the objective function
207#[derive(Debug, Clone)]
208pub struct RegularizationTerm {
209    /// Type of regularization
210    pub reg_type: RegularizationType,
211    /// Regularization strength
212    pub strength: f64,
213    /// Parameters to regularize
214    pub parameter_indices: Vec<usize>,
215}
216
217/// Types of regularization
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub enum RegularizationType {
220    L1,
221    L2,
222    ElasticNet,
223    TotalVariation,
224    Sparsity,
225    Smoothness,
226}
227
228/// Hybrid optimization result
229#[derive(Debug, Clone)]
230pub struct HybridOptimizationResult {
231    /// Optimal parameters
232    pub optimal_parameters: Vec<f64>,
233    /// Optimal objective value
234    pub optimal_value: f64,
235    /// Number of iterations
236    pub iterations: usize,
237    /// Convergence status
238    pub converged: bool,
239    /// Execution history
240    pub history: OptimizationHistory,
241    /// Final quantum state information
242    pub quantum_info: QuantumStateInfo,
243}
244
245/// Optimization history tracking
246#[derive(Debug, Clone)]
247pub struct OptimizationHistory {
248    /// Objective values over iterations
249    pub objective_values: Vec<f64>,
250    /// Parameter values over iterations
251    pub parameter_history: Vec<Vec<f64>>,
252    /// Gradient norms
253    pub gradient_norms: Vec<f64>,
254    /// Step sizes used
255    pub step_sizes: Vec<f64>,
256    /// Timing information
257    pub execution_times: Vec<f64>,
258}
259
260/// Information about final quantum states
261#[derive(Debug, Clone)]
262pub struct QuantumStateInfo {
263    /// Final quantum states for each circuit
264    pub final_states: HashMap<String, Vec<Complex64>>,
265    /// Measurement statistics
266    pub measurement_stats: HashMap<String, MeasurementStatistics>,
267    /// Entanglement measures
268    pub entanglement_info: HashMap<String, EntanglementInfo>,
269}
270
271/// Statistics from quantum measurements
272#[derive(Debug, Clone)]
273pub struct MeasurementStatistics {
274    /// Mean values
275    pub means: Vec<f64>,
276    /// Standard deviations
277    pub std_devs: Vec<f64>,
278    /// Correlations between measurements
279    pub correlations: Vec<Vec<f64>>,
280    /// Number of shots used
281    pub num_shots: usize,
282}
283
284/// Entanglement information
285#[derive(Debug, Clone)]
286pub struct EntanglementInfo {
287    /// Von Neumann entropy
288    pub von_neumann_entropy: f64,
289    /// Mutual information matrix
290    pub mutual_information: Vec<Vec<f64>>,
291    /// Entanglement spectrum
292    pub entanglement_spectrum: Vec<f64>,
293}
294
295/// Hybrid optimizer for quantum-classical co-optimization
296pub struct HybridOptimizer {
297    /// Optimization algorithm
298    pub algorithm: HybridOptimizationAlgorithm,
299    /// Maximum iterations
300    pub max_iterations: usize,
301    /// Convergence tolerance
302    pub tolerance: f64,
303    /// Learning rate schedule
304    pub learning_rate_schedule: LearningRateSchedule,
305    /// Parallelization settings
306    pub parallelization: ParallelizationConfig,
307}
308
309/// Hybrid optimization algorithms
310#[derive(Debug, Clone, PartialEq, Eq)]
311pub enum HybridOptimizationAlgorithm {
312    /// Coordinate descent (alternate quantum and classical optimization)
313    CoordinateDescent,
314    /// Simultaneous optimization of all parameters
315    SimultaneousOptimization,
316    /// Hierarchical optimization (coarse-to-fine)
317    HierarchicalOptimization,
318    /// Adaptive algorithm selection
319    AdaptiveOptimization,
320    /// Custom algorithm
321    Custom(String),
322}
323
324/// Learning rate schedules
325#[derive(Debug, Clone)]
326pub struct LearningRateSchedule {
327    /// Initial learning rate
328    pub initial_rate: f64,
329    /// Schedule type
330    pub schedule_type: ScheduleType,
331    /// Schedule parameters
332    pub parameters: HashMap<String, f64>,
333}
334
335/// Types of learning rate schedules
336#[derive(Debug, Clone, PartialEq, Eq)]
337pub enum ScheduleType {
338    Constant,
339    LinearDecay,
340    ExponentialDecay,
341    StepDecay,
342    CosineAnnealing,
343    Adaptive,
344}
345
346/// Parallelization configuration
347#[derive(Debug, Clone)]
348pub struct ParallelizationConfig {
349    /// Number of parallel quantum circuit evaluations
350    pub quantum_parallelism: usize,
351    /// Number of parallel classical processing threads
352    pub classical_parallelism: usize,
353    /// Enable asynchronous execution
354    pub asynchronous: bool,
355    /// Load balancing strategy
356    pub load_balancing: LoadBalancingStrategy,
357}
358
359/// Load balancing strategies
360#[derive(Debug, Clone, PartialEq, Eq)]
361pub enum LoadBalancingStrategy {
362    RoundRobin,
363    WorkStealing,
364    Dynamic,
365    Static,
366}
367
368impl<const N: usize> HybridOptimizationProblem<N> {
369    /// Create a new hybrid optimization problem
370    #[must_use]
371    pub fn new() -> Self {
372        Self {
373            quantum_circuits: Vec::new(),
374            classical_steps: Vec::new(),
375            data_flow: DataFlowGraph {
376                nodes: Vec::new(),
377                edges: Vec::new(),
378                execution_order: Vec::new(),
379            },
380            global_parameters: Vec::new(),
381            objective: ObjectiveFunction {
382                function_type: ObjectiveFunctionType::ExpectationValue,
383                target: None,
384                weights: vec![1.0],
385                regularization: Vec::new(),
386            },
387        }
388    }
389
390    /// Add a quantum circuit component
391    pub fn add_quantum_component(
392        &mut self,
393        id: String,
394        circuit: Circuit<N>,
395        parameter_indices: Vec<usize>,
396    ) -> QuantRS2Result<()> {
397        // Validate parameter indices
398        for &idx in &parameter_indices {
399            if idx >= self.global_parameters.len() {
400                return Err(QuantRS2Error::InvalidInput(format!(
401                    "Parameter index {} out of range (total parameters: {})",
402                    idx,
403                    self.global_parameters.len()
404                )));
405            }
406        }
407
408        let component = ParameterizedQuantumComponent {
409            circuit,
410            parameter_indices,
411            classical_inputs: Vec::new(),
412            quantum_outputs: Vec::new(),
413            id: id.clone(),
414        };
415
416        self.quantum_circuits.push(component);
417        self.data_flow.nodes.push(id);
418        Ok(())
419    }
420
421    /// Add a classical processing step
422    pub fn add_classical_step(
423        &mut self,
424        id: String,
425        step_type: ClassicalStepType,
426        inputs: Vec<String>,
427        outputs: Vec<String>,
428    ) -> QuantRS2Result<()> {
429        let step = ClassicalProcessingStep {
430            id: id.clone(),
431            step_type,
432            inputs,
433            outputs,
434            parameters: HashMap::new(),
435        };
436
437        self.classical_steps.push(step);
438        self.data_flow.nodes.push(id);
439        Ok(())
440    }
441
442    /// Add data flow edge between components
443    pub fn add_data_flow(
444        &mut self,
445        source: String,
446        target: String,
447        data_type: DataType,
448    ) -> QuantRS2Result<()> {
449        // Validate that source and target exist
450        if !self.data_flow.nodes.contains(&source) {
451            return Err(QuantRS2Error::InvalidInput(format!(
452                "Source component '{source}' not found"
453            )));
454        }
455        if !self.data_flow.nodes.contains(&target) {
456            return Err(QuantRS2Error::InvalidInput(format!(
457                "Target component '{target}' not found"
458            )));
459        }
460
461        self.data_flow.edges.push((source, target, data_type));
462        Ok(())
463    }
464
465    /// Set global parameters
466    pub fn set_global_parameters(&mut self, parameters: Vec<f64>) {
467        self.global_parameters = parameters;
468    }
469
470    /// Add regularization term
471    pub fn add_regularization(
472        &mut self,
473        reg_type: RegularizationType,
474        strength: f64,
475        parameter_indices: Vec<usize>,
476    ) -> QuantRS2Result<()> {
477        // Validate parameter indices
478        for &idx in &parameter_indices {
479            if idx >= self.global_parameters.len() {
480                return Err(QuantRS2Error::InvalidInput(format!(
481                    "Parameter index {idx} out of range"
482                )));
483            }
484        }
485
486        self.objective.regularization.push(RegularizationTerm {
487            reg_type,
488            strength,
489            parameter_indices,
490        });
491
492        Ok(())
493    }
494
495    /// Validate the optimization problem
496    pub fn validate(&self) -> QuantRS2Result<()> {
497        // Check that all components are connected properly
498        for edge in &self.data_flow.edges {
499            let (source, target, _) = edge;
500            if !self.data_flow.nodes.contains(source) {
501                return Err(QuantRS2Error::InvalidInput(format!(
502                    "Data flow edge references non-existent source '{source}'"
503                )));
504            }
505            if !self.data_flow.nodes.contains(target) {
506                return Err(QuantRS2Error::InvalidInput(format!(
507                    "Data flow edge references non-existent target '{target}'"
508                )));
509            }
510        }
511
512        // Check for circular dependencies
513        if self.has_circular_dependencies()? {
514            return Err(QuantRS2Error::InvalidInput(
515                "Circular dependencies detected in data flow graph".to_string(),
516            ));
517        }
518
519        Ok(())
520    }
521
522    /// Check for circular dependencies in the data flow graph.
523    ///
524    /// Performs a standard three-colour (white/gray/black) depth-first search
525    /// over `data_flow.nodes`/`data_flow.edges`: a node is *gray* while it is
526    /// on the current DFS recursion stack and *black* once fully explored.
527    /// Encountering an edge into a gray node means the recursion stack itself
528    /// forms a cycle (e.g. `A -> B -> C -> A`), not merely a direct self-loop.
529    fn has_circular_dependencies(&self) -> QuantRS2Result<bool> {
530        #[derive(Clone, Copy, PartialEq, Eq)]
531        enum Colour {
532            White,
533            Gray,
534            Black,
535        }
536
537        // Adjacency list keyed by node name, built once up front.
538        let mut adjacency: HashMap<&str, Vec<&str>> = HashMap::new();
539        for node in &self.data_flow.nodes {
540            adjacency.entry(node.as_str()).or_default();
541        }
542        for (source, target, _) in &self.data_flow.edges {
543            adjacency
544                .entry(source.as_str())
545                .or_default()
546                .push(target.as_str());
547        }
548
549        let mut colour: HashMap<&str, Colour> = self
550            .data_flow
551            .nodes
552            .iter()
553            .map(|n| (n.as_str(), Colour::White))
554            .collect();
555
556        // Iterative DFS (explicit stack) to avoid unbounded recursion depth on
557        // large graphs; each stack frame tracks its outgoing-edge cursor.
558        for start in &self.data_flow.nodes {
559            if colour.get(start.as_str()).copied() != Some(Colour::White) {
560                continue;
561            }
562
563            let mut stack: Vec<(&str, usize)> = vec![(start.as_str(), 0)];
564            colour.insert(start.as_str(), Colour::Gray);
565
566            while let Some((node, cursor)) = stack.pop() {
567                let neighbours = adjacency.get(node).map(Vec::as_slice).unwrap_or(&[]);
568                if cursor < neighbours.len() {
569                    let next = neighbours[cursor];
570                    // Resume this frame at the following neighbour once we
571                    // return to it.
572                    stack.push((node, cursor + 1));
573                    match colour.get(next).copied() {
574                        Some(Colour::Gray) => return Ok(true), // back-edge => cycle
575                        Some(Colour::White) => {
576                            colour.insert(next, Colour::Gray);
577                            stack.push((next, 0));
578                        }
579                        Some(Colour::Black) | None => {}
580                    }
581                } else {
582                    colour.insert(node, Colour::Black);
583                }
584            }
585        }
586
587        Ok(false)
588    }
589}
590
591impl Default for HybridOptimizationProblem<4> {
592    fn default() -> Self {
593        Self::new()
594    }
595}
596
597impl HybridOptimizer {
598    /// Create a new hybrid optimizer
599    #[must_use]
600    pub fn new(algorithm: HybridOptimizationAlgorithm) -> Self {
601        Self {
602            algorithm,
603            max_iterations: 1000,
604            tolerance: 1e-6,
605            learning_rate_schedule: LearningRateSchedule {
606                initial_rate: 0.01,
607                schedule_type: ScheduleType::Constant,
608                parameters: HashMap::new(),
609            },
610            parallelization: ParallelizationConfig {
611                quantum_parallelism: 1,
612                classical_parallelism: 1,
613                asynchronous: false,
614                load_balancing: LoadBalancingStrategy::RoundRobin,
615            },
616        }
617    }
618
619    /// Optimize a hybrid quantum-classical problem.
620    ///
621    /// The concrete update rule performed each iteration depends on
622    /// [`Self::algorithm`] — see [`Self::active_parameter_mask`] for exactly
623    /// how each [`HybridOptimizationAlgorithm`] variant differs from plain
624    /// full-batch gradient descent. [`HybridOptimizationAlgorithm::Custom`]
625    /// names an algorithm this optimizer does not implement and is rejected
626    /// with an honest [`QuantRS2Error::UnsupportedOperation`] rather than
627    /// silently running as if it were [`HybridOptimizationAlgorithm::SimultaneousOptimization`].
628    pub fn optimize<const N: usize>(
629        &self,
630        problem: &mut HybridOptimizationProblem<N>,
631    ) -> QuantRS2Result<HybridOptimizationResult> {
632        // Validate the problem first
633        problem.validate()?;
634
635        if let HybridOptimizationAlgorithm::Custom(name) = &self.algorithm {
636            return Err(QuantRS2Error::UnsupportedOperation(format!(
637                "custom hybrid optimization algorithm '{name}' is not implemented; use \
638                 CoordinateDescent, SimultaneousOptimization, HierarchicalOptimization, or \
639                 AdaptiveOptimization, each of which runs a genuinely distinct update rule"
640            )));
641        }
642
643        // Global-parameter indices that drive at least one quantum gate
644        // (via some component's `parameter_indices`) versus the remainder,
645        // which only ever appear in classical regularization terms. This
646        // partition is what `CoordinateDescent` and `AdaptiveOptimization`
647        // alternate between.
648        let quantum_indices = quantum_parameter_indices(problem);
649
650        // Initialize optimization history
651        let mut history = OptimizationHistory {
652            objective_values: Vec::new(),
653            parameter_history: Vec::new(),
654            gradient_norms: Vec::new(),
655            step_sizes: Vec::new(),
656            execution_times: Vec::new(),
657        };
658
659        let mut current_parameters = problem.global_parameters.clone();
660        let mut best_parameters = current_parameters.clone();
661        let mut best_value = f64::INFINITY;
662        let num_params = current_parameters.len();
663
664        // Main optimization loop
665        for iteration in 0..self.max_iterations {
666            let start_time = std::time::Instant::now();
667
668            // Evaluate objective function
669            let current_value = self.evaluate_objective(problem, &current_parameters)?;
670
671            if current_value < best_value {
672                best_value = current_value;
673                best_parameters.clone_from(&current_parameters);
674            }
675
676            // Store history
677            history.objective_values.push(current_value);
678            history.parameter_history.push(current_parameters.clone());
679
680            // Compute gradients (parameter-shift for the quantum part plus the
681            // analytic regularization derivative).
682            let gradients = self.compute_gradients(problem, &current_parameters)?;
683            let gradient_norm = gradients.iter().map(|g| g * g).sum::<f64>().sqrt();
684            history.gradient_norms.push(gradient_norm);
685
686            // Check convergence
687            if gradient_norm < self.tolerance {
688                let execution_time = start_time.elapsed().as_secs_f64();
689                history.execution_times.push(execution_time);
690
691                // Make the problem (and hence the extracted quantum state)
692                // reflect the best parameters found before reporting.
693                problem.global_parameters.clone_from(&best_parameters);
694                let quantum_info = self.extract_quantum_info(problem)?;
695                return Ok(HybridOptimizationResult {
696                    optimal_parameters: best_parameters,
697                    optimal_value: best_value,
698                    iterations: iteration + 1,
699                    converged: true,
700                    history,
701                    quantum_info,
702                });
703            }
704
705            // Which parameters this iteration actually updates, per
706            // `self.algorithm`.
707            let active = self.active_parameter_mask(
708                &quantum_indices,
709                iteration,
710                num_params,
711                &history.objective_values,
712            );
713
714            // Update parameters (only the active block), and track the real
715            // step actually taken -- not the full unmasked gradient -- so
716            // `history.step_sizes` honestly reflects block algorithms too.
717            let learning_rate = self.get_learning_rate(iteration, &history.gradient_norms);
718            let mut applied_grad_norm_sq = 0.0;
719            for (i, gradient) in gradients.iter().enumerate() {
720                if active[i] {
721                    current_parameters[i] -= learning_rate * gradient;
722                    applied_grad_norm_sq += gradient * gradient;
723                }
724            }
725
726            let step_size = learning_rate * applied_grad_norm_sq.sqrt();
727            history.step_sizes.push(step_size);
728
729            let execution_time = start_time.elapsed().as_secs_f64();
730            history.execution_times.push(execution_time);
731        }
732
733        // Maximum iterations reached: report the best parameters seen.
734        problem.global_parameters.clone_from(&best_parameters);
735        let quantum_info = self.extract_quantum_info(problem)?;
736        Ok(HybridOptimizationResult {
737            optimal_parameters: best_parameters,
738            optimal_value: best_value,
739            iterations: self.max_iterations,
740            converged: false,
741            history,
742            quantum_info,
743        })
744    }
745
746    /// Evaluate the hybrid objective function for a concrete parameter vector.
747    ///
748    /// This is a *real* evaluation, not a placeholder.  For every quantum
749    /// component the provided `parameters` are bound into the component's
750    /// parameterized rotation gates (the `k`-th entry of the component's
751    /// `parameter_indices` drives the `k`-th parameterized gate, in gate order —
752    /// the standard ansatz convention) and the resulting state `|ψ(θ)⟩` is
753    /// produced by an exact dense state-vector simulation
754    /// ([`statevector::simulate`]).  A scalar cost is then derived from that
755    /// state according to [`ObjectiveFunctionType`]:
756    ///
757    /// * [`ObjectiveFunctionType::ExpectationValue`] /
758    ///   [`ObjectiveFunctionType::CostFunction`] / [`ObjectiveFunctionType::Custom`]
759    ///   minimize `⟨ψ| (Σ_q Z_q) |ψ⟩` (the canonical diagonal cost Hamiltonian
760    ///   whose ground state is `|0…0⟩`).
761    /// * [`ObjectiveFunctionType::Fidelity`] maximizes the overlap with `|0…0⟩`,
762    ///   expressed as the minimization objective `1 − |⟨0…0|ψ⟩|²`.
763    /// * [`ObjectiveFunctionType::MultiObjective`] combines its sub-objectives.
764    ///
765    /// Per-component contributions are combined with `objective.weights`
766    /// (defaulting to weight `1`), and every regularization term is added on top
767    /// via [`Self::regularization_value`].  These regularization terms make the
768    /// objective depend on parameters that do not drive any gate, exactly as a
769    /// real hybrid cost would.
770    fn evaluate_objective<const N: usize>(
771        &self,
772        problem: &HybridOptimizationProblem<N>,
773        parameters: &[f64],
774    ) -> QuantRS2Result<f64> {
775        let eval_component = |component_index: usize| -> QuantRS2Result<f64> {
776            let component = &problem.quantum_circuits[component_index];
777            let bound = bind_parameters(component, parameters)?;
778            let state = statevector::simulate(&bound)?;
779            let contribution =
780                Self::objective_from_state(&state, N, &problem.objective.function_type)?;
781            let weight = problem
782                .objective
783                .weights
784                .get(component_index)
785                .copied()
786                .unwrap_or(1.0);
787            Ok(weight * contribution)
788        };
789
790        // Each component's state-vector simulation is fully independent, so
791        // `parallelization.quantum_parallelism` (the configured number of
792        // parallel quantum circuit evaluations) genuinely drives whether this
793        // runs across the SciRS2 parallel executor or sequentially in-order.
794        let component_indices: Vec<usize> = (0..problem.quantum_circuits.len()).collect();
795        let component_values: Vec<QuantRS2Result<f64>> =
796            if self.parallelization.quantum_parallelism > 1 && component_indices.len() > 1 {
797                component_indices
798                    .par_iter()
799                    .map(|&idx| eval_component(idx))
800                    .collect()
801            } else {
802                component_indices
803                    .iter()
804                    .map(|&idx| eval_component(idx))
805                    .collect()
806            };
807
808        let mut value = 0.0;
809        for contribution in component_values {
810            value += contribution?;
811        }
812
813        // Classical regularization terms operate directly on the parameter
814        // vector and are genuine (parameter-dependent) contributions; they are
815        // the "classical processing" this optimizer performs, so
816        // `parallelization.classical_parallelism` drives their evaluation.
817        let regularization_values: Vec<QuantRS2Result<f64>> =
818            if self.parallelization.classical_parallelism > 1
819                && problem.objective.regularization.len() > 1
820            {
821                problem
822                    .objective
823                    .regularization
824                    .par_iter()
825                    .map(|term| Self::regularization_value(term, parameters))
826                    .collect()
827            } else {
828                problem
829                    .objective
830                    .regularization
831                    .iter()
832                    .map(|term| Self::regularization_value(term, parameters))
833                    .collect()
834            };
835        for contribution in regularization_values {
836            value += contribution?;
837        }
838
839        Ok(value)
840    }
841
842    /// Derive a scalar cost from a simulated state according to `function_type`.
843    fn objective_from_state(
844        state: &[Complex64],
845        num_qubits: usize,
846        function_type: &ObjectiveFunctionType,
847    ) -> QuantRS2Result<f64> {
848        match function_type {
849            ObjectiveFunctionType::ExpectationValue
850            | ObjectiveFunctionType::CostFunction
851            | ObjectiveFunctionType::Custom(_) => {
852                // ⟨Σ_q Z_q⟩ for the diagonal cost Hamiltonian.
853                Ok(statevector::sum_z_expectation(state, num_qubits))
854            }
855            ObjectiveFunctionType::Fidelity => {
856                // Maximize fidelity with |0…0⟩ ⇒ minimize 1 − |⟨0…0|ψ⟩|².
857                let amplitude = state.first().copied().unwrap_or(Complex64::new(0.0, 0.0));
858                Ok(1.0 - amplitude.norm_sqr())
859            }
860            ObjectiveFunctionType::MultiObjective(sub_objectives) => {
861                let mut total = 0.0;
862                for sub in sub_objectives {
863                    total += Self::objective_from_state(state, num_qubits, sub)?;
864                }
865                Ok(total)
866            }
867        }
868    }
869
870    /// Value of a single regularization term for the given parameter vector.
871    fn regularization_value(term: &RegularizationTerm, parameters: &[f64]) -> QuantRS2Result<f64> {
872        let selected = collect_parameters(term, parameters)?;
873        let penalty = match term.reg_type {
874            RegularizationType::L1 | RegularizationType::Sparsity => {
875                selected.iter().map(|p| p.abs()).sum::<f64>()
876            }
877            RegularizationType::L2 => selected.iter().map(|p| p * p).sum::<f64>(),
878            RegularizationType::ElasticNet => {
879                let l1 = selected.iter().map(|p| p.abs()).sum::<f64>();
880                let l2 = selected.iter().map(|p| p * p).sum::<f64>();
881                0.5 * l1 + 0.5 * l2
882            }
883            RegularizationType::TotalVariation | RegularizationType::Smoothness => {
884                // Sum of squared consecutive differences (∝ discrete gradient
885                // energy), the standard smoothness/total-variation penalty.
886                selected
887                    .windows(2)
888                    .map(|w| {
889                        let d = w[1] - w[0];
890                        d * d
891                    })
892                    .sum::<f64>()
893            }
894        };
895        Ok(term.strength * penalty)
896    }
897
898    /// Analytic gradient of a single regularization term w.r.t. every global
899    /// parameter (zero for parameters the term does not touch).
900    fn regularization_gradient(
901        term: &RegularizationTerm,
902        parameters: &[f64],
903        gradient: &mut [f64],
904    ) -> QuantRS2Result<()> {
905        for &idx in &term.parameter_indices {
906            if idx >= parameters.len() {
907                return Err(QuantRS2Error::InvalidInput(format!(
908                    "Regularization parameter index {idx} out of range (total parameters: {})",
909                    parameters.len()
910                )));
911            }
912        }
913
914        match term.reg_type {
915            RegularizationType::L1 | RegularizationType::Sparsity => {
916                for &idx in &term.parameter_indices {
917                    gradient[idx] += term.strength * parameters[idx].signum();
918                }
919            }
920            RegularizationType::L2 => {
921                for &idx in &term.parameter_indices {
922                    gradient[idx] += term.strength * 2.0 * parameters[idx];
923                }
924            }
925            RegularizationType::ElasticNet => {
926                for &idx in &term.parameter_indices {
927                    gradient[idx] +=
928                        term.strength * (0.5 * parameters[idx].signum() + parameters[idx]);
929                }
930            }
931            RegularizationType::TotalVariation | RegularizationType::Smoothness => {
932                // d/dθ_k Σ_j (θ_{j+1} − θ_j)² for the ordered selected indices.
933                let indices = &term.parameter_indices;
934                for window in indices.windows(2) {
935                    let (lo, hi) = (window[0], window[1]);
936                    let diff = parameters[hi] - parameters[lo];
937                    gradient[hi] += term.strength * 2.0 * diff;
938                    gradient[lo] -= term.strength * 2.0 * diff;
939                }
940            }
941        }
942
943        Ok(())
944    }
945
946    /// Compute the objective gradient with respect to every global parameter.
947    ///
948    /// The quantum contribution of each parameter is obtained with the analytic
949    /// **parameter-shift rule** — for rotation gates `U(θ) = exp(−i θ P / 2)`
950    /// (the `RX`/`RY`/`RZ` gates this module binds), `∂⟨H⟩/∂θ = ½[E(θ + π/2) −
951    /// E(θ − π/2)]` is exact.  The classical regularization terms contribute
952    /// their exact analytic derivative.  Parameters that drive no gate still get
953    /// their (regularization) gradient, so the result is a faithful gradient of
954    /// the real objective evaluated by [`Self::evaluate_objective`].
955    fn compute_gradients<const N: usize>(
956        &self,
957        problem: &HybridOptimizationProblem<N>,
958        parameters: &[f64],
959    ) -> QuantRS2Result<Vec<f64>> {
960        let num_params = parameters.len();
961        let shift = std::f64::consts::FRAC_PI_2;
962
963        // Flatten every (component, parameterized-gate) parameter-shift
964        // evaluation into an independent job `(component_index, global_index,
965        // weight)`. Each job requires two full state-vector simulations and
966        // is otherwise completely independent of every other job, which is
967        // exactly what `parallelization.quantum_parallelism` ("number of
968        // parallel quantum circuit evaluations") promises to parallelize.
969        let mut jobs: Vec<(usize, usize, f64)> = Vec::new();
970        for (component_index, component) in problem.quantum_circuits.iter().enumerate() {
971            let num_param_gates = count_parameterized_gates(&component.circuit);
972            let weight = problem
973                .objective
974                .weights
975                .get(component_index)
976                .copied()
977                .unwrap_or(1.0);
978
979            for slot in 0..num_param_gates.min(component.parameter_indices.len()) {
980                let global_index = component.parameter_indices[slot];
981                if global_index >= num_params {
982                    return Err(QuantRS2Error::InvalidInput(format!(
983                        "Component '{}' references parameter index {} but only {} parameters exist",
984                        component.id, global_index, num_params
985                    )));
986                }
987                jobs.push((component_index, global_index, weight));
988            }
989        }
990
991        let eval_job = |&(component_index, global_index, weight): &(usize, usize, f64)| -> QuantRS2Result<(usize, f64)> {
992            let component = &problem.quantum_circuits[component_index];
993
994            let mut plus = parameters.to_vec();
995            plus[global_index] += shift;
996            let bound_plus = bind_parameters(component, &plus)?;
997            let state_plus = statevector::simulate(&bound_plus)?;
998            let energy_plus =
999                Self::objective_from_state(&state_plus, N, &problem.objective.function_type)?;
1000
1001            let mut minus = parameters.to_vec();
1002            minus[global_index] -= shift;
1003            let bound_minus = bind_parameters(component, &minus)?;
1004            let state_minus = statevector::simulate(&bound_minus)?;
1005            let energy_minus =
1006                Self::objective_from_state(&state_minus, N, &problem.objective.function_type)?;
1007
1008            Ok((global_index, weight * 0.5 * (energy_plus - energy_minus)))
1009        };
1010
1011        let contributions: Vec<QuantRS2Result<(usize, f64)>> =
1012            if self.parallelization.quantum_parallelism > 1 && jobs.len() > 1 {
1013                jobs.par_iter().map(eval_job).collect()
1014            } else {
1015                jobs.iter().map(eval_job).collect()
1016            };
1017
1018        let mut gradients = vec![0.0; num_params];
1019        for contribution in contributions {
1020            let (global_index, value) = contribution?;
1021            gradients[global_index] += value;
1022        }
1023
1024        // Every regularization term's analytic gradient is independent of
1025        // every other term, so `parallelization.classical_parallelism` (the
1026        // "classical processing" parallel worker count) drives whether these
1027        // run concurrently, each accumulating into its own gradient buffer
1028        // that is then summed sequentially.
1029        if self.parallelization.classical_parallelism > 1
1030            && problem.objective.regularization.len() > 1
1031        {
1032            let partials: Vec<QuantRS2Result<Vec<f64>>> = problem
1033                .objective
1034                .regularization
1035                .par_iter()
1036                .map(|term| {
1037                    let mut partial = vec![0.0; num_params];
1038                    Self::regularization_gradient(term, parameters, &mut partial)?;
1039                    Ok(partial)
1040                })
1041                .collect();
1042            for partial in partials {
1043                let partial = partial?;
1044                for (g, p) in gradients.iter_mut().zip(partial) {
1045                    *g += p;
1046                }
1047            }
1048        } else {
1049            for term in &problem.objective.regularization {
1050                Self::regularization_gradient(term, parameters, &mut gradients)?;
1051            }
1052        }
1053
1054        Ok(gradients)
1055    }
1056
1057    /// Get the learning rate for the current iteration under
1058    /// `self.learning_rate_schedule.schedule_type`.
1059    ///
1060    /// `gradient_norm_history` is `history.gradient_norms` as built up so far
1061    /// (including the value just recorded for `iteration`); it drives
1062    /// [`ScheduleType::Adaptive`], the only schedule whose rate depends on the
1063    /// optimization trajectory rather than purely on `iteration`.
1064    fn get_learning_rate(&self, iteration: usize, gradient_norm_history: &[f64]) -> f64 {
1065        let initial_rate = self.learning_rate_schedule.initial_rate;
1066        let params = &self.learning_rate_schedule.parameters;
1067
1068        match self.learning_rate_schedule.schedule_type {
1069            ScheduleType::Constant => initial_rate,
1070            ScheduleType::LinearDecay => {
1071                let decay_rate = params.get("decay_rate").copied().unwrap_or(0.001);
1072                initial_rate / (1.0 + decay_rate * iteration as f64)
1073            }
1074            ScheduleType::ExponentialDecay => {
1075                let decay_rate = params.get("decay_rate").copied().unwrap_or(0.95);
1076                initial_rate * decay_rate.powi(iteration as i32)
1077            }
1078            ScheduleType::StepDecay => {
1079                // Piecewise-constant: multiply by `decay_factor` every
1080                // `step_size` iterations, e.g. rate, rate*f, rate*f^2, ...
1081                let step_size = params.get("step_size").copied().unwrap_or(100.0).max(1.0);
1082                let decay_factor = params.get("decay_factor").copied().unwrap_or(0.5);
1083                let num_steps = (iteration as f64 / step_size).floor();
1084                initial_rate * decay_factor.powf(num_steps)
1085            }
1086            ScheduleType::CosineAnnealing => {
1087                // Standard cosine annealing from `initial_rate` down to
1088                // `min_rate` over `max_iterations`.
1089                let min_rate = params.get("min_rate").copied().unwrap_or(0.0);
1090                let total = (self.max_iterations.max(1) - 1) as f64;
1091                let progress = if total > 0.0 {
1092                    (iteration as f64 / total).min(1.0)
1093                } else {
1094                    0.0
1095                };
1096                min_rate
1097                    + 0.5
1098                        * (initial_rate - min_rate)
1099                        * (1.0 + (std::f64::consts::PI * progress).cos())
1100            }
1101            ScheduleType::Adaptive => {
1102                // Scale the rate by the ratio of the previous to the current
1103                // gradient norm: a shrinking gradient (converging nicely)
1104                // grows the rate (up to `max_scale`); a growing gradient
1105                // (overshooting / diverging) shrinks it (down to
1106                // `min_scale`), a simple, bounded Rprop-style adaptation.
1107                let min_scale = params.get("min_scale").copied().unwrap_or(0.5);
1108                let max_scale = params.get("max_scale").copied().unwrap_or(2.0);
1109                let scale = match gradient_norm_history {
1110                    [.., previous, current] => {
1111                        let ratio = previous / current.max(1e-15);
1112                        ratio.clamp(min_scale, max_scale)
1113                    }
1114                    _ => 1.0,
1115                };
1116                initial_rate * scale
1117            }
1118        }
1119    }
1120
1121    /// Which global-parameter indices are updated this iteration, per
1122    /// `self.algorithm`.
1123    ///
1124    /// * [`HybridOptimizationAlgorithm::SimultaneousOptimization`] updates
1125    ///   every parameter every iteration (plain full-batch gradient descent).
1126    /// * [`HybridOptimizationAlgorithm::CoordinateDescent`] alternates whole
1127    ///   blocks: on even iterations only `quantum_indices` (parameters that
1128    ///   drive at least one gate) update; on odd iterations only the
1129    ///   remaining classical/regularization-only parameters update. If either
1130    ///   block is empty the alternation is degenerate, so every parameter is
1131    ///   simply updated every iteration.
1132    /// * [`HybridOptimizationAlgorithm::HierarchicalOptimization`] anneals
1133    ///   from coarse to fine resolution: early iterations only update
1134    ///   parameters at indices that are multiples of a shrinking
1135    ///   power-of-two stride, converging to "every parameter" (stride `1`) by
1136    ///   the end of the run -- a coarse-to-fine parameter grouping.
1137    /// * [`HybridOptimizationAlgorithm::AdaptiveOptimization`] behaves like
1138    ///   `SimultaneousOptimization` while the objective keeps improving, and
1139    ///   falls back to the `CoordinateDescent` block schedule as soon as it
1140    ///   fails to improve on the previous iteration (an adaptive escape from
1141    ///   a stalled full-batch step).
1142    ///
1143    /// [`HybridOptimizationAlgorithm::Custom`] never reaches this method:
1144    /// `optimize` rejects it up front with an honest
1145    /// [`QuantRS2Error::UnsupportedOperation`].
1146    fn active_parameter_mask(
1147        &self,
1148        quantum_indices: &HashSet<usize>,
1149        iteration: usize,
1150        num_params: usize,
1151        recent_objectives: &[f64],
1152    ) -> Vec<bool> {
1153        match &self.algorithm {
1154            HybridOptimizationAlgorithm::SimultaneousOptimization => vec![true; num_params],
1155            HybridOptimizationAlgorithm::CoordinateDescent => {
1156                coordinate_descent_mask(quantum_indices, iteration, num_params)
1157            }
1158            HybridOptimizationAlgorithm::HierarchicalOptimization => {
1159                hierarchical_mask(iteration, self.max_iterations, num_params)
1160            }
1161            HybridOptimizationAlgorithm::AdaptiveOptimization => {
1162                let improving = match recent_objectives {
1163                    [.., previous, current] => *current < previous - 1e-12,
1164                    _ => true,
1165                };
1166                if improving {
1167                    vec![true; num_params]
1168                } else {
1169                    coordinate_descent_mask(quantum_indices, iteration, num_params)
1170                }
1171            }
1172            HybridOptimizationAlgorithm::Custom(_) => vec![true; num_params],
1173        }
1174    }
1175
1176    /// Extract real quantum-state information for every component.
1177    ///
1178    /// Each component's circuit is bound with the problem's current
1179    /// `global_parameters` and simulated exactly; from the resulting amplitudes
1180    /// we record:
1181    ///
1182    /// * `final_states` — the full `2^N` state vector `|ψ(θ)⟩`.
1183    /// * `measurement_stats` — per-qubit `⟨Z⟩` means with their statistical
1184    ///   standard deviations `√(1 − ⟨Z⟩²)` (the exact spread of a `±1` Z
1185    ///   measurement) computed directly from the state.
1186    /// * `entanglement_info` — the von Neumann entropy of qubit 0's reduced
1187    ///   density matrix together with that single-qubit entanglement spectrum.
1188    ///
1189    /// Nothing here is fabricated: every number is derived from the simulated
1190    /// amplitudes.  A component with no parameterized gates simply yields the
1191    /// state produced by its fixed gates.
1192    fn extract_quantum_info<const N: usize>(
1193        &self,
1194        problem: &HybridOptimizationProblem<N>,
1195    ) -> QuantRS2Result<QuantumStateInfo> {
1196        let mut final_states = HashMap::new();
1197        let mut measurement_stats = HashMap::new();
1198        let mut entanglement_info = HashMap::new();
1199
1200        for component in &problem.quantum_circuits {
1201            let bound = bind_parameters(component, &problem.global_parameters)?;
1202            let state = statevector::simulate(&bound)?;
1203
1204            // Per-qubit ⟨Z⟩ means and their exact Z-measurement std-devs.
1205            let mut means = Vec::with_capacity(N);
1206            let mut std_devs = Vec::with_capacity(N);
1207            for qubit in 0..N {
1208                let z_expectation = statevector::single_z_expectation(&state, qubit);
1209                means.push(z_expectation);
1210                // Var(Z) = ⟨Z²⟩ − ⟨Z⟩² = 1 − ⟨Z⟩² for a ±1-valued observable.
1211                std_devs.push((1.0 - z_expectation * z_expectation).max(0.0).sqrt());
1212            }
1213
1214            measurement_stats.insert(
1215                component.id.clone(),
1216                MeasurementStatistics {
1217                    means,
1218                    std_devs,
1219                    correlations: Vec::new(),
1220                    num_shots: 0,
1221                },
1222            );
1223
1224            // Entanglement of qubit 0 with the rest of the register.
1225            if N >= 1 {
1226                let spectrum = statevector::single_qubit_eigenvalues(&state, 0);
1227                let entropy = von_neumann_entropy(&spectrum);
1228                entanglement_info.insert(
1229                    component.id.clone(),
1230                    EntanglementInfo {
1231                        von_neumann_entropy: entropy,
1232                        mutual_information: Vec::new(),
1233                        entanglement_spectrum: spectrum,
1234                    },
1235                );
1236            }
1237
1238            final_states.insert(component.id.clone(), state);
1239        }
1240
1241        Ok(QuantumStateInfo {
1242            final_states,
1243            measurement_stats,
1244            entanglement_info,
1245        })
1246    }
1247}
1248
1249/// Collect the global-parameter indices that drive at least one parameterized
1250/// gate (RX/RY/RZ) of some quantum component, i.e. the indices that admit the
1251/// parameter-shift rule in [`HybridOptimizer::compute_gradients`].
1252///
1253/// Every other index only ever appears in classical regularization terms.
1254/// This partition is what [`HybridOptimizationAlgorithm::CoordinateDescent`]
1255/// and [`HybridOptimizationAlgorithm::AdaptiveOptimization`] alternate
1256/// between.
1257fn quantum_parameter_indices<const N: usize>(
1258    problem: &HybridOptimizationProblem<N>,
1259) -> HashSet<usize> {
1260    let mut indices = HashSet::new();
1261    for component in &problem.quantum_circuits {
1262        let num_param_gates = count_parameterized_gates(&component.circuit);
1263        for &idx in component.parameter_indices.iter().take(num_param_gates) {
1264            indices.insert(idx);
1265        }
1266    }
1267    indices
1268}
1269
1270/// Block-coordinate-descent mask: alternates between the `quantum_indices`
1271/// block (even iterations) and the complementary classical block (odd
1272/// iterations). Degenerates to "update everything" when one of the two
1273/// blocks is empty, since there is then nothing to alternate with.
1274fn coordinate_descent_mask(
1275    quantum_indices: &HashSet<usize>,
1276    iteration: usize,
1277    num_params: usize,
1278) -> Vec<bool> {
1279    let classical_count = num_params.saturating_sub(quantum_indices.len());
1280    if quantum_indices.is_empty() || classical_count == 0 {
1281        return vec![true; num_params];
1282    }
1283
1284    let update_quantum_this_round = iteration % 2 == 0;
1285    (0..num_params)
1286        .map(|i| quantum_indices.contains(&i) == update_quantum_this_round)
1287        .collect()
1288}
1289
1290/// Coarse-to-fine hierarchical mask: at the coarsest level (`iteration ==
1291/// 0`) only every `2^max_level`-th parameter updates; the level shrinks by
1292/// one every `max_iterations / (max_level + 1)` iterations until it reaches
1293/// `0` (stride `1`, i.e. every parameter updates), refining the resolution
1294/// as optimization proceeds.
1295fn hierarchical_mask(iteration: usize, max_iterations: usize, num_params: usize) -> Vec<bool> {
1296    if num_params == 0 {
1297        return Vec::new();
1298    }
1299
1300    let max_level = (num_params as f64).log2().floor() as u32;
1301    let num_phases = max_level + 1;
1302    let phase_len = ((max_iterations.max(1) as f64) / (num_phases as f64))
1303        .ceil()
1304        .max(1.0) as usize;
1305    let phase = (iteration / phase_len).min(max_level as usize) as u32;
1306    let level = max_level - phase;
1307    let stride = 1usize << level;
1308
1309    (0..num_params).map(|i| i % stride == 0).collect()
1310}
1311
1312/// Count the parameterized rotation gates (RX/RY/RZ) in a circuit, in gate order.
1313fn count_parameterized_gates<const N: usize>(circuit: &Circuit<N>) -> usize {
1314    circuit
1315        .gates()
1316        .iter()
1317        .filter(|gate| {
1318            let any = gate.as_any();
1319            any.is::<RotationX>() || any.is::<RotationY>() || any.is::<RotationZ>()
1320        })
1321        .count()
1322}
1323
1324/// Bind `parameters` into a copy of `component`'s circuit.
1325///
1326/// The `k`-th entry of `component.parameter_indices` supplies the angle for the
1327/// `k`-th parameterized rotation gate (RX/RY/RZ) encountered in gate order; all
1328/// other gates are preserved verbatim.  If a component lists more parameter
1329/// indices than it has parameterized gates the surplus indices are ignored
1330/// (they may, for example, only feed classical regularization), and vice-versa.
1331fn bind_parameters<const N: usize>(
1332    component: &ParameterizedQuantumComponent<N>,
1333    parameters: &[f64],
1334) -> QuantRS2Result<Circuit<N>> {
1335    let old_gates = component.circuit.gates_as_boxes();
1336    let mut param_slot = 0usize;
1337    let mut new_gates: Vec<Box<dyn GateOp>> = Vec::with_capacity(old_gates.len());
1338
1339    for gate in old_gates {
1340        let any = gate.as_any();
1341        // Resolve the global parameter for the current parameterized gate, if any.
1342        let resolve = |slot: usize| -> QuantRS2Result<Option<f64>> {
1343            match component.parameter_indices.get(slot) {
1344                Some(&global_index) => match parameters.get(global_index) {
1345                    Some(&value) => Ok(Some(value)),
1346                    None => Err(QuantRS2Error::InvalidInput(format!(
1347                        "Component '{}' references parameter index {} but only {} parameters exist",
1348                        component.id,
1349                        global_index,
1350                        parameters.len()
1351                    ))),
1352                },
1353                // No global parameter mapped to this gate: keep its existing angle.
1354                None => Ok(None),
1355            }
1356        };
1357
1358        if let Some(rx) = any.downcast_ref::<RotationX>() {
1359            let theta = resolve(param_slot)?.unwrap_or(rx.theta);
1360            param_slot += 1;
1361            new_gates.push(Box::new(RotationX {
1362                target: rx.target,
1363                theta,
1364            }));
1365        } else if let Some(ry) = any.downcast_ref::<RotationY>() {
1366            let theta = resolve(param_slot)?.unwrap_or(ry.theta);
1367            param_slot += 1;
1368            new_gates.push(Box::new(RotationY {
1369                target: ry.target,
1370                theta,
1371            }));
1372        } else if let Some(rz) = any.downcast_ref::<RotationZ>() {
1373            let theta = resolve(param_slot)?.unwrap_or(rz.theta);
1374            param_slot += 1;
1375            new_gates.push(Box::new(RotationZ {
1376                target: rz.target,
1377                theta,
1378            }));
1379        } else {
1380            new_gates.push(gate);
1381        }
1382    }
1383
1384    Circuit::<N>::from_gates(new_gates)
1385}
1386
1387/// Collect the parameter values referenced by a regularization term, in order.
1388fn collect_parameters(term: &RegularizationTerm, parameters: &[f64]) -> QuantRS2Result<Vec<f64>> {
1389    let mut selected = Vec::with_capacity(term.parameter_indices.len());
1390    for &idx in &term.parameter_indices {
1391        let value = parameters.get(idx).copied().ok_or_else(|| {
1392            QuantRS2Error::InvalidInput(format!(
1393                "Regularization parameter index {idx} out of range (total parameters: {})",
1394                parameters.len()
1395            ))
1396        })?;
1397        selected.push(value);
1398    }
1399    Ok(selected)
1400}
1401
1402/// Von Neumann entropy `S = −Σ_i λ_i log₂ λ_i` of a probability/eigenvalue spectrum.
1403fn von_neumann_entropy(eigenvalues: &[f64]) -> f64 {
1404    let mut entropy = 0.0;
1405    for &lambda in eigenvalues {
1406        if lambda > 1e-12 {
1407            entropy -= lambda * lambda.log2();
1408        }
1409    }
1410    entropy
1411}
1412
1413/// Dense state-vector simulation utilities used by the hybrid objective.
1414///
1415/// `quantrs2-circuit` is a dependency of `quantrs2-sim`, so it cannot depend on
1416/// the simulator crate (that would be a dependency cycle).  These helpers
1417/// therefore provide a small, self-contained exact state-vector engine driven
1418/// purely by the generic [`GateOp::matrix`] / [`GateOp::qubits`] interface, so
1419/// they correctly handle *every* gate type a component circuit can contain.
1420mod statevector {
1421    use super::{Circuit, GateOp};
1422    use quantrs2_core::error::{QuantRS2Error, QuantRS2Result};
1423    use scirs2_core::Complex64;
1424
1425    /// Simulate `circuit` on `2^N` amplitudes starting from `|0…0⟩`.
1426    pub fn simulate<const N: usize>(circuit: &Circuit<N>) -> QuantRS2Result<Vec<Complex64>> {
1427        let dim = 1usize << N;
1428        let mut state = vec![Complex64::new(0.0, 0.0); dim];
1429        state[0] = Complex64::new(1.0, 0.0);
1430
1431        for gate in circuit.gates() {
1432            apply_gate(&mut state, N, gate.as_ref())?;
1433        }
1434
1435        Ok(state)
1436    }
1437
1438    /// Apply a single (possibly multi-qubit) gate to the state vector in place.
1439    ///
1440    /// The gate's `2^k × 2^k` unitary (row-major, `k = gate.num_qubits()`) is
1441    /// applied to the subspace spanned by the gate's qubits.  Qubit `q` is the
1442    /// bit at position `q` of the basis index (little-endian), matching the
1443    /// framework's `QubitId` convention.
1444    fn apply_gate(
1445        state: &mut [Complex64],
1446        num_qubits: usize,
1447        gate: &dyn GateOp,
1448    ) -> QuantRS2Result<()> {
1449        let targets: Vec<usize> = gate.qubits().iter().map(|q| q.id() as usize).collect();
1450        let k = targets.len();
1451        if k == 0 {
1452            return Ok(());
1453        }
1454        for &t in &targets {
1455            if t >= num_qubits {
1456                return Err(QuantRS2Error::InvalidInput(format!(
1457                    "Gate '{}' acts on qubit {} but circuit only has {} qubits",
1458                    gate.name(),
1459                    t,
1460                    num_qubits
1461                )));
1462            }
1463        }
1464
1465        let matrix = gate.matrix()?;
1466        let side = 1usize << k;
1467        if matrix.len() != side * side {
1468            return Err(QuantRS2Error::InvalidInput(format!(
1469                "Gate '{}' returned a {}-element matrix but {} qubits require {}",
1470                gate.name(),
1471                matrix.len(),
1472                k,
1473                side * side
1474            )));
1475        }
1476
1477        // `bit_masks[b]` is the state-vector bit driven by bit `b` of the local
1478        // gate-block index.  The gate matrix follows the standard convention in
1479        // which the *first* qubit of `gate.qubits()` is the most-significant bit
1480        // of the block index, so we reverse the qubit order here: local bit 0
1481        // (LSB) ↔ the last qubit, local bit k−1 (MSB) ↔ the first qubit.
1482        let bit_masks: Vec<usize> = targets.iter().rev().map(|&t| 1usize << t).collect();
1483        let mut fixed_mask = 0usize;
1484        for &m in &bit_masks {
1485            fixed_mask |= m;
1486        }
1487        let dim = state.len();
1488
1489        let mut visited = vec![false; dim];
1490        let mut amplitudes = vec![Complex64::new(0.0, 0.0); side];
1491        let mut indices = vec![0usize; side];
1492
1493        for base in 0..dim {
1494            if visited[base] || (base & fixed_mask) != 0 {
1495                continue;
1496            }
1497
1498            for (local, slot) in indices.iter_mut().enumerate() {
1499                let mut idx = base;
1500                for (bit, &mask) in bit_masks.iter().enumerate() {
1501                    if (local >> bit) & 1 == 1 {
1502                        idx |= mask;
1503                    }
1504                }
1505                *slot = idx;
1506                amplitudes[local] = state[idx];
1507                visited[idx] = true;
1508            }
1509
1510            for r in 0..side {
1511                let mut acc = Complex64::new(0.0, 0.0);
1512                let row = r * side;
1513                for (c, amp) in amplitudes.iter().enumerate() {
1514                    acc += matrix[row + c] * amp;
1515                }
1516                state[indices[r]] = acc;
1517            }
1518        }
1519
1520        Ok(())
1521    }
1522
1523    /// `⟨ψ| Z_qubit |ψ⟩` for a single qubit (eigenvalue `+1` on `|0⟩`, `−1` on `|1⟩`).
1524    pub fn single_z_expectation(state: &[Complex64], qubit: usize) -> f64 {
1525        let mask = 1usize << qubit;
1526        let mut expectation = 0.0;
1527        for (idx, amp) in state.iter().enumerate() {
1528            let sign = if idx & mask == 0 { 1.0 } else { -1.0 };
1529            expectation += sign * amp.norm_sqr();
1530        }
1531        expectation
1532    }
1533
1534    /// `⟨ψ| (Σ_q Z_q) |ψ⟩` — the diagonal cost Hamiltonian energy.
1535    pub fn sum_z_expectation(state: &[Complex64], num_qubits: usize) -> f64 {
1536        (0..num_qubits)
1537            .map(|q| single_z_expectation(state, q))
1538            .sum()
1539    }
1540
1541    /// Eigenvalues of qubit `qubit`'s reduced density matrix.
1542    ///
1543    /// Tracing out every other qubit yields a `2 × 2` Hermitian density matrix
1544    /// `ρ`; its two eigenvalues quantify the entanglement of that qubit with the
1545    /// remainder of the register (both `0.5` ⇒ maximal entanglement, `{1, 0}` ⇒
1546    /// product state).
1547    pub fn single_qubit_eigenvalues(state: &[Complex64], qubit: usize) -> Vec<f64> {
1548        let mask = 1usize << qubit;
1549        // ρ = [[r00, r01], [r10, r11]] with r10 = conj(r01).
1550        let mut r00 = 0.0;
1551        let mut r11 = 0.0;
1552        let mut r01 = Complex64::new(0.0, 0.0);
1553        for (idx, amp) in state.iter().enumerate() {
1554            if idx & mask == 0 {
1555                r00 += amp.norm_sqr();
1556                let partner = idx | mask;
1557                r01 += amp.conj() * state[partner];
1558            } else {
1559                r11 += amp.norm_sqr();
1560            }
1561        }
1562
1563        // Eigenvalues of a 2×2 Hermitian matrix: (tr ± √(tr² − 4 det)) / 2.
1564        let trace = r00 + r11;
1565        let det = r00 * r11 - r01.norm_sqr();
1566        let discriminant = (trace * trace - 4.0 * det).max(0.0).sqrt();
1567        let lambda_plus = 0.5 * (trace + discriminant);
1568        let lambda_minus = 0.5 * (trace - discriminant);
1569        vec![lambda_plus.max(0.0), lambda_minus.max(0.0)]
1570    }
1571}
1572
1573impl Default for HybridOptimizer {
1574    fn default() -> Self {
1575        Self::new(HybridOptimizationAlgorithm::CoordinateDescent)
1576    }
1577}
1578
1579#[cfg(test)]
1580mod tests {
1581    use super::*;
1582
1583    #[test]
1584    fn test_hybrid_problem_creation() {
1585        let problem = HybridOptimizationProblem::<4>::new();
1586        assert_eq!(problem.quantum_circuits.len(), 0);
1587        assert_eq!(problem.classical_steps.len(), 0);
1588    }
1589
1590    #[test]
1591    fn test_component_addition() {
1592        let mut problem = HybridOptimizationProblem::<2>::new();
1593        problem.set_global_parameters(vec![0.1, 0.2, 0.3]);
1594
1595        let circuit = Circuit::<2>::new();
1596        problem
1597            .add_quantum_component("q1".to_string(), circuit, vec![0, 1])
1598            .expect("add_quantum_component should succeed");
1599
1600        assert_eq!(problem.quantum_circuits.len(), 1);
1601        assert_eq!(problem.data_flow.nodes.len(), 1);
1602    }
1603
1604    #[test]
1605    fn test_data_flow() {
1606        let mut problem = HybridOptimizationProblem::<2>::new();
1607        problem.set_global_parameters(vec![0.1, 0.2]);
1608
1609        let circuit = Circuit::<2>::new();
1610        problem
1611            .add_quantum_component("q1".to_string(), circuit, vec![0])
1612            .expect("add_quantum_component should succeed");
1613        problem
1614            .add_classical_step(
1615                "c1".to_string(),
1616                ClassicalStepType::LinearAlgebra(LinearAlgebraOp::MatrixMultiplication),
1617                vec!["q1".to_string()],
1618                vec!["output".to_string()],
1619            )
1620            .expect("add_classical_step should succeed");
1621
1622        problem
1623            .add_data_flow(
1624                "q1".to_string(),
1625                "c1".to_string(),
1626                DataType::Measurements(vec![0.1, 0.2]),
1627            )
1628            .expect("add_data_flow should succeed");
1629
1630        assert_eq!(problem.data_flow.edges.len(), 1);
1631    }
1632
1633    #[test]
1634    fn test_optimizer_creation() {
1635        let optimizer = HybridOptimizer::new(HybridOptimizationAlgorithm::SimultaneousOptimization);
1636        assert_eq!(
1637            optimizer.algorithm,
1638            HybridOptimizationAlgorithm::SimultaneousOptimization
1639        );
1640        assert_eq!(optimizer.max_iterations, 1000);
1641    }
1642
1643    /// Build a one-qubit problem whose single RY gate is driven by parameter 0,
1644    /// with an `ExpectationValue` objective (⟨Z⟩).
1645    fn single_ry_problem(theta: f64) -> HybridOptimizationProblem<1> {
1646        let mut problem = HybridOptimizationProblem::<1>::new();
1647        problem.set_global_parameters(vec![theta]);
1648        let mut circuit = Circuit::<1>::new();
1649        circuit
1650            .ry(QubitId(0), 0.0)
1651            .expect("add RY gate to test circuit");
1652        problem
1653            .add_quantum_component("q".to_string(), circuit, vec![0])
1654            .expect("add quantum component");
1655        problem
1656    }
1657
1658    /// `⟨0|RY(θ)† Z RY(θ)|0⟩ = cos θ`.  The former fabrication returned a
1659    /// constant `1.0`; this pins the real objective to the analytic value.
1660    #[test]
1661    fn test_objective_matches_analytic_cos() {
1662        use std::f64::consts::PI;
1663
1664        let optimizer = HybridOptimizer::default();
1665        for &theta in &[0.0, PI / 6.0, PI / 3.0, PI / 2.0, 2.0 * PI / 3.0, PI] {
1666            let problem = single_ry_problem(theta);
1667            let value = optimizer
1668                .evaluate_objective(&problem, &problem.global_parameters)
1669                .expect("evaluate objective");
1670            assert!(
1671                (value - theta.cos()).abs() < 1e-9,
1672                "objective for RY({theta}) was {value}, expected {}",
1673                theta.cos()
1674            );
1675        }
1676    }
1677
1678    /// The objective must depend on the parameters — a constant `1.0`
1679    /// fabrication would make every value identical.
1680    #[test]
1681    fn test_objective_is_not_constant() {
1682        use std::f64::consts::PI;
1683
1684        let optimizer = HybridOptimizer::default();
1685        let p0 = single_ry_problem(0.0);
1686        let p_pi = single_ry_problem(PI);
1687        let e0 = optimizer
1688            .evaluate_objective(&p0, &p0.global_parameters)
1689            .expect("e0");
1690        let e_pi = optimizer
1691            .evaluate_objective(&p_pi, &p_pi.global_parameters)
1692            .expect("e_pi");
1693
1694        assert!((e0 - 1.0).abs() < 1e-9, "⟨Z⟩ at θ=0 should be +1, got {e0}");
1695        assert!(
1696            (e_pi + 1.0).abs() < 1e-9,
1697            "⟨Z⟩ at θ=π should be -1, got {e_pi}"
1698        );
1699        assert!(
1700            (e0 - e_pi).abs() > 1.0,
1701            "objective must vary with parameters (e0={e0}, e_pi={e_pi})"
1702        );
1703    }
1704
1705    /// The fidelity objective rewards overlap with |0…0⟩: it is `0` for the
1706    /// untouched state and grows as the state rotates away.
1707    #[test]
1708    fn test_objective_fidelity_variant() {
1709        use std::f64::consts::PI;
1710
1711        let optimizer = HybridOptimizer::default();
1712        let mut problem = single_ry_problem(0.0);
1713        problem.objective.function_type = ObjectiveFunctionType::Fidelity;
1714
1715        // θ=0 ⇒ state is |0⟩ ⇒ fidelity 1 ⇒ objective 1 − 1 = 0.
1716        let e0 = optimizer
1717            .evaluate_objective(&problem, &[0.0])
1718            .expect("fidelity θ=0");
1719        assert!(
1720            (e0 - 0.0).abs() < 1e-9,
1721            "expected 0 fidelity-cost, got {e0}"
1722        );
1723
1724        // θ=π ⇒ state is |1⟩ ⇒ fidelity 0 ⇒ objective 1.
1725        let e_pi = optimizer
1726            .evaluate_objective(&problem, &[PI])
1727            .expect("fidelity θ=π");
1728        assert!(
1729            (e_pi - 1.0).abs() < 1e-9,
1730            "expected 1 fidelity-cost, got {e_pi}"
1731        );
1732    }
1733
1734    /// The analytic parameter-shift gradient must agree with a central finite
1735    /// difference of the *real* objective at a generic, non-symmetric point.
1736    #[test]
1737    fn test_parameter_shift_gradient_matches_finite_difference() {
1738        use std::f64::consts::PI;
1739
1740        let optimizer = HybridOptimizer::default();
1741
1742        // Two-qubit component: RY(q0), RZ(q1), CNOT, RY(q1) — three param gates.
1743        let mut problem = HybridOptimizationProblem::<2>::new();
1744        let base = vec![0.31, -0.52, 1.07 - PI / 4.0];
1745        problem.set_global_parameters(base.clone());
1746
1747        let mut circuit = Circuit::<2>::new();
1748        circuit.ry(QubitId(0), 0.0).expect("ry0");
1749        circuit.rz(QubitId(1), 0.0).expect("rz1");
1750        circuit.cnot(QubitId(0), QubitId(1)).expect("cnot");
1751        circuit.ry(QubitId(1), 0.0).expect("ry1");
1752        problem
1753            .add_quantum_component("q".to_string(), circuit, vec![0, 1, 2])
1754            .expect("add component");
1755
1756        // A non-trivial multi-term L2 regularization so the classical
1757        // derivative path is exercised alongside the quantum one.
1758        problem
1759            .add_regularization(RegularizationType::L2, 0.13, vec![0, 2])
1760            .expect("add reg");
1761
1762        let analytic = optimizer
1763            .compute_gradients(&problem, &base)
1764            .expect("analytic gradient");
1765        assert_eq!(analytic.len(), base.len());
1766
1767        let eps = 1e-6;
1768        for i in 0..base.len() {
1769            let mut plus = base.clone();
1770            plus[i] += eps;
1771            let ep = optimizer.evaluate_objective(&problem, &plus).expect("e+");
1772
1773            let mut minus = base.clone();
1774            minus[i] -= eps;
1775            let em = optimizer.evaluate_objective(&problem, &minus).expect("e-");
1776
1777            let numeric = (ep - em) / (2.0 * eps);
1778            assert!(
1779                (analytic[i] - numeric).abs() < 1e-5,
1780                "param {i}: analytic {} vs finite-difference {}",
1781                analytic[i],
1782                numeric
1783            );
1784        }
1785    }
1786
1787    /// The L2 regularization term genuinely contributes to the objective and is
1788    /// not silently ignored.
1789    #[test]
1790    fn test_regularization_contributes() {
1791        let optimizer = HybridOptimizer::default();
1792
1793        // θ = π/2 ⇒ ⟨Z⟩ = cos(π/2) = 0, so any non-zero value comes from the
1794        // regularization term alone.
1795        let mut problem = single_ry_problem(std::f64::consts::FRAC_PI_2);
1796        let without = optimizer
1797            .evaluate_objective(&problem, &problem.global_parameters.clone())
1798            .expect("without reg");
1799        assert!(
1800            without.abs() < 1e-9,
1801            "quantum part should vanish, got {without}"
1802        );
1803
1804        problem
1805            .add_regularization(RegularizationType::L2, 2.0, vec![0])
1806            .expect("add reg");
1807        let with = optimizer
1808            .evaluate_objective(&problem, &problem.global_parameters.clone())
1809            .expect("with reg");
1810        // L2 penalty = strength * θ² = 2.0 * (π/2)².
1811        let expected = 2.0 * (std::f64::consts::FRAC_PI_2).powi(2);
1812        assert!(
1813            (with - expected).abs() < 1e-9,
1814            "regularized objective {with}, expected {expected}"
1815        );
1816    }
1817
1818    /// End-to-end: minimizing ⟨Z⟩ with an RY ansatz must drive the objective
1819    /// toward the ground-state value `-1` and populate real quantum info.
1820    #[test]
1821    fn test_optimize_reaches_z_ground_state() {
1822        let mut optimizer = HybridOptimizer::default();
1823        optimizer.learning_rate_schedule.initial_rate = 0.3;
1824        optimizer.max_iterations = 500;
1825
1826        // Start away from the minimum (θ=π) and the maximum (θ=0).
1827        let mut problem = single_ry_problem(0.6);
1828
1829        let result = optimizer.optimize(&mut problem).expect("optimize");
1830        assert!(
1831            (result.optimal_value + 1.0).abs() < 1e-3,
1832            "optimized objective {} should approach -1",
1833            result.optimal_value
1834        );
1835
1836        // The extracted quantum info must be real, not empty.
1837        let stats = result
1838            .quantum_info
1839            .measurement_stats
1840            .get("q")
1841            .expect("measurement stats present");
1842        // At the minimum the state is |1⟩, so ⟨Z⟩ ≈ -1.
1843        assert!(
1844            (stats.means[0] + 1.0).abs() < 1e-2,
1845            "⟨Z⟩ at optimum should be ≈ -1, got {}",
1846            stats.means[0]
1847        );
1848        let state = result
1849            .quantum_info
1850            .final_states
1851            .get("q")
1852            .expect("final state present");
1853        assert_eq!(state.len(), 2, "1-qubit state must have 2 amplitudes");
1854    }
1855
1856    /// `extract_quantum_info` produces real entanglement: a Bell-state circuit
1857    /// has maximally mixed single-qubit marginals (entropy ≈ 1 bit), whereas a
1858    /// product state has zero entanglement entropy.
1859    #[test]
1860    fn test_extract_quantum_info_entanglement() {
1861        let optimizer = HybridOptimizer::default();
1862
1863        // Bell state: H(q0) then CNOT(q0,q1).
1864        let mut problem = HybridOptimizationProblem::<2>::new();
1865        let mut circuit = Circuit::<2>::new();
1866        circuit.h(QubitId(0)).expect("h");
1867        circuit.cnot(QubitId(0), QubitId(1)).expect("cnot");
1868        problem
1869            .add_quantum_component("bell".to_string(), circuit, Vec::new())
1870            .expect("add component");
1871
1872        let info = optimizer
1873            .extract_quantum_info(&problem)
1874            .expect("extract info");
1875        let ent = info
1876            .entanglement_info
1877            .get("bell")
1878            .expect("entanglement info present");
1879        assert!(
1880            (ent.von_neumann_entropy - 1.0).abs() < 1e-9,
1881            "Bell state entropy should be 1 bit, got {}",
1882            ent.von_neumann_entropy
1883        );
1884
1885        // Product state |00⟩ (empty circuit) has zero entanglement.
1886        let mut product = HybridOptimizationProblem::<2>::new();
1887        product
1888            .add_quantum_component("prod".to_string(), Circuit::<2>::new(), Vec::new())
1889            .expect("add product component");
1890        let product_info = optimizer
1891            .extract_quantum_info(&product)
1892            .expect("extract product info");
1893        let prod_ent = product_info
1894            .entanglement_info
1895            .get("prod")
1896            .expect("product entanglement info");
1897        assert!(
1898            prod_ent.von_neumann_entropy < 1e-9,
1899            "product state entropy should be 0, got {}",
1900            prod_ent.von_neumann_entropy
1901        );
1902    }
1903
1904    /// `has_circular_dependencies` must catch a cycle that spans more than one
1905    /// edge (`A -> B -> C -> A`), not just a direct self-loop.
1906    #[test]
1907    fn test_multi_node_cycle_is_detected() {
1908        let mut problem = HybridOptimizationProblem::<1>::new();
1909        problem.data_flow.nodes = vec!["A".to_string(), "B".to_string(), "C".to_string()];
1910        problem.data_flow.edges = vec![
1911            (
1912                "A".to_string(),
1913                "B".to_string(),
1914                DataType::Probabilities(vec![]),
1915            ),
1916            (
1917                "B".to_string(),
1918                "C".to_string(),
1919                DataType::Probabilities(vec![]),
1920            ),
1921            (
1922                "C".to_string(),
1923                "A".to_string(),
1924                DataType::Probabilities(vec![]),
1925            ),
1926        ];
1927
1928        let result = problem.validate();
1929        assert!(
1930            result.is_err(),
1931            "A->B->C->A must be flagged as a circular dependency"
1932        );
1933    }
1934
1935    /// A genuine DAG (no cycle at all, not even a self-loop) must validate.
1936    #[test]
1937    fn test_acyclic_data_flow_validates() {
1938        let mut problem = HybridOptimizationProblem::<1>::new();
1939        problem.data_flow.nodes = vec!["A".to_string(), "B".to_string(), "C".to_string()];
1940        problem.data_flow.edges = vec![
1941            (
1942                "A".to_string(),
1943                "B".to_string(),
1944                DataType::Probabilities(vec![]),
1945            ),
1946            (
1947                "A".to_string(),
1948                "C".to_string(),
1949                DataType::Probabilities(vec![]),
1950            ),
1951            (
1952                "B".to_string(),
1953                "C".to_string(),
1954                DataType::Probabilities(vec![]),
1955            ),
1956        ];
1957
1958        assert!(
1959            problem.validate().is_ok(),
1960            "acyclic data flow must not be rejected as circular"
1961        );
1962    }
1963
1964    /// `ScheduleType::StepDecay` must be piecewise-constant, dropping by
1965    /// `decay_factor` every `step_size` iterations, not silently aliasing the
1966    /// constant initial rate.
1967    #[test]
1968    fn test_step_decay_learning_rate() {
1969        let mut optimizer =
1970            HybridOptimizer::new(HybridOptimizationAlgorithm::SimultaneousOptimization);
1971        optimizer.learning_rate_schedule.schedule_type = ScheduleType::StepDecay;
1972        optimizer.learning_rate_schedule.initial_rate = 0.1;
1973        optimizer
1974            .learning_rate_schedule
1975            .parameters
1976            .insert("step_size".to_string(), 10.0);
1977        optimizer
1978            .learning_rate_schedule
1979            .parameters
1980            .insert("decay_factor".to_string(), 0.5);
1981
1982        let empty_history: Vec<f64> = Vec::new();
1983        assert!((optimizer.get_learning_rate(0, &empty_history) - 0.1).abs() < 1e-12);
1984        assert!((optimizer.get_learning_rate(9, &empty_history) - 0.1).abs() < 1e-12);
1985        assert!((optimizer.get_learning_rate(10, &empty_history) - 0.05).abs() < 1e-12);
1986        assert!((optimizer.get_learning_rate(20, &empty_history) - 0.025).abs() < 1e-12);
1987    }
1988
1989    /// `ScheduleType::CosineAnnealing` must trace a cosine curve from
1990    /// `initial_rate` at iteration 0 down to `min_rate` at the final
1991    /// iteration, not silently alias the constant initial rate.
1992    #[test]
1993    fn test_cosine_annealing_learning_rate() {
1994        let mut optimizer =
1995            HybridOptimizer::new(HybridOptimizationAlgorithm::SimultaneousOptimization);
1996        optimizer.learning_rate_schedule.schedule_type = ScheduleType::CosineAnnealing;
1997        optimizer.learning_rate_schedule.initial_rate = 1.0;
1998        optimizer.max_iterations = 101; // iterations 0..=100
1999        optimizer
2000            .learning_rate_schedule
2001            .parameters
2002            .insert("min_rate".to_string(), 0.0);
2003
2004        let empty_history: Vec<f64> = Vec::new();
2005        let start = optimizer.get_learning_rate(0, &empty_history);
2006        let mid = optimizer.get_learning_rate(50, &empty_history);
2007        let end = optimizer.get_learning_rate(100, &empty_history);
2008
2009        assert!(
2010            (start - 1.0).abs() < 1e-9,
2011            "rate at iter 0 should be ~1.0, got {start}"
2012        );
2013        assert!(
2014            mid < start && mid > end,
2015            "rate should monotonically decay across the run"
2016        );
2017        assert!(
2018            end.abs() < 1e-9,
2019            "rate at final iter should be ~0.0, got {end}"
2020        );
2021    }
2022
2023    /// `ScheduleType::Adaptive` must scale the rate by the gradient-norm
2024    /// trend: a shrinking gradient grows the rate, a growing gradient shrinks
2025    /// it, rather than silently aliasing the constant initial rate.
2026    #[test]
2027    fn test_adaptive_learning_rate_tracks_gradient_trend() {
2028        let mut optimizer =
2029            HybridOptimizer::new(HybridOptimizationAlgorithm::SimultaneousOptimization);
2030        optimizer.learning_rate_schedule.schedule_type = ScheduleType::Adaptive;
2031        optimizer.learning_rate_schedule.initial_rate = 0.1;
2032
2033        // No history yet: falls back to the initial rate.
2034        let none: Vec<f64> = Vec::new();
2035        assert!((optimizer.get_learning_rate(0, &none) - 0.1).abs() < 1e-12);
2036
2037        // Shrinking gradient norm (converging) => rate should grow.
2038        let shrinking = vec![1.0, 0.5];
2039        let grown = optimizer.get_learning_rate(1, &shrinking);
2040        assert!(
2041            grown > 0.1,
2042            "shrinking gradient norm should grow the rate, got {grown}"
2043        );
2044
2045        // Growing gradient norm (diverging) => rate should shrink.
2046        let growing = vec![0.5, 1.0];
2047        let shrunk = optimizer.get_learning_rate(1, &growing);
2048        assert!(
2049            shrunk < 0.1,
2050            "growing gradient norm should shrink the rate, got {shrunk}"
2051        );
2052    }
2053
2054    /// `quantum_parameter_indices` must contain exactly the global indices
2055    /// referenced by a component's parameterized gates, not indices that only
2056    /// feed classical regularization.
2057    #[test]
2058    fn test_quantum_parameter_indices_partition() {
2059        // 2 global parameters: index 0 drives the RY gate, index 1 is
2060        // regularization-only.
2061        let mut problem = single_ry_problem(0.3);
2062        problem.set_global_parameters(vec![0.3, 99.0]);
2063        problem
2064            .add_regularization(RegularizationType::L2, 1.0, vec![1])
2065            .expect("add reg");
2066
2067        let indices = quantum_parameter_indices(&problem);
2068        assert!(indices.contains(&0), "index 0 drives the RY gate");
2069        assert!(
2070            !indices.contains(&1),
2071            "index 1 only feeds regularization, must not be 'quantum'"
2072        );
2073    }
2074
2075    /// `CoordinateDescent` must alternate: quantum-only indices update on
2076    /// even iterations, classical-only indices update on odd iterations.
2077    #[test]
2078    fn test_coordinate_descent_mask_alternates() {
2079        let mut quantum = HashSet::new();
2080        quantum.insert(0);
2081        // num_params = 2: index 0 quantum, index 1 classical.
2082        let even = coordinate_descent_mask(&quantum, 0, 2);
2083        let odd = coordinate_descent_mask(&quantum, 1, 2);
2084        assert_eq!(
2085            even,
2086            vec![true, false],
2087            "even iteration updates the quantum block"
2088        );
2089        assert_eq!(
2090            odd,
2091            vec![false, true],
2092            "odd iteration updates the classical block"
2093        );
2094    }
2095
2096    /// With no classical parameters at all, `CoordinateDescent` must degrade
2097    /// to updating every parameter every iteration rather than stalling.
2098    #[test]
2099    fn test_coordinate_descent_mask_degenerates_without_classical_block() {
2100        let mut quantum = HashSet::new();
2101        quantum.insert(0);
2102        quantum.insert(1);
2103        let mask = coordinate_descent_mask(&quantum, 1, 2);
2104        assert_eq!(mask, vec![true, true]);
2105    }
2106
2107    /// `HierarchicalOptimization` must start coarse (few active parameters)
2108    /// and refine to "every parameter active" by the final iteration.
2109    #[test]
2110    fn test_hierarchical_mask_coarse_to_fine() {
2111        let num_params = 8;
2112        let max_iterations = 80;
2113
2114        let coarse = hierarchical_mask(0, max_iterations, num_params);
2115        let coarse_active = coarse.iter().filter(|&&b| b).count();
2116        assert!(
2117            coarse_active < num_params,
2118            "iteration 0 should not yet update every parameter, got {coarse_active}/{num_params}"
2119        );
2120        assert!(coarse[0], "index 0 is always active at every level");
2121
2122        let fine = hierarchical_mask(max_iterations - 1, max_iterations, num_params);
2123        assert!(
2124            fine.iter().all(|&b| b),
2125            "the final iteration must update every parameter"
2126        );
2127    }
2128
2129    /// `HybridOptimizationAlgorithm::Custom` must be an honest error, not a
2130    /// silent alias for `SimultaneousOptimization`.
2131    #[test]
2132    fn test_custom_algorithm_is_honest_error() {
2133        let optimizer =
2134            HybridOptimizer::new(HybridOptimizationAlgorithm::Custom("my-algo".to_string()));
2135        let mut problem = single_ry_problem(0.3);
2136        let result = optimizer.optimize(&mut problem);
2137        assert!(
2138            matches!(result, Err(QuantRS2Error::UnsupportedOperation(_))),
2139            "Custom algorithm must error honestly, got {result:?}"
2140        );
2141    }
2142
2143    /// End-to-end: `CoordinateDescent` on a problem with both a quantum and a
2144    /// classical parameter must still converge (alternating updates instead
2145    /// of a single full-batch step per iteration).
2146    #[test]
2147    fn test_coordinate_descent_end_to_end_converges() {
2148        let mut optimizer = HybridOptimizer::new(HybridOptimizationAlgorithm::CoordinateDescent);
2149        optimizer.learning_rate_schedule.initial_rate = 0.3;
2150        optimizer.max_iterations = 2000;
2151
2152        // Index 0 drives the RY gate (quantum); index 1 only feeds an L2
2153        // regularization term (classical), so the two blocks alternate.
2154        let mut problem = single_ry_problem(0.6);
2155        problem.set_global_parameters(vec![0.6, 5.0]);
2156        problem
2157            .add_regularization(RegularizationType::L2, 0.5, vec![1])
2158            .expect("add reg");
2159
2160        let result = optimizer.optimize(&mut problem).expect("optimize");
2161        assert!(
2162            (result.optimal_value + 1.0).abs() < 1e-2,
2163            "quantum part of the objective {} should approach -1",
2164            result.optimal_value
2165        );
2166        assert!(
2167            result.optimal_parameters[1].abs() < 1e-1,
2168            "classical parameter should be driven toward 0 by L2 regularization, got {}",
2169            result.optimal_parameters[1]
2170        );
2171    }
2172
2173    /// Parallel evaluation (quantum/classical parallelism > 1) must produce
2174    /// the same objective and gradients as the sequential path -- the
2175    /// parallelism setting changes *how* the work is scheduled, not the
2176    /// answer.
2177    #[test]
2178    fn test_parallelization_matches_sequential_result() {
2179        let mut problem = single_ry_problem(0.4);
2180        problem.set_global_parameters(vec![0.4, 2.0]);
2181        problem
2182            .add_regularization(RegularizationType::L2, 1.0, vec![1])
2183            .expect("add reg");
2184
2185        let sequential =
2186            HybridOptimizer::new(HybridOptimizationAlgorithm::SimultaneousOptimization);
2187        let mut parallel =
2188            HybridOptimizer::new(HybridOptimizationAlgorithm::SimultaneousOptimization);
2189        parallel.parallelization.quantum_parallelism = 8;
2190        parallel.parallelization.classical_parallelism = 8;
2191
2192        let seq_value = sequential
2193            .evaluate_objective(&problem, &problem.global_parameters.clone())
2194            .expect("sequential objective");
2195        let par_value = parallel
2196            .evaluate_objective(&problem, &problem.global_parameters.clone())
2197            .expect("parallel objective");
2198        assert!((seq_value - par_value).abs() < 1e-12);
2199
2200        let seq_grad = sequential
2201            .compute_gradients(&problem, &problem.global_parameters.clone())
2202            .expect("sequential gradients");
2203        let par_grad = parallel
2204            .compute_gradients(&problem, &problem.global_parameters.clone())
2205            .expect("parallel gradients");
2206        assert_eq!(seq_grad.len(), par_grad.len());
2207        for (s, p) in seq_grad.iter().zip(par_grad.iter()) {
2208            assert!((s - p).abs() < 1e-9, "sequential {s} vs parallel {p}");
2209        }
2210    }
2211}