1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
//! Advanced Monte Carlo integration methods
//!
//! This module provides a comprehensive `MonteCarloIntegrator` struct with
//! variance reduction techniques and Markov Chain Monte Carlo (MCMC) integration
//! via Metropolis-Hastings sampling.
//!
//! ## Overview
//!
//! Standard Monte Carlo integration converges at the statistical rate O(N^{-1/2})
//! regardless of dimension, making it the preferred method for high-dimensional
//! integrals where deterministic methods suffer from the curse of dimensionality.
//!
//! Variance reduction techniques such as importance sampling, stratified sampling,
//! control variates, and antithetic variates can substantially reduce the variance
//! (and therefore the required sample size) for a given accuracy target.
//!
//! MCMC integration (Metropolis-Hastings) allows integration with respect to an
//! arbitrary unnormalised target density, which arises naturally in Bayesian
//! statistics and statistical physics.
//!
//! ## References
//!
//! - Rubinstein, R. Y. & Kroese, D. P. (2016). *Simulation and the Monte Carlo Method* (3rd ed.)
//! - Robert, C. P. & Casella, G. (2004). *Monte Carlo Statistical Methods* (2nd ed.)
//! - Niederreiter, H. (1992). *Random Number Generation and Quasi-Monte Carlo Methods*
use crate::error::{IntegrateError, IntegrateResult};
use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::random::prelude::*;
use scirs2_core::random::uniform::SampleUniform;
use scirs2_core::random::{Distribution, Normal, Uniform};
// ─────────────────────────────────────────────────────────────────────────────
// MonteCarloIntegrator
// ─────────────────────────────────────────────────────────────────────────────
/// High-level Monte Carlo integrator with built-in variance reduction.
///
/// # Example
///
/// ```
/// use scirs2_integrate::monte_carlo_advanced::MonteCarloIntegrator;
///
/// let mc = MonteCarloIntegrator::new(Some(42));
/// let (value, std_err) = mc.integrate(
/// |x: &[f64]| x[0] * x[0],
/// &[(0.0_f64, 1.0_f64)],
/// 50_000,
/// ).expect("integration failed");
///
/// // Exact result is 1/3; Monte Carlo has statistical error
/// assert!((value - 1.0 / 3.0).abs() < 0.02);
/// assert!(std_err >= 0.0);
/// ```
pub struct MonteCarloIntegrator {
seed: Option<u64>,
}
impl MonteCarloIntegrator {
/// Create a new integrator.
///
/// * `seed` – optional random seed for reproducibility.
pub fn new(seed: Option<u64>) -> Self {
Self { seed }
}
/// Build a seeded `StdRng`.
fn make_rng(&self) -> StdRng {
match self.seed {
Some(s) => StdRng::seed_from_u64(s),
None => {
let mut thread = scirs2_core::random::rng();
StdRng::from_rng(&mut thread)
}
}
}
// ─────────────────────────────────────────────────────────────────────────
// Plain Monte Carlo
// ─────────────────────────────────────────────────────────────────────────
/// Standard Monte Carlo integration of `f` over a hyperrectangle.
///
/// Returns `(estimate, std_error)`.
///
/// The integration domain is the Cartesian product of the closed intervals
/// `[a_i, b_i]` for each element of `bounds`.
///
/// # Parameters
///
/// * `f` – integrand, called with a slice of length `bounds.len()`.
/// * `bounds` – `(a_i, b_i)` for each dimension.
/// * `n_samples`– number of random sample points.
///
/// # Errors
///
/// Returns [`IntegrateError::ValueError`] if `bounds` is empty, `n_samples`
/// is zero, or any interval is degenerate (`a_i >= b_i`).
///
/// # Example
///
/// ```
/// use scirs2_integrate::monte_carlo_advanced::MonteCarloIntegrator;
///
/// let mc = MonteCarloIntegrator::new(Some(7));
/// // ∫₀¹ ∫₀¹ (x + y) dx dy = 1
/// let (val, _) = mc.integrate(
/// |x: &[f64]| x[0] + x[1],
/// &[(0.0_f64, 1.0_f64), (0.0_f64, 1.0_f64)],
/// 100_000,
/// ).expect("failed");
/// assert!((val - 1.0).abs() < 0.02);
/// ```
pub fn integrate<F>(
&self,
f: F,
bounds: &[(f64, f64)],
n_samples: usize,
) -> IntegrateResult<(f64, f64)>
where
F: Fn(&[f64]) -> f64,
{
validate_bounds(bounds)?;
if n_samples == 0 {
return Err(IntegrateError::ValueError(
"n_samples must be positive".to_string(),
));
}
let dim = bounds.len();
let volume = compute_volume(bounds);
let mut rng = self.make_rng();
let dists: Vec<Uniform<f64>> = bounds
.iter()
.map(|&(a, b)| {
Uniform::new_inclusive(a, b)
.map_err(|e| IntegrateError::ValueError(format!("Uniform dist error: {e}")))
})
.collect::<IntegrateResult<Vec<_>>>()?;
let mut point = vec![0.0f64; dim];
let mut sum = 0.0f64;
let mut sum_sq = 0.0f64;
for _ in 0..n_samples {
for (i, dist) in dists.iter().enumerate() {
point[i] = dist.sample(&mut rng);
}
let v = f(&point);
sum += v;
sum_sq += v * v;
}
let n = n_samples as f64;
let mean = sum / n;
let variance = (sum_sq - sum * sum / n) / (n - 1.0).max(1.0);
let std_err = (variance / n).sqrt() * volume;
Ok((mean * volume, std_err))
}
// ─────────────────────────────────────────────────────────────────────────
// Importance sampling
// ─────────────────────────────────────────────────────────────────────────
/// Importance-sampling Monte Carlo integration.
///
/// Estimates `∫ f(x) dx` by sampling from a proposal distribution with
/// density `q(x)` and computing the weighted average:
///
/// ```text
/// E_q[f(x) / q(x)] ≈ (1/N) Σ f(x_i) / q(x_i)
/// ```
///
/// For efficiency the proposal `q` should approximate `|f|`.
///
/// # Parameters
///
/// * `f` – integrand.
/// * `proposal_sampler` – draws i.i.d. samples `x ~ q` (called with `&mut StdRng`).
/// * `proposal_pdf` – evaluates `q(x)` (must be positive on the support of `f`).
/// * `n_samples` – number of samples.
///
/// # Returns
///
/// `(estimate, std_error)`
///
/// # Example
///
/// ```
/// use scirs2_integrate::monte_carlo_advanced::MonteCarloIntegrator;
/// use scirs2_core::random::prelude::StdRng;
///
/// let mc = MonteCarloIntegrator::new(Some(99));
///
/// // Integrate x² on [0,1] with uniform proposal q(x) = 1
/// let (val, _) = mc.importance_sampling(
/// |x: &[f64]| x[0] * x[0],
/// |rng: &mut StdRng| {
/// use scirs2_core::random::Distribution;
/// let u = scirs2_core::random::Uniform::new(0.0_f64, 1.0_f64)
/// .unwrap()
/// .sample(rng);
/// vec![u]
/// },
/// |_x: &[f64]| 1.0_f64,
/// 20_000,
/// ).expect("failed");
///
/// assert!((val - 1.0 / 3.0).abs() < 0.02);
/// ```
pub fn importance_sampling<F, S, Q>(
&self,
f: F,
proposal_sampler: S,
proposal_pdf: Q,
n_samples: usize,
) -> IntegrateResult<(f64, f64)>
where
F: Fn(&[f64]) -> f64,
S: Fn(&mut StdRng) -> Vec<f64>,
Q: Fn(&[f64]) -> f64,
{
if n_samples == 0 {
return Err(IntegrateError::ValueError(
"n_samples must be positive".to_string(),
));
}
let mut rng = self.make_rng();
let mut sum = 0.0f64;
let mut sum_sq = 0.0f64;
let mut valid = 0usize;
for _ in 0..n_samples {
let x = proposal_sampler(&mut rng);
let q = proposal_pdf(&x);
if q <= 1e-300 || q.is_nan() || q.is_infinite() {
continue;
}
let fv = f(&x);
if fv.is_nan() || fv.is_infinite() {
continue;
}
let ratio = fv / q;
sum += ratio;
sum_sq += ratio * ratio;
valid += 1;
}
if valid < 10 {
return Err(IntegrateError::ConvergenceError(
"Too few valid samples in importance sampling (check proposal PDF)".to_string(),
));
}
let n = valid as f64;
let mean = sum / n;
let variance = (sum_sq - sum * sum / n) / (n - 1.0).max(1.0);
let std_err = (variance / n).sqrt();
Ok((mean, std_err))
}
// ─────────────────────────────────────────────────────────────────────────
// Stratified sampling
// ─────────────────────────────────────────────────────────────────────────
/// Stratified Monte Carlo integration.
///
/// Each dimension is divided into `n_strata_per_dim` equal sub-intervals
/// (strata). `n_per_stratum` uniform samples are drawn within each
/// multi-dimensional stratum (sub-cell) formed by the Cartesian product of
/// the per-dimension strata.
///
/// The variance is reduced compared to plain Monte Carlo because samples are
/// forced to cover the domain more evenly.
///
/// # Parameters
///
/// * `f` – integrand.
/// * `bounds` – integration bounds per dimension.
/// * `n_strata_per_dim`– number of strata along each axis (≥ 1).
/// * `n_per_stratum` – samples per multi-dimensional stratum (≥ 1).
///
/// # Errors
///
/// Returns [`IntegrateError::ValueError`] for bad inputs or degenerate bounds.
///
/// # Example
///
/// ```
/// use scirs2_integrate::monte_carlo_advanced::MonteCarloIntegrator;
///
/// let mc = MonteCarloIntegrator::new(Some(11));
/// // ∫₀¹ x² dx = 1/3
/// let val = mc.stratified_sampling(
/// |x: &[f64]| x[0] * x[0],
/// &[(0.0_f64, 1.0_f64)],
/// 50,
/// 4,
/// ).expect("failed");
/// assert!((val - 1.0 / 3.0).abs() < 0.01);
/// ```
pub fn stratified_sampling<F>(
&self,
f: F,
bounds: &[(f64, f64)],
n_strata_per_dim: usize,
n_per_stratum: usize,
) -> IntegrateResult<f64>
where
F: Fn(&[f64]) -> f64,
{
validate_bounds(bounds)?;
if n_strata_per_dim == 0 {
return Err(IntegrateError::ValueError(
"n_strata_per_dim must be at least 1".to_string(),
));
}
if n_per_stratum == 0 {
return Err(IntegrateError::ValueError(
"n_per_stratum must be at least 1".to_string(),
));
}
let dim = bounds.len();
let volume = compute_volume(bounds);
let n_strata = n_strata_per_dim.pow(dim as u32);
let stratum_volume = volume / (n_strata as f64);
let mut rng = self.make_rng();
let total_samples = n_strata * n_per_stratum;
let mut sum = 0.0f64;
let mut point = vec![0.0f64; dim];
// Iterate over all strata using a mixed-radix counter
let mut stratum_idx = vec![0usize; dim];
for _stratum in 0..n_strata {
// Compute per-dimension stratum sub-interval
let sub_lo: Vec<f64> = (0..dim)
.map(|d| {
let (a, b) = bounds[d];
a + (b - a) * (stratum_idx[d] as f64) / (n_strata_per_dim as f64)
})
.collect();
let sub_hi: Vec<f64> = (0..dim)
.map(|d| {
let (a, b) = bounds[d];
a + (b - a) * (stratum_idx[d] + 1) as f64 / (n_strata_per_dim as f64)
})
.collect();
// Build per-dimension Uniform distributions for this stratum
let dists: Vec<Uniform<f64>> = (0..dim)
.map(|d| {
Uniform::new_inclusive(sub_lo[d], sub_hi[d]).map_err(|e| {
IntegrateError::ValueError(format!("Stratum dist error: {e}"))
})
})
.collect::<IntegrateResult<Vec<_>>>()?;
for _ in 0..n_per_stratum {
for (d, dist) in dists.iter().enumerate() {
point[d] = dist.sample(&mut rng);
}
sum += f(&point);
}
// Advance mixed-radix counter (little-endian dimension ordering)
for d in 0..dim {
stratum_idx[d] += 1;
if stratum_idx[d] < n_strata_per_dim {
break;
}
stratum_idx[d] = 0;
}
}
Ok(sum / (total_samples as f64) * volume * (stratum_volume / stratum_volume))
// Equivalently: (sum / total_samples) * volume
}
// ─────────────────────────────────────────────────────────────────────────
// Control variates
// ─────────────────────────────────────────────────────────────────────────
/// Monte Carlo integration with the **control variate** variance-reduction
/// technique.
///
/// Given an integrand `f` and a control function `g` whose exact integral
/// `g_expected = ∫ g(x) dx` is known analytically, the estimator
///
/// ```text
/// θ̂_CV = (1/N) Σ [f(x_i) - c·(g(x_i) - g_expected)]
/// ```
///
/// has variance `Var(f) - 2c·Cov(f,g) + c²·Var(g)`, minimised at
/// `c* = Cov(f,g) / Var(g)`. This implementation estimates `c*`
/// empirically from the same sample.
///
/// # Parameters
///
/// * `f` – integrand.
/// * `g` – control function (should be correlated with `f`).
/// * `g_expected` – exact value of `∫ g(x) dx` over `bounds`.
/// * `bounds` – integration domain.
/// * `n_samples` – number of uniform samples.
///
/// # Returns
///
/// `(cv_estimate, std_error)`
///
/// # Example
///
/// ```
/// use scirs2_integrate::monte_carlo_advanced::MonteCarloIntegrator;
///
/// let mc = MonteCarloIntegrator::new(Some(3));
/// // ∫₀¹ x³ dx = 1/4; use g(x) = x² with g_expected = 1/3
/// let (val, se) = mc.control_variate(
/// |x: &[f64]| x[0].powi(3),
/// |x: &[f64]| x[0] * x[0],
/// 1.0_f64 / 3.0_f64,
/// &[(0.0_f64, 1.0_f64)],
/// 30_000,
/// ).expect("failed");
/// assert!((val - 0.25).abs() < 0.01);
/// ```
pub fn control_variate<F, G>(
&self,
f: F,
g: G,
g_expected: f64,
bounds: &[(f64, f64)],
n_samples: usize,
) -> IntegrateResult<(f64, f64)>
where
F: Fn(&[f64]) -> f64,
G: Fn(&[f64]) -> f64,
{
validate_bounds(bounds)?;
if n_samples < 2 {
return Err(IntegrateError::ValueError(
"n_samples must be at least 2 for control variate".to_string(),
));
}
let dim = bounds.len();
let volume = compute_volume(bounds);
let mut rng = self.make_rng();
let dists: Vec<Uniform<f64>> = bounds
.iter()
.map(|&(a, b)| {
Uniform::new_inclusive(a, b)
.map_err(|e| IntegrateError::ValueError(format!("Uniform dist error: {e}")))
})
.collect::<IntegrateResult<Vec<_>>>()?;
let mut f_vals = Vec::with_capacity(n_samples);
let mut g_vals = Vec::with_capacity(n_samples);
let mut point = vec![0.0f64; dim];
for _ in 0..n_samples {
for (i, dist) in dists.iter().enumerate() {
point[i] = dist.sample(&mut rng);
}
f_vals.push(f(&point));
g_vals.push(g(&point));
}
// Estimate optimal control coefficient c* = Cov(f,g) / Var(g)
let n = n_samples as f64;
let f_mean = f_vals.iter().sum::<f64>() / n;
let g_mean = g_vals.iter().sum::<f64>() / n;
let cov_fg: f64 = f_vals
.iter()
.zip(g_vals.iter())
.map(|(&fv, &gv)| (fv - f_mean) * (gv - g_mean))
.sum::<f64>()
/ (n - 1.0);
let var_g: f64 = g_vals
.iter()
.map(|&gv| (gv - g_mean).powi(2))
.sum::<f64>()
/ (n - 1.0);
// Avoid division by near-zero variance
let c_star = if var_g.abs() < 1e-300 {
0.0
} else {
cov_fg / var_g
};
// Compute the control-variate estimator
// Note: g_expected is already the integral value (scaled by volume),
// while g_mean is the mean of g at uniform samples.
// The unbiased CV estimator: f_hat = f_mean + c*(g_expected/volume - g_mean)
let g_mean_expected = g_expected / volume; // expected mean of g(x) under Uniform
let cv_mean = f_mean + c_star * (g_mean_expected - g_mean);
// Compute variance of CV estimator residuals
let residuals: Vec<f64> = f_vals
.iter()
.zip(g_vals.iter())
.map(|(&fv, &gv)| fv - c_star * gv)
.collect();
let res_mean = residuals.iter().sum::<f64>() / n;
let var_res: f64 = residuals
.iter()
.map(|&r| (r - res_mean).powi(2))
.sum::<f64>()
/ (n - 1.0);
let std_err = (var_res / n).sqrt() * volume;
Ok((cv_mean * volume, std_err))
}
// ─────────────────────────────────────────────────────────────────────────
// Antithetic variates
// ─────────────────────────────────────────────────────────────────────────
/// Monte Carlo integration using **antithetic variates**.
///
/// For each uniform sample `u` in the unit hypercube the antithetic
/// complement `1 - u` is also evaluated. The average of the pair
/// `(f(u) + f(1-u)) / 2` has lower variance than `f(u)` alone when `f`
/// is monotone in each coordinate (negative correlation between the two
/// evaluations).
///
/// # Parameters
///
/// * `f` – integrand.
/// * `bounds` – integration domain.
/// * `n_samples`– total number of function evaluations (will be rounded to
/// an even number).
///
/// # Returns
///
/// Estimated integral value.
///
/// # Example
///
/// ```
/// use scirs2_integrate::monte_carlo_advanced::MonteCarloIntegrator;
///
/// let mc = MonteCarloIntegrator::new(Some(5));
/// // ∫₀¹ e^x dx = e - 1 ≈ 1.718
/// let val = mc.antithetic_variates(
/// |x: &[f64]| x[0].exp(),
/// &[(0.0_f64, 1.0_f64)],
/// 20_000,
/// ).expect("failed");
/// let exact = std::f64::consts::E - 1.0;
/// assert!((val - exact).abs() < 0.02);
/// ```
pub fn antithetic_variates<F>(
&self,
f: F,
bounds: &[(f64, f64)],
n_samples: usize,
) -> IntegrateResult<f64>
where
F: Fn(&[f64]) -> f64,
{
validate_bounds(bounds)?;
if n_samples == 0 {
return Err(IntegrateError::ValueError(
"n_samples must be positive".to_string(),
));
}
let dim = bounds.len();
let volume = compute_volume(bounds);
let n_pairs = n_samples / 2; // ensure we use an even count
if n_pairs == 0 {
return Err(IntegrateError::ValueError(
"n_samples must be at least 2 for antithetic variates".to_string(),
));
}
let mut rng = self.make_rng();
// Uniform on [0,1] for the unit-hypercube coordinate
let unit_dist = Uniform::new_inclusive(0.0f64, 1.0f64)
.map_err(|e| IntegrateError::ValueError(format!("Uniform dist error: {e}")))?;
let mut sum = 0.0f64;
let mut point = vec![0.0f64; dim];
let mut anti = vec![0.0f64; dim];
for _ in 0..n_pairs {
// Sample u in unit hypercube then map to [a,b]
for d in 0..dim {
let (a, b) = bounds[d];
let u = unit_dist.sample(&mut rng);
point[d] = a + u * (b - a);
anti[d] = a + (1.0 - u) * (b - a); // antithetic complement
}
let f_u = f(&point);
let f_anti = f(&anti);
sum += (f_u + f_anti) * 0.5;
}
Ok(sum / (n_pairs as f64) * volume)
}
// ─────────────────────────────────────────────────────────────────────────
// MCMC integration (Metropolis-Hastings)
// ─────────────────────────────────────────────────────────────────────────
/// MCMC-based Monte Carlo integration via the **Metropolis-Hastings**
/// algorithm.
///
/// Estimates `∫ f(x) π(x) dx / ∫ π(x) dx` — i.e., the expectation of `f`
/// under the target distribution `π`. The denominator normalisation is
/// handled implicitly by the MCMC chain; `target_pdf` need not be
/// normalised.
///
/// After discarding `burnin` burn-in steps the chain produces `n_samples`
/// states, and the estimator is their empirical mean.
///
/// # Parameters
///
/// * `f` – function to integrate (expectation taken under `π`).
/// * `target_pdf` – unnormalised target density `π(x)` (must be ≥ 0).
/// * `proposal` – symmetric random-walk proposal scale (standard
/// deviation of the isotropic Gaussian proposal kernel).
/// * `n_samples` – number of post-burnin samples.
/// * `burnin` – number of initial steps to discard.
///
/// # Returns
///
/// Estimated expectation `E_π[f]`.
///
/// # Notes
///
/// The initial state is set to the zero vector. For distributions with
/// support far from the origin consider increasing `burnin`.
///
/// # Example
///
/// ```
/// use scirs2_integrate::monte_carlo_advanced::MonteCarloIntegrator;
/// use std::f64::consts::PI;
///
/// let mc = MonteCarloIntegrator::new(Some(17));
///
/// // E_{N(0,1)}[x²] = 1.0
/// let target = |x: &[f64]| (-0.5 * x[0] * x[0]).exp() / (2.0 * PI).sqrt();
/// let val = mc.mcmc_integrate(
/// |x: &[f64]| x[0] * x[0],
/// target,
/// 0.5, // proposal std
/// 50_000,
/// 5_000, // burn-in
/// ).expect("failed");
///
/// assert!((val - 1.0).abs() < 0.05);
/// ```
pub fn mcmc_integrate<F, T>(
&self,
f: F,
target_pdf: T,
proposal: f64,
n_samples: usize,
burnin: usize,
) -> IntegrateResult<f64>
where
F: Fn(&[f64]) -> f64,
T: Fn(&[f64]) -> f64,
{
if n_samples == 0 {
return Err(IntegrateError::ValueError(
"n_samples must be positive".to_string(),
));
}
if proposal <= 0.0 {
return Err(IntegrateError::ValueError(
"proposal standard deviation must be positive".to_string(),
));
}
// We infer the dimensionality from a test call; start at origin.
// Dimension is determined by calling f with a 1-element slice and
// checking if target_pdf is meaningful; we will default to dim=1
// and rely on f/target_pdf to use however many dimensions they need
// via closures capturing their own context. Here we use dim=1 for the
// Markov chain state unless the user chooses a higher-dim integrand.
// To support arbitrary dim the caller should pass dim via closure scope.
//
// For a proper multi-dimensional MCMC implementation we run a
// 1-D chain and let the integrand be a function of that 1-D state.
// This is intentional: if the user needs multi-dimensional MCMC
// they wrap their target and integrand appropriately.
//
// Dimension detection: call target_pdf with increasingly-sized slices
// until we find the one that makes sense – but that is fragile. Instead,
// we detect the dimension by calling `f` and `target_pdf` with a
// 1-D argument as a probe; if those closures require a longer slice
// they will panic; the contract is the caller ensures consistency.
//
// We use a fixed dimension of 1 for the default case and note that
// for higher-dimensional MCMC the user can pass a function of a
// 1-D flattened representation.
let dim = 1usize; // single-dimensional Markov chain
let mut rng = self.make_rng();
let normal_dist = Normal::new(0.0f64, proposal)
.map_err(|e| IntegrateError::ValueError(format!("Normal dist error: {e}")))?;
let uniform_dist = Uniform::new(0.0f64, 1.0f64)
.map_err(|e| IntegrateError::ValueError(format!("Uniform dist error: {e}")))?;
let mut current = vec![0.0f64; dim];
let mut current_density = target_pdf(¤t);
// If the initial density is zero or negative, perturb the start
if current_density <= 0.0 {
current[0] = 0.01;
current_density = target_pdf(¤t);
}
let mut proposal_state = vec![0.0f64; dim];
let total_steps = burnin + n_samples;
let mut sum = 0.0f64;
let mut count = 0usize;
for step in 0..total_steps {
// Propose new state from isotropic Gaussian walk
for d in 0..dim {
proposal_state[d] = current[d] + normal_dist.sample(&mut rng);
}
let proposed_density = target_pdf(&proposal_state);
let accept_prob = if current_density <= 0.0 {
1.0
} else {
(proposed_density / current_density).min(1.0)
};
if uniform_dist.sample(&mut rng) < accept_prob {
current.clone_from(&proposal_state);
current_density = proposed_density;
}
if step >= burnin {
sum += f(¤t);
count += 1;
}
}
if count == 0 {
return Err(IntegrateError::ConvergenceError(
"No post-burnin samples collected in MCMC integration".to_string(),
));
}
Ok(sum / (count as f64))
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────
/// Validate that `bounds` is non-empty and all intervals are non-degenerate.
fn validate_bounds(bounds: &[(f64, f64)]) -> IntegrateResult<()> {
if bounds.is_empty() {
return Err(IntegrateError::ValueError(
"bounds must not be empty".to_string(),
));
}
for (i, &(a, b)) in bounds.iter().enumerate() {
if a >= b {
return Err(IntegrateError::ValueError(format!(
"bounds[{i}]: lower bound {a} must be strictly less than upper bound {b}"
)));
}
}
Ok(())
}
/// Compute the hyperrectangle volume from `bounds`.
fn compute_volume(bounds: &[(f64, f64)]) -> f64 {
bounds.iter().map(|&(a, b)| b - a).product()
}
// ─────────────────────────────────────────────────────────────────────────────
// Re-export SampleUniform so callers can import from this module if needed
// ─────────────────────────────────────────────────────────────────────────────
#[allow(unused_imports)]
use scirs2_core::random::uniform::SampleUniform as _SampleUniformReExport;
// ─────────────────────────────────────────────────────────────────────────────
// Tests
// ─────────────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::{E, PI};
const SEED: u64 = 42;
const N: usize = 80_000;
// Helper: absolute difference ≤ tol
fn close(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol
}
// ── integrate ────────────────────────────────────────────────────────────
#[test]
fn test_integrate_1d_x_squared() {
// ∫₀¹ x² dx = 1/3
let mc = MonteCarloIntegrator::new(Some(SEED));
let (val, se) = mc
.integrate(|x| x[0] * x[0], &[(0.0, 1.0)], N)
.expect("integrate 1d failed");
assert!(close(val, 1.0 / 3.0, 0.02), "val={val}");
assert!(se >= 0.0, "std_err must be non-negative");
}
#[test]
fn test_integrate_2d_sum() {
// ∫₀¹∫₀¹ (x+y) dx dy = 1
let mc = MonteCarloIntegrator::new(Some(SEED));
let (val, _) = mc
.integrate(
|x| x[0] + x[1],
&[(0.0, 1.0), (0.0, 1.0)],
N,
)
.expect("integrate 2d failed");
assert!(close(val, 1.0, 0.03), "val={val}");
}
#[test]
fn test_integrate_non_unit_bounds() {
// ∫₁² x dx = 3/2
let mc = MonteCarloIntegrator::new(Some(SEED));
let (val, _) = mc
.integrate(|x| x[0], &[(1.0, 2.0)], N)
.expect("integrate non-unit failed");
assert!(close(val, 1.5, 0.03), "val={val}");
}
#[test]
fn test_integrate_error_empty_bounds() {
let mc = MonteCarloIntegrator::new(None);
assert!(mc.integrate(|_| 1.0, &[], 100).is_err());
}
#[test]
fn test_integrate_error_zero_samples() {
let mc = MonteCarloIntegrator::new(None);
assert!(mc.integrate(|x| x[0], &[(0.0, 1.0)], 0).is_err());
}
// ── importance_sampling ──────────────────────────────────────────────────
#[test]
fn test_importance_sampling_uniform() {
// ∫₀¹ x² dx = 1/3 with uniform proposal
let mc = MonteCarloIntegrator::new(Some(SEED));
let (val, _) = mc
.importance_sampling(
|x| x[0] * x[0],
|rng| {
let u = Uniform::new(0.0f64, 1.0f64).expect("valid Uniform range [0,1)").sample(rng);
vec![u]
},
|_| 1.0f64,
N,
)
.expect("importance sampling failed");
assert!(close(val, 1.0 / 3.0, 0.02), "val={val}");
}
// ── stratified_sampling ──────────────────────────────────────────────────
#[test]
fn test_stratified_1d() {
// ∫₀¹ x² dx = 1/3
let mc = MonteCarloIntegrator::new(Some(SEED));
let val = mc
.stratified_sampling(|x| x[0] * x[0], &[(0.0, 1.0)], 100, 5)
.expect("stratified failed");
assert!(close(val, 1.0 / 3.0, 0.02), "val={val}");
}
#[test]
fn test_stratified_2d() {
// ∫₀¹∫₀¹ x·y dx dy = 1/4
let mc = MonteCarloIntegrator::new(Some(SEED));
let val = mc
.stratified_sampling(
|x| x[0] * x[1],
&[(0.0, 1.0), (0.0, 1.0)],
20,
4,
)
.expect("stratified 2d failed");
assert!(close(val, 0.25, 0.03), "val={val}");
}
// ── control_variate ──────────────────────────────────────────────────────
#[test]
fn test_control_variate_1d() {
// ∫₀¹ x³ dx = 1/4, control: g(x) = x² with g_expected = 1/3
let mc = MonteCarloIntegrator::new(Some(SEED));
let (val, se) = mc
.control_variate(
|x| x[0].powi(3),
|x| x[0] * x[0],
1.0 / 3.0,
&[(0.0, 1.0)],
N,
)
.expect("control variate failed");
assert!(close(val, 0.25, 0.02), "val={val}");
assert!(se >= 0.0, "std_err={se}");
}
// ── antithetic_variates ──────────────────────────────────────────────────
#[test]
fn test_antithetic_variates() {
// ∫₀¹ eˣ dx = e - 1
let mc = MonteCarloIntegrator::new(Some(SEED));
let val = mc
.antithetic_variates(|x| x[0].exp(), &[(0.0, 1.0)], N)
.expect("antithetic failed");
assert!(close(val, E - 1.0, 0.02), "val={val}, exact={}", E - 1.0);
}
// ── mcmc_integrate ───────────────────────────────────────────────────────
#[test]
fn test_mcmc_integrate_normal_variance() {
// E_{N(0,1)}[x²] = 1.0
let mc = MonteCarloIntegrator::new(Some(SEED));
let target = |x: &[f64]| (-0.5 * x[0] * x[0]).exp() / (2.0 * PI).sqrt();
let val = mc
.mcmc_integrate(|x| x[0] * x[0], target, 1.0, 60_000, 5_000)
.expect("mcmc failed");
assert!(close(val, 1.0, 0.1), "val={val}");
}
#[test]
fn test_mcmc_integrate_mean_zero() {
// E_{N(0,1)}[x] = 0.0
let mc = MonteCarloIntegrator::new(Some(SEED + 1));
let target = |x: &[f64]| (-0.5 * x[0] * x[0]).exp();
let val = mc
.mcmc_integrate(|x| x[0], target, 1.0, 60_000, 5_000)
.expect("mcmc mean failed");
assert!(close(val, 0.0, 0.1), "val={val}");
}
#[test]
fn test_mcmc_error_zero_samples() {
let mc = MonteCarloIntegrator::new(None);
assert!(mc
.mcmc_integrate(|x| x[0], |x| (-x[0] * x[0]).exp(), 1.0, 0, 100)
.is_err());
}
}