use crate::error::{QuantumError, QuantumResult};
use crate::gates::controlled::apply_cnot;
use crate::gates::parametric::gate_ry;
use crate::statevec::apply_1q::apply_1q_inplace;
use crate::statevec::state::StateVector;
#[derive(Debug, Clone)]
pub struct TrainableKernelConfig {
pub n_features: usize,
pub iters: usize,
pub lr: f32,
}
impl TrainableKernelConfig {
pub fn new(n_features: usize, iters: usize, lr: f32) -> QuantumResult<Self> {
if n_features == 0 {
return Err(QuantumError::InvalidParameter {
name: "n_features".into(),
});
}
if iters == 0 {
return Err(QuantumError::InvalidParameter {
name: "iters".into(),
});
}
if !lr.is_finite() || lr <= 0.0 {
return Err(QuantumError::InvalidParameter { name: "lr".into() });
}
Ok(Self {
n_features,
iters,
lr,
})
}
}
#[derive(Debug, Clone)]
pub struct TrainableKernel {
n_features: usize,
pub theta: Vec<f32>,
}
impl TrainableKernel {
#[must_use]
pub fn new(n_features: usize) -> Self {
Self {
n_features,
theta: vec![1.0_f32; n_features],
}
}
pub fn with_theta(n_features: usize, theta: Vec<f32>) -> QuantumResult<Self> {
if theta.len() != n_features {
return Err(QuantumError::DimensionMismatch {
expected: n_features,
got: theta.len(),
});
}
Ok(Self { n_features, theta })
}
#[must_use]
pub fn n_features(&self) -> usize {
self.n_features
}
fn embed_with(&self, x: &[f32], theta: &[f32]) -> QuantumResult<StateVector> {
if x.len() != self.n_features {
return Err(QuantumError::DimensionMismatch {
expected: self.n_features,
got: x.len(),
});
}
let n = self.n_features;
let mut sv = StateVector::new_zero_state(n)?;
for q in 0..n {
apply_1q_inplace(&mut sv, q, &gate_ry(theta[q] * x[q]))?;
}
for q in 0..n.saturating_sub(1) {
apply_cnot(&mut sv, q, q + 1)?;
}
Ok(sv)
}
pub fn kernel(&self, x: &[f32], y: &[f32]) -> QuantumResult<f32> {
self.kernel_with(x, y, &self.theta)
}
fn kernel_with(&self, x: &[f32], y: &[f32], theta: &[f32]) -> QuantumResult<f32> {
let psi_x = self.embed_with(x, theta)?;
let psi_y = self.embed_with(y, theta)?;
let ip = psi_x.inner_product(&psi_y)?;
Ok(ip.norm_sqr())
}
pub fn gram(&self, xs: &[Vec<f32>]) -> QuantumResult<Vec<Vec<f32>>> {
if xs.is_empty() {
return Err(QuantumError::EmptyInput);
}
let m = xs.len();
let mut mat = vec![vec![0.0_f32; m]; m];
for i in 0..m {
for j in i..m {
let k = self.kernel(&xs[i], &xs[j])?;
mat[i][j] = k;
mat[j][i] = k;
}
}
Ok(mat)
}
fn alignment_with(&self, xs: &[Vec<f32>], labels: &[f32], theta: &[f32]) -> QuantumResult<f32> {
let m = xs.len();
let mut k_dot_y = 0.0_f32;
let mut k_norm_sq = 0.0_f32;
let mut y_norm_sq = 0.0_f32;
for i in 0..m {
for j in 0..m {
let kij = self.kernel_with(&xs[i], &xs[j], theta)?;
let yij = labels[i] * labels[j];
k_dot_y += kij * yij;
k_norm_sq += kij * kij;
y_norm_sq += yij * yij;
}
}
let denom = (k_norm_sq.sqrt() * y_norm_sq.sqrt()).max(1e-12);
Ok(k_dot_y / denom)
}
pub fn alignment(&self, xs: &[Vec<f32>], labels: &[f32]) -> QuantumResult<f32> {
if xs.is_empty() {
return Err(QuantumError::EmptyInput);
}
if labels.len() != xs.len() {
return Err(QuantumError::DimensionMismatch {
expected: xs.len(),
got: labels.len(),
});
}
self.alignment_with(xs, labels, &self.theta)
}
pub fn train_alignment(
&mut self,
xs: &[Vec<f32>],
labels: &[f32],
cfg: &TrainableKernelConfig,
) -> QuantumResult<Vec<f32>> {
if xs.is_empty() {
return Err(QuantumError::EmptyInput);
}
if labels.len() != xs.len() {
return Err(QuantumError::DimensionMismatch {
expected: xs.len(),
got: labels.len(),
});
}
let shift = std::f32::consts::FRAC_PI_2;
let mut history = Vec::with_capacity(cfg.iters + 1);
history.push(self.alignment_with(xs, labels, &self.theta)?);
for _ in 0..cfg.iters {
let mut grad = vec![0.0_f32; self.n_features];
for p in 0..self.n_features {
let mut t_plus = self.theta.clone();
let mut t_minus = self.theta.clone();
t_plus[p] += shift;
t_minus[p] -= shift;
let a_plus = self.alignment_with(xs, labels, &t_plus)?;
let a_minus = self.alignment_with(xs, labels, &t_minus)?;
grad[p] = 0.5 * (a_plus - a_minus);
}
for (t, g) in self.theta.iter_mut().zip(grad.iter()) {
*t += cfg.lr * g;
}
history.push(self.alignment_with(xs, labels, &self.theta)?);
}
Ok(history)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_validation() {
assert!(TrainableKernelConfig::new(0, 5, 0.1).is_err());
assert!(TrainableKernelConfig::new(2, 0, 0.1).is_err());
assert!(TrainableKernelConfig::new(2, 5, 0.0).is_err());
assert!(TrainableKernelConfig::new(2, 5, 0.1).is_ok());
}
#[test]
fn self_kernel_is_one() {
let tk = TrainableKernel::new(2);
let x = vec![0.5_f32, 1.0];
let k = tk.kernel(&x, &x).expect("kernel");
assert!((k - 1.0).abs() < 1e-5, "k={k}");
}
#[test]
fn gram_symmetric_diag_one() {
let tk = TrainableKernel::new(2);
let xs = vec![vec![0.3_f32, 0.7], vec![1.0_f32, -0.5]];
let g = tk.gram(&xs).expect("gram");
assert!((g[0][0] - 1.0).abs() < 1e-5);
assert!((g[1][1] - 1.0).abs() < 1e-5);
assert!((g[0][1] - g[1][0]).abs() < 1e-6);
}
#[test]
fn alignment_in_unit_interval() {
let tk = TrainableKernel::new(2);
let xs = vec![
vec![0.2_f32, 0.1],
vec![0.3_f32, 0.2],
vec![2.0_f32, 1.8],
vec![2.1_f32, 1.9],
];
let labels = vec![1.0_f32, 1.0, -1.0, -1.0];
let a = tk.alignment(&xs, &labels).expect("alignment");
assert!((-1.0..=1.0).contains(&a), "alignment={a}");
}
#[test]
fn training_increases_alignment() {
let xs = vec![
vec![0.1_f32, 0.0],
vec![0.0_f32, 0.1],
vec![1.5_f32, 1.6],
vec![1.6_f32, 1.5],
];
let labels = vec![1.0_f32, 1.0, -1.0, -1.0];
let cfg = TrainableKernelConfig::new(2, 25, 0.4).expect("cfg");
let mut tk = TrainableKernel::new(2);
let history = tk.train_alignment(&xs, &labels, &cfg).expect("train");
let first = history[0];
let last = *history.last().expect("non-empty history");
assert!(last >= first - 1e-3, "alignment dropped: {first} → {last}");
assert!(last.is_finite());
}
#[test]
fn dimension_mismatch_rejected() {
let tk = TrainableKernel::new(3);
let x = vec![0.1_f32, 0.2]; let y = vec![0.1_f32, 0.2, 0.3];
assert!(tk.kernel(&x, &y).is_err());
}
}