use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::Complex64;
use std::collections::HashMap;
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FeatureMapType {
ZFeatureMap,
ZZFeatureMap,
PauliFeatureMap,
AngleEncoding,
AmplitudeEncoding,
}
#[derive(Debug, Clone)]
pub struct QSVMParams {
pub feature_map: FeatureMapType,
pub reps: usize,
pub c: f64,
pub tolerance: f64,
pub num_qubits: usize,
pub depth: usize,
pub gamma: Option<f64>,
pub regularization: f64,
pub max_iterations: usize,
pub seed: Option<u64>,
}
impl Default for QSVMParams {
fn default() -> Self {
Self {
feature_map: FeatureMapType::ZZFeatureMap,
reps: 2,
c: 1.0,
tolerance: 1e-3,
num_qubits: 4,
depth: 2,
gamma: None,
regularization: 1.0,
max_iterations: 1000,
seed: None,
}
}
}
impl QSVMParams {
pub fn c_parameter(&self) -> f64 {
self.c
}
pub fn set_c_parameter(&mut self, value: f64) {
self.c = value;
self.regularization = value;
}
}
pub struct QuantumKernel {
feature_map: FeatureMapType,
reps: usize,
}
impl QuantumKernel {
pub fn new(feature_map: FeatureMapType, reps: usize) -> Self {
Self { feature_map, reps }
}
pub fn compute(&self, x1: &Array1<f64>, x2: &Array1<f64>) -> f64 {
match self.feature_map {
FeatureMapType::ZFeatureMap => self.z_feature_map_kernel(x1, x2),
FeatureMapType::ZZFeatureMap => self.zz_feature_map_kernel(x1, x2),
FeatureMapType::PauliFeatureMap => self.zz_feature_map_kernel(x1, x2), FeatureMapType::AngleEncoding => self.angle_encoding_kernel(x1, x2),
FeatureMapType::AmplitudeEncoding => self.amplitude_encoding_kernel(x1, x2),
}
}
fn z_feature_map_kernel(&self, x1: &Array1<f64>, x2: &Array1<f64>) -> f64 {
let n = x1.len();
let mut kernel_val = 1.0;
for _ in 0..self.reps {
for i in 0..n {
let phase_diff = (x1[i] - x2[i]) * PI;
kernel_val *= phase_diff.cos();
}
}
kernel_val
}
fn zz_feature_map_kernel(&self, x1: &Array1<f64>, x2: &Array1<f64>) -> f64 {
let n = x1.len();
let mut kernel_val = 1.0;
for rep in 0..self.reps {
for i in 0..n {
let phase_diff = (x1[i] - x2[i]) * PI * (rep + 1) as f64;
kernel_val *= phase_diff.cos();
}
for i in 0..n - 1 {
let interaction = (x1[i] - x2[i]) * (x1[i + 1] - x2[i + 1]) * PI;
kernel_val *= interaction.cos();
}
}
kernel_val
}
fn angle_encoding_kernel(&self, x1: &Array1<f64>, x2: &Array1<f64>) -> f64 {
let mut sum = 0.0;
for i in 0..x1.len() {
sum += (x1[i] - x2[i]).powi(2);
}
(-sum / 2.0).exp()
}
fn amplitude_encoding_kernel(&self, x1: &Array1<f64>, x2: &Array1<f64>) -> f64 {
let norm1 = x1.dot(x1).sqrt();
let norm2 = x2.dot(x2).sqrt();
if norm1 < 1e-10 || norm2 < 1e-10 {
return 0.0;
}
let x1_norm = x1 / norm1;
let x2_norm = x2 / norm2;
x1_norm.dot(&x2_norm).powi(2)
}
pub fn compute_kernel_matrix(&self, data: &Array2<f64>) -> Array2<f64> {
let n_samples = data.nrows();
let mut kernel_matrix = Array2::zeros((n_samples, n_samples));
for i in 0..n_samples {
for j in i..n_samples {
let kernel_val = self.compute(&data.row(i).to_owned(), &data.row(j).to_owned());
kernel_matrix[[i, j]] = kernel_val;
kernel_matrix[[j, i]] = kernel_val; }
}
kernel_matrix
}
}
pub struct QSVM {
params: QSVMParams,
kernel: QuantumKernel,
support_vectors: Option<Array2<f64>>,
support_labels: Option<Array1<i32>>,
alphas: Option<Array1<f64>>,
bias: f64,
kernel_matrix_cache: HashMap<(usize, usize), f64>,
}
impl QSVM {
pub fn new(params: QSVMParams) -> Self {
let kernel = QuantumKernel::new(params.feature_map, params.reps);
Self {
params,
kernel,
support_vectors: None,
support_labels: None,
alphas: None,
bias: 0.0,
kernel_matrix_cache: HashMap::new(),
}
}
pub fn fit(&mut self, x: &Array2<f64>, y: &Array1<i32>) -> Result<(), String> {
let n_samples = x.nrows();
for &label in y.iter() {
if label != -1 && label != 1 {
return Err("QSVM requires binary labels: -1 or 1".to_string());
}
}
let mut alphas = Array1::zeros(n_samples);
let kernel_matrix = self.kernel.compute_kernel_matrix(x);
let mut converged = false;
let mut iter = 0;
while !converged && iter < self.params.max_iterations {
let old_alphas = alphas.clone();
for i in 0..n_samples {
let ei = self.compute_error(&kernel_matrix, &alphas, y, i);
if !self.check_kkt(alphas[i], y[i], ei) {
let j = self.select_second_alpha(i, n_samples, &kernel_matrix, &alphas, y);
if i == j {
continue;
}
let ej = self.compute_error(&kernel_matrix, &alphas, y, j);
let alpha_i_old = alphas[i];
let alpha_j_old = alphas[j];
let (l, h) = self.compute_bounds(y[i], y[j], alphas[i], alphas[j]);
if (h - l).abs() < 1e-10 {
continue;
}
let eta =
kernel_matrix[[i, i]] + kernel_matrix[[j, j]] - 2.0 * kernel_matrix[[i, j]];
if eta <= 0.0 {
continue;
}
alphas[j] += y[j] as f64 * (ei - ej) / eta;
alphas[j] = alphas[j].clamp(l, h);
if (alphas[j] - alpha_j_old).abs() < 1e-5 {
continue;
}
alphas[i] += y[i] as f64 * y[j] as f64 * (alpha_j_old - alphas[j]);
}
}
let alpha_change: f64 = (&alphas - &old_alphas).mapv(|a| a.abs()).sum();
converged = alpha_change < self.params.tolerance;
iter += 1;
}
let mut support_indices = Vec::new();
let mut support_alphas = Vec::new();
for i in 0..n_samples {
if alphas[i] > 1e-5 {
support_indices.push(i);
support_alphas.push(alphas[i]);
}
}
if support_indices.is_empty() {
return Err("No support vectors found".to_string());
}
let n_support = support_indices.len();
let n_features = x.ncols();
let mut support_vectors = Array2::zeros((n_support, n_features));
let mut support_labels = Array1::zeros(n_support);
for (idx, &i) in support_indices.iter().enumerate() {
support_vectors.row_mut(idx).assign(&x.row(i));
support_labels[idx] = y[i];
}
self.support_vectors = Some(support_vectors);
self.support_labels = Some(support_labels);
self.alphas = Some(Array1::from_vec(support_alphas));
self.compute_bias(&kernel_matrix, &alphas, y, &support_indices);
Ok(())
}
fn compute_error(
&self,
kernel_matrix: &Array2<f64>,
alphas: &Array1<f64>,
y: &Array1<i32>,
i: usize,
) -> f64 {
let mut sum = self.bias;
for j in 0..alphas.len() {
if alphas[j] > 0.0 {
sum += alphas[j] * y[j] as f64 * kernel_matrix[[i, j]];
}
}
sum - y[i] as f64
}
fn check_kkt(&self, alpha: f64, y: i32, error: f64) -> bool {
let y_error = y as f64 * error;
if alpha < 1e-5 {
y_error >= -self.params.tolerance
} else if alpha > self.params.c - 1e-5 {
y_error <= self.params.tolerance
} else {
(y_error).abs() <= self.params.tolerance
}
}
fn select_second_alpha(
&self,
i: usize,
n_samples: usize,
kernel_matrix: &Array2<f64>,
alphas: &Array1<f64>,
y: &Array1<i32>,
) -> usize {
let ei = self.compute_error(kernel_matrix, alphas, y, i);
let mut max_step = 0.0;
let mut best_j = i;
for j in 0..n_samples {
if i == j {
continue;
}
let ej = self.compute_error(kernel_matrix, alphas, y, j);
let step = (ei - ej).abs();
if step > max_step {
max_step = step;
best_j = j;
}
}
best_j
}
fn compute_bounds(&self, yi: i32, yj: i32, alpha_i: f64, alpha_j: f64) -> (f64, f64) {
if yi != yj {
let l = (alpha_j - alpha_i).max(0.0);
let h = (self.params.c + alpha_j - alpha_i).min(self.params.c);
(l, h)
} else {
let l = (alpha_i + alpha_j - self.params.c).max(0.0);
let h = (alpha_i + alpha_j).min(self.params.c);
(l, h)
}
}
fn compute_bias(
&mut self,
kernel_matrix: &Array2<f64>,
alphas: &Array1<f64>,
y: &Array1<i32>,
support_indices: &[usize],
) {
let mut bias_sum = 0.0;
let mut count = 0;
for &i in support_indices {
if alphas[i] > 1e-5 && alphas[i] < self.params.c - 1e-5 {
let mut sum = 0.0;
for j in 0..alphas.len() {
if alphas[j] > 1e-5 {
sum += alphas[j] * y[j] as f64 * kernel_matrix[[i, j]];
}
}
bias_sum += y[i] as f64 - sum;
count += 1;
}
}
self.bias = if count > 0 {
bias_sum / count as f64
} else {
0.0
};
}
pub fn predict(&self, x: &Array2<f64>) -> Result<Array1<i32>, String> {
let support_vectors = self.support_vectors.as_ref().ok_or("Model not trained")?;
let support_labels = self.support_labels.as_ref().ok_or("Model not trained")?;
let alphas = self.alphas.as_ref().ok_or("Model not trained")?;
let n_samples = x.nrows();
let mut predictions = Array1::zeros(n_samples);
for i in 0..n_samples {
let mut score = self.bias;
for (j, sv) in support_vectors.rows().into_iter().enumerate() {
let kernel_val = self.kernel.compute(&x.row(i).to_owned(), &sv.to_owned());
score += alphas[j] * support_labels[j] as f64 * kernel_val;
}
predictions[i] = if score >= 0.0 { 1 } else { -1 };
}
Ok(predictions)
}
pub fn decision_function(&self, x: &Array2<f64>) -> Result<Array1<f64>, String> {
let support_vectors = self.support_vectors.as_ref().ok_or("Model not trained")?;
let support_labels = self.support_labels.as_ref().ok_or("Model not trained")?;
let alphas = self.alphas.as_ref().ok_or("Model not trained")?;
let n_samples = x.nrows();
let mut scores = Array1::zeros(n_samples);
for i in 0..n_samples {
let mut score = self.bias;
for (j, sv) in support_vectors.rows().into_iter().enumerate() {
let kernel_val = self.kernel.compute(&x.row(i).to_owned(), &sv.to_owned());
score += alphas[j] * support_labels[j] as f64 * kernel_val;
}
scores[i] = score;
}
Ok(scores)
}
pub fn n_support_vectors(&self) -> usize {
self.support_vectors
.as_ref()
.map(|sv| sv.nrows())
.unwrap_or(0)
}
}
pub struct QuantumKernelRidge {
kernel: QuantumKernel,
alpha: f64,
training_data: Option<Array2<f64>>,
coefficients: Option<Array1<f64>>,
}
impl QuantumKernelRidge {
pub fn new(feature_map: FeatureMapType, reps: usize, alpha: f64) -> Self {
Self {
kernel: QuantumKernel::new(feature_map, reps),
alpha,
training_data: None,
coefficients: None,
}
}
pub fn fit(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<(), String> {
let mut k = self.kernel.compute_kernel_matrix(x);
let n = k.nrows();
for i in 0..n {
k[[i, i]] += self.alpha;
}
match Self::solve_linear_system(&k, y) {
Ok(coeffs) => {
self.training_data = Some(x.clone());
self.coefficients = Some(coeffs);
Ok(())
}
Err(e) => Err(format!("Failed to solve linear system: {}", e)),
}
}
fn solve_linear_system(a: &Array2<f64>, b: &Array1<f64>) -> Result<Array1<f64>, String> {
let n = a.nrows();
if n == 0 {
return Err("Empty matrix".to_string());
}
if n != a.ncols() {
return Err(format!("Matrix is not square: {}×{}", n, a.ncols()));
}
if n != b.len() {
return Err(format!(
"Dimension mismatch: matrix is {}×{} but rhs has {} elements",
n,
n,
b.len()
));
}
let mut l = Array2::<f64>::zeros((n, n));
for i in 0..n {
let mut sum_sq = a[[i, i]];
for k in 0..i {
sum_sq -= l[[i, k]] * l[[i, k]];
}
if sum_sq <= 0.0 {
return Self::solve_gaussian_elimination(a, b);
}
l[[i, i]] = sum_sq.sqrt();
for j in (i + 1)..n {
let mut sum_prod = a[[j, i]];
for k in 0..i {
sum_prod -= l[[j, k]] * l[[i, k]];
}
l[[j, i]] = sum_prod / l[[i, i]];
}
}
let mut y = Array1::<f64>::zeros(n);
for i in 0..n {
let mut s = b[i];
for k in 0..i {
s -= l[[i, k]] * y[k];
}
let diag = l[[i, i]];
if diag.abs() < 1e-14 {
return Err(format!("Singular Cholesky factor at row {i}"));
}
y[i] = s / diag;
}
let mut x = Array1::<f64>::zeros(n);
for i in (0..n).rev() {
let mut s = y[i];
for k in (i + 1)..n {
s -= l[[k, i]] * x[k]; }
let diag = l[[i, i]];
if diag.abs() < 1e-14 {
return Err(format!("Singular Cholesky factor at column {i}"));
}
x[i] = s / diag;
}
Ok(x)
}
fn solve_gaussian_elimination(a: &Array2<f64>, b: &Array1<f64>) -> Result<Array1<f64>, String> {
let n = a.nrows();
let mut aug: Vec<Vec<f64>> = (0..n)
.map(|i| {
let mut row: Vec<f64> = (0..n).map(|j| a[[i, j]]).collect();
row.push(b[i]);
row
})
.collect();
for col in 0..n {
let pivot_row = (col..n)
.max_by(|&r1, &r2| {
aug[r1][col]
.abs()
.partial_cmp(&aug[r2][col].abs())
.unwrap_or(std::cmp::Ordering::Equal)
})
.ok_or("Empty column range during elimination")?;
aug.swap(col, pivot_row);
let pivot = aug[col][col];
if pivot.abs() < 1e-14 {
return Err(format!("Singular or near-singular matrix at column {col}"));
}
for row in (col + 1)..n {
let factor = aug[row][col] / pivot;
for j in col..=n {
let val = aug[col][j];
aug[row][j] -= factor * val;
}
}
}
let mut x = vec![0.0_f64; n];
for i in (0..n).rev() {
let mut s = aug[i][n];
for j in (i + 1)..n {
s -= aug[i][j] * x[j];
}
let diag = aug[i][i];
if diag.abs() < 1e-14 {
return Err(format!(
"Singular matrix during back-substitution at row {i}"
));
}
x[i] = s / diag;
}
Ok(Array1::from_vec(x))
}
pub fn predict(&self, x: &Array2<f64>) -> Result<Array1<f64>, String> {
let training_data = self.training_data.as_ref().ok_or("Model not trained")?;
let coefficients = self.coefficients.as_ref().ok_or("Model not trained")?;
let n_samples = x.nrows();
let mut predictions = Array1::zeros(n_samples);
for i in 0..n_samples {
let mut sum = 0.0;
for (j, coeff) in coefficients.iter().enumerate() {
let kernel_val = self
.kernel
.compute(&x.row(i).to_owned(), &training_data.row(j).to_owned());
sum += coeff * kernel_val;
}
predictions[i] = sum;
}
Ok(predictions)
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
#[test]
fn test_quantum_kernel_computation() {
let kernel = QuantumKernel::new(FeatureMapType::ZFeatureMap, 1);
let x1 = array![0.5, 0.5];
let x2 = array![0.5, 0.5];
let kernel_val = kernel.compute(&x1, &x2);
assert!((kernel_val - 1.0).abs() < 1e-6);
let x3 = array![0.0, 1.0];
let kernel_val2 = kernel.compute(&x1, &x3);
assert!(kernel_val2 < 1.0); }
#[test]
fn test_qsvm_basic() {
let x = array![[0.0, 0.0], [0.1, 0.1], [1.0, 1.0], [0.9, 0.9],];
let y = array![-1, -1, 1, 1];
let params = QSVMParams::default();
let mut qsvm = QSVM::new(params);
qsvm.fit(&x, &y).expect("fit should succeed");
assert!(qsvm.n_support_vectors() > 0);
let predictions = qsvm.predict(&x).expect("predict should succeed");
for i in 0..y.len() {
assert_eq!(predictions[i], y[i]);
}
}
}