use std::f64::consts::PI;
use scirs2_core::ndarray::{Array1, Array2, ArrayView2};
use scirs2_core::random::{
seeded_rng, Cauchy, Distribution, Normal, SeedableRng, StandardNormal, StudentT, Uniform,
};
use scirs2_linalg::{eigh, solve};
use crate::error::{Result, TransformError};
#[derive(Debug, Clone, PartialEq)]
pub enum ShiftInvariantKernel {
RBF {
gamma: f64,
},
Laplacian {
gamma: f64,
},
Cauchy {
gamma: f64,
},
Matern32 {
length_scale: f64,
},
Matern52 {
length_scale: f64,
},
}
impl ShiftInvariantKernel {
fn sample_frequencies(
&self,
n_samples: usize,
input_dim: usize,
seed: u64,
) -> std::result::Result<Array2<f64>, TransformError> {
let mut rng = seeded_rng(seed);
let total = n_samples * input_dim;
let mut data = Vec::with_capacity(total);
match self {
ShiftInvariantKernel::RBF { gamma } => {
let std_dev = (2.0 * gamma).sqrt();
let dist = Normal::new(0.0_f64, std_dev)
.map_err(|e| TransformError::ComputationError(e.to_string()))?;
for _ in 0..total {
data.push(dist.sample(&mut rng));
}
}
ShiftInvariantKernel::Laplacian { gamma } => {
let dist = Cauchy::new(0.0_f64, *gamma)
.map_err(|e| TransformError::ComputationError(e.to_string()))?;
for _ in 0..total {
let s = dist.sample(&mut rng);
data.push(s.max(-1e6).min(1e6));
}
}
ShiftInvariantKernel::Cauchy { gamma } => {
let b = 1.0 / gamma;
let udist = Uniform::new(-0.5_f64, 0.5_f64)
.map_err(|e| TransformError::ComputationError(e.to_string()))?;
for _ in 0..total {
let u: f64 = udist.sample(&mut rng);
let sign = if u >= 0.0 { 1.0 } else { -1.0 };
let val = -b * sign * (1.0 - 2.0 * u.abs()).ln();
data.push(val);
}
}
ShiftInvariantKernel::Matern32 { length_scale } => {
let scale = (6.0_f64).sqrt() / length_scale;
let dist = StudentT::new(3.0_f64)
.map_err(|e| TransformError::ComputationError(e.to_string()))?;
for _ in 0..total {
data.push(dist.sample(&mut rng) * scale);
}
}
ShiftInvariantKernel::Matern52 { length_scale } => {
let scale = (10.0_f64).sqrt() / length_scale;
let dist = StudentT::new(5.0_f64)
.map_err(|e| TransformError::ComputationError(e.to_string()))?;
for _ in 0..total {
data.push(dist.sample(&mut rng) * scale);
}
}
}
Array2::from_shape_vec((input_dim, n_samples), data)
.map_err(|e| TransformError::ComputationError(e.to_string()))
}
pub fn compute_exact(&self, x: &[f64], y: &[f64]) -> f64 {
debug_assert_eq!(x.len(), y.len(), "Input dimensions must match");
match self {
ShiftInvariantKernel::RBF { gamma } => {
let sq_dist: f64 = x
.iter()
.zip(y.iter())
.map(|(a, b)| (a - b).powi(2))
.sum();
(-gamma * sq_dist).exp()
}
ShiftInvariantKernel::Laplacian { gamma } => {
let l1_dist: f64 = x
.iter()
.zip(y.iter())
.map(|(a, b)| (a - b).abs())
.sum();
(-gamma * l1_dist).exp()
}
ShiftInvariantKernel::Cauchy { gamma } => {
x.iter()
.zip(y.iter())
.map(|(a, b)| 1.0 / (1.0 + gamma * (a - b).powi(2)))
.product()
}
ShiftInvariantKernel::Matern32 { length_scale } => {
let dist: f64 = x
.iter()
.zip(y.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
.sqrt();
let r = 3.0_f64.sqrt() * dist / length_scale;
(1.0 + r) * (-r).exp()
}
ShiftInvariantKernel::Matern52 { length_scale } => {
let dist: f64 = x
.iter()
.zip(y.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
.sqrt();
let r = 5.0_f64.sqrt() * dist / length_scale;
(1.0 + r + r.powi(2) / 3.0) * (-r).exp()
}
}
}
}
#[derive(Debug, Clone)]
pub struct RandomFourierFeatures {
pub n_components: usize,
pub kernel: ShiftInvariantKernel,
weights: Option<Array2<f64>>,
biases: Option<Array1<f64>>,
}
impl RandomFourierFeatures {
pub fn new(n_components: usize, kernel: ShiftInvariantKernel) -> Self {
RandomFourierFeatures {
n_components,
kernel,
weights: None,
biases: None,
}
}
pub fn fit(&mut self, input_dim: usize, seed: u64) -> Result<()> {
if input_dim == 0 {
return Err(TransformError::InvalidInput(
"input_dim must be positive".to_string(),
));
}
if self.n_components == 0 {
return Err(TransformError::InvalidInput(
"n_components must be positive".to_string(),
));
}
let weights = self
.kernel
.sample_frequencies(self.n_components, input_dim, seed)?;
let mut rng = seeded_rng(seed.wrapping_add(1));
let bias_dist = Uniform::new(0.0_f64, 2.0 * PI)
.map_err(|e| TransformError::ComputationError(e.to_string()))?;
let biases: Vec<f64> = (0..self.n_components)
.map(|_| bias_dist.sample(&mut rng))
.collect();
self.weights = Some(weights);
self.biases = Some(
Array1::from_vec(biases),
);
Ok(())
}
pub fn transform(&self, x: ArrayView2<f64>) -> Result<Array2<f64>> {
let weights = self.weights.as_ref().ok_or_else(|| {
TransformError::NotFitted("RandomFourierFeatures not fitted".to_string())
})?;
let biases = self.biases.as_ref().ok_or_else(|| {
TransformError::NotFitted("RandomFourierFeatures not fitted".to_string())
})?;
let (n_samples, n_features) = (x.nrows(), x.ncols());
let expected_dim = weights.nrows();
if n_features != expected_dim {
return Err(TransformError::InvalidInput(format!(
"Expected {} features, got {}",
expected_dim, n_features
)));
}
let n_components = self.n_components;
let scale = (2.0_f64 / n_components as f64).sqrt();
let mut z = Array2::<f64>::zeros((n_samples, n_components));
for i in 0..n_samples {
for j in 0..n_components {
let dot: f64 = x
.row(i)
.iter()
.zip(weights.column(j).iter())
.map(|(a, b)| a * b)
.sum();
z[[i, j]] = scale * (dot + biases[j]).cos();
}
}
Ok(z)
}
pub fn fit_transform(
&mut self,
x: ArrayView2<f64>,
seed: u64,
) -> Result<Array2<f64>> {
let input_dim = x.ncols();
self.fit(input_dim, seed)?;
self.transform(x)
}
pub fn approximate_kernel(&self, x: ArrayView2<f64>) -> Result<Array2<f64>> {
let z = self.transform(x)?;
let n = z.nrows();
let d = z.ncols();
let mut k = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in i..n {
let dot: f64 = (0..d).map(|l| z[[i, l]] * z[[j, l]]).sum();
k[[i, j]] = dot;
k[[j, i]] = dot;
}
}
Ok(k)
}
pub fn approximate_cross_kernel(
&self,
x: ArrayView2<f64>,
y: ArrayView2<f64>,
) -> Result<Array2<f64>> {
let zx = self.transform(x)?;
let zy = self.transform(y)?;
let (nx, ny, d) = (zx.nrows(), zy.nrows(), zx.ncols());
let mut k = Array2::<f64>::zeros((nx, ny));
for i in 0..nx {
for j in 0..ny {
k[[i, j]] = (0..d).map(|l| zx[[i, l]] * zy[[j, l]]).sum();
}
}
Ok(k)
}
pub fn is_fitted(&self) -> bool {
self.weights.is_some()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum LandmarkSelection {
Random,
KMeans {
n_iter: usize,
},
Uniform,
}
#[derive(Debug, Clone)]
pub struct NystromApproximation {
pub n_components: usize,
pub kernel: ShiftInvariantKernel,
landmarks: Option<Array2<f64>>,
kernel_inv_sqrt: Option<Array2<f64>>,
}
impl NystromApproximation {
pub fn new(n_components: usize, kernel: ShiftInvariantKernel) -> Self {
NystromApproximation {
n_components,
kernel,
landmarks: None,
kernel_inv_sqrt: None,
}
}
pub fn fit(
&mut self,
x: ArrayView2<f64>,
selection: LandmarkSelection,
seed: u64,
) -> Result<()> {
let (n_samples, n_features) = (x.nrows(), x.ncols());
if n_samples == 0 || n_features == 0 {
return Err(TransformError::InvalidInput("Empty input data".to_string()));
}
let m = self.n_components.min(n_samples);
if m == 0 {
return Err(TransformError::InvalidInput(
"n_components must be positive".to_string(),
));
}
let landmark_indices = match selection {
LandmarkSelection::Random => {
sample_without_replacement(n_samples, m, seed)?
}
LandmarkSelection::Uniform => {
(0..m)
.map(|i| i * n_samples / m)
.collect::<Vec<usize>>()
}
LandmarkSelection::KMeans { n_iter } => {
kmeans_plus_plus_indices(x, m, n_iter, seed)?
}
};
let mut landmarks = Array2::<f64>::zeros((m, n_features));
for (row_out, &idx) in landmark_indices.iter().enumerate() {
landmarks.row_mut(row_out).assign(&x.row(idx));
}
let k_mm = compute_kernel_matrix_between(&landmarks.view(), &landmarks.view(), &self.kernel);
let (eigenvalues, eigenvectors) = eigh(&k_mm.view(), None)
.map_err(|e| TransformError::ComputationError(e.to_string()))?;
let threshold = 1e-10 * eigenvalues
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max)
.max(1e-12);
let n_eig = eigenvalues.len();
let mut inv_sqrt_diag = Vec::with_capacity(n_eig);
for &ev in eigenvalues.iter() {
if ev > threshold {
inv_sqrt_diag.push(1.0 / ev.sqrt());
} else {
inv_sqrt_diag.push(0.0);
}
}
let mut kernel_inv_sqrt = Array2::<f64>::zeros((m, m));
for i in 0..m {
for j in 0..m {
let val: f64 = (0..n_eig)
.map(|k| eigenvectors[[i, k]] * inv_sqrt_diag[k] * eigenvectors[[j, k]])
.sum();
kernel_inv_sqrt[[i, j]] = val;
}
}
self.landmarks = Some(landmarks);
self.kernel_inv_sqrt = Some(kernel_inv_sqrt);
Ok(())
}
pub fn transform(&self, x: ArrayView2<f64>) -> Result<Array2<f64>> {
let landmarks = self.landmarks.as_ref().ok_or_else(|| {
TransformError::NotFitted("NystromApproximation not fitted".to_string())
})?;
let kernel_inv_sqrt = self.kernel_inv_sqrt.as_ref().ok_or_else(|| {
TransformError::NotFitted("NystromApproximation not fitted".to_string())
})?;
let (n_samples, n_features) = (x.nrows(), x.ncols());
let expected_dim = landmarks.ncols();
if n_features != expected_dim {
return Err(TransformError::InvalidInput(format!(
"Expected {} features, got {}",
expected_dim, n_features
)));
}
let m = landmarks.nrows();
let k_xm = compute_kernel_matrix_between(&x, &landmarks.view(), &self.kernel);
let mut phi = Array2::<f64>::zeros((n_samples, m));
for i in 0..n_samples {
for j in 0..m {
phi[[i, j]] = (0..m)
.map(|k| k_xm[[i, k]] * kernel_inv_sqrt[[k, j]])
.sum();
}
}
Ok(phi)
}
pub fn fit_transform(
&mut self,
x: ArrayView2<f64>,
selection: LandmarkSelection,
seed: u64,
) -> Result<Array2<f64>> {
self.fit(x, selection, seed)?;
self.transform(x)
}
pub fn approximate_kernel(&self, x: ArrayView2<f64>) -> Result<Array2<f64>> {
let phi = self.transform(x)?;
let n = phi.nrows();
let m = phi.ncols();
let mut k = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in i..n {
let dot: f64 = (0..m).map(|l| phi[[i, l]] * phi[[j, l]]).sum();
k[[i, j]] = dot;
k[[j, i]] = dot;
}
}
Ok(k)
}
}
#[derive(Debug, Clone)]
pub struct TensorSketch {
pub n_components: usize,
pub degree: usize,
hash_functions: Option<Vec<Array1<usize>>>,
signs: Option<Vec<Array1<f64>>>,
}
impl TensorSketch {
pub fn new(n_components: usize, degree: usize) -> Self {
TensorSketch {
n_components,
degree,
hash_functions: None,
signs: None,
}
}
pub fn fit(&mut self, input_dim: usize, seed: u64) -> Result<()> {
if input_dim == 0 {
return Err(TransformError::InvalidInput("input_dim must be > 0".to_string()));
}
if self.degree == 0 {
return Err(TransformError::InvalidInput("degree must be > 0".to_string()));
}
if self.n_components == 0 {
return Err(TransformError::InvalidInput(
"n_components must be > 0".to_string(),
));
}
let d = self.degree;
let mut hash_functions = Vec::with_capacity(d);
let mut signs = Vec::with_capacity(d);
for k in 0..d {
let mut rng = seeded_rng(seed.wrapping_add(k as u64));
let h_dist = Uniform::new(0_usize, self.n_components)
.map_err(|e| TransformError::ComputationError(e.to_string()))?;
let s_dist = Uniform::new(0_usize, 2_usize)
.map_err(|e| TransformError::ComputationError(e.to_string()))?;
let h: Vec<usize> = (0..input_dim)
.map(|_| h_dist.sample(&mut rng))
.collect();
let s: Vec<f64> = (0..input_dim)
.map(|_| if s_dist.sample(&mut rng) == 0 { -1.0 } else { 1.0 })
.collect();
hash_functions.push(Array1::from_vec(h));
signs.push(Array1::from_vec(s));
}
self.hash_functions = Some(hash_functions);
self.signs = Some(signs);
Ok(())
}
pub fn transform(&self, x: ArrayView2<f64>) -> Result<Array2<f64>> {
let hash_functions = self.hash_functions.as_ref().ok_or_else(|| {
TransformError::NotFitted("TensorSketch not fitted".to_string())
})?;
let signs = self.signs.as_ref().ok_or_else(|| {
TransformError::NotFitted("TensorSketch not fitted".to_string())
})?;
let (n_samples, n_features) = (x.nrows(), x.ncols());
let expected_dim = hash_functions[0].len();
if n_features != expected_dim {
return Err(TransformError::InvalidInput(format!(
"Expected {} features, got {}",
expected_dim, n_features
)));
}
let n_components = self.n_components;
let degree = self.degree;
let mut output = Array2::<f64>::zeros((n_samples, n_components));
for i in 0..n_samples {
let row = x.row(i);
let mut sketches: Vec<Vec<f64>> = Vec::with_capacity(degree);
for k in 0..degree {
let mut sketch = vec![0.0_f64; n_components];
for (l, (&xl, &sl)) in row.iter().zip(signs[k].iter()).enumerate() {
let j = hash_functions[k][l];
sketch[j] += sl * xl;
}
sketches.push(sketch);
}
let combined = sketches
.iter()
.skip(1)
.fold(sketches[0].clone(), |acc, sk| {
circular_convolve(&acc, sk)
});
for j in 0..n_components {
output[[i, j]] = combined[j];
}
}
Ok(output)
}
pub fn fit_transform(&mut self, x: ArrayView2<f64>, seed: u64) -> Result<Array2<f64>> {
let input_dim = x.ncols();
self.fit(input_dim, seed)?;
self.transform(x)
}
}
fn circular_convolve(a: &[f64], b: &[f64]) -> Vec<f64> {
let n = a.len();
let mut result = vec![0.0_f64; n];
for j in 0..n {
if a[j] == 0.0 {
continue;
}
for k in 0..n {
result[(j + k) % n] += a[j] * b[k];
}
}
result
}
#[derive(Debug, Clone)]
pub struct KernelRidgeRegressionRF {
pub n_components: usize,
pub kernel: ShiftInvariantKernel,
pub alpha: f64,
weights: Option<Array1<f64>>,
rff: RandomFourierFeatures,
}
impl KernelRidgeRegressionRF {
pub fn new(n_components: usize, kernel: ShiftInvariantKernel, alpha: f64) -> Self {
let rff = RandomFourierFeatures::new(n_components, kernel.clone());
KernelRidgeRegressionRF {
n_components,
kernel,
alpha,
weights: None,
rff,
}
}
pub fn fit(
&mut self,
x: ArrayView2<f64>,
y: &Array1<f64>,
seed: u64,
) -> Result<()> {
let n_samples = x.nrows();
if y.len() != n_samples {
return Err(TransformError::InvalidInput(format!(
"X has {} samples but y has {} labels",
n_samples,
y.len()
)));
}
if self.alpha <= 0.0 {
return Err(TransformError::InvalidInput(
"alpha must be positive".to_string(),
));
}
let z = self.rff.fit_transform(x, seed)?;
let d = self.n_components;
let mut a = Array2::<f64>::zeros((d, d));
for k in 0..d {
for l in k..d {
let val: f64 = (0..n_samples).map(|i| z[[i, k]] * z[[i, l]]).sum();
a[[k, l]] = val;
a[[l, k]] = val;
}
a[[k, k]] += self.alpha;
}
let mut b = Array1::<f64>::zeros(d);
for k in 0..d {
b[k] = (0..n_samples).map(|i| z[[i, k]] * y[i]).sum();
}
let w: Array1<f64> = solve(&a.view(), &b.view(), None)
.map_err(|e| TransformError::ComputationError(e.to_string()))?;
self.weights = Some(w);
Ok(())
}
pub fn predict(&self, x: ArrayView2<f64>) -> Result<Array1<f64>> {
let weights = self.weights.as_ref().ok_or_else(|| {
TransformError::NotFitted("KernelRidgeRegressionRF not fitted".to_string())
})?;
let z = self.rff.transform(x)?;
let n_samples = z.nrows();
let d = z.ncols();
let mut preds = Array1::<f64>::zeros(n_samples);
for i in 0..n_samples {
preds[i] = (0..d).map(|j| z[[i, j]] * weights[j]).sum();
}
Ok(preds)
}
}
pub fn kernel_matrix(x: ArrayView2<f64>, kernel: &ShiftInvariantKernel) -> Array2<f64> {
let n = x.nrows();
let mut k = Array2::<f64>::zeros((n, n));
for i in 0..n {
let xi: Vec<f64> = x.row(i).to_vec();
for j in i..n {
let xj: Vec<f64> = x.row(j).to_vec();
let val = kernel.compute_exact(&xi, &xj);
k[[i, j]] = val;
k[[j, i]] = val;
}
}
k
}
pub fn cross_kernel_matrix(
x: ArrayView2<f64>,
y: ArrayView2<f64>,
kernel: &ShiftInvariantKernel,
) -> Array2<f64> {
let (nx, ny) = (x.nrows(), y.nrows());
let mut k = Array2::<f64>::zeros((nx, ny));
for i in 0..nx {
let xi: Vec<f64> = x.row(i).to_vec();
for j in 0..ny {
let yj: Vec<f64> = y.row(j).to_vec();
k[[i, j]] = kernel.compute_exact(&xi, &yj);
}
}
k
}
pub fn mmd_test(
x: ArrayView2<f64>,
y: ArrayView2<f64>,
kernel: ShiftInvariantKernel,
n_components: usize,
n_bootstrap: usize,
seed: u64,
) -> Result<(f64, f64)> {
let (nx, n_features) = (x.nrows(), x.ncols());
let ny = y.nrows();
if n_features != y.ncols() {
return Err(TransformError::InvalidInput(
"X and Y must have the same number of features".to_string(),
));
}
if nx == 0 || ny == 0 {
return Err(TransformError::InvalidInput(
"Both X and Y must be non-empty".to_string(),
));
}
let input_dim = n_features;
let mut rff = RandomFourierFeatures::new(n_components, kernel);
rff.fit(input_dim, seed)?;
let zx = rff.transform(x)?;
let zy = rff.transform(y)?;
let mut kxx_sum = 0.0_f64;
for i in 0..nx {
for j in 0..nx {
if i != j {
let dot: f64 = (0..n_components).map(|d| zx[[i, d]] * zx[[j, d]]).sum();
kxx_sum += dot;
}
}
}
let mut kyy_sum = 0.0_f64;
for i in 0..ny {
for j in 0..ny {
if i != j {
let dot: f64 = (0..n_components).map(|d| zy[[i, d]] * zy[[j, d]]).sum();
kyy_sum += dot;
}
}
}
let mut kxy_sum = 0.0_f64;
for i in 0..nx {
for j in 0..ny {
let dot: f64 = (0..n_components).map(|d| zx[[i, d]] * zy[[j, d]]).sum();
kxy_sum += dot;
}
}
let mmd_sq = kxx_sum / (nx * (nx - 1)) as f64
+ kyy_sum / (ny * (ny - 1)) as f64
- 2.0 * kxy_sum / (nx * ny) as f64;
let mmd_stat = mmd_sq.max(0.0).sqrt();
let n_total = nx + ny;
let mut pooled = Array2::<f64>::zeros((n_total, n_components));
for i in 0..nx {
pooled.row_mut(i).assign(&zx.row(i));
}
for i in 0..ny {
pooled.row_mut(nx + i).assign(&zy.row(i));
}
let mut k_pool = Array2::<f64>::zeros((n_total, n_total));
for i in 0..n_total {
for j in i..n_total {
let dot: f64 = (0..n_components).map(|d| pooled[[i, d]] * pooled[[j, d]]).sum();
k_pool[[i, j]] = dot;
k_pool[[j, i]] = dot;
}
}
let mut exceed_count = 0_usize;
let mut rng = seeded_rng(seed.wrapping_add(999));
let idx_dist = Uniform::new(0_usize, n_total)
.map_err(|e| TransformError::ComputationError(e.to_string()))?;
for _ in 0..n_bootstrap {
let mut indices: Vec<usize> = (0..n_total).collect();
for i in (1..n_total).rev() {
let j = idx_dist.sample(&mut rng) % (i + 1);
indices.swap(i, j);
}
let bx = &indices[..nx];
let by = &indices[nx..];
let mut boot_kxx = 0.0_f64;
for i in 0..nx {
for j in 0..nx {
if i != j {
boot_kxx += k_pool[[bx[i], bx[j]]];
}
}
}
let mut boot_kyy = 0.0_f64;
let ny_boot = by.len();
for i in 0..ny_boot {
for j in 0..ny_boot {
if i != j {
boot_kyy += k_pool[[by[i], by[j]]];
}
}
}
let mut boot_kxy = 0.0_f64;
for i in 0..nx {
for j in 0..ny_boot {
boot_kxy += k_pool[[bx[i], by[j]]];
}
}
let boot_mmd_sq = boot_kxx / (nx * (nx - 1)).max(1) as f64
+ boot_kyy / (ny_boot * (ny_boot - 1)).max(1) as f64
- 2.0 * boot_kxy / (nx * ny_boot).max(1) as f64;
let boot_mmd = boot_mmd_sq.max(0.0).sqrt();
if boot_mmd >= mmd_stat {
exceed_count += 1;
}
}
let p_value = if n_bootstrap > 0 {
exceed_count as f64 / n_bootstrap as f64
} else {
1.0
};
Ok((mmd_stat, p_value))
}
fn compute_kernel_matrix_between(
x: &ArrayView2<f64>,
y: &ArrayView2<f64>,
kernel: &ShiftInvariantKernel,
) -> Array2<f64> {
let (nx, ny) = (x.nrows(), y.nrows());
let mut k = Array2::<f64>::zeros((nx, ny));
for i in 0..nx {
let xi: Vec<f64> = x.row(i).to_vec();
for j in 0..ny {
let yj: Vec<f64> = y.row(j).to_vec();
k[[i, j]] = kernel.compute_exact(&xi, &yj);
}
}
k
}
fn sample_without_replacement(n: usize, m: usize, seed: u64) -> Result<Vec<usize>> {
if m > n {
return Err(TransformError::InvalidInput(format!(
"Cannot sample {} items from {} without replacement",
m, n
)));
}
let mut indices: Vec<usize> = (0..n).collect();
let mut rng = seeded_rng(seed);
for i in 0..m {
let j_dist = Uniform::new(i, n)
.map_err(|e| TransformError::ComputationError(e.to_string()))?;
let j = j_dist.sample(&mut rng);
indices.swap(i, j);
}
Ok(indices[..m].to_vec())
}
fn kmeans_plus_plus_indices(
x: ArrayView2<f64>,
m: usize,
n_iter: usize,
seed: u64,
) -> Result<Vec<usize>> {
let n = x.nrows();
if m >= n {
return Ok((0..n).collect());
}
let mut rng = seeded_rng(seed);
let unif_n = Uniform::new(0_usize, n)
.map_err(|e| TransformError::ComputationError(e.to_string()))?;
let unif_01 = Uniform::new(0.0_f64, 1.0_f64)
.map_err(|e| TransformError::ComputationError(e.to_string()))?;
let first = unif_n.sample(&mut rng);
let mut centers: Vec<Vec<f64>> = vec![x.row(first).to_vec()];
for _ in 1..m {
let mut min_sq_dists: Vec<f64> = (0..n)
.map(|i| {
let xi: Vec<f64> = x.row(i).to_vec();
centers
.iter()
.map(|c| {
xi.iter().zip(c.iter()).map(|(a, b)| (a - b).powi(2)).sum::<f64>()
})
.fold(f64::INFINITY, f64::min)
})
.collect();
let total: f64 = min_sq_dists.iter().sum();
if total < 1e-12 {
centers.push(x.row(unif_n.sample(&mut rng)).to_vec());
continue;
}
for d in min_sq_dists.iter_mut() {
*d /= total;
}
let threshold = unif_01.sample(&mut rng);
let mut cumulative = 0.0;
let mut chosen = n - 1;
for (i, &prob) in min_sq_dists.iter().enumerate() {
cumulative += prob;
if cumulative >= threshold {
chosen = i;
break;
}
}
centers.push(x.row(chosen).to_vec());
}
let n_features = x.ncols();
let mut center_matrix: Vec<Vec<f64>> = centers;
for _iter in 0..n_iter {
let mut assignments: Vec<usize> = Vec::with_capacity(n);
for i in 0..n {
let xi: Vec<f64> = x.row(i).to_vec();
let best = center_matrix
.iter()
.enumerate()
.map(|(c_idx, c)| {
let d: f64 = xi
.iter()
.zip(c.iter())
.map(|(a, b)| (a - b).powi(2))
.sum();
(c_idx, d)
})
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(idx, _)| idx)
.unwrap_or(0);
assignments.push(best);
}
let mut new_centers = vec![vec![0.0_f64; n_features]; m];
let mut counts = vec![0_usize; m];
for (i, &c_idx) in assignments.iter().enumerate() {
counts[c_idx] += 1;
let xi: Vec<f64> = x.row(i).to_vec();
for d in 0..n_features {
new_centers[c_idx][d] += xi[d];
}
}
for c_idx in 0..m {
if counts[c_idx] > 0 {
let cnt = counts[c_idx] as f64;
for d in 0..n_features {
new_centers[c_idx][d] /= cnt;
}
} else {
new_centers[c_idx] = x.row(unif_n.sample(&mut rng)).to_vec();
}
}
center_matrix = new_centers;
}
let landmark_indices: Vec<usize> = center_matrix
.iter()
.map(|c| {
(0..n)
.map(|i| {
let xi: Vec<f64> = x.row(i).to_vec();
let d: f64 = xi
.iter()
.zip(c.iter())
.map(|(a, b)| (a - b).powi(2))
.sum();
(i, d)
})
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(idx, _)| idx)
.unwrap_or(0)
})
.collect();
let mut seen = std::collections::HashSet::new();
let unique: Vec<usize> = landmark_indices
.into_iter()
.filter(|&idx| seen.insert(idx))
.collect();
let mut result = unique;
if result.len() < m {
let mut remaining: Vec<usize> = (0..n).filter(|i| !result.contains(i)).collect();
let needed = m - result.len();
let take = needed.min(remaining.len());
for i in 0..take {
let j_dist = Uniform::new(i, remaining.len())
.map_err(|e| TransformError::ComputationError(e.to_string()))?;
let j = j_dist.sample(&mut rng);
remaining.swap(i, j);
}
result.extend_from_slice(&remaining[..take]);
}
Ok(result[..m.min(result.len())].to_vec())
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
fn make_data(n: usize, d: usize, seed: u64) -> Array2<f64> {
let mut rng = seeded_rng(seed);
let dist = Normal::new(0.0_f64, 1.0_f64).expect("Normal distribution creation failed");
let data: Vec<f64> = (0..n * d).map(|_| dist.sample(&mut rng)).collect();
Array2::from_shape_vec((n, d), data).expect("Failed to create data array")
}
fn make_two_distributions(
n: usize,
d: usize,
mean_shift: f64,
seed: u64,
) -> (Array2<f64>, Array2<f64>) {
let x = make_data(n, d, seed);
let mut y = make_data(n, d, seed.wrapping_add(1000));
y.mapv_inplace(|v| v + mean_shift);
(x, y)
}
#[test]
fn test_rff_output_shape() {
let x = make_data(30, 5, 1);
let kernel = ShiftInvariantKernel::RBF { gamma: 0.5 };
let mut rff = RandomFourierFeatures::new(50, kernel);
let z = rff.fit_transform(x.view(), 42).expect("RFF fit_transform failed");
assert_eq!(z.shape(), &[30, 50], "RFF output shape mismatch");
}
#[test]
fn test_rff_approximates_rbf_kernel() {
let x = make_data(20, 4, 7);
let kernel = ShiftInvariantKernel::RBF { gamma: 0.5 };
let k_exact = kernel_matrix(x.view(), &kernel);
let mut rff = RandomFourierFeatures::new(500, kernel);
let k_approx = rff
.fit_transform(x.view(), 0)
.and_then(|_| rff.approximate_kernel(x.view()))
.expect("RFF kernel approximation failed");
let n = x.nrows();
let mut max_err = 0.0_f64;
for i in 0..n {
for j in 0..n {
let err = (k_exact[[i, j]] - k_approx[[i, j]]).abs();
if err > max_err {
max_err = err;
}
}
}
assert!(
max_err < 0.15,
"RFF max kernel approximation error too large: {:.4} (expected < 0.15)",
max_err
);
}
#[test]
fn test_rff_laplacian_kernel() {
let x = make_data(10, 3, 2);
let kernel = ShiftInvariantKernel::Laplacian { gamma: 1.0 };
let mut rff = RandomFourierFeatures::new(64, kernel);
let z = rff.fit_transform(x.view(), 1).expect("Laplacian RFF failed");
assert_eq!(z.shape(), &[10, 64]);
assert!(z.iter().all(|v| v.is_finite()), "Non-finite RFF values");
}
#[test]
fn test_rff_cauchy_kernel() {
let x = make_data(10, 3, 3);
let kernel = ShiftInvariantKernel::Cauchy { gamma: 0.5 };
let mut rff = RandomFourierFeatures::new(64, kernel);
let z = rff.fit_transform(x.view(), 2).expect("Cauchy RFF failed");
assert_eq!(z.shape(), &[10, 64]);
assert!(z.iter().all(|v| v.is_finite()));
}
#[test]
fn test_rff_matern32_kernel() {
let x = make_data(15, 4, 4);
let kernel = ShiftInvariantKernel::Matern32 { length_scale: 1.0 };
let mut rff = RandomFourierFeatures::new(100, kernel);
let z = rff.fit_transform(x.view(), 3).expect("Matern32 RFF failed");
assert_eq!(z.shape(), &[15, 100]);
assert!(z.iter().all(|v| v.is_finite()));
}
#[test]
fn test_rff_matern52_kernel() {
let x = make_data(15, 4, 5);
let kernel = ShiftInvariantKernel::Matern52 { length_scale: 1.5 };
let mut rff = RandomFourierFeatures::new(100, kernel);
let z = rff.fit_transform(x.view(), 4).expect("Matern52 RFF failed");
assert_eq!(z.shape(), &[15, 100]);
assert!(z.iter().all(|v| v.is_finite()));
}
#[test]
fn test_rbf_spectral_density_is_gaussian() {
let gamma = 2.0;
let kernel = ShiftInvariantKernel::RBF { gamma };
let n_samples = 5000;
let input_dim = 1;
let weights = kernel
.sample_frequencies(n_samples, input_dim, 77)
.expect("Frequency sampling failed");
let expected_std = (2.0 * gamma).sqrt();
let mean: f64 = weights.iter().sum::<f64>() / n_samples as f64;
let variance: f64 = weights.iter().map(|&w| (w - mean).powi(2)).sum::<f64>()
/ n_samples as f64;
let std_dev = variance.sqrt();
assert!(
mean.abs() < 0.1,
"RBF spectral mean not near 0: {:.4}",
mean
);
assert!(
(std_dev - expected_std).abs() < 0.2,
"RBF spectral std {:.4} not near expected {:.4}",
std_dev,
expected_std
);
}
#[test]
fn test_nystrom_output_shape() {
let x = make_data(40, 5, 10);
let kernel = ShiftInvariantKernel::RBF { gamma: 1.0 };
let mut nystrom = NystromApproximation::new(15, kernel);
let phi = nystrom
.fit_transform(x.view(), LandmarkSelection::Random, 0)
.expect("Nystrom fit_transform failed");
assert_eq!(phi.shape(), &[40, 15], "Nystrom output shape mismatch");
}
#[test]
fn test_nystrom_kernel_psd() {
let x = make_data(20, 3, 11);
let kernel = ShiftInvariantKernel::RBF { gamma: 0.5 };
let mut nystrom = NystromApproximation::new(10, kernel);
nystrom
.fit(x.view(), LandmarkSelection::Random, 5)
.expect("Nystrom fit failed");
let k_approx = nystrom
.approximate_kernel(x.view())
.expect("Nystrom kernel failed");
let n = k_approx.nrows();
for i in 0..n {
for j in 0..n {
assert!(
(k_approx[[i, j]] - k_approx[[j, i]]).abs() < 1e-10,
"Nystrom kernel not symmetric at ({}, {})",
i,
j
);
}
}
let (evals, _) = eigh(&k_approx.view(), None).expect("eigh failed");
let min_eval = evals.iter().cloned().fold(f64::INFINITY, f64::min);
assert!(
min_eval >= -1e-8,
"Nystrom kernel not PSD: min eigenvalue = {:.2e}",
min_eval
);
}
#[test]
fn test_nystrom_kmeans_selection() {
let x = make_data(50, 4, 12);
let kernel = ShiftInvariantKernel::RBF { gamma: 0.5 };
let mut nystrom = NystromApproximation::new(10, kernel);
let phi = nystrom
.fit_transform(x.view(), LandmarkSelection::KMeans { n_iter: 5 }, 6)
.expect("Nystrom KMeans failed");
assert_eq!(phi.shape(), &[50, 10]);
}
#[test]
fn test_nystrom_uniform_selection() {
let x = make_data(30, 3, 13);
let kernel = ShiftInvariantKernel::RBF { gamma: 1.0 };
let mut nystrom = NystromApproximation::new(8, kernel);
let phi = nystrom
.fit_transform(x.view(), LandmarkSelection::Uniform, 7)
.expect("Nystrom Uniform failed");
assert_eq!(phi.shape(), &[30, 8]);
}
#[test]
fn test_tensor_sketch_output_shape() {
let x = make_data(25, 6, 20);
let mut ts = TensorSketch::new(32, 3);
let z = ts.fit_transform(x.view(), 0).expect("TensorSketch failed");
assert_eq!(z.shape(), &[25, 32], "TensorSketch output shape mismatch");
}
#[test]
fn test_tensor_sketch_degree1_values_finite() {
let x = make_data(10, 4, 21);
let mut ts = TensorSketch::new(16, 1);
let z = ts.fit_transform(x.view(), 1).expect("TensorSketch degree-1 failed");
assert_eq!(z.shape(), &[10, 16]);
assert!(z.iter().all(|v| v.is_finite()), "Non-finite tensor sketch values");
}
#[test]
fn test_krr_rf_output_shape() {
let x = make_data(40, 5, 30);
let y: Vec<f64> = (0..40).map(|i| i as f64 * 0.1).collect();
let y_arr = Array1::from_vec(y);
let kernel = ShiftInvariantKernel::RBF { gamma: 0.5 };
let mut krr = KernelRidgeRegressionRF::new(100, kernel, 0.01);
krr.fit(x.view(), &y_arr, 42).expect("KRR fit failed");
let preds = krr.predict(x.view()).expect("KRR predict failed");
assert_eq!(preds.len(), 40, "KRR prediction shape mismatch");
assert!(
preds.iter().all(|v| v.is_finite()),
"Non-finite KRR predictions"
);
}
#[test]
fn test_krr_rf_reasonable_predictions() {
let n = 30;
let d = 3;
let x = make_data(n, d, 31);
let y: Array1<f64> = Array1::from_iter(x.column(0).iter().cloned());
let kernel = ShiftInvariantKernel::RBF { gamma: 0.5 };
let mut krr = KernelRidgeRegressionRF::new(200, kernel, 0.001);
krr.fit(x.view(), &y, 99).expect("KRR fit failed");
let preds = krr.predict(x.view()).expect("KRR predict failed");
let mse: f64 = preds
.iter()
.zip(y.iter())
.map(|(p, t)| (p - t).powi(2))
.sum::<f64>()
/ n as f64;
assert!(mse.is_finite(), "KRR MSE is not finite");
assert!(
mse < 10.0,
"KRR MSE too large: {:.4} (sanity check failure)",
mse
);
}
#[test]
fn test_exact_kernel_matrix_psd() {
let x = make_data(15, 3, 40);
let kernel = ShiftInvariantKernel::RBF { gamma: 0.5 };
let k = kernel_matrix(x.view(), &kernel);
let n = k.nrows();
for i in 0..n {
for j in 0..n {
assert!(
(k[[i, j]] - k[[j, i]]).abs() < 1e-12,
"Kernel matrix not symmetric"
);
}
}
let (evals, _) = eigh(&k.view(), None).expect("eigh failed on kernel matrix");
let min_eval = evals.iter().cloned().fold(f64::INFINITY, f64::min);
assert!(
min_eval >= -1e-8,
"Kernel matrix not PSD: min eigenvalue = {:.2e}",
min_eval
);
}
#[test]
fn test_mmd_same_distribution_small() {
let n = 100;
let d = 5;
let x = make_data(n, d, 50);
let y = make_data(n, d, 51);
let kernel = ShiftInvariantKernel::RBF { gamma: 0.5 };
let (mmd_stat, _p_value) =
mmd_test(x.view(), y.view(), kernel, 200, 100, 0).expect("MMD test failed");
assert!(
mmd_stat < 0.1,
"MMD between same distribution should be small, got {:.4}",
mmd_stat
);
}
#[test]
fn test_mmd_different_distributions_larger() {
let n = 80;
let d = 4;
let (x1, y1) = make_two_distributions(n, d, 0.0, 60);
let (x2, y2) = make_two_distributions(n, d, 5.0, 61);
let kernel1 = ShiftInvariantKernel::RBF { gamma: 0.1 };
let kernel2 = ShiftInvariantKernel::RBF { gamma: 0.1 };
let (mmd_same, _) =
mmd_test(x1.view(), y1.view(), kernel1, 200, 50, 0).expect("MMD same failed");
let (mmd_diff, _) =
mmd_test(x2.view(), y2.view(), kernel2, 200, 50, 1).expect("MMD diff failed");
assert!(
mmd_diff > mmd_same,
"MMD for different distributions ({:.4}) should exceed same-distribution ({:.4})",
mmd_diff,
mmd_same
);
}
#[test]
fn test_cross_kernel_shape() {
let x = make_data(10, 4, 70);
let y = make_data(7, 4, 71);
let kernel = ShiftInvariantKernel::RBF { gamma: 1.0 };
let k = cross_kernel_matrix(x.view(), y.view(), &kernel);
assert_eq!(k.shape(), &[10, 7], "Cross kernel shape mismatch");
}
#[test]
fn test_rff_cross_kernel_shape() {
let x = make_data(12, 3, 80);
let y = make_data(8, 3, 81);
let kernel = ShiftInvariantKernel::RBF { gamma: 0.5 };
let mut rff = RandomFourierFeatures::new(100, kernel);
rff.fit(3, 0).expect("RFF fit failed");
let k = rff
.approximate_cross_kernel(x.view(), y.view())
.expect("cross kernel failed");
assert_eq!(k.shape(), &[12, 8]);
}
#[test]
fn test_not_fitted_error() {
let x = make_data(5, 3, 90);
let kernel = ShiftInvariantKernel::RBF { gamma: 1.0 };
let rff = RandomFourierFeatures::new(10, kernel);
let result = rff.transform(x.view());
assert!(
result.is_err(),
"Should return error when not fitted"
);
}
#[test]
fn test_nystrom_not_fitted_error() {
let x = make_data(5, 3, 91);
let kernel = ShiftInvariantKernel::RBF { gamma: 1.0 };
let nystrom = NystromApproximation::new(3, kernel);
let result = nystrom.transform(x.view());
assert!(result.is_err(), "Nystrom should return error when not fitted");
}
#[test]
fn test_rff_dimension_mismatch() {
let x_fit = make_data(10, 4, 100);
let x_test = make_data(5, 6, 101);
let kernel = ShiftInvariantKernel::RBF { gamma: 1.0 };
let mut rff = RandomFourierFeatures::new(50, kernel);
rff.fit(4, 0).expect("fit failed");
let result = rff.transform(x_test.view());
assert!(result.is_err(), "Should error on dimension mismatch");
drop(x_fit);
}
#[test]
fn test_exact_matern_kernel_at_zero() {
let x = vec![1.0, 2.0, 3.0];
let kernel_32 = ShiftInvariantKernel::Matern32 { length_scale: 1.0 };
let kernel_52 = ShiftInvariantKernel::Matern52 { length_scale: 1.0 };
let k32 = kernel_32.compute_exact(&x, &x);
let k52 = kernel_52.compute_exact(&x, &x);
assert!(
(k32 - 1.0).abs() < 1e-12,
"Matern32 k(x,x) should be 1, got {:.6}",
k32
);
assert!(
(k52 - 1.0).abs() < 1e-12,
"Matern52 k(x,x) should be 1, got {:.6}",
k52
);
}
#[test]
fn test_rff_is_fitted() {
let kernel = ShiftInvariantKernel::RBF { gamma: 0.5 };
let mut rff = RandomFourierFeatures::new(50, kernel);
assert!(!rff.is_fitted(), "Should not be fitted before fit()");
rff.fit(4, 42).expect("fit failed");
assert!(rff.is_fitted(), "Should be fitted after fit()");
}
}