1use crate::bayesian::{BayesianOptimizer, rand_u01};
14
15#[must_use]
21pub fn latin_hypercube(bounds: &[(f64, f64)], n: usize, seed: u64) -> Vec<Vec<f64>> {
22 let dim = bounds.len();
23 let mut rng = seed;
24 let mut points = Vec::with_capacity(n);
25 let mut perms = Vec::with_capacity(dim);
27 for _ in 0..dim {
28 let mut perm: Vec<usize> = (0..n).collect();
29 for i in (1..n).rev() {
31 let j = (rand_u01(&mut rng) * (i + 1) as f64).floor() as usize;
32 perm.swap(i, j);
33 }
34 perms.push(perm);
35 }
36 for (i, _) in (0..n).enumerate() {
37 let mut row = Vec::with_capacity(dim);
38 for (d, &(lo, hi)) in bounds.iter().enumerate() {
39 let stratum = perms[d][i];
40 let pos = rand_u01(&mut rng).clamp(1e-12, 1.0 - 1e-12);
41 let v = (stratum as f64 + pos) / n as f64;
42 row.push((hi - lo).mul_add(v, lo));
43 }
44 points.push(row);
45 }
46 points
47}
48
49fn hermite(n: usize, x: f64) -> f64 {
53 if n == 0 {
54 return 1.0;
55 }
56 if n == 1 {
57 return x;
58 }
59 let mut h_prev2 = 1.0;
60 let mut h_prev1 = x;
61 let mut h = 0.0;
62 for k in 2..=n {
63 h = x.mul_add(h_prev1, -((k as f64 - 1.0) * h_prev2));
64 h_prev2 = h_prev1;
65 h_prev1 = h;
66 }
67 h
68}
69
70type MultiIndex = Vec<usize>;
72
73fn multi_indices(dim: usize, max_degree: usize) -> Vec<MultiIndex> {
76 let mut out = Vec::new();
77 let mut idx = vec![0usize; dim];
78 loop {
79 if idx.iter().sum::<usize>() <= max_degree {
80 out.push(idx.clone());
81 }
82 let mut i = 0;
84 while i < dim {
85 idx[i] += 1;
86 if idx[i] <= max_degree {
87 break;
88 }
89 idx[i] = 0;
90 i += 1;
91 }
92 if i == dim {
93 break;
94 }
95 }
96 out
97}
98
99pub struct Pce {
102 pub indices: Vec<MultiIndex>,
104 pub coefficients: Vec<f64>,
106 pub total_variance: f64,
108 pub sobol_first_order: Vec<f64>,
110 pub sobol_total: Vec<f64>,
112 pub r_squared: f64,
114}
115
116impl Pce {
117 #[must_use]
124 pub fn fit(x: &[Vec<f64>], y: &[f64], bounds: &[(f64, f64)], max_degree: usize) -> Self {
125 let dim = bounds.len();
126 let indices = multi_indices(dim, max_degree.min(6));
127 let n = x.len();
128
129 let mut design = vec![0.0_f64; n * indices.len()];
131 for (i, xi) in x.iter().enumerate() {
132 for (j, idx) in indices.iter().enumerate() {
133 let mut val = 1.0;
134 for (d, °) in idx.iter().enumerate() {
135 let (lo, hi) = bounds[d];
136 let xi_d = ((xi[d] - lo) / (hi - lo).max(1e-12))
137 .mul_add(2.0, -1.0)
138 .clamp(-1.0, 1.0);
139 val *= hermite(deg, xi_d);
140 }
141 design[i * indices.len() + j] = val;
142 }
143 }
144
145 let m = indices.len();
147 let mut at_a = vec![0.0_f64; m * m];
148 let mut at_y = vec![0.0_f64; m];
149 for i in 0..n {
150 for a in 0..m {
151 at_y[a] = design[i * m + a].mul_add(y[i], at_y[a]);
152 for b in 0..m {
153 at_a[a * m + b] = design[i * m + a].mul_add(design[i * m + b], at_a[a * m + b]);
154 }
155 }
156 }
157 let coeffs = solve_linear(&at_a, &at_y, m);
159
160 let total_variance = coeffs.iter().skip(1).map(|c| c * c).sum::<f64>();
162 let mut sobol_first = vec![0.0; dim];
163 let mut sobol_total = vec![0.0; dim];
164 for (j, idx) in indices.iter().enumerate() {
165 let c2 = coeffs[j] * coeffs[j];
166 for (d, °) in idx.iter().enumerate() {
167 if deg > 0 {
168 sobol_total[d] += c2;
169 if idx.iter().filter(|&&k| k > 0).count() == 1 {
170 sobol_first[d] += c2;
171 }
172 }
173 }
174 }
175 let scale = |v: f64| {
176 if total_variance > 1e-300 {
177 v / total_variance
178 } else {
179 0.0
180 }
181 };
182 let sobol_first_order = sobol_first.iter().map(|&v| scale(v)).collect();
183 let sobol_total = sobol_total.iter().map(|&v| scale(v)).collect();
184
185 let mean_y = y.iter().sum::<f64>() / n.max(1) as f64;
187 let mut sst = 0.0;
188 let mut sse = 0.0;
189 for (i, yi) in y.iter().enumerate() {
190 let mut pred = 0.0;
191 for (j, c) in coeffs.iter().enumerate() {
192 pred += c * design[i * m + j];
193 }
194 sst += (yi - mean_y).powi(2);
195 sse += (yi - pred).powi(2);
196 }
197 let r_squared = if sst > 1e-300 { 1.0 - sse / sst } else { 1.0 };
198
199 Self {
200 indices,
201 coefficients: coeffs,
202 total_variance,
203 sobol_first_order,
204 sobol_total,
205 r_squared,
206 }
207 }
208}
209
210fn solve_linear(a: &[f64], b: &[f64], m: usize) -> Vec<f64> {
213 let mut aug = a.to_vec();
214 let mut rhs = b.to_vec();
215 for col in 0..m {
216 let mut pivot = col;
218 let mut max_val = aug[col * m + col].abs();
219 for row in (col + 1)..m {
220 let v = aug[row * m + col].abs();
221 if v > max_val {
222 max_val = v;
223 pivot = row;
224 }
225 }
226 if max_val < 1e-12 {
227 continue;
229 }
230 if pivot != col {
231 for k in 0..m {
232 aug.swap(col * m + k, pivot * m + k);
233 }
234 rhs.swap(col, pivot);
235 }
236 let pivot_val = aug[col * m + col];
237 for row in (col + 1)..m {
238 let factor = aug[row * m + col] / pivot_val;
239 if factor == 0.0 {
240 continue;
241 }
242 for k in col..m {
243 aug[row * m + k] = factor.mul_add(-aug[col * m + k], aug[row * m + k]);
244 }
245 rhs[row] = factor.mul_add(-rhs[col], rhs[row]);
246 }
247 }
248 let mut c = vec![0.0_f64; m];
250 for row in (0..m).rev() {
251 let mut sum = rhs[row];
252 for k in (row + 1)..m {
253 sum = aug[row * m + k].mul_add(-c[k], sum);
254 }
255 let diag = aug[row * m + row];
256 c[row] = if diag.abs() > 1e-300 { sum / diag } else { 0.0 };
257 }
258 c
259}
260
261#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
263pub struct SuperforecasterResult {
264 pub best_params: Vec<f64>,
266 pub best_fitness: f64,
268 pub pce_r_squared: f64,
270 pub sobol_first_order: Vec<f64>,
272 pub sobol_total: Vec<f64>,
274 pub n_initial: usize,
276 pub n_bo_iterations: usize,
278}
279
280pub fn superforecaster<F>(
287 bounds: &[(f64, f64)],
288 fitness: F,
289 n_initial: usize,
290 n_bo_iterations: usize,
291 pce_degree: usize,
292 seed: u64,
293) -> SuperforecasterResult
294where
295 F: Fn(&[f64]) -> f64 + Clone,
296{
297 let lhs = latin_hypercube(bounds, n_initial.max(2), seed);
299 let mut xs = Vec::with_capacity(n_initial + n_bo_iterations);
300 let mut ys = Vec::with_capacity(n_initial + n_bo_iterations);
301 for x in &lhs {
302 xs.push(x.clone());
303 ys.push(fitness(x));
304 }
305
306 let pce = Pce::fit(&xs, &ys, bounds, pce_degree);
308
309 let mut opt = BayesianOptimizer::new(fitness, seed.wrapping_add(1));
312 let (_, (best_params, best_fitness)) = opt
313 .optimize(bounds, n_initial, n_bo_iterations, 100, 0.01)
314 .unwrap_or_else(|_| {
315 let mut best_idx = 0;
317 for (i, y) in ys.iter().enumerate() {
318 if y > &ys[best_idx] {
319 best_idx = i;
320 }
321 }
322 (Vec::new(), (xs[best_idx].clone(), ys[best_idx]))
323 });
324
325 let (best_params, best_fitness) = {
328 let (mut bp, mut bf) = (best_params, best_fitness);
329 for (x, y) in xs.iter().zip(ys.iter()) {
330 if *y > bf {
331 bp.clone_from(x);
332 bf = *y;
333 }
334 }
335 (bp, bf)
336 };
337
338 SuperforecasterResult {
339 best_params,
340 best_fitness,
341 pce_r_squared: pce.r_squared,
342 sobol_first_order: pce.sobol_first_order,
343 sobol_total: pce.sobol_total,
344 n_initial: n_initial.max(2),
345 n_bo_iterations,
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 #![allow(clippy::suboptimal_flops)] use super::*;
353
354 #[test]
355 fn lhs_has_stratified_coverage() {
356 let bounds = [(0.0, 10.0), (0.0, 10.0)];
357 let points = latin_hypercube(&bounds, 10, 1);
358 assert_eq!(points.len(), 10);
359 let mut strata: Vec<usize> = points
361 .iter()
362 .map(|p| ((p[0] / 10.0) * 10.0).floor() as usize)
363 .collect();
364 strata.sort_unstable();
365 assert_eq!(strata, vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
366 }
367
368 #[test]
369 fn pce_recovers_linear_surface() {
370 let bounds = [(0.0, 1.0), (0.0, 1.0)];
372 let xs: Vec<Vec<f64>> = (0..50)
373 .map(|i| vec![f64::from(i % 10) / 9.0, f64::from(i / 10) / 4.0])
374 .collect();
375 let ys: Vec<f64> = xs.iter().map(|x| 2.0 * x[0] + 3.0 * x[1]).collect();
376 let pce = Pce::fit(&xs, &ys, &bounds, 2);
377 assert!(pce.r_squared > 0.99, "R² = {}", pce.r_squared);
378 assert!(pce.sobol_first_order[0] > 0.1);
380 assert!(pce.sobol_first_order[1] > 0.1);
381 }
382
383 #[test]
384 fn pce_detects_dominant_variable() {
385 let bounds = [(0.0, 1.0), (0.0, 1.0)];
387 let xs: Vec<Vec<f64>> = (0..40)
388 .map(|i| vec![f64::from(i % 8) / 7.0, f64::from(i / 8) / 4.0])
389 .collect();
390 let ys: Vec<f64> = xs.iter().map(|x| 5.0 * x[0] + 0.1 * x[1]).collect();
391 let pce = Pce::fit(&xs, &ys, &bounds, 2);
392 assert!(
393 pce.sobol_first_order[0] > pce.sobol_first_order[1],
394 "S0 = {}, S1 = {}",
395 pce.sobol_first_order[0],
396 pce.sobol_first_order[1]
397 );
398 }
399
400 #[test]
401 fn multi_indices_total_degree() {
402 let idx = multi_indices(2, 2);
403 assert_eq!(idx.len(), 6);
405 assert!(idx.iter().all(|m| m.iter().sum::<usize>() <= 2));
406 }
407
408 #[test]
409 fn superforecaster_finds_optimum_and_sensitivities() {
410 let bounds = [(0.0, 10.0)];
411 let result = superforecaster(
412 &bounds,
413 |x: &[f64]| -(x[0] - 3.0).powi(2) + 5.0,
414 8,
415 10,
416 3,
417 42,
418 );
419 assert!(
420 (result.best_params[0] - 3.0).abs() < 0.5,
421 "best x = {}",
422 result.best_params[0]
423 );
424 assert!((result.best_fitness - 5.0).abs() < 0.5);
425 assert!(
426 result.sobol_first_order[0] > 0.9,
427 "S = {}",
428 result.sobol_first_order[0]
429 );
430 assert!(result.pce_r_squared > 0.9, "R² = {}", result.pce_r_squared);
431 }
432}