Skip to main content

sklears_datasets/generators/
performance.rs

1//! Performance-optimized dataset generation
2//!
3//! This module provides high-performance dataset generation capabilities including:
4//! - Streaming dataset generation with lazy evaluation
5//! - Parallel data generation using rayon
6//! - Memory-efficient generation for large datasets
7//! - Chunked processing for distributed systems
8//! - Distributed dataset generation across multiple nodes
9
10use 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
17/// Boxed dynamic error type for parallel/distributed operations
18type BoxedError = Box<dyn std::error::Error + Send + Sync>;
19/// Generator function type: takes chunk size and seed, returns data or error
20type GeneratorFn<T> = Box<dyn Fn(usize, Option<u64>) -> Result<T, BoxedError>>;
21/// Result type for parallel classification: ParallelGenerationResult of (features, labels)
22type ParallelClassResult = Result<ParallelGenerationResult<(Array2<f64>, Array1<i32>)>, BoxedError>;
23/// Result type for parallel regression: ParallelGenerationResult of (features, targets)
24type ParallelRegrResult = Result<ParallelGenerationResult<(Array2<f64>, Array1<f64>)>, BoxedError>;
25/// Result type for parallel blobs: same as classification
26type ParallelBlobsResult = ParallelClassResult;
27/// Result type for distributed classification
28type DistClassResult = Result<DistributedGenerationResult<(Array2<f64>, Array1<i32>)>, BoxedError>;
29/// Result type for distributed regression
30type DistRegrResult = Result<DistributedGenerationResult<(Array2<f64>, Array1<f64>)>, BoxedError>;
31/// Result type for distributed blobs
32type DistBlobsResult = DistClassResult;
33
34/// Configuration for streaming dataset generation
35#[derive(Debug, Clone)]
36pub struct StreamConfig {
37    /// Chunk size for streaming generation
38    pub chunk_size: usize,
39    /// Total number of samples to generate
40    pub total_samples: usize,
41    /// Random seed for reproducibility
42    pub random_state: Option<u64>,
43    /// Number of parallel workers
44    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
58/// Iterator for streaming dataset generation
59pub 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        // Generate unique seed for this chunk if random_state is provided
98        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
110/// Streaming classification dataset generator
111pub 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, // n_informative
119            0,          // n_redundant
120            n_classes, seed,
121        )
122        .expect("operation should succeed")
123    })
124}
125
126/// Streaming regression dataset generator
127pub 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, // n_informative
134            0.1,        // noise
135            seed,
136        )
137        .expect("operation should succeed")
138    })
139}
140
141/// Streaming blob dataset generator
142pub 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, // cluster_std
150            seed,
151        )
152        .expect("operation should succeed")
153    })
154}
155
156/// Parallel dataset generation result
157#[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
164/// Generate datasets in parallel using multiple threads
165pub 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            // Use worker_id as part of the seed for reproducibility
193            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 the sender so the receiver knows when all workers are done
212    drop(tx);
213
214    // Collect results in order
215    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    // Wait for all threads to complete
231    for handle in handles {
232        handle.join().map_err(|_| "Thread panicked")?;
233    }
234
235    // Filter out None values and collect results
236    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
247/// Parallel classification dataset generation
248pub 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, // n_informative
257            0,          // n_redundant
258            n_classes, seed,
259        )
260        .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
261    })
262}
263
264/// Parallel regression dataset generation
265pub 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, // n_informative
273            0.1,        // noise
274            seed,
275        )
276        .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
277    })
278}
279
280/// Parallel blob dataset generation
281pub 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, // cluster_std
290            seed,
291        )
292        .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
293    })
294}
295
296/// Memory-efficient dataset generator that yields chunks on demand
297pub 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    /// Generate the next chunk of data
325    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        // Generate seed based on current position for reproducibility
334        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    /// Get progress information
343    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    /// Check if generation is complete
349    pub fn is_complete(&self) -> bool {
350        self.generated_samples >= self.total_samples
351    }
352}
353
354/// Create a lazy classification dataset generator
355pub 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, // n_informative
369                0,          // n_redundant
370                n_classes, seed,
371            )
372            .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
373        },
374    )
375}
376
377/// Create a lazy regression dataset generator
378pub 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, // n_informative
391                0.1,        // noise
392                seed,
393            )
394            .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
395        },
396    )
397}
398
399/// Configuration for distributed dataset generation
400#[derive(Debug, Clone)]
401pub struct DistributedConfig {
402    /// Total number of samples to generate
403    pub total_samples: usize,
404    /// Number of nodes in the distributed cluster
405    pub n_nodes: usize,
406    /// Node identifier (0 to n_nodes-1)
407    pub node_id: usize,
408    /// Random seed for reproducibility
409    pub random_state: Option<u64>,
410    /// Timeout for node communication
411    pub timeout: Duration,
412    /// Load balancing strategy
413    pub load_balancing: LoadBalancingStrategy,
414}
415
416/// Load balancing strategies for distributed generation
417#[derive(Debug, Clone)]
418pub enum LoadBalancingStrategy {
419    /// Equal distribution of samples across nodes
420    EqualSplit,
421    /// Weighted distribution based on node capabilities
422    Weighted(Vec<f64>),
423    /// Dynamic load balancing based on node performance
424    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), // 5 minutes
435            load_balancing: LoadBalancingStrategy::EqualSplit,
436        }
437    }
438}
439
440/// Node information for distributed generation
441#[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/// Status of a node in distributed generation
452#[derive(Debug, Clone, PartialEq)]
453pub enum NodeStatus {
454    /// Idle
455    Idle,
456    /// Working
457    Working,
458    /// Completed
459    Completed,
460    /// Failed
461    Failed,
462}
463
464/// Result of distributed dataset generation
465#[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/// Result from a single node
476#[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/// Distributed dataset generator coordinator
485#[derive(Debug)]
486pub struct DistributedGenerator {
487    config: DistributedConfig,
488    nodes: HashMap<usize, NodeInfo>,
489}
490
491impl DistributedGenerator {
492    /// Create a new distributed generator
493    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    /// Calculate sample distribution across nodes
519    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                // Iterate through nodes in order (0 to n_nodes-1) to ensure deterministic assignment
545                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                            // Assign remaining samples to last node to ensure exact total
554                            node.samples_assigned = self.config.total_samples - assigned_samples;
555                        }
556                    }
557                }
558            }
559            LoadBalancingStrategy::Dynamic => {
560                // Start with equal split, can be adjusted during runtime
561                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    /// Get the samples assigned to current node
576    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    /// Mark current node as working
584    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    /// Mark current node as completed
592    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    /// Calculate load balance efficiency
601    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
642/// Generate distributed classification dataset
643pub 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    // Generate unique seed for this node
656    let node_seed = config
657        .random_state
658        .map(|seed| seed + config.node_id as u64 * 12345);
659
660    generator.start_generation();
661
662    // Generate data for this node
663    let generation_start = Instant::now();
664    let (x, y) = make_classification(
665        samples_for_this_node,
666        n_features,
667        n_features, // n_informative
668        0,          // n_redundant
669        n_classes,
670        node_seed,
671    )?;
672    let generation_time = generation_start.elapsed();
673
674    generator.complete_generation(samples_for_this_node);
675
676    // Create node result
677    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, // Only current node in this implementation
697        load_balance_efficiency,
698    })
699}
700
701/// Generate distributed regression dataset
702pub 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    // Generate unique seed for this node
711    let node_seed = config
712        .random_state
713        .map(|seed| seed + config.node_id as u64 * 12345);
714
715    generator.start_generation();
716
717    // Generate data for this node
718    let generation_start = Instant::now();
719    let (x, y) = make_regression(
720        samples_for_this_node,
721        n_features,
722        n_features, // n_informative
723        0.1,        // noise
724        node_seed,
725    )?;
726    let generation_time = generation_start.elapsed();
727
728    generator.complete_generation(samples_for_this_node);
729
730    // Create node result
731    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, // Only current node in this implementation
751        load_balance_efficiency,
752    })
753}
754
755/// Generate distributed blob dataset
756pub 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    // Generate unique seed for this node
769    let node_seed = config
770        .random_state
771        .map(|seed| seed + config.node_id as u64 * 12345);
772
773    generator.start_generation();
774
775    // Generate data for this node
776    let generation_start = Instant::now();
777    let (x, y) = make_blobs(
778        samples_for_this_node,
779        n_features,
780        centers,
781        1.0, // cluster_std
782        node_seed,
783    )?;
784    let generation_time = generation_start.elapsed();
785
786    generator.complete_generation(samples_for_this_node);
787
788    // Create node result
789    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, // Only current node in this implementation
809        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); // 4 features
832            assert!(y.iter().all(|&label| label < 3)); // 3 classes (0, 1, 2)
833
834            if i < 2 {
835                assert_eq!(x.nrows(), 100); // First two chunks should be full size
836                assert_eq!(y.len(), 100);
837            } else {
838                assert_eq!(x.nrows(), 100); // Last chunk should be remaining samples
839                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        // Verify all chunks have correct number of features
859        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); // 150 + 150 + 150 + 50
892        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); // Should be reasonably fast
911        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        // Check equal split: 1000 / 3 = 333 remainder 1
946        // Node 0: 334, Node 1: 333, Node 2: 333
947        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        // Check weighted split: 500, 300, 200
975        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        // Check that node 1 gets approximately 250 samples (1000 / 4)
1000        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        // Check that node 0 gets 400 samples (800 / 2)
1024        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        // Check that node 2 gets 200 samples (600 / 3)
1048        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, // Invalid: should be 0, 1, or 2
1065            ..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]), // Wrong length
1081            ..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        // Simulate nodes completing at different times
1104        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}