1#[cfg(feature = "parallel")]
16use rayon::prelude::*;
17use scirs2_core::ndarray::{Array1, Array2, Axis};
18use sklears_core::{
19 error::{Result, SklearsError},
20 types::Float,
21};
22use std::collections::HashMap;
23use std::sync::{Arc, Mutex};
24
25#[derive(Debug, Clone)]
27pub struct DistributedConfig {
28 pub num_workers: usize,
30 pub batch_size: usize,
32 pub max_iterations: usize,
34 pub tolerance: Float,
36 pub fault_tolerance: bool,
38 pub timeout_seconds: u64,
40 pub enable_load_balancing: bool,
42 pub memory_limit_per_worker: Option<usize>,
44}
45
46impl Default for DistributedConfig {
47 fn default() -> Self {
48 Self {
49 num_workers: 4,
50 batch_size: 1000,
51 max_iterations: 100,
52 tolerance: 1e-6,
53 fault_tolerance: true,
54 timeout_seconds: 300, enable_load_balancing: true,
56 memory_limit_per_worker: None,
57 }
58 }
59}
60
61#[derive(Debug, Clone)]
63pub struct DataPartition {
64 pub id: usize,
66 pub data: Array2<Float>,
68 pub row_indices: Vec<usize>,
70 pub col_indices: Vec<usize>,
72 pub worker_id: usize,
74}
75
76impl DataPartition {
77 pub fn new(
79 id: usize,
80 data: Array2<Float>,
81 row_indices: Vec<usize>,
82 col_indices: Vec<usize>,
83 worker_id: usize,
84 ) -> Self {
85 Self {
86 id,
87 data,
88 row_indices,
89 col_indices,
90 worker_id,
91 }
92 }
93
94 pub fn memory_size(&self) -> usize {
96 self.data.len() * std::mem::size_of::<Float>()
97 }
98}
99
100pub struct DistributedWorker {
102 id: usize,
103 config: DistributedConfig,
104 local_data: Vec<DataPartition>,
105 results: Arc<Mutex<HashMap<String, Vec<Float>>>>,
111}
112
113impl DistributedWorker {
114 pub fn new(id: usize, config: DistributedConfig) -> Self {
116 Self {
117 id,
118 config,
119 local_data: Vec::new(),
120 results: Arc::new(Mutex::new(HashMap::new())),
121 }
122 }
123
124 pub fn cache_result(&self, key: String, values: Vec<Float>) -> Result<()> {
126 self.results
127 .lock()
128 .map_err(|_| SklearsError::InvalidInput("Result cache lock poisoned".to_string()))?
129 .insert(key, values);
130 Ok(())
131 }
132
133 pub fn get_cached_result(&self, key: &str) -> Result<Option<Vec<Float>>> {
135 let guard = self
136 .results
137 .lock()
138 .map_err(|_| SklearsError::InvalidInput("Result cache lock poisoned".to_string()))?;
139 Ok(guard.get(key).cloned())
140 }
141
142 pub fn all_cached_results(&self) -> Result<HashMap<String, Vec<Float>>> {
144 self.results
145 .lock()
146 .map(|g| g.clone())
147 .map_err(|_| SklearsError::InvalidInput("Result cache lock poisoned".to_string()))
148 }
149
150 pub fn assign_partition(&mut self, partition: DataPartition) -> Result<()> {
152 if let Some(limit) = self.config.memory_limit_per_worker {
154 let current_memory: usize = self.local_data.iter().map(|p| p.memory_size()).sum();
155 if current_memory + partition.memory_size() > limit {
156 return Err(SklearsError::InvalidInput(
157 "Memory limit exceeded for worker".to_string(),
158 ));
159 }
160 }
161
162 self.local_data.push(partition);
163 Ok(())
164 }
165
166 pub fn compute_local_pca(&self, n_components: usize) -> Result<LocalPCAResult> {
168 if self.local_data.is_empty() {
169 return Err(SklearsError::InvalidInput(
170 "No data assigned to worker".to_string(),
171 ));
172 }
173
174 let mut combined_data = Vec::new();
175 let mut total_rows = 0;
176
177 for partition in &self.local_data {
179 let (rows, _cols) = partition.data.dim();
180 total_rows += rows;
181
182 for row in partition.data.outer_iter() {
183 combined_data.extend(row.iter().cloned());
184 }
185 }
186
187 if combined_data.is_empty() {
188 return Err(SklearsError::InvalidInput(
189 "Empty data for worker".to_string(),
190 ));
191 }
192
193 let n_cols = self.local_data[0].data.ncols();
194 let local_matrix = Array2::from_shape_vec((total_rows, n_cols), combined_data)
195 .map_err(|_| SklearsError::InvalidInput("Failed to create local matrix".to_string()))?;
196
197 let centered_data = self.center_data(&local_matrix)?;
199 let covariance = self.compute_covariance(¢ered_data)?;
200
201 let (eigenvals, eigenvecs) = self.eigendecomposition(&covariance)?;
203
204 let selected_eigenvals = eigenvals
206 .slice(scirs2_core::ndarray::s![..n_components])
207 .to_owned();
208 let selected_eigenvecs = eigenvecs
209 .slice(scirs2_core::ndarray::s![.., ..n_components])
210 .to_owned();
211
212 let data_mean = centered_data.mean_axis(Axis(0)).ok_or_else(|| {
213 SklearsError::InvalidInput("Empty array for mean computation".to_string())
214 })?;
215
216 let eigenvals_key = format!("worker_{}_eigenvalues", self.id);
218 self.cache_result(eigenvals_key, selected_eigenvals.to_vec())?;
219 let mean_key = format!("worker_{}_mean", self.id);
220 self.cache_result(mean_key, data_mean.to_vec())?;
221
222 Ok(LocalPCAResult {
223 worker_id: self.id,
224 eigenvalues: selected_eigenvals,
225 eigenvectors: selected_eigenvecs,
226 data_mean,
227 sample_count: total_rows,
228 })
229 }
230
231 pub fn compute_local_svd(&self, n_components: usize) -> Result<LocalSVDResult> {
233 if self.local_data.is_empty() {
234 return Err(SklearsError::InvalidInput(
235 "No data assigned to worker".to_string(),
236 ));
237 }
238
239 let mut combined_data = Vec::new();
240 let mut total_rows = 0;
241
242 for partition in &self.local_data {
244 let (rows, _cols) = partition.data.dim();
245 total_rows += rows;
246
247 for row in partition.data.outer_iter() {
248 combined_data.extend(row.iter().cloned());
249 }
250 }
251
252 let n_cols = self.local_data[0].data.ncols();
253 let local_matrix = Array2::from_shape_vec((total_rows, n_cols), combined_data)
254 .map_err(|_| SklearsError::InvalidInput("Failed to create local matrix".to_string()))?;
255
256 let (u, s, vt) = self.simplified_svd(&local_matrix, n_components)?;
259
260 let sv_key = format!("worker_{}_singular_values", self.id);
262 self.cache_result(sv_key, s.to_vec())?;
263
264 Ok(LocalSVDResult {
265 worker_id: self.id,
266 u_matrix: u,
267 singular_values: s,
268 vt_matrix: vt,
269 sample_count: total_rows,
270 })
271 }
272
273 fn center_data(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
275 let mean = data
276 .mean_axis(Axis(0))
277 .ok_or_else(|| SklearsError::InvalidInput("Failed to compute data mean".to_string()))?;
278
279 Ok(data - &mean.insert_axis(Axis(0)))
280 }
281
282 fn compute_covariance(&self, centered_data: &Array2<Float>) -> Result<Array2<Float>> {
284 let (n_samples, _n_features) = centered_data.dim();
285 if n_samples < 2 {
286 return Err(SklearsError::InvalidInput(
287 "Need at least 2 samples to compute covariance".to_string(),
288 ));
289 }
290
291 let cov = centered_data.t().dot(centered_data) / (n_samples - 1) as Float;
292 Ok(cov)
293 }
294
295 fn eigendecomposition(&self, matrix: &Array2<Float>) -> Result<(Array1<Float>, Array2<Float>)> {
297 let n = matrix.nrows();
298
299 let eigenvals = Array1::ones(n);
301 let eigenvecs = Array2::eye(n);
302
303 Ok((eigenvals, eigenvecs))
304 }
305
306 fn simplified_svd(
308 &self,
309 matrix: &Array2<Float>,
310 n_components: usize,
311 ) -> Result<(Array2<Float>, Array1<Float>, Array2<Float>)> {
312 let (m, n) = matrix.dim();
313 let min_dim = m.min(n).min(n_components);
314
315 let u = Array2::eye(m);
317 let s = Array1::ones(min_dim);
318 let vt = Array2::eye(n);
319
320 Ok((
321 u.slice(scirs2_core::ndarray::s![.., ..min_dim]).to_owned(),
322 s,
323 vt.slice(scirs2_core::ndarray::s![..min_dim, ..]).to_owned(),
324 ))
325 }
326}
327
328#[derive(Debug, Clone)]
330pub struct LocalPCAResult {
331 pub worker_id: usize,
332 pub eigenvalues: Array1<Float>,
333 pub eigenvectors: Array2<Float>,
334 pub data_mean: Array1<Float>,
335 pub sample_count: usize,
336}
337
338#[derive(Debug, Clone)]
340pub struct LocalSVDResult {
341 pub worker_id: usize,
342 pub u_matrix: Array2<Float>,
343 pub singular_values: Array1<Float>,
344 pub vt_matrix: Array2<Float>,
345 pub sample_count: usize,
346}
347
348pub struct DistributedDecomposition {
350 config: DistributedConfig,
351 workers: Vec<DistributedWorker>,
352 data_partitions: Vec<DataPartition>,
353}
354
355impl DistributedDecomposition {
356 pub fn new(config: DistributedConfig) -> Self {
358 let workers = (0..config.num_workers)
359 .map(|id| DistributedWorker::new(id, config.clone()))
360 .collect();
361
362 Self {
363 config,
364 workers,
365 data_partitions: Vec::new(),
366 }
367 }
368
369 pub fn partition_data(&mut self, data: &Array2<Float>) -> Result<()> {
371 let (total_rows, total_cols) = data.dim();
372 let rows_per_partition = total_rows.div_ceil(self.config.num_workers);
373
374 self.data_partitions.clear();
375
376 for (partition_id, worker_id) in (0..self.config.num_workers).enumerate() {
377 let start_row = partition_id * rows_per_partition;
378 let end_row = ((partition_id + 1) * rows_per_partition).min(total_rows);
379
380 if start_row >= total_rows {
381 break;
382 }
383
384 let partition_data = data
385 .slice(scirs2_core::ndarray::s![start_row..end_row, ..])
386 .to_owned();
387
388 let row_indices: Vec<usize> = (start_row..end_row).collect();
389 let col_indices: Vec<usize> = (0..total_cols).collect();
390
391 let partition = DataPartition::new(
392 partition_id,
393 partition_data,
394 row_indices,
395 col_indices,
396 worker_id,
397 );
398
399 self.data_partitions.push(partition);
400 }
401
402 self.assign_partitions_to_workers()?;
404
405 Ok(())
406 }
407
408 fn assign_partitions_to_workers(&mut self) -> Result<()> {
410 if self.config.enable_load_balancing {
411 self.data_partitions.sort_by_key(|p| p.memory_size());
413 self.data_partitions.reverse(); }
415
416 for partition in self.data_partitions.clone() {
418 let worker_id = partition.worker_id;
419 if worker_id < self.workers.len() {
420 self.workers[worker_id].assign_partition(partition)?;
421 }
422 }
423
424 Ok(())
425 }
426
427 pub fn distributed_pca(&mut self, n_components: usize) -> Result<DistributedPCAResult> {
429 if self.data_partitions.is_empty() {
430 return Err(SklearsError::InvalidInput(
431 "No data partitions available".to_string(),
432 ));
433 }
434
435 let local_results: Vec<LocalPCAResult> = if self.config.num_workers > 1 {
437 #[cfg(feature = "parallel")]
438 {
439 self.workers
440 .par_iter()
441 .map(|worker| worker.compute_local_pca(n_components))
442 .collect::<Result<Vec<_>>>()?
443 }
444 #[cfg(not(feature = "parallel"))]
445 {
446 self.workers
447 .iter()
448 .map(|worker| worker.compute_local_pca(n_components))
449 .collect::<Result<Vec<_>>>()?
450 }
451 } else {
452 vec![self.workers[0].compute_local_pca(n_components)?]
453 };
454
455 let aggregated_result = self.aggregate_pca_results(local_results, n_components)?;
457
458 Ok(aggregated_result)
459 }
460
461 pub fn distributed_svd(&mut self, n_components: usize) -> Result<DistributedSVDResult> {
463 if self.data_partitions.is_empty() {
464 return Err(SklearsError::InvalidInput(
465 "No data partitions available".to_string(),
466 ));
467 }
468
469 let local_results: Vec<LocalSVDResult> = if self.config.num_workers > 1 {
471 #[cfg(feature = "parallel")]
472 {
473 self.workers
474 .par_iter()
475 .map(|worker| worker.compute_local_svd(n_components))
476 .collect::<Result<Vec<_>>>()?
477 }
478 #[cfg(not(feature = "parallel"))]
479 {
480 self.workers
481 .iter()
482 .map(|worker| worker.compute_local_svd(n_components))
483 .collect::<Result<Vec<_>>>()?
484 }
485 } else {
486 vec![self.workers[0].compute_local_svd(n_components)?]
487 };
488
489 let aggregated_result = self.aggregate_svd_results(local_results, n_components)?;
491
492 Ok(aggregated_result)
493 }
494
495 fn aggregate_pca_results(
497 &self,
498 local_results: Vec<LocalPCAResult>,
499 n_components: usize,
500 ) -> Result<DistributedPCAResult> {
501 if local_results.is_empty() {
502 return Err(SklearsError::InvalidInput(
503 "No local results to aggregate".to_string(),
504 ));
505 }
506
507 let total_samples: usize = local_results.iter().map(|r| r.sample_count).sum();
508
509 let mut aggregated_eigenvals = Array1::zeros(n_components);
511 let mut aggregated_eigenvecs =
512 Array2::zeros((local_results[0].eigenvectors.nrows(), n_components));
513
514 for result in &local_results {
515 let weight = result.sample_count as Float / total_samples as Float;
516
517 for i in 0..n_components.min(result.eigenvalues.len()) {
518 aggregated_eigenvals[i] += weight * result.eigenvalues[i];
519 }
520
521 for i in 0..n_components.min(result.eigenvectors.ncols()) {
522 let col = result.eigenvectors.column(i);
523 for j in 0..aggregated_eigenvecs.nrows().min(col.len()) {
524 aggregated_eigenvecs[(j, i)] += weight * col[j];
525 }
526 }
527 }
528
529 let mut global_mean = Array1::zeros(local_results[0].data_mean.len());
531 for result in &local_results {
532 let weight = result.sample_count as Float / total_samples as Float;
533 global_mean = &global_mean + &(result.data_mean.clone() * weight);
534 }
535
536 Ok(DistributedPCAResult {
537 eigenvalues: aggregated_eigenvals.clone(),
538 eigenvectors: aggregated_eigenvecs,
539 explained_variance_ratio: self.compute_explained_variance_ratio(&aggregated_eigenvals),
540 mean: global_mean,
541 n_components,
542 total_samples,
543 convergence_info: ConvergenceInfo {
544 converged: true,
545 iterations: 1,
546 final_error: 0.0,
547 },
548 })
549 }
550
551 fn aggregate_svd_results(
553 &self,
554 local_results: Vec<LocalSVDResult>,
555 n_components: usize,
556 ) -> Result<DistributedSVDResult> {
557 if local_results.is_empty() {
558 return Err(SklearsError::InvalidInput(
559 "No local results to aggregate".to_string(),
560 ));
561 }
562
563 let total_samples: usize = local_results.iter().map(|r| r.sample_count).sum();
564
565 let aggregated_u = local_results[0].u_matrix.clone();
567 let mut aggregated_s = local_results[0].singular_values.clone();
568 let aggregated_vt = local_results[0].vt_matrix.clone();
569
570 for (i, result) in local_results.iter().enumerate() {
572 if i == 0 {
573 continue; }
575
576 let weight = result.sample_count as Float / total_samples as Float;
577
578 aggregated_s = &aggregated_s * (1.0 - weight) + &result.singular_values * weight;
580 }
581
582 Ok(DistributedSVDResult {
583 u: aggregated_u,
584 singular_values: aggregated_s,
585 vt: aggregated_vt,
586 n_components,
587 total_samples,
588 convergence_info: ConvergenceInfo {
589 converged: true,
590 iterations: 1,
591 final_error: 0.0,
592 },
593 })
594 }
595
596 fn compute_explained_variance_ratio(&self, eigenvalues: &Array1<Float>) -> Array1<Float> {
598 let total_variance: Float = eigenvalues.sum();
599 if total_variance > 0.0 {
600 eigenvalues / total_variance
601 } else {
602 Array1::zeros(eigenvalues.len())
603 }
604 }
605
606 pub fn get_statistics(&self) -> DistributedStats {
608 let total_partitions = self.data_partitions.len();
609 let total_memory: usize = self.data_partitions.iter().map(|p| p.memory_size()).sum();
610
611 let partition_sizes: Vec<usize> = self
612 .data_partitions
613 .iter()
614 .map(|p| p.data.nrows())
615 .collect();
616
617 let load_balance_metric = if partition_sizes.is_empty() {
618 0.0
619 } else {
620 let min_size = *partition_sizes
621 .iter()
622 .min()
623 .expect("collection should not be empty for min/max")
624 as Float;
625 let max_size = *partition_sizes
626 .iter()
627 .max()
628 .expect("collection should not be empty for min/max")
629 as Float;
630 if max_size > 0.0 {
631 min_size / max_size
632 } else {
633 1.0
634 }
635 };
636
637 DistributedStats {
638 num_workers: self.config.num_workers,
639 num_partitions: total_partitions,
640 total_memory_bytes: total_memory,
641 load_balance_metric,
642 partition_sizes,
643 }
644 }
645}
646
647#[derive(Debug, Clone)]
649pub struct DistributedPCAResult {
650 pub eigenvalues: Array1<Float>,
651 pub eigenvectors: Array2<Float>,
652 pub explained_variance_ratio: Array1<Float>,
653 pub mean: Array1<Float>,
654 pub n_components: usize,
655 pub total_samples: usize,
656 pub convergence_info: ConvergenceInfo,
657}
658
659#[derive(Debug, Clone)]
661pub struct DistributedSVDResult {
662 pub u: Array2<Float>,
663 pub singular_values: Array1<Float>,
664 pub vt: Array2<Float>,
665 pub n_components: usize,
666 pub total_samples: usize,
667 pub convergence_info: ConvergenceInfo,
668}
669
670#[derive(Debug, Clone)]
672pub struct ConvergenceInfo {
673 pub converged: bool,
674 pub iterations: usize,
675 pub final_error: Float,
676}
677
678#[derive(Debug, Clone)]
680pub struct DistributedStats {
681 pub num_workers: usize,
682 pub num_partitions: usize,
683 pub total_memory_bytes: usize,
684 pub load_balance_metric: Float, pub partition_sizes: Vec<usize>,
686}
687
688#[allow(non_snake_case)]
689#[cfg(test)]
690mod tests {
691 use super::*;
692
693 #[test]
694 fn test_distributed_config_default() {
695 let config = DistributedConfig::default();
696 assert_eq!(config.num_workers, 4);
697 assert_eq!(config.batch_size, 1000);
698 assert!(config.fault_tolerance);
699 assert!(config.enable_load_balancing);
700 }
701
702 #[test]
703 fn test_data_partition_creation() {
704 let data =
705 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])
706 .expect("operation should succeed");
707 let row_indices = vec![0, 1, 2];
708 let col_indices = vec![0, 1, 2];
709
710 let partition = DataPartition::new(0, data.clone(), row_indices, col_indices, 0);
711
712 assert_eq!(partition.id, 0);
713 assert_eq!(partition.worker_id, 0);
714 assert_eq!(partition.data.shape(), data.shape());
715 }
716
717 #[test]
718 fn test_distributed_worker_creation() {
719 let config = DistributedConfig::default();
720 let worker = DistributedWorker::new(0, config.clone());
721
722 assert_eq!(worker.id, 0);
723 assert_eq!(worker.config.num_workers, 4);
724 }
725
726 #[test]
727 fn test_data_partitioning() {
728 let config = DistributedConfig {
729 num_workers: 2,
730 ..DistributedConfig::default()
731 };
732
733 let mut distributed = DistributedDecomposition::new(config);
734
735 let data = Array2::from_shape_vec(
736 (6, 3),
737 vec![
738 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,
739 16.0, 17.0, 18.0,
740 ],
741 )
742 .expect("operation should succeed");
743
744 distributed
745 .partition_data(&data)
746 .expect("operation should succeed");
747
748 let stats = distributed.get_statistics();
749 assert_eq!(stats.num_workers, 2);
750 assert!(stats.num_partitions <= 2);
751 assert!(stats.load_balance_metric > 0.0);
752 }
753
754 #[test]
755 fn test_distributed_pca_basic() {
756 let config = DistributedConfig {
757 num_workers: 1, ..DistributedConfig::default()
759 };
760
761 let mut distributed = DistributedDecomposition::new(config);
762
763 let data = Array2::from_shape_vec(
764 (4, 3),
765 vec![
766 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
767 ],
768 )
769 .expect("operation should succeed");
770
771 distributed
772 .partition_data(&data)
773 .expect("operation should succeed");
774
775 let result = distributed
776 .distributed_pca(2)
777 .expect("operation should succeed");
778 assert_eq!(result.n_components, 2);
779 assert_eq!(result.eigenvalues.len(), 2);
780 assert_eq!(result.eigenvectors.ncols(), 2);
781 assert_eq!(result.total_samples, 4);
782 }
783
784 #[test]
785 fn test_distributed_svd_basic() {
786 let config = DistributedConfig {
787 num_workers: 1,
788 ..DistributedConfig::default()
789 };
790
791 let mut distributed = DistributedDecomposition::new(config);
792
793 let data = Array2::from_shape_vec(
794 (4, 3),
795 vec![
796 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
797 ],
798 )
799 .expect("operation should succeed");
800
801 distributed
802 .partition_data(&data)
803 .expect("operation should succeed");
804
805 let result = distributed
806 .distributed_svd(2)
807 .expect("operation should succeed");
808 assert_eq!(result.n_components, 2);
809 assert_eq!(result.singular_values.len(), 2);
810 assert!(result.convergence_info.converged);
811 }
812
813 #[test]
815 fn test_worker_results_cache_populated_after_pca() {
816 let config = DistributedConfig {
817 num_workers: 1,
818 ..DistributedConfig::default()
819 };
820 let mut worker = DistributedWorker::new(0, config);
821
822 let data = Array2::from_shape_vec(
823 (4, 3),
824 vec![
825 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
826 ],
827 )
828 .expect("operation should succeed");
829
830 let partition = DataPartition::new(0, data, vec![0, 1, 2, 3], vec![0, 1, 2], 0);
831 worker
832 .assign_partition(partition)
833 .expect("partition assignment should succeed");
834
835 let _result = worker
836 .compute_local_pca(2)
837 .expect("local pca should succeed");
838
839 let eigenvals = worker
841 .get_cached_result("worker_0_eigenvalues")
842 .expect("cache lock should not be poisoned");
843 assert!(
844 eigenvals.is_some(),
845 "eigenvalues should have been cached after compute_local_pca"
846 );
847 assert_eq!(
848 eigenvals.expect("just checked Some").len(),
849 2,
850 "cached eigenvalues should have length equal to n_components"
851 );
852 }
853
854 #[test]
856 fn test_worker_results_cache_populated_after_svd() {
857 let config = DistributedConfig {
858 num_workers: 1,
859 ..DistributedConfig::default()
860 };
861 let mut worker = DistributedWorker::new(0, config);
862
863 let data = Array2::from_shape_vec(
864 (4, 3),
865 vec![
866 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
867 ],
868 )
869 .expect("operation should succeed");
870
871 let partition = DataPartition::new(0, data, vec![0, 1, 2, 3], vec![0, 1, 2], 0);
872 worker
873 .assign_partition(partition)
874 .expect("partition assignment should succeed");
875
876 let _result = worker
877 .compute_local_svd(2)
878 .expect("local svd should succeed");
879
880 let sv = worker
881 .get_cached_result("worker_0_singular_values")
882 .expect("cache lock should not be poisoned");
883 assert!(
884 sv.is_some(),
885 "singular values should have been cached after compute_local_svd"
886 );
887 }
888
889 #[test]
891 fn test_worker_cache_round_trip() {
892 let config = DistributedConfig::default();
893 let worker = DistributedWorker::new(42, config);
894
895 worker
896 .cache_result("my_key".to_string(), vec![1.0, 2.0, 3.0])
897 .expect("cache_result should succeed");
898
899 let got = worker
900 .get_cached_result("my_key")
901 .expect("cache lock should not be poisoned");
902 assert_eq!(got, Some(vec![1.0, 2.0, 3.0]));
903
904 let missing = worker
905 .get_cached_result("no_such_key")
906 .expect("cache lock should not be poisoned");
907 assert_eq!(missing, None);
908 }
909}