quantrs2_ml/
qsvm.rs

1//! Quantum Support Vector Machine (QSVM) implementation
2//!
3//! This module implements quantum-enhanced support vector machines for
4//! classification tasks using quantum feature maps and kernel methods.
5
6use scirs2_core::ndarray::{Array1, Array2};
7use scirs2_core::Complex64;
8use std::collections::HashMap;
9use std::f64::consts::PI;
10
11/// Quantum feature map types
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum FeatureMapType {
14    /// Pauli-Z feature map: exp(i·φ(x)·Z)
15    ZFeatureMap,
16    /// Pauli-ZZ feature map: exp(i·φ(x)·ZZ)
17    ZZFeatureMap,
18    /// Pauli feature map (general)
19    PauliFeatureMap,
20    /// Custom angle encoding
21    AngleEncoding,
22    /// Amplitude encoding
23    AmplitudeEncoding,
24}
25
26/// Parameters for QSVM
27#[derive(Debug, Clone)]
28pub struct QSVMParams {
29    /// Type of quantum feature map
30    pub feature_map: FeatureMapType,
31    /// Number of repetitions of the feature map circuit
32    pub reps: usize,
33    /// Regularization parameter (also accessible as c_parameter for compatibility)
34    pub c: f64,
35    /// Tolerance for convergence
36    pub tolerance: f64,
37    /// Number of qubits
38    pub num_qubits: usize,
39    /// Circuit depth
40    pub depth: usize,
41    /// Gamma parameter for RBF-like kernels
42    pub gamma: Option<f64>,
43    /// Regularization parameter (alias for c)
44    pub regularization: f64,
45    /// Maximum iterations for optimization
46    pub max_iterations: usize,
47    /// Random seed for reproducibility
48    pub seed: Option<u64>,
49}
50
51impl Default for QSVMParams {
52    fn default() -> Self {
53        Self {
54            feature_map: FeatureMapType::ZZFeatureMap,
55            reps: 2,
56            c: 1.0,
57            tolerance: 1e-3,
58            num_qubits: 4,
59            depth: 2,
60            gamma: None,
61            regularization: 1.0,
62            max_iterations: 1000,
63            seed: None,
64        }
65    }
66}
67
68impl QSVMParams {
69    /// Get c_parameter (alias for c)
70    pub fn c_parameter(&self) -> f64 {
71        self.c
72    }
73
74    /// Set c_parameter (updates both c and regularization)
75    pub fn set_c_parameter(&mut self, value: f64) {
76        self.c = value;
77        self.regularization = value;
78    }
79}
80
81/// Quantum kernel computation
82pub struct QuantumKernel {
83    feature_map: FeatureMapType,
84    reps: usize,
85}
86
87impl QuantumKernel {
88    /// Create a new quantum kernel
89    pub fn new(feature_map: FeatureMapType, reps: usize) -> Self {
90        Self { feature_map, reps }
91    }
92
93    /// Compute the quantum kernel between two data points
94    pub fn compute(&self, x1: &Array1<f64>, x2: &Array1<f64>) -> f64 {
95        match self.feature_map {
96            FeatureMapType::ZFeatureMap => self.z_feature_map_kernel(x1, x2),
97            FeatureMapType::ZZFeatureMap => self.zz_feature_map_kernel(x1, x2),
98            FeatureMapType::PauliFeatureMap => self.zz_feature_map_kernel(x1, x2), // Use ZZ as fallback
99            FeatureMapType::AngleEncoding => self.angle_encoding_kernel(x1, x2),
100            FeatureMapType::AmplitudeEncoding => self.amplitude_encoding_kernel(x1, x2),
101        }
102    }
103
104    /// Z feature map kernel
105    fn z_feature_map_kernel(&self, x1: &Array1<f64>, x2: &Array1<f64>) -> f64 {
106        let n = x1.len();
107        let mut kernel_val = 1.0;
108
109        for _ in 0..self.reps {
110            for i in 0..n {
111                let phase_diff = (x1[i] - x2[i]) * PI;
112                kernel_val *= phase_diff.cos();
113            }
114        }
115
116        kernel_val
117    }
118
119    /// ZZ feature map kernel (includes entanglement)
120    fn zz_feature_map_kernel(&self, x1: &Array1<f64>, x2: &Array1<f64>) -> f64 {
121        let n = x1.len();
122        let mut kernel_val = 1.0;
123
124        for rep in 0..self.reps {
125            // Single-qubit rotations
126            for i in 0..n {
127                let phase_diff = (x1[i] - x2[i]) * PI * (rep + 1) as f64;
128                kernel_val *= phase_diff.cos();
129            }
130
131            // Two-qubit interactions
132            for i in 0..n - 1 {
133                let interaction = (x1[i] - x2[i]) * (x1[i + 1] - x2[i + 1]) * PI;
134                kernel_val *= interaction.cos();
135            }
136        }
137
138        kernel_val
139    }
140
141    /// Angle encoding kernel
142    fn angle_encoding_kernel(&self, x1: &Array1<f64>, x2: &Array1<f64>) -> f64 {
143        let mut sum = 0.0;
144        for i in 0..x1.len() {
145            sum += (x1[i] - x2[i]).powi(2);
146        }
147        (-sum / 2.0).exp()
148    }
149
150    /// Amplitude encoding kernel
151    fn amplitude_encoding_kernel(&self, x1: &Array1<f64>, x2: &Array1<f64>) -> f64 {
152        // Normalize vectors
153        let norm1 = x1.dot(x1).sqrt();
154        let norm2 = x2.dot(x2).sqrt();
155
156        if norm1 < 1e-10 || norm2 < 1e-10 {
157            return 0.0;
158        }
159
160        let x1_norm = x1 / norm1;
161        let x2_norm = x2 / norm2;
162
163        // Inner product gives fidelity
164        x1_norm.dot(&x2_norm).powi(2)
165    }
166
167    /// Compute the full kernel matrix for a dataset
168    pub fn compute_kernel_matrix(&self, data: &Array2<f64>) -> Array2<f64> {
169        let n_samples = data.nrows();
170        let mut kernel_matrix = Array2::zeros((n_samples, n_samples));
171
172        for i in 0..n_samples {
173            for j in i..n_samples {
174                let kernel_val = self.compute(&data.row(i).to_owned(), &data.row(j).to_owned());
175                kernel_matrix[[i, j]] = kernel_val;
176                kernel_matrix[[j, i]] = kernel_val; // Symmetric
177            }
178        }
179
180        kernel_matrix
181    }
182}
183
184/// Quantum Support Vector Machine classifier
185pub struct QSVM {
186    params: QSVMParams,
187    kernel: QuantumKernel,
188    support_vectors: Option<Array2<f64>>,
189    support_labels: Option<Array1<i32>>,
190    alphas: Option<Array1<f64>>,
191    bias: f64,
192    kernel_matrix_cache: HashMap<(usize, usize), f64>,
193}
194
195impl QSVM {
196    /// Create a new QSVM classifier
197    pub fn new(params: QSVMParams) -> Self {
198        let kernel = QuantumKernel::new(params.feature_map, params.reps);
199        Self {
200            params,
201            kernel,
202            support_vectors: None,
203            support_labels: None,
204            alphas: None,
205            bias: 0.0,
206            kernel_matrix_cache: HashMap::new(),
207        }
208    }
209
210    /// Train the QSVM using SMO (Sequential Minimal Optimization)
211    pub fn fit(&mut self, x: &Array2<f64>, y: &Array1<i32>) -> Result<(), String> {
212        let n_samples = x.nrows();
213
214        // Validate labels are binary (-1 or 1)
215        for &label in y.iter() {
216            if label != -1 && label != 1 {
217                return Err("QSVM requires binary labels: -1 or 1".to_string());
218            }
219        }
220
221        // Initialize alphas
222        let mut alphas = Array1::zeros(n_samples);
223
224        // Precompute kernel matrix
225        let kernel_matrix = self.kernel.compute_kernel_matrix(x);
226
227        // SMO optimization
228        let mut converged = false;
229        let mut iter = 0;
230
231        while !converged && iter < self.params.max_iterations {
232            let old_alphas = alphas.clone();
233
234            // Select working set (simplified SMO)
235            for i in 0..n_samples {
236                // Compute error for i
237                let ei = self.compute_error(&kernel_matrix, &alphas, y, i);
238
239                // Check KKT conditions
240                if !self.check_kkt(alphas[i], y[i], ei) {
241                    // Select j != i
242                    let j = self.select_second_alpha(i, n_samples, &kernel_matrix, &alphas, y);
243                    if i == j {
244                        continue;
245                    }
246
247                    // Compute error for j
248                    let ej = self.compute_error(&kernel_matrix, &alphas, y, j);
249
250                    // Save old alphas
251                    let alpha_i_old = alphas[i];
252                    let alpha_j_old = alphas[j];
253
254                    // Compute bounds
255                    let (l, h) = self.compute_bounds(y[i], y[j], alphas[i], alphas[j]);
256
257                    if (h - l).abs() < 1e-10 {
258                        continue;
259                    }
260
261                    // Compute eta
262                    let eta =
263                        kernel_matrix[[i, i]] + kernel_matrix[[j, j]] - 2.0 * kernel_matrix[[i, j]];
264
265                    if eta <= 0.0 {
266                        continue;
267                    }
268
269                    // Update alpha_j
270                    alphas[j] += y[j] as f64 * (ei - ej) / eta;
271                    alphas[j] = alphas[j].clamp(l, h);
272
273                    if (alphas[j] - alpha_j_old).abs() < 1e-5 {
274                        continue;
275                    }
276
277                    // Update alpha_i
278                    alphas[i] += y[i] as f64 * y[j] as f64 * (alpha_j_old - alphas[j]);
279                }
280            }
281
282            // Check convergence
283            let alpha_change: f64 = (&alphas - &old_alphas).mapv(|a| a.abs()).sum();
284            converged = alpha_change < self.params.tolerance;
285            iter += 1;
286        }
287
288        // Extract support vectors
289        let mut support_indices = Vec::new();
290        let mut support_alphas = Vec::new();
291
292        for i in 0..n_samples {
293            if alphas[i] > 1e-5 {
294                support_indices.push(i);
295                support_alphas.push(alphas[i]);
296            }
297        }
298
299        if support_indices.is_empty() {
300            return Err("No support vectors found".to_string());
301        }
302
303        // Store support vectors
304        let n_support = support_indices.len();
305        let n_features = x.ncols();
306        let mut support_vectors = Array2::zeros((n_support, n_features));
307        let mut support_labels = Array1::zeros(n_support);
308
309        for (idx, &i) in support_indices.iter().enumerate() {
310            support_vectors.row_mut(idx).assign(&x.row(i));
311            support_labels[idx] = y[i];
312        }
313
314        self.support_vectors = Some(support_vectors);
315        self.support_labels = Some(support_labels);
316        self.alphas = Some(Array1::from_vec(support_alphas));
317
318        // Compute bias
319        self.compute_bias(&kernel_matrix, &alphas, y, &support_indices);
320
321        Ok(())
322    }
323
324    /// Compute error for a given sample
325    fn compute_error(
326        &self,
327        kernel_matrix: &Array2<f64>,
328        alphas: &Array1<f64>,
329        y: &Array1<i32>,
330        i: usize,
331    ) -> f64 {
332        let mut sum = self.bias;
333        for j in 0..alphas.len() {
334            if alphas[j] > 0.0 {
335                sum += alphas[j] * y[j] as f64 * kernel_matrix[[i, j]];
336            }
337        }
338        sum - y[i] as f64
339    }
340
341    /// Check KKT conditions
342    fn check_kkt(&self, alpha: f64, y: i32, error: f64) -> bool {
343        let y_error = y as f64 * error;
344
345        if alpha < 1e-5 {
346            y_error >= -self.params.tolerance
347        } else if alpha > self.params.c - 1e-5 {
348            y_error <= self.params.tolerance
349        } else {
350            (y_error).abs() <= self.params.tolerance
351        }
352    }
353
354    /// Select second alpha using maximum step heuristic
355    fn select_second_alpha(
356        &self,
357        i: usize,
358        n_samples: usize,
359        kernel_matrix: &Array2<f64>,
360        alphas: &Array1<f64>,
361        y: &Array1<i32>,
362    ) -> usize {
363        let ei = self.compute_error(kernel_matrix, alphas, y, i);
364        let mut max_step = 0.0;
365        let mut best_j = i;
366
367        for j in 0..n_samples {
368            if i == j {
369                continue;
370            }
371
372            let ej = self.compute_error(kernel_matrix, alphas, y, j);
373            let step = (ei - ej).abs();
374
375            if step > max_step {
376                max_step = step;
377                best_j = j;
378            }
379        }
380
381        best_j
382    }
383
384    /// Compute bounds for alpha updates
385    fn compute_bounds(&self, yi: i32, yj: i32, alpha_i: f64, alpha_j: f64) -> (f64, f64) {
386        if yi != yj {
387            let l = (alpha_j - alpha_i).max(0.0);
388            let h = (self.params.c + alpha_j - alpha_i).min(self.params.c);
389            (l, h)
390        } else {
391            let l = (alpha_i + alpha_j - self.params.c).max(0.0);
392            let h = (alpha_i + alpha_j).min(self.params.c);
393            (l, h)
394        }
395    }
396
397    /// Compute bias term
398    fn compute_bias(
399        &mut self,
400        kernel_matrix: &Array2<f64>,
401        alphas: &Array1<f64>,
402        y: &Array1<i32>,
403        support_indices: &[usize],
404    ) {
405        let mut bias_sum = 0.0;
406        let mut count = 0;
407
408        for &i in support_indices {
409            if alphas[i] > 1e-5 && alphas[i] < self.params.c - 1e-5 {
410                let mut sum = 0.0;
411                for j in 0..alphas.len() {
412                    if alphas[j] > 1e-5 {
413                        sum += alphas[j] * y[j] as f64 * kernel_matrix[[i, j]];
414                    }
415                }
416                bias_sum += y[i] as f64 - sum;
417                count += 1;
418            }
419        }
420
421        self.bias = if count > 0 {
422            bias_sum / count as f64
423        } else {
424            0.0
425        };
426    }
427
428    /// Predict labels for new data
429    pub fn predict(&self, x: &Array2<f64>) -> Result<Array1<i32>, String> {
430        let support_vectors = self.support_vectors.as_ref().ok_or("Model not trained")?;
431        let support_labels = self.support_labels.as_ref().ok_or("Model not trained")?;
432        let alphas = self.alphas.as_ref().ok_or("Model not trained")?;
433
434        let n_samples = x.nrows();
435        let mut predictions = Array1::zeros(n_samples);
436
437        for i in 0..n_samples {
438            let mut score = self.bias;
439
440            for (j, sv) in support_vectors.rows().into_iter().enumerate() {
441                let kernel_val = self.kernel.compute(&x.row(i).to_owned(), &sv.to_owned());
442                score += alphas[j] * support_labels[j] as f64 * kernel_val;
443            }
444
445            predictions[i] = if score >= 0.0 { 1 } else { -1 };
446        }
447
448        Ok(predictions)
449    }
450
451    /// Get decision function values
452    pub fn decision_function(&self, x: &Array2<f64>) -> Result<Array1<f64>, String> {
453        let support_vectors = self.support_vectors.as_ref().ok_or("Model not trained")?;
454        let support_labels = self.support_labels.as_ref().ok_or("Model not trained")?;
455        let alphas = self.alphas.as_ref().ok_or("Model not trained")?;
456
457        let n_samples = x.nrows();
458        let mut scores = Array1::zeros(n_samples);
459
460        for i in 0..n_samples {
461            let mut score = self.bias;
462
463            for (j, sv) in support_vectors.rows().into_iter().enumerate() {
464                let kernel_val = self.kernel.compute(&x.row(i).to_owned(), &sv.to_owned());
465                score += alphas[j] * support_labels[j] as f64 * kernel_val;
466            }
467
468            scores[i] = score;
469        }
470
471        Ok(scores)
472    }
473
474    /// Get the number of support vectors
475    pub fn n_support_vectors(&self) -> usize {
476        self.support_vectors
477            .as_ref()
478            .map(|sv| sv.nrows())
479            .unwrap_or(0)
480    }
481}
482
483/// Quantum kernel ridge regression for comparison
484pub struct QuantumKernelRidge {
485    kernel: QuantumKernel,
486    alpha: f64,
487    training_data: Option<Array2<f64>>,
488    coefficients: Option<Array1<f64>>,
489}
490
491impl QuantumKernelRidge {
492    /// Create new quantum kernel ridge regression
493    pub fn new(feature_map: FeatureMapType, reps: usize, alpha: f64) -> Self {
494        Self {
495            kernel: QuantumKernel::new(feature_map, reps),
496            alpha,
497            training_data: None,
498            coefficients: None,
499        }
500    }
501
502    /// Fit the model
503    pub fn fit(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<(), String> {
504        // Compute kernel matrix
505        let mut k = self.kernel.compute_kernel_matrix(x);
506
507        // Add regularization to diagonal
508        let n = k.nrows();
509        for i in 0..n {
510            k[[i, i]] += self.alpha;
511        }
512
513        // Solve K * coefficients = y
514        // Using simple matrix inversion (in practice, use Cholesky decomposition)
515        match Self::solve_linear_system(&k, y) {
516            Ok(coeffs) => {
517                self.training_data = Some(x.clone());
518                self.coefficients = Some(coeffs);
519                Ok(())
520            }
521            Err(e) => Err(format!("Failed to solve linear system: {}", e)),
522        }
523    }
524
525    /// Simple linear system solver (placeholder)
526    fn solve_linear_system(a: &Array2<f64>, b: &Array1<f64>) -> Result<Array1<f64>, String> {
527        // This is a simplified implementation
528        // In practice, use proper linear algebra libraries
529        let n = a.nrows();
530        if n != b.len() {
531            return Err("Dimension mismatch".to_string());
532        }
533
534        // For now, return zeros
535        // TODO: Implement proper linear solver
536        Ok(Array1::zeros(n))
537    }
538
539    /// Predict values for new data
540    pub fn predict(&self, x: &Array2<f64>) -> Result<Array1<f64>, String> {
541        let training_data = self.training_data.as_ref().ok_or("Model not trained")?;
542        let coefficients = self.coefficients.as_ref().ok_or("Model not trained")?;
543
544        let n_samples = x.nrows();
545        let mut predictions = Array1::zeros(n_samples);
546
547        for i in 0..n_samples {
548            let mut sum = 0.0;
549            for (j, coeff) in coefficients.iter().enumerate() {
550                let kernel_val = self
551                    .kernel
552                    .compute(&x.row(i).to_owned(), &training_data.row(j).to_owned());
553                sum += coeff * kernel_val;
554            }
555            predictions[i] = sum;
556        }
557
558        Ok(predictions)
559    }
560}
561
562#[cfg(test)]
563mod tests {
564    use super::*;
565    use scirs2_core::ndarray::array;
566
567    #[test]
568    fn test_quantum_kernel_computation() {
569        let kernel = QuantumKernel::new(FeatureMapType::ZFeatureMap, 1);
570
571        let x1 = array![0.5, 0.5];
572        let x2 = array![0.5, 0.5];
573
574        let kernel_val = kernel.compute(&x1, &x2);
575        assert!((kernel_val - 1.0).abs() < 1e-6); // Same vectors should give 1
576
577        let x3 = array![0.0, 1.0];
578        let kernel_val2 = kernel.compute(&x1, &x3);
579        assert!(kernel_val2 < 1.0); // Different vectors should give < 1
580    }
581
582    #[test]
583    fn test_qsvm_basic() {
584        // Create simple linearly separable dataset
585        let x = array![[0.0, 0.0], [0.1, 0.1], [1.0, 1.0], [0.9, 0.9],];
586
587        let y = array![-1, -1, 1, 1];
588
589        let params = QSVMParams::default();
590        let mut qsvm = QSVM::new(params);
591
592        // Train
593        qsvm.fit(&x, &y).unwrap();
594
595        // Check that we have support vectors
596        assert!(qsvm.n_support_vectors() > 0);
597
598        // Predict on training data
599        let predictions = qsvm.predict(&x).unwrap();
600
601        // Should classify training data correctly
602        for i in 0..y.len() {
603            assert_eq!(predictions[i], y[i]);
604        }
605    }
606}