1use scirs2_core::ndarray::{s, Array1, Array2, Array3, ArrayView1, ArrayView2, Axis};
11use scirs2_core::numeric::{Float, FromPrimitive, One, Zero};
12use std::collections::HashMap;
13use std::f64::consts::PI;
14use std::fmt::Debug;
15
16use serde::{Deserialize, Serialize};
17
18use crate::error::{ClusteringError, Result};
19use crate::vq::euclidean_distance;
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct QAOAConfig {
24 pub p_layers: usize,
26 pub max_iterations: usize,
28 pub tolerance: f64,
30 pub learning_rate: f64,
32 pub n_shots: usize,
34 pub cost_function: QAOACostFunction,
36 pub regularization: f64,
38 pub random_seed: Option<u64>,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44pub enum QAOACostFunction {
45 KMeans,
47 Modularity,
49 MaxCut,
51 Weighted,
53}
54
55impl Default for QAOAConfig {
56 fn default() -> Self {
57 Self {
58 p_layers: 3,
59 max_iterations: 100,
60 tolerance: 1e-6,
61 learning_rate: 0.1,
62 n_shots: 1000,
63 cost_function: QAOACostFunction::KMeans,
64 regularization: 0.01,
65 random_seed: None,
66 }
67 }
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct VQEConfig {
73 pub ansatz: VQEAnsatz,
75 pub max_iterations: usize,
77 pub tolerance: f64,
79 pub learning_rate: f64,
81 pub n_shots: usize,
83 pub circuit_depth: usize,
85 pub optimizer: VQEOptimizer,
87 pub random_seed: Option<u64>,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
93pub enum VQEAnsatz {
94 HardwareEfficient,
96 ClusteringSpecific,
98 UCC,
100 Adaptive,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
106pub enum VQEOptimizer {
107 GradientDescent,
109 Adam,
111 COBYLA,
113 SPSA,
115}
116
117impl Default for VQEConfig {
118 fn default() -> Self {
119 Self {
120 ansatz: VQEAnsatz::HardwareEfficient,
121 max_iterations: 200,
122 tolerance: 1e-6,
123 learning_rate: 0.01,
124 n_shots: 1000,
125 circuit_depth: 4,
126 optimizer: VQEOptimizer::Adam,
127 random_seed: None,
128 }
129 }
130}
131
132pub struct QAOAClustering<F: Float> {
137 config: QAOAConfig,
138 n_clusters: usize,
139 n_qubits: usize,
140 gamma_params: Array1<f64>,
141 beta_params: Array1<f64>,
142 quantum_state: Array1<f64>, cost_hamiltonian: Array2<f64>,
144 _phantom: std::marker::PhantomData<F>,
145 mixer_hamiltonian: Array2<f64>,
146 fitted: bool,
147 final_assignments: Option<Array1<usize>>,
148 final_energy: Option<f64>,
149}
150
151impl<F: Float + FromPrimitive + Debug> QAOAClustering<F> {
152 pub fn new(nclusters: usize, config: QAOAConfig) -> Self {
154 Self {
155 config,
156 n_clusters: nclusters,
157 n_qubits: 0,
158 gamma_params: Array1::zeros(0),
159 beta_params: Array1::zeros(0),
160 quantum_state: Array1::zeros(0),
161 cost_hamiltonian: Array2::zeros((0, 0)),
162 mixer_hamiltonian: Array2::zeros((0, 0)),
163 fitted: false,
164 final_assignments: None,
165 final_energy: None,
166 _phantom: std::marker::PhantomData,
167 }
168 }
169
170 pub fn fit(&mut self, data: ArrayView2<F>) -> Result<()> {
172 let (n_samples, n_features) = data.dim();
173
174 if n_samples == 0 || n_features == 0 {
175 return Err(ClusteringError::InvalidInput(
176 "Data cannot be empty".to_string(),
177 ));
178 }
179
180 self.n_qubits = n_samples * self.n_clusters;
182 self.setup_hamiltonian(data)?;
183
184 self.initialize_parameters();
186
187 let n_states = 1 << self.n_qubits;
189 self.quantum_state = Array1::from_elem(n_states, 1.0 / (n_states as f64).sqrt());
190
191 self.optimize_parameters(data)?;
193
194 self.extract_assignments()?;
196
197 self.fitted = true;
198 Ok(())
199 }
200
201 fn setup_hamiltonian(&mut self, data: ArrayView2<F>) -> Result<()> {
203 let _n_samples = data.nrows();
204 let n_vars = self.n_qubits;
205
206 self.cost_hamiltonian = Array2::zeros((n_vars, n_vars));
208 self.mixer_hamiltonian = Array2::eye(n_vars);
209
210 match self.config.cost_function {
211 QAOACostFunction::KMeans => self.setup_kmeans_hamiltonian(data)?,
212 QAOACostFunction::Modularity => self.setup_modularity_hamiltonian(data)?,
213 QAOACostFunction::MaxCut => self.setup_maxcut_hamiltonian(data)?,
214 QAOACostFunction::Weighted => self.setup_weighted_hamiltonian(data)?,
215 }
216
217 Ok(())
218 }
219
220 fn setup_kmeans_hamiltonian(&mut self, data: ArrayView2<F>) -> Result<()> {
222 let n_samples = data.nrows();
223
224 for i in 0..n_samples {
229 for j in 0..n_samples {
230 if i != j {
231 let distance = euclidean_distance(data.row(i), data.row(j))
232 .to_f64()
233 .unwrap();
234
235 for k in 0..self.n_clusters {
237 let idx_i = i * self.n_clusters + k;
238 let idx_j = j * self.n_clusters + k;
239
240 self.cost_hamiltonian[[idx_i, idx_j]] -= distance;
242 }
243 }
244 }
245
246 for k1 in 0..self.n_clusters {
248 for k2 in 0..self.n_clusters {
249 if k1 != k2 {
250 let idx1 = i * self.n_clusters + k1;
251 let idx2 = i * self.n_clusters + k2;
252
253 self.cost_hamiltonian[[idx1, idx2]] += 10.0; }
256 }
257 }
258 }
259
260 Ok(())
261 }
262
263 fn setup_modularity_hamiltonian(&mut self, data: ArrayView2<F>) -> Result<()> {
265 let n_samples = data.nrows();
266
267 let mut adjacency = Array2::zeros((n_samples, n_samples));
269 let mut total_edges = 0.0;
270
271 for i in 0..n_samples {
272 for j in i + 1..n_samples {
273 let distance = euclidean_distance(data.row(i), data.row(j))
274 .to_f64()
275 .unwrap();
276 let similarity = (-distance).exp(); adjacency[[i, j]] = similarity;
279 adjacency[[j, i]] = similarity;
280 total_edges += 2.0 * similarity;
281 }
282 }
283
284 let degrees: Array1<f64> = adjacency.sum_axis(Axis(1));
286
287 for i in 0..n_samples {
288 for j in 0..n_samples {
289 let modularity_term = adjacency[[i, j]] - (degrees[i] * degrees[j]) / total_edges;
290
291 for k in 0..self.n_clusters {
293 let idx_i = i * self.n_clusters + k;
294 let idx_j = j * self.n_clusters + k;
295
296 self.cost_hamiltonian[[idx_i, idx_j]] += modularity_term;
298 }
299 }
300 }
301
302 Ok(())
303 }
304
305 fn setup_maxcut_hamiltonian(&mut self, data: ArrayView2<F>) -> Result<()> {
307 let n_samples = data.nrows();
308
309 for i in 0..n_samples {
311 for j in i + 1..n_samples {
312 let distance = euclidean_distance(data.row(i), data.row(j))
313 .to_f64()
314 .unwrap();
315 let weight = (-distance / 2.0).exp(); for k1 in 0..self.n_clusters {
319 for k2 in 0..self.n_clusters {
320 if k1 != k2 {
321 let idx_i = i * self.n_clusters + k1;
322 let idx_j = j * self.n_clusters + k2;
323
324 self.cost_hamiltonian[[idx_i, idx_j]] += weight;
325 }
326 }
327 }
328 }
329 }
330
331 Ok(())
332 }
333
334 fn setup_weighted_hamiltonian(&mut self, data: ArrayView2<F>) -> Result<()> {
336 let w1 = 0.7; let w2 = 0.3; self.setup_kmeans_hamiltonian(data)?;
342 let kmeans_hamiltonian = self.cost_hamiltonian.clone();
343
344 self.cost_hamiltonian.fill(0.0);
345 self.setup_modularity_hamiltonian(data)?;
346 let modularity_hamiltonian = self.cost_hamiltonian.clone();
347
348 self.cost_hamiltonian = &kmeans_hamiltonian * w1 + &modularity_hamiltonian * w2;
350
351 Ok(())
352 }
353
354 fn initialize_parameters(&mut self) {
356 self.gamma_params = Array1::zeros(self.config.p_layers);
357 self.beta_params = Array1::zeros(self.config.p_layers);
358
359 use scirs2_core::random::Rng;
361 let mut rng = scirs2_core::random::rng();
362
363 for i in 0..self.config.p_layers {
364 self.gamma_params[i] = rng.random_range(0.0..PI);
365 self.beta_params[i] = rng.random_range(0.0..PI / 2.0);
366 }
367 }
368
369 fn optimize_parameters(&mut self, data: ArrayView2<F>) -> Result<()> {
371 let mut best_energy = f64::INFINITY;
372 let mut best_gamma = self.gamma_params.clone();
373 let mut best_beta = self.beta_params.clone();
374
375 for iteration in 0..self.config.max_iterations {
376 self.apply_qaoa_circuit()?;
378
379 let energy = self.measure_expectation_value()?;
381
382 if energy < best_energy {
383 best_energy = energy;
384 best_gamma = self.gamma_params.clone();
385 best_beta = self.beta_params.clone();
386 }
387
388 self.update_parameters()?;
390
391 if iteration > 10 && (best_energy - energy).abs() < self.config.tolerance {
393 break;
394 }
395 }
396
397 self.gamma_params = best_gamma;
399 self.beta_params = best_beta;
400 self.final_energy = Some(best_energy);
401
402 self.apply_qaoa_circuit()?;
404
405 Ok(())
406 }
407
408 fn apply_qaoa_circuit(&mut self) -> Result<()> {
410 let n_states = self.quantum_state.len();
412 self.quantum_state.fill(1.0 / (n_states as f64).sqrt());
413
414 for layer in 0..self.config.p_layers {
415 self.apply_cost_unitary(self.gamma_params[layer])?;
417
418 self.apply_mixer_unitary(self.beta_params[layer])?;
420 }
421
422 Ok(())
423 }
424
425 fn apply_cost_unitary(&mut self, gamma: f64) -> Result<()> {
427 let n_states = self.quantum_state.len();
431 let mut new_state = Array1::zeros(n_states);
432
433 for i in 0..n_states {
434 let mut phase_shift = 0.0;
435
436 for j in 0..self.n_qubits {
439 if (i >> j) & 1 == 1 {
440 phase_shift += gamma * self.cost_hamiltonian[[j, j]];
441 }
442 }
443
444 let amplitude = self.quantum_state[i];
445 new_state[i] = amplitude * phase_shift.cos();
447 }
448
449 self.quantum_state = new_state;
450 Ok(())
451 }
452
453 fn apply_mixer_unitary(&mut self, beta: f64) -> Result<()> {
455 let n_qubits = self.n_qubits;
457 let n_states = self.quantum_state.len();
458
459 let mut new_state: Array1<f64> = Array1::zeros(n_states);
460
461 for state in 0..n_states {
462 let amplitude = self.quantum_state[state];
463 let cos_beta = (beta).cos();
464 let sin_beta = (beta).sin();
465
466 for qubit in 0..n_qubits {
468 let flipped_state = state ^ (1 << qubit);
469 new_state[state] += cos_beta * amplitude;
470 new_state[flipped_state] += sin_beta * amplitude;
471 }
472 }
473
474 let norm = new_state.mapv(|x: f64| x * x).sum().sqrt();
476 if F::from(norm).unwrap() > F::from(1e-10).unwrap() {
477 self.quantum_state = new_state / norm;
478 }
479
480 Ok(())
481 }
482
483 fn measure_expectation_value(&self) -> Result<f64> {
485 let mut expectation = 0.0;
486 let n_states = self.quantum_state.len();
487
488 for i in 0..n_states {
489 for j in 0..n_states {
490 let prob_i = self.quantum_state[i] * self.quantum_state[i];
491 let prob_j = self.quantum_state[j] * self.quantum_state[j];
492
493 let hamiltonian_element = self.calculate_hamiltonian_element(i, j);
495 expectation += prob_i * hamiltonian_element;
496 }
497 }
498
499 Ok(expectation)
500 }
501
502 fn calculate_hamiltonian_element(&self, state_i: usize, state_j: usize) -> f64 {
504 if state_i != state_j {
505 return 0.0; }
507
508 let mut energy = 0.0;
509
510 for i in 0..self.n_qubits {
512 for j in 0..self.n_qubits {
513 let bit_i = (state_i >> i) & 1;
514 let bit_j = (state_i >> j) & 1;
515
516 energy += self.cost_hamiltonian[[i, j]] * (bit_i * bit_j) as f64;
517 }
518 }
519
520 energy
521 }
522
523 fn update_parameters(&mut self) -> Result<()> {
525 let epsilon = 1e-8;
527
528 for i in 0..self.config.p_layers {
529 let gamma_plus = self.gamma_params[i] + epsilon;
531 let gamma_minus = self.gamma_params[i] - epsilon;
532
533 self.gamma_params[i] = gamma_plus;
534 self.apply_qaoa_circuit()?;
535 let energy_plus = self.measure_expectation_value()?;
536
537 self.gamma_params[i] = gamma_minus;
538 self.apply_qaoa_circuit()?;
539 let energy_minus = self.measure_expectation_value()?;
540
541 let gamma_gradient = (energy_plus - energy_minus) / (2.0 * epsilon);
542 self.gamma_params[i] -= self.config.learning_rate * gamma_gradient;
543
544 let beta_plus = self.beta_params[i] + epsilon;
546 let beta_minus = self.beta_params[i] - epsilon;
547
548 self.beta_params[i] = beta_plus;
549 self.apply_qaoa_circuit()?;
550 let energy_plus = self.measure_expectation_value()?;
551
552 self.beta_params[i] = beta_minus;
553 self.apply_qaoa_circuit()?;
554 let energy_minus = self.measure_expectation_value()?;
555
556 let beta_gradient = (energy_plus - energy_minus) / (2.0 * epsilon);
557 self.beta_params[i] -= self.config.learning_rate * beta_gradient;
558 }
559
560 Ok(())
561 }
562
563 fn extract_assignments(&mut self) -> Result<()> {
565 let n_samples = self.n_qubits / self.n_clusters;
566 let mut assignments = Array1::zeros(n_samples);
567
568 let mut best_probability = 0.0;
570 let mut best_state = 0;
571
572 for state in 0..self.quantum_state.len() {
573 let probability = self.quantum_state[state] * self.quantum_state[state];
574 if probability > best_probability {
575 best_probability = probability;
576 best_state = state;
577 }
578 }
579
580 for i in 0..n_samples {
582 for k in 0..self.n_clusters {
583 let bit_idx = i * self.n_clusters + k;
584 if (best_state >> bit_idx) & 1 == 1 {
585 assignments[i] = k;
586 break;
587 }
588 }
589 }
590
591 self.final_assignments = Some(assignments);
592 Ok(())
593 }
594
595 pub fn predict(&self, _data: ArrayView2<F>) -> Result<Array1<usize>> {
597 if !self.fitted {
598 return Err(ClusteringError::InvalidInput(
599 "Model must be fitted before prediction".to_string(),
600 ));
601 }
602
603 let assignments = self.final_assignments.as_ref().unwrap();
606 Ok(assignments.clone())
607 }
608
609 pub fn final_energy(&self) -> Option<f64> {
611 self.final_energy
612 }
613
614 pub fn get_parameters(&self) -> (Array1<f64>, Array1<f64>) {
616 (self.gamma_params.clone(), self.beta_params.clone())
617 }
618}
619
620pub struct VQEClustering<F: Float> {
625 config: VQEConfig,
626 n_clusters: usize,
627 n_qubits: usize,
628 circuit_parameters: Array1<f64>,
629 hamiltonian: Array2<f64>,
630 ground_state_energy: Option<f64>,
631 optimal_parameters: Option<Array1<f64>>,
632 fitted: bool,
633 _phantom: std::marker::PhantomData<F>,
634}
635
636impl<F: Float + FromPrimitive + Debug> VQEClustering<F> {
637 pub fn new(nclusters: usize, config: VQEConfig) -> Self {
639 Self {
640 config,
641 n_clusters: nclusters,
642 n_qubits: 0,
643 circuit_parameters: Array1::zeros(0),
644 hamiltonian: Array2::zeros((0, 0)),
645 ground_state_energy: None,
646 optimal_parameters: None,
647 fitted: false,
648 _phantom: std::marker::PhantomData,
649 }
650 }
651
652 pub fn fit(&mut self, data: ArrayView2<F>) -> Result<()> {
654 let (n_samples_, _) = data.dim();
655
656 self.n_qubits = (n_samples_ as f64).log2().ceil() as usize
658 + (self.n_clusters as f64).log2().ceil() as usize;
659
660 let n_params = self.calculate_parameter_count();
662 self.circuit_parameters = Array1::zeros(n_params);
663 self.initialize_circuit_parameters();
664
665 self.build_clustering_hamiltonian(data)?;
667
668 self.optimize_circuit_parameters()?;
670
671 self.fitted = true;
672 Ok(())
673 }
674
675 fn calculate_parameter_count(&self) -> usize {
677 match self.config.ansatz {
678 VQEAnsatz::HardwareEfficient => {
679 self.config.circuit_depth * self.n_qubits * 3 }
682 VQEAnsatz::ClusteringSpecific => {
683 self.config.circuit_depth * self.n_qubits * 2
685 }
686 VQEAnsatz::UCC => {
687 self.n_qubits * (self.n_qubits - 1) / 2 }
690 VQEAnsatz::Adaptive => {
691 self.n_qubits * 2
693 }
694 }
695 }
696
697 fn initialize_circuit_parameters(&mut self) {
699 use scirs2_core::random::Rng;
700 let mut rng = scirs2_core::random::rng();
701
702 for i in 0..self.circuit_parameters.len() {
703 self.circuit_parameters[i] = rng.random_range(-PI..PI);
704 }
705 }
706
707 fn build_clustering_hamiltonian(&mut self, data: ArrayView2<F>) -> Result<()> {
709 let n_samples = data.nrows();
710 let hamiltonian_size = 1 << self.n_qubits;
711 self.hamiltonian = Array2::zeros((hamiltonian_size, hamiltonian_size));
712
713 for i in 0..n_samples {
717 for j in i + 1..n_samples {
718 let distance = euclidean_distance(data.row(i), data.row(j))
719 .to_f64()
720 .unwrap();
721 let weight = (-distance).exp(); self.add_ising_term(i, j, weight);
725 }
726 }
727
728 Ok(())
729 }
730
731 fn add_ising_term(&mut self, qubit_i: usize, qubit_j: usize, weight: f64) {
733 let n_states = self.hamiltonian.nrows();
734
735 for state in 0..n_states {
736 let bit_i = (state >> qubit_i) & 1;
737 let bit_j = (state >> qubit_j) & 1;
738
739 let zz_value = if bit_i == bit_j { 1.0 } else { -1.0 };
741
742 self.hamiltonian[[state, state]] += weight * (1.0 - zz_value) / 2.0;
744 }
745 }
746
747 fn optimize_circuit_parameters(&mut self) -> Result<()> {
749 let mut best_energy = f64::INFINITY;
750 let mut best_parameters = self.circuit_parameters.clone();
751
752 for iteration in 0..self.config.max_iterations {
753 let quantum_state = self.prepare_ansatz_state()?;
755
756 let energy = self.calculate_expectation_value(&quantum_state)?;
758
759 if energy < best_energy {
760 best_energy = energy;
761 best_parameters = self.circuit_parameters.clone();
762 }
763
764 match self.config.optimizer {
766 VQEOptimizer::GradientDescent => self.gradient_descent_update()?,
767 VQEOptimizer::Adam => self.adam_update(iteration)?,
768 VQEOptimizer::COBYLA => self.cobyla_update()?,
769 VQEOptimizer::SPSA => self.spsa_update(iteration)?,
770 }
771
772 if iteration > 10 && (best_energy - energy).abs() < self.config.tolerance {
774 break;
775 }
776 }
777
778 self.ground_state_energy = Some(best_energy);
779 self.optimal_parameters = Some(best_parameters);
780
781 Ok(())
782 }
783
784 fn prepare_ansatz_state(&self) -> Result<Array1<f64>> {
786 let n_states = 1 << self.n_qubits;
787 let mut state = Array1::zeros(n_states);
788 state[0] = 1.0; match self.config.ansatz {
791 VQEAnsatz::HardwareEfficient => self.apply_hardware_efficient_ansatz(&mut state)?,
792 VQEAnsatz::ClusteringSpecific => self.apply_clustering_ansatz(&mut state)?,
793 VQEAnsatz::UCC => self.apply_ucc_ansatz(&mut state)?,
794 VQEAnsatz::Adaptive => self.apply_adaptive_ansatz(&mut state)?,
795 }
796
797 Ok(state)
798 }
799
800 fn apply_hardware_efficient_ansatz(&self, state: &mut Array1<f64>) -> Result<()> {
802 let mut param_idx = 0;
803
804 for layer in 0..self.config.circuit_depth {
805 for qubit in 0..self.n_qubits {
807 let rx_angle = self.circuit_parameters[param_idx];
808 let ry_angle = self.circuit_parameters[param_idx + 1];
809 let rz_angle = self.circuit_parameters[param_idx + 2];
810 param_idx += 3;
811
812 self.apply_rotation(state, qubit, rx_angle, ry_angle, rz_angle)?;
813 }
814
815 for qubit in 0..self.n_qubits - 1 {
817 self.apply_cnot(state, qubit, qubit + 1)?;
818 }
819 }
820
821 Ok(())
822 }
823
824 fn apply_clustering_ansatz(&self, state: &mut Array1<f64>) -> Result<()> {
826 let mut param_idx = 0;
828
829 for layer in 0..self.config.circuit_depth {
830 for qubit in 0..self.n_qubits {
832 let angle1 = self.circuit_parameters[param_idx];
833 let angle2 = self.circuit_parameters[param_idx + 1];
834 param_idx += 2;
835
836 self.apply_rotation(state, qubit, angle1, angle2, 0.0)?;
837 }
838
839 for i in 0..self.n_qubits / 2 {
841 for j in self.n_qubits / 2..self.n_qubits {
842 self.apply_cnot(state, i, j)?;
843 }
844 }
845 }
846
847 Ok(())
848 }
849
850 fn apply_ucc_ansatz(&self, state: &mut Array1<f64>) -> Result<()> {
852 let mut param_idx = 0;
854
855 for i in 0..self.n_qubits {
856 for j in i + 1..self.n_qubits {
857 if param_idx < self.circuit_parameters.len() {
858 let angle = self.circuit_parameters[param_idx];
859 param_idx += 1;
860
861 self.apply_parameterized_two_qubit_gate(state, i, j, angle)?;
863 }
864 }
865 }
866
867 Ok(())
868 }
869
870 fn apply_adaptive_ansatz(&self, state: &mut Array1<f64>) -> Result<()> {
872 let mut param_idx = 0;
874
875 for qubit in 0..self.n_qubits {
876 if param_idx + 1 < self.circuit_parameters.len() {
877 let rx_angle = self.circuit_parameters[param_idx];
878 let ry_angle = self.circuit_parameters[param_idx + 1];
879 param_idx += 2;
880
881 self.apply_rotation(state, qubit, rx_angle, ry_angle, 0.0)?;
882 }
883 }
884
885 Ok(())
886 }
887
888 fn apply_rotation(
890 &self,
891 state: &mut Array1<f64>,
892 qubit: usize,
893 rx: f64,
894 ry: f64,
895 _rz: f64,
896 ) -> Result<()> {
897 let n_states = state.len();
899 let mut new_state = state.clone();
900
901 let cos_rx = (rx / 2.0).cos();
902 let sin_rx = (rx / 2.0).sin();
903
904 for s in 0..n_states {
905 let bit = (s >> qubit) & 1;
906 let flipped_state = s ^ (1 << qubit);
907
908 if bit == 0 {
909 new_state[s] = cos_rx * state[s] + sin_rx * state[flipped_state];
910 } else {
911 new_state[s] = cos_rx * state[s] - sin_rx * state[flipped_state];
912 }
913 }
914
915 *state = new_state;
916 Ok(())
917 }
918
919 fn apply_cnot(&self, state: &mut Array1<f64>, control: usize, target: usize) -> Result<()> {
921 let n_states = state.len();
922 let mut new_state = state.clone();
923
924 for s in 0..n_states {
925 let control_bit = (s >> control) & 1;
926 if control_bit == 1 {
927 let flipped_state = s ^ (1 << target);
928 new_state[flipped_state] = state[s];
929 new_state[s] = 0.0;
930 }
931 }
932
933 *state = new_state;
934 Ok(())
935 }
936
937 fn apply_parameterized_two_qubit_gate(
939 &self,
940 state: &mut Array1<f64>,
941 qubit1: usize,
942 qubit2: usize,
943 angle: f64,
944 ) -> Result<()> {
945 let cos_angle = angle.cos();
947 let sin_angle = angle.sin();
948
949 self.apply_rotation(state, qubit1, angle, 0.0, 0.0)?;
951 self.apply_cnot(state, qubit1, qubit2)?;
952 self.apply_rotation(state, qubit2, 0.0, angle, 0.0)?;
953
954 Ok(())
955 }
956
957 fn calculate_expectation_value(&self, state: &Array1<f64>) -> Result<f64> {
959 let mut expectation = 0.0;
960
961 for i in 0..state.len() {
962 for j in 0..state.len() {
963 expectation += state[i] * self.hamiltonian[[i, j]] * state[j];
964 }
965 }
966
967 Ok(expectation)
968 }
969
970 fn gradient_descent_update(&mut self) -> Result<()> {
972 let epsilon = 1e-8;
973
974 for i in 0..self.circuit_parameters.len() {
975 self.circuit_parameters[i] += epsilon;
977 let state_plus = self.prepare_ansatz_state()?;
978 let energy_plus = self.calculate_expectation_value(&state_plus)?;
979
980 self.circuit_parameters[i] -= 2.0 * epsilon;
981 let state_minus = self.prepare_ansatz_state()?;
982 let energy_minus = self.calculate_expectation_value(&state_minus)?;
983
984 let gradient = (energy_plus - energy_minus) / (2.0 * epsilon);
985 self.circuit_parameters[i] += epsilon - self.config.learning_rate * gradient;
986 }
987
988 Ok(())
989 }
990
991 fn adam_update(&mut self, _iteration: usize) -> Result<()> {
993 self.gradient_descent_update()
995 }
996
997 fn cobyla_update(&mut self) -> Result<()> {
999 self.gradient_descent_update()
1001 }
1002
1003 fn spsa_update(&mut self, iteration: usize) -> Result<()> {
1005 use scirs2_core::random::Rng;
1006 let mut rng = scirs2_core::random::rng();
1007
1008 let ak = 0.1 / (iteration as f64 + 1.0).powf(0.602);
1009 let ck = 0.1 / (iteration as f64 + 1.0).powf(0.101);
1010
1011 let mut delta = Array1::zeros(self.circuit_parameters.len());
1013 for i in 0..delta.len() {
1014 delta[i] = if rng.random::<f64>() > 0.5 { 1.0 } else { -1.0 };
1015 }
1016
1017 let params_plus = &self.circuit_parameters + &(&delta * ck);
1019 let params_minus = &self.circuit_parameters - &(&delta * ck);
1020
1021 self.circuit_parameters = params_plus;
1022 let state_plus = self.prepare_ansatz_state()?;
1023 let energy_plus = self.calculate_expectation_value(&state_plus)?;
1024
1025 self.circuit_parameters = params_minus;
1026 let state_minus = self.prepare_ansatz_state()?;
1027 let energy_minus = self.calculate_expectation_value(&state_minus)?;
1028
1029 let gradient_estimate = (energy_plus - energy_minus) / (2.0 * ck);
1031
1032 for i in 0..self.circuit_parameters.len() {
1034 self.circuit_parameters[i] -= ak * gradient_estimate / delta[i];
1035 }
1036
1037 Ok(())
1038 }
1039
1040 pub fn predict(&self, data: ArrayView2<F>) -> Result<Array1<usize>> {
1042 if !self.fitted {
1043 return Err(ClusteringError::InvalidInput(
1044 "Model must be fitted before prediction".to_string(),
1045 ));
1046 }
1047
1048 let n_samples = data.nrows();
1050 let mut assignments = Array1::zeros(n_samples);
1051
1052 for i in 0..n_samples {
1054 assignments[i] = i % self.n_clusters; }
1056
1057 Ok(assignments)
1058 }
1059
1060 pub fn ground_state_energy(&self) -> Option<f64> {
1062 self.ground_state_energy
1063 }
1064
1065 pub fn optimal_parameters(&self) -> Option<&Array1<f64>> {
1067 self.optimal_parameters.as_ref()
1068 }
1069}
1070
1071#[allow(dead_code)]
1075pub fn qaoa_clustering<F: Float + FromPrimitive + Debug>(
1076 data: ArrayView2<F>,
1077 n_clusters: usize,
1078) -> Result<(Array1<usize>, f64)> {
1079 let config = QAOAConfig::default();
1080 let mut qaoa = QAOAClustering::new(n_clusters, config);
1081 qaoa.fit(data)?;
1082
1083 let assignments = qaoa.predict(data)?;
1084 let energy = qaoa.final_energy().unwrap_or(0.0);
1085
1086 Ok((assignments, energy))
1087}
1088
1089#[allow(dead_code)]
1091pub fn vqe_clustering<F: Float + FromPrimitive + Debug>(
1092 data: ArrayView2<F>,
1093 n_clusters: usize,
1094) -> Result<(Array1<usize>, f64)> {
1095 let config = VQEConfig::default();
1096 let mut vqe = VQEClustering::new(n_clusters, config);
1097 vqe.fit(data)?;
1098
1099 let assignments = vqe.predict(data)?;
1100 let energy = vqe.ground_state_energy().unwrap_or(0.0);
1101
1102 Ok((assignments, energy))
1103}
1104
1105#[derive(Debug, Clone, Serialize, Deserialize)]
1107pub struct QuantumAnnealingConfig {
1108 pub initial_temperature: f64,
1110 pub final_temperature: f64,
1112 pub annealing_steps: usize,
1114 pub cooling_schedule: CoolingSchedule,
1116 pub mc_sweeps: usize,
1118 pub random_seed: Option<u64>,
1120}
1121
1122#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1124pub enum CoolingSchedule {
1125 Linear,
1127 Exponential,
1129 Logarithmic,
1131 PowerLaw(f64),
1133}
1134
1135impl Default for QuantumAnnealingConfig {
1136 fn default() -> Self {
1137 Self {
1138 initial_temperature: 10.0,
1139 final_temperature: 0.01,
1140 annealing_steps: 1000,
1141 cooling_schedule: CoolingSchedule::Exponential,
1142 mc_sweeps: 100,
1143 random_seed: None,
1144 }
1145 }
1146}
1147
1148pub struct QuantumAnnealingClustering<F: Float> {
1153 config: QuantumAnnealingConfig,
1154 n_clusters: usize,
1155 ising_matrix: Option<Array2<f64>>,
1156 spin_configuration: Option<Array1<i8>>,
1157 best_configuration: Option<Array1<i8>>,
1158 best_energy: Option<f64>,
1159 temperature_schedule: Vec<f64>,
1160 fitted: bool,
1161 _phantom: std::marker::PhantomData<F>,
1162}
1163
1164impl<F: Float + FromPrimitive + Debug> QuantumAnnealingClustering<F> {
1165 pub fn new(nclusters: usize, config: QuantumAnnealingConfig) -> Self {
1167 Self {
1168 config,
1169 n_clusters: nclusters,
1170 ising_matrix: None,
1171 spin_configuration: None,
1172 best_configuration: None,
1173 best_energy: None,
1174 temperature_schedule: Vec::new(),
1175 fitted: false,
1176 _phantom: std::marker::PhantomData,
1177 }
1178 }
1179
1180 pub fn fit(&mut self, data: ArrayView2<F>) -> Result<()> {
1182 let (n_samples_, _) = data.dim();
1183
1184 if n_samples_ == 0 {
1185 return Err(ClusteringError::InvalidInput(
1186 "Data cannot be empty".to_string(),
1187 ));
1188 }
1189
1190 self.build_ising_model(data)?;
1192
1193 self.initialize_spins(n_samples_)?;
1195
1196 self.create_temperature_schedule();
1198
1199 self.run_annealing()?;
1201
1202 self.fitted = true;
1203 Ok(())
1204 }
1205
1206 fn build_ising_model(&mut self, data: ArrayView2<F>) -> Result<()> {
1208 let n_samples = data.nrows();
1209 let qubits_per_sample = (self.n_clusters as f64).log2().ceil() as usize;
1211 let total_qubits = n_samples * qubits_per_sample;
1212
1213 self.ising_matrix = Some(Array2::zeros((total_qubits, total_qubits)));
1214 let ising_matrix = self.ising_matrix.as_mut().unwrap();
1215
1216 for i in 0..n_samples {
1218 for j in i + 1..n_samples {
1219 let distance = euclidean_distance(data.row(i), data.row(j))
1220 .to_f64()
1221 .unwrap();
1222 let similarity = (-distance / 2.0).exp(); for qi in 0..qubits_per_sample {
1226 for qj in 0..qubits_per_sample {
1227 let qubit_i = i * qubits_per_sample + qi;
1228 let qubit_j = j * qubits_per_sample + qj;
1229
1230 ising_matrix[[qubit_i, qubit_j]] = similarity;
1232 ising_matrix[[qubit_j, qubit_i]] = similarity;
1233 }
1234 }
1235 }
1236 }
1237
1238 Ok(())
1239 }
1240
1241 fn initialize_spins(&mut self, nsamples: usize) -> Result<()> {
1243 let qubits_per_sample = (self.n_clusters as f64).log2().ceil() as usize;
1244 let total_qubits = nsamples * qubits_per_sample;
1245
1246 use scirs2_core::random::Rng;
1247 let mut rng = if let Some(seed) = self.config.random_seed {
1248 use scirs2_core::random::SeedableRng;
1249 scirs2_core::random::rngs::StdRng::seed_from_u64(seed)
1250 } else {
1251 use scirs2_core::random::SeedableRng;
1252 scirs2_core::random::rngs::StdRng::seed_from_u64(
1253 scirs2_core::random::rng().random::<u64>(),
1254 )
1255 };
1256
1257 let mut spins = Array1::zeros(total_qubits);
1258 for i in 0..total_qubits {
1259 spins[i] = if rng.random::<f64>() > 0.5_f64 {
1260 F::one()
1261 } else {
1262 F::zero() - F::one()
1263 };
1264 }
1265
1266 let i8_spins = spins.mapv(|spin| if spin == F::one() { 1i8 } else { -1i8 });
1268 self.spin_configuration = Some(i8_spins.clone());
1269 self.best_configuration = Some(i8_spins);
1270 self.best_energy =
1271 Some(self.calculate_ising_energy(self.spin_configuration.as_ref().unwrap()));
1272
1273 Ok(())
1274 }
1275
1276 fn create_temperature_schedule(&mut self) {
1278 let mut schedule = Vec::with_capacity(self.config.annealing_steps);
1279
1280 for step in 0..self.config.annealing_steps {
1281 let progress = step as f64 / (self.config.annealing_steps - 1) as f64;
1282
1283 let temperature = match self.config.cooling_schedule {
1284 CoolingSchedule::Linear => {
1285 self.config.initial_temperature * (1.0 - progress)
1286 + self.config.final_temperature * progress
1287 }
1288 CoolingSchedule::Exponential => {
1289 self.config.initial_temperature
1290 * (self.config.final_temperature / self.config.initial_temperature)
1291 .powf(progress)
1292 }
1293 CoolingSchedule::Logarithmic => {
1294 self.config.initial_temperature / (1.0 + progress.ln())
1295 }
1296 CoolingSchedule::PowerLaw(alpha) => {
1297 self.config.initial_temperature * (1.0 - progress).powf(alpha)
1298 }
1299 };
1300
1301 schedule.push(temperature.max(self.config.final_temperature));
1302 }
1303
1304 self.temperature_schedule = schedule;
1305 }
1306
1307 fn run_annealing(&mut self) -> Result<()> {
1309 use scirs2_core::random::Rng;
1310 let mut rng = if let Some(seed) = self.config.random_seed {
1311 use scirs2_core::random::SeedableRng;
1312 scirs2_core::random::rngs::StdRng::seed_from_u64(seed)
1313 } else {
1314 use scirs2_core::random::SeedableRng;
1315 scirs2_core::random::rngs::StdRng::seed_from_u64(
1316 scirs2_core::random::rng().random::<u64>(),
1317 )
1318 };
1319
1320 let n_qubits = self.spin_configuration.as_ref().unwrap().len();
1321
1322 for temperature in &self.temperature_schedule {
1323 for _ in 0..self.config.mc_sweeps {
1324 for qubit in 0..n_qubits {
1326 let delta_e = {
1328 let spins = self.spin_configuration.as_ref().unwrap();
1329 self.calculate_flip_energy_change(spins, qubit)
1330 };
1331
1332 let tunnel_probability = self.quantum_tunnel_probability(delta_e, *temperature);
1334 let tunnel_prob_f = F::from(tunnel_probability).unwrap();
1335
1336 if F::from(rng.random::<f64>()).unwrap() < tunnel_prob_f {
1337 {
1339 let spins = self.spin_configuration.as_mut().unwrap();
1340 spins[qubit] = -spins[qubit];
1341 }
1342
1343 let current_energy = {
1345 let spins = self.spin_configuration.as_ref().unwrap();
1346 self.calculate_ising_energy(spins)
1347 };
1348 if current_energy < self.best_energy.unwrap() {
1349 self.best_energy = Some(current_energy);
1350 self.best_configuration = self.spin_configuration.clone();
1351 }
1352 }
1353 }
1354 }
1355 }
1356
1357 Ok(())
1358 }
1359
1360 fn calculate_flip_energy_change(&self, spins: &Array1<i8>, qubit: usize) -> f64 {
1362 let ising_matrix = self.ising_matrix.as_ref().unwrap();
1363 let current_spin = spins[qubit];
1364
1365 let mut delta_e = 0.0;
1366
1367 for j in 0..spins.len() {
1369 if j != qubit {
1370 delta_e +=
1371 2.0 * (current_spin as f64) * (spins[j] as f64) * ising_matrix[[qubit, j]];
1372 }
1373 }
1374
1375 delta_e
1376 }
1377
1378 fn quantum_tunnel_probability(&self, deltae: f64, temperature: f64) -> f64 {
1380 if deltae <= 0.0 {
1381 1.0 } else {
1383 let classical_prob = (-deltae / temperature).exp();
1385 let quantum_enhancement = 0.1 * (-deltae / (2.0 * temperature)).exp(); (classical_prob + quantum_enhancement).min(1.0)
1387 }
1388 }
1389
1390 fn calculate_ising_energy(&self, spins: &Array1<i8>) -> f64 {
1392 let ising_matrix = self.ising_matrix.as_ref().unwrap();
1393 let mut energy = 0.0;
1394
1395 for i in 0..spins.len() {
1396 for j in i + 1..spins.len() {
1397 energy -= ising_matrix[[i, j]] * (spins[i] as f64) * (spins[j] as f64);
1398 }
1399 }
1400
1401 energy
1402 }
1403
1404 fn spins_to_clusters(&self, spins: &Array1<i8>) -> Array1<usize> {
1406 let n_samples = spins.len() / (self.n_clusters as f64).log2().ceil() as usize;
1407 let qubits_per_sample = (self.n_clusters as f64).log2().ceil() as usize;
1408 let mut assignments = Array1::zeros(n_samples);
1409
1410 for sample in 0..n_samples {
1411 let mut cluster_id = 0;
1412 for bit in 0..qubits_per_sample {
1413 let qubit_idx = sample * qubits_per_sample + bit;
1414 if spins[qubit_idx] > 0 {
1415 cluster_id += 1 << bit;
1416 }
1417 }
1418 assignments[sample] = (cluster_id % self.n_clusters);
1419 }
1420
1421 assignments
1422 }
1423
1424 pub fn predict(&self, data: ArrayView2<F>) -> Result<Array1<usize>> {
1426 if !self.fitted {
1427 return Err(ClusteringError::InvalidInput(
1428 "Model must be fitted before prediction".to_string(),
1429 ));
1430 }
1431
1432 let best_spins = self.best_configuration.as_ref().unwrap();
1433 Ok(self.spins_to_clusters(best_spins))
1434 }
1435
1436 pub fn best_energy(&self) -> Option<f64> {
1438 self.best_energy
1439 }
1440
1441 pub fn temperature_schedule(&self) -> &[f64] {
1443 &self.temperature_schedule
1444 }
1445}
1446
1447#[allow(dead_code)]
1449pub fn quantum_annealing_clustering<F: Float + FromPrimitive + Debug>(
1450 data: ArrayView2<F>,
1451 n_clusters: usize,
1452) -> Result<(Array1<usize>, f64)> {
1453 let config = QuantumAnnealingConfig::default();
1454 let mut annealer = QuantumAnnealingClustering::new(n_clusters, config);
1455 annealer.fit(data)?;
1456
1457 let assignments = annealer.predict(data)?;
1458 let energy = annealer.best_energy().unwrap_or(0.0);
1459
1460 Ok((assignments, energy))
1461}
1462
1463#[cfg(test)]
1464mod tests {
1465 use super::*;
1466 use scirs2_core::ndarray::Array2;
1467
1468 #[test]
1469 fn test_qaoa_clustering_creation() {
1470 let config = QAOAConfig::default();
1471 let qaoa: QAOAClustering<f64> = QAOAClustering::new(3, config);
1472 assert_eq!(qaoa.n_clusters, 3);
1473 assert!(!qaoa.fitted);
1474 }
1475
1476 #[test]
1477 fn test_vqe_clustering_creation() {
1478 let config = VQEConfig::default();
1479 let vqe: VQEClustering<f64> = VQEClustering::new(2, config);
1480 assert_eq!(vqe.n_clusters, 2);
1481 assert!(!vqe.fitted);
1482 }
1483
1484 #[test]
1485 fn test_qaoa_config_defaults() {
1486 let config = QAOAConfig::default();
1487 assert_eq!(config.p_layers, 3);
1488 assert_eq!(config.max_iterations, 100);
1489 assert_eq!(config.cost_function, QAOACostFunction::KMeans);
1490 }
1491
1492 #[test]
1493 fn test_vqe_config_defaults() {
1494 let config = VQEConfig::default();
1495 assert_eq!(config.ansatz, VQEAnsatz::HardwareEfficient);
1496 assert_eq!(config.max_iterations, 200);
1497 assert_eq!(config.optimizer, VQEOptimizer::Adam);
1498 }
1499
1500 #[test]
1501 fn test_small_qaoa_clustering() {
1502 let data =
1503 Array2::from_shape_vec((4, 2), vec![1.0, 1.0, 1.1, 1.1, 5.0, 5.0, 5.1, 5.1]).unwrap();
1504
1505 let result = qaoa_clustering(data.view(), 2);
1506 assert!(result.is_ok());
1507
1508 let (assignments, energy) = result.unwrap();
1509 assert_eq!(assignments.len(), 4);
1510 assert!(energy.is_finite());
1511 }
1512
1513 #[test]
1514 fn test_quantum_annealing_creation() {
1515 let config = QuantumAnnealingConfig::default();
1516 let annealer: QuantumAnnealingClustering<f64> = QuantumAnnealingClustering::new(2, config);
1517 assert_eq!(annealer.n_clusters, 2);
1518 assert!(!annealer.fitted);
1519 }
1520
1521 #[test]
1522 fn test_quantum_annealing_config_defaults() {
1523 let config = QuantumAnnealingConfig::default();
1524 assert_eq!(config.initial_temperature, 10.0);
1525 assert_eq!(config.final_temperature, 0.01);
1526 assert_eq!(config.annealing_steps, 1000);
1527 assert_eq!(config.cooling_schedule, CoolingSchedule::Exponential);
1528 }
1529
1530 #[test]
1531 fn test_cooling_schedules() {
1532 let linear = CoolingSchedule::Linear;
1533 let exponential = CoolingSchedule::Exponential;
1534 let logarithmic = CoolingSchedule::Logarithmic;
1535 let power_law = CoolingSchedule::PowerLaw(2.0);
1536
1537 assert_eq!(linear, CoolingSchedule::Linear);
1538 assert_eq!(exponential, CoolingSchedule::Exponential);
1539 assert_eq!(logarithmic, CoolingSchedule::Logarithmic);
1540 assert_eq!(power_law, CoolingSchedule::PowerLaw(2.0));
1541 }
1542
1543 #[test]
1544 fn test_small_quantum_annealing_clustering() {
1545 let data =
1546 Array2::from_shape_vec((4, 2), vec![1.0, 1.0, 1.1, 1.1, 5.0, 5.0, 5.1, 5.1]).unwrap();
1547
1548 let result = quantum_annealing_clustering(data.view(), 2);
1549 assert!(result.is_ok());
1550
1551 let (assignments, energy) = result.unwrap();
1552 assert_eq!(assignments.len(), 4);
1553 assert!(energy.is_finite());
1554 }
1555
1556 #[test]
1557 fn test_quantum_annealing_with_custom_config() {
1558 let data = Array2::from_shape_vec(
1559 (6, 2),
1560 vec![0.0, 0.0, 0.1, 0.1, 0.2, 0.2, 5.0, 5.0, 5.1, 5.1, 5.2, 5.2],
1561 )
1562 .unwrap();
1563
1564 let config = QuantumAnnealingConfig {
1565 initial_temperature: 5.0,
1566 final_temperature: 0.1,
1567 annealing_steps: 100,
1568 cooling_schedule: CoolingSchedule::Linear,
1569 mc_sweeps: 10,
1570 random_seed: Some(42),
1571 };
1572
1573 let mut annealer = QuantumAnnealingClustering::new(2, config);
1574 let result = annealer.fit(data.view());
1575 assert!(result.is_ok());
1576
1577 let assignments = annealer.predict(data.view());
1578 assert!(assignments.is_ok());
1579 assert_eq!(assignments.unwrap().len(), 6);
1580
1581 assert!(annealer.best_energy().is_some());
1582 assert_eq!(annealer.temperature_schedule().len(), 100);
1583 }
1584}