use crate::error::QuadratureError;
#[cfg(not(feature = "std"))]
use alloc::{vec, vec::Vec};
const BITS: u32 = 32;
pub struct SobolSequence {
dim: usize,
direction: Vec<u32>,
index: u64,
state: Vec<u32>,
}
impl SobolSequence {
pub fn new(dim: usize) -> Result<Self, QuadratureError> {
if dim == 0 {
return Err(QuadratureError::InvalidInput("dimension must be >= 1"));
}
if dim > MAX_DIM {
return Err(QuadratureError::InvalidInput(
"Sobol sequence supports at most 40 dimensions",
));
}
let b = BITS as usize;
let mut direction = vec![0u32; dim * b];
for (i, d) in direction.iter_mut().enumerate().take(b) {
#[allow(clippy::cast_possible_truncation)]
let i_u32 = i as u32;
*d = 1u32 << (BITS - 1 - i_u32);
}
for j in 1..dim {
let entry = &SOBOL_TABLE[j - 1];
let s = entry.degree;
let a = entry.coeffs;
for i in 0..s as usize {
#[allow(clippy::cast_possible_truncation)]
let i_u32 = i as u32;
direction[j * b + i] = entry.m[i] << (BITS - 1 - i_u32);
}
for i in s as usize..b {
let mut v = direction[j * b + i - s as usize] >> s;
v ^= direction[j * b + i - s as usize];
for k in 1..s as usize {
if (a >> (s as usize - 1 - k)) & 1 == 1 {
v ^= direction[j * b + i - k];
}
}
direction[j * b + i] = v;
}
}
Ok(Self {
dim,
direction,
index: 0,
state: vec![0u32; dim],
})
}
pub fn next_point(&mut self, point: &mut [f64]) {
assert!(point.len() >= self.dim);
self.index += 1;
assert!(
self.index < (1u64 << BITS),
"Sobol sequence exhausted: at most 2^32 - 1 points are available"
);
let c = (self.index - 1).trailing_ones() as usize;
let b = BITS as usize;
#[allow(clippy::cast_precision_loss)]
let norm = 1.0 / (1u64 << BITS) as f64;
for (j, p) in point.iter_mut().enumerate().take(self.dim) {
self.state[j] ^= self.direction[j * b + c];
*p = f64::from(self.state[j]) * norm;
}
}
pub fn skip(&mut self, n: u64) {
assert!(
n < (1u64 << BITS) - 1,
"Sobol sequence exhausted: at most 2^32 - 1 points are available"
);
let b = BITS as usize;
self.state.fill(0);
self.index = 0;
let gray = n ^ (n >> 1);
for bit in 0..BITS {
if (gray >> bit) & 1 == 1 {
for j in 0..self.dim {
self.state[j] ^= self.direction[j * b + bit as usize];
}
}
}
self.index = n;
}
#[must_use]
pub fn index(&self) -> u64 {
self.index
}
#[must_use]
pub fn dim(&self) -> usize {
self.dim
}
}
const MAX_DIM: usize = 40;
struct SobolEntry {
degree: u32,
coeffs: u32,
m: [u32; 8],
}
static SOBOL_TABLE: [SobolEntry; 39] = [
SobolEntry {
degree: 1,
coeffs: 0,
m: [1, 0, 0, 0, 0, 0, 0, 0],
}, SobolEntry {
degree: 2,
coeffs: 1,
m: [1, 3, 0, 0, 0, 0, 0, 0],
}, SobolEntry {
degree: 3,
coeffs: 1,
m: [1, 3, 1, 0, 0, 0, 0, 0],
}, SobolEntry {
degree: 3,
coeffs: 2,
m: [1, 1, 1, 0, 0, 0, 0, 0],
}, SobolEntry {
degree: 4,
coeffs: 1,
m: [1, 1, 3, 3, 0, 0, 0, 0],
}, SobolEntry {
degree: 4,
coeffs: 4,
m: [1, 3, 5, 13, 0, 0, 0, 0],
}, SobolEntry {
degree: 5,
coeffs: 2,
m: [1, 1, 5, 5, 17, 0, 0, 0],
}, SobolEntry {
degree: 5,
coeffs: 4,
m: [1, 1, 5, 5, 5, 0, 0, 0],
}, SobolEntry {
degree: 5,
coeffs: 7,
m: [1, 1, 7, 11, 19, 0, 0, 0],
}, SobolEntry {
degree: 5,
coeffs: 11,
m: [1, 1, 5, 1, 1, 0, 0, 0],
}, SobolEntry {
degree: 5,
coeffs: 13,
m: [1, 1, 1, 3, 11, 0, 0, 0],
}, SobolEntry {
degree: 5,
coeffs: 14,
m: [1, 3, 5, 5, 31, 0, 0, 0],
}, SobolEntry {
degree: 6,
coeffs: 1,
m: [1, 3, 3, 9, 7, 49, 0, 0],
}, SobolEntry {
degree: 6,
coeffs: 13,
m: [1, 1, 1, 15, 21, 21, 0, 0],
}, SobolEntry {
degree: 6,
coeffs: 16,
m: [1, 3, 1, 13, 27, 49, 0, 0],
}, SobolEntry {
degree: 6,
coeffs: 19,
m: [1, 1, 1, 15, 7, 5, 0, 0],
}, SobolEntry {
degree: 6,
coeffs: 22,
m: [1, 3, 1, 15, 13, 25, 0, 0],
}, SobolEntry {
degree: 6,
coeffs: 25,
m: [1, 1, 5, 5, 19, 61, 0, 0],
}, SobolEntry {
degree: 7,
coeffs: 1,
m: [1, 3, 7, 11, 23, 15, 103, 0],
}, SobolEntry {
degree: 7,
coeffs: 4,
m: [1, 3, 7, 13, 13, 15, 69, 0],
}, SobolEntry {
degree: 7,
coeffs: 7,
m: [1, 1, 3, 13, 7, 35, 63, 0],
}, SobolEntry {
degree: 7,
coeffs: 8,
m: [1, 3, 5, 9, 1, 25, 53, 0],
}, SobolEntry {
degree: 7,
coeffs: 14,
m: [1, 3, 1, 13, 9, 35, 107, 0],
}, SobolEntry {
degree: 7,
coeffs: 19,
m: [1, 3, 1, 5, 27, 61, 31, 0],
}, SobolEntry {
degree: 7,
coeffs: 21,
m: [1, 1, 5, 11, 19, 41, 61, 0],
}, SobolEntry {
degree: 7,
coeffs: 28,
m: [1, 3, 5, 3, 3, 13, 69, 0],
}, SobolEntry {
degree: 7,
coeffs: 31,
m: [1, 1, 7, 13, 1, 19, 1, 0],
}, SobolEntry {
degree: 7,
coeffs: 32,
m: [1, 3, 7, 5, 13, 19, 59, 0],
}, SobolEntry {
degree: 7,
coeffs: 37,
m: [1, 1, 3, 9, 25, 29, 41, 0],
}, SobolEntry {
degree: 7,
coeffs: 41,
m: [1, 3, 5, 13, 23, 1, 55, 0],
}, SobolEntry {
degree: 7,
coeffs: 42,
m: [1, 3, 7, 3, 13, 59, 17, 0],
}, SobolEntry {
degree: 7,
coeffs: 50,
m: [1, 3, 1, 3, 5, 53, 69, 0],
}, SobolEntry {
degree: 7,
coeffs: 55,
m: [1, 1, 5, 5, 23, 33, 13, 0],
}, SobolEntry {
degree: 7,
coeffs: 56,
m: [1, 1, 7, 7, 1, 61, 123, 0],
}, SobolEntry {
degree: 7,
coeffs: 59,
m: [1, 1, 7, 9, 13, 61, 49, 0],
}, SobolEntry {
degree: 7,
coeffs: 62,
m: [1, 3, 3, 5, 3, 55, 33, 0],
}, SobolEntry {
degree: 8,
coeffs: 14,
m: [1, 3, 1, 15, 31, 13, 49, 245],
}, SobolEntry {
degree: 8,
coeffs: 21,
m: [1, 3, 5, 15, 31, 59, 63, 97],
}, SobolEntry {
degree: 8,
coeffs: 22,
m: [1, 3, 1, 11, 11, 11, 77, 249],
}, ];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_point_dim1() {
let mut sob = SobolSequence::new(1).unwrap();
let mut pt = [0.0];
sob.next_point(&mut pt);
assert!((pt[0] - 0.5).abs() < 1e-10); }
#[test]
fn points_in_unit_cube() {
let mut sob = SobolSequence::new(5).unwrap();
let mut pt = vec![0.0; 5];
for _ in 0..100 {
sob.next_point(&mut pt);
for &x in &pt {
assert!(x >= 0.0 && x < 1.0, "x={x} out of [0,1)");
}
}
}
#[test]
fn skip_and_generate() {
let mut sob1 = SobolSequence::new(3).unwrap();
let mut pt1 = vec![0.0; 3];
for _ in 0..10 {
sob1.next_point(&mut pt1);
}
let mut sob2 = SobolSequence::new(3).unwrap();
sob2.skip(9);
let mut pt2 = vec![0.0; 3];
sob2.next_point(&mut pt2);
for j in 0..3 {
assert!(
(pt1[j] - pt2[j]).abs() < 1e-14,
"j={j}: {} vs {}",
pt1[j],
pt2[j]
);
}
}
#[test]
fn invalid_dim() {
assert!(SobolSequence::new(0).is_err());
assert!(SobolSequence::new(41).is_err());
}
#[test]
#[should_panic(expected = "exhausted")]
fn skip_past_period_panics() {
let mut sob = SobolSequence::new(2).unwrap();
sob.skip((1u64 << BITS) - 1);
}
#[test]
fn high_dim_points_in_unit_cube() {
let dim = 20;
let mut sob = SobolSequence::new(dim).unwrap();
let mut pt = vec![0.0; dim];
for i in 0..200 {
sob.next_point(&mut pt);
for (j, &x) in pt.iter().enumerate() {
assert!(
x >= 0.0 && x < 1.0,
"point {i}, dim {j}: x={x} out of [0,1)"
);
}
}
}
#[test]
fn high_dim_30_points_in_unit_cube() {
let dim = 30;
let mut sob = SobolSequence::new(dim).unwrap();
let mut pt = vec![0.0; dim];
for i in 0..100 {
sob.next_point(&mut pt);
for (j, &x) in pt.iter().enumerate() {
assert!(
x >= 0.0 && x < 1.0,
"point {i}, dim {j}: x={x} out of [0,1)"
);
}
}
}
#[test]
fn dim2_matches_reference_sequence() {
let expected = [
[0.5, 0.5],
[0.75, 0.25],
[0.25, 0.75],
[0.375, 0.375],
[0.875, 0.875],
];
let mut sob = SobolSequence::new(2).unwrap();
let mut pt = [0.0; 2];
for (n, exp) in expected.iter().enumerate() {
sob.next_point(&mut pt);
for j in 0..2 {
assert!(
(pt[j] - exp[j]).abs() < 1e-12,
"point {}, dim {j}: {} vs {}",
n + 1,
pt[j],
exp[j]
);
}
}
}
#[test]
fn all_dimensions_pairwise_distinct() {
let dim = MAX_DIM;
let n = 256;
let mut sob = SobolSequence::new(dim).unwrap();
let mut cols: Vec<Vec<f64>> = (0..dim).map(|_| Vec::with_capacity(n)).collect();
let mut pt = vec![0.0; dim];
for _ in 0..n {
sob.next_point(&mut pt);
for (j, &x) in pt.iter().enumerate() {
cols[j].push(x);
}
}
for a in 0..dim {
for b in (a + 1)..dim {
assert!(cols[a] != cols[b], "dimensions {a} and {b} are identical");
}
}
}
#[test]
fn direction_table_structurally_valid() {
let mut counts = [0usize; 9]; let mut prev_degree = 0u32;
for (k, e) in SOBOL_TABLE.iter().enumerate() {
assert!(e.degree >= prev_degree, "degree decreased at entry {k}");
prev_degree = e.degree;
let s = e.degree as usize;
assert!((1..=8).contains(&s), "degree {s} out of range at entry {k}");
counts[s] += 1;
for i in 0..s {
let m = e.m[i];
assert!(m % 2 == 1, "m[{i}]={m} not odd at entry {k}");
assert!(
m < (1u32 << (i + 1)),
"m[{i}]={m} >= 2^{} at entry {k}",
i + 1
);
}
for i in s..8 {
assert_eq!(e.m[i], 0, "unused m[{i}] nonzero at entry {k}");
}
}
assert_eq!(&counts[1..=8], &[1, 1, 2, 2, 6, 6, 18, 3]);
for (k, e) in SOBOL_TABLE.iter().enumerate() {
for (l, f) in SOBOL_TABLE.iter().enumerate().skip(k + 1) {
assert!(
!(e.degree == f.degree && e.coeffs == f.coeffs),
"duplicate polynomial at entries {k} and {l}"
);
}
}
}
#[test]
fn sobol_couples_dimensions() {
let dim = 5;
let n = 4096;
let mut sob = SobolSequence::new(dim).unwrap();
let mut pt = vec![0.0; dim];
let mut sum = 0.0;
for _ in 0..n {
sob.next_point(&mut pt);
sum += pt[1] * pt[2];
}
let estimate = sum / n as f64;
assert!(
(estimate - 0.25).abs() < 1e-3,
"estimate={estimate}, expected 0.25"
);
}
#[test]
fn max_dim_construction() {
let sob = SobolSequence::new(40);
assert!(sob.is_ok());
let mut sob = sob.unwrap();
assert_eq!(sob.dim(), 40);
let mut pt = vec![0.0; 40];
sob.next_point(&mut pt);
for (j, &x) in pt.iter().enumerate() {
assert!(x >= 0.0 && x < 1.0, "dim {j}: x={x} out of [0,1)");
}
}
}