Skip to main content

sklears_datasets/generators/
multimodal.rs

1//! Multi-modal and multi-agent environment generators
2//!
3//! This module contains generators for multi-modal datasets and multi-agent
4//! reinforcement learning environments including vision-language, audio-visual,
5//! and cooperative multi-agent scenarios.
6
7use scirs2_core::ndarray::{s, Array1, Array2, Array3};
8use scirs2_core::random::prelude::*;
9use scirs2_core::random::rngs::StdRng;
10use scirs2_core::random::{Normal, RngExt};
11use sklears_core::error::{Result, SklearsError};
12use std::f64::consts::PI;
13
14/// Result type for multi-agent environment: (states, actions, rewards, cumulative_rewards)
15type MultiAgentResult = (Array3<usize>, Array3<usize>, Array2<f64>, Array1<f64>);
16/// Result type for communication cost datasets: (upload, download, round_times, bandwidth)
17type CommCostResult = (Array2<f64>, Array2<f64>, Array1<f64>, Array1<f64>);
18/// Result type for sensor fusion dataset: (sensor_data, timestamps, events)
19type SensorFusionResult = (Vec<Array2<f64>>, Array1<f64>, Array1<usize>);
20/// Result type for multimodal alignment dataset: (modality_data, alignment, scores)
21type MultimodalAlignResult = (Vec<Array2<f64>>, Array2<f64>, Array1<f64>);
22/// Result type for cross-modal retrieval dataset: (source, target, labels, difficulty_scores)
23type CrossModalRetrievalResult = (Array2<f64>, Array2<f64>, Array1<usize>, Array1<f64>);
24
25/// Multi-agent environment configuration
26#[derive(Debug, Clone)]
27pub struct MultiAgentConfig {
28    pub n_agents: usize,
29    pub n_states: usize,
30    pub n_actions: usize,
31    pub cooperation_level: f64,
32    pub communication_enabled: bool,
33    pub reward_sharing: bool,
34}
35
36/// Generate multi-agent environment simulation data
37pub fn make_multi_agent_environment(
38    config: MultiAgentConfig,
39    n_episodes: usize,
40    episode_length: usize,
41    random_state: Option<u64>,
42) -> Result<MultiAgentResult> {
43    if config.n_agents == 0 || config.n_states == 0 || config.n_actions == 0 {
44        return Err(SklearsError::InvalidInput(
45            "n_agents, n_states, and n_actions must be positive".to_string(),
46        ));
47    }
48
49    if config.cooperation_level < 0.0 || config.cooperation_level > 1.0 {
50        return Err(SklearsError::InvalidInput(
51            "cooperation_level must be in [0, 1]".to_string(),
52        ));
53    }
54
55    let mut rng = if let Some(seed) = random_state {
56        StdRng::seed_from_u64(seed)
57    } else {
58        StdRng::from_rng(&mut scirs2_core::random::thread_rng())
59    };
60
61    // States: [episode, timestep, agent] -> state_id
62    let mut states = Array3::zeros((n_episodes, episode_length, config.n_agents));
63    // Actions: [episode, timestep, agent] -> action_id
64    let mut actions = Array3::zeros((n_episodes, episode_length, config.n_agents));
65    // Rewards: [episode, timestep] -> reward (shared or individual)
66    let mut rewards = Array2::zeros((n_episodes, episode_length));
67    // Global rewards per episode
68    let mut episode_rewards = Array1::zeros(n_episodes);
69
70    for episode in 0..n_episodes {
71        let mut episode_reward = 0.0;
72
73        for timestep in 0..episode_length {
74            let mut timestep_reward = 0.0;
75
76            // Generate states and actions for each agent
77            for agent in 0..config.n_agents {
78                // State depends on previous states if not first timestep
79                let state = if timestep == 0 {
80                    rng.random_range(0..config.n_states)
81                } else {
82                    // State transition influenced by other agents if cooperation is enabled
83                    if config.cooperation_level > 0.0
84                        && rng.random::<f64>() < config.cooperation_level
85                    {
86                        let other_agent = rng.random_range(0..config.n_agents);
87                        if other_agent != agent {
88                            let other_state = states[[episode, timestep - 1, other_agent]];
89                            (other_state + rng.random_range(0..3)) % config.n_states
90                        } else {
91                            rng.random_range(0..config.n_states)
92                        }
93                    } else {
94                        rng.random_range(0..config.n_states)
95                    }
96                };
97
98                states[[episode, timestep, agent]] = state;
99
100                // Action selection - can be influenced by communication
101                let action = if config.communication_enabled && rng.random::<f64>() < 0.3 {
102                    // Agent considers other agents' previous actions
103                    if timestep > 0 {
104                        let other_agent = rng.random_range(0..config.n_agents);
105                        let other_action = actions[[episode, timestep - 1, other_agent]];
106                        (other_action + rng.random_range(0..2)) % config.n_actions
107                    } else {
108                        rng.random_range(0..config.n_actions)
109                    }
110                } else {
111                    rng.random_range(0..config.n_actions)
112                };
113
114                actions[[episode, timestep, agent]] = action;
115
116                // Calculate individual reward
117                let individual_reward = if state == action % config.n_states {
118                    1.0
119                } else {
120                    0.1 * rng.random::<f64>()
121                };
122
123                if config.reward_sharing {
124                    timestep_reward += individual_reward / config.n_agents as f64;
125                } else {
126                    timestep_reward += individual_reward;
127                }
128            }
129
130            // Add cooperation bonus
131            if config.cooperation_level > 0.0 {
132                let mut coordination_bonus = 0.0;
133                for agent1 in 0..config.n_agents {
134                    for agent2 in (agent1 + 1)..config.n_agents {
135                        let state_diff = (states[[episode, timestep, agent1]] as i32
136                            - states[[episode, timestep, agent2]] as i32)
137                            .abs();
138                        if state_diff <= 1 {
139                            coordination_bonus += 0.5 * config.cooperation_level;
140                        }
141                    }
142                }
143                timestep_reward += coordination_bonus;
144            }
145
146            rewards[[episode, timestep]] = timestep_reward;
147            episode_reward += timestep_reward;
148        }
149
150        episode_rewards[episode] = episode_reward;
151    }
152
153    Ok((states, actions, rewards, episode_rewards))
154}
155
156/// Generate vision-language aligned dataset
157pub fn make_vision_language_dataset(
158    n_samples: usize,
159    image_size: (usize, usize),
160    vocab_size: usize,
161    max_sequence_length: usize,
162    alignment_strength: f64,
163    random_state: Option<u64>,
164) -> Result<(Array3<f64>, Array2<usize>, Array1<f64>)> {
165    if n_samples == 0 || vocab_size == 0 || max_sequence_length == 0 {
166        return Err(SklearsError::InvalidInput(
167            "n_samples, vocab_size, and max_sequence_length must be positive".to_string(),
168        ));
169    }
170
171    if !(0.0..=1.0).contains(&alignment_strength) {
172        return Err(SklearsError::InvalidInput(
173            "alignment_strength must be in [0, 1]".to_string(),
174        ));
175    }
176
177    let mut rng = if let Some(seed) = random_state {
178        StdRng::seed_from_u64(seed)
179    } else {
180        StdRng::from_rng(&mut scirs2_core::random::thread_rng())
181    };
182
183    let (height, width) = image_size;
184
185    // Generate synthetic images
186    let mut images = Array3::zeros((n_samples, height, width));
187    let normal = Normal::new(0.5, 0.3).expect("operation should succeed");
188
189    for i in 0..n_samples {
190        for h in 0..height {
191            for w in 0..width {
192                let sample: f64 = rng.sample(normal);
193                images[[i, h, w]] = sample.clamp(0.0, 1.0);
194            }
195        }
196    }
197
198    // Generate text sequences aligned with images
199    let mut texts = Array2::zeros((n_samples, max_sequence_length));
200    let mut alignment_scores = Array1::zeros(n_samples);
201
202    for i in 0..n_samples {
203        // Calculate image features (mean intensity in regions)
204        let image_mean = images.slice(s![i, .., ..]).mean().unwrap_or(0.5);
205
206        // Generate text based on image features
207        for j in 0..max_sequence_length {
208            let token = if rng.random::<f64>() < alignment_strength {
209                // Aligned token based on image features
210
211                ((image_mean * vocab_size as f64) as usize).min(vocab_size - 1)
212            } else {
213                // Random token
214                rng.random_range(0..vocab_size)
215            };
216            texts[[i, j]] = token;
217        }
218
219        // Calculate alignment score
220        let text_diversity = texts
221            .slice(s![i, ..])
222            .iter()
223            .map(|&t| t as f64 / vocab_size as f64)
224            .collect::<Vec<_>>();
225        let text_mean = text_diversity.iter().sum::<f64>() / max_sequence_length as f64;
226        alignment_scores[i] = 1.0 - (image_mean - text_mean).abs();
227    }
228
229    Ok((images, texts, alignment_scores))
230}
231
232/// Generate audio-visual aligned dataset
233pub fn make_audio_visual_dataset(
234    n_samples: usize,
235    audio_length: usize,
236    video_frames: usize,
237    frame_size: (usize, usize),
238    sync_strength: f64,
239    random_state: Option<u64>,
240) -> Result<(Array2<f64>, Array2<f64>, Array1<f64>)> {
241    if n_samples == 0 || audio_length == 0 || video_frames == 0 {
242        return Err(SklearsError::InvalidInput(
243            "n_samples, audio_length, and video_frames must be positive".to_string(),
244        ));
245    }
246
247    if !(0.0..=1.0).contains(&sync_strength) {
248        return Err(SklearsError::InvalidInput(
249            "sync_strength must be in [0, 1]".to_string(),
250        ));
251    }
252
253    let mut rng = if let Some(seed) = random_state {
254        StdRng::seed_from_u64(seed)
255    } else {
256        StdRng::from_rng(&mut scirs2_core::random::thread_rng())
257    };
258
259    let (height, width) = frame_size;
260
261    // Generate audio signals
262    let mut audio = Array2::zeros((n_samples, audio_length));
263    let normal = Normal::new(0.0, 0.5).expect("operation should succeed");
264
265    for i in 0..n_samples {
266        let base_frequency = rng.random_range(0.1..0.5);
267        for t in 0..audio_length {
268            let time = t as f64 / audio_length as f64;
269            let signal = (2.0 * PI * base_frequency * time).sin();
270            let noise: f64 = rng.sample(normal);
271            audio[[i, t]] = signal + 0.1 * noise;
272        }
273    }
274
275    // Generate video frames synchronized with audio
276    let mut video = Array2::zeros((n_samples, video_frames * height * width));
277    let mut sync_scores = Array1::zeros(n_samples);
278
279    for i in 0..n_samples {
280        let audio_energy = audio.slice(s![i, ..]).mapv(|x| x * x).mean().unwrap_or(0.0);
281
282        for frame in 0..video_frames {
283            let frame_start = frame * height * width;
284            let audio_frame_idx = (frame * audio_length) / video_frames;
285            let audio_frame_energy = if audio_frame_idx < audio_length {
286                audio[[i, audio_frame_idx]].abs()
287            } else {
288                0.0
289            };
290
291            for pixel in 0..(height * width) {
292                let idx = frame_start + pixel;
293                if rng.random::<f64>() < sync_strength {
294                    // Synchronized pixel based on audio
295                    video[[i, idx]] = audio_frame_energy + 0.2 * rng.random::<f64>();
296                } else {
297                    // Random pixel
298                    video[[i, idx]] = rng.random::<f64>();
299                }
300            }
301        }
302
303        // Calculate synchronization score
304        let video_energy = video.slice(s![i, ..]).mapv(|x| x * x).mean().unwrap_or(0.0);
305        sync_scores[i] = 1.0 - (audio_energy.sqrt() - video_energy.sqrt()).abs();
306    }
307
308    Ok((audio, video, sync_scores))
309}
310
311/// Communication cost configuration for federated learning
312#[derive(Debug, Clone)]
313pub struct CommunicationCostConfig {
314    pub n_clients: usize,
315    pub network_topology: String, // "star", "ring", "full_mesh", "hierarchical"
316    pub bandwidth_mbps: f64,
317    pub latency_ms: f64,
318    pub packet_loss_rate: f64,
319    pub compression_ratio: f64,
320}
321
322/// Generate communication cost datasets for federated learning
323pub fn make_communication_cost_datasets(
324    config: CommunicationCostConfig,
325    n_rounds: usize,
326    model_size_mb: f64,
327    random_state: Option<u64>,
328) -> Result<CommCostResult> {
329    if config.n_clients == 0 || n_rounds == 0 {
330        return Err(SklearsError::InvalidInput(
331            "n_clients and n_rounds must be positive".to_string(),
332        ));
333    }
334
335    if model_size_mb <= 0.0 || config.bandwidth_mbps <= 0.0 || config.latency_ms < 0.0 {
336        return Err(SklearsError::InvalidInput(
337            "model_size_mb, bandwidth_mbps must be positive, latency_ms must be non-negative"
338                .to_string(),
339        ));
340    }
341
342    if config.packet_loss_rate < 0.0 || config.packet_loss_rate > 1.0 {
343        return Err(SklearsError::InvalidInput(
344            "packet_loss_rate must be in [0, 1]".to_string(),
345        ));
346    }
347
348    if config.compression_ratio <= 0.0 || config.compression_ratio > 1.0 {
349        return Err(SklearsError::InvalidInput(
350            "compression_ratio must be in (0, 1]".to_string(),
351        ));
352    }
353
354    let mut rng = if let Some(seed) = random_state {
355        StdRng::seed_from_u64(seed)
356    } else {
357        StdRng::from_rng(&mut scirs2_core::random::thread_rng())
358    };
359
360    // Communication costs per round per client
361    let mut upload_costs = Array2::zeros((n_rounds, config.n_clients));
362    let mut download_costs = Array2::zeros((n_rounds, config.n_clients));
363    let mut round_times = Array1::zeros(n_rounds);
364    let mut total_bandwidth_usage = Array1::zeros(n_rounds);
365
366    // Calculate topology-specific parameters
367    let (_n_connections, aggregation_factor) = match config.network_topology.as_str() {
368        "star" => (1, 1.0), // Each client connects to server
369        "ring" => (2, 0.5), // Each client connects to 2 neighbors
370        "full_mesh" => (config.n_clients - 1, 1.0 / (config.n_clients - 1) as f64),
371        "hierarchical" => ((config.n_clients as f64).sqrt() as usize, 0.3),
372        _ => {
373            return Err(SklearsError::InvalidInput(
374                "Invalid network topology. Must be one of: star, ring, full_mesh, hierarchical"
375                    .to_string(),
376            ))
377        }
378    };
379
380    let compressed_model_size = model_size_mb * config.compression_ratio;
381
382    for round in 0..n_rounds {
383        let mut max_round_time: f64 = 0.0;
384        let mut total_bandwidth_round = 0.0;
385
386        for client in 0..config.n_clients {
387            // Add random variation to network conditions
388            let bandwidth_variation = 1.0 + 0.3 * (rng.random::<f64>() - 0.5); // ±15% variation
389            let latency_variation = 1.0 + 0.5 * (rng.random::<f64>() - 0.5); // ±25% variation
390            let effective_bandwidth = config.bandwidth_mbps * bandwidth_variation;
391            let effective_latency = config.latency_ms * latency_variation;
392
393            // Calculate upload cost (client to server/aggregator)
394            let upload_time = (compressed_model_size / effective_bandwidth) * 1000.0; // Convert to ms
395            let upload_total_time = upload_time + effective_latency;
396
397            // Factor in packet loss (retransmissions)
398            let retransmission_factor = if config.packet_loss_rate > 0.0 {
399                1.0 / (1.0 - config.packet_loss_rate).max(0.1) // Avoid division by zero
400            } else {
401                1.0
402            };
403
404            let upload_cost = upload_total_time * retransmission_factor;
405            upload_costs[[round, client]] = upload_cost;
406
407            // Calculate download cost (server/aggregator to client)
408            let download_time = (compressed_model_size / effective_bandwidth) * 1000.0; // Convert to ms
409            let download_total_time = download_time + effective_latency;
410            let download_cost = download_total_time * retransmission_factor * aggregation_factor;
411            download_costs[[round, client]] = download_cost;
412
413            // Calculate maximum time for this round (bottleneck client)
414            let client_round_time = upload_cost + download_cost;
415            max_round_time = max_round_time.max(client_round_time);
416
417            // Calculate bandwidth usage
418            let bandwidth_used = (compressed_model_size * 2.0) * retransmission_factor; // Upload + download
419            total_bandwidth_round += bandwidth_used;
420        }
421
422        round_times[round] = max_round_time;
423        total_bandwidth_usage[round] = total_bandwidth_round;
424    }
425
426    Ok((
427        upload_costs,
428        download_costs,
429        round_times,
430        total_bandwidth_usage,
431    ))
432}
433
434/// Generate sensor fusion datasets for multi-modal data
435pub fn make_sensor_fusion_dataset(
436    n_samples: usize,
437    sensor_types: Vec<String>, // e.g., ["accelerometer", "gyroscope", "magnetometer", "camera", "lidar"]
438    temporal_length: usize,
439    sync_accuracy: f64, // Synchronization accuracy between sensors
440    random_state: Option<u64>,
441) -> Result<SensorFusionResult> {
442    if n_samples == 0 || sensor_types.is_empty() || temporal_length == 0 {
443        return Err(SklearsError::InvalidInput(
444            "n_samples, sensor_types, and temporal_length must be positive".to_string(),
445        ));
446    }
447
448    if !(0.0..=1.0).contains(&sync_accuracy) {
449        return Err(SklearsError::InvalidInput(
450            "sync_accuracy must be in [0, 1]".to_string(),
451        ));
452    }
453
454    let mut rng = if let Some(seed) = random_state {
455        StdRng::seed_from_u64(seed)
456    } else {
457        StdRng::from_rng(&mut scirs2_core::random::thread_rng())
458    };
459
460    let n_sensors = sensor_types.len();
461    let mut sensor_data = Vec::new();
462    let mut fusion_quality = Array1::zeros(n_samples);
463    let mut ground_truth_events = Array1::zeros(n_samples);
464
465    // Define sensor characteristics
466    let sensor_dimensions: Vec<usize> = sensor_types
467        .iter()
468        .map(|sensor_type| {
469            match sensor_type.as_str() {
470                "accelerometer" => 3, // x, y, z
471                "gyroscope" => 3,     // roll, pitch, yaw
472                "magnetometer" => 3,  // x, y, z
473                "camera" => 16,       // Feature vector from image
474                "lidar" => 8,         // Distance measurements
475                "gps" => 2,           // lat, lon
476                "microphone" => 1,    // Audio signal
477                "pressure" => 1,      // Atmospheric pressure
478                _ => 4,               // Default dimension
479            }
480        })
481        .collect();
482
483    // Initialize sensor data arrays
484    for &dim in &sensor_dimensions {
485        sensor_data.push(Array2::zeros((n_samples, temporal_length * dim)));
486    }
487
488    let normal = Normal::new(0.0, 1.0).expect("operation should succeed");
489
490    for sample in 0..n_samples {
491        // Generate a ground truth event (e.g., motion pattern)
492        let event_type = rng.random_range(0..5); // 5 different event types
493        ground_truth_events[sample] = event_type;
494
495        // Generate base signal for this event
496        let base_frequency = 0.1 + (event_type as f64) * 0.05;
497        let base_amplitude = 0.5 + (event_type as f64) * 0.2;
498
499        let mut sensor_correlations = Vec::new();
500
501        for (sensor_idx, sensor_type) in sensor_types.iter().enumerate() {
502            let dims = sensor_dimensions[sensor_idx];
503            let mut sensor_correlation = 0.0;
504
505            for t in 0..temporal_length {
506                let time = t as f64 / temporal_length as f64;
507
508                // Generate base signal
509                let base_signal = base_amplitude * (2.0 * PI * base_frequency * time).sin();
510
511                // Add sync error
512                let sync_error = if rng.random::<f64>() > sync_accuracy {
513                    0.1 * rng.random::<f64>() // Random desynchronization
514                } else {
515                    0.0
516                };
517
518                for dim in 0..dims {
519                    let feature_idx = t * dims + dim;
520
521                    // Generate sensor-specific signal
522                    let sensor_signal = match sensor_type.as_str() {
523                        "accelerometer" => {
524                            let gravity_component = if dim == 2 { 9.81 } else { 0.0 };
525                            base_signal * (1.0 + dim as f64 * 0.3) + gravity_component
526                        }
527                        "gyroscope" => {
528                            base_signal * (2.0 + dim as f64 * 0.5) // Higher frequency for rotation
529                        }
530                        "magnetometer" => {
531                            let magnetic_field = if dim == 0 { 25.0 } else { 5.0 };
532                            base_signal * 0.5 + magnetic_field
533                        }
534                        "camera" => {
535                            let pixel_intensity = 0.5 + 0.3 * base_signal;
536                            pixel_intensity.clamp(0.0, 1.0)
537                        }
538                        "lidar" => {
539                            let distance = 5.0 + 2.0 * base_signal + 0.5 * (dim as f64);
540                            distance.max(0.1)
541                        }
542                        "gps" => {
543                            let coord_variation = 0.0001 * base_signal;
544                            if dim == 0 {
545                                37.7749 + coord_variation
546                            } else {
547                                -122.4194 + coord_variation
548                            }
549                        }
550                        "microphone" => base_signal * 0.8,
551                        "pressure" => {
552                            1013.25 + 10.0 * base_signal // Standard atmospheric pressure with variation
553                        }
554                        _ => base_signal,
555                    };
556
557                    // Add noise and sync error
558                    let noise: f64 = rng.sample(normal);
559                    let final_signal = sensor_signal + 0.1 * noise + sync_error;
560
561                    sensor_data[sensor_idx][[sample, feature_idx]] = final_signal;
562
563                    // Calculate correlation with base signal for quality assessment
564                    sensor_correlation += (final_signal - sensor_signal).abs();
565                }
566            }
567
568            sensor_correlations.push(sensor_correlation / (temporal_length * dims) as f64);
569        }
570
571        // Calculate overall fusion quality
572        let avg_correlation = sensor_correlations.iter().sum::<f64>() / n_sensors as f64;
573        fusion_quality[sample] = (1.0 - avg_correlation.min(1.0)).max(0.0);
574    }
575
576    Ok((sensor_data, fusion_quality, ground_truth_events))
577}
578
579/// Generate multi-modal alignment datasets
580pub fn make_multimodal_alignment_dataset(
581    n_samples: usize,
582    modality_types: Vec<String>, // e.g., ["text", "image", "audio", "video"]
583    alignment_strength: f64,
584    cross_modal_noise: f64,
585    random_state: Option<u64>,
586) -> Result<MultimodalAlignResult> {
587    if n_samples == 0 || modality_types.is_empty() {
588        return Err(SklearsError::InvalidInput(
589            "n_samples and modality_types must be positive".to_string(),
590        ));
591    }
592
593    if !(0.0..=1.0).contains(&alignment_strength) {
594        return Err(SklearsError::InvalidInput(
595            "alignment_strength must be in [0, 1]".to_string(),
596        ));
597    }
598
599    if !(0.0..=1.0).contains(&cross_modal_noise) {
600        return Err(SklearsError::InvalidInput(
601            "cross_modal_noise must be in [0, 1]".to_string(),
602        ));
603    }
604
605    let mut rng = if let Some(seed) = random_state {
606        StdRng::seed_from_u64(seed)
607    } else {
608        StdRng::from_rng(&mut scirs2_core::random::thread_rng())
609    };
610
611    let n_modalities = modality_types.len();
612    let mut modality_data = Vec::new();
613    let mut alignment_scores = Array1::zeros(n_samples);
614
615    // Define modality dimensions
616    let modality_dimensions: Vec<usize> = modality_types
617        .iter()
618        .map(|modality| {
619            match modality.as_str() {
620                "text" => 300,   // Text embeddings
621                "image" => 2048, // Image features
622                "audio" => 128,  // Audio features
623                "video" => 1024, // Video features
624                _ => 256,        // Default dimension
625            }
626        })
627        .collect();
628
629    // Initialize modality data arrays
630    for &dim in &modality_dimensions {
631        modality_data.push(Array2::zeros((n_samples, dim)));
632    }
633
634    // Cross-modal alignment matrix
635    let mut cross_modal_alignment = Array2::zeros((n_samples, n_modalities * n_modalities));
636
637    let normal = Normal::new(0.0, 1.0).expect("operation should succeed");
638
639    for sample in 0..n_samples {
640        // Generate shared semantic content
641        let mut shared_content = Array1::zeros(64); // Shared semantic space
642        for i in 0..64 {
643            shared_content[i] = rng.sample(normal);
644        }
645
646        let mut modality_embeddings = Vec::new();
647
648        for (mod_idx, modality) in modality_types.iter().enumerate() {
649            let dims = modality_dimensions[mod_idx];
650            let mut embedding = Array1::zeros(dims);
651
652            // Generate modality-specific content based on shared content
653            for i in 0..dims {
654                let shared_influence = if i < 64 {
655                    alignment_strength * shared_content[i]
656                } else {
657                    0.0
658                };
659
660                let modality_specific = match modality.as_str() {
661                    "text" => {
662                        // Text features: semantic similarity, word frequency, etc.
663                        let word_freq = rng.random::<f64>() * 0.1;
664                        let semantic_sim = shared_influence * 0.8;
665                        semantic_sim + word_freq
666                    }
667                    "image" => {
668                        // Image features: visual patterns, colors, textures
669                        let visual_pattern = (shared_influence * 2.0).sin() * 0.5;
670                        let color_intensity = rng.random::<f64>() * 0.3;
671                        visual_pattern + color_intensity
672                    }
673                    "audio" => {
674                        // Audio features: spectral content, rhythm, etc.
675                        let spectral_content = shared_influence * 0.6;
676                        let rhythm_component = ((i as f64 / 10.0) * PI).sin() * 0.2;
677                        spectral_content + rhythm_component
678                    }
679                    "video" => {
680                        // Video features: temporal patterns, motion, etc.
681                        let temporal_pattern = shared_influence * 0.7;
682                        let motion_component = rng.random::<f64>() * 0.2;
683                        temporal_pattern + motion_component
684                    }
685                    _ => shared_influence,
686                };
687
688                let noise: f64 = rng.sample(normal);
689                embedding[i] = modality_specific + cross_modal_noise * noise;
690            }
691
692            modality_embeddings.push(embedding.clone());
693
694            // Store in output array
695            for i in 0..dims {
696                modality_data[mod_idx][[sample, i]] = embedding[i];
697            }
698        }
699
700        // Calculate cross-modal alignment scores
701        let mut total_alignment = 0.0;
702        let mut alignment_count = 0;
703
704        for i in 0..n_modalities {
705            for j in (i + 1)..n_modalities {
706                // Calculate cross-modal similarity using shared content overlap
707                // Since modalities have different dimensions, we use the shared content portion
708                let shared_len = 64
709                    .min(modality_embeddings[i].len())
710                    .min(modality_embeddings[j].len());
711
712                let mut dot_product = 0.0;
713                let mut norm_i_sq = 0.0;
714                let mut norm_j_sq = 0.0;
715
716                // k is used for arithmetic indexing into two different arrays
717                #[allow(clippy::needless_range_loop)]
718                for k in 0..shared_len {
719                    let val_i = modality_embeddings[i][k];
720                    let val_j = modality_embeddings[j][k];
721                    dot_product += val_i * val_j;
722                    norm_i_sq += val_i * val_i;
723                    norm_j_sq += val_j * val_j;
724                }
725
726                let cosine_sim = if norm_i_sq > 0.0 && norm_j_sq > 0.0 {
727                    dot_product / (norm_i_sq.sqrt() * norm_j_sq.sqrt())
728                } else {
729                    0.0
730                };
731
732                cross_modal_alignment[[sample, i * n_modalities + j]] = cosine_sim;
733                total_alignment += cosine_sim.abs();
734                alignment_count += 1;
735            }
736        }
737
738        alignment_scores[sample] = total_alignment / alignment_count as f64;
739    }
740
741    Ok((modality_data, cross_modal_alignment, alignment_scores))
742}
743
744/// Generate cross-modal retrieval datasets
745pub fn make_cross_modal_retrieval_dataset(
746    n_samples: usize,
747    source_modality: String,   // "text", "image", "audio"
748    target_modality: String,   // "text", "image", "audio"
749    n_distractors: usize,      // Number of negative examples per positive
750    retrieval_difficulty: f64, // 0.0 (easy) to 1.0 (hard)
751    random_state: Option<u64>,
752) -> Result<CrossModalRetrievalResult> {
753    if n_samples == 0 || n_distractors == 0 {
754        return Err(SklearsError::InvalidInput(
755            "n_samples and n_distractors must be positive".to_string(),
756        ));
757    }
758
759    if !(0.0..=1.0).contains(&retrieval_difficulty) {
760        return Err(SklearsError::InvalidInput(
761            "retrieval_difficulty must be in [0, 1]".to_string(),
762        ));
763    }
764
765    let mut rng = if let Some(seed) = random_state {
766        StdRng::seed_from_u64(seed)
767    } else {
768        StdRng::from_rng(&mut scirs2_core::random::thread_rng())
769    };
770
771    // Define embedding dimensions for each modality
772    let source_dim = match source_modality.as_str() {
773        "text" => 300,
774        "image" => 2048,
775        "audio" => 128,
776        _ => 256,
777    };
778
779    let target_dim = match target_modality.as_str() {
780        "text" => 300,
781        "image" => 2048,
782        "audio" => 128,
783        _ => 256,
784    };
785
786    let total_targets = n_samples * (1 + n_distractors);
787    let mut source_embeddings = Array2::zeros((n_samples, source_dim));
788    let mut target_embeddings = Array2::zeros((total_targets, target_dim));
789    let mut ground_truth_indices = Array1::zeros(n_samples);
790    let mut retrieval_scores = Array1::zeros(n_samples);
791
792    let normal = Normal::new(0.0, 1.0).expect("operation should succeed");
793
794    for sample in 0..n_samples {
795        // Generate shared semantic representation
796        let mut shared_content = Array1::zeros(64);
797        for i in 0..64 {
798            shared_content[i] = rng.sample(normal);
799        }
800
801        // Generate source modality embedding
802        for i in 0..source_dim {
803            let shared_influence = if i < 64 { 0.8 * shared_content[i] } else { 0.0 };
804
805            let modality_specific = match source_modality.as_str() {
806                "text" => {
807                    let semantic_weight = 0.6 + 0.4 * rng.random::<f64>();
808                    shared_influence * semantic_weight
809                }
810                "image" => (shared_influence * 1.5).sin() * 0.7,
811                "audio" => shared_influence * 0.8,
812                _ => shared_influence,
813            };
814
815            let noise: f64 = rng.sample(normal);
816            source_embeddings[[sample, i]] = modality_specific + 0.1 * noise;
817        }
818
819        // Generate positive target (ground truth)
820        let target_idx = sample * (1 + n_distractors);
821        ground_truth_indices[sample] = target_idx;
822
823        for i in 0..target_dim {
824            let shared_influence = if i < 64 { 0.8 * shared_content[i] } else { 0.0 };
825
826            let difficulty_noise = retrieval_difficulty * rng.sample(normal);
827
828            let modality_specific = match target_modality.as_str() {
829                "text" => {
830                    let semantic_weight = 0.6 + 0.4 * rng.random::<f64>();
831                    shared_influence * semantic_weight
832                }
833                "image" => (shared_influence * 1.5).sin() * 0.7,
834                "audio" => shared_influence * 0.8,
835                _ => shared_influence,
836            };
837
838            let noise: f64 = rng.sample(normal);
839            target_embeddings[[target_idx, i]] = modality_specific + 0.1 * noise + difficulty_noise;
840        }
841
842        // Generate negative targets (distractors)
843        for distractor in 0..n_distractors {
844            let distractor_idx = target_idx + 1 + distractor;
845
846            // Generate different shared content for distractors
847            let mut distractor_content = Array1::zeros(64);
848            for i in 0..64 {
849                distractor_content[i] = rng.sample(normal);
850            }
851
852            for i in 0..target_dim {
853                let shared_influence = if i < 64 {
854                    0.8 * distractor_content[i]
855                } else {
856                    0.0
857                };
858
859                let modality_specific = match target_modality.as_str() {
860                    "text" => {
861                        let semantic_weight = 0.6 + 0.4 * rng.random::<f64>();
862                        shared_influence * semantic_weight
863                    }
864                    "image" => (shared_influence * 1.5).sin() * 0.7,
865                    "audio" => shared_influence * 0.8,
866                    _ => shared_influence,
867                };
868
869                let noise: f64 = rng.sample(normal);
870                target_embeddings[[distractor_idx, i]] = modality_specific + 0.1 * noise;
871            }
872        }
873
874        // Calculate retrieval score (similarity between source and ground truth target)
875        // Use shared content dimensions for cross-modal similarity
876        let shared_len = 64.min(source_dim).min(target_dim);
877
878        let mut dot_product = 0.0;
879        let mut source_norm_sq: f64 = 0.0;
880        let mut target_norm_sq: f64 = 0.0;
881
882        for k in 0..shared_len {
883            let source_val = source_embeddings[[sample, k]];
884            let target_val = target_embeddings[[target_idx, k]];
885            dot_product += source_val * target_val;
886            source_norm_sq += source_val * source_val;
887            target_norm_sq += target_val * target_val;
888        }
889
890        let cosine_sim = if source_norm_sq > 0.0 && target_norm_sq > 0.0 {
891            dot_product / (source_norm_sq.sqrt() * target_norm_sq.sqrt())
892        } else {
893            0.0
894        };
895
896        retrieval_scores[sample] = cosine_sim;
897    }
898
899    Ok((
900        source_embeddings,
901        target_embeddings,
902        ground_truth_indices,
903        retrieval_scores,
904    ))
905}
906
907#[allow(non_snake_case)]
908#[cfg(test)]
909mod tests {
910    use super::*;
911
912    #[test]
913    fn test_make_multi_agent_environment() {
914        let config = MultiAgentConfig {
915            n_agents: 3,
916            n_states: 4,
917            n_actions: 2,
918            cooperation_level: 0.5,
919            communication_enabled: true,
920            reward_sharing: false,
921        };
922
923        let (states, actions, rewards, episode_rewards) =
924            make_multi_agent_environment(config, 10, 20, Some(42))
925                .expect("operation should succeed");
926
927        assert_eq!(states.shape(), &[10, 20, 3]);
928        assert_eq!(actions.shape(), &[10, 20, 3]);
929        assert_eq!(rewards.shape(), &[10, 20]);
930        assert_eq!(episode_rewards.len(), 10);
931
932        // Check state values are within bounds
933        for &state in states.iter() {
934            assert!(state < 4, "States should be within [0, n_states)");
935        }
936
937        // Check action values are within bounds
938        for &action in actions.iter() {
939            assert!(action < 2, "Actions should be within [0, n_actions)");
940        }
941    }
942
943    #[test]
944    fn test_make_vision_language_dataset() {
945        let (images, texts, alignment_scores) =
946            make_vision_language_dataset(50, (32, 32), 1000, 10, 0.7, Some(42))
947                .expect("operation should succeed");
948
949        assert_eq!(images.shape(), &[50, 32, 32]);
950        assert_eq!(texts.shape(), &[50, 10]);
951        assert_eq!(alignment_scores.len(), 50);
952
953        // Check image values are in [0, 1]
954        for &pixel in images.iter() {
955            assert!(
956                (0.0..=1.0).contains(&pixel),
957                "Image pixels should be in [0, 1]"
958            );
959        }
960
961        // Check text tokens are within vocabulary
962        for &token in texts.iter() {
963            assert!(token < 1000, "Text tokens should be within vocab_size");
964        }
965
966        // Check alignment scores are in [0, 1]
967        for &score in alignment_scores.iter() {
968            assert!(
969                (0.0..=1.0).contains(&score),
970                "Alignment scores should be in [0, 1]"
971            );
972        }
973    }
974
975    #[test]
976    fn test_make_audio_visual_dataset() {
977        let (audio, video, sync_scores) =
978            make_audio_visual_dataset(20, 100, 10, (8, 8), 0.8, Some(42))
979                .expect("operation should succeed");
980
981        assert_eq!(audio.shape(), &[20, 100]);
982        assert_eq!(video.shape(), &[20, 640]); // 10 frames * 8 * 8 pixels
983        assert_eq!(sync_scores.len(), 20);
984
985        // Check that audio has some variation
986        let audio_var = audio.var(0.0);
987        assert!(audio_var > 0.0, "Audio should have variation");
988
989        // Check sync scores are reasonable
990        for &score in sync_scores.iter() {
991            assert!(
992                (0.0..=1.0).contains(&score),
993                "Sync scores should be in [0, 1]"
994            );
995        }
996    }
997
998    #[test]
999    fn test_make_communication_cost_datasets() {
1000        let config = CommunicationCostConfig {
1001            n_clients: 5,
1002            network_topology: "star".to_string(),
1003            bandwidth_mbps: 10.0,
1004            latency_ms: 50.0,
1005            packet_loss_rate: 0.05,
1006            compression_ratio: 0.8,
1007        };
1008
1009        let (upload_costs, download_costs, round_times, bandwidth_usage) =
1010            make_communication_cost_datasets(config, 10, 2.5, Some(42))
1011                .expect("operation should succeed");
1012
1013        assert_eq!(upload_costs.shape(), &[10, 5]);
1014        assert_eq!(download_costs.shape(), &[10, 5]);
1015        assert_eq!(round_times.len(), 10);
1016        assert_eq!(bandwidth_usage.len(), 10);
1017
1018        // Check that costs are positive
1019        for &cost in upload_costs.iter() {
1020            assert!(cost > 0.0, "Upload costs should be positive");
1021        }
1022
1023        for &cost in download_costs.iter() {
1024            assert!(cost > 0.0, "Download costs should be positive");
1025        }
1026
1027        for &time in round_times.iter() {
1028            assert!(time > 0.0, "Round times should be positive");
1029        }
1030
1031        for &usage in bandwidth_usage.iter() {
1032            assert!(usage > 0.0, "Bandwidth usage should be positive");
1033        }
1034    }
1035
1036    #[test]
1037    fn test_make_sensor_fusion_dataset() {
1038        let sensor_types = vec![
1039            "accelerometer".to_string(),
1040            "gyroscope".to_string(),
1041            "camera".to_string(),
1042        ];
1043
1044        let (sensor_data, fusion_quality, ground_truth_events) =
1045            make_sensor_fusion_dataset(20, sensor_types, 10, 0.9, Some(42))
1046                .expect("operation should succeed");
1047
1048        assert_eq!(sensor_data.len(), 3); // 3 sensors
1049        assert_eq!(sensor_data[0].shape(), &[20, 30]); // accelerometer: 20 samples, 10 timesteps * 3 dims
1050        assert_eq!(sensor_data[1].shape(), &[20, 30]); // gyroscope: 20 samples, 10 timesteps * 3 dims
1051        assert_eq!(sensor_data[2].shape(), &[20, 160]); // camera: 20 samples, 10 timesteps * 16 dims
1052        assert_eq!(fusion_quality.len(), 20);
1053        assert_eq!(ground_truth_events.len(), 20);
1054
1055        // Check fusion quality is in [0, 1]
1056        for &quality in fusion_quality.iter() {
1057            assert!(
1058                (0.0..=1.0).contains(&quality),
1059                "Fusion quality should be in [0, 1]"
1060            );
1061        }
1062
1063        // Check ground truth events are within expected range
1064        for &event in ground_truth_events.iter() {
1065            assert!(event < 5, "Ground truth events should be within [0, 5)");
1066        }
1067    }
1068
1069    #[test]
1070    fn test_make_multimodal_alignment_dataset() {
1071        let modality_types = vec!["text".to_string(), "image".to_string(), "audio".to_string()];
1072
1073        let (modality_data, cross_modal_alignment, alignment_scores) =
1074            make_multimodal_alignment_dataset(15, modality_types, 0.8, 0.2, Some(42))
1075                .expect("operation should succeed");
1076
1077        assert_eq!(modality_data.len(), 3); // 3 modalities
1078        assert_eq!(modality_data[0].shape(), &[15, 300]); // text: 15 samples, 300 dims
1079        assert_eq!(modality_data[1].shape(), &[15, 2048]); // image: 15 samples, 2048 dims
1080        assert_eq!(modality_data[2].shape(), &[15, 128]); // audio: 15 samples, 128 dims
1081        assert_eq!(cross_modal_alignment.shape(), &[15, 9]); // 15 samples, 3*3 alignment matrix
1082        assert_eq!(alignment_scores.len(), 15);
1083
1084        // Check alignment scores are reasonable
1085        for &score in alignment_scores.iter() {
1086            assert!(
1087                (0.0..=1.0).contains(&score),
1088                "Alignment scores should be in [0, 1]"
1089            );
1090        }
1091    }
1092
1093    #[test]
1094    fn test_make_cross_modal_retrieval_dataset() {
1095        let (source_embeddings, target_embeddings, ground_truth_indices, retrieval_scores) =
1096            make_cross_modal_retrieval_dataset(
1097                10,
1098                "text".to_string(),
1099                "image".to_string(),
1100                5,
1101                0.3,
1102                Some(42),
1103            )
1104            .expect("operation should succeed");
1105
1106        assert_eq!(source_embeddings.shape(), &[10, 300]); // 10 samples, 300 text dims
1107        assert_eq!(target_embeddings.shape(), &[60, 2048]); // 10 * (1 + 5) targets, 2048 image dims
1108        assert_eq!(ground_truth_indices.len(), 10);
1109        assert_eq!(retrieval_scores.len(), 10);
1110
1111        // Check ground truth indices are within expected range
1112        for &idx in ground_truth_indices.iter() {
1113            assert!(
1114                idx < 60,
1115                "Ground truth indices should be within target range"
1116            );
1117        }
1118
1119        // Check retrieval scores are reasonable
1120        for &score in retrieval_scores.iter() {
1121            assert!(
1122                (-1.0..=1.0).contains(&score),
1123                "Retrieval scores should be in [-1, 1] (cosine similarity)"
1124            );
1125        }
1126    }
1127}