use scirs2_core::ndarray::{Array2, ArrayView2, ScalarOperand};
use scirs2_core::numeric::{Float, NumAssign, Zero};
use scirs2_core::random::{self, Rng, RngExt};
use std::iter::Sum;
use crate::error::{LinalgError, LinalgResult};
#[allow(dead_code)]
pub fn gaussian_randommatrix<F: Float + NumAssign + Zero + Sum + ScalarOperand>(
n_components: usize,
n_features: usize,
) -> LinalgResult<Array2<F>> {
if n_components >= n_features {
return Err(LinalgError::DimensionError(format!(
"n_components must be less than n_features, got {n_components} >= {n_features}"
)));
}
let mut rng = scirs2_core::random::rng();
let scale = F::from(1.0 / (n_components as f64).sqrt()).expect("Operation failed");
let mut _components = Array2::<F>::zeros((n_features, n_components));
for i in 0..n_features {
for j in 0..n_components {
let u1: f64 = rng.random_range(0.0..1.0);
let u2: f64 = rng.random_range(0.0..1.0);
let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
let value = F::from(z).expect("Operation failed") * scale;
_components[[i, j]] = value;
}
}
Ok(_components)
}
#[allow(dead_code)]
pub fn sparse_randommatrix<F: Float + NumAssign + Zero + Sum + ScalarOperand>(
n_components: usize,
n_features: usize,
density: f64,
) -> LinalgResult<Array2<F>> {
if n_components >= n_features {
return Err(LinalgError::DimensionError(format!(
"n_components must be less than n_features, got {n_components} >= {n_features}"
)));
}
if density <= 0.0 || density > 1.0 {
return Err(LinalgError::ValueError(format!(
"density must be in (0, 1], got {density}"
)));
}
let mut rng = scirs2_core::random::rng();
let scale = F::from(1.0 / (density * n_components as f64).sqrt()).expect("Operation failed");
let prob_zero = 1.0 - density;
let prob_neg = density / 2.0;
let mut _components = Array2::<F>::zeros((n_features, n_components));
for i in 0..n_features {
for j in 0..n_components {
let prob = rng.random_range(0.0..1.0);
let value = if prob < prob_neg {
-scale
} else if prob < prob_neg + prob_zero {
F::zero()
} else {
scale
};
_components[[i, j]] = value;
}
}
Ok(_components)
}
#[allow(dead_code)]
pub fn very_sparse_randommatrix<F: Float + NumAssign + Zero + Sum + ScalarOperand>(
n_components: usize,
n_features: usize,
) -> LinalgResult<Array2<F>> {
if n_components >= n_features {
return Err(LinalgError::DimensionError(format!(
"n_components must be less than n_features, got {n_components} >= {n_features}"
)));
}
let mut rng = scirs2_core::random::rng();
let s = (n_features as f64).sqrt();
let prob_nonzero = 1.0 / s;
let prob_neg = prob_nonzero / 2.0;
let scale = F::from((s / n_components as f64).sqrt()).expect("Operation failed");
let mut _components = Array2::<F>::zeros((n_features, n_components));
for i in 0..n_features {
for j in 0..n_components {
let prob = rng.random_range(0.0..1.0);
let value = if prob < prob_neg {
F::from(-1.0).expect("Operation failed") * scale
} else if prob < prob_nonzero {
F::from(1.0).expect("Operation failed") * scale
} else {
F::zero()
};
_components[[i, j]] = value;
}
}
Ok(_components)
}
#[allow(dead_code)]
pub fn project<F: Float + NumAssign + Sum + ScalarOperand>(
x: &ArrayView2<F>,
components: &ArrayView2<F>,
) -> LinalgResult<Array2<F>> {
let (_n_samples, n_features) = x.dim();
let (n_components_features, _n_components) = components.dim();
if n_features != n_components_features {
return Err(LinalgError::DimensionError(format!(
"Incompatible dimensions: x has {n_features} features but components has {n_components_features} features"
)));
}
let x_projected = x.dot(components);
Ok(x_projected)
}
#[allow(dead_code)]
pub fn johnson_lindenstrauss_transform<F: Float + NumAssign + Zero + Sum + ScalarOperand>(
x: &ArrayView2<F>,
eps: f64,
) -> LinalgResult<(Array2<F>, Array2<F>)> {
if eps <= 0.0 || eps >= 1.0 {
return Err(LinalgError::ValueError(format!(
"eps must be in (0, 1), got {eps}"
)));
}
let (n_samples, n_features) = x.dim();
let n_components = johnson_lindenstrauss_min_dim(n_samples, eps)?;
let n_components = n_components.min(n_features - 1);
let components = gaussian_randommatrix(n_components, n_features)?;
let x_projected = project(x, &components.view())?;
Ok((x_projected, components))
}
#[allow(dead_code)]
pub fn johnson_lindenstrauss_min_dim(_nsamples: usize, eps: f64) -> LinalgResult<usize> {
if eps <= 0.0 || eps >= 1.0 {
return Err(LinalgError::ValueError(format!(
"eps must be in (0, 1), got {eps}"
)));
}
let denominator = eps.powi(2) / 2.0 - eps.powi(3) / 3.0;
let min_dim = (4.0 * (_nsamples as f64).ln() / denominator).ceil() as usize;
Ok(min_dim)
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
#[test]
fn test_gaussian_randommatrix() {
let n_components = 10;
let n_features = 100;
let components =
gaussian_randommatrix::<f64>(n_components, n_features).expect("Operation failed");
assert_eq!(components.shape(), &[n_features, n_components]);
let result = gaussian_randommatrix::<f64>(n_features, n_features);
assert!(result.is_err());
}
#[test]
fn test_sparse_randommatrix() {
let n_components = 10;
let n_features = 100;
let density = 0.1;
let components = sparse_randommatrix::<f64>(n_components, n_features, density)
.expect("Operation failed");
assert_eq!(components.shape(), &[n_features, n_components]);
let non_zeros = components.iter().filter(|&&x| x != 0.0).count();
let total_elements = n_features * n_components;
let actual_density = non_zeros as f64 / total_elements as f64;
assert!(actual_density > density * 0.5 && actual_density < density * 1.5);
let result = sparse_randommatrix::<f64>(n_features, n_features, density);
assert!(result.is_err());
let result = sparse_randommatrix::<f64>(n_components, n_features, 1.5);
assert!(result.is_err());
}
#[test]
fn test_very_sparse_randommatrix() {
let n_components = 10;
let n_features = 100;
let components =
very_sparse_randommatrix::<f64>(n_components, n_features).expect("Operation failed");
assert_eq!(components.shape(), &[n_features, n_components]);
let non_zeros = components.iter().filter(|&&x| x != 0.0).count();
let total_elements = n_features * n_components;
let expected_density = 1.0 / (n_features as f64).sqrt();
let actual_density = non_zeros as f64 / total_elements as f64;
assert!(actual_density > expected_density * 0.3 && actual_density < expected_density * 2.0);
}
#[test]
fn test_project() {
let n_samples = 5;
let n_features = 20;
let n_components = 3;
let mut x = Array2::<f64>::zeros((n_samples, n_features));
for i in 0..n_samples {
for j in 0..n_features {
x[[i, j]] = (i * n_features + j) as f64;
}
}
let components =
gaussian_randommatrix::<f64>(n_components, n_features).expect("Operation failed");
let x_projected = project(&x.view(), &components.view()).expect("Operation failed");
assert_eq!(x_projected.shape(), &[n_samples, n_components]);
let expected_proj = x.dot(&components);
assert_eq!(x_projected, expected_proj);
}
#[test]
fn test_johnson_lindenstrauss_transform() {
let n_samples = 50;
let n_features = 100;
let eps = 0.2;
let x = Array2::<f64>::ones((n_samples, n_features));
let (x_projected, components) =
johnson_lindenstrauss_transform(&x.view(), eps).expect("Operation failed");
assert!(x_projected.shape()[1] < n_features);
let min_dim = johnson_lindenstrauss_min_dim(n_samples, eps).expect("Operation failed");
assert!(x_projected.shape()[1] <= min_dim);
let expected_proj = project(&x.view(), &components.view()).expect("Operation failed");
assert_eq!(x_projected, expected_proj);
}
#[test]
fn test_johnson_lindenstrauss_min_dim() {
assert!(johnson_lindenstrauss_min_dim(10000, 0.1).expect("Operation failed") >= 500);
assert!(johnson_lindenstrauss_min_dim(1000, 0.0).is_err());
assert!(johnson_lindenstrauss_min_dim(1000, 1.0).is_err());
}
}