oxicuda_quantum/kernel/
projected.rs1use crate::error::{QuantumError, QuantumResult};
34use crate::pauli::expval::expectation_value;
35use crate::pauli::hamiltonian::Hamiltonian;
36use crate::pauli::pauli_string::PauliOp;
37use crate::statevec::state::StateVector;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum PqkEmbedding {
43 Angle,
45 ZzFeatureMap { reps: usize },
47}
48
49#[derive(Debug, Clone)]
51pub struct ProjectedKernelConfig {
52 pub gamma: f32,
54 pub embedding: PqkEmbedding,
56}
57
58impl ProjectedKernelConfig {
59 pub fn new(gamma: f32, embedding: PqkEmbedding) -> QuantumResult<Self> {
64 if gamma.is_nan() || gamma <= 0.0 || !gamma.is_finite() {
65 return Err(QuantumError::InvalidParameter {
66 name: "gamma".into(),
67 });
68 }
69 Ok(Self { gamma, embedding })
70 }
71}
72
73pub fn local_pauli_features(data: &[f32], embedding: PqkEmbedding) -> QuantumResult<Vec<f32>> {
79 if data.is_empty() {
80 return Err(QuantumError::EmptyInput);
81 }
82 let sv: StateVector = match embedding {
83 PqkEmbedding::Angle => crate::embedding::angle::angle_embedding(data)?,
84 PqkEmbedding::ZzFeatureMap { reps } => {
85 crate::embedding::zz_feature::zz_feature_map(data, reps.max(1))?
86 }
87 };
88 let n = sv.n_qubits;
89 let mut feats = Vec::with_capacity(3 * n);
90 for q in 0..n {
91 for p in [PauliOp::X, PauliOp::Y, PauliOp::Z] {
92 let mut ops = vec![PauliOp::I; n];
93 ops[q] = p;
94 let mut ham = Hamiltonian::new();
95 ham.add_term(1.0, ops);
96 let ev = expectation_value(&sv, &ham)?;
97 feats.push(ev);
98 }
99 }
100 Ok(feats)
101}
102
103pub fn projected_kernel(x: &[f32], y: &[f32], cfg: &ProjectedKernelConfig) -> QuantumResult<f32> {
109 if x.len() != y.len() {
110 return Err(QuantumError::DimensionMismatch {
111 expected: x.len(),
112 got: y.len(),
113 });
114 }
115 let fx = local_pauli_features(x, cfg.embedding)?;
116 let fy = local_pauli_features(y, cfg.embedding)?;
117 let sq_dist: f32 = fx
118 .iter()
119 .zip(fy.iter())
120 .map(|(a, b)| {
121 let d = a - b;
122 d * d
123 })
124 .sum();
125 Ok((-cfg.gamma * sq_dist).exp())
126}
127
128pub fn projected_kernel_matrix(
137 xs: &[Vec<f32>],
138 cfg: &ProjectedKernelConfig,
139) -> QuantumResult<Vec<Vec<f32>>> {
140 if xs.is_empty() {
141 return Err(QuantumError::EmptyInput);
142 }
143 let m = xs.len();
144 let mut feats = Vec::with_capacity(m);
146 for x in xs {
147 feats.push(local_pauli_features(x, cfg.embedding)?);
148 }
149 let mut mat = vec![vec![0.0_f32; m]; m];
150 for i in 0..m {
151 for j in i..m {
152 let sq_dist: f32 = feats[i]
153 .iter()
154 .zip(feats[j].iter())
155 .map(|(a, b)| {
156 let d = a - b;
157 d * d
158 })
159 .sum();
160 let k = (-cfg.gamma * sq_dist).exp();
161 mat[i][j] = k;
162 mat[j][i] = k;
163 }
164 }
165 Ok(mat)
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn config_rejects_bad_gamma() {
174 assert!(ProjectedKernelConfig::new(0.0, PqkEmbedding::Angle).is_err());
175 assert!(ProjectedKernelConfig::new(-1.0, PqkEmbedding::Angle).is_err());
176 assert!(ProjectedKernelConfig::new(1.0, PqkEmbedding::Angle).is_ok());
177 }
178
179 #[test]
180 fn self_kernel_is_one() {
181 let cfg = ProjectedKernelConfig::new(1.0, PqkEmbedding::Angle).expect("cfg");
182 let x = vec![0.5_f32, 1.2, 0.3];
183 let k = projected_kernel(&x, &x, &cfg).expect("kernel");
184 assert!((k - 1.0).abs() < 1e-6, "k={k}");
185 }
186
187 #[test]
188 fn kernel_in_unit_interval_and_decreases_with_distance() {
189 let cfg = ProjectedKernelConfig::new(2.0, PqkEmbedding::Angle).expect("cfg");
190 let x = vec![0.1_f32, 0.2];
191 let near = vec![0.15_f32, 0.22];
192 let far = vec![3.0_f32, -2.5];
193 let k_near = projected_kernel(&x, &near, &cfg).expect("k_near");
194 let k_far = projected_kernel(&x, &far, &cfg).expect("k_far");
195 assert!((0.0..=1.0).contains(&k_near), "k_near={k_near}");
196 assert!((0.0..=1.0).contains(&k_far), "k_far={k_far}");
197 assert!(k_near > k_far, "near {k_near} should exceed far {k_far}");
198 }
199
200 #[test]
201 fn gram_matrix_is_symmetric_psd_diag_one() {
202 let cfg = ProjectedKernelConfig::new(1.5, PqkEmbedding::Angle).expect("cfg");
203 let xs = vec![vec![0.3_f32, 0.7], vec![1.0_f32, -0.5], vec![-0.2_f32, 0.9]];
204 let mat = projected_kernel_matrix(&xs, &cfg).expect("matrix");
205 for (i, row) in mat.iter().enumerate() {
207 assert!((row[i] - 1.0).abs() < 1e-6);
208 for (j, &v) in row.iter().enumerate() {
209 assert!((v - mat[j][i]).abs() < 1e-6);
210 }
211 }
212 use crate::handle::LcgRng;
214 let mut rng = LcgRng::new(9);
215 for _ in 0..20 {
216 let v: Vec<f32> = (0..3).map(|_| rng.next_normal()).collect();
217 let mut quad = 0.0_f32;
218 for i in 0..3 {
219 for j in 0..3 {
220 quad += v[i] * mat[i][j] * v[j];
221 }
222 }
223 assert!(quad >= -1e-4, "quadratic form negative: {quad}");
224 }
225 }
226
227 #[test]
228 fn zz_embedding_variant_works() {
229 let cfg =
230 ProjectedKernelConfig::new(1.0, PqkEmbedding::ZzFeatureMap { reps: 1 }).expect("cfg");
231 let x = vec![0.4_f32, 0.8];
232 let y = vec![0.5_f32, 0.7];
233 let k = projected_kernel(&x, &y, &cfg).expect("k");
234 assert!((0.0..=1.0).contains(&k), "k={k}");
235 }
236}