1use rand::{Rng, SeedableRng};
14use rand_xoshiro::Xoshiro256PlusPlus;
15
16use crate::numeric::statistics::StatisticsAccumulator;
17
18#[derive(Debug, Clone, Copy)]
20pub struct IntegrateResult {
21 pub integral: f64,
23 pub error: f64,
25}
26
27pub trait Integrator {
29 fn integrate<F: Fn(&[f64]) -> f64>(&mut self, f: &F) -> IntegrateResult;
32}
33
34#[derive(Debug, Clone, Copy)]
50pub struct VegasOptions {
51 pub n_bins: usize,
53 pub n_samples: usize,
55 pub iterations: usize,
57 pub learning_rate: f64,
59 pub seed: u64,
61}
62
63impl Default for VegasOptions {
64 fn default() -> Self {
65 Self {
66 n_bins: 64,
67 n_samples: 10_000,
68 iterations: 10,
69 learning_rate: 1.5,
70 seed: 0x0C45,
71 }
72 }
73}
74
75#[derive(Debug, Clone)]
78struct GridAxis {
79 boundaries: Vec<f64>,
81 bin_accum: Vec<f64>,
83}
84
85impl GridAxis {
86 fn new(n_bins: usize) -> Self {
87 let boundaries = (0..=n_bins).map(|i| i as f64 / n_bins as f64).collect();
88 Self {
89 boundaries,
90 bin_accum: vec![0.0; n_bins],
91 }
92 }
93
94 fn sample<R: Rng>(&self, rng: &mut R) -> (f64, f64) {
98 let n = self.bin_accum.len();
99 let b = rng.random_range(0..n);
100 let lo = self.boundaries[b];
101 let hi = self.boundaries[b + 1];
102 let u = rng.random::<f64>();
103 let x = lo + (hi - lo) * u;
104 (x, (hi - lo) * n as f64)
105 }
106
107 fn add_training(&mut self, x: f64, weight: f64, f2: f64) {
109 let b = match self
111 .boundaries
112 .binary_search_by(|v| v.partial_cmp(&x).unwrap_or(std::cmp::Ordering::Equal))
113 {
114 Ok(i) => i.min(self.bin_accum.len().saturating_sub(1)),
115 Err(i) => i
116 .saturating_sub(1)
117 .min(self.bin_accum.len().saturating_sub(1)),
118 };
119 if b < self.bin_accum.len() {
120 self.bin_accum[b] += weight * f2;
121 }
122 }
123
124 fn update(&mut self, learning_rate: f64) {
129 let n = self.bin_accum.len();
130 if n == 0 {
131 return;
132 }
133 let total: f64 = self.bin_accum.iter().sum();
134 if total <= 0.0 {
135 return;
136 }
137 let avg = total / n as f64;
139 let mut d = vec![0.0; n];
140 for (i, d_slot) in d.iter_mut().enumerate() {
141 let prev = if i > 0 { self.bin_accum[i - 1] } else { 0.0 };
142 let next = if i + 1 < n {
143 self.bin_accum[i + 1]
144 } else {
145 0.0
146 };
147 let smooth = (prev + self.bin_accum[i] + next) / 3.0;
148 *d_slot = smooth / avg;
149 }
150 if (learning_rate - 1.0).abs() > 1e-12 {
152 for v in d.iter_mut() {
153 *v = v.max(1e-30).powf(1.0 / learning_rate);
154 }
155 }
156 let mut cum = vec![0.0; n + 1];
158 for i in 0..n {
159 cum[i + 1] = cum[i] + d[i];
160 }
161 let final_cum = cum[n];
162 if final_cum <= 0.0 {
163 return;
164 }
165 let mut new_boundaries = vec![0.0; n + 1];
166 new_boundaries[0] = 0.0;
167 new_boundaries[n] = 1.0;
168 let mut j = 0;
169 for (i, boundary) in new_boundaries.iter_mut().enumerate().take(n).skip(1) {
170 let target = i as f64 / n as f64 * final_cum;
171 while j < n && cum[j + 1] < target {
172 j += 1;
173 }
174 let lo = cum[j];
175 let hi = cum[j + 1];
176 let frac = if hi > lo {
177 (target - lo) / (hi - lo)
178 } else {
179 0.0
180 };
181 *boundary = (j as f64 + frac) / n as f64;
182 }
183 for i in 1..=n {
185 if new_boundaries[i] < new_boundaries[i - 1] {
186 new_boundaries[i] = new_boundaries[i - 1];
187 }
188 }
189 new_boundaries[n] = 1.0;
190 self.boundaries = new_boundaries;
191 self.bin_accum.fill(0.0);
192 }
193}
194
195pub struct Vegas {
197 opts: VegasOptions,
198 axes: Vec<GridAxis>,
199 accumulator: StatisticsAccumulator,
200}
201
202impl Vegas {
203 pub fn new(n_dims: usize, opts: VegasOptions) -> Self {
205 let axes = (0..n_dims).map(|_| GridAxis::new(opts.n_bins)).collect();
206 Self {
207 opts,
208 axes,
209 accumulator: StatisticsAccumulator::new(),
210 }
211 }
212
213 pub fn result(&self) -> IntegrateResult {
215 IntegrateResult {
216 integral: self.accumulator.integral(),
217 error: self.accumulator.error(),
218 }
219 }
220
221 pub fn iterations(&self) -> usize {
223 self.accumulator.iterations()
224 }
225}
226
227impl Integrator for Vegas {
228 fn integrate<F: Fn(&[f64]) -> f64>(&mut self, f: &F) -> IntegrateResult {
229 let n_dims = self.axes.len();
230 let mut rng = Xoshiro256PlusPlus::seed_from_u64(self.opts.seed);
231 for _ in 0..self.opts.iterations {
232 for _ in 0..self.opts.n_samples {
233 let mut x = Vec::with_capacity(n_dims);
235 let mut jac = 1.0;
236 for axis in self.axes.iter_mut() {
237 let (xi, wi) = axis.sample(&mut rng);
238 x.push(xi);
239 jac *= wi;
240 }
241 let fx = f(&x);
242 self.accumulator.add_sample(jac, fx);
243 let f2 = fx * fx;
244 for (i, xi) in x.iter().enumerate() {
245 self.axes[i].add_training(*xi, jac, f2);
246 }
247 }
248 self.accumulator.finalize_iteration();
249 for axis in self.axes.iter_mut() {
250 axis.update(self.opts.learning_rate);
251 }
252 }
253 self.result()
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn integrates_constant_exactly() {
263 let mut v = Vegas::new(1, VegasOptions::default());
265 let r = v.integrate(&|_x: &[f64]| 7.0);
266 assert!((r.integral - 7.0).abs() < 1e-9, "got {}", r.integral);
267 assert!(r.error < 1e-6, "error {}", r.error);
268 }
269
270 #[test]
271 fn integrates_linear_to_one_percent() {
272 let opts = VegasOptions {
274 n_samples: 20_000,
275 iterations: 8,
276 ..VegasOptions::default()
277 };
278 let mut v = Vegas::new(1, opts);
279 let r = v.integrate(&|x: &[f64]| x[0]);
280 assert!((r.integral - 0.5).abs() < 0.01, "got {}", r.integral);
281 }
282
283 #[test]
284 fn integrates_gaussian_peak() {
285 let opts = VegasOptions {
288 n_bins: 128,
289 n_samples: 20_000,
290 iterations: 12,
291 ..VegasOptions::default()
292 };
293 let mut v = Vegas::new(1, opts);
294 let r = v.integrate(&|x: &[f64]| (-50.0 * (x[0] - 0.5).powi(2)).exp());
295 let analytic = (std::f64::consts::PI / 50.0).sqrt();
296 assert!(
297 (r.integral - analytic).abs() < 0.02 * analytic,
298 "got {}, expected {}",
299 r.integral,
300 analytic
301 );
302 }
303
304 #[test]
305 fn integrates_two_dimensional_product() {
306 let opts = VegasOptions {
308 n_samples: 20_000,
309 iterations: 8,
310 ..VegasOptions::default()
311 };
312 let mut v = Vegas::new(2, opts);
313 let r = v.integrate(&|x: &[f64]| x[0] * x[1]);
314 assert!((r.integral - 0.25).abs() < 0.01, "got {}", r.integral);
315 }
316
317 #[test]
318 fn deterministic_across_runs_with_same_seed() {
319 let opts = VegasOptions {
320 n_samples: 5000,
321 iterations: 4,
322 seed: 42,
323 ..VegasOptions::default()
324 };
325 let mut a = Vegas::new(1, opts);
326 let ra = a.integrate(&|x: &[f64]| x[0] * x[0]);
327 let mut b = Vegas::new(1, opts);
328 let rb = b.integrate(&|x: &[f64]| x[0] * x[0]);
329 assert_eq!(ra.integral, rb.integral);
330 assert_eq!(ra.error, rb.error);
331 }
332
333 #[test]
334 fn integrate_1d_over_physical_bounds() {
335 use super::super::integrate_1d;
336 let r = integrate_1d(|x| x, 0.0, 2.0, Default::default());
338 assert!((r.integral - 2.0).abs() < 0.02, "got {}", r.integral);
339 let r2 = integrate_1d(|x| x * x, 1.0, 2.0, VegasOptions::default());
341 assert!(
342 (r2.integral - 7.0 / 3.0).abs() < 0.03,
343 "got {}",
344 r2.integral
345 );
346 }
347}