#[cfg(feature = "parallel")]
use rayon::prelude::*;
use scirs2_core::ndarray::{Array1, Array2, Axis};
use sklears_core::{
error::{Result, SklearsError},
types::Float,
};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone)]
pub struct DistributedConfig {
pub num_workers: usize,
pub batch_size: usize,
pub max_iterations: usize,
pub tolerance: Float,
pub fault_tolerance: bool,
pub timeout_seconds: u64,
pub enable_load_balancing: bool,
pub memory_limit_per_worker: Option<usize>,
}
impl Default for DistributedConfig {
fn default() -> Self {
Self {
num_workers: 4,
batch_size: 1000,
max_iterations: 100,
tolerance: 1e-6,
fault_tolerance: true,
timeout_seconds: 300, enable_load_balancing: true,
memory_limit_per_worker: None,
}
}
}
#[derive(Debug, Clone)]
pub struct DataPartition {
pub id: usize,
pub data: Array2<Float>,
pub row_indices: Vec<usize>,
pub col_indices: Vec<usize>,
pub worker_id: usize,
}
impl DataPartition {
pub fn new(
id: usize,
data: Array2<Float>,
row_indices: Vec<usize>,
col_indices: Vec<usize>,
worker_id: usize,
) -> Self {
Self {
id,
data,
row_indices,
col_indices,
worker_id,
}
}
pub fn memory_size(&self) -> usize {
self.data.len() * std::mem::size_of::<Float>()
}
}
pub struct DistributedWorker {
id: usize,
config: DistributedConfig,
local_data: Vec<DataPartition>,
results: Arc<Mutex<HashMap<String, Vec<Float>>>>,
}
impl DistributedWorker {
pub fn new(id: usize, config: DistributedConfig) -> Self {
Self {
id,
config,
local_data: Vec::new(),
results: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn cache_result(&self, key: String, values: Vec<Float>) -> Result<()> {
self.results
.lock()
.map_err(|_| SklearsError::InvalidInput("Result cache lock poisoned".to_string()))?
.insert(key, values);
Ok(())
}
pub fn get_cached_result(&self, key: &str) -> Result<Option<Vec<Float>>> {
let guard = self
.results
.lock()
.map_err(|_| SklearsError::InvalidInput("Result cache lock poisoned".to_string()))?;
Ok(guard.get(key).cloned())
}
pub fn all_cached_results(&self) -> Result<HashMap<String, Vec<Float>>> {
self.results
.lock()
.map(|g| g.clone())
.map_err(|_| SklearsError::InvalidInput("Result cache lock poisoned".to_string()))
}
pub fn assign_partition(&mut self, partition: DataPartition) -> Result<()> {
if let Some(limit) = self.config.memory_limit_per_worker {
let current_memory: usize = self.local_data.iter().map(|p| p.memory_size()).sum();
if current_memory + partition.memory_size() > limit {
return Err(SklearsError::InvalidInput(
"Memory limit exceeded for worker".to_string(),
));
}
}
self.local_data.push(partition);
Ok(())
}
pub fn compute_local_pca(&self, n_components: usize) -> Result<LocalPCAResult> {
if self.local_data.is_empty() {
return Err(SklearsError::InvalidInput(
"No data assigned to worker".to_string(),
));
}
let mut combined_data = Vec::new();
let mut total_rows = 0;
for partition in &self.local_data {
let (rows, _cols) = partition.data.dim();
total_rows += rows;
for row in partition.data.outer_iter() {
combined_data.extend(row.iter().cloned());
}
}
if combined_data.is_empty() {
return Err(SklearsError::InvalidInput(
"Empty data for worker".to_string(),
));
}
let n_cols = self.local_data[0].data.ncols();
let local_matrix = Array2::from_shape_vec((total_rows, n_cols), combined_data)
.map_err(|_| SklearsError::InvalidInput("Failed to create local matrix".to_string()))?;
let centered_data = self.center_data(&local_matrix)?;
let covariance = self.compute_covariance(¢ered_data)?;
let (eigenvals, eigenvecs) = self.eigendecomposition(&covariance)?;
let selected_eigenvals = eigenvals
.slice(scirs2_core::ndarray::s![..n_components])
.to_owned();
let selected_eigenvecs = eigenvecs
.slice(scirs2_core::ndarray::s![.., ..n_components])
.to_owned();
let data_mean = centered_data.mean_axis(Axis(0)).ok_or_else(|| {
SklearsError::InvalidInput("Empty array for mean computation".to_string())
})?;
let eigenvals_key = format!("worker_{}_eigenvalues", self.id);
self.cache_result(eigenvals_key, selected_eigenvals.to_vec())?;
let mean_key = format!("worker_{}_mean", self.id);
self.cache_result(mean_key, data_mean.to_vec())?;
Ok(LocalPCAResult {
worker_id: self.id,
eigenvalues: selected_eigenvals,
eigenvectors: selected_eigenvecs,
data_mean,
sample_count: total_rows,
})
}
pub fn compute_local_svd(&self, n_components: usize) -> Result<LocalSVDResult> {
if self.local_data.is_empty() {
return Err(SklearsError::InvalidInput(
"No data assigned to worker".to_string(),
));
}
let mut combined_data = Vec::new();
let mut total_rows = 0;
for partition in &self.local_data {
let (rows, _cols) = partition.data.dim();
total_rows += rows;
for row in partition.data.outer_iter() {
combined_data.extend(row.iter().cloned());
}
}
let n_cols = self.local_data[0].data.ncols();
let local_matrix = Array2::from_shape_vec((total_rows, n_cols), combined_data)
.map_err(|_| SklearsError::InvalidInput("Failed to create local matrix".to_string()))?;
let (u, s, vt) = self.simplified_svd(&local_matrix, n_components)?;
let sv_key = format!("worker_{}_singular_values", self.id);
self.cache_result(sv_key, s.to_vec())?;
Ok(LocalSVDResult {
worker_id: self.id,
u_matrix: u,
singular_values: s,
vt_matrix: vt,
sample_count: total_rows,
})
}
fn center_data(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
let mean = data
.mean_axis(Axis(0))
.ok_or_else(|| SklearsError::InvalidInput("Failed to compute data mean".to_string()))?;
Ok(data - &mean.insert_axis(Axis(0)))
}
fn compute_covariance(&self, centered_data: &Array2<Float>) -> Result<Array2<Float>> {
let (n_samples, _n_features) = centered_data.dim();
if n_samples < 2 {
return Err(SklearsError::InvalidInput(
"Need at least 2 samples to compute covariance".to_string(),
));
}
let cov = centered_data.t().dot(centered_data) / (n_samples - 1) as Float;
Ok(cov)
}
fn eigendecomposition(&self, matrix: &Array2<Float>) -> Result<(Array1<Float>, Array2<Float>)> {
let n = matrix.nrows();
let eigenvals = Array1::ones(n);
let eigenvecs = Array2::eye(n);
Ok((eigenvals, eigenvecs))
}
fn simplified_svd(
&self,
matrix: &Array2<Float>,
n_components: usize,
) -> Result<(Array2<Float>, Array1<Float>, Array2<Float>)> {
let (m, n) = matrix.dim();
let min_dim = m.min(n).min(n_components);
let u = Array2::eye(m);
let s = Array1::ones(min_dim);
let vt = Array2::eye(n);
Ok((
u.slice(scirs2_core::ndarray::s![.., ..min_dim]).to_owned(),
s,
vt.slice(scirs2_core::ndarray::s![..min_dim, ..]).to_owned(),
))
}
}
#[derive(Debug, Clone)]
pub struct LocalPCAResult {
pub worker_id: usize,
pub eigenvalues: Array1<Float>,
pub eigenvectors: Array2<Float>,
pub data_mean: Array1<Float>,
pub sample_count: usize,
}
#[derive(Debug, Clone)]
pub struct LocalSVDResult {
pub worker_id: usize,
pub u_matrix: Array2<Float>,
pub singular_values: Array1<Float>,
pub vt_matrix: Array2<Float>,
pub sample_count: usize,
}
pub struct DistributedDecomposition {
config: DistributedConfig,
workers: Vec<DistributedWorker>,
data_partitions: Vec<DataPartition>,
}
impl DistributedDecomposition {
pub fn new(config: DistributedConfig) -> Self {
let workers = (0..config.num_workers)
.map(|id| DistributedWorker::new(id, config.clone()))
.collect();
Self {
config,
workers,
data_partitions: Vec::new(),
}
}
pub fn partition_data(&mut self, data: &Array2<Float>) -> Result<()> {
let (total_rows, total_cols) = data.dim();
let rows_per_partition = total_rows.div_ceil(self.config.num_workers);
self.data_partitions.clear();
for (partition_id, worker_id) in (0..self.config.num_workers).enumerate() {
let start_row = partition_id * rows_per_partition;
let end_row = ((partition_id + 1) * rows_per_partition).min(total_rows);
if start_row >= total_rows {
break;
}
let partition_data = data
.slice(scirs2_core::ndarray::s![start_row..end_row, ..])
.to_owned();
let row_indices: Vec<usize> = (start_row..end_row).collect();
let col_indices: Vec<usize> = (0..total_cols).collect();
let partition = DataPartition::new(
partition_id,
partition_data,
row_indices,
col_indices,
worker_id,
);
self.data_partitions.push(partition);
}
self.assign_partitions_to_workers()?;
Ok(())
}
fn assign_partitions_to_workers(&mut self) -> Result<()> {
if self.config.enable_load_balancing {
self.data_partitions.sort_by_key(|p| p.memory_size());
self.data_partitions.reverse(); }
for partition in self.data_partitions.clone() {
let worker_id = partition.worker_id;
if worker_id < self.workers.len() {
self.workers[worker_id].assign_partition(partition)?;
}
}
Ok(())
}
pub fn distributed_pca(&mut self, n_components: usize) -> Result<DistributedPCAResult> {
if self.data_partitions.is_empty() {
return Err(SklearsError::InvalidInput(
"No data partitions available".to_string(),
));
}
let local_results: Vec<LocalPCAResult> = if self.config.num_workers > 1 {
#[cfg(feature = "parallel")]
{
self.workers
.par_iter()
.map(|worker| worker.compute_local_pca(n_components))
.collect::<Result<Vec<_>>>()?
}
#[cfg(not(feature = "parallel"))]
{
self.workers
.iter()
.map(|worker| worker.compute_local_pca(n_components))
.collect::<Result<Vec<_>>>()?
}
} else {
vec![self.workers[0].compute_local_pca(n_components)?]
};
let aggregated_result = self.aggregate_pca_results(local_results, n_components)?;
Ok(aggregated_result)
}
pub fn distributed_svd(&mut self, n_components: usize) -> Result<DistributedSVDResult> {
if self.data_partitions.is_empty() {
return Err(SklearsError::InvalidInput(
"No data partitions available".to_string(),
));
}
let local_results: Vec<LocalSVDResult> = if self.config.num_workers > 1 {
#[cfg(feature = "parallel")]
{
self.workers
.par_iter()
.map(|worker| worker.compute_local_svd(n_components))
.collect::<Result<Vec<_>>>()?
}
#[cfg(not(feature = "parallel"))]
{
self.workers
.iter()
.map(|worker| worker.compute_local_svd(n_components))
.collect::<Result<Vec<_>>>()?
}
} else {
vec![self.workers[0].compute_local_svd(n_components)?]
};
let aggregated_result = self.aggregate_svd_results(local_results, n_components)?;
Ok(aggregated_result)
}
fn aggregate_pca_results(
&self,
local_results: Vec<LocalPCAResult>,
n_components: usize,
) -> Result<DistributedPCAResult> {
if local_results.is_empty() {
return Err(SklearsError::InvalidInput(
"No local results to aggregate".to_string(),
));
}
let total_samples: usize = local_results.iter().map(|r| r.sample_count).sum();
let mut aggregated_eigenvals = Array1::zeros(n_components);
let mut aggregated_eigenvecs =
Array2::zeros((local_results[0].eigenvectors.nrows(), n_components));
for result in &local_results {
let weight = result.sample_count as Float / total_samples as Float;
for i in 0..n_components.min(result.eigenvalues.len()) {
aggregated_eigenvals[i] += weight * result.eigenvalues[i];
}
for i in 0..n_components.min(result.eigenvectors.ncols()) {
let col = result.eigenvectors.column(i);
for j in 0..aggregated_eigenvecs.nrows().min(col.len()) {
aggregated_eigenvecs[(j, i)] += weight * col[j];
}
}
}
let mut global_mean = Array1::zeros(local_results[0].data_mean.len());
for result in &local_results {
let weight = result.sample_count as Float / total_samples as Float;
global_mean = &global_mean + &(result.data_mean.clone() * weight);
}
Ok(DistributedPCAResult {
eigenvalues: aggregated_eigenvals.clone(),
eigenvectors: aggregated_eigenvecs,
explained_variance_ratio: self.compute_explained_variance_ratio(&aggregated_eigenvals),
mean: global_mean,
n_components,
total_samples,
convergence_info: ConvergenceInfo {
converged: true,
iterations: 1,
final_error: 0.0,
},
})
}
fn aggregate_svd_results(
&self,
local_results: Vec<LocalSVDResult>,
n_components: usize,
) -> Result<DistributedSVDResult> {
if local_results.is_empty() {
return Err(SklearsError::InvalidInput(
"No local results to aggregate".to_string(),
));
}
let total_samples: usize = local_results.iter().map(|r| r.sample_count).sum();
let aggregated_u = local_results[0].u_matrix.clone();
let mut aggregated_s = local_results[0].singular_values.clone();
let aggregated_vt = local_results[0].vt_matrix.clone();
for (i, result) in local_results.iter().enumerate() {
if i == 0 {
continue; }
let weight = result.sample_count as Float / total_samples as Float;
aggregated_s = &aggregated_s * (1.0 - weight) + &result.singular_values * weight;
}
Ok(DistributedSVDResult {
u: aggregated_u,
singular_values: aggregated_s,
vt: aggregated_vt,
n_components,
total_samples,
convergence_info: ConvergenceInfo {
converged: true,
iterations: 1,
final_error: 0.0,
},
})
}
fn compute_explained_variance_ratio(&self, eigenvalues: &Array1<Float>) -> Array1<Float> {
let total_variance: Float = eigenvalues.sum();
if total_variance > 0.0 {
eigenvalues / total_variance
} else {
Array1::zeros(eigenvalues.len())
}
}
pub fn get_statistics(&self) -> DistributedStats {
let total_partitions = self.data_partitions.len();
let total_memory: usize = self.data_partitions.iter().map(|p| p.memory_size()).sum();
let partition_sizes: Vec<usize> = self
.data_partitions
.iter()
.map(|p| p.data.nrows())
.collect();
let load_balance_metric = if partition_sizes.is_empty() {
0.0
} else {
let min_size = *partition_sizes
.iter()
.min()
.expect("collection should not be empty for min/max")
as Float;
let max_size = *partition_sizes
.iter()
.max()
.expect("collection should not be empty for min/max")
as Float;
if max_size > 0.0 {
min_size / max_size
} else {
1.0
}
};
DistributedStats {
num_workers: self.config.num_workers,
num_partitions: total_partitions,
total_memory_bytes: total_memory,
load_balance_metric,
partition_sizes,
}
}
}
#[derive(Debug, Clone)]
pub struct DistributedPCAResult {
pub eigenvalues: Array1<Float>,
pub eigenvectors: Array2<Float>,
pub explained_variance_ratio: Array1<Float>,
pub mean: Array1<Float>,
pub n_components: usize,
pub total_samples: usize,
pub convergence_info: ConvergenceInfo,
}
#[derive(Debug, Clone)]
pub struct DistributedSVDResult {
pub u: Array2<Float>,
pub singular_values: Array1<Float>,
pub vt: Array2<Float>,
pub n_components: usize,
pub total_samples: usize,
pub convergence_info: ConvergenceInfo,
}
#[derive(Debug, Clone)]
pub struct ConvergenceInfo {
pub converged: bool,
pub iterations: usize,
pub final_error: Float,
}
#[derive(Debug, Clone)]
pub struct DistributedStats {
pub num_workers: usize,
pub num_partitions: usize,
pub total_memory_bytes: usize,
pub load_balance_metric: Float, pub partition_sizes: Vec<usize>,
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_distributed_config_default() {
let config = DistributedConfig::default();
assert_eq!(config.num_workers, 4);
assert_eq!(config.batch_size, 1000);
assert!(config.fault_tolerance);
assert!(config.enable_load_balancing);
}
#[test]
fn test_data_partition_creation() {
let data =
Array2::from_shape_vec((3, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0])
.expect("operation should succeed");
let row_indices = vec![0, 1, 2];
let col_indices = vec![0, 1, 2];
let partition = DataPartition::new(0, data.clone(), row_indices, col_indices, 0);
assert_eq!(partition.id, 0);
assert_eq!(partition.worker_id, 0);
assert_eq!(partition.data.shape(), data.shape());
}
#[test]
fn test_distributed_worker_creation() {
let config = DistributedConfig::default();
let worker = DistributedWorker::new(0, config.clone());
assert_eq!(worker.id, 0);
assert_eq!(worker.config.num_workers, 4);
}
#[test]
fn test_data_partitioning() {
let config = DistributedConfig {
num_workers: 2,
..DistributedConfig::default()
};
let mut distributed = DistributedDecomposition::new(config);
let data = Array2::from_shape_vec(
(6, 3),
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0,
16.0, 17.0, 18.0,
],
)
.expect("operation should succeed");
distributed
.partition_data(&data)
.expect("operation should succeed");
let stats = distributed.get_statistics();
assert_eq!(stats.num_workers, 2);
assert!(stats.num_partitions <= 2);
assert!(stats.load_balance_metric > 0.0);
}
#[test]
fn test_distributed_pca_basic() {
let config = DistributedConfig {
num_workers: 1, ..DistributedConfig::default()
};
let mut distributed = DistributedDecomposition::new(config);
let data = Array2::from_shape_vec(
(4, 3),
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
],
)
.expect("operation should succeed");
distributed
.partition_data(&data)
.expect("operation should succeed");
let result = distributed
.distributed_pca(2)
.expect("operation should succeed");
assert_eq!(result.n_components, 2);
assert_eq!(result.eigenvalues.len(), 2);
assert_eq!(result.eigenvectors.ncols(), 2);
assert_eq!(result.total_samples, 4);
}
#[test]
fn test_distributed_svd_basic() {
let config = DistributedConfig {
num_workers: 1,
..DistributedConfig::default()
};
let mut distributed = DistributedDecomposition::new(config);
let data = Array2::from_shape_vec(
(4, 3),
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
],
)
.expect("operation should succeed");
distributed
.partition_data(&data)
.expect("operation should succeed");
let result = distributed
.distributed_svd(2)
.expect("operation should succeed");
assert_eq!(result.n_components, 2);
assert_eq!(result.singular_values.len(), 2);
assert!(result.convergence_info.converged);
}
#[test]
fn test_worker_results_cache_populated_after_pca() {
let config = DistributedConfig {
num_workers: 1,
..DistributedConfig::default()
};
let mut worker = DistributedWorker::new(0, config);
let data = Array2::from_shape_vec(
(4, 3),
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
],
)
.expect("operation should succeed");
let partition = DataPartition::new(0, data, vec![0, 1, 2, 3], vec![0, 1, 2], 0);
worker
.assign_partition(partition)
.expect("partition assignment should succeed");
let _result = worker
.compute_local_pca(2)
.expect("local pca should succeed");
let eigenvals = worker
.get_cached_result("worker_0_eigenvalues")
.expect("cache lock should not be poisoned");
assert!(
eigenvals.is_some(),
"eigenvalues should have been cached after compute_local_pca"
);
assert_eq!(
eigenvals.expect("just checked Some").len(),
2,
"cached eigenvalues should have length equal to n_components"
);
}
#[test]
fn test_worker_results_cache_populated_after_svd() {
let config = DistributedConfig {
num_workers: 1,
..DistributedConfig::default()
};
let mut worker = DistributedWorker::new(0, config);
let data = Array2::from_shape_vec(
(4, 3),
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
],
)
.expect("operation should succeed");
let partition = DataPartition::new(0, data, vec![0, 1, 2, 3], vec![0, 1, 2], 0);
worker
.assign_partition(partition)
.expect("partition assignment should succeed");
let _result = worker
.compute_local_svd(2)
.expect("local svd should succeed");
let sv = worker
.get_cached_result("worker_0_singular_values")
.expect("cache lock should not be poisoned");
assert!(
sv.is_some(),
"singular values should have been cached after compute_local_svd"
);
}
#[test]
fn test_worker_cache_round_trip() {
let config = DistributedConfig::default();
let worker = DistributedWorker::new(42, config);
worker
.cache_result("my_key".to_string(), vec![1.0, 2.0, 3.0])
.expect("cache_result should succeed");
let got = worker
.get_cached_result("my_key")
.expect("cache lock should not be poisoned");
assert_eq!(got, Some(vec![1.0, 2.0, 3.0]));
let missing = worker
.get_cached_result("no_such_key")
.expect("cache lock should not be poisoned");
assert_eq!(missing, None);
}
}