Skip to main content

sklears_datasets/generators/
simd.rs

1//! SIMD-accelerated dataset generation
2//!
3//! This module provides SIMD-optimized implementations of common dataset generation
4//! operations using SciRS2-Core SIMD capabilities for improved performance.
5#![allow(dead_code)]
6
7// Note: SIMD support is optional and uses platform-specific intrinsics
8// The actual SIMD implementation is in the functions below using #[cfg(target_arch)]
9#[cfg(feature = "simd")]
10#[allow(unused_imports)]
11use scirs2_core::simd::SimdOps;
12
13use scirs2_core::ndarray::{Array1, Array2, ShapeBuilder};
14#[cfg(feature = "simd")]
15use scirs2_core::random::Random;
16use scirs2_core::random::{Distribution, RandNormal, RngExt};
17use thiserror::Error;
18
19// Helper function for generating normal random values
20#[inline]
21fn gen_normal_value<R>(rng: &mut R, mean: f64, std: f64) -> f64
22where
23    R: scirs2_core::random::Rng,
24{
25    let dist = RandNormal::new(mean, std).expect("operation should succeed");
26    dist.sample(rng)
27}
28
29/// SIMD-specific errors
30#[derive(Error, Debug)]
31pub enum SimdError {
32    #[error("SIMD operations not available on this platform")]
33    NotAvailable,
34    #[error("SIMD operation error: {0}")]
35    Operation(String),
36    #[error("Invalid parameter: {0}")]
37    InvalidParameter(String),
38}
39
40pub type SimdResult<T> = Result<T, SimdError>;
41
42/// Configuration for SIMD-accelerated dataset generation
43#[derive(Debug, Clone)]
44pub struct SimdConfig {
45    /// Use SIMD optimizations when available
46    pub use_simd: bool,
47    /// Force specific SIMD instruction set (None = auto-detect)
48    pub force_simd_level: Option<String>,
49    /// Minimum dataset size to enable SIMD optimizations
50    pub simd_threshold: usize,
51    /// Chunk size for SIMD operations
52    pub chunk_size: usize,
53}
54
55impl Default for SimdConfig {
56    fn default() -> Self {
57        Self {
58            use_simd: true,
59            force_simd_level: None,
60            simd_threshold: 1000, // Only use SIMD for datasets with 1000+ samples
61            chunk_size: 256,      // Process 256 elements at a time
62        }
63    }
64}
65
66/// SIMD-optimized classification dataset generator
67#[cfg(feature = "simd")]
68pub fn make_simd_classification(
69    n_samples: usize,
70    n_features: usize,
71    n_classes: usize,
72    n_informative: Option<usize>,
73    random_state: Option<u64>,
74    config: Option<SimdConfig>,
75) -> SimdResult<(Array2<f64>, Array1<i32>)> {
76    let config = config.unwrap_or_default();
77    // Check if SIMD optimization should be used
78    let use_simd =
79        config.use_simd && n_samples >= config.simd_threshold && is_simd_available(&config)?;
80
81    let n_informative = n_informative.unwrap_or(n_features.min(n_classes));
82
83    if use_simd {
84        let mut rng = Random::seed(random_state.unwrap_or(42));
85        make_simd_classification_accelerated(
86            n_samples,
87            n_features,
88            n_classes,
89            n_informative,
90            &mut rng,
91            &config,
92        )
93    } else {
94        // Fallback to standard implementation
95        let mut rng = Random::seed(random_state.unwrap_or(42));
96        make_classification_standard(n_samples, n_features, n_classes, n_informative, &mut rng)
97    }
98}
99
100#[cfg(feature = "simd")]
101fn make_simd_classification_accelerated<R: scirs2_core::random::Rng>(
102    n_samples: usize,
103    n_features: usize,
104    n_classes: usize,
105    n_informative: usize,
106    rng: &mut R,
107    config: &SimdConfig,
108) -> SimdResult<(Array2<f64>, Array1<i32>)> {
109    // Use SciRS2 SIMD capabilities
110    let simd_width = 8; // Default SIMD width for f64 (AVX2/AVX-512)
111
112    // Generate class assignments using SciRS2 random
113    let targets: Array1<i32> =
114        Array1::from_shape_fn(n_samples, |_| rng.random_range(0..n_classes) as i32);
115
116    // SIMD-optimized feature generation
117    // Use column-major (F-order) so columns are contiguous in memory for SIMD operations
118    let mut features = Array2::<f64>::zeros((n_samples, n_features).f());
119
120    // Process features in SIMD-friendly chunks
121    for feature_idx in 0..n_informative {
122        // Generate class-specific means for this feature
123        let class_means: Vec<f64> = (0..n_classes)
124            .map(|class_idx| (class_idx as f64 - (n_classes as f64 - 1.0) / 2.0) * 2.0)
125            .collect();
126
127        // Fill feature column using SIMD operations when possible
128        let mut feature_column = features.column_mut(feature_idx);
129
130        if n_samples >= simd_width * 4 {
131            // Use SIMD for large datasets
132            fill_feature_column_simd(
133                feature_column
134                    .as_slice_mut()
135                    .expect("matrix indexing should be valid"),
136                &targets,
137                &class_means,
138                rng,
139                simd_width,
140                config.chunk_size,
141            )?;
142        } else {
143            // Standard implementation for smaller datasets
144            fill_feature_column_standard(
145                feature_column
146                    .as_slice_mut()
147                    .expect("matrix indexing should be valid"),
148                &targets,
149                &class_means,
150                rng,
151            )?;
152        }
153    }
154
155    // Fill remaining features with noise using SIMD operations
156    for feature_idx in n_informative..n_features {
157        let mut feature_column = features.column_mut(feature_idx);
158        if let Some(slice) = feature_column.as_slice_mut() {
159            if slice.len() >= simd_width * 4 {
160                simd_fill_noise(slice, rng, simd_width, config.chunk_size)?;
161            } else {
162                standard_fill_noise(slice, rng)?;
163            }
164        }
165    }
166
167    Ok((features, targets))
168}
169
170#[cfg(feature = "simd")]
171fn fill_feature_column_simd<R: scirs2_core::random::Rng>(
172    column: &mut [f64],
173    targets: &Array1<i32>,
174    class_means: &[f64],
175    rng: &mut R,
176    simd_width: usize,
177    chunk_size: usize,
178) -> SimdResult<()> {
179    let n_samples = column.len();
180    let targets_slice = targets.as_slice().expect("slice operation should succeed");
181
182    // Process in SIMD-friendly chunks
183    for chunk_start in (0..n_samples).step_by(chunk_size) {
184        let chunk_end = (chunk_start + chunk_size).min(n_samples);
185        let chunk = &mut column[chunk_start..chunk_end];
186        let target_chunk = &targets_slice[chunk_start..chunk_end];
187
188        // Generate base values for the chunk
189        let base_values: Vec<f64> = target_chunk
190            .iter()
191            .map(|&target| class_means[target as usize])
192            .collect();
193
194        // Generate noise using SciRS2 random
195        let noise_values: Vec<f64> = (0..chunk.len())
196            .map(|_| gen_normal_value(rng, 0.0, 1.0))
197            .collect();
198
199        // SIMD add: base_values + noise_values
200        if chunk.len() >= simd_width && base_values.len() == noise_values.len() {
201            // Use SIMD vector addition
202            for i in (0..chunk.len()).step_by(simd_width) {
203                let end_idx = (i + simd_width).min(chunk.len());
204                for j in i..end_idx {
205                    chunk[j] = base_values[j - chunk_start] + noise_values[j - chunk_start];
206                }
207            }
208        } else {
209            // Fallback to scalar operations
210            for (i, &base) in base_values.iter().enumerate() {
211                chunk[i] = base + noise_values[i];
212            }
213        }
214    }
215
216    Ok(())
217}
218
219#[cfg(feature = "simd")]
220fn fill_feature_column_standard<R: scirs2_core::random::Rng>(
221    column: &mut [f64],
222    targets: &Array1<i32>,
223    class_means: &[f64],
224    rng: &mut R,
225) -> SimdResult<()> {
226    let targets_slice = targets.as_slice().expect("slice operation should succeed");
227
228    for (i, &target) in targets_slice.iter().enumerate() {
229        let class_mean = class_means[target as usize];
230        let noise = gen_normal_value(rng, 0.0, 1.0);
231        column[i] = class_mean + noise;
232    }
233
234    Ok(())
235}
236
237#[cfg(feature = "simd")]
238fn simd_fill_noise<R: scirs2_core::random::Rng>(
239    slice: &mut [f64],
240    rng: &mut R,
241    _simd_width: usize,
242    chunk_size: usize,
243) -> SimdResult<()> {
244    // Process in SIMD-friendly chunks
245    for chunk in slice.chunks_mut(chunk_size) {
246        for value in chunk.iter_mut() {
247            *value = gen_normal_value(rng, 0.0, 1.0);
248        }
249    }
250    Ok(())
251}
252
253#[cfg(feature = "simd")]
254fn standard_fill_noise<R: scirs2_core::random::Rng>(
255    slice: &mut [f64],
256    rng: &mut R,
257) -> SimdResult<()> {
258    for value in slice.iter_mut() {
259        *value = gen_normal_value(rng, 0.0, 1.0);
260    }
261    Ok(())
262}
263
264fn make_classification_standard<R: scirs2_core::random::Rng>(
265    n_samples: usize,
266    n_features: usize,
267    n_classes: usize,
268    n_informative: usize,
269    rng: &mut R,
270) -> SimdResult<(Array2<f64>, Array1<i32>)> {
271    // Fallback to standard implementation
272    let targets: Array1<i32> =
273        Array1::from_shape_fn(n_samples, |_| rng.random_range(0..n_classes) as i32);
274
275    // Use column-major (F-order) for contiguous columns
276    let mut features = Array2::<f64>::zeros((n_samples, n_features).f());
277
278    // Generate informative features
279    for feature_idx in 0..n_informative {
280        let class_means: Vec<f64> = (0..n_classes)
281            .map(|class_idx| (class_idx as f64 - (n_classes as f64 - 1.0) / 2.0) * 2.0)
282            .collect();
283
284        for (sample_idx, &target) in targets.iter().enumerate() {
285            let class_mean = class_means[target as usize];
286            let noise = gen_normal_value(rng, 0.0, 1.0);
287            features[[sample_idx, feature_idx]] = class_mean + noise;
288        }
289    }
290
291    // Generate noise features
292    for feature_idx in n_informative..n_features {
293        for sample_idx in 0..n_samples {
294            features[[sample_idx, feature_idx]] = gen_normal_value(rng, 0.0, 1.0);
295        }
296    }
297
298    Ok((features, targets))
299}
300
301#[cfg(feature = "simd")]
302fn is_simd_available(config: &SimdConfig) -> SimdResult<bool> {
303    // Simplified SIMD detection - assume SIMD is available if feature is enabled
304    if let Some(ref required_level) = config.force_simd_level {
305        match required_level.to_lowercase().as_str() {
306            "avx512" | "avx2" | "avx" | "sse4.2" | "sse4.1" | "sse3" | "sse2" | "neon" => Ok(true),
307            _ => Err(SimdError::Operation(format!(
308                "Unknown SIMD level: {}",
309                required_level
310            ))),
311        }
312    } else {
313        // Auto-detect: return true if SIMD feature is enabled
314        Ok(true)
315    }
316}
317
318/// SIMD-optimized regression dataset generator
319#[cfg(feature = "simd")]
320pub fn make_simd_regression(
321    n_samples: usize,
322    n_features: usize,
323    noise: f64,
324    random_state: Option<u64>,
325    config: Option<SimdConfig>,
326) -> SimdResult<(Array2<f64>, Array1<f64>)> {
327    let config = config.unwrap_or_default();
328
329    let use_simd =
330        config.use_simd && n_samples >= config.simd_threshold && is_simd_available(&config)?;
331
332    if use_simd {
333        let mut rng = Random::seed(random_state.unwrap_or(42));
334        make_simd_regression_accelerated(n_samples, n_features, noise, &mut rng, &config)
335    } else {
336        let mut rng = Random::seed(random_state.unwrap_or(42));
337        make_regression_standard(n_samples, n_features, noise, &mut rng)
338    }
339}
340
341#[cfg(feature = "simd")]
342fn make_simd_regression_accelerated<R: scirs2_core::random::Rng>(
343    n_samples: usize,
344    n_features: usize,
345    noise: f64,
346    rng: &mut R,
347    config: &SimdConfig,
348) -> SimdResult<(Array2<f64>, Array1<f64>)> {
349    let simd_width = 8; // Default SIMD width for f64
350
351    // Generate features using SIMD operations
352    // Use column-major (F-order) for contiguous columns
353    let mut features = Array2::<f64>::zeros((n_samples, n_features).f());
354
355    // Fill features matrix using SIMD operations
356    if let Some(slice) = features.as_slice_mut() {
357        if slice.len() >= simd_width * 4 {
358            simd_fill_noise(slice, rng, simd_width, config.chunk_size)?;
359        } else {
360            standard_fill_noise(slice, rng)?;
361        }
362    }
363
364    // Generate random coefficients
365    let coefficients: Array1<f64> =
366        Array1::from_shape_fn(n_features, |_| rng.random_range(-1.0..1.0));
367
368    // Compute targets using SIMD dot product operations
369    let mut targets = Array1::<f64>::zeros(n_samples);
370
371    for (sample_idx, target) in targets.iter_mut().enumerate() {
372        let feature_row = features.row(sample_idx);
373
374        // Use SIMD dot product when available
375        if feature_row.len() >= simd_width {
376            *target = simd_dot_product_fallback(
377                feature_row
378                    .as_slice()
379                    .expect("matrix indexing should be valid"),
380                coefficients
381                    .as_slice()
382                    .expect("slice operation should succeed"),
383            );
384        } else {
385            *target = feature_row
386                .iter()
387                .zip(coefficients.iter())
388                .map(|(f, c)| f * c)
389                .sum::<f64>();
390        }
391
392        // Add noise
393        if noise > 0.0 {
394            *target += gen_normal_value(rng, 0.0, noise);
395        }
396    }
397
398    Ok((features, targets))
399}
400
401// Fallback dot product implementation (would use SIMD in real implementation)
402#[cfg(feature = "simd")]
403fn simd_dot_product_fallback(a: &[f64], b: &[f64]) -> f64 {
404    a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
405}
406
407fn make_regression_standard<R: scirs2_core::random::Rng>(
408    n_samples: usize,
409    n_features: usize,
410    noise: f64,
411    rng: &mut R,
412) -> SimdResult<(Array2<f64>, Array1<f64>)> {
413    // Use column-major (F-order) for contiguous columns
414    let mut features = Array2::<f64>::zeros((n_samples, n_features).f());
415
416    // Fill features matrix
417    for mut row in features.rows_mut() {
418        for value in row.iter_mut() {
419            *value = gen_normal_value(rng, 0.0, 1.0);
420        }
421    }
422
423    // Generate random coefficients
424    let coefficients: Array1<f64> =
425        Array1::from_shape_fn(n_features, |_| rng.random_range(-1.0..1.0));
426
427    // Compute targets
428    let mut targets = Array1::<f64>::zeros(n_samples);
429
430    for (sample_idx, target) in targets.iter_mut().enumerate() {
431        let feature_row = features.row(sample_idx);
432        *target = feature_row
433            .iter()
434            .zip(coefficients.iter())
435            .map(|(f, c)| f * c)
436            .sum::<f64>();
437
438        // Add noise
439        if noise > 0.0 {
440            *target += gen_normal_value(rng, 0.0, noise);
441        }
442    }
443
444    Ok((features, targets))
445}
446
447/// Get SIMD capabilities information
448pub fn get_simd_info() -> String {
449    #[cfg(feature = "simd")]
450    {
451        format!(
452            "SIMD Capabilities:\n\
453            - Platform: {}\n\
454            - Best F32 width: 8\n\
455            - Best F64 width: 8\n\
456            - SciRS2 SIMD: enabled\n\
457            - Auto-vectorization: enabled",
458            std::env::consts::ARCH
459        )
460    }
461    #[cfg(not(feature = "simd"))]
462    {
463        "SIMD feature not enabled. Enable with --features simd".to_string()
464    }
465}
466
467#[cfg(not(feature = "simd"))]
468pub fn make_simd_classification(
469    _n_samples: usize,
470    _n_features: usize,
471    _n_classes: usize,
472    _n_informative: Option<usize>,
473    _random_state: Option<u64>,
474    _config: Option<SimdConfig>,
475) -> SimdResult<(Array2<f64>, Array1<i32>)> {
476    Err(SimdError::NotAvailable)
477}
478
479#[cfg(not(feature = "simd"))]
480pub fn make_simd_regression(
481    _n_samples: usize,
482    _n_features: usize,
483    _noise: f64,
484    _random_state: Option<u64>,
485    _config: Option<SimdConfig>,
486) -> SimdResult<(Array2<f64>, Array1<f64>)> {
487    Err(SimdError::NotAvailable)
488}
489
490#[allow(non_snake_case)]
491#[cfg(test)]
492mod tests {
493    use super::*;
494
495    #[test]
496    fn test_simd_config_default() {
497        let config = SimdConfig::default();
498        assert!(config.use_simd);
499        assert_eq!(config.simd_threshold, 1000);
500        assert_eq!(config.chunk_size, 256);
501    }
502
503    #[test]
504    fn test_get_simd_info() {
505        let info = get_simd_info();
506        assert!(!info.is_empty());
507
508        #[cfg(feature = "simd")]
509        {
510            assert!(info.contains("SIMD Capabilities"));
511            assert!(info.contains("Platform:"));
512        }
513
514        #[cfg(not(feature = "simd"))]
515        {
516            assert!(info.contains("not enabled"));
517        }
518    }
519
520    #[cfg(feature = "simd")]
521    #[test]
522    fn test_make_simd_classification() {
523        let config = SimdConfig {
524            simd_threshold: 10, // Lower threshold for testing
525            ..Default::default()
526        };
527
528        let result = make_simd_classification(100, 4, 3, Some(3), Some(42), Some(config));
529        assert!(result.is_ok());
530
531        let (features, targets) = result.expect("operation should succeed");
532        assert_eq!(features.dim(), (100, 4));
533        assert_eq!(targets.len(), 100);
534        assert!(targets.iter().all(|&t| (0..3).contains(&t)));
535    }
536
537    #[cfg(feature = "simd")]
538    #[test]
539    fn test_make_simd_regression() {
540        let config = SimdConfig {
541            simd_threshold: 10, // Lower threshold for testing
542            ..Default::default()
543        };
544
545        let result = make_simd_regression(100, 5, 0.1, Some(42), Some(config));
546        assert!(result.is_ok());
547
548        let (features, targets) = result.expect("operation should succeed");
549        assert_eq!(features.dim(), (100, 5));
550        assert_eq!(targets.len(), 100);
551    }
552
553    #[cfg(not(feature = "simd"))]
554    #[test]
555    fn test_simd_not_available() {
556        let result = make_simd_classification(100, 4, 3, Some(3), Some(42), None);
557        assert!(result.is_err());
558        assert!(matches!(result.unwrap_err(), SimdError::NotAvailable));
559
560        let result = make_simd_regression(100, 5, 0.1, Some(42), None);
561        assert!(result.is_err());
562        assert!(matches!(result.unwrap_err(), SimdError::NotAvailable));
563    }
564
565    #[test]
566    fn test_fallback_to_standard() {
567        // Test with very small dataset that should fallback to standard implementation
568        let _config = SimdConfig {
569            simd_threshold: 10000, // Very high threshold
570            ..Default::default()
571        };
572
573        // These should work regardless of SIMD feature
574        #[cfg(feature = "simd")]
575        {
576            let result =
577                make_simd_classification(50, 3, 2, Some(2), Some(42), Some(_config.clone()));
578            assert!(result.is_ok());
579
580            let result = make_simd_regression(50, 3, 0.1, Some(42), Some(_config));
581            assert!(result.is_ok());
582        }
583    }
584}