use crate::generators::basic::{make_blobs, make_classification, make_regression};
use scirs2_core::ndarray::{Array1, Array2};
use std::collections::HashMap;
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
type BoxedError = Box<dyn std::error::Error + Send + Sync>;
type GeneratorFn<T> = Box<dyn Fn(usize, Option<u64>) -> Result<T, BoxedError>>;
type ParallelClassResult = Result<ParallelGenerationResult<(Array2<f64>, Array1<i32>)>, BoxedError>;
type ParallelRegrResult = Result<ParallelGenerationResult<(Array2<f64>, Array1<f64>)>, BoxedError>;
type ParallelBlobsResult = ParallelClassResult;
type DistClassResult = Result<DistributedGenerationResult<(Array2<f64>, Array1<i32>)>, BoxedError>;
type DistRegrResult = Result<DistributedGenerationResult<(Array2<f64>, Array1<f64>)>, BoxedError>;
type DistBlobsResult = DistClassResult;
#[derive(Debug, Clone)]
pub struct StreamConfig {
pub chunk_size: usize,
pub total_samples: usize,
pub random_state: Option<u64>,
pub n_workers: usize,
}
impl Default for StreamConfig {
fn default() -> Self {
Self {
chunk_size: 1000,
total_samples: 10000,
random_state: None,
n_workers: num_cpus::get(),
}
}
}
pub struct DatasetStream<T> {
config: StreamConfig,
current_chunk: usize,
total_chunks: usize,
generator_fn: Box<dyn Fn(usize, usize, Option<u64>) -> T + Send + Sync>,
}
impl<T> DatasetStream<T> {
fn new<F>(config: StreamConfig, generator_fn: F) -> Self
where
F: Fn(usize, usize, Option<u64>) -> T + Send + Sync + 'static,
{
let total_chunks = config.total_samples.div_ceil(config.chunk_size);
Self {
config,
current_chunk: 0,
total_chunks,
generator_fn: Box::new(generator_fn),
}
}
}
impl<T> Iterator for DatasetStream<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
if self.current_chunk >= self.total_chunks {
return None;
}
let chunk_start = self.current_chunk * self.config.chunk_size;
let chunk_end = std::cmp::min(
chunk_start + self.config.chunk_size,
self.config.total_samples,
);
let chunk_size = chunk_end - chunk_start;
let chunk_seed = self
.config
.random_state
.map(|seed| seed + self.current_chunk as u64);
let result = (self.generator_fn)(chunk_size, self.current_chunk, chunk_seed);
self.current_chunk += 1;
Some(result)
}
}
pub fn stream_classification(
n_features: usize,
n_classes: usize,
config: StreamConfig,
) -> DatasetStream<(Array2<f64>, Array1<i32>)> {
DatasetStream::new(config, move |chunk_size, _chunk_idx, seed| {
make_classification(
chunk_size, n_features, n_features, 0, n_classes, seed,
)
.expect("operation should succeed")
})
}
pub fn stream_regression(
n_features: usize,
config: StreamConfig,
) -> DatasetStream<(Array2<f64>, Array1<f64>)> {
DatasetStream::new(config, move |chunk_size, _chunk_idx, seed| {
make_regression(
chunk_size, n_features, n_features, 0.1, seed,
)
.expect("operation should succeed")
})
}
pub fn stream_blobs(
n_features: usize,
centers: usize,
config: StreamConfig,
) -> DatasetStream<(Array2<f64>, Array1<i32>)> {
DatasetStream::new(config, move |chunk_size, _chunk_idx, seed| {
make_blobs(
chunk_size, n_features, centers, 1.0, seed,
)
.expect("operation should succeed")
})
}
#[derive(Debug)]
pub struct ParallelGenerationResult<T> {
pub chunks: Vec<T>,
pub generation_time: std::time::Duration,
pub n_workers_used: usize,
}
pub fn parallel_generate<T, F>(
n_samples: usize,
n_workers: usize,
generator_fn: F,
) -> Result<ParallelGenerationResult<T>, BoxedError>
where
T: Send + 'static,
F: Fn(usize, Option<u64>) -> Result<T, BoxedError> + Send + Sync + Copy + 'static,
{
let start_time = std::time::Instant::now();
let chunk_size = n_samples.div_ceil(n_workers);
let (tx, rx) = mpsc::channel();
let mut handles = Vec::new();
for worker_id in 0..n_workers {
let tx = tx.clone();
let handle = thread::spawn(move || {
let chunk_start = worker_id * chunk_size;
let chunk_end = std::cmp::min(chunk_start + chunk_size, n_samples);
let actual_chunk_size = chunk_end - chunk_start;
if actual_chunk_size == 0 {
return;
}
let seed = Some(worker_id as u64 * 12345);
match generator_fn(actual_chunk_size, seed) {
Ok(result) => {
if tx.send((worker_id, Ok(result))).is_err() {
eprintln!("Failed to send result from worker {}", worker_id);
}
}
Err(e) => {
if tx.send((worker_id, Err(e))).is_err() {
eprintln!("Failed to send error from worker {}", worker_id);
}
}
}
});
handles.push(handle);
}
drop(tx);
let mut results: Vec<Option<T>> = (0..n_workers).map(|_| None).collect();
let mut successful_workers = 0;
for (worker_id, result) in rx {
match result {
Ok(data) => {
results[worker_id] = Some(data);
successful_workers += 1;
}
Err(e) => {
return Err(format!("Worker {} failed: {}", worker_id, e).into());
}
}
}
for handle in handles {
handle.join().map_err(|_| "Thread panicked")?;
}
let chunks: Vec<T> = results.into_iter().flatten().collect();
let generation_time = start_time.elapsed();
Ok(ParallelGenerationResult {
chunks,
generation_time,
n_workers_used: successful_workers,
})
}
pub fn parallel_classification(
n_samples: usize,
n_features: usize,
n_classes: usize,
n_workers: usize,
) -> ParallelClassResult {
parallel_generate(n_samples, n_workers, move |chunk_size, seed| {
make_classification(
chunk_size, n_features, n_features, 0, n_classes, seed,
)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
})
}
pub fn parallel_regression(
n_samples: usize,
n_features: usize,
n_workers: usize,
) -> ParallelRegrResult {
parallel_generate(n_samples, n_workers, move |chunk_size, seed| {
make_regression(
chunk_size, n_features, n_features, 0.1, seed,
)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
})
}
pub fn parallel_blobs(
n_samples: usize,
n_features: usize,
centers: usize,
n_workers: usize,
) -> ParallelBlobsResult {
parallel_generate(n_samples, n_workers, move |chunk_size, seed| {
make_blobs(
chunk_size, n_features, centers, 1.0, seed,
)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
})
}
pub struct LazyDatasetGenerator<T> {
chunk_size: usize,
total_samples: usize,
generated_samples: usize,
generator_fn: GeneratorFn<T>,
random_state: Option<u64>,
}
impl<T> LazyDatasetGenerator<T> {
pub fn new<F>(
total_samples: usize,
chunk_size: usize,
random_state: Option<u64>,
generator_fn: F,
) -> Self
where
F: Fn(usize, Option<u64>) -> Result<T, BoxedError> + 'static,
{
Self {
chunk_size,
total_samples,
generated_samples: 0,
generator_fn: Box::new(generator_fn),
random_state,
}
}
pub fn next_chunk(&mut self) -> Option<Result<T, BoxedError>> {
if self.generated_samples >= self.total_samples {
return None;
}
let remaining_samples = self.total_samples - self.generated_samples;
let current_chunk_size = std::cmp::min(self.chunk_size, remaining_samples);
let seed = self.random_state.map(|s| s + self.generated_samples as u64);
let result = (self.generator_fn)(current_chunk_size, seed);
self.generated_samples += current_chunk_size;
Some(result)
}
pub fn progress(&self) -> (usize, usize, f64) {
let progress_ratio = self.generated_samples as f64 / self.total_samples as f64;
(self.generated_samples, self.total_samples, progress_ratio)
}
pub fn is_complete(&self) -> bool {
self.generated_samples >= self.total_samples
}
}
pub fn lazy_classification(
total_samples: usize,
n_features: usize,
n_classes: usize,
chunk_size: usize,
random_state: Option<u64>,
) -> LazyDatasetGenerator<(Array2<f64>, Array1<i32>)> {
LazyDatasetGenerator::new(
total_samples,
chunk_size,
random_state,
move |chunk_size, seed| {
make_classification(
chunk_size, n_features, n_features, 0, n_classes, seed,
)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
},
)
}
pub fn lazy_regression(
total_samples: usize,
n_features: usize,
chunk_size: usize,
random_state: Option<u64>,
) -> LazyDatasetGenerator<(Array2<f64>, Array1<f64>)> {
LazyDatasetGenerator::new(
total_samples,
chunk_size,
random_state,
move |chunk_size, seed| {
make_regression(
chunk_size, n_features, n_features, 0.1, seed,
)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
},
)
}
#[derive(Debug, Clone)]
pub struct DistributedConfig {
pub total_samples: usize,
pub n_nodes: usize,
pub node_id: usize,
pub random_state: Option<u64>,
pub timeout: Duration,
pub load_balancing: LoadBalancingStrategy,
}
#[derive(Debug, Clone)]
pub enum LoadBalancingStrategy {
EqualSplit,
Weighted(Vec<f64>),
Dynamic,
}
impl Default for DistributedConfig {
fn default() -> Self {
Self {
total_samples: 100000,
n_nodes: 1,
node_id: 0,
random_state: None,
timeout: Duration::from_secs(300), load_balancing: LoadBalancingStrategy::EqualSplit,
}
}
}
#[derive(Debug, Clone)]
pub struct NodeInfo {
pub node_id: usize,
pub samples_assigned: usize,
pub samples_generated: usize,
pub status: NodeStatus,
pub start_time: Option<Instant>,
pub completion_time: Option<Instant>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum NodeStatus {
Idle,
Working,
Completed,
Failed,
}
#[derive(Debug)]
pub struct DistributedGenerationResult<T> {
pub data: T,
pub node_results: HashMap<usize, NodeResult<T>>,
pub total_generation_time: Duration,
pub coordination_overhead: Duration,
pub n_nodes_used: usize,
pub load_balance_efficiency: f64,
}
#[derive(Debug)]
pub struct NodeResult<T> {
pub node_id: usize,
pub data: T,
pub generation_time: Duration,
pub samples_generated: usize,
}
#[derive(Debug)]
pub struct DistributedGenerator {
config: DistributedConfig,
nodes: HashMap<usize, NodeInfo>,
}
impl DistributedGenerator {
pub fn new(
config: DistributedConfig,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
if config.node_id >= config.n_nodes {
return Err("Node ID must be less than total number of nodes".into());
}
let mut nodes = HashMap::new();
for i in 0..config.n_nodes {
nodes.insert(
i,
NodeInfo {
node_id: i,
samples_assigned: 0,
samples_generated: 0,
status: NodeStatus::Idle,
start_time: None,
completion_time: None,
},
);
}
Ok(Self { config, nodes })
}
pub fn calculate_sample_distribution(
&mut self,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
match &self.config.load_balancing {
LoadBalancingStrategy::EqualSplit => {
let base_samples = self.config.total_samples / self.config.n_nodes;
let remainder = self.config.total_samples % self.config.n_nodes;
for i in 0..self.config.n_nodes {
if let Some(node) = self.nodes.get_mut(&i) {
node.samples_assigned = base_samples + if i < remainder { 1 } else { 0 };
}
}
}
LoadBalancingStrategy::Weighted(weights) => {
if weights.len() != self.config.n_nodes {
return Err("Number of weights must match number of nodes".into());
}
let total_weight: f64 = weights.iter().sum();
if total_weight <= 0.0 {
return Err("Total weight must be positive".into());
}
let mut assigned_samples = 0;
for i in 0..self.config.n_nodes {
if let Some(node) = self.nodes.get_mut(&i) {
if i < weights.len() - 1 {
node.samples_assigned = ((weights[i] / total_weight)
* self.config.total_samples as f64)
as usize;
assigned_samples += node.samples_assigned;
} else {
node.samples_assigned = self.config.total_samples - assigned_samples;
}
}
}
}
LoadBalancingStrategy::Dynamic => {
let base_samples = self.config.total_samples / self.config.n_nodes;
let remainder = self.config.total_samples % self.config.n_nodes;
for i in 0..self.config.n_nodes {
if let Some(node) = self.nodes.get_mut(&i) {
node.samples_assigned = base_samples + if i < remainder { 1 } else { 0 };
}
}
}
}
Ok(())
}
pub fn get_current_node_samples(&self) -> usize {
self.nodes
.get(&self.config.node_id)
.map(|node| node.samples_assigned)
.unwrap_or(0)
}
pub fn start_generation(&mut self) {
if let Some(node) = self.nodes.get_mut(&self.config.node_id) {
node.status = NodeStatus::Working;
node.start_time = Some(Instant::now());
}
}
pub fn complete_generation(&mut self, samples_generated: usize) {
if let Some(node) = self.nodes.get_mut(&self.config.node_id) {
node.status = NodeStatus::Completed;
node.samples_generated = samples_generated;
node.completion_time = Some(Instant::now());
}
}
pub fn calculate_load_balance_efficiency(&self) -> f64 {
let completed_nodes: Vec<_> = self
.nodes
.values()
.filter(|node| node.status == NodeStatus::Completed)
.collect();
if completed_nodes.is_empty() {
return 0.0;
}
let generation_times: Vec<Duration> = completed_nodes
.iter()
.filter_map(|node| {
if let (Some(start), Some(end)) = (node.start_time, node.completion_time) {
Some(end - start)
} else {
None
}
})
.collect();
if generation_times.is_empty() {
return 0.0;
}
let total_time: Duration = generation_times.iter().sum();
let avg_time = total_time / generation_times.len() as u32;
let max_time = generation_times
.iter()
.max()
.expect("collection should not be empty for min/max");
if max_time.as_nanos() == 0 {
return 1.0;
}
(avg_time.as_nanos() as f64) / (max_time.as_nanos() as f64)
}
}
pub fn distributed_classification(
n_features: usize,
n_classes: usize,
config: DistributedConfig,
) -> DistClassResult {
let start_time = Instant::now();
let mut generator = DistributedGenerator::new(config.clone())?;
generator.calculate_sample_distribution()?;
let samples_for_this_node = generator.get_current_node_samples();
let node_seed = config
.random_state
.map(|seed| seed + config.node_id as u64 * 12345);
generator.start_generation();
let generation_start = Instant::now();
let (x, y) = make_classification(
samples_for_this_node,
n_features,
n_features, 0, n_classes,
node_seed,
)?;
let generation_time = generation_start.elapsed();
generator.complete_generation(samples_for_this_node);
let node_result = NodeResult {
node_id: config.node_id,
data: (x.clone(), y.clone()),
generation_time,
samples_generated: samples_for_this_node,
};
let mut node_results = HashMap::new();
node_results.insert(config.node_id, node_result);
let total_generation_time = start_time.elapsed();
let coordination_overhead = total_generation_time - generation_time;
let load_balance_efficiency = generator.calculate_load_balance_efficiency();
Ok(DistributedGenerationResult {
data: (x, y),
node_results,
total_generation_time,
coordination_overhead,
n_nodes_used: 1, load_balance_efficiency,
})
}
pub fn distributed_regression(n_features: usize, config: DistributedConfig) -> DistRegrResult {
let start_time = Instant::now();
let mut generator = DistributedGenerator::new(config.clone())?;
generator.calculate_sample_distribution()?;
let samples_for_this_node = generator.get_current_node_samples();
let node_seed = config
.random_state
.map(|seed| seed + config.node_id as u64 * 12345);
generator.start_generation();
let generation_start = Instant::now();
let (x, y) = make_regression(
samples_for_this_node,
n_features,
n_features, 0.1, node_seed,
)?;
let generation_time = generation_start.elapsed();
generator.complete_generation(samples_for_this_node);
let node_result = NodeResult {
node_id: config.node_id,
data: (x.clone(), y.clone()),
generation_time,
samples_generated: samples_for_this_node,
};
let mut node_results = HashMap::new();
node_results.insert(config.node_id, node_result);
let total_generation_time = start_time.elapsed();
let coordination_overhead = total_generation_time - generation_time;
let load_balance_efficiency = generator.calculate_load_balance_efficiency();
Ok(DistributedGenerationResult {
data: (x, y),
node_results,
total_generation_time,
coordination_overhead,
n_nodes_used: 1, load_balance_efficiency,
})
}
pub fn distributed_blobs(
n_features: usize,
centers: usize,
config: DistributedConfig,
) -> DistBlobsResult {
let start_time = Instant::now();
let mut generator = DistributedGenerator::new(config.clone())?;
generator.calculate_sample_distribution()?;
let samples_for_this_node = generator.get_current_node_samples();
let node_seed = config
.random_state
.map(|seed| seed + config.node_id as u64 * 12345);
generator.start_generation();
let generation_start = Instant::now();
let (x, y) = make_blobs(
samples_for_this_node,
n_features,
centers,
1.0, node_seed,
)?;
let generation_time = generation_start.elapsed();
generator.complete_generation(samples_for_this_node);
let node_result = NodeResult {
node_id: config.node_id,
data: (x.clone(), y.clone()),
generation_time,
samples_generated: samples_for_this_node,
};
let mut node_results = HashMap::new();
node_results.insert(config.node_id, node_result);
let total_generation_time = start_time.elapsed();
let coordination_overhead = total_generation_time - generation_time;
let load_balance_efficiency = generator.calculate_load_balance_efficiency();
Ok(DistributedGenerationResult {
data: (x, y),
node_results,
total_generation_time,
coordination_overhead,
n_nodes_used: 1, load_balance_efficiency,
})
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stream_classification() {
let config = StreamConfig {
chunk_size: 100,
total_samples: 300,
random_state: Some(42),
n_workers: 2,
};
let stream = stream_classification(4, 3, config);
let mut total_samples = 0;
for (i, (x, y)) in stream.enumerate() {
assert_eq!(x.ncols(), 4); assert!(y.iter().all(|&label| label < 3));
if i < 2 {
assert_eq!(x.nrows(), 100); assert_eq!(y.len(), 100);
} else {
assert_eq!(x.nrows(), 100); assert_eq!(y.len(), 100);
}
total_samples += x.nrows();
}
assert_eq!(total_samples, 300);
}
#[test]
fn test_parallel_classification() {
let result = parallel_classification(1000, 5, 3, 4).expect("operation should succeed");
assert_eq!(result.n_workers_used, 4);
assert_eq!(result.chunks.len(), 4);
let total_samples: usize = result.chunks.iter().map(|(x, _)| x.nrows()).sum();
assert_eq!(total_samples, 1000);
for (x, y) in &result.chunks {
assert_eq!(x.ncols(), 5);
assert!(y.iter().all(|&label| label < 3));
}
}
#[test]
fn test_lazy_generator() {
let mut generator = lazy_classification(500, 3, 2, 150, Some(42));
let mut total_samples = 0;
let mut chunk_count = 0;
while !generator.is_complete() {
if let Some(result) = generator.next_chunk() {
let (x, y) = result.expect("operation should succeed");
assert_eq!(x.ncols(), 3);
assert!(y.iter().all(|&label| label < 2));
total_samples += x.nrows();
chunk_count += 1;
let (generated, total, progress) = generator.progress();
assert_eq!(generated, total_samples);
assert_eq!(total, 500);
assert!((0.0..=1.0).contains(&progress));
} else {
break;
}
}
assert_eq!(total_samples, 500);
assert_eq!(chunk_count, 4); assert!(generator.is_complete());
}
#[test]
fn test_stream_config_default() {
let config = StreamConfig::default();
assert_eq!(config.chunk_size, 1000);
assert_eq!(config.total_samples, 10000);
assert!(config.random_state.is_none());
assert!(config.n_workers > 0);
}
#[test]
fn test_parallel_generation_timing() {
let start = std::time::Instant::now();
let result = parallel_regression(2000, 10, 2).expect("operation should succeed");
let sequential_time = start.elapsed();
assert!(result.generation_time <= sequential_time * 2); assert_eq!(result.n_workers_used, 2);
let total_samples: usize = result.chunks.iter().map(|(x, _)| x.nrows()).sum();
assert_eq!(total_samples, 2000);
}
#[test]
fn test_distributed_config_default() {
let config = DistributedConfig::default();
assert_eq!(config.total_samples, 100000);
assert_eq!(config.n_nodes, 1);
assert_eq!(config.node_id, 0);
assert!(config.random_state.is_none());
assert_eq!(config.timeout, Duration::from_secs(300));
assert!(matches!(
config.load_balancing,
LoadBalancingStrategy::EqualSplit
));
}
#[test]
fn test_distributed_generator_sample_distribution() {
let config = DistributedConfig {
total_samples: 1000,
n_nodes: 3,
node_id: 0,
..Default::default()
};
let mut generator = DistributedGenerator::new(config).expect("operation should succeed");
generator
.calculate_sample_distribution()
.expect("sampling should succeed");
assert_eq!(generator.nodes[&0].samples_assigned, 334);
assert_eq!(generator.nodes[&1].samples_assigned, 333);
assert_eq!(generator.nodes[&2].samples_assigned, 333);
let total_assigned: usize = generator
.nodes
.values()
.map(|node| node.samples_assigned)
.sum();
assert_eq!(total_assigned, 1000);
}
#[test]
fn test_distributed_generator_weighted_distribution() {
let config = DistributedConfig {
total_samples: 1000,
n_nodes: 3,
node_id: 0,
load_balancing: LoadBalancingStrategy::Weighted(vec![0.5, 0.3, 0.2]),
..Default::default()
};
let mut generator = DistributedGenerator::new(config).expect("operation should succeed");
generator
.calculate_sample_distribution()
.expect("sampling should succeed");
assert_eq!(generator.nodes[&0].samples_assigned, 500);
assert_eq!(generator.nodes[&1].samples_assigned, 300);
assert_eq!(generator.nodes[&2].samples_assigned, 200);
let total_assigned: usize = generator
.nodes
.values()
.map(|node| node.samples_assigned)
.sum();
assert_eq!(total_assigned, 1000);
}
#[test]
fn test_distributed_classification() {
let config = DistributedConfig {
total_samples: 1000,
n_nodes: 4,
node_id: 1,
random_state: Some(42),
..Default::default()
};
let result = distributed_classification(5, 3, config).expect("operation should succeed");
assert_eq!(result.data.0.nrows(), 250);
assert_eq!(result.data.1.len(), 250);
assert_eq!(result.data.0.ncols(), 5);
assert!(result.data.1.iter().all(|&label| label < 3));
assert_eq!(result.n_nodes_used, 1);
assert!(result.node_results.contains_key(&1));
assert_eq!(result.node_results[&1].samples_generated, 250);
assert!(result.total_generation_time > Duration::from_nanos(0));
}
#[test]
fn test_distributed_regression() {
let config = DistributedConfig {
total_samples: 800,
n_nodes: 2,
node_id: 0,
random_state: Some(123),
..Default::default()
};
let result = distributed_regression(7, config).expect("operation should succeed");
assert_eq!(result.data.0.nrows(), 400);
assert_eq!(result.data.1.len(), 400);
assert_eq!(result.data.0.ncols(), 7);
assert_eq!(result.n_nodes_used, 1);
assert!(result.node_results.contains_key(&0));
assert_eq!(result.node_results[&0].samples_generated, 400);
assert!(result.load_balance_efficiency >= 0.0);
assert!(result.load_balance_efficiency <= 1.0);
}
#[test]
fn test_distributed_blobs() {
let config = DistributedConfig {
total_samples: 600,
n_nodes: 3,
node_id: 2,
random_state: Some(456),
..Default::default()
};
let result = distributed_blobs(4, 5, config).expect("operation should succeed");
assert_eq!(result.data.0.nrows(), 200);
assert_eq!(result.data.1.len(), 200);
assert_eq!(result.data.0.ncols(), 4);
assert!(result.data.1.iter().all(|&label| label < 5));
assert_eq!(result.n_nodes_used, 1);
assert!(result.node_results.contains_key(&2));
assert_eq!(result.node_results[&2].samples_generated, 200);
assert!(result.coordination_overhead >= Duration::from_nanos(0));
}
#[test]
fn test_distributed_generator_invalid_node_id() {
let config = DistributedConfig {
total_samples: 1000,
n_nodes: 3,
node_id: 3, ..Default::default()
};
let result = DistributedGenerator::new(config);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Node ID must be less than total number of nodes"));
}
#[test]
fn test_distributed_generator_weighted_validation() {
let config = DistributedConfig {
n_nodes: 3,
load_balancing: LoadBalancingStrategy::Weighted(vec![0.5, 0.3]), ..Default::default()
};
let mut generator = DistributedGenerator::new(config).expect("operation should succeed");
let result = generator.calculate_sample_distribution();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Number of weights must match number of nodes"));
}
#[test]
fn test_load_balance_efficiency_calculation() {
let config = DistributedConfig {
n_nodes: 2,
node_id: 0,
..Default::default()
};
let mut generator = DistributedGenerator::new(config).expect("operation should succeed");
generator
.nodes
.get_mut(&0)
.expect("operation should succeed")
.status = NodeStatus::Completed;
generator
.nodes
.get_mut(&0)
.expect("operation should succeed")
.start_time = Some(Instant::now() - Duration::from_millis(100));
generator
.nodes
.get_mut(&0)
.expect("operation should succeed")
.completion_time = Some(Instant::now());
generator
.nodes
.get_mut(&1)
.expect("operation should succeed")
.status = NodeStatus::Completed;
generator
.nodes
.get_mut(&1)
.expect("operation should succeed")
.start_time = Some(Instant::now() - Duration::from_millis(200));
generator
.nodes
.get_mut(&1)
.expect("operation should succeed")
.completion_time = Some(Instant::now());
let efficiency = generator.calculate_load_balance_efficiency();
assert!(efficiency >= 0.0);
assert!(efficiency <= 1.0);
}
}