1use crate::error::{QuantumError, QuantumResult};
34use crate::gates::controlled::apply_cnot;
35use crate::gates::parametric::gate_ry;
36use crate::statevec::apply_1q::apply_1q_inplace;
37use crate::statevec::state::StateVector;
38
39#[derive(Debug, Clone)]
41pub struct TrainableKernelConfig {
42 pub n_features: usize,
44 pub iters: usize,
46 pub lr: f32,
48}
49
50impl TrainableKernelConfig {
51 pub fn new(n_features: usize, iters: usize, lr: f32) -> QuantumResult<Self> {
57 if n_features == 0 {
58 return Err(QuantumError::InvalidParameter {
59 name: "n_features".into(),
60 });
61 }
62 if iters == 0 {
63 return Err(QuantumError::InvalidParameter {
64 name: "iters".into(),
65 });
66 }
67 if !lr.is_finite() || lr <= 0.0 {
68 return Err(QuantumError::InvalidParameter { name: "lr".into() });
69 }
70 Ok(Self {
71 n_features,
72 iters,
73 lr,
74 })
75 }
76}
77
78#[derive(Debug, Clone)]
80pub struct TrainableKernel {
81 n_features: usize,
82 pub theta: Vec<f32>,
84}
85
86impl TrainableKernel {
87 #[must_use]
89 pub fn new(n_features: usize) -> Self {
90 Self {
91 n_features,
92 theta: vec![1.0_f32; n_features],
93 }
94 }
95
96 pub fn with_theta(n_features: usize, theta: Vec<f32>) -> QuantumResult<Self> {
101 if theta.len() != n_features {
102 return Err(QuantumError::DimensionMismatch {
103 expected: n_features,
104 got: theta.len(),
105 });
106 }
107 Ok(Self { n_features, theta })
108 }
109
110 #[must_use]
112 pub fn n_features(&self) -> usize {
113 self.n_features
114 }
115
116 fn embed_with(&self, x: &[f32], theta: &[f32]) -> QuantumResult<StateVector> {
118 if x.len() != self.n_features {
119 return Err(QuantumError::DimensionMismatch {
120 expected: self.n_features,
121 got: x.len(),
122 });
123 }
124 let n = self.n_features;
125 let mut sv = StateVector::new_zero_state(n)?;
126 for q in 0..n {
127 apply_1q_inplace(&mut sv, q, &gate_ry(theta[q] * x[q]))?;
128 }
129 for q in 0..n.saturating_sub(1) {
130 apply_cnot(&mut sv, q, q + 1)?;
131 }
132 Ok(sv)
133 }
134
135 pub fn kernel(&self, x: &[f32], y: &[f32]) -> QuantumResult<f32> {
140 self.kernel_with(x, y, &self.theta)
141 }
142
143 fn kernel_with(&self, x: &[f32], y: &[f32], theta: &[f32]) -> QuantumResult<f32> {
145 let psi_x = self.embed_with(x, theta)?;
146 let psi_y = self.embed_with(y, theta)?;
147 let ip = psi_x.inner_product(&psi_y)?;
148 Ok(ip.norm_sqr())
149 }
150
151 pub fn gram(&self, xs: &[Vec<f32>]) -> QuantumResult<Vec<Vec<f32>>> {
157 if xs.is_empty() {
158 return Err(QuantumError::EmptyInput);
159 }
160 let m = xs.len();
161 let mut mat = vec![vec![0.0_f32; m]; m];
162 for i in 0..m {
163 for j in i..m {
164 let k = self.kernel(&xs[i], &xs[j])?;
165 mat[i][j] = k;
166 mat[j][i] = k;
167 }
168 }
169 Ok(mat)
170 }
171
172 fn alignment_with(&self, xs: &[Vec<f32>], labels: &[f32], theta: &[f32]) -> QuantumResult<f32> {
175 let m = xs.len();
176 let mut k_dot_y = 0.0_f32;
177 let mut k_norm_sq = 0.0_f32;
178 let mut y_norm_sq = 0.0_f32;
179 for i in 0..m {
180 for j in 0..m {
181 let kij = self.kernel_with(&xs[i], &xs[j], theta)?;
182 let yij = labels[i] * labels[j];
183 k_dot_y += kij * yij;
184 k_norm_sq += kij * kij;
185 y_norm_sq += yij * yij;
186 }
187 }
188 let denom = (k_norm_sq.sqrt() * y_norm_sq.sqrt()).max(1e-12);
189 Ok(k_dot_y / denom)
190 }
191
192 pub fn alignment(&self, xs: &[Vec<f32>], labels: &[f32]) -> QuantumResult<f32> {
198 if xs.is_empty() {
199 return Err(QuantumError::EmptyInput);
200 }
201 if labels.len() != xs.len() {
202 return Err(QuantumError::DimensionMismatch {
203 expected: xs.len(),
204 got: labels.len(),
205 });
206 }
207 self.alignment_with(xs, labels, &self.theta)
208 }
209
210 pub fn train_alignment(
220 &mut self,
221 xs: &[Vec<f32>],
222 labels: &[f32],
223 cfg: &TrainableKernelConfig,
224 ) -> QuantumResult<Vec<f32>> {
225 if xs.is_empty() {
226 return Err(QuantumError::EmptyInput);
227 }
228 if labels.len() != xs.len() {
229 return Err(QuantumError::DimensionMismatch {
230 expected: xs.len(),
231 got: labels.len(),
232 });
233 }
234 let shift = std::f32::consts::FRAC_PI_2;
235 let mut history = Vec::with_capacity(cfg.iters + 1);
236 history.push(self.alignment_with(xs, labels, &self.theta)?);
237
238 for _ in 0..cfg.iters {
239 let mut grad = vec![0.0_f32; self.n_features];
240 for p in 0..self.n_features {
241 let mut t_plus = self.theta.clone();
242 let mut t_minus = self.theta.clone();
243 t_plus[p] += shift;
244 t_minus[p] -= shift;
245 let a_plus = self.alignment_with(xs, labels, &t_plus)?;
246 let a_minus = self.alignment_with(xs, labels, &t_minus)?;
247 grad[p] = 0.5 * (a_plus - a_minus);
248 }
249 for (t, g) in self.theta.iter_mut().zip(grad.iter()) {
251 *t += cfg.lr * g;
252 }
253 history.push(self.alignment_with(xs, labels, &self.theta)?);
254 }
255 Ok(history)
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn config_validation() {
265 assert!(TrainableKernelConfig::new(0, 5, 0.1).is_err());
266 assert!(TrainableKernelConfig::new(2, 0, 0.1).is_err());
267 assert!(TrainableKernelConfig::new(2, 5, 0.0).is_err());
268 assert!(TrainableKernelConfig::new(2, 5, 0.1).is_ok());
269 }
270
271 #[test]
272 fn self_kernel_is_one() {
273 let tk = TrainableKernel::new(2);
274 let x = vec![0.5_f32, 1.0];
275 let k = tk.kernel(&x, &x).expect("kernel");
276 assert!((k - 1.0).abs() < 1e-5, "k={k}");
277 }
278
279 #[test]
280 fn gram_symmetric_diag_one() {
281 let tk = TrainableKernel::new(2);
282 let xs = vec![vec![0.3_f32, 0.7], vec![1.0_f32, -0.5]];
283 let g = tk.gram(&xs).expect("gram");
284 assert!((g[0][0] - 1.0).abs() < 1e-5);
285 assert!((g[1][1] - 1.0).abs() < 1e-5);
286 assert!((g[0][1] - g[1][0]).abs() < 1e-6);
287 }
288
289 #[test]
290 fn alignment_in_unit_interval() {
291 let tk = TrainableKernel::new(2);
292 let xs = vec![
293 vec![0.2_f32, 0.1],
294 vec![0.3_f32, 0.2],
295 vec![2.0_f32, 1.8],
296 vec![2.1_f32, 1.9],
297 ];
298 let labels = vec![1.0_f32, 1.0, -1.0, -1.0];
299 let a = tk.alignment(&xs, &labels).expect("alignment");
300 assert!((-1.0..=1.0).contains(&a), "alignment={a}");
301 }
302
303 #[test]
304 fn training_increases_alignment() {
305 let xs = vec![
308 vec![0.1_f32, 0.0],
309 vec![0.0_f32, 0.1],
310 vec![1.5_f32, 1.6],
311 vec![1.6_f32, 1.5],
312 ];
313 let labels = vec![1.0_f32, 1.0, -1.0, -1.0];
314 let cfg = TrainableKernelConfig::new(2, 25, 0.4).expect("cfg");
315 let mut tk = TrainableKernel::new(2);
316 let history = tk.train_alignment(&xs, &labels, &cfg).expect("train");
317 let first = history[0];
318 let last = *history.last().expect("non-empty history");
319 assert!(last >= first - 1e-3, "alignment dropped: {first} → {last}");
320 assert!(last.is_finite());
321 }
322
323 #[test]
324 fn dimension_mismatch_rejected() {
325 let tk = TrainableKernel::new(3);
326 let x = vec![0.1_f32, 0.2]; let y = vec![0.1_f32, 0.2, 0.3];
328 assert!(tk.kernel(&x, &y).is_err());
329 }
330}