1use crate::generators::basic::{make_blobs, make_classification, make_regression};
11use scirs2_core::ndarray::{Array1, Array2};
12use std::collections::HashMap;
13use std::sync::mpsc;
14use std::thread;
15use std::time::{Duration, Instant};
16
17type BoxedError = Box<dyn std::error::Error + Send + Sync>;
19type GeneratorFn<T> = Box<dyn Fn(usize, Option<u64>) -> Result<T, BoxedError>>;
21type ParallelClassResult = Result<ParallelGenerationResult<(Array2<f64>, Array1<i32>)>, BoxedError>;
23type ParallelRegrResult = Result<ParallelGenerationResult<(Array2<f64>, Array1<f64>)>, BoxedError>;
25type ParallelBlobsResult = ParallelClassResult;
27type DistClassResult = Result<DistributedGenerationResult<(Array2<f64>, Array1<i32>)>, BoxedError>;
29type DistRegrResult = Result<DistributedGenerationResult<(Array2<f64>, Array1<f64>)>, BoxedError>;
31type DistBlobsResult = DistClassResult;
33
34#[derive(Debug, Clone)]
36pub struct StreamConfig {
37 pub chunk_size: usize,
39 pub total_samples: usize,
41 pub random_state: Option<u64>,
43 pub n_workers: usize,
45}
46
47impl Default for StreamConfig {
48 fn default() -> Self {
49 Self {
50 chunk_size: 1000,
51 total_samples: 10000,
52 random_state: None,
53 n_workers: num_cpus::get(),
54 }
55 }
56}
57
58pub struct DatasetStream<T> {
60 config: StreamConfig,
61 current_chunk: usize,
62 total_chunks: usize,
63 generator_fn: Box<dyn Fn(usize, usize, Option<u64>) -> T + Send + Sync>,
64}
65
66impl<T> DatasetStream<T> {
67 fn new<F>(config: StreamConfig, generator_fn: F) -> Self
68 where
69 F: Fn(usize, usize, Option<u64>) -> T + Send + Sync + 'static,
70 {
71 let total_chunks = config.total_samples.div_ceil(config.chunk_size);
72
73 Self {
74 config,
75 current_chunk: 0,
76 total_chunks,
77 generator_fn: Box::new(generator_fn),
78 }
79 }
80}
81
82impl<T> Iterator for DatasetStream<T> {
83 type Item = T;
84
85 fn next(&mut self) -> Option<Self::Item> {
86 if self.current_chunk >= self.total_chunks {
87 return None;
88 }
89
90 let chunk_start = self.current_chunk * self.config.chunk_size;
91 let chunk_end = std::cmp::min(
92 chunk_start + self.config.chunk_size,
93 self.config.total_samples,
94 );
95 let chunk_size = chunk_end - chunk_start;
96
97 let chunk_seed = self
99 .config
100 .random_state
101 .map(|seed| seed + self.current_chunk as u64);
102
103 let result = (self.generator_fn)(chunk_size, self.current_chunk, chunk_seed);
104 self.current_chunk += 1;
105
106 Some(result)
107 }
108}
109
110pub fn stream_classification(
112 n_features: usize,
113 n_classes: usize,
114 config: StreamConfig,
115) -> DatasetStream<(Array2<f64>, Array1<i32>)> {
116 DatasetStream::new(config, move |chunk_size, _chunk_idx, seed| {
117 make_classification(
118 chunk_size, n_features, n_features, 0, n_classes, seed,
121 )
122 .expect("operation should succeed")
123 })
124}
125
126pub fn stream_regression(
128 n_features: usize,
129 config: StreamConfig,
130) -> DatasetStream<(Array2<f64>, Array1<f64>)> {
131 DatasetStream::new(config, move |chunk_size, _chunk_idx, seed| {
132 make_regression(
133 chunk_size, n_features, n_features, 0.1, seed,
136 )
137 .expect("operation should succeed")
138 })
139}
140
141pub fn stream_blobs(
143 n_features: usize,
144 centers: usize,
145 config: StreamConfig,
146) -> DatasetStream<(Array2<f64>, Array1<i32>)> {
147 DatasetStream::new(config, move |chunk_size, _chunk_idx, seed| {
148 make_blobs(
149 chunk_size, n_features, centers, 1.0, seed,
151 )
152 .expect("operation should succeed")
153 })
154}
155
156#[derive(Debug)]
158pub struct ParallelGenerationResult<T> {
159 pub chunks: Vec<T>,
160 pub generation_time: std::time::Duration,
161 pub n_workers_used: usize,
162}
163
164pub fn parallel_generate<T, F>(
166 n_samples: usize,
167 n_workers: usize,
168 generator_fn: F,
169) -> Result<ParallelGenerationResult<T>, BoxedError>
170where
171 T: Send + 'static,
172 F: Fn(usize, Option<u64>) -> Result<T, BoxedError> + Send + Sync + Copy + 'static,
173{
174 let start_time = std::time::Instant::now();
175
176 let chunk_size = n_samples.div_ceil(n_workers);
177 let (tx, rx) = mpsc::channel();
178
179 let mut handles = Vec::new();
180
181 for worker_id in 0..n_workers {
182 let tx = tx.clone();
183 let handle = thread::spawn(move || {
184 let chunk_start = worker_id * chunk_size;
185 let chunk_end = std::cmp::min(chunk_start + chunk_size, n_samples);
186 let actual_chunk_size = chunk_end - chunk_start;
187
188 if actual_chunk_size == 0 {
189 return;
190 }
191
192 let seed = Some(worker_id as u64 * 12345);
194
195 match generator_fn(actual_chunk_size, seed) {
196 Ok(result) => {
197 if tx.send((worker_id, Ok(result))).is_err() {
198 eprintln!("Failed to send result from worker {}", worker_id);
199 }
200 }
201 Err(e) => {
202 if tx.send((worker_id, Err(e))).is_err() {
203 eprintln!("Failed to send error from worker {}", worker_id);
204 }
205 }
206 }
207 });
208 handles.push(handle);
209 }
210
211 drop(tx);
213
214 let mut results: Vec<Option<T>> = (0..n_workers).map(|_| None).collect();
216 let mut successful_workers = 0;
217
218 for (worker_id, result) in rx {
219 match result {
220 Ok(data) => {
221 results[worker_id] = Some(data);
222 successful_workers += 1;
223 }
224 Err(e) => {
225 return Err(format!("Worker {} failed: {}", worker_id, e).into());
226 }
227 }
228 }
229
230 for handle in handles {
232 handle.join().map_err(|_| "Thread panicked")?;
233 }
234
235 let chunks: Vec<T> = results.into_iter().flatten().collect();
237
238 let generation_time = start_time.elapsed();
239
240 Ok(ParallelGenerationResult {
241 chunks,
242 generation_time,
243 n_workers_used: successful_workers,
244 })
245}
246
247pub fn parallel_classification(
249 n_samples: usize,
250 n_features: usize,
251 n_classes: usize,
252 n_workers: usize,
253) -> ParallelClassResult {
254 parallel_generate(n_samples, n_workers, move |chunk_size, seed| {
255 make_classification(
256 chunk_size, n_features, n_features, 0, n_classes, seed,
259 )
260 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
261 })
262}
263
264pub fn parallel_regression(
266 n_samples: usize,
267 n_features: usize,
268 n_workers: usize,
269) -> ParallelRegrResult {
270 parallel_generate(n_samples, n_workers, move |chunk_size, seed| {
271 make_regression(
272 chunk_size, n_features, n_features, 0.1, seed,
275 )
276 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
277 })
278}
279
280pub fn parallel_blobs(
282 n_samples: usize,
283 n_features: usize,
284 centers: usize,
285 n_workers: usize,
286) -> ParallelBlobsResult {
287 parallel_generate(n_samples, n_workers, move |chunk_size, seed| {
288 make_blobs(
289 chunk_size, n_features, centers, 1.0, seed,
291 )
292 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
293 })
294}
295
296pub struct LazyDatasetGenerator<T> {
298 chunk_size: usize,
299 total_samples: usize,
300 generated_samples: usize,
301 generator_fn: GeneratorFn<T>,
302 random_state: Option<u64>,
303}
304
305impl<T> LazyDatasetGenerator<T> {
306 pub fn new<F>(
307 total_samples: usize,
308 chunk_size: usize,
309 random_state: Option<u64>,
310 generator_fn: F,
311 ) -> Self
312 where
313 F: Fn(usize, Option<u64>) -> Result<T, BoxedError> + 'static,
314 {
315 Self {
316 chunk_size,
317 total_samples,
318 generated_samples: 0,
319 generator_fn: Box::new(generator_fn),
320 random_state,
321 }
322 }
323
324 pub fn next_chunk(&mut self) -> Option<Result<T, BoxedError>> {
326 if self.generated_samples >= self.total_samples {
327 return None;
328 }
329
330 let remaining_samples = self.total_samples - self.generated_samples;
331 let current_chunk_size = std::cmp::min(self.chunk_size, remaining_samples);
332
333 let seed = self.random_state.map(|s| s + self.generated_samples as u64);
335
336 let result = (self.generator_fn)(current_chunk_size, seed);
337 self.generated_samples += current_chunk_size;
338
339 Some(result)
340 }
341
342 pub fn progress(&self) -> (usize, usize, f64) {
344 let progress_ratio = self.generated_samples as f64 / self.total_samples as f64;
345 (self.generated_samples, self.total_samples, progress_ratio)
346 }
347
348 pub fn is_complete(&self) -> bool {
350 self.generated_samples >= self.total_samples
351 }
352}
353
354pub fn lazy_classification(
356 total_samples: usize,
357 n_features: usize,
358 n_classes: usize,
359 chunk_size: usize,
360 random_state: Option<u64>,
361) -> LazyDatasetGenerator<(Array2<f64>, Array1<i32>)> {
362 LazyDatasetGenerator::new(
363 total_samples,
364 chunk_size,
365 random_state,
366 move |chunk_size, seed| {
367 make_classification(
368 chunk_size, n_features, n_features, 0, n_classes, seed,
371 )
372 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
373 },
374 )
375}
376
377pub fn lazy_regression(
379 total_samples: usize,
380 n_features: usize,
381 chunk_size: usize,
382 random_state: Option<u64>,
383) -> LazyDatasetGenerator<(Array2<f64>, Array1<f64>)> {
384 LazyDatasetGenerator::new(
385 total_samples,
386 chunk_size,
387 random_state,
388 move |chunk_size, seed| {
389 make_regression(
390 chunk_size, n_features, n_features, 0.1, seed,
393 )
394 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
395 },
396 )
397}
398
399#[derive(Debug, Clone)]
401pub struct DistributedConfig {
402 pub total_samples: usize,
404 pub n_nodes: usize,
406 pub node_id: usize,
408 pub random_state: Option<u64>,
410 pub timeout: Duration,
412 pub load_balancing: LoadBalancingStrategy,
414}
415
416#[derive(Debug, Clone)]
418pub enum LoadBalancingStrategy {
419 EqualSplit,
421 Weighted(Vec<f64>),
423 Dynamic,
425}
426
427impl Default for DistributedConfig {
428 fn default() -> Self {
429 Self {
430 total_samples: 100000,
431 n_nodes: 1,
432 node_id: 0,
433 random_state: None,
434 timeout: Duration::from_secs(300), load_balancing: LoadBalancingStrategy::EqualSplit,
436 }
437 }
438}
439
440#[derive(Debug, Clone)]
442pub struct NodeInfo {
443 pub node_id: usize,
444 pub samples_assigned: usize,
445 pub samples_generated: usize,
446 pub status: NodeStatus,
447 pub start_time: Option<Instant>,
448 pub completion_time: Option<Instant>,
449}
450
451#[derive(Debug, Clone, PartialEq)]
453pub enum NodeStatus {
454 Idle,
456 Working,
458 Completed,
460 Failed,
462}
463
464#[derive(Debug)]
466pub struct DistributedGenerationResult<T> {
467 pub data: T,
468 pub node_results: HashMap<usize, NodeResult<T>>,
469 pub total_generation_time: Duration,
470 pub coordination_overhead: Duration,
471 pub n_nodes_used: usize,
472 pub load_balance_efficiency: f64,
473}
474
475#[derive(Debug)]
477pub struct NodeResult<T> {
478 pub node_id: usize,
479 pub data: T,
480 pub generation_time: Duration,
481 pub samples_generated: usize,
482}
483
484#[derive(Debug)]
486pub struct DistributedGenerator {
487 config: DistributedConfig,
488 nodes: HashMap<usize, NodeInfo>,
489}
490
491impl DistributedGenerator {
492 pub fn new(
494 config: DistributedConfig,
495 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
496 if config.node_id >= config.n_nodes {
497 return Err("Node ID must be less than total number of nodes".into());
498 }
499
500 let mut nodes = HashMap::new();
501 for i in 0..config.n_nodes {
502 nodes.insert(
503 i,
504 NodeInfo {
505 node_id: i,
506 samples_assigned: 0,
507 samples_generated: 0,
508 status: NodeStatus::Idle,
509 start_time: None,
510 completion_time: None,
511 },
512 );
513 }
514
515 Ok(Self { config, nodes })
516 }
517
518 pub fn calculate_sample_distribution(
520 &mut self,
521 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
522 match &self.config.load_balancing {
523 LoadBalancingStrategy::EqualSplit => {
524 let base_samples = self.config.total_samples / self.config.n_nodes;
525 let remainder = self.config.total_samples % self.config.n_nodes;
526
527 for i in 0..self.config.n_nodes {
528 if let Some(node) = self.nodes.get_mut(&i) {
529 node.samples_assigned = base_samples + if i < remainder { 1 } else { 0 };
530 }
531 }
532 }
533 LoadBalancingStrategy::Weighted(weights) => {
534 if weights.len() != self.config.n_nodes {
535 return Err("Number of weights must match number of nodes".into());
536 }
537
538 let total_weight: f64 = weights.iter().sum();
539 if total_weight <= 0.0 {
540 return Err("Total weight must be positive".into());
541 }
542
543 let mut assigned_samples = 0;
544 for i in 0..self.config.n_nodes {
546 if let Some(node) = self.nodes.get_mut(&i) {
547 if i < weights.len() - 1 {
548 node.samples_assigned = ((weights[i] / total_weight)
549 * self.config.total_samples as f64)
550 as usize;
551 assigned_samples += node.samples_assigned;
552 } else {
553 node.samples_assigned = self.config.total_samples - assigned_samples;
555 }
556 }
557 }
558 }
559 LoadBalancingStrategy::Dynamic => {
560 let base_samples = self.config.total_samples / self.config.n_nodes;
562 let remainder = self.config.total_samples % self.config.n_nodes;
563
564 for i in 0..self.config.n_nodes {
565 if let Some(node) = self.nodes.get_mut(&i) {
566 node.samples_assigned = base_samples + if i < remainder { 1 } else { 0 };
567 }
568 }
569 }
570 }
571
572 Ok(())
573 }
574
575 pub fn get_current_node_samples(&self) -> usize {
577 self.nodes
578 .get(&self.config.node_id)
579 .map(|node| node.samples_assigned)
580 .unwrap_or(0)
581 }
582
583 pub fn start_generation(&mut self) {
585 if let Some(node) = self.nodes.get_mut(&self.config.node_id) {
586 node.status = NodeStatus::Working;
587 node.start_time = Some(Instant::now());
588 }
589 }
590
591 pub fn complete_generation(&mut self, samples_generated: usize) {
593 if let Some(node) = self.nodes.get_mut(&self.config.node_id) {
594 node.status = NodeStatus::Completed;
595 node.samples_generated = samples_generated;
596 node.completion_time = Some(Instant::now());
597 }
598 }
599
600 pub fn calculate_load_balance_efficiency(&self) -> f64 {
602 let completed_nodes: Vec<_> = self
603 .nodes
604 .values()
605 .filter(|node| node.status == NodeStatus::Completed)
606 .collect();
607
608 if completed_nodes.is_empty() {
609 return 0.0;
610 }
611
612 let generation_times: Vec<Duration> = completed_nodes
613 .iter()
614 .filter_map(|node| {
615 if let (Some(start), Some(end)) = (node.start_time, node.completion_time) {
616 Some(end - start)
617 } else {
618 None
619 }
620 })
621 .collect();
622
623 if generation_times.is_empty() {
624 return 0.0;
625 }
626
627 let total_time: Duration = generation_times.iter().sum();
628 let avg_time = total_time / generation_times.len() as u32;
629 let max_time = generation_times
630 .iter()
631 .max()
632 .expect("collection should not be empty for min/max");
633
634 if max_time.as_nanos() == 0 {
635 return 1.0;
636 }
637
638 (avg_time.as_nanos() as f64) / (max_time.as_nanos() as f64)
639 }
640}
641
642pub fn distributed_classification(
644 n_features: usize,
645 n_classes: usize,
646 config: DistributedConfig,
647) -> DistClassResult {
648 let start_time = Instant::now();
649
650 let mut generator = DistributedGenerator::new(config.clone())?;
651 generator.calculate_sample_distribution()?;
652
653 let samples_for_this_node = generator.get_current_node_samples();
654
655 let node_seed = config
657 .random_state
658 .map(|seed| seed + config.node_id as u64 * 12345);
659
660 generator.start_generation();
661
662 let generation_start = Instant::now();
664 let (x, y) = make_classification(
665 samples_for_this_node,
666 n_features,
667 n_features, 0, n_classes,
670 node_seed,
671 )?;
672 let generation_time = generation_start.elapsed();
673
674 generator.complete_generation(samples_for_this_node);
675
676 let node_result = NodeResult {
678 node_id: config.node_id,
679 data: (x.clone(), y.clone()),
680 generation_time,
681 samples_generated: samples_for_this_node,
682 };
683
684 let mut node_results = HashMap::new();
685 node_results.insert(config.node_id, node_result);
686
687 let total_generation_time = start_time.elapsed();
688 let coordination_overhead = total_generation_time - generation_time;
689 let load_balance_efficiency = generator.calculate_load_balance_efficiency();
690
691 Ok(DistributedGenerationResult {
692 data: (x, y),
693 node_results,
694 total_generation_time,
695 coordination_overhead,
696 n_nodes_used: 1, load_balance_efficiency,
698 })
699}
700
701pub fn distributed_regression(n_features: usize, config: DistributedConfig) -> DistRegrResult {
703 let start_time = Instant::now();
704
705 let mut generator = DistributedGenerator::new(config.clone())?;
706 generator.calculate_sample_distribution()?;
707
708 let samples_for_this_node = generator.get_current_node_samples();
709
710 let node_seed = config
712 .random_state
713 .map(|seed| seed + config.node_id as u64 * 12345);
714
715 generator.start_generation();
716
717 let generation_start = Instant::now();
719 let (x, y) = make_regression(
720 samples_for_this_node,
721 n_features,
722 n_features, 0.1, node_seed,
725 )?;
726 let generation_time = generation_start.elapsed();
727
728 generator.complete_generation(samples_for_this_node);
729
730 let node_result = NodeResult {
732 node_id: config.node_id,
733 data: (x.clone(), y.clone()),
734 generation_time,
735 samples_generated: samples_for_this_node,
736 };
737
738 let mut node_results = HashMap::new();
739 node_results.insert(config.node_id, node_result);
740
741 let total_generation_time = start_time.elapsed();
742 let coordination_overhead = total_generation_time - generation_time;
743 let load_balance_efficiency = generator.calculate_load_balance_efficiency();
744
745 Ok(DistributedGenerationResult {
746 data: (x, y),
747 node_results,
748 total_generation_time,
749 coordination_overhead,
750 n_nodes_used: 1, load_balance_efficiency,
752 })
753}
754
755pub fn distributed_blobs(
757 n_features: usize,
758 centers: usize,
759 config: DistributedConfig,
760) -> DistBlobsResult {
761 let start_time = Instant::now();
762
763 let mut generator = DistributedGenerator::new(config.clone())?;
764 generator.calculate_sample_distribution()?;
765
766 let samples_for_this_node = generator.get_current_node_samples();
767
768 let node_seed = config
770 .random_state
771 .map(|seed| seed + config.node_id as u64 * 12345);
772
773 generator.start_generation();
774
775 let generation_start = Instant::now();
777 let (x, y) = make_blobs(
778 samples_for_this_node,
779 n_features,
780 centers,
781 1.0, node_seed,
783 )?;
784 let generation_time = generation_start.elapsed();
785
786 generator.complete_generation(samples_for_this_node);
787
788 let node_result = NodeResult {
790 node_id: config.node_id,
791 data: (x.clone(), y.clone()),
792 generation_time,
793 samples_generated: samples_for_this_node,
794 };
795
796 let mut node_results = HashMap::new();
797 node_results.insert(config.node_id, node_result);
798
799 let total_generation_time = start_time.elapsed();
800 let coordination_overhead = total_generation_time - generation_time;
801 let load_balance_efficiency = generator.calculate_load_balance_efficiency();
802
803 Ok(DistributedGenerationResult {
804 data: (x, y),
805 node_results,
806 total_generation_time,
807 coordination_overhead,
808 n_nodes_used: 1, load_balance_efficiency,
810 })
811}
812
813#[allow(non_snake_case)]
814#[cfg(test)]
815mod tests {
816 use super::*;
817
818 #[test]
819 fn test_stream_classification() {
820 let config = StreamConfig {
821 chunk_size: 100,
822 total_samples: 300,
823 random_state: Some(42),
824 n_workers: 2,
825 };
826
827 let stream = stream_classification(4, 3, config);
828 let mut total_samples = 0;
829
830 for (i, (x, y)) in stream.enumerate() {
831 assert_eq!(x.ncols(), 4); assert!(y.iter().all(|&label| label < 3)); if i < 2 {
835 assert_eq!(x.nrows(), 100); assert_eq!(y.len(), 100);
837 } else {
838 assert_eq!(x.nrows(), 100); assert_eq!(y.len(), 100);
840 }
841
842 total_samples += x.nrows();
843 }
844
845 assert_eq!(total_samples, 300);
846 }
847
848 #[test]
849 fn test_parallel_classification() {
850 let result = parallel_classification(1000, 5, 3, 4).expect("operation should succeed");
851
852 assert_eq!(result.n_workers_used, 4);
853 assert_eq!(result.chunks.len(), 4);
854
855 let total_samples: usize = result.chunks.iter().map(|(x, _)| x.nrows()).sum();
856 assert_eq!(total_samples, 1000);
857
858 for (x, y) in &result.chunks {
860 assert_eq!(x.ncols(), 5);
861 assert!(y.iter().all(|&label| label < 3));
862 }
863 }
864
865 #[test]
866 fn test_lazy_generator() {
867 let mut generator = lazy_classification(500, 3, 2, 150, Some(42));
868
869 let mut total_samples = 0;
870 let mut chunk_count = 0;
871
872 while !generator.is_complete() {
873 if let Some(result) = generator.next_chunk() {
874 let (x, y) = result.expect("operation should succeed");
875 assert_eq!(x.ncols(), 3);
876 assert!(y.iter().all(|&label| label < 2));
877
878 total_samples += x.nrows();
879 chunk_count += 1;
880
881 let (generated, total, progress) = generator.progress();
882 assert_eq!(generated, total_samples);
883 assert_eq!(total, 500);
884 assert!((0.0..=1.0).contains(&progress));
885 } else {
886 break;
887 }
888 }
889
890 assert_eq!(total_samples, 500);
891 assert_eq!(chunk_count, 4); assert!(generator.is_complete());
893 }
894
895 #[test]
896 fn test_stream_config_default() {
897 let config = StreamConfig::default();
898 assert_eq!(config.chunk_size, 1000);
899 assert_eq!(config.total_samples, 10000);
900 assert!(config.random_state.is_none());
901 assert!(config.n_workers > 0);
902 }
903
904 #[test]
905 fn test_parallel_generation_timing() {
906 let start = std::time::Instant::now();
907 let result = parallel_regression(2000, 10, 2).expect("operation should succeed");
908 let sequential_time = start.elapsed();
909
910 assert!(result.generation_time <= sequential_time * 2); assert_eq!(result.n_workers_used, 2);
912
913 let total_samples: usize = result.chunks.iter().map(|(x, _)| x.nrows()).sum();
914 assert_eq!(total_samples, 2000);
915 }
916
917 #[test]
918 fn test_distributed_config_default() {
919 let config = DistributedConfig::default();
920 assert_eq!(config.total_samples, 100000);
921 assert_eq!(config.n_nodes, 1);
922 assert_eq!(config.node_id, 0);
923 assert!(config.random_state.is_none());
924 assert_eq!(config.timeout, Duration::from_secs(300));
925 assert!(matches!(
926 config.load_balancing,
927 LoadBalancingStrategy::EqualSplit
928 ));
929 }
930
931 #[test]
932 fn test_distributed_generator_sample_distribution() {
933 let config = DistributedConfig {
934 total_samples: 1000,
935 n_nodes: 3,
936 node_id: 0,
937 ..Default::default()
938 };
939
940 let mut generator = DistributedGenerator::new(config).expect("operation should succeed");
941 generator
942 .calculate_sample_distribution()
943 .expect("sampling should succeed");
944
945 assert_eq!(generator.nodes[&0].samples_assigned, 334);
948 assert_eq!(generator.nodes[&1].samples_assigned, 333);
949 assert_eq!(generator.nodes[&2].samples_assigned, 333);
950
951 let total_assigned: usize = generator
952 .nodes
953 .values()
954 .map(|node| node.samples_assigned)
955 .sum();
956 assert_eq!(total_assigned, 1000);
957 }
958
959 #[test]
960 fn test_distributed_generator_weighted_distribution() {
961 let config = DistributedConfig {
962 total_samples: 1000,
963 n_nodes: 3,
964 node_id: 0,
965 load_balancing: LoadBalancingStrategy::Weighted(vec![0.5, 0.3, 0.2]),
966 ..Default::default()
967 };
968
969 let mut generator = DistributedGenerator::new(config).expect("operation should succeed");
970 generator
971 .calculate_sample_distribution()
972 .expect("sampling should succeed");
973
974 assert_eq!(generator.nodes[&0].samples_assigned, 500);
976 assert_eq!(generator.nodes[&1].samples_assigned, 300);
977 assert_eq!(generator.nodes[&2].samples_assigned, 200);
978
979 let total_assigned: usize = generator
980 .nodes
981 .values()
982 .map(|node| node.samples_assigned)
983 .sum();
984 assert_eq!(total_assigned, 1000);
985 }
986
987 #[test]
988 fn test_distributed_classification() {
989 let config = DistributedConfig {
990 total_samples: 1000,
991 n_nodes: 4,
992 node_id: 1,
993 random_state: Some(42),
994 ..Default::default()
995 };
996
997 let result = distributed_classification(5, 3, config).expect("operation should succeed");
998
999 assert_eq!(result.data.0.nrows(), 250);
1001 assert_eq!(result.data.1.len(), 250);
1002 assert_eq!(result.data.0.ncols(), 5);
1003 assert!(result.data.1.iter().all(|&label| label < 3));
1004
1005 assert_eq!(result.n_nodes_used, 1);
1006 assert!(result.node_results.contains_key(&1));
1007 assert_eq!(result.node_results[&1].samples_generated, 250);
1008 assert!(result.total_generation_time > Duration::from_nanos(0));
1009 }
1010
1011 #[test]
1012 fn test_distributed_regression() {
1013 let config = DistributedConfig {
1014 total_samples: 800,
1015 n_nodes: 2,
1016 node_id: 0,
1017 random_state: Some(123),
1018 ..Default::default()
1019 };
1020
1021 let result = distributed_regression(7, config).expect("operation should succeed");
1022
1023 assert_eq!(result.data.0.nrows(), 400);
1025 assert_eq!(result.data.1.len(), 400);
1026 assert_eq!(result.data.0.ncols(), 7);
1027
1028 assert_eq!(result.n_nodes_used, 1);
1029 assert!(result.node_results.contains_key(&0));
1030 assert_eq!(result.node_results[&0].samples_generated, 400);
1031 assert!(result.load_balance_efficiency >= 0.0);
1032 assert!(result.load_balance_efficiency <= 1.0);
1033 }
1034
1035 #[test]
1036 fn test_distributed_blobs() {
1037 let config = DistributedConfig {
1038 total_samples: 600,
1039 n_nodes: 3,
1040 node_id: 2,
1041 random_state: Some(456),
1042 ..Default::default()
1043 };
1044
1045 let result = distributed_blobs(4, 5, config).expect("operation should succeed");
1046
1047 assert_eq!(result.data.0.nrows(), 200);
1049 assert_eq!(result.data.1.len(), 200);
1050 assert_eq!(result.data.0.ncols(), 4);
1051 assert!(result.data.1.iter().all(|&label| label < 5));
1052
1053 assert_eq!(result.n_nodes_used, 1);
1054 assert!(result.node_results.contains_key(&2));
1055 assert_eq!(result.node_results[&2].samples_generated, 200);
1056 assert!(result.coordination_overhead >= Duration::from_nanos(0));
1057 }
1058
1059 #[test]
1060 fn test_distributed_generator_invalid_node_id() {
1061 let config = DistributedConfig {
1062 total_samples: 1000,
1063 n_nodes: 3,
1064 node_id: 3, ..Default::default()
1066 };
1067
1068 let result = DistributedGenerator::new(config);
1069 assert!(result.is_err());
1070 assert!(result
1071 .unwrap_err()
1072 .to_string()
1073 .contains("Node ID must be less than total number of nodes"));
1074 }
1075
1076 #[test]
1077 fn test_distributed_generator_weighted_validation() {
1078 let config = DistributedConfig {
1079 n_nodes: 3,
1080 load_balancing: LoadBalancingStrategy::Weighted(vec![0.5, 0.3]), ..Default::default()
1082 };
1083
1084 let mut generator = DistributedGenerator::new(config).expect("operation should succeed");
1085 let result = generator.calculate_sample_distribution();
1086 assert!(result.is_err());
1087 assert!(result
1088 .unwrap_err()
1089 .to_string()
1090 .contains("Number of weights must match number of nodes"));
1091 }
1092
1093 #[test]
1094 fn test_load_balance_efficiency_calculation() {
1095 let config = DistributedConfig {
1096 n_nodes: 2,
1097 node_id: 0,
1098 ..Default::default()
1099 };
1100
1101 let mut generator = DistributedGenerator::new(config).expect("operation should succeed");
1102
1103 generator
1105 .nodes
1106 .get_mut(&0)
1107 .expect("operation should succeed")
1108 .status = NodeStatus::Completed;
1109 generator
1110 .nodes
1111 .get_mut(&0)
1112 .expect("operation should succeed")
1113 .start_time = Some(Instant::now() - Duration::from_millis(100));
1114 generator
1115 .nodes
1116 .get_mut(&0)
1117 .expect("operation should succeed")
1118 .completion_time = Some(Instant::now());
1119
1120 generator
1121 .nodes
1122 .get_mut(&1)
1123 .expect("operation should succeed")
1124 .status = NodeStatus::Completed;
1125 generator
1126 .nodes
1127 .get_mut(&1)
1128 .expect("operation should succeed")
1129 .start_time = Some(Instant::now() - Duration::from_millis(200));
1130 generator
1131 .nodes
1132 .get_mut(&1)
1133 .expect("operation should succeed")
1134 .completion_time = Some(Instant::now());
1135
1136 let efficiency = generator.calculate_load_balance_efficiency();
1137 assert!(efficiency >= 0.0);
1138 assert!(efficiency <= 1.0);
1139 }
1140}