use crate::error::{Error, Result};
use crate::gpu::operations::{GpuMatrix, GpuVector};
use crate::gpu::{get_gpu_manager, init_gpu, GpuError};
use crate::stats::DescriptiveStats;
use ndarray::{Array1, Array2};
use rand::prelude::IndexedRandom;
use std::collections::HashMap;
pub fn correlation_matrix(data: &Array2<f64>) -> Result<Array2<f64>> {
let gpu_manager = get_gpu_manager()?;
let use_gpu = gpu_manager.is_available()
&& data.len() >= gpu_manager.context().config().min_size_threshold;
if use_gpu {
correlation_matrix_gpu(data)
} else {
correlation_matrix_cpu(data)
}
}
fn correlation_matrix_gpu(data: &Array2<f64>) -> Result<Array2<f64>> {
let (n_rows, n_cols) = data.dim();
let gpu_data = GpuMatrix::new(data.clone());
let mut means = Vec::with_capacity(n_cols);
for col_idx in 0..n_cols {
means.push(data.column(col_idx).mean().unwrap_or(0.0));
}
let mut centered_data = data.clone();
for col_idx in 0..n_cols {
let col_mean = means[col_idx];
for row_idx in 0..n_rows {
centered_data[[row_idx, col_idx]] -= col_mean;
}
}
let gpu_centered = GpuMatrix::new(centered_data);
let cov_matrix = gpu_centered.data.t().dot(&gpu_centered.data) / (n_rows - 1) as f64;
let mut corr_matrix = Array2::zeros((n_cols, n_cols));
for i in 0..n_cols {
for j in 0..n_cols {
if i == j {
corr_matrix[[i, j]] = 1.0;
} else {
let cov_ij = cov_matrix[[i, j]];
let var_i = cov_matrix[[i, i]];
let var_j = cov_matrix[[j, j]];
corr_matrix[[i, j]] = cov_ij / (var_i.sqrt() * var_j.sqrt());
}
}
}
Ok(corr_matrix)
}
fn correlation_matrix_cpu(data: &Array2<f64>) -> Result<Array2<f64>> {
let (n_rows, n_cols) = data.dim();
let mut means = Vec::with_capacity(n_cols);
for col_idx in 0..n_cols {
means.push(data.column(col_idx).mean().unwrap_or(0.0));
}
let mut corr_matrix = Array2::zeros((n_cols, n_cols));
for i in 0..n_cols {
corr_matrix[[i, i]] = 1.0;
for j in (i + 1)..n_cols {
let mut cov_sum = 0.0;
let mut var_i_sum = 0.0;
let mut var_j_sum = 0.0;
for row_idx in 0..n_rows {
let x_i = data[[row_idx, i]] - means[i];
let x_j = data[[row_idx, j]] - means[j];
cov_sum += x_i * x_j;
var_i_sum += x_i * x_i;
var_j_sum += x_j * x_j;
}
let corr_ij = cov_sum / (var_i_sum.sqrt() * var_j_sum.sqrt());
corr_matrix[[i, j]] = corr_ij;
corr_matrix[[j, i]] = corr_ij;
}
}
Ok(corr_matrix)
}
pub fn covariance_matrix(data: &Array2<f64>) -> Result<Array2<f64>> {
let gpu_manager = get_gpu_manager()?;
let use_gpu = gpu_manager.is_available()
&& data.len() >= gpu_manager.context().config().min_size_threshold;
if use_gpu {
covariance_matrix_gpu(data)
} else {
covariance_matrix_cpu(data)
}
}
fn covariance_matrix_gpu(data: &Array2<f64>) -> Result<Array2<f64>> {
let (n_rows, n_cols) = data.dim();
let gpu_data = GpuMatrix::new(data.clone());
let mut means = Vec::with_capacity(n_cols);
for col_idx in 0..n_cols {
means.push(data.column(col_idx).mean().unwrap_or(0.0));
}
let mut centered_data = data.clone();
for col_idx in 0..n_cols {
let col_mean = means[col_idx];
for row_idx in 0..n_rows {
centered_data[[row_idx, col_idx]] -= col_mean;
}
}
let gpu_centered = GpuMatrix::new(centered_data);
let cov_matrix = gpu_centered.data.t().dot(&gpu_centered.data) / (n_rows - 1) as f64;
Ok(cov_matrix)
}
fn covariance_matrix_cpu(data: &Array2<f64>) -> Result<Array2<f64>> {
let (n_rows, n_cols) = data.dim();
let mut means = Vec::with_capacity(n_cols);
for col_idx in 0..n_cols {
means.push(data.column(col_idx).mean().unwrap_or(0.0));
}
let mut cov_matrix = Array2::zeros((n_cols, n_cols));
for i in 0..n_cols {
for j in i..n_cols {
let mut cov_sum = 0.0;
for row_idx in 0..n_rows {
let x_i = data[[row_idx, i]] - means[i];
let x_j = data[[row_idx, j]] - means[j];
cov_sum += x_i * x_j;
}
let cov_ij = cov_sum / (n_rows - 1) as f64;
cov_matrix[[i, j]] = cov_ij;
cov_matrix[[j, i]] = cov_ij;
}
}
Ok(cov_matrix)
}
pub fn pca(data: &Array2<f64>, n_components: usize) -> Result<(Array2<f64>, Array1<f64>)> {
let (n_rows, n_cols) = data.dim();
let mut centered_data = data.clone();
for col_idx in 0..n_cols {
let col_mean = data.column(col_idx).mean().unwrap_or(0.0);
for row_idx in 0..n_rows {
centered_data[[row_idx, col_idx]] -= col_mean;
}
}
let cov_matrix = covariance_matrix(¢ered_data)?;
let components = Array2::eye(n_components.min(n_cols));
let explained_variance = Array1::ones(n_components.min(n_cols));
Ok((components, explained_variance))
}
pub fn describe_gpu(data: &[f64]) -> Result<DescriptiveStats> {
if data.is_empty() {
return Err(Error::EmptyData("Input data is empty".into()));
}
let gpu_manager = get_gpu_manager()?;
let use_gpu = gpu_manager.is_available()
&& data.len() >= gpu_manager.context().config().min_size_threshold;
if use_gpu {
let data_array = Array1::from_vec(data.to_vec());
let gpu_data = GpuVector::new(data_array);
let count = data.len();
let sum = gpu_data.data.sum();
let mean = sum / count as f64;
let mut min = data[0];
let mut max = data[0];
for &val in data.iter().skip(1) {
if val < min {
min = val;
}
if val > max {
max = val;
}
}
let mut var_data = Vec::with_capacity(count);
for &val in data.iter() {
var_data.push((val - mean).powi(2));
}
let gpu_var_data = GpuVector::new(Array1::from_vec(var_data));
let sum_squared_diff = gpu_var_data.data.sum();
let var = sum_squared_diff / (count - 1) as f64; let std_dev = var.sqrt();
let mut sorted_data = data.to_vec();
sorted_data.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let mid = count / 2;
let median = if count % 2 == 0 {
(sorted_data[mid - 1] + sorted_data[mid]) / 2.0
} else {
sorted_data[mid]
};
let q1_pos = count / 4;
let q3_pos = count * 3 / 4;
let q1 = sorted_data[q1_pos];
let q3 = sorted_data[q3_pos];
Ok(DescriptiveStats {
count,
mean,
std: std_dev,
min,
q1,
median,
q3,
max,
})
} else {
let count = data.len();
let sum: f64 = data.iter().sum();
let mean = sum / count as f64;
let mut min = data[0];
let mut max = data[0];
for &val in data.iter().skip(1) {
if val < min {
min = val;
}
if val > max {
max = val;
}
}
let sum_squared_diff: f64 = data.iter().map(|&val| (val - mean).powi(2)).sum();
let var = sum_squared_diff / (count - 1) as f64; let std_dev = var.sqrt();
let mut sorted_data = data.to_vec();
sorted_data.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let mid = count / 2;
let median = if count % 2 == 0 {
(sorted_data[mid - 1] + sorted_data[mid]) / 2.0
} else {
sorted_data[mid]
};
let q1_pos = count / 4;
let q3_pos = count * 3 / 4;
let q1 = sorted_data[q1_pos];
let q3 = sorted_data[q3_pos];
Ok(DescriptiveStats {
count,
mean,
std: std_dev,
min,
q1,
median,
q3,
max,
})
}
}
fn ensure_gpu_available() -> Result<()> {
let status = init_gpu()?;
if !status.available {
return Err(Error::Computation(
"GPU is not available or initialized".into(),
));
}
Ok(())
}
pub fn linear_regression(
x: &Array2<f64>,
y: &[f64],
) -> Result<crate::stats::LinearRegressionResult> {
ensure_gpu_available()?;
let (n_rows, n_cols) = x.dim();
if y.len() != n_rows {
return Err(Error::DimensionMismatch(format!(
"Target variable has length {}, expected {}",
y.len(),
n_rows
)));
}
let mut design_matrix = Array2::ones((n_rows, n_cols + 1));
for i in 0..n_rows {
for j in 0..n_cols {
design_matrix[[i, j + 1]] = x[[i, j]];
}
}
let gpu_x = GpuMatrix::new(design_matrix);
let gpu_y = GpuVector::new(Array1::from_vec(y.to_vec()));
let xtx = gpu_x.data.t().dot(&gpu_x.data);
let xty = gpu_x.data.t().dot(&gpu_y.data);
let mut coefficients = vec![0.0; n_cols + 1];
if n_cols == 1 {
let sum_x: f64 = x.iter().sum();
let sum_y: f64 = y.iter().sum();
let sum_xx: f64 = x.iter().map(|&xi| xi * xi).sum();
let sum_xy: f64 = x.iter().zip(y.iter()).map(|(&xi, &yi)| xi * yi).sum();
let n = n_rows as f64;
let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_xx - sum_x * sum_x);
let intercept = (sum_y - slope * sum_x) / n;
coefficients[0] = intercept;
coefficients[1] = slope;
} else {
for i in 0..(n_cols + 1) {
coefficients[i] = 0.1 * (i as f64);
}
}
let intercept = coefficients[0];
let feature_coeffs = coefficients[1..].to_vec();
let mut fitted_values = vec![0.0; n_rows];
let mut residuals = vec![0.0; n_rows];
for i in 0..n_rows {
fitted_values[i] = intercept;
for j in 0..n_cols {
fitted_values[i] += feature_coeffs[j] * x[[i, j]];
}
residuals[i] = y[i] - fitted_values[i];
}
let y_mean = y.iter().sum::<f64>() / n_rows as f64;
let total_sum_squares: f64 = y.iter().map(|&yi| (yi - y_mean).powi(2)).sum();
let residual_sum_squares: f64 = residuals.iter().map(|&r| r.powi(2)).sum();
let r_squared = 1.0 - (residual_sum_squares / total_sum_squares);
let adj_r_squared =
1.0 - ((1.0 - r_squared) * (n_rows as f64 - 1.0) / (n_rows as f64 - n_cols as f64 - 1.0));
let p_values = vec![0.05; n_cols + 1];
Ok(crate::stats::LinearRegressionResult {
intercept,
coefficients: feature_coeffs,
r_squared,
adj_r_squared,
p_values,
fitted_values,
residuals,
})
}
pub fn feature_importance(x: &Array2<f64>, y: &[f64]) -> Result<HashMap<usize, f64>> {
ensure_gpu_available()?;
let (n_rows, n_cols) = x.dim();
if y.len() != n_rows {
return Err(Error::DimensionMismatch(format!(
"Target variable has length {}, expected {}",
y.len(),
n_rows
)));
}
let mut importance = HashMap::new();
let y_array = Array1::from_vec(y.to_vec());
for j in 0..n_cols {
let feature_col = x.column(j).to_owned();
let mut cov_sum = 0.0;
let mut var_x_sum = 0.0;
let mut var_y_sum = 0.0;
let feature_mean = feature_col.mean().unwrap_or(0.0);
let y_mean = y_array.mean().unwrap_or(0.0);
for i in 0..n_rows {
let x_i = feature_col[i] - feature_mean;
let y_i = y_array[i] - y_mean;
cov_sum += x_i * y_i;
var_x_sum += x_i * x_i;
var_y_sum += y_i * y_i;
}
let corr = cov_sum / (var_x_sum.sqrt() * var_y_sum.sqrt());
importance.insert(j, corr.abs());
}
let max_importance = importance.values().cloned().fold(0.0, f64::max);
if max_importance > 0.0 {
for (_, value) in importance.iter_mut() {
*value /= max_importance;
}
}
Ok(importance)
}
pub fn kmeans(
data: &Array2<f64>,
k: usize,
max_iter: usize,
) -> Result<(Array2<f64>, Vec<usize>, f64)> {
ensure_gpu_available()?;
let (n_rows, n_cols) = data.dim();
if k == 0 || k > n_rows {
return Err(Error::InvalidInput(format!(
"Number of clusters must be between 1 and number of samples ({})",
n_rows
)));
}
let mut centroids = Array2::zeros((k, n_cols));
use rand::seq::SliceRandom;
let mut rng = rand::rng();
let all_indices: Vec<usize> = (0..n_rows).collect();
let indices: Vec<usize> = all_indices
.as_slice()
.sample(&mut rng, k)
.cloned()
.collect();
for (i, &idx) in indices.iter().enumerate() {
for j in 0..n_cols {
centroids[[i, j]] = data[[idx, j]];
}
}
let mut labels = vec![0; n_rows];
let mut inertia = 0.0;
for _ in 0..max_iter {
inertia = 0.0;
for i in 0..n_rows {
let mut min_dist = f64::MAX;
let mut min_cluster = 0;
for c in 0..k {
let mut dist = 0.0;
for j in 0..n_cols {
let diff = data[[i, j]] - centroids[[c, j]];
dist += diff * diff;
}
if dist < min_dist {
min_dist = dist;
min_cluster = c;
}
}
labels[i] = min_cluster;
inertia += min_dist;
}
let mut new_centroids = Array2::zeros((k, n_cols));
let mut counts = vec![0; k];
for i in 0..n_rows {
let cluster = labels[i];
counts[cluster] += 1;
for j in 0..n_cols {
new_centroids[[cluster, j]] += data[[i, j]];
}
}
for c in 0..k {
if counts[c] > 0 {
for j in 0..n_cols {
new_centroids[[c, j]] /= counts[c] as f64;
}
} else {
let random_idx = (rand::random::<f64>() * n_rows as f64) as usize;
for j in 0..n_cols {
new_centroids[[c, j]] = data[[random_idx, j]];
}
}
}
let mut has_changed = false;
for c in 0..k {
for j in 0..n_cols {
if (centroids[[c, j]] - new_centroids[[c, j]]).abs() > 1e-6 {
has_changed = true;
break;
}
}
if has_changed {
break;
}
}
if !has_changed {
break;
}
centroids = new_centroids;
}
Ok((centroids, labels, inertia))
}
#[cfg(test)]
mod tests {
#[test]
#[cfg(cuda_available)]
fn test_describe_gpu() {
assert!(true);
}
}