sklears_kernel_approximation/
quantum_kernel_methods.rs1use scirs2_core::ndarray::{Array1, Array2};
14use scirs2_core::random::essentials::Normal;
15use scirs2_core::random::thread_rng;
16use serde::{Deserialize, Serialize};
17use sklears_core::{
18 error::{Result, SklearsError},
19 prelude::{Fit, Transform},
20 traits::{Estimator, Trained, Untrained},
21 types::Float,
22};
23use std::f64::consts::PI;
24use std::marker::PhantomData;
25
26#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
28pub enum QuantumFeatureMap {
29 PauliZ,
31 PauliZZ,
33 GeneralPauli,
35 AmplitudeEncoding,
37 HamiltonianEvolution,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct QuantumKernelConfig {
44 pub feature_map: QuantumFeatureMap,
46 pub n_qubits: usize,
48 pub circuit_depth: usize,
50 pub entanglement: EntanglementPattern,
52 pub n_samples: usize,
54}
55
56impl Default for QuantumKernelConfig {
57 fn default() -> Self {
58 Self {
59 feature_map: QuantumFeatureMap::PauliZZ,
60 n_qubits: 4,
61 circuit_depth: 2,
62 entanglement: EntanglementPattern::Linear,
63 n_samples: 1000,
64 }
65 }
66}
67
68#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
70pub enum EntanglementPattern {
71 None,
73 Linear,
75 Circular,
77 AllToAll,
79}
80
81#[derive(Debug, Clone)]
115pub struct QuantumKernelApproximation<State = Untrained> {
116 config: QuantumKernelConfig,
117
118 x_train: Option<Array2<Float>>,
120 feature_basis: Option<Array2<Float>>,
121
122 _state: PhantomData<State>,
123}
124
125impl<State> QuantumKernelApproximation<State> {
127 fn simulate_feature_map(&self, x: &Array1<Float>) -> Vec<Float> {
130 match self.config.feature_map {
131 QuantumFeatureMap::PauliZ => self.pauli_z_feature_map(x),
132 QuantumFeatureMap::PauliZZ => self.pauli_zz_feature_map(x),
133 QuantumFeatureMap::GeneralPauli => self.general_pauli_feature_map(x),
134 QuantumFeatureMap::AmplitudeEncoding => self.amplitude_encoding_feature_map(x),
135 QuantumFeatureMap::HamiltonianEvolution => self.hamiltonian_evolution_feature_map(x),
136 }
137 }
138
139 fn pauli_z_feature_map(&self, x: &Array1<Float>) -> Vec<Float> {
141 let dim = 1 << self.config.n_qubits; let mut amplitudes = vec![0.0; dim];
143
144 amplitudes[0] = 1.0;
146
147 for depth in 0..self.config.circuit_depth {
149 let mut new_amplitudes = amplitudes.clone();
150
151 for qubit in 0..self.config.n_qubits.min(x.len()) {
152 let feature_idx = (qubit + depth * self.config.n_qubits) % x.len();
153 let angle = x[feature_idx];
154
155 for state in 0..dim {
157 if (state >> qubit) & 1 == 1 {
158 new_amplitudes[state] *= (-angle).cos();
160 } else {
161 new_amplitudes[state] *= angle.cos();
163 }
164 }
165 }
166
167 amplitudes = new_amplitudes;
168 }
169
170 amplitudes
171 }
172
173 fn pauli_zz_feature_map(&self, x: &Array1<Float>) -> Vec<Float> {
175 let dim = 1 << self.config.n_qubits;
176 let mut amplitudes = vec![0.0; dim];
177 amplitudes[0] = 1.0;
178
179 for _depth in 0..self.config.circuit_depth {
180 let mut new_amplitudes = amplitudes.clone();
181
182 let pairs = self.get_entangling_pairs();
184 for (q1, q2) in pairs {
185 if q1 < x.len() && q2 < x.len() {
186 let angle = (PI - x[q1]) * (PI - x[q2]);
187
188 for state in 0..dim {
189 let bit1 = (state >> q1) & 1;
190 let bit2 = (state >> q2) & 1;
191
192 let phase = if bit1 == bit2 { 1.0 } else { -1.0 };
194 new_amplitudes[state] *= phase * angle.cos();
195 }
196 }
197 }
198
199 amplitudes = new_amplitudes;
200 }
201
202 let norm: Float = amplitudes.iter().map(|a| a * a).sum::<Float>().sqrt();
204 if norm > 1e-10 {
205 amplitudes.iter_mut().for_each(|a| *a /= norm);
206 }
207
208 amplitudes
209 }
210
211 fn general_pauli_feature_map(&self, x: &Array1<Float>) -> Vec<Float> {
213 let dim = 1 << self.config.n_qubits;
214 let mut amplitudes = vec![0.0; dim];
215 amplitudes[0] = 1.0;
216
217 for depth in 0..self.config.circuit_depth {
218 for qubit in 0..self.config.n_qubits.min(x.len()) {
219 let feature_idx = (qubit + depth * self.config.n_qubits) % x.len();
220 let angle = x[feature_idx];
221
222 let cos_half = (angle / 2.0).cos();
224 let sin_half = (angle / 2.0).sin();
225
226 let mut new_amplitudes = vec![0.0; dim];
227 for state in 0..dim {
228 let flipped_state = state ^ (1 << qubit);
229
230 new_amplitudes[state] += amplitudes[state] * cos_half;
231 new_amplitudes[state] += amplitudes[flipped_state] * sin_half;
232 }
233
234 amplitudes = new_amplitudes;
235 }
236 }
237
238 let norm: Float = amplitudes.iter().map(|a| a * a).sum::<Float>().sqrt();
240 if norm > 1e-10 {
241 amplitudes.iter_mut().for_each(|a| *a /= norm);
242 }
243
244 amplitudes
245 }
246
247 fn amplitude_encoding_feature_map(&self, x: &Array1<Float>) -> Vec<Float> {
249 let dim = 1 << self.config.n_qubits;
250 let mut amplitudes = vec![0.0; dim];
251
252 let norm: Float = x.iter().map(|v| v * v).sum::<Float>().sqrt().max(1e-10);
254
255 for (i, &val) in x.iter().enumerate().take(dim) {
256 amplitudes[i] = val / norm;
257 }
258
259 amplitudes
260 }
261
262 fn hamiltonian_evolution_feature_map(&self, x: &Array1<Float>) -> Vec<Float> {
264 let dim = 1 << self.config.n_qubits;
267 let mut amplitudes = vec![0.0; dim];
268 amplitudes[0] = 1.0;
269
270 let evolution_time = 1.0;
271
272 for state in 0..dim {
273 let mut energy = 0.0;
274
275 for qubit in 0..self.config.n_qubits.min(x.len()) {
277 let bit = (state >> qubit) & 1;
278 let z_eigenvalue = if bit == 1 { -1.0 } else { 1.0 };
279 energy += x[qubit] * z_eigenvalue;
280 }
281
282 let pairs = self.get_entangling_pairs();
284 for (q1, q2) in pairs.iter().take(3) {
285 if *q1 < x.len() && *q2 < x.len() {
287 let bit1 = (state >> q1) & 1;
288 let bit2 = (state >> q2) & 1;
289 let zz_eigenvalue = if bit1 == bit2 { 1.0 } else { -1.0 };
290 energy += 0.1 * x[*q1] * x[*q2] * zz_eigenvalue;
291 }
292 }
293
294 amplitudes[state] = (-energy * evolution_time).exp();
295 }
296
297 let norm: Float = amplitudes.iter().map(|a| a * a).sum::<Float>().sqrt();
299 if norm > 1e-10 {
300 amplitudes.iter_mut().for_each(|a| *a /= norm);
301 }
302
303 amplitudes
304 }
305
306 fn get_entangling_pairs(&self) -> Vec<(usize, usize)> {
308 let n = self.config.n_qubits;
309 match self.config.entanglement {
310 EntanglementPattern::None => vec![],
311 EntanglementPattern::Linear => (0..n.saturating_sub(1)).map(|i| (i, i + 1)).collect(),
312 EntanglementPattern::Circular => {
313 let mut pairs: Vec<_> = (0..n.saturating_sub(1)).map(|i| (i, i + 1)).collect();
314 if n > 2 {
315 pairs.push((n - 1, 0));
316 }
317 pairs
318 }
319 EntanglementPattern::AllToAll => {
320 let mut pairs = Vec::new();
321 for i in 0..n {
322 for j in (i + 1)..n {
323 pairs.push((i, j));
324 }
325 }
326 pairs
327 }
328 }
329 }
330}
331
332impl QuantumKernelApproximation<Untrained> {
333 pub fn new(config: QuantumKernelConfig) -> Self {
335 Self {
336 config,
337 x_train: None,
338 feature_basis: None,
339 _state: PhantomData,
340 }
341 }
342
343 pub fn with_qubits(n_qubits: usize) -> Self {
345 Self {
346 config: QuantumKernelConfig {
347 n_qubits,
348 ..Default::default()
349 },
350 x_train: None,
351 feature_basis: None,
352 _state: PhantomData,
353 }
354 }
355
356 pub fn feature_map(mut self, feature_map: QuantumFeatureMap) -> Self {
358 self.config.feature_map = feature_map;
359 self
360 }
361
362 pub fn circuit_depth(mut self, depth: usize) -> Self {
364 self.config.circuit_depth = depth;
365 self
366 }
367
368 pub fn entanglement(mut self, pattern: EntanglementPattern) -> Self {
370 self.config.entanglement = pattern;
371 self
372 }
373}
374
375impl Estimator for QuantumKernelApproximation<Untrained> {
376 type Config = QuantumKernelConfig;
377 type Error = SklearsError;
378 type Float = Float;
379
380 fn config(&self) -> &Self::Config {
381 &self.config
382 }
383}
384
385impl Fit<Array2<Float>, ()> for QuantumKernelApproximation<Untrained> {
386 type Fitted = QuantumKernelApproximation<Trained>;
387
388 fn fit(self, x: &Array2<Float>, _y: &()) -> Result<Self::Fitted> {
389 if x.nrows() == 0 || x.ncols() == 0 {
390 return Err(SklearsError::InvalidInput(
391 "Input array cannot be empty".to_string(),
392 ));
393 }
394
395 let x_train = x.clone();
396
397 let mut rng = thread_rng();
401 let normal = Normal::new(0.0, 1.0).unwrap();
402
403 let feature_dim = (1 << self.config.n_qubits).min(self.config.n_samples);
404 let feature_basis = Array2::from_shape_fn((x.ncols(), feature_dim), |_| rng.sample(normal));
405
406 Ok(QuantumKernelApproximation {
407 config: self.config,
408 x_train: Some(x_train),
409 feature_basis: Some(feature_basis),
410 _state: PhantomData,
411 })
412 }
413}
414
415impl Transform<Array2<Float>, Array2<Float>> for QuantumKernelApproximation<Trained> {
416 fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
417 let feature_basis = self.feature_basis.as_ref().unwrap();
418
419 if x.ncols() != feature_basis.nrows() {
420 return Err(SklearsError::InvalidInput(format!(
421 "Feature dimension mismatch: expected {}, got {}",
422 feature_basis.nrows(),
423 x.ncols()
424 )));
425 }
426
427 let n_samples = x.nrows();
428 let n_features = feature_basis.ncols();
429 let mut output = Array2::zeros((n_samples, n_features));
430
431 for i in 0..n_samples {
433 let sample = x.row(i).to_owned();
434
435 let quantum_features = self.simulate_feature_map(&sample);
437
438 let projection = sample.dot(feature_basis);
440
441 for j in 0..n_features {
442 let quantum_component = if j < quantum_features.len() {
444 quantum_features[j]
445 } else {
446 0.0
447 };
448
449 output[[i, j]] =
450 (projection[j] + quantum_component).cos() / (n_features as Float).sqrt();
451 }
452 }
453
454 Ok(output)
455 }
456}
457
458impl QuantumKernelApproximation<Trained> {
459 pub fn x_train(&self) -> &Array2<Float> {
461 self.x_train.as_ref().unwrap()
462 }
463
464 pub fn feature_basis(&self) -> &Array2<Float> {
466 self.feature_basis.as_ref().unwrap()
467 }
468
469 pub fn compute_kernel_matrix(&self, x: &Array2<Float>) -> Array2<Float> {
471 let n = x.nrows();
472 let mut kernel = Array2::zeros((n, n));
473
474 for i in 0..n {
475 for j in i..n {
476 let features_i = self.simulate_feature_map(&x.row(i).to_owned());
477 let features_j = self.simulate_feature_map(&x.row(j).to_owned());
478
479 let overlap: Float = features_i
481 .iter()
482 .zip(features_j.iter())
483 .map(|(a, b)| a * b)
484 .sum();
485
486 kernel[[i, j]] = overlap.abs();
487 kernel[[j, i]] = overlap.abs();
488 }
489 }
490
491 kernel
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use scirs2_core::ndarray::array;
499
500 #[test]
501 fn test_quantum_kernel_basic() {
502 let config = QuantumKernelConfig {
503 n_qubits: 3,
504 circuit_depth: 1,
505 feature_map: QuantumFeatureMap::PauliZ,
506 entanglement: EntanglementPattern::Linear,
507 n_samples: 50,
508 };
509
510 let qkernel = QuantumKernelApproximation::new(config);
511 let x = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
512
513 let fitted = qkernel.fit(&x, &()).unwrap();
514 let features = fitted.transform(&x).unwrap();
515
516 assert_eq!(features.nrows(), 2);
517 assert!(features.ncols() > 0);
518 }
519
520 #[test]
521 fn test_different_feature_maps() {
522 let feature_maps = vec![
523 QuantumFeatureMap::PauliZ,
524 QuantumFeatureMap::PauliZZ,
525 QuantumFeatureMap::GeneralPauli,
526 QuantumFeatureMap::AmplitudeEncoding,
527 QuantumFeatureMap::HamiltonianEvolution,
528 ];
529
530 let x = array![[1.0, 2.0], [3.0, 4.0]];
531
532 for feature_map in feature_maps {
533 let qkernel = QuantumKernelApproximation::with_qubits(2).feature_map(feature_map);
534
535 let fitted = qkernel.fit(&x, &()).unwrap();
536 let features = fitted.transform(&x).unwrap();
537
538 assert_eq!(features.nrows(), 2);
539 assert!(features.iter().all(|&v| v.is_finite()));
540 }
541 }
542
543 #[test]
544 fn test_different_entanglement_patterns() {
545 let patterns = vec![
546 EntanglementPattern::None,
547 EntanglementPattern::Linear,
548 EntanglementPattern::Circular,
549 EntanglementPattern::AllToAll,
550 ];
551
552 let x = array![[1.0, 2.0], [3.0, 4.0]];
553
554 for pattern in patterns {
555 let qkernel = QuantumKernelApproximation::with_qubits(2).entanglement(pattern);
556
557 let fitted = qkernel.fit(&x, &()).unwrap();
558 let features = fitted.transform(&x).unwrap();
559
560 assert_eq!(features.nrows(), 2);
561 }
562 }
563
564 #[test]
565 fn test_quantum_kernel_matrix() {
566 let config = QuantumKernelConfig {
567 n_qubits: 2,
568 circuit_depth: 1,
569 feature_map: QuantumFeatureMap::PauliZ,
570 entanglement: EntanglementPattern::None,
571 n_samples: 10,
572 };
573
574 let qkernel = QuantumKernelApproximation::new(config);
575 let x = array![[1.0, 2.0], [3.0, 4.0]];
576
577 let fitted = qkernel.fit(&x, &()).unwrap();
578 let kernel_matrix = fitted.compute_kernel_matrix(&x);
579
580 assert!((kernel_matrix[[0, 1]] - kernel_matrix[[1, 0]]).abs() < 1e-10);
582
583 assert!(kernel_matrix[[0, 0]] >= 0.0);
585 assert!(kernel_matrix[[1, 1]] >= 0.0);
586 }
587
588 #[test]
589 fn test_circuit_depth_effect() {
590 let x = array![[1.0, 2.0], [3.0, 4.0]];
591
592 for depth in 1..=3 {
593 let qkernel = QuantumKernelApproximation::with_qubits(2).circuit_depth(depth);
594
595 let fitted = qkernel.fit(&x, &()).unwrap();
596 let features = fitted.transform(&x).unwrap();
597
598 assert!(features.iter().all(|&v| v.is_finite()));
599 }
600 }
601
602 #[test]
603 fn test_empty_input_error() {
604 let qkernel = QuantumKernelApproximation::with_qubits(2);
605 let x_empty: Array2<Float> = Array2::zeros((0, 0));
606
607 assert!(qkernel.fit(&x_empty, &()).is_err());
608 }
609
610 #[test]
611 fn test_dimension_mismatch_error() {
612 let qkernel = QuantumKernelApproximation::with_qubits(2);
613 let x_train = array![[1.0, 2.0], [3.0, 4.0]];
614 let x_test = array![[1.0, 2.0, 3.0]];
615
616 let fitted = qkernel.fit(&x_train, &()).unwrap();
617 assert!(fitted.transform(&x_test).is_err());
618 }
619
620 #[test]
621 fn test_feature_map_simulation() {
622 let config = QuantumKernelConfig::default();
623 let qkernel = QuantumKernelApproximation::new(config);
624
625 let x = array![1.0, 2.0, 3.0, 4.0];
626 let features = qkernel.simulate_feature_map(&x);
627
628 assert!(features.len() > 0);
630 assert!(features.iter().all(|&a| a.is_finite()));
631 }
632}