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).expect("operation should succeed");
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
418 .feature_basis
419 .as_ref()
420 .expect("operation should succeed");
421
422 if x.ncols() != feature_basis.nrows() {
423 return Err(SklearsError::InvalidInput(format!(
424 "Feature dimension mismatch: expected {}, got {}",
425 feature_basis.nrows(),
426 x.ncols()
427 )));
428 }
429
430 let n_samples = x.nrows();
431 let n_features = feature_basis.ncols();
432 let mut output = Array2::zeros((n_samples, n_features));
433
434 for i in 0..n_samples {
436 let sample = x.row(i).to_owned();
437
438 let quantum_features = self.simulate_feature_map(&sample);
440
441 let projection = sample.dot(feature_basis);
443
444 for j in 0..n_features {
445 let quantum_component = if j < quantum_features.len() {
447 quantum_features[j]
448 } else {
449 0.0
450 };
451
452 output[[i, j]] =
453 (projection[j] + quantum_component).cos() / (n_features as Float).sqrt();
454 }
455 }
456
457 Ok(output)
458 }
459}
460
461impl QuantumKernelApproximation<Trained> {
462 pub fn x_train(&self) -> &Array2<Float> {
464 self.x_train.as_ref().expect("operation should succeed")
465 }
466
467 pub fn feature_basis(&self) -> &Array2<Float> {
469 self.feature_basis
470 .as_ref()
471 .expect("operation should succeed")
472 }
473
474 pub fn compute_kernel_matrix(&self, x: &Array2<Float>) -> Array2<Float> {
476 let n = x.nrows();
477 let mut kernel = Array2::zeros((n, n));
478
479 for i in 0..n {
480 for j in i..n {
481 let features_i = self.simulate_feature_map(&x.row(i).to_owned());
482 let features_j = self.simulate_feature_map(&x.row(j).to_owned());
483
484 let overlap: Float = features_i
486 .iter()
487 .zip(features_j.iter())
488 .map(|(a, b)| a * b)
489 .sum();
490
491 kernel[[i, j]] = overlap.abs();
492 kernel[[j, i]] = overlap.abs();
493 }
494 }
495
496 kernel
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503 use scirs2_core::ndarray::array;
504
505 #[test]
506 fn test_quantum_kernel_basic() {
507 let config = QuantumKernelConfig {
508 n_qubits: 3,
509 circuit_depth: 1,
510 feature_map: QuantumFeatureMap::PauliZ,
511 entanglement: EntanglementPattern::Linear,
512 n_samples: 50,
513 };
514
515 let qkernel = QuantumKernelApproximation::new(config);
516 let x = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
517
518 let fitted = qkernel.fit(&x, &()).expect("operation should succeed");
519 let features = fitted.transform(&x).expect("operation should succeed");
520
521 assert_eq!(features.nrows(), 2);
522 assert!(features.ncols() > 0);
523 }
524
525 #[test]
526 fn test_different_feature_maps() {
527 let feature_maps = vec![
528 QuantumFeatureMap::PauliZ,
529 QuantumFeatureMap::PauliZZ,
530 QuantumFeatureMap::GeneralPauli,
531 QuantumFeatureMap::AmplitudeEncoding,
532 QuantumFeatureMap::HamiltonianEvolution,
533 ];
534
535 let x = array![[1.0, 2.0], [3.0, 4.0]];
536
537 for feature_map in feature_maps {
538 let qkernel = QuantumKernelApproximation::with_qubits(2).feature_map(feature_map);
539
540 let fitted = qkernel.fit(&x, &()).expect("operation should succeed");
541 let features = fitted.transform(&x).expect("operation should succeed");
542
543 assert_eq!(features.nrows(), 2);
544 assert!(features.iter().all(|&v| v.is_finite()));
545 }
546 }
547
548 #[test]
549 fn test_different_entanglement_patterns() {
550 let patterns = vec![
551 EntanglementPattern::None,
552 EntanglementPattern::Linear,
553 EntanglementPattern::Circular,
554 EntanglementPattern::AllToAll,
555 ];
556
557 let x = array![[1.0, 2.0], [3.0, 4.0]];
558
559 for pattern in patterns {
560 let qkernel = QuantumKernelApproximation::with_qubits(2).entanglement(pattern);
561
562 let fitted = qkernel.fit(&x, &()).expect("operation should succeed");
563 let features = fitted.transform(&x).expect("operation should succeed");
564
565 assert_eq!(features.nrows(), 2);
566 }
567 }
568
569 #[test]
570 fn test_quantum_kernel_matrix() {
571 let config = QuantumKernelConfig {
572 n_qubits: 2,
573 circuit_depth: 1,
574 feature_map: QuantumFeatureMap::PauliZ,
575 entanglement: EntanglementPattern::None,
576 n_samples: 10,
577 };
578
579 let qkernel = QuantumKernelApproximation::new(config);
580 let x = array![[1.0, 2.0], [3.0, 4.0]];
581
582 let fitted = qkernel.fit(&x, &()).expect("operation should succeed");
583 let kernel_matrix = fitted.compute_kernel_matrix(&x);
584
585 assert!((kernel_matrix[[0, 1]] - kernel_matrix[[1, 0]]).abs() < 1e-10);
587
588 assert!(kernel_matrix[[0, 0]] >= 0.0);
590 assert!(kernel_matrix[[1, 1]] >= 0.0);
591 }
592
593 #[test]
594 fn test_circuit_depth_effect() {
595 let x = array![[1.0, 2.0], [3.0, 4.0]];
596
597 for depth in 1..=3 {
598 let qkernel = QuantumKernelApproximation::with_qubits(2).circuit_depth(depth);
599
600 let fitted = qkernel.fit(&x, &()).expect("operation should succeed");
601 let features = fitted.transform(&x).expect("operation should succeed");
602
603 assert!(features.iter().all(|&v| v.is_finite()));
604 }
605 }
606
607 #[test]
608 fn test_empty_input_error() {
609 let qkernel = QuantumKernelApproximation::with_qubits(2);
610 let x_empty: Array2<Float> = Array2::zeros((0, 0));
611
612 assert!(qkernel.fit(&x_empty, &()).is_err());
613 }
614
615 #[test]
616 fn test_dimension_mismatch_error() {
617 let qkernel = QuantumKernelApproximation::with_qubits(2);
618 let x_train = array![[1.0, 2.0], [3.0, 4.0]];
619 let x_test = array![[1.0, 2.0, 3.0]];
620
621 let fitted = qkernel
622 .fit(&x_train, &())
623 .expect("operation should succeed");
624 assert!(fitted.transform(&x_test).is_err());
625 }
626
627 #[test]
628 fn test_feature_map_simulation() {
629 let config = QuantumKernelConfig::default();
630 let qkernel = QuantumKernelApproximation::new(config);
631
632 let x = array![1.0, 2.0, 3.0, 4.0];
633 let features = qkernel.simulate_feature_map(&x);
634
635 assert!(features.len() > 0);
637 assert!(features.iter().all(|&a| a.is_finite()));
638 }
639}