use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::random::rngs::StdRng;
use scirs2_core::random::{Rng, RngExt, SeedableRng};
use crate::error::{OptimizeError, OptimizeResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SamplingStrategy {
Random,
LatinHypercube,
Sobol,
Halton,
}
impl Default for SamplingStrategy {
fn default() -> Self {
Self::LatinHypercube
}
}
#[derive(Debug, Clone)]
pub struct SamplingConfig {
pub lhs_maximin_iters: usize,
pub seed: Option<u64>,
pub scramble: bool,
}
impl Default for SamplingConfig {
fn default() -> Self {
Self {
lhs_maximin_iters: 100,
seed: None,
scramble: true,
}
}
}
pub fn generate_samples(
n_samples: usize,
bounds: &[(f64, f64)],
strategy: SamplingStrategy,
config: Option<SamplingConfig>,
) -> OptimizeResult<Array2<f64>> {
let config = config.unwrap_or_default();
let n_dims = bounds.len();
if n_samples == 0 {
return Ok(Array2::zeros((0, n_dims)));
}
if n_dims == 0 {
return Err(OptimizeError::InvalidInput(
"Bounds must have at least one dimension".to_string(),
));
}
for (i, &(lo, hi)) in bounds.iter().enumerate() {
if lo >= hi {
return Err(OptimizeError::InvalidInput(format!(
"Lower bound must be strictly less than upper bound for dimension {} (got [{}, {}])",
i, lo, hi
)));
}
if !lo.is_finite() || !hi.is_finite() {
return Err(OptimizeError::InvalidInput(format!(
"Bounds must be finite for dimension {} (got [{}, {}])",
i, lo, hi
)));
}
}
match strategy {
SamplingStrategy::Random => random_sampling(n_samples, bounds, &config),
SamplingStrategy::LatinHypercube => latin_hypercube_sampling(n_samples, bounds, &config),
SamplingStrategy::Sobol => sobol_sampling(n_samples, bounds, &config),
SamplingStrategy::Halton => halton_sampling(n_samples, bounds, &config),
}
}
fn random_sampling(
n_samples: usize,
bounds: &[(f64, f64)],
config: &SamplingConfig,
) -> OptimizeResult<Array2<f64>> {
let n_dims = bounds.len();
let mut rng = make_rng(config.seed);
let mut samples = Array2::zeros((n_samples, n_dims));
for i in 0..n_samples {
for (j, &(lo, hi)) in bounds.iter().enumerate() {
samples[[i, j]] = lo + rng.random_range(0.0..1.0) * (hi - lo);
}
}
Ok(samples)
}
fn latin_hypercube_sampling(
n_samples: usize,
bounds: &[(f64, f64)],
config: &SamplingConfig,
) -> OptimizeResult<Array2<f64>> {
let n_dims = bounds.len();
let mut rng = make_rng(config.seed);
let mut unit_samples = Array2::zeros((n_samples, n_dims));
for j in 0..n_dims {
let mut perm: Vec<usize> = (0..n_samples).collect();
for i in (1..n_samples).rev() {
let swap_idx = rng.random_range(0..=i);
perm.swap(i, swap_idx);
}
for i in 0..n_samples {
let u: f64 = rng.random_range(0.0..1.0);
unit_samples[[i, j]] = (perm[i] as f64 + u) / n_samples as f64;
}
}
if config.lhs_maximin_iters > 0 && n_samples > 2 {
let mut best_min_dist = compute_min_distance(&unit_samples);
for _ in 0..config.lhs_maximin_iters {
let dim = rng.random_range(0..n_dims);
let r1 = rng.random_range(0..n_samples);
let mut r2 = rng.random_range(0..n_samples.saturating_sub(1));
if r2 >= r1 {
r2 += 1;
}
let tmp = unit_samples[[r1, dim]];
unit_samples[[r1, dim]] = unit_samples[[r2, dim]];
unit_samples[[r2, dim]] = tmp;
let new_min_dist = compute_min_distance(&unit_samples);
if new_min_dist > best_min_dist {
best_min_dist = new_min_dist;
} else {
let tmp = unit_samples[[r1, dim]];
unit_samples[[r1, dim]] = unit_samples[[r2, dim]];
unit_samples[[r2, dim]] = tmp;
}
}
}
let mut result = Array2::zeros((n_samples, n_dims));
for i in 0..n_samples {
for (j, &(lo, hi)) in bounds.iter().enumerate() {
result[[i, j]] = lo + unit_samples[[i, j]] * (hi - lo);
}
}
Ok(result)
}
fn compute_min_distance(samples: &Array2<f64>) -> f64 {
let n = samples.nrows();
if n < 2 {
return f64::INFINITY;
}
let mut min_dist = f64::INFINITY;
for i in 0..n {
for j in (i + 1)..n {
let mut sq_dist = 0.0;
for k in 0..samples.ncols() {
let d = samples[[i, k]] - samples[[j, k]];
sq_dist += d * d;
}
if sq_dist < min_dist {
min_dist = sq_dist;
}
}
}
min_dist.sqrt()
}
fn sobol_sampling(
n_samples: usize,
bounds: &[(f64, f64)],
config: &SamplingConfig,
) -> OptimizeResult<Array2<f64>> {
let n_dims = bounds.len();
let mut samples = Array2::zeros((n_samples, n_dims));
let direction_numbers = get_sobol_direction_numbers(n_dims)?;
for j in 0..n_dims {
let dirs = &direction_numbers[j];
let mut x: u64 = 0;
for i in 0..n_samples {
if j == 0 {
x = gray_code_sobol(i as u64 + 1);
} else {
if i == 0 {
x = 0;
} else {
let c = rightmost_zero_bit(i as u64);
let dir_idx = c.min(dirs.len() - 1);
x ^= dirs[dir_idx];
}
}
let value = x as f64 / (1u64 << 32) as f64;
let scrambled = if config.scramble {
let mut rng = make_rng(config.seed.map(|s| s.wrapping_add(j as u64 * 1000 + 7)));
let shift: f64 = rng.random_range(0.0..1.0);
(value + shift) % 1.0
} else {
value
};
let (lo, hi) = bounds[j];
samples[[i, j]] = lo + scrambled * (hi - lo);
}
}
Ok(samples)
}
fn gray_code_sobol(n: u64) -> u64 {
let mut result: u64 = 0;
let mut val = n;
let mut bit = 1u64 << 31;
while val > 0 {
if val & 1 != 0 {
result ^= bit;
}
val >>= 1;
bit >>= 1;
}
result
}
fn rightmost_zero_bit(n: u64) -> usize {
let mut val = n;
let mut c = 0usize;
while val & 1 != 0 {
val >>= 1;
c += 1;
}
c
}
fn get_sobol_direction_numbers(n_dims: usize) -> OptimizeResult<Vec<Vec<u64>>> {
let max_bits = 32usize;
let mut all_dirs = Vec::with_capacity(n_dims);
all_dirs.push(vec![0u64; max_bits]);
if n_dims <= 1 {
return Ok(all_dirs);
}
let primitive_polys: &[(u32, u32)] = &[
(1, 0), (2, 1), (3, 1), (3, 2), (4, 1), (4, 4), (5, 2), (5, 4), (5, 7), (5, 11), (5, 13), (5, 14), (6, 1), (6, 13), (6, 16), (6, 19), (6, 22), (6, 25), (7, 1), (7, 4), ];
let initial_m: &[&[u64]] = &[
&[1], &[1, 1], &[1, 1, 1], &[1, 3, 1], &[1, 1, 1, 1], &[1, 1, 3, 1], &[1, 3, 5, 1, 3], &[1, 3, 3, 1, 1], &[1, 3, 7, 7, 5], &[1, 1, 5, 1, 15], &[1, 3, 1, 3, 5], &[1, 3, 7, 7, 5], &[1, 1, 1, 1, 1, 1], &[1, 1, 5, 3, 13, 7], &[1, 3, 3, 1, 1, 1], &[1, 1, 1, 5, 7, 11], &[1, 1, 7, 3, 29, 3], &[1, 3, 7, 7, 21, 25], &[1, 1, 1, 1, 1, 1, 1], &[1, 3, 1, 1, 1, 7, 1], ];
for dim_idx in 1..n_dims {
let poly_idx = if dim_idx - 1 < primitive_polys.len() {
dim_idx - 1
} else {
(dim_idx - 1) % primitive_polys.len()
};
let (degree, poly_bits) = primitive_polys[poly_idx];
let s = degree as usize;
let mut dirs = vec![0u64; max_bits];
let init = if dim_idx - 1 < initial_m.len() {
initial_m[dim_idx - 1]
} else {
&[1u64; 1][..] };
for k in 0..s.min(max_bits) {
let m_k = if k < init.len() { init[k] } else { 1 };
dirs[k] = m_k << (max_bits - k - 1);
}
for k in s..max_bits {
let mut new_v = dirs[k - s] ^ (dirs[k - s] >> s);
for j in 1..s {
if (poly_bits >> (s - 1 - j)) & 1 == 1 {
new_v ^= dirs[k - j];
}
}
dirs[k] = new_v;
}
all_dirs.push(dirs);
}
Ok(all_dirs)
}
fn halton_sampling(
n_samples: usize,
bounds: &[(f64, f64)],
config: &SamplingConfig,
) -> OptimizeResult<Array2<f64>> {
let n_dims = bounds.len();
let primes = first_n_primes(n_dims);
let mut samples = Array2::zeros((n_samples, n_dims));
let shifts: Vec<f64> = if config.scramble {
let mut rng = make_rng(config.seed);
(0..n_dims).map(|_| rng.random_range(0.0..1.0)).collect()
} else {
vec![0.0; n_dims]
};
for i in 0..n_samples {
for j in 0..n_dims {
let raw = radical_inverse(i as u64 + 1, primes[j]);
let value = if config.scramble {
(raw + shifts[j]) % 1.0
} else {
raw
};
let (lo, hi) = bounds[j];
samples[[i, j]] = lo + value * (hi - lo);
}
}
Ok(samples)
}
fn radical_inverse(n: u64, base: u64) -> f64 {
let mut result = 0.0;
let mut denom = 1.0;
let mut val = n;
while val > 0 {
denom *= base as f64;
result += (val % base) as f64 / denom;
val /= base;
}
result
}
fn first_n_primes(n: usize) -> Vec<u64> {
if n == 0 {
return Vec::new();
}
let mut primes = Vec::with_capacity(n);
let mut candidate = 2u64;
while primes.len() < n {
let is_prime = primes
.iter()
.take_while(|&&p| p * p <= candidate)
.all(|&p| candidate % p != 0);
if is_prime {
primes.push(candidate);
}
candidate += 1;
}
primes
}
fn make_rng(seed: Option<u64>) -> StdRng {
match seed {
Some(s) => StdRng::seed_from_u64(s),
None => {
let s: u64 = scirs2_core::random::rng().random();
StdRng::seed_from_u64(s)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bounds_2d() -> Vec<(f64, f64)> {
vec![(-5.0, 5.0), (0.0, 10.0)]
}
fn bounds_5d() -> Vec<(f64, f64)> {
vec![
(0.0, 1.0),
(-1.0, 1.0),
(0.0, 100.0),
(-10.0, 10.0),
(5.0, 15.0),
]
}
#[test]
fn test_random_sampling_shape() {
let samples = generate_samples(20, &bounds_2d(), SamplingStrategy::Random, None)
.expect("should succeed");
assert_eq!(samples.nrows(), 20);
assert_eq!(samples.ncols(), 2);
}
#[test]
fn test_random_sampling_within_bounds() {
let b = bounds_2d();
let samples =
generate_samples(100, &b, SamplingStrategy::Random, None).expect("should succeed");
for i in 0..samples.nrows() {
for (j, &(lo, hi)) in b.iter().enumerate() {
assert!(
samples[[i, j]] >= lo && samples[[i, j]] <= hi,
"sample[{},{}] = {} not in [{}, {}]",
i,
j,
samples[[i, j]],
lo,
hi
);
}
}
}
#[test]
fn test_lhs_shape_and_bounds() {
let b = bounds_5d();
let samples = generate_samples(30, &b, SamplingStrategy::LatinHypercube, None)
.expect("should succeed");
assert_eq!(samples.nrows(), 30);
assert_eq!(samples.ncols(), 5);
for i in 0..samples.nrows() {
for (j, &(lo, hi)) in b.iter().enumerate() {
assert!(
samples[[i, j]] >= lo && samples[[i, j]] <= hi,
"LHS sample[{},{}] = {} not in [{}, {}]",
i,
j,
samples[[i, j]],
lo,
hi
);
}
}
}
#[test]
fn test_lhs_stratification() {
let n = 10;
let bounds = vec![(0.0, 1.0); 3];
let cfg = SamplingConfig {
lhs_maximin_iters: 0, seed: Some(42),
scramble: false,
};
let samples = generate_samples(n, &bounds, SamplingStrategy::LatinHypercube, Some(cfg))
.expect("should succeed");
for j in 0..3 {
let mut strata = vec![false; n];
for i in 0..n {
let stratum = (samples[[i, j]] * n as f64).floor() as usize;
let stratum = stratum.min(n - 1);
strata[stratum] = true;
}
for (s, &occupied) in strata.iter().enumerate() {
assert!(occupied, "Stratum {} in dimension {} is unoccupied", s, j);
}
}
}
#[test]
fn test_lhs_maximin_improves_spacing() {
let n = 15;
let bounds = vec![(0.0, 1.0); 2];
let cfg0 = SamplingConfig {
lhs_maximin_iters: 0,
seed: Some(123),
scramble: false,
};
let s0 = generate_samples(n, &bounds, SamplingStrategy::LatinHypercube, Some(cfg0))
.expect("should succeed");
let cfg1 = SamplingConfig {
lhs_maximin_iters: 500,
seed: Some(123),
scramble: false,
};
let s1 = generate_samples(n, &bounds, SamplingStrategy::LatinHypercube, Some(cfg1))
.expect("should succeed");
let d0 = compute_min_distance(&s0);
let d1 = compute_min_distance(&s1);
assert!(
d1 >= d0 - 1e-12,
"Maximin LHS should not decrease min distance: d_opt={} < d_raw={}",
d1,
d0
);
}
#[test]
fn test_sobol_shape_and_bounds() {
let b = bounds_2d();
let samples =
generate_samples(32, &b, SamplingStrategy::Sobol, None).expect("should succeed");
assert_eq!(samples.nrows(), 32);
assert_eq!(samples.ncols(), 2);
for i in 0..samples.nrows() {
for (j, &(lo, hi)) in b.iter().enumerate() {
assert!(
samples[[i, j]] >= lo && samples[[i, j]] <= hi,
"Sobol sample[{},{}] = {} not in [{}, {}]",
i,
j,
samples[[i, j]],
lo,
hi
);
}
}
}
#[test]
fn test_sobol_reproducibility() {
let b = bounds_2d();
let cfg = SamplingConfig {
seed: Some(99),
scramble: true,
..Default::default()
};
let s1 = generate_samples(16, &b, SamplingStrategy::Sobol, Some(cfg.clone()))
.expect("should succeed");
let s2 =
generate_samples(16, &b, SamplingStrategy::Sobol, Some(cfg)).expect("should succeed");
assert_eq!(s1, s2);
}
#[test]
fn test_halton_shape_and_bounds() {
let b = bounds_5d();
let samples =
generate_samples(50, &b, SamplingStrategy::Halton, None).expect("should succeed");
assert_eq!(samples.nrows(), 50);
assert_eq!(samples.ncols(), 5);
for i in 0..samples.nrows() {
for (j, &(lo, hi)) in b.iter().enumerate() {
assert!(
samples[[i, j]] >= lo && samples[[i, j]] <= hi,
"Halton sample[{},{}] = {} not in [{}, {}]",
i,
j,
samples[[i, j]],
lo,
hi
);
}
}
}
#[test]
fn test_halton_low_discrepancy() {
let bounds = vec![(0.0, 1.0)];
let cfg = SamplingConfig {
seed: None,
scramble: false,
..Default::default()
};
let samples = generate_samples(4, &bounds, SamplingStrategy::Halton, Some(cfg))
.expect("should succeed");
let expected = [0.5, 0.25, 0.75, 0.125];
for (i, &exp) in expected.iter().enumerate() {
assert!(
(samples[[i, 0]] - exp).abs() < 1e-10,
"Halton[{}] = {}, expected {}",
i,
samples[[i, 0]],
exp
);
}
}
#[test]
fn test_zero_samples() {
let samples = generate_samples(0, &bounds_2d(), SamplingStrategy::Random, None)
.expect("should succeed");
assert_eq!(samples.nrows(), 0);
}
#[test]
fn test_single_sample() {
for strategy in &[
SamplingStrategy::Random,
SamplingStrategy::LatinHypercube,
SamplingStrategy::Sobol,
SamplingStrategy::Halton,
] {
let samples =
generate_samples(1, &bounds_2d(), *strategy, None).expect("should succeed");
assert_eq!(samples.nrows(), 1);
assert_eq!(samples.ncols(), 2);
}
}
#[test]
fn test_invalid_bounds_rejected() {
let result = generate_samples(10, &[(5.0, 5.0)], SamplingStrategy::Random, None);
assert!(result.is_err());
let result = generate_samples(
10,
&[(f64::NEG_INFINITY, 1.0)],
SamplingStrategy::Random,
None,
);
assert!(result.is_err());
}
#[test]
fn test_first_n_primes() {
let p = first_n_primes(10);
assert_eq!(p, vec![2, 3, 5, 7, 11, 13, 17, 19, 23, 29]);
}
#[test]
fn test_radical_inverse() {
assert!((radical_inverse(1, 2) - 0.5).abs() < 1e-15);
assert!((radical_inverse(2, 2) - 0.25).abs() < 1e-15);
assert!((radical_inverse(3, 2) - 0.75).abs() < 1e-15);
assert!((radical_inverse(1, 3) - 1.0 / 3.0).abs() < 1e-15);
}
#[test]
fn test_high_dimensional_sampling() {
let bounds: Vec<(f64, f64)> = (0..15).map(|_| (0.0, 1.0)).collect();
for strategy in &[
SamplingStrategy::Random,
SamplingStrategy::LatinHypercube,
SamplingStrategy::Sobol,
SamplingStrategy::Halton,
] {
let samples = generate_samples(20, &bounds, *strategy, None).expect("should succeed");
assert_eq!(samples.nrows(), 20);
assert_eq!(samples.ncols(), 15);
}
}
}