Skip to main content

quantrs2_ml/
kernels.rs

1//! Quantum kernel methods for support vector machines and kernel PCA.
2//!
3//! Computes quantum kernel matrices by estimating the overlap
4//! ⟨φ(x)|φ(x′)⟩ between feature-map states encoded by parameterised
5//! quantum circuits, enabling quantum-enhanced SVMs and kernel regression.
6
7use crate::error::{MLError, Result};
8use quantrs2_circuit::builder::Simulator;
9use quantrs2_circuit::prelude::Circuit;
10use quantrs2_sim::statevector::StateVectorSimulator;
11use scirs2_core::ndarray::{Array1, Array2};
12
13/// Kernel method for quantum machine learning
14#[derive(Debug, Clone, Copy)]
15pub enum KernelMethod {
16    /// Linear kernel
17    Linear,
18
19    /// Polynomial kernel
20    Polynomial,
21
22    /// Radial basis function (RBF) kernel
23    RBF,
24
25    /// Quantum kernel
26    QuantumKernel,
27
28    /// Hybrid classical-quantum kernel
29    HybridKernel,
30}
31
32/// Kernel function for machine learning
33pub trait KernelFunction {
34    /// Computes the kernel value for two vectors
35    fn compute(&self, x1: &Array1<f64>, x2: &Array1<f64>) -> Result<f64>;
36
37    /// Computes the kernel matrix for a dataset
38    fn compute_matrix(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
39        let n = x.nrows();
40        let mut kernel_matrix = Array2::zeros((n, n));
41
42        for i in 0..n {
43            let x_i = x.row(i).to_owned();
44
45            for j in 0..=i {
46                let x_j = x.row(j).to_owned();
47
48                let k_ij = self.compute(&x_i, &x_j)?;
49                kernel_matrix[[i, j]] = k_ij;
50
51                if i != j {
52                    kernel_matrix[[j, i]] = k_ij; // Symmetric
53                }
54            }
55        }
56
57        Ok(kernel_matrix)
58    }
59}
60
61/// Linear kernel for classical machine learning
62#[derive(Debug, Clone)]
63pub struct LinearKernel;
64
65impl KernelFunction for LinearKernel {
66    fn compute(&self, x1: &Array1<f64>, x2: &Array1<f64>) -> Result<f64> {
67        if x1.len() != x2.len() {
68            return Err(MLError::InvalidParameter(format!(
69                "Vector dimensions mismatch: {} != {}",
70                x1.len(),
71                x2.len()
72            )));
73        }
74
75        let dot_product = x1.iter().zip(x2.iter()).map(|(&a, &b)| a * b).sum();
76
77        Ok(dot_product)
78    }
79}
80
81/// Polynomial kernel for classical machine learning
82#[derive(Debug, Clone)]
83pub struct PolynomialKernel {
84    /// Degree of the polynomial
85    pub degree: usize,
86
87    /// Coefficient
88    pub coef: f64,
89}
90
91impl PolynomialKernel {
92    /// Creates a new polynomial kernel
93    pub fn new(degree: usize, coef: f64) -> Self {
94        PolynomialKernel { degree, coef }
95    }
96}
97
98impl KernelFunction for PolynomialKernel {
99    fn compute(&self, x1: &Array1<f64>, x2: &Array1<f64>) -> Result<f64> {
100        if x1.len() != x2.len() {
101            return Err(MLError::InvalidParameter(format!(
102                "Vector dimensions mismatch: {} != {}",
103                x1.len(),
104                x2.len()
105            )));
106        }
107
108        let dot_product = x1.iter().zip(x2.iter()).map(|(&a, &b)| a * b).sum::<f64>();
109        let value = (dot_product + self.coef).powi(self.degree as i32);
110
111        Ok(value)
112    }
113}
114
115/// Radial basis function (RBF) kernel for classical machine learning
116#[derive(Debug, Clone)]
117pub struct RBFKernel {
118    /// Gamma parameter
119    pub gamma: f64,
120}
121
122impl RBFKernel {
123    /// Creates a new RBF kernel
124    pub fn new(gamma: f64) -> Self {
125        RBFKernel { gamma }
126    }
127}
128
129impl KernelFunction for RBFKernel {
130    fn compute(&self, x1: &Array1<f64>, x2: &Array1<f64>) -> Result<f64> {
131        if x1.len() != x2.len() {
132            return Err(MLError::InvalidParameter(format!(
133                "Vector dimensions mismatch: {} != {}",
134                x1.len(),
135                x2.len()
136            )));
137        }
138
139        let squared_distance = x1
140            .iter()
141            .zip(x2.iter())
142            .map(|(&a, &b)| (a - b).powi(2))
143            .sum::<f64>();
144
145        let value = (-self.gamma * squared_distance).exp();
146
147        Ok(value)
148    }
149}
150
151/// Quantum kernel for quantum machine learning
152#[derive(Debug, Clone)]
153pub struct QuantumKernel {
154    /// Number of qubits
155    pub num_qubits: usize,
156
157    /// Feature dimension
158    pub feature_dim: usize,
159
160    /// Number of measurements to estimate the kernel
161    pub num_measurements: usize,
162}
163
164impl QuantumKernel {
165    /// Creates a new quantum kernel
166    pub fn new(num_qubits: usize, feature_dim: usize) -> Self {
167        QuantumKernel {
168            num_qubits,
169            feature_dim,
170            num_measurements: 1000,
171        }
172    }
173
174    /// Sets the number of measurements to estimate the kernel
175    pub fn with_measurements(mut self, num_measurements: usize) -> Self {
176        self.num_measurements = num_measurements;
177        self
178    }
179
180    /// Encodes a feature vector into a quantum circuit
181    fn encode_features<const N: usize>(
182        &self,
183        features: &Array1<f64>,
184        circuit: &mut Circuit<N>,
185    ) -> Result<()> {
186        // This is a simplified implementation
187        // In a real system, this would use more sophisticated feature encoding
188
189        for i in 0..N.min(features.len()) {
190            let angle = features[i] * std::f64::consts::PI;
191            circuit.ry(i, angle)?;
192        }
193
194        Ok(())
195    }
196
197    /// Prepares a quantum circuit for kernel estimation
198    fn prepare_kernel_circuit<const N: usize>(
199        &self,
200        x1: &Array1<f64>,
201        x2: &Array1<f64>,
202    ) -> Result<Circuit<N>> {
203        let mut circuit = Circuit::<N>::new();
204
205        // Apply Hadamard to all qubits
206        for i in 0..N.min(self.num_qubits) {
207            circuit.h(i)?;
208        }
209
210        // Encode the first feature vector
211        self.encode_features(x1, &mut circuit)?;
212
213        // Apply X gates as separators
214        for i in 0..N.min(self.num_qubits) {
215            circuit.x(i)?;
216        }
217
218        // Encode the second feature vector
219        self.encode_features(x2, &mut circuit)?;
220
221        // Apply Hadamard gates again
222        for i in 0..N.min(self.num_qubits) {
223            circuit.h(i)?;
224        }
225
226        Ok(circuit)
227    }
228}
229
230impl QuantumKernel {
231    /// Builds the kernel-estimation circuit on an `N`-qubit register, runs it
232    /// on the real state-vector simulator, and returns the fidelity kernel
233    /// value `|⟨0|U|0⟩|^2` where `U` is `H ⋅ encode(x2) ⋅ X ⋅ encode(x1) ⋅ H`
234    /// (the standard SWAP-test-free construction: since `H` is self-inverse
235    /// and the `X` gates only relabel which computational-basis amplitude
236    /// carries the overlap, the probability of observing all zeros encodes
237    /// the squared overlap between the two feature-map states).
238    fn compute_sized<const N: usize>(&self, x1: &Array1<f64>, x2: &Array1<f64>) -> Result<f64> {
239        let circuit = self.prepare_kernel_circuit::<N>(x1, x2)?;
240        let simulator = StateVectorSimulator::new();
241        let register = simulator.run(&circuit)?;
242
243        // Probability amplitude of the all-zeros computational basis state.
244        let amplitude_zero = register.amplitudes()[0];
245        Ok(amplitude_zero.norm_sqr().clamp(0.0, 1.0))
246    }
247}
248
249impl KernelFunction for QuantumKernel {
250    fn compute(&self, x1: &Array1<f64>, x2: &Array1<f64>) -> Result<f64> {
251        if x1.len() != x2.len() {
252            return Err(MLError::InvalidParameter(format!(
253                "Vector dimensions mismatch: {} != {}",
254                x1.len(),
255                x2.len()
256            )));
257        }
258
259        if x1.len() != self.feature_dim {
260            return Err(MLError::InvalidParameter(format!(
261                "Feature dimension mismatch: {} != {}",
262                x1.len(),
263                self.feature_dim
264            )));
265        }
266
267        // Estimate the kernel value by actually simulating the quantum
268        // feature-map circuit (prepare_kernel_circuit/encode_features),
269        // dispatching to the smallest supported register that can hold
270        // `num_qubits`, matching the pattern used by QuantumNeuralNetwork.
271        match self.num_qubits {
272            0 => Err(MLError::InvalidConfiguration(
273                "QuantumKernel requires at least one qubit".to_string(),
274            )),
275            1..=2 => self.compute_sized::<2>(x1, x2),
276            3..=4 => self.compute_sized::<4>(x1, x2),
277            5..=8 => self.compute_sized::<8>(x1, x2),
278            9..=16 => self.compute_sized::<16>(x1, x2),
279            n => Err(MLError::NotSupported(format!(
280                "Quantum kernel estimation supports at most 16 qubits on the \
281                 state-vector backend, got {n}"
282            ))),
283        }
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290    use scirs2_core::ndarray::array;
291
292    #[test]
293    fn test_quantum_kernel_exploration() {
294        let kernel = QuantumKernel::new(2, 2);
295        let x = array![0.3, 0.5];
296        let y = array![0.3, 0.5];
297        let z = array![0.9, 0.1];
298
299        let k_xx = kernel.compute(&x, &x).expect("compute self");
300        let k_xy = kernel.compute(&x, &y).expect("compute identical");
301        let k_xz = kernel.compute(&x, &z).expect("compute distinct");
302        let k_yx = kernel.compute(&y, &x).expect("compute swapped");
303        eprintln!("k_xx={k_xx} k_xy={k_xy} k_xz={k_xz} k_yx={k_yx}");
304    }
305}