scirs2_integrate/
qmc.rs

1//! Quasi-Monte Carlo integration
2//!
3//! This module provides Quasi-Monte Carlo integration methods for numerical integration
4//! of multidimensional functions.
5//!
6//! QMC integration uses low-discrepancy sequences to generate evaluation points,
7//! offering better convergence rates than traditional Monte Carlo methods for many
8//! integration problems, especially in higher dimensions.
9//!
10//! The module includes several low-discrepancy sequence generators:
11//! - **Sobol sequences**: Well-distributed points with good coverage
12//! - **Halton sequences**: Based on van der Corput sequences with different prime bases
13//! - **Faure sequences**: Based on prime bases with matrix scrambling for improved uniformity
14
15use scirs2_core::ndarray::{s, Array1, Array2, ArrayView1};
16use scirs2_core::numeric::{Float, FromPrimitive};
17use scirs2_core::random::random;
18use std::fmt;
19
20use crate::error::{IntegrateError, IntegrateResult};
21
22/// Result type for QMC integration
23#[derive(Clone, Debug)]
24pub struct QMCQuadResult<T> {
25    /// The estimate of the integral
26    pub integral: T,
27    /// The error estimate
28    pub standard_error: T,
29}
30
31impl<T: fmt::Display> fmt::Display for QMCQuadResult<T> {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        write!(
34            f,
35            "QMCQuadResult(integral={}, standard_error={})",
36            self.integral, self.standard_error
37        )
38    }
39}
40
41/// Trait for quasi-random number generators
42pub trait QRNGEngine: Send + Sync {
43    /// Generate n points in d dimensions in the unit hypercube [0,1]^d
44    fn random(&mut self, n: usize) -> Array2<f64>;
45
46    /// Dimensionality of the generator
47    fn dim(&self) -> usize;
48
49    /// Create a new instance from a seed
50    fn new_from_seed(&self, seed: u64) -> Box<dyn QRNGEngine>;
51}
52
53/// Simple pseudorandom number generator for benchmarking
54pub struct RandomGenerator {
55    dim: usize,
56    // We don't use the _seed for random generation, just for new_from_seed
57    _seed: u64,
58}
59
60impl RandomGenerator {
61    /// Create a new random number generator
62    pub fn new(dim: usize, seed: Option<u64>) -> Self {
63        let _seed = seed.unwrap_or_else(random::<u64>);
64
65        Self { dim, _seed }
66    }
67}
68
69impl QRNGEngine for RandomGenerator {
70    fn random(&mut self, n: usize) -> Array2<f64> {
71        let mut result = Array2::zeros((n, self.dim));
72
73        for i in 0..n {
74            for j in 0..self.dim {
75                result[[i, j]] = random::<f64>();
76            }
77        }
78
79        result
80    }
81
82    fn dim(&self) -> usize {
83        self.dim
84    }
85
86    fn new_from_seed(&self, seed: u64) -> Box<dyn QRNGEngine> {
87        Box::new(Self::new(self.dim, Some(seed)))
88    }
89}
90
91/// Sobol sequence generator
92pub struct Sobol {
93    dim: usize,
94    seed: u64,
95    curr_index: u64,
96    direction_numbers: Vec<Vec<u64>>,
97    last_point: Vec<u64>,
98}
99
100impl Sobol {
101    /// Create a new Sobol sequence generator
102    pub fn new(dim: usize, seed: Option<u64>) -> Self {
103        let seed = seed.unwrap_or_else(random::<u64>);
104
105        let mut sobol = Self {
106            dim,
107            seed,
108            curr_index: 0,
109            direction_numbers: Vec::new(),
110            last_point: vec![0; dim],
111        };
112
113        sobol.initialize_direction_numbers();
114        sobol
115    }
116
117    /// Initialize direction numbers for Sobol sequence
118    fn initialize_direction_numbers(&mut self) {
119        self.direction_numbers = vec![Vec::new(); self.dim];
120
121        // First dimension uses powers of 2
122        self.direction_numbers[0] = (0..64).map(|i| 1u64 << (63 - i)).collect();
123
124        // For higher dimensions, use primitive polynomials and initial direction numbers
125        // This is a simplified set for up to 10 dimensions
126        let primitive_polynomials = [
127            0,  // dimension 0 (not used)
128            0,  // dimension 1 (powers of 2)
129            3,  // x + 1
130            7,  // x^2 + x + 1
131            11, // x^3 + x + 1
132            13, // x^3 + x^2 + 1
133            19, // x^4 + x + 1
134            25, // x^4 + x^3 + 1
135            37, // x^5 + x^2 + 1
136            41, // x^5 + x^3 + 1
137            55, // x^5 + x^4 + x^2 + x + 1
138        ];
139
140        let initial_numbers = vec![
141            vec![], // dimension 0
142            vec![], // dimension 1 (powers of 2)
143            vec![1],
144            vec![1, 1],
145            vec![1, 3, 1],
146            vec![1, 1, 3],
147            vec![1, 3, 3, 9],
148            vec![1, 1, 5, 5],
149            vec![1, 3, 1, 13],
150            vec![1, 1, 5, 5, 17],
151            vec![1, 3, 5, 5, 5],
152        ];
153
154        for d in 1..std::cmp::min(self.dim, primitive_polynomials.len()) {
155            let poly = primitive_polynomials[d];
156            let init_nums = &initial_numbers[d];
157
158            self.direction_numbers[d] = vec![0; 64];
159
160            // Set initial direction numbers
161            for (i, &num) in init_nums.iter().enumerate() {
162                self.direction_numbers[d][i] = (num as u64) << (63 - i);
163            }
164
165            // Generate remaining direction numbers using recurrence relation
166            let degree = self.bit_length(poly) - 1;
167            for i in degree..64 {
168                let mut value = self.direction_numbers[d][i - degree];
169
170                // Apply primitive polynomial recurrence
171                let mut poly_temp = poly;
172                for j in 1..degree {
173                    if poly_temp & 1 == 1 {
174                        value ^= self.direction_numbers[d][i - j];
175                    }
176                    poly_temp >>= 1;
177                }
178
179                self.direction_numbers[d][i] = value;
180            }
181        }
182
183        // For dimensions beyond our predefined set, use van der Corput sequences
184        for d in primitive_polynomials.len()..self.dim {
185            self.direction_numbers[d] = (0..64)
186                .map(|i| {
187                    let base = 2 + (d - primitive_polynomials.len()) as u64;
188                    self.van_der_corput_direction_number(i, base)
189                })
190                .collect();
191        }
192    }
193
194    /// Calculate bit length of a number
195    fn bit_length(&self, mut n: u64) -> usize {
196        let mut length = 0;
197        while n > 0 {
198            length += 1;
199            n >>= 1;
200        }
201        length
202    }
203
204    /// Generate van der Corput direction number as fallback
205    fn van_der_corput_direction_number(&self, i: usize, base: u64) -> u64 {
206        if i == 0 {
207            1u64 << 63
208        } else {
209            let mut value = 0u64;
210            let mut n = i + 1;
211            let mut denom = base;
212
213            while n > 0 && denom <= (1u64 << 63) {
214                value |= ((n % base as usize) as u64) << (64 - self.bit_length(denom));
215                n /= base as usize;
216                denom *= base;
217            }
218
219            value
220        }
221    }
222
223    /// Generate a Sobol sequence point using proper Sobol algorithm
224    fn generate_point(&mut self) -> Vec<f64> {
225        if self.curr_index == 0 {
226            self.curr_index = 1;
227            return vec![0.0; self.dim];
228        }
229
230        // Find rightmost zero bit in Gray code representation
231        let gray_code_index = self.curr_index ^ (self.curr_index >> 1);
232        let rightmost_zero = (!gray_code_index).trailing_zeros() as usize;
233
234        // Update the Sobol point
235        for d in 0..self.dim {
236            if rightmost_zero < self.direction_numbers[d].len() {
237                self.last_point[d] ^= self.direction_numbers[d][rightmost_zero];
238            }
239        }
240
241        self.curr_index += 1;
242
243        // Convert to floating point
244        self.last_point
245            .iter()
246            .map(|&x| (x as f64) / u64::MAX as f64)
247            .collect()
248    }
249}
250
251impl QRNGEngine for Sobol {
252    fn random(&mut self, n: usize) -> Array2<f64> {
253        let mut result = Array2::zeros((n, self.dim));
254
255        for i in 0..n {
256            let point = self.generate_point();
257            for j in 0..self.dim {
258                result[[i, j]] = point[j];
259            }
260        }
261
262        result
263    }
264
265    fn dim(&self) -> usize {
266        self.dim
267    }
268
269    fn new_from_seed(&self, seed: u64) -> Box<dyn QRNGEngine> {
270        Box::new(Self::new(self.dim, Some(seed)))
271    }
272}
273
274/// Halton sequence generator
275pub struct Halton {
276    dim: usize,
277    seed: u64,
278    curr_index: usize,
279}
280
281impl Halton {
282    /// Create a new Halton sequence generator
283    pub fn new(dim: usize, seed: Option<u64>) -> Self {
284        let seed = seed.unwrap_or_else(random::<u64>);
285
286        Self {
287            dim,
288            seed,
289            curr_index: 0,
290        }
291    }
292
293    /// Generate a Halton sequence point
294    fn generate_point(&mut self) -> Vec<f64> {
295        let first_primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53];
296        let mut result = vec![0.0; self.dim];
297
298        // Scrambling offset using seed
299        let offset = (self.seed % 1000) as usize;
300
301        for d in 0..self.dim {
302            let base = if d < first_primes.len() {
303                first_primes[d]
304            } else {
305                // For dimensions beyond stored primes, use a simple formula
306                first_primes[d % first_primes.len()] + d
307            };
308
309            let mut f = 1.0;
310            let mut r = 0.0;
311            let mut n = self.curr_index + offset;
312
313            while n > 0 {
314                f /= base as f64;
315                r += f * (n % base) as f64;
316                n /= base;
317            }
318
319            result[d] = r;
320        }
321
322        self.curr_index += 1;
323        result
324    }
325}
326
327impl QRNGEngine for Halton {
328    fn random(&mut self, n: usize) -> Array2<f64> {
329        let mut result = Array2::zeros((n, self.dim));
330
331        for i in 0..n {
332            let point = self.generate_point();
333            for j in 0..self.dim {
334                result[[i, j]] = point[j];
335            }
336        }
337
338        result
339    }
340
341    fn dim(&self) -> usize {
342        self.dim
343    }
344
345    fn new_from_seed(&self, seed: u64) -> Box<dyn QRNGEngine> {
346        Box::new(Self::new(self.dim, Some(seed)))
347    }
348}
349
350/// Faure sequence generator
351pub struct Faure {
352    dim: usize,
353    seed: u64,
354    curr_index: usize,
355}
356
357impl Faure {
358    /// Create a new Faure sequence generator
359    pub fn new(dim: usize, seed: Option<u64>) -> Self {
360        let seed = seed.unwrap_or_else(random::<u64>);
361
362        Self {
363            dim,
364            seed,
365            curr_index: 0,
366        }
367    }
368
369    /// Generate a Faure sequence point
370    ///
371    /// The Faure sequence is based on a prime base p >= dim, where p is the smallest
372    /// prime number greater than or equal to the dimension.
373    fn generate_point(&mut self) -> Vec<f64> {
374        let base = self.find_prime_base(self.dim);
375        let mut result = vec![0.0; self.dim];
376
377        // Scrambling offset using seed
378        let offset = (self.seed % 1000) as usize;
379        let scrambled_index = self.curr_index + offset;
380
381        // Generate coordinates using Faure matrices
382        for (d, result_elem) in result.iter_mut().enumerate().take(self.dim) {
383            *result_elem = self.faure_coordinate(scrambled_index, d, base);
384        }
385
386        self.curr_index += 1;
387        result
388    }
389
390    /// Find the smallest prime >= n
391    fn find_prime_base(&self, n: usize) -> usize {
392        if n <= 2 {
393            return 2;
394        }
395
396        let mut candidate = n;
397        while !self.is_prime(candidate) {
398            candidate += 1;
399        }
400        candidate
401    }
402
403    /// Simple primality test
404    fn is_prime(&self, n: usize) -> bool {
405        if n < 2 {
406            return false;
407        }
408        if n == 2 {
409            return true;
410        }
411        if n.is_multiple_of(2) {
412            return false;
413        }
414
415        let sqrt_n = (n as f64).sqrt() as usize;
416        for i in (3..=sqrt_n).step_by(2) {
417            if n.is_multiple_of(i) {
418                return false;
419            }
420        }
421        true
422    }
423
424    /// Generate d-th coordinate using Faure construction
425    fn faure_coordinate(&self, index: usize, dimension: usize, base: usize) -> f64 {
426        if index == 0 {
427            return 0.0;
428        }
429
430        // Convert index to base-p representation and apply Faure matrix
431        let digits = self.to_base_digits(index, base);
432        let mut result = 0.0;
433        let mut base_power = base as f64;
434
435        // Apply Faure matrix transformation for dimension d
436        for (i, &digit) in digits.iter().enumerate() {
437            let transformed_digit = self.faure_matrix_element(i, dimension, base) * digit;
438            result += (transformed_digit % base) as f64 / base_power;
439            base_power *= base as f64;
440        }
441
442        result.fract() // Ensure result is in [0, 1)
443    }
444
445    /// Convert integer to base-p digits (least significant first)
446    fn to_base_digits(&self, mut n: usize, base: usize) -> Vec<usize> {
447        if n == 0 {
448            return vec![0];
449        }
450
451        let mut digits = Vec::new();
452        while n > 0 {
453            digits.push(n % base);
454            n /= base;
455        }
456        digits
457    }
458
459    /// Simplified Faure matrix element (for educational implementation)
460    /// In practice, this would use precomputed Pascal triangle modulo p
461    fn faure_matrix_element(&self, i: usize, dimension: usize, base: usize) -> usize {
462        // Simplified implementation using binomial coefficients mod p
463        // For a proper implementation, use Lucas' theorem and Pascal's triangle
464        if dimension == 0 {
465            if i == 0 {
466                1
467            } else {
468                0
469            }
470        } else {
471            // Approximate with a simple formula for demonstration
472            let offset = dimension * 7 + self.seed as usize % 13; // Add some scrambling
473            ((i + offset + 1) % base + 1) % base
474        }
475    }
476}
477
478impl QRNGEngine for Faure {
479    fn random(&mut self, n: usize) -> Array2<f64> {
480        let mut result = Array2::zeros((n, self.dim));
481
482        for i in 0..n {
483            let point = self.generate_point();
484            for j in 0..self.dim {
485                result[[i, j]] = point[j];
486            }
487        }
488
489        result
490    }
491
492    fn dim(&self) -> usize {
493        self.dim
494    }
495
496    fn new_from_seed(&self, seed: u64) -> Box<dyn QRNGEngine> {
497        Box::new(Self::new(self.dim, Some(seed)))
498    }
499}
500
501/// Scale samples from unit hypercube to the integration range [a, b]
502#[allow(dead_code)]
503pub fn scale<T: Float + FromPrimitive>(
504    sample: &Array2<T>,
505    a: &Array1<T>,
506    b: &Array1<T>,
507) -> Array2<T> {
508    let mut scaled = Array2::<T>::zeros(sample.raw_dim());
509    let dim = sample.shape()[1];
510
511    for i in 0..sample.shape()[0] {
512        for j in 0..dim {
513            scaled[[i, j]] = a[j] + (b[j] - a[j]) * sample[[i, j]];
514        }
515    }
516
517    scaled
518}
519
520/// Compute an integral in N dimensions using Quasi-Monte Carlo quadrature.
521///
522/// # Parameters
523///
524/// * `func` - Function to integrate that takes a point in n-dimensional space and
525///   returns a scalar value.
526/// * `a` - Lower integration bounds, array of length equal to number of dimensions.
527/// * `b` - Upper integration bounds, array of length equal to number of dimensions.
528/// * `n_estimates` - Number of statistically independent samples to use (default: 8).
529/// * `n_points` - Number of QMC points per sample (default: 1024).
530/// * `qrng` - QRNGEngine to use for sampling points. If None, a Halton sequence is used.
531/// * `log` - If true, treat func as returning the log of the integrand and return log
532///   of the result.
533///
534/// # Returns
535///
536/// * `QMCQuadResult` containing the integral estimate and standard error.
537///
538/// # Examples
539///
540/// ```
541/// use scirs2_core::ndarray::{Array1, ArrayView1};
542/// use scirs2_integrate::qmc::{qmc_quad, Halton};
543///
544/// let f = |x: ArrayView1<f64>| x[0].powi(2) * x[1].exp();
545/// let a = Array1::from_vec(vec![0.0, 0.0]);
546/// let b = Array1::from_vec(vec![1.0, 1.0]);
547///
548/// let qrng = Halton::new(2, Some(42));
549/// let result = qmc_quad(f, &a, &b, None, None, Some(Box::new(qrng)), false).unwrap();
550/// println!("Integral: {}, Error: {}", result.integral, result.standard_error);
551/// ```
552#[allow(dead_code)]
553pub fn qmc_quad<F>(
554    func: F,
555    a: &Array1<f64>,
556    b: &Array1<f64>,
557    n_estimates: Option<usize>,
558    n_points: Option<usize>,
559    qrng: Option<Box<dyn QRNGEngine>>,
560    log: bool,
561) -> IntegrateResult<QMCQuadResult<f64>>
562where
563    F: Fn(ArrayView1<f64>) -> f64,
564{
565    let n_estimates = n_estimates.unwrap_or(8);
566    let n_points = n_points.unwrap_or(1024);
567
568    // Input validation
569    if a.len() != b.len() {
570        return Err(IntegrateError::ValueError(
571            "Dimension mismatch: 'a' and 'b' must have the same length".to_string(),
572        ));
573    }
574
575    let dim = a.len();
576
577    // Initialize QRNG if not provided
578    let mut qrng = qrng.unwrap_or_else(|| Box::new(Halton::new(dim, None)));
579
580    if qrng.dim() != dim {
581        return Err(IntegrateError::ValueError(format!(
582            "QRNG dimension ({}) does not match integration dimension ({})",
583            qrng.dim(),
584            dim
585        )));
586    }
587
588    // Check if a = b for any dimension, return 0 if so
589    for i in 0..dim {
590        if (a[i] - b[i]).abs() < f64::EPSILON {
591            return Ok(QMCQuadResult {
592                integral: if log { f64::NEG_INFINITY } else { 0.0 },
593                standard_error: 0.0,
594            });
595        }
596    }
597
598    // Swap limits if a > b and record the sign change
599    let mut a_mod = a.clone();
600    let mut b_mod = b.clone();
601    let mut sign = 1.0;
602
603    for i in 0..dim {
604        if a[i] > b[i] {
605            a_mod[i] = b[i];
606            b_mod[i] = a[i];
607            sign *= -1.0;
608        }
609    }
610
611    // Volume of the hypercube
612    let volume = (0..dim).map(|i| b_mod[i] - a_mod[i]).product::<f64>();
613    let delta = volume / (n_points as f64);
614
615    // Prepare for multiple _estimates
616    let mut estimates = Array1::<f64>::zeros(n_estimates);
617
618    // Generate independent samples and compute _estimates
619    for i in 0..n_estimates {
620        // Generate QMC sample
621        let sample = qrng.random(n_points);
622
623        // Scale to integration domain
624        let x = scale(&sample, &a_mod, &b_mod);
625
626        // Evaluate function at sample _points
627        let mut sum = 0.0;
628
629        if log {
630            let mut max_val = f64::NEG_INFINITY;
631            let mut log_values = Vec::with_capacity(n_points);
632
633            for j in 0..n_points {
634                let val = func(x.slice(s![j, ..]));
635                log_values.push(val);
636                if val > max_val {
637                    max_val = val;
638                }
639            }
640
641            // Compute log sum exp
642            let mut sum_exp = 0.0;
643            for val in log_values {
644                sum_exp += (val - max_val).exp();
645            }
646
647            estimates[i] = max_val + sum_exp.ln() + delta.ln();
648        } else {
649            for j in 0..n_points {
650                sum += func(x.slice(s![j, ..]));
651            }
652
653            estimates[i] = sum * delta;
654        }
655
656        // Get a new QRNG for next estimate with different scrambling
657        let seed = i as u64 + 1;
658        qrng = qrng.new_from_seed(seed);
659    }
660
661    // Compute final estimate and error
662    let integral = if log {
663        let mut max_val = f64::NEG_INFINITY;
664        for i in 0..n_estimates {
665            if estimates[i] > max_val {
666                max_val = estimates[i];
667            }
668        }
669
670        let mut sum_exp = 0.0;
671        for i in 0..n_estimates {
672            sum_exp += (estimates[i] - max_val).exp();
673        }
674
675        max_val + (sum_exp / (n_estimates as f64)).ln()
676    } else {
677        estimates.sum() / (n_estimates as f64)
678    };
679
680    // Compute standard error
681    let standard_error = if n_estimates > 1 {
682        if log {
683            // For log space, compute standard error differently
684            let mean = integral;
685            let mut variance = 0.0;
686
687            for i in 0..n_estimates {
688                let diff = (estimates[i] - mean).exp();
689                variance += (diff - 1.0).powi(2);
690            }
691
692            variance /= (n_estimates - 1) as f64;
693            (variance / (n_estimates as f64)).sqrt().ln()
694        } else {
695            let mean = integral;
696            let mut variance = 0.0;
697
698            for estimate in estimates.iter().take(n_estimates) {
699                variance += (estimate - mean).powi(2);
700            }
701
702            variance /= (n_estimates - 1) as f64;
703            (variance / (n_estimates as f64)).sqrt()
704        }
705    } else if log {
706        f64::NEG_INFINITY
707    } else {
708        0.0
709    };
710
711    // Apply sign correction for reversed limits
712    let final_integral = if log && sign < 0.0 {
713        // For negative results in log space, we'd need complex numbers
714        // Since we don't support complex results, we'll just negate the result
715        // and warn about it in the documentation
716        -integral
717    } else {
718        integral * sign
719    };
720
721    Ok(QMCQuadResult {
722        integral: final_integral,
723        standard_error,
724    })
725}
726
727/// Parallel Quasi-Monte Carlo integration with workers parameter
728///
729/// This function provides the same functionality as `qmc_quad` but with
730/// explicit control over the number of worker threads for parallel evaluation.
731///
732/// # Arguments
733///
734/// * `func` - The function to integrate
735/// * `a` - Lower bounds of integration for each dimension
736/// * `b` - Upper bounds of integration for each dimension
737/// * `n_estimates` - Number of independent estimates to compute (default: 8)
738/// * `n_points` - Number of sample points per estimate (default: 1024)
739/// * `qrng` - QRNGEngine to use for sampling points. If None, a Halton sequence is used.
740/// * `log` - If true, treat func as returning the log of the integrand and return log
741///   of the result.
742/// * `workers` - Number of worker threads to use. If None, uses all available cores.
743///
744/// # Returns
745///
746/// * `QMCQuadResult` containing the integral estimate and standard error.
747///
748/// # Examples
749///
750/// ```
751/// use scirs2_integrate::qmc::{qmc_quad_parallel, Halton};
752/// use scirs2_core::ndarray::{Array1, ArrayView1};
753///
754/// let f = |x: ArrayView1<f64>| x[0].powi(2) * x[1].exp();
755/// let a = Array1::from_vec(vec![0.0, 0.0]);
756/// let b = Array1::from_vec(vec![1.0, 1.0]);
757///
758/// let qrng = Halton::new(2, Some(42));
759/// let result = qmc_quad_parallel(
760///     f, &a, &b, None, None, Some(Box::new(qrng)), false, Some(4)
761/// ).unwrap();
762/// println!("Integral: {}, Error: {}", result.integral, result.standard_error);
763/// ```
764#[allow(dead_code)]
765pub fn qmc_quad_parallel<F>(
766    func: F,
767    a: &Array1<f64>,
768    b: &Array1<f64>,
769    n_estimates: Option<usize>,
770    n_points: Option<usize>,
771    qrng: Option<Box<dyn QRNGEngine>>,
772    log: bool,
773    workers: Option<usize>,
774) -> IntegrateResult<QMCQuadResult<f64>>
775where
776    F: Fn(ArrayView1<f64>) -> f64 + Send + Sync,
777{
778    let n_estimates = n_estimates.unwrap_or(8);
779    let n_points = n_points.unwrap_or(1024);
780
781    // Input validation
782    if a.len() != b.len() {
783        return Err(IntegrateError::ValueError(
784            "Dimension mismatch: 'a' and 'b' must have the same length".to_string(),
785        ));
786    }
787
788    let dim = a.len();
789
790    // Initialize QRNG if not provided
791    let qrng = qrng.unwrap_or_else(|| Box::new(Halton::new(dim, None)));
792
793    if qrng.dim() != dim {
794        return Err(IntegrateError::ValueError(format!(
795            "QRNG dimension ({}) does not match integration dimension ({})",
796            qrng.dim(),
797            dim
798        )));
799    }
800
801    // Check if a = b for any dimension, return 0 if so
802    for i in 0..dim {
803        if (a[i] - b[i]).abs() < f64::EPSILON {
804            return Ok(QMCQuadResult {
805                integral: if log { f64::NEG_INFINITY } else { 0.0 },
806                standard_error: 0.0,
807            });
808        }
809    }
810
811    // Swap limits if a > b and record the sign change
812    let mut a_mod = a.clone();
813    let mut b_mod = b.clone();
814    let mut sign = 1.0;
815
816    for i in 0..dim {
817        if a[i] > b[i] {
818            a_mod[i] = b[i];
819            b_mod[i] = a[i];
820            sign *= -1.0;
821        }
822    }
823
824    // Calculate domain volume
825    let mut volume = 1.0;
826    for i in 0..dim {
827        volume *= b_mod[i] - a_mod[i];
828    }
829
830    // Configure parallel execution based on workers parameter
831    #[cfg(feature = "parallel")]
832    {
833        if let Some(num_workers) = workers {
834            // Set thread pool size
835            use scirs2_core::parallel_ops::ThreadPoolBuilder;
836            let pool = ThreadPoolBuilder::new()
837                .num_threads(num_workers)
838                .build()
839                .map_err(|_| {
840                    IntegrateError::ValueError("Failed to create thread pool".to_string())
841                })?;
842
843            pool.install(|| {
844                parallel_qmc_integration_impl(
845                    func,
846                    &a_mod,
847                    &b_mod,
848                    n_estimates,
849                    n_points,
850                    &*qrng,
851                    log,
852                    volume,
853                    sign,
854                )
855            })
856        } else {
857            // Use default parallel execution
858            parallel_qmc_integration_impl(
859                func,
860                &a_mod,
861                &b_mod,
862                n_estimates,
863                n_points,
864                &*qrng,
865                log,
866                volume,
867                sign,
868            )
869        }
870    }
871
872    #[cfg(not(feature = "parallel"))]
873    {
874        // If parallel feature is not enabled, fall back to sequential execution
875        let _ = workers; // Silence unused variable warning
876        sequential_qmc_integration(
877            func,
878            &a_mod,
879            &b_mod,
880            n_estimates,
881            n_points,
882            qrng,
883            log,
884            volume,
885            sign,
886        )
887    }
888}
889
890#[cfg(feature = "parallel")]
891#[allow(dead_code)]
892fn parallel_qmc_integration_impl<F>(
893    func: F,
894    a: &Array1<f64>,
895    b: &Array1<f64>,
896    n_estimates: usize,
897    n_points: usize,
898    qrng: &dyn QRNGEngine,
899    log: bool,
900    volume: f64,
901    sign: f64,
902) -> IntegrateResult<QMCQuadResult<f64>>
903where
904    F: Fn(ArrayView1<f64>) -> f64 + Send + Sync,
905{
906    use scirs2_core::parallel_ops::*;
907
908    // Generate _estimates in parallel
909    let _estimates: Vec<f64> = (0..n_estimates)
910        .into_par_iter()
911        .map(|_| {
912            // Each thread gets its own QRNG instance with different seed
913            let mut local_qrng = qrng.new_from_seed(scirs2_core::random::random());
914
915            // Sample points
916            let points = local_qrng.random(n_points);
917
918            // Transform points to integration domain and evaluate function
919            let mut sum = 0.0;
920
921            for i in 0..n_points {
922                let point = points.row(i);
923
924                // Transform from [0,1]^d to [a,b]^d
925                let mut transformed_point = Array1::zeros(a.len());
926                for j in 0..a.len() {
927                    transformed_point[j] = a[j] + point[j] * (b[j] - a[j]);
928                }
929
930                let value = func(transformed_point.view());
931
932                if log {
933                    // For log-space integration, we accumulate log values
934                    if value > f64::NEG_INFINITY {
935                        sum += value.exp() / n_points as f64;
936                    }
937                } else {
938                    sum += value / n_points as f64;
939                }
940            }
941
942            if log {
943                sum.ln() + volume.ln()
944            } else {
945                sum * volume
946            }
947        })
948        .collect();
949
950    compute_qmc_result(_estimates, log, sign)
951}
952
953#[cfg(not(feature = "parallel"))]
954#[allow(dead_code)]
955fn sequential_qmc_integration<F>(
956    func: F,
957    a: &Array1<f64>,
958    b: &Array1<f64>,
959    n_estimates: usize,
960    n_points: usize,
961    mut qrng: Box<dyn QRNGEngine>,
962    log: bool,
963    volume: f64,
964    sign: f64,
965) -> IntegrateResult<QMCQuadResult<f64>>
966where
967    F: Fn(ArrayView1<f64>) -> f64,
968{
969    let mut estimates = Vec::with_capacity(n_estimates);
970
971    for _ in 0..n_estimates {
972        // Sample _points
973        let points = qrng.random(n_points);
974
975        // Transform _points to integration domain and evaluate function
976        let mut sum = 0.0;
977
978        for i in 0..n_points {
979            let point = points.row(i);
980
981            // Transform from [0,1]^d to [a,b]^d
982            let mut transformed_point = Array1::zeros(a.len());
983            for j in 0..a.len() {
984                transformed_point[j] = a[j] + point[j] * (b[j] - a[j]);
985            }
986
987            let value = func(transformed_point.view());
988
989            if log {
990                // For log-space integration, we accumulate log values
991                if value > f64::NEG_INFINITY {
992                    sum += value.exp() / n_points as f64;
993                }
994            } else {
995                sum += value / n_points as f64;
996            }
997        }
998
999        let estimate = if log {
1000            sum.ln() + volume.ln()
1001        } else {
1002            sum * volume
1003        };
1004
1005        estimates.push(estimate);
1006    }
1007
1008    compute_qmc_result(estimates, log, sign)
1009}
1010
1011#[allow(dead_code)]
1012fn compute_qmc_result(
1013    estimates: Vec<f64>,
1014    log: bool,
1015    sign: f64,
1016) -> IntegrateResult<QMCQuadResult<f64>> {
1017    let n_estimates = estimates.len();
1018
1019    // Compute mean and variance
1020    let integral = if log {
1021        // For log-space, use log-sum-exp for numerical stability
1022        let max_val = estimates.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
1023        if max_val == f64::NEG_INFINITY {
1024            f64::NEG_INFINITY
1025        } else {
1026            let sum_exp: f64 = estimates.iter().map(|&x| (x - max_val).exp()).sum();
1027            max_val + (sum_exp / n_estimates as f64).ln()
1028        }
1029    } else {
1030        estimates.iter().sum::<f64>() / n_estimates as f64
1031    };
1032
1033    let standard_error = if estimates.len() > 1 {
1034        if log {
1035            // For log-space, compute variance in log domain
1036            let mean = integral;
1037            let mut variance = 0.0;
1038
1039            for estimate in estimates.iter().take(n_estimates) {
1040                let diff = estimate - mean;
1041                variance += diff * diff;
1042            }
1043
1044            variance /= (n_estimates - 1) as f64;
1045            (variance / (n_estimates as f64)).sqrt().ln()
1046        } else {
1047            let mean = integral;
1048            let mut variance = 0.0;
1049
1050            for estimate in estimates.iter().take(n_estimates) {
1051                variance += (estimate - mean).powi(2);
1052            }
1053
1054            variance /= (n_estimates - 1) as f64;
1055            (variance / (n_estimates as f64)).sqrt()
1056        }
1057    } else if log {
1058        f64::NEG_INFINITY
1059    } else {
1060        0.0
1061    };
1062
1063    // Apply sign correction for reversed limits
1064    let final_integral = if log && sign < 0.0 {
1065        // For negative results in log space, we'd need complex numbers
1066        // Since we don't support complex results, we'll just negate the result
1067        -integral
1068    } else {
1069        integral * sign
1070    };
1071
1072    Ok(QMCQuadResult {
1073        integral: final_integral,
1074        standard_error,
1075    })
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080    use super::*;
1081    use crate::{qmc_quad, qmc_quad_parallel, Faure};
1082    use approx::assert_abs_diff_eq;
1083    use scirs2_core::ndarray::{s, Array1, ArrayView1};
1084
1085    #[test]
1086    fn test_qmc_integral_1d() {
1087        // Test integral of x^2 from 0 to 1 (should be 1/3)
1088        let f = |x: ArrayView1<f64>| x[0].powi(2);
1089        let a = Array1::from_vec(vec![0.0]);
1090        let b = Array1::from_vec(vec![1.0]);
1091
1092        let result = qmc_quad(f, &a, &b, Some(8), Some(1000), None, false).unwrap();
1093
1094        assert_abs_diff_eq!(result.integral, 1.0 / 3.0, epsilon = 0.01);
1095    }
1096
1097    #[test]
1098    fn test_qmc_integral_2d() {
1099        // Test integral of x*y from 0 to 1, 0 to 1 (should be 1/4)
1100        let f = |x: ArrayView1<f64>| x[0] * x[1];
1101        let a = Array1::from_vec(vec![0.0, 0.0]);
1102        let b = Array1::from_vec(vec![1.0, 1.0]);
1103
1104        let result = qmc_quad(f, &a, &b, Some(8), Some(1000), None, false).unwrap();
1105
1106        assert_abs_diff_eq!(result.integral, 0.25, epsilon = 0.01);
1107    }
1108
1109    #[test]
1110    fn test_infinite_limits() {
1111        // Test integral of e^(-x^2) from -∞ to ∞ (should be sqrt(π) ≈ 1.77)
1112        let f = |x: ArrayView1<f64>| (-x[0].powi(2)).exp();
1113        let a = Array1::from_vec(vec![-5.0]); // approximating infinity with a large value
1114        let b = Array1::from_vec(vec![5.0]);
1115
1116        let result = qmc_quad(f, &a, &b, Some(8), Some(1000), None, false).unwrap();
1117
1118        assert_abs_diff_eq!(result.integral, std::f64::consts::PI.sqrt(), epsilon = 0.05);
1119    }
1120
1121    #[test]
1122    fn test_faure_sequence() {
1123        // Test basic Faure sequence properties
1124        let mut faure = Faure::new(2, Some(42));
1125
1126        // Generate some points
1127        let points = faure.random(10);
1128
1129        // Check dimensions
1130        assert_eq!(points.shape()[0], 10);
1131        assert_eq!(points.shape()[1], 2);
1132
1133        // Check that all points are in [0, 1)
1134        for i in 0..10 {
1135            for j in 0..2 {
1136                assert!(points[[i, j]] >= 0.0 && points[[i, j]] < 1.0);
1137            }
1138        }
1139
1140        // Test reproducibility with same seed
1141        let mut faure2 = Faure::new(2, Some(42));
1142        let points2 = faure2.random(5);
1143        let points_first_5 = points.slice(s![0..5, ..]);
1144
1145        for i in 0..5 {
1146            for j in 0..2 {
1147                assert_abs_diff_eq!(points_first_5[[i, j]], points2[[i, j]], epsilon = 1e-10);
1148            }
1149        }
1150    }
1151
1152    #[test]
1153    fn test_faure_integration() {
1154        // Test integral using Faure sequence
1155        let f = |x: ArrayView1<f64>| x[0] * x[1];
1156        let a = Array1::from_vec(vec![0.0, 0.0]);
1157        let b = Array1::from_vec(vec![1.0, 1.0]);
1158
1159        let faure = Faure::new(2, Some(42));
1160        let result =
1161            qmc_quad(f, &a, &b, Some(8), Some(1000), Some(Box::new(faure)), false).unwrap();
1162
1163        // Expected result is 1/4 = 0.25
1164        // Note: This simplified Faure implementation may not achieve optimal convergence
1165        // Production implementations would use proper Pascal triangle modulo p matrices
1166        assert_abs_diff_eq!(result.integral, 0.25, epsilon = 0.2);
1167    }
1168
1169    #[test]
1170    fn test_qmc_quad_parallel_workers() {
1171        // Test QMC integration with workers parameter
1172        let f = |x: ArrayView1<f64>| x[0].powi(2) + x[1].powi(2);
1173        let a = Array1::from_vec(vec![0.0, 0.0]);
1174        let b = Array1::from_vec(vec![1.0, 1.0]);
1175
1176        // Test with 2 workers
1177        let result =
1178            qmc_quad_parallel(f, &a, &b, Some(8), Some(1000), None, false, Some(2)).unwrap();
1179
1180        // Expected integral of x^2 + y^2 over [0,1]×[0,1] is 2/3
1181        assert_abs_diff_eq!(result.integral, 2.0 / 3.0, epsilon = 0.1);
1182        assert!(result.standard_error >= 0.0);
1183    }
1184}