use std::sync::Arc;
use crate::error::{KernelError, Result};
use crate::types::Kernel;
use super::smo::SmoConfig;
struct SvrState {
alpha_pos: Vec<f64>,
alpha_neg: Vec<f64>,
b: f64,
grad_cache: Vec<f64>,
n: usize,
c: f64,
eps: f64,
tol: f64,
y: Vec<f64>,
kernel_matrix: Vec<Vec<f64>>,
}
impl SvrState {
fn regression_fn(&self, i: usize) -> f64 {
let mut val = 0.0_f64;
for j in 0..self.n {
let beta_j = self.alpha_pos[j] - self.alpha_neg[j];
if beta_j.abs() > 1e-12 {
val += beta_j * self.kernel_matrix[j][i];
}
}
val - self.b
}
fn refresh_grad(&mut self, k: usize) {
let i = k % self.n;
let fx = self.regression_fn(i);
if k < self.n {
self.grad_cache[k] = fx - self.y[i] + self.eps;
} else {
self.grad_cache[k] = self.y[i] + self.eps - fx;
}
}
fn alpha(&self, k: usize) -> f64 {
if k < self.n {
self.alpha_pos[k]
} else {
self.alpha_neg[k - self.n]
}
}
fn set_alpha(&mut self, k: usize, val: f64) {
if k < self.n {
self.alpha_pos[k] = val;
} else {
self.alpha_neg[k - self.n] = val;
}
}
fn sign(&self, k: usize) -> f64 {
if k < self.n {
1.0
} else {
-1.0
}
}
fn orig(&self, k: usize) -> usize {
k % self.n
}
fn k_aug(&self, k1: usize, k2: usize) -> f64 {
self.sign(k1) * self.sign(k2) * self.kernel_matrix[self.orig(k1)][self.orig(k2)]
}
fn is_non_bound(&self, k: usize) -> bool {
let a = self.alpha(k);
a > 0.0 && a < self.c
}
fn kkt_violated(&self, k: usize) -> bool {
let g = self.grad_cache[k];
let a = self.alpha(k);
(g < -self.tol && a < self.c) || (g > self.tol && a > 0.0)
}
fn take_step(&mut self, k1: usize, k2: usize) -> bool {
if k1 == k2 {
return false;
}
if k1 % self.n == k2 % self.n {
return false;
}
let a1 = self.alpha(k1);
let a2 = self.alpha(k2);
let g1 = self.grad_cache[k1];
let g2 = self.grad_cache[k2];
let k_aug11 = self.k_aug(k1, k1);
let k_aug12 = self.k_aug(k1, k2);
let k_aug22 = self.k_aug(k2, k2);
let s_aug = self.sign(k1) * self.sign(k2);
let (lo, hi) = if (s_aug - 1.0).abs() > 1e-10 {
(f64::max(0.0, a2 - a1), f64::min(self.c, self.c + a2 - a1))
} else {
(f64::max(0.0, a1 + a2 - self.c), f64::min(self.c, a1 + a2))
};
if lo >= hi {
return false;
}
let eta = k_aug11 + k_aug22 - 2.0 * k_aug12;
let a2_new = if eta > 1e-12 {
let a2_unc = a2 + self.sign(k2) * (g1 - g2) / eta;
a2_unc.max(lo).min(hi)
} else {
let gamma = a1 + s_aug * a2;
let a1_at_lo = gamma - s_aug * lo;
let a1_at_hi = gamma - s_aug * hi;
let f1_proxy = g1 + self.sign(k1);
let f2_proxy = g2 + self.sign(k2);
let lobj = -0.5 * k_aug11 * a1_at_lo * a1_at_lo
- 0.5 * k_aug22 * lo * lo
- s_aug * k_aug12 * a1_at_lo * lo
- self.sign(k1) * a1_at_lo * f1_proxy
- self.sign(k2) * lo * f2_proxy
+ a1_at_lo
+ lo;
let hobj = -0.5 * k_aug11 * a1_at_hi * a1_at_hi
- 0.5 * k_aug22 * hi * hi
- s_aug * k_aug12 * a1_at_hi * hi
- self.sign(k1) * a1_at_hi * f1_proxy
- self.sign(k2) * hi * f2_proxy
+ a1_at_hi
+ hi;
if lobj > hobj + 1e-12 {
lo
} else if hobj > lobj + 1e-12 {
hi
} else {
a2
}
};
if (a2_new - a2).abs() < 1e-5 * (a2_new + a2 + 1e-10) {
return false;
}
let a1_new = a1 + s_aug * (a2 - a2_new);
let b_old = self.b;
let b1_cand =
b_old + self.sign(k1) * (g1 + k_aug11 * (a1_new - a1) + k_aug12 * (a2_new - a2));
let b2_cand =
b_old + self.sign(k2) * (g2 + k_aug12 * (a1_new - a1) + k_aug22 * (a2_new - a2));
let b_new = if a1_new > 1e-8 * self.c && a1_new < self.c * (1.0 - 1e-8) {
b1_cand
} else if a2_new > 1e-8 * self.c && a2_new < self.c * (1.0 - 1e-8) {
b2_cand
} else {
(b1_cand + b2_cand) * 0.5
};
self.set_alpha(k1, a1_new);
self.set_alpha(k2, a2_new);
self.b = b_new;
let delta_b = b_new - b_old;
let delta_a1 = a1_new - a1;
let delta_a2 = a2_new - a2;
let n2 = 2 * self.n;
for j in 0..n2 {
if self.is_non_bound(j) {
let delta_f = self.sign(k1)
* delta_a1
* self.kernel_matrix[self.orig(k1)][self.orig(j)]
+ self.sign(k2) * delta_a2 * self.kernel_matrix[self.orig(k2)][self.orig(j)];
self.grad_cache[j] += self.sign(j) * (delta_f - delta_b);
}
}
self.refresh_grad(k1);
self.refresh_grad(k2);
true
}
fn examine(&mut self, k2: usize, random_offset: usize) -> bool {
if !self.is_non_bound(k2) {
self.refresh_grad(k2);
}
if !self.kkt_violated(k2) {
return false;
}
let n2 = 2 * self.n;
let g2 = self.grad_cache[k2];
let non_bound: Vec<usize> = (0..n2).filter(|&j| self.is_non_bound(j)).collect();
if non_bound.len() > 1 {
let mut best_k1 = None;
let mut best_diff = 0.0_f64;
for &j in &non_bound {
if j == k2 {
continue;
}
let diff = (self.grad_cache[j] - g2).abs();
if diff > best_diff {
best_diff = diff;
best_k1 = Some(j);
}
}
if let Some(k1) = best_k1 {
if self.take_step(k1, k2) {
return true;
}
}
}
if !non_bound.is_empty() {
let start = random_offset % non_bound.len();
for jj in 0..non_bound.len() {
let k1 = non_bound[(start + jj) % non_bound.len()];
if k1 == k2 {
continue;
}
if self.take_step(k1, k2) {
return true;
}
}
}
let start = random_offset % n2;
for jj in 0..n2 {
let k1 = (start + jj) % n2;
if k1 == k2 {
continue;
}
if self.take_step(k1, k2) {
return true;
}
}
false
}
}
fn smo_svr_direct(
x: &[Vec<f64>],
y: &[f64],
kernel: &Arc<dyn Kernel>,
config: &SmoConfig,
eps: f64,
) -> Result<(Vec<f64>, Vec<f64>, f64)> {
let n = x.len();
if n == 0 {
return Err(KernelError::DimensionMismatch {
expected: vec![1],
got: vec![0],
context: "smo_svr_direct: empty training set".to_string(),
});
}
let mut kernel_matrix = vec![vec![0.0_f64; n]; n];
for i in 0..n {
for j in i..n {
let k_val = kernel.compute(&x[i], &x[j])?;
kernel_matrix[i][j] = k_val;
kernel_matrix[j][i] = k_val;
}
}
let alpha_pos = vec![0.0_f64; n];
let alpha_neg = vec![0.0_f64; n];
let b = 0.0_f64;
let n2 = 2 * n;
let mut grad_cache = vec![0.0_f64; n2];
for k in 0..n {
grad_cache[k] = eps - y[k]; grad_cache[k + n] = y[k] + eps; }
let mut state = SvrState {
alpha_pos,
alpha_neg,
b,
grad_cache,
n,
c: config.c,
eps,
tol: config.tol,
y: y.to_vec(),
kernel_matrix,
};
let mut examine_all = true;
let mut total_passes = 0usize;
let mut random_offset: usize = 37;
loop {
let mut num_changed = 0usize;
let was_examine_all = examine_all;
if examine_all {
for k2 in 0..n2 {
if state.examine(k2, random_offset) {
num_changed += 1;
}
random_offset = random_offset
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407)
& 0xFFFF;
}
} else {
let non_bound: Vec<usize> = (0..n2).filter(|&j| state.is_non_bound(j)).collect();
for k2 in non_bound {
if state.examine(k2, random_offset) {
num_changed += 1;
}
random_offset = random_offset
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407)
& 0xFFFF;
}
}
total_passes += 1;
if total_passes > config.max_iter {
return Err(KernelError::ComputationError(format!(
"SVR SMO did not converge after {} passes (tol={}, C={}, ε={}). \
Consider increasing max_iter or adjusting hyperparameters.",
config.max_iter, config.tol, config.c, eps
)));
}
if was_examine_all && num_changed == 0 {
break;
}
if was_examine_all {
examine_all = false;
} else if num_changed == 0 {
examine_all = true;
}
}
Ok((state.alpha_pos, state.alpha_neg, state.b))
}
pub struct SvrModel {
kernel: Arc<dyn Kernel>,
config: SmoConfig,
pub epsilon: f64,
}
impl std::fmt::Debug for SvrModel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SvrModel")
.field("kernel", &self.kernel.name())
.field("C", &self.config.c)
.field("epsilon", &self.epsilon)
.finish()
}
}
impl SvrModel {
pub fn new(kernel: Arc<dyn Kernel>, c: f64, epsilon: f64) -> Result<Self> {
if c <= 0.0 {
return Err(KernelError::InvalidParameter {
parameter: "C".to_string(),
value: c.to_string(),
reason: "C must be strictly positive".to_string(),
});
}
if epsilon < 0.0 {
return Err(KernelError::InvalidParameter {
parameter: "epsilon".to_string(),
value: epsilon.to_string(),
reason: "epsilon must be non-negative".to_string(),
});
}
Ok(Self {
kernel,
config: SmoConfig {
c,
epsilon,
..SmoConfig::default()
},
epsilon,
})
}
pub fn new_with_config(
kernel: Arc<dyn Kernel>,
epsilon: f64,
config: SmoConfig,
) -> Result<Self> {
if config.c <= 0.0 {
return Err(KernelError::InvalidParameter {
parameter: "C".to_string(),
value: config.c.to_string(),
reason: "C must be strictly positive".to_string(),
});
}
if epsilon < 0.0 {
return Err(KernelError::InvalidParameter {
parameter: "epsilon".to_string(),
value: epsilon.to_string(),
reason: "epsilon must be non-negative".to_string(),
});
}
Ok(Self {
kernel,
config,
epsilon,
})
}
pub fn fit(&self, x: &[Vec<f64>], y: &[f64]) -> Result<SvrFitted> {
let n = x.len();
if n == 0 {
return Err(KernelError::DimensionMismatch {
expected: vec![1],
got: vec![0],
context: "SvrModel::fit: training set cannot be empty".to_string(),
});
}
if y.len() != n {
return Err(KernelError::DimensionMismatch {
expected: vec![n],
got: vec![y.len()],
context: "SvrModel::fit: y must have the same length as x".to_string(),
});
}
if n > 1 {
let d0 = x[0].len();
for (i, xi) in x.iter().enumerate().skip(1) {
if xi.len() != d0 {
return Err(KernelError::DimensionMismatch {
expected: vec![d0],
got: vec![xi.len()],
context: format!("SvrModel::fit: x[{}] has wrong dimension", i),
});
}
}
}
let (alpha_pos, alpha_neg, b) =
smo_svr_direct(x, y, &self.kernel, &self.config, self.epsilon)?;
let sv_threshold = 1e-8 * self.config.c;
let mut support_vectors = Vec::new();
let mut support_coefficients = Vec::new();
for i in 0..n {
let beta_i = alpha_pos[i] - alpha_neg[i];
if beta_i.abs() > sv_threshold {
support_vectors.push(x[i].clone());
support_coefficients.push(beta_i);
}
}
Ok(SvrFitted {
support_vectors,
support_coefficients,
bias: b,
kernel: Arc::clone(&self.kernel),
})
}
}
pub struct SvrFitted {
pub support_vectors: Vec<Vec<f64>>,
pub support_coefficients: Vec<f64>,
pub bias: f64,
kernel: Arc<dyn Kernel>,
}
impl std::fmt::Debug for SvrFitted {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SvrFitted")
.field("num_support_vectors", &self.support_vectors.len())
.field("bias", &self.bias)
.finish()
}
}
impl SvrFitted {
pub fn predict(&self, x: &[f64]) -> Result<f64> {
let mut val = 0.0_f64;
for (sv, &coef) in self
.support_vectors
.iter()
.zip(self.support_coefficients.iter())
{
val += coef * self.kernel.compute(sv, x)?;
}
Ok(val - self.bias)
}
pub fn predict_batch(&self, x: &[Vec<f64>]) -> Result<Vec<f64>> {
x.iter().map(|xi| self.predict(xi)).collect()
}
pub fn num_support_vectors(&self) -> usize {
self.support_vectors.len()
}
}