use std::sync::Arc;
use crate::error::KernelError;
use crate::tensor_kernels::types::{LinearKernel, RbfKernel};
use crate::types::{Kernel, RbfKernelConfig};
use super::smo::SmoConfig;
use super::svc::SvcModel;
use super::svr::SvrModel;
fn linear_kernel() -> Arc<dyn Kernel> {
Arc::new(LinearKernel::new())
}
fn rbf_kernel(gamma: f64) -> Arc<dyn Kernel> {
Arc::new(RbfKernel::new(RbfKernelConfig::new(gamma)).expect("valid gamma"))
}
#[test]
fn test_linear_svc_binary_separable() {
let x_pos = [vec![1.0, 1.0], vec![2.0, 1.0], vec![1.0, 2.0]];
let x_neg = [vec![-1.0, -1.0], vec![-2.0, -1.0], vec![-1.0, -2.0]];
let x: Vec<Vec<f64>> = x_pos.iter().chain(x_neg.iter()).cloned().collect();
let y: Vec<i32> = vec![1, 1, 1, -1, -1, -1];
let model = SvcModel::new(linear_kernel(), 10.0).expect("valid C");
let fitted = model.fit(&x, &y).expect("fit should succeed");
for (xi, &yi) in x.iter().zip(y.iter()) {
let pred = fitted.predict(xi).expect("predict");
assert_eq!(
pred, yi,
"Linear SVC misclassified training point {:?}: got {}, expected {}",
xi, pred, yi
);
}
}
#[test]
fn test_rbf_svc_xor() {
let x = vec![
vec![1.0, 1.0],
vec![-1.0, -1.0],
vec![1.0, -1.0],
vec![-1.0, 1.0],
];
let y = vec![-1, -1, 1, 1];
let model = SvcModel::new(rbf_kernel(4.0), 100.0).expect("valid params");
let fitted = model.fit(&x, &y).expect("fit should succeed");
for (xi, &yi) in x.iter().zip(y.iter()) {
let pred = fitted.predict(xi).expect("predict");
assert_eq!(
pred, yi,
"RBF XOR SVC misclassified {:?}: got {}, expected {}",
xi, pred, yi
);
}
}
#[test]
fn test_svc_single_class_error() {
let x = vec![vec![1.0, 2.0], vec![3.0, 4.0], vec![5.0, 6.0]];
let y = vec![1, 1, 1];
let model = SvcModel::new(linear_kernel(), 1.0).expect("valid C");
let result = model.fit(&x, &y);
assert!(
result.is_err(),
"Expected error when all labels are the same"
);
assert!(
matches!(result, Err(KernelError::InvalidParameter { .. })),
"Expected InvalidParameter error, got: {:?}",
result
);
}
#[test]
fn test_svc_empty_data_error() {
let model = SvcModel::new(linear_kernel(), 1.0).expect("valid C");
let result = model.fit(&[], &[]);
assert!(result.is_err());
assert!(
matches!(result, Err(KernelError::DimensionMismatch { .. })),
"Expected DimensionMismatch, got {:?}",
result
);
}
#[test]
fn test_svc_negative_c_error() {
let result = SvcModel::new(linear_kernel(), -1.0);
assert!(result.is_err());
assert!(
matches!(result, Err(KernelError::InvalidParameter { .. })),
"Expected InvalidParameter, got {:?}",
result
);
}
#[test]
fn test_svc_zero_c_error() {
let result = SvcModel::new(linear_kernel(), 0.0);
assert!(result.is_err());
assert!(
matches!(result, Err(KernelError::InvalidParameter { .. })),
"Expected InvalidParameter for C=0, got {:?}",
result
);
}
#[test]
fn test_svc_kkt_conditions() {
let x = vec![
vec![2.0, 0.0],
vec![3.0, 0.0],
vec![-2.0, 0.0],
vec![-3.0, 0.0],
];
let y = vec![1, 1, -1, -1];
let tol = 1e-3;
let config = SmoConfig {
c: 1.0,
tol,
max_iter: 50_000,
..SmoConfig::default()
};
let model = SvcModel::new_with_config(linear_kernel(), config).expect("valid");
let fitted = model.fit(&x, &y).expect("fit");
for (xi, &yi) in x.iter().zip(y.iter()) {
let df = fitted.decision_function(xi).expect("decision function");
let functional_margin = (yi as f64) * df;
assert!(
functional_margin >= 1.0 - 2.0 * tol,
"KKT feasibility violated at {:?}: y*f(x) = {}, expected >= {}",
xi,
functional_margin,
1.0 - 2.0 * tol
);
}
}
#[test]
fn test_svc_multiclass_ovr() {
let x: Vec<Vec<f64>> = vec![
vec![5.0, 0.0],
vec![5.5, 0.5],
vec![4.5, -0.5],
vec![-5.0, 0.0],
vec![-5.5, 0.5],
vec![-4.5, -0.5],
vec![0.0, 5.0],
vec![0.5, 5.5],
vec![-0.5, 4.5],
];
let y: Vec<i32> = vec![0, 0, 0, 1, 1, 1, 2, 2, 2];
let model = SvcModel::new(linear_kernel(), 1.0).expect("valid C");
let fitted = model.fit(&x, &y).expect("fit 3-class");
for (xi, &yi) in x.iter().zip(y.iter()) {
let pred = fitted.predict(xi).expect("predict");
assert_eq!(
pred, yi,
"Multiclass SVC misclassified {:?}: got {}, expected {}",
xi, pred, yi
);
}
}
#[test]
fn test_svc_num_support_vectors() {
let x = vec![
vec![1.0, 0.0],
vec![2.0, 0.0],
vec![3.0, 0.0],
vec![-1.0, 0.0],
vec![-2.0, 0.0],
vec![-3.0, 0.0],
];
let y = vec![1, 1, 1, -1, -1, -1];
let model = SvcModel::new(linear_kernel(), 1.0).expect("valid C");
let fitted = model.fit(&x, &y).expect("fit");
let n_sv = fitted.num_support_vectors();
assert!(
n_sv <= 4,
"Expected at most 4 support vectors for well-separated linear data, got {}",
n_sv
);
assert!(
n_sv >= 1,
"Expected at least 1 support vector, got {}",
n_sv
);
}
#[test]
fn test_svc_decision_function_signs() {
let x = vec![vec![3.0], vec![4.0], vec![-3.0], vec![-4.0]];
let y = vec![1, 1, -1, -1];
let model = SvcModel::new(linear_kernel(), 10.0).expect("valid C");
let fitted = model.fit(&x, &y).expect("fit");
for (xi, &yi) in x.iter().zip(y.iter()) {
let df = fitted.decision_function(xi).expect("decision function");
let sign = if df >= 0.0 { 1_i32 } else { -1_i32 };
assert_eq!(
sign, yi,
"decision_function sign wrong at {:?}: df={}, expected label {}",
xi, df, yi
);
}
}
#[test]
fn test_svr_linear_recovery() {
let x_vals: Vec<f64> = (1..=10).map(|i| i as f64).collect();
let y_vals: Vec<f64> = x_vals.iter().map(|&xi| 2.0 * xi + 1.0).collect();
let x: Vec<Vec<f64>> = x_vals.iter().map(|&xi| vec![xi]).collect();
let model = SvrModel::new(linear_kernel(), 10.0, 0.1).expect("valid params");
let fitted = model.fit(&x, &y_vals).expect("fit linear");
let pred = fitted.predict(&[5.0]).expect("predict");
let expected = 11.0;
assert!(
(pred - expected).abs() < 1.5,
"SVR linear recovery: predicted {}, expected {} (within 1.5)",
pred,
expected
);
}
#[test]
fn test_svr_rbf_regression() {
let x: Vec<Vec<f64>> = vec![vec![-2.0], vec![-1.0], vec![0.0], vec![1.0], vec![2.0]];
let y: Vec<f64> = x.iter().map(|xi| xi[0] * xi[0]).collect();
let config = super::smo::SmoConfig {
c: 10.0,
tol: 1e-3,
max_iter: 50_000,
..Default::default()
};
let model = SvrModel::new_with_config(rbf_kernel(1.0), 0.5, config).expect("valid params");
let fitted = model.fit(&x, &y).expect("fit RBF SVR");
let preds = fitted.predict_batch(&x).expect("predict_batch");
let mae: f64 = preds
.iter()
.zip(y.iter())
.map(|(&p, &t)| (p - t).abs())
.sum::<f64>()
/ x.len() as f64;
assert!(
mae < 0.8,
"SVR RBF: training MAE = {} (expected < 0.8)",
mae
);
}
#[test]
fn test_svr_empty_error() {
let model = SvrModel::new(linear_kernel(), 1.0, 0.1).expect("valid params");
let result = model.fit(&[], &[]);
assert!(result.is_err(), "Expected error for empty training set");
assert!(
matches!(result, Err(KernelError::DimensionMismatch { .. })),
"Expected DimensionMismatch, got {:?}",
result
);
}
#[test]
fn test_svr_negative_c_error() {
let result = SvrModel::new(linear_kernel(), 0.0, 0.1);
assert!(result.is_err());
assert!(
matches!(result, Err(KernelError::InvalidParameter { .. })),
"Expected InvalidParameter for C=0, got {:?}",
result
);
}
#[test]
fn test_svr_predict_batch_shape() {
let x: Vec<Vec<f64>> = (0..5).map(|i| vec![i as f64]).collect();
let y: Vec<f64> = x.iter().map(|xi| xi[0] * 2.0).collect();
let model = SvrModel::new(linear_kernel(), 5.0, 0.1).expect("valid");
let fitted = model.fit(&x, &y).expect("fit");
let test_x: Vec<Vec<f64>> = (0..8).map(|i| vec![i as f64 * 0.5]).collect();
let preds = fitted.predict_batch(&test_x).expect("predict_batch");
assert_eq!(
preds.len(),
test_x.len(),
"predict_batch output length mismatch"
);
}
#[test]
fn test_smo_config_default() {
let cfg = SmoConfig::default();
assert_eq!(cfg.c, 1.0, "default C should be 1.0");
assert_eq!(cfg.tol, 1e-3, "default tol should be 1e-3");
assert_eq!(cfg.epsilon, 0.1, "default epsilon should be 0.1");
assert_eq!(cfg.max_iter, 10_000, "default max_iter should be 10000");
}
#[test]
fn test_smo_config_clone_debug() {
let cfg = SmoConfig {
c: 5.0,
tol: 1e-4,
epsilon: 0.5,
max_iter: 1000,
};
let cloned = cfg.clone();
assert_eq!(cloned.c, cfg.c);
let dbg = format!("{:?}", cfg);
assert!(dbg.contains("SmoConfig"));
}
#[test]
fn test_svc_predict_batch_shape() {
let x = vec![
vec![1.0, 0.0],
vec![0.0, 1.0],
vec![-1.0, 0.0],
vec![0.0, -1.0],
];
let y = vec![1, 1, -1, -1];
let model = SvcModel::new(linear_kernel(), 1.0).expect("valid C");
let fitted = model.fit(&x, &y).expect("fit");
let test_x = vec![vec![2.0, 1.0], vec![-2.0, -1.0], vec![0.5, 0.5]];
let preds = fitted.predict_batch(&test_x).expect("predict_batch");
assert_eq!(preds.len(), 3, "predict_batch output length mismatch");
}
#[test]
fn test_svc_mismatched_lengths_error() {
let x = vec![vec![1.0], vec![2.0], vec![3.0]];
let y = vec![1, -1];
let model = SvcModel::new(linear_kernel(), 1.0).expect("valid C");
let result = model.fit(&x, &y);
assert!(
matches!(result, Err(KernelError::DimensionMismatch { .. })),
"Expected DimensionMismatch, got {:?}",
result
);
}
#[test]
fn test_svr_negative_epsilon_error() {
let result = SvrModel::new(linear_kernel(), 1.0, -0.1);
assert!(result.is_err());
assert!(
matches!(result, Err(KernelError::InvalidParameter { .. })),
"Expected InvalidParameter for negative epsilon, got {:?}",
result
);
}
#[test]
fn test_svr_mismatched_lengths_error() {
let x = vec![vec![1.0], vec![2.0]];
let y = vec![1.0, 2.0, 3.0];
let model = SvrModel::new(linear_kernel(), 1.0, 0.1).expect("valid");
let result = model.fit(&x, &y);
assert!(
matches!(result, Err(KernelError::DimensionMismatch { .. })),
"Expected DimensionMismatch, got {:?}",
result
);
}