Skip to main content

scirs2_stats/
sampling_simd.rs

1//! SIMD-optimized sampling operations
2//!
3//! This module provides SIMD-accelerated implementations of distribution sampling
4//! operations using scirs2-core's unified SIMD operations for v0.2.0.
5
6use crate::error::{StatsError, StatsResult};
7use scirs2_core::ndarray::{Array1, ArrayView1};
8use scirs2_core::numeric::{Float, NumCast};
9use scirs2_core::random::uniform::SampleUniform;
10use scirs2_core::random::Rng;
11use scirs2_core::simd_ops::{AutoOptimizer, SimdUnifiedOps};
12
13/// SIMD-optimized Box-Muller transform for normal distribution sampling
14///
15/// This function generates pairs of independent standard normal random variables
16/// using the Box-Muller transform with SIMD acceleration for improved performance.
17///
18/// # Arguments
19///
20/// * `n` - Number of samples to generate (must be even for Box-Muller)
21/// * `mean` - Mean of the normal distribution
22/// * `std_dev` - Standard deviation of the normal distribution
23/// * `seed` - Optional random seed
24///
25/// # Returns
26///
27/// Array of normally distributed random samples
28///
29/// # Examples
30///
31/// ```
32/// use scirs2_stats::sampling_simd::box_muller_simd;
33///
34/// let samples = box_muller_simd(1000, 0.0, 1.0, Some(42)).expect("Sampling failed");
35/// assert_eq!(samples.len(), 1000);
36/// ```
37pub fn box_muller_simd<F>(
38    n: usize,
39    mean: F,
40    std_dev: F,
41    seed: Option<u64>,
42) -> StatsResult<Array1<F>>
43where
44    F: Float + NumCast + SimdUnifiedOps + SampleUniform,
45{
46    if std_dev <= F::zero() {
47        return Err(StatsError::invalid_argument(
48            "Standard deviation must be positive",
49        ));
50    }
51
52    // Ensure even number of samples for Box-Muller
53    let n_pairs = (n + 1) / 2;
54    let n_total = n_pairs * 2;
55
56    let mut rng = {
57        let s = seed.unwrap_or_else(|| {
58            use scirs2_core::random::Rng;
59            scirs2_core::random::thread_rng().random()
60        });
61        scirs2_core::random::seeded_rng(s)
62    };
63
64    let optimizer = AutoOptimizer::new();
65
66    // Generate uniform random numbers
67    let u1: Array1<F> = Array1::from_shape_fn(n_pairs, |_| {
68        F::from(rng.gen_range(F::epsilon().to_f64().unwrap_or(1e-10)..1.0))
69            .unwrap_or_else(|| F::one())
70    });
71    let u2: Array1<F> = Array1::from_shape_fn(n_pairs, |_| {
72        F::from(rng.gen_range(0.0..1.0)).unwrap_or_else(|| F::zero())
73    });
74
75    if optimizer.should_use_simd(n_pairs) {
76        // SIMD path for Box-Muller transform
77        let two_pi = F::from(2.0 * std::f64::consts::PI).unwrap_or_else(|| F::one());
78
79        // Compute: sqrt(-2 * ln(u1))
80        let ln_u1 = F::simd_ln(&u1.view());
81        let minus_two = F::from(-2.0).unwrap_or_else(|| F::one());
82        let minus_two_array = Array1::from_elem(n_pairs, minus_two);
83        let neg_two_ln_u1 = F::simd_mul(&minus_two_array.view(), &ln_u1.view());
84        let sqrt_term = F::simd_sqrt(&neg_two_ln_u1.view());
85
86        // Compute: 2 * pi * u2
87        let two_pi_array = Array1::from_elem(n_pairs, two_pi);
88        let two_pi_u2 = F::simd_mul(&two_pi_array.view(), &u2.view());
89
90        // Compute: cos(2*pi*u2) and sin(2*pi*u2)
91        let cos_term = F::simd_cos(&two_pi_u2.view());
92        let sin_term = F::simd_sin(&two_pi_u2.view());
93
94        // z0 = sqrt_term * cos_term
95        // z1 = sqrt_term * sin_term
96        let z0 = F::simd_mul(&sqrt_term.view(), &cos_term.view());
97        let z1 = F::simd_mul(&sqrt_term.view(), &sin_term.view());
98
99        // Scale and shift: z * std_dev + mean
100        let std_dev_array = Array1::from_elem(n_pairs, std_dev);
101        let mean_array = Array1::from_elem(n_pairs, mean);
102
103        let z0_scaled = F::simd_fma(&z0.view(), &std_dev_array.view(), &mean_array.view());
104        let z1_scaled = F::simd_fma(&z1.view(), &std_dev_array.view(), &mean_array.view());
105
106        // Interleave results
107        let mut result = Array1::zeros(n_total);
108        for i in 0..n_pairs {
109            result[2 * i] = z0_scaled[i];
110            if 2 * i + 1 < n_total {
111                result[2 * i + 1] = z1_scaled[i];
112            }
113        }
114
115        // Trim to requested size
116        Ok(result.slice(scirs2_core::ndarray::s![..n]).to_owned())
117    } else {
118        // Scalar fallback
119        let mut result = Array1::zeros(n_total);
120        let two_pi = F::from(2.0 * std::f64::consts::PI).unwrap_or_else(|| F::one());
121        let two = F::from(2.0).unwrap_or_else(|| F::one());
122
123        for i in 0..n_pairs {
124            let r = (-two * u1[i].ln()).sqrt();
125            let theta = two_pi * u2[i];
126
127            result[2 * i] = mean + std_dev * r * theta.cos();
128            if 2 * i + 1 < n_total {
129                result[2 * i + 1] = mean + std_dev * r * theta.sin();
130            }
131        }
132
133        Ok(result.slice(scirs2_core::ndarray::s![..n]).to_owned())
134    }
135}
136
137/// SIMD-optimized inverse transform sampling
138///
139/// This function generates random samples using the inverse CDF method with
140/// SIMD acceleration where applicable.
141///
142/// # Arguments
143///
144/// * `n` - Number of samples to generate
145/// * `inverse_cdf` - Function that computes the inverse CDF
146/// * `seed` - Optional random seed
147///
148/// # Returns
149///
150/// Array of random samples from the distribution
151pub fn inverse_transform_simd<F, InvCDF>(
152    n: usize,
153    inverse_cdf: InvCDF,
154    seed: Option<u64>,
155) -> StatsResult<Array1<F>>
156where
157    F: Float + NumCast + SimdUnifiedOps + SampleUniform,
158    InvCDF: Fn(F) -> F,
159{
160    let mut rng = {
161        let s = seed.unwrap_or_else(|| {
162            use scirs2_core::random::Rng;
163            scirs2_core::random::thread_rng().random()
164        });
165        scirs2_core::random::seeded_rng(s)
166    };
167
168    // Generate uniform random numbers
169    let u: Array1<F> = Array1::from_shape_fn(n, |_| {
170        F::from(rng.gen_range(0.0..1.0)).unwrap_or_else(|| F::zero())
171    });
172
173    // Apply inverse CDF
174    let result = u.mapv(|ui| inverse_cdf(ui));
175
176    Ok(result)
177}
178
179/// SIMD-optimized exponential distribution sampling
180///
181/// Generates samples from an exponential distribution with given rate parameter
182/// using SIMD-accelerated logarithm operations.
183///
184/// # Arguments
185///
186/// * `n` - Number of samples to generate
187/// * `rate` - Rate parameter (lambda > 0)
188/// * `seed` - Optional random seed
189///
190/// # Returns
191///
192/// Array of exponentially distributed random samples
193pub fn exponential_simd<F>(n: usize, rate: F, seed: Option<u64>) -> StatsResult<Array1<F>>
194where
195    F: Float + NumCast + SimdUnifiedOps + SampleUniform,
196{
197    if rate <= F::zero() {
198        return Err(StatsError::invalid_argument("Rate must be positive"));
199    }
200
201    let mut rng = {
202        let s = seed.unwrap_or_else(|| {
203            use scirs2_core::random::Rng;
204            scirs2_core::random::thread_rng().random()
205        });
206        scirs2_core::random::seeded_rng(s)
207    };
208
209    let optimizer = AutoOptimizer::new();
210
211    // Generate uniform random numbers in (0, 1]
212    let u: Array1<F> = Array1::from_shape_fn(n, |_| {
213        F::from(rng.gen_range(F::epsilon().to_f64().unwrap_or(1e-10)..1.0))
214            .unwrap_or_else(|| F::one())
215    });
216
217    if optimizer.should_use_simd(n) {
218        // SIMD path: -ln(u) / rate
219        let ln_u = F::simd_ln(&u.view());
220        let minus_one = F::from(-1.0).unwrap_or_else(|| F::one());
221        let minus_one_array = Array1::from_elem(n, minus_one);
222        let neg_ln_u = F::simd_mul(&minus_one_array.view(), &ln_u.view());
223
224        let inv_rate = F::one() / rate;
225        let inv_rate_array = Array1::from_elem(n, inv_rate);
226        let result = F::simd_mul(&neg_ln_u.view(), &inv_rate_array.view());
227
228        Ok(result)
229    } else {
230        // Scalar fallback
231        Ok(u.mapv(|ui| -ui.ln() / rate))
232    }
233}
234
235/// SIMD-optimized bootstrap sampling with replacement
236///
237/// Generates bootstrap samples from the input data using SIMD operations
238/// for improved performance on large datasets.
239///
240/// # Arguments
241///
242/// * `data` - Input data array
243/// * `n_samples` - Number of bootstrap samples to generate
244/// * `seed` - Optional random seed
245///
246/// # Returns
247///
248/// Array of bootstrap samples
249pub fn bootstrap_simd<F>(
250    data: &ArrayView1<F>,
251    n_samples: usize,
252    seed: Option<u64>,
253) -> StatsResult<Array1<F>>
254where
255    F: Float + NumCast + SimdUnifiedOps,
256{
257    if data.is_empty() {
258        return Err(StatsError::invalid_argument("Data array cannot be empty"));
259    }
260
261    let n_data = data.len();
262    let mut rng = {
263        let s = seed.unwrap_or_else(|| {
264            use scirs2_core::random::Rng;
265            scirs2_core::random::thread_rng().random()
266        });
267        scirs2_core::random::seeded_rng(s)
268    };
269
270    // Generate random indices
271    let mut result = Array1::zeros(n_samples);
272    for i in 0..n_samples {
273        let idx = rng.gen_range(0..n_data);
274        result[i] = data[idx];
275    }
276
277    Ok(result)
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use approx::assert_abs_diff_eq;
284
285    #[test]
286    fn test_box_muller_simd_basic() {
287        let samples = box_muller_simd(1000, 0.0, 1.0, Some(42)).expect("Sampling failed");
288        assert_eq!(samples.len(), 1000);
289
290        // Check that mean is approximately 0
291        let mean: f64 = samples.iter().sum::<f64>() / samples.len() as f64;
292        assert_abs_diff_eq!(mean, 0.0, epsilon = 0.1);
293
294        // Check that std dev is approximately 1
295        let variance: f64 =
296            samples.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / samples.len() as f64;
297        assert_abs_diff_eq!(variance.sqrt(), 1.0, epsilon = 0.1);
298    }
299
300    #[test]
301    fn test_exponential_simd_basic() {
302        let rate = 2.0;
303        let samples = exponential_simd(1000, rate, Some(42)).expect("Sampling failed");
304        assert_eq!(samples.len(), 1000);
305
306        // Check that mean is approximately 1/rate
307        let mean: f64 = samples.iter().sum::<f64>() / samples.len() as f64;
308        let expected_mean = 1.0 / rate;
309        assert_abs_diff_eq!(mean, expected_mean, epsilon = 0.1);
310    }
311
312    #[test]
313    fn test_bootstrap_simd_basic() {
314        use scirs2_core::ndarray::array;
315
316        let data = array![1.0, 2.0, 3.0, 4.0, 5.0];
317        let samples = bootstrap_simd(&data.view(), 100, Some(42)).expect("Bootstrap failed");
318        assert_eq!(samples.len(), 100);
319
320        // Check that all samples are from the original data
321        for &sample in samples.iter() {
322            assert!(data.iter().any(|&x| (x - sample).abs() < 1e-10));
323        }
324    }
325}