1use 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#[derive(Debug, Clone)]
22pub struct HybridOptimizationProblem<const N: usize> {
23 pub quantum_circuits: Vec<ParameterizedQuantumComponent<N>>,
25 pub classical_steps: Vec<ClassicalProcessingStep>,
27 pub data_flow: DataFlowGraph,
29 pub global_parameters: Vec<f64>,
31 pub objective: ObjectiveFunction,
33}
34
35#[derive(Debug, Clone)]
37pub struct ParameterizedQuantumComponent<const N: usize> {
38 pub circuit: Circuit<N>,
40 pub parameter_indices: Vec<usize>,
42 pub classical_inputs: Vec<String>,
44 pub quantum_outputs: Vec<String>,
46 pub id: String,
48}
49
50#[derive(Debug, Clone)]
52pub struct ClassicalProcessingStep {
53 pub id: String,
55 pub step_type: ClassicalStepType,
57 pub inputs: Vec<String>,
59 pub outputs: Vec<String>,
61 pub parameters: HashMap<String, f64>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum ClassicalStepType {
68 LinearAlgebra(LinearAlgebraOp),
70 MachineLearning(MLModelType),
72 Optimization(OptimizationMethod),
74 DataProcessing(DataProcessingOp),
76 ControlFlow(ControlFlowType),
78 ParameterUpdate(UpdateRule),
80 Custom(String),
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum LinearAlgebraOp {
87 MatrixMultiplication,
88 Eigendecomposition,
89 SVD,
90 LeastSquares,
91 LinearSolve,
92 TensorContraction,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
97pub enum MLModelType {
98 NeuralNetwork,
99 SupportVectorMachine,
100 RandomForest,
101 GaussianProcess,
102 LinearRegression,
103 LogisticRegression,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
108pub enum OptimizationMethod {
109 GradientDescent,
110 BFGS,
111 NelderMead,
112 SimulatedAnnealing,
113 GeneticAlgorithm,
114 BayesianOptimization,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
119pub enum DataProcessingOp {
120 Normalization,
121 Standardization,
122 PCA,
123 FeatureSelection,
124 DataAugmentation,
125 OutlierRemoval,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
130pub enum ControlFlowType {
131 Conditional,
132 Loop,
133 Parallel,
134 Adaptive,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
139pub enum UpdateRule {
140 GradientBased,
141 MomentumBased,
142 AdamOptimizer,
143 AdaGrad,
144 RMSProp,
145 Custom(String),
146}
147
148#[derive(Debug, Clone)]
150pub struct DataFlowGraph {
151 pub nodes: Vec<String>,
153 pub edges: Vec<(String, String, DataType)>,
155 pub execution_order: Vec<Vec<String>>,
157}
158
159#[derive(Debug, Clone, PartialEq)]
161pub enum DataType {
162 Measurements(Vec<f64>),
164 Probabilities(Vec<f64>),
166 Matrix(Vec<Vec<f64>>),
168 Scalar(f64),
170 Parameters(Vec<f64>),
172 Control(bool),
174 Custom(String),
176}
177
178#[derive(Debug, Clone)]
180pub struct ObjectiveFunction {
181 pub function_type: ObjectiveFunctionType,
183 pub target: Option<f64>,
185 pub weights: Vec<f64>,
187 pub regularization: Vec<RegularizationTerm>,
189}
190
191#[derive(Debug, Clone, PartialEq)]
193pub enum ObjectiveFunctionType {
194 ExpectationValue,
196 Fidelity,
198 CostFunction,
200 MultiObjective(Vec<Self>),
202 Custom(String),
204}
205
206#[derive(Debug, Clone)]
208pub struct RegularizationTerm {
209 pub reg_type: RegularizationType,
211 pub strength: f64,
213 pub parameter_indices: Vec<usize>,
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
219pub enum RegularizationType {
220 L1,
221 L2,
222 ElasticNet,
223 TotalVariation,
224 Sparsity,
225 Smoothness,
226}
227
228#[derive(Debug, Clone)]
230pub struct HybridOptimizationResult {
231 pub optimal_parameters: Vec<f64>,
233 pub optimal_value: f64,
235 pub iterations: usize,
237 pub converged: bool,
239 pub history: OptimizationHistory,
241 pub quantum_info: QuantumStateInfo,
243}
244
245#[derive(Debug, Clone)]
247pub struct OptimizationHistory {
248 pub objective_values: Vec<f64>,
250 pub parameter_history: Vec<Vec<f64>>,
252 pub gradient_norms: Vec<f64>,
254 pub step_sizes: Vec<f64>,
256 pub execution_times: Vec<f64>,
258}
259
260#[derive(Debug, Clone)]
262pub struct QuantumStateInfo {
263 pub final_states: HashMap<String, Vec<Complex64>>,
265 pub measurement_stats: HashMap<String, MeasurementStatistics>,
267 pub entanglement_info: HashMap<String, EntanglementInfo>,
269}
270
271#[derive(Debug, Clone)]
273pub struct MeasurementStatistics {
274 pub means: Vec<f64>,
276 pub std_devs: Vec<f64>,
278 pub correlations: Vec<Vec<f64>>,
280 pub num_shots: usize,
282}
283
284#[derive(Debug, Clone)]
286pub struct EntanglementInfo {
287 pub von_neumann_entropy: f64,
289 pub mutual_information: Vec<Vec<f64>>,
291 pub entanglement_spectrum: Vec<f64>,
293}
294
295pub struct HybridOptimizer {
297 pub algorithm: HybridOptimizationAlgorithm,
299 pub max_iterations: usize,
301 pub tolerance: f64,
303 pub learning_rate_schedule: LearningRateSchedule,
305 pub parallelization: ParallelizationConfig,
307}
308
309#[derive(Debug, Clone, PartialEq, Eq)]
311pub enum HybridOptimizationAlgorithm {
312 CoordinateDescent,
314 SimultaneousOptimization,
316 HierarchicalOptimization,
318 AdaptiveOptimization,
320 Custom(String),
322}
323
324#[derive(Debug, Clone)]
326pub struct LearningRateSchedule {
327 pub initial_rate: f64,
329 pub schedule_type: ScheduleType,
331 pub parameters: HashMap<String, f64>,
333}
334
335#[derive(Debug, Clone, PartialEq, Eq)]
337pub enum ScheduleType {
338 Constant,
339 LinearDecay,
340 ExponentialDecay,
341 StepDecay,
342 CosineAnnealing,
343 Adaptive,
344}
345
346#[derive(Debug, Clone)]
348pub struct ParallelizationConfig {
349 pub quantum_parallelism: usize,
351 pub classical_parallelism: usize,
353 pub asynchronous: bool,
355 pub load_balancing: LoadBalancingStrategy,
357}
358
359#[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 #[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 pub fn add_quantum_component(
392 &mut self,
393 id: String,
394 circuit: Circuit<N>,
395 parameter_indices: Vec<usize>,
396 ) -> QuantRS2Result<()> {
397 for &idx in ¶meter_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 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 pub fn add_data_flow(
444 &mut self,
445 source: String,
446 target: String,
447 data_type: DataType,
448 ) -> QuantRS2Result<()> {
449 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 pub fn set_global_parameters(&mut self, parameters: Vec<f64>) {
467 self.global_parameters = parameters;
468 }
469
470 pub fn add_regularization(
472 &mut self,
473 reg_type: RegularizationType,
474 strength: f64,
475 parameter_indices: Vec<usize>,
476 ) -> QuantRS2Result<()> {
477 for &idx in ¶meter_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 pub fn validate(&self) -> QuantRS2Result<()> {
497 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 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 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 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 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 stack.push((node, cursor + 1));
573 match colour.get(next).copied() {
574 Some(Colour::Gray) => return Ok(true), 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 #[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 pub fn optimize<const N: usize>(
629 &self,
630 problem: &mut HybridOptimizationProblem<N>,
631 ) -> QuantRS2Result<HybridOptimizationResult> {
632 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 let quantum_indices = quantum_parameter_indices(problem);
649
650 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 for iteration in 0..self.max_iterations {
666 let start_time = std::time::Instant::now();
667
668 let current_value = self.evaluate_objective(problem, ¤t_parameters)?;
670
671 if current_value < best_value {
672 best_value = current_value;
673 best_parameters.clone_from(¤t_parameters);
674 }
675
676 history.objective_values.push(current_value);
678 history.parameter_history.push(current_parameters.clone());
679
680 let gradients = self.compute_gradients(problem, ¤t_parameters)?;
683 let gradient_norm = gradients.iter().map(|g| g * g).sum::<f64>().sqrt();
684 history.gradient_norms.push(gradient_norm);
685
686 if gradient_norm < self.tolerance {
688 let execution_time = start_time.elapsed().as_secs_f64();
689 history.execution_times.push(execution_time);
690
691 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 let active = self.active_parameter_mask(
708 &quantum_indices,
709 iteration,
710 num_params,
711 &history.objective_values,
712 );
713
714 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 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 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 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 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 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 Ok(statevector::sum_z_expectation(state, num_qubits))
854 }
855 ObjectiveFunctionType::Fidelity => {
856 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
1249fn 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
1270fn 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
1290fn 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
1312fn 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
1324fn 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 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 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
1387fn 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
1402fn 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
1413mod statevector {
1421 use super::{Circuit, GateOp};
1422 use quantrs2_core::error::{QuantRS2Error, QuantRS2Result};
1423 use scirs2_core::Complex64;
1424
1425 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 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 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 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 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 pub fn single_qubit_eigenvalues(state: &[Complex64], qubit: usize) -> Vec<f64> {
1548 let mask = 1usize << qubit;
1549 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 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 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 #[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 #[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 #[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 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 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 #[test]
1737 fn test_parameter_shift_gradient_matches_finite_difference() {
1738 use std::f64::consts::PI;
1739
1740 let optimizer = HybridOptimizer::default();
1741
1742 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 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 #[test]
1790 fn test_regularization_contributes() {
1791 let optimizer = HybridOptimizer::default();
1792
1793 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 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 #[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 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 let stats = result
1838 .quantum_info
1839 .measurement_stats
1840 .get("q")
1841 .expect("measurement stats present");
1842 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 #[test]
1860 fn test_extract_quantum_info_entanglement() {
1861 let optimizer = HybridOptimizer::default();
1862
1863 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 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 #[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 #[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 #[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 #[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; 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 #[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 let none: Vec<f64> = Vec::new();
2035 assert!((optimizer.get_learning_rate(0, &none) - 0.1).abs() < 1e-12);
2036
2037 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 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 #[test]
2058 fn test_quantum_parameter_indices_partition() {
2059 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 #[test]
2078 fn test_coordinate_descent_mask_alternates() {
2079 let mut quantum = HashSet::new();
2080 quantum.insert(0);
2081 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 #[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 #[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 #[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 #[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 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 #[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}