1use hpr_core::random::SeededRng;
61use serde::{Deserialize, Serialize};
62
63use super::{Factor, check_factors, check_outputs, check_size};
64use crate::error::AnalysisError;
65
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69#[serde(try_from = "SobolData")]
70pub struct Sobol {
71 factors: Vec<Factor>,
72 rows: usize,
73}
74
75#[derive(Deserialize)]
77#[serde(deny_unknown_fields)]
78struct SobolData {
79 factors: Vec<Factor>,
80 rows: usize,
81}
82
83impl TryFrom<SobolData> for Sobol {
84 type Error = AnalysisError;
85
86 fn try_from(data: SobolData) -> Result<Self, AnalysisError> {
87 Self::new(data.factors, data.rows)
88 }
89}
90
91#[derive(Debug, Clone, PartialEq)]
95pub struct SobolDesign {
96 factors: Vec<Factor>,
97 a: Vec<f64>,
99 b: Vec<f64>,
100}
101
102#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
104#[non_exhaustive]
105pub struct SobolIndex {
106 pub name: String,
108 pub first_order: f64,
110 pub first_order_standard_error: f64,
112 pub total: f64,
114 pub total_standard_error: f64,
116}
117
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
120#[non_exhaustive]
121pub struct SobolIndices {
122 pub rows: usize,
124 pub mean: f64,
126 pub variance: f64,
128 pub factors: Vec<SobolIndex>,
130}
131
132impl Sobol {
133 pub fn new(factors: Vec<Factor>, rows: usize) -> Result<Self, AnalysisError> {
144 check_factors(&factors)?;
145 if rows < 2 {
146 return Err(AnalysisError::TooFew {
147 what: "Sobol' rows",
148 count: rows,
149 minimum: 2,
150 });
151 }
152 let k = factors.len();
153 check_size("Sobol' points", rows, k.saturating_add(2), k)?;
154 Ok(Self { factors, rows })
155 }
156
157 pub fn factors(&self) -> &[Factor] {
159 &self.factors
160 }
161
162 pub fn rows(&self) -> usize {
164 self.rows
165 }
166
167 pub fn design(&self, seed: u64) -> SobolDesign {
171 let k = self.factors.len();
172 let mut a = Vec::with_capacity(self.rows * k);
173 let mut b = Vec::with_capacity(self.rows * k);
174 for j in 0..self.rows {
175 let mut rng = SeededRng::for_stream(seed, &[j as u64]);
177 a.extend((0..k).map(|_| rng.uniform()));
178 b.extend((0..k).map(|_| rng.uniform()));
179 }
180 SobolDesign {
181 factors: self.factors.clone(),
182 a,
183 b,
184 }
185 }
186
187 pub fn indices(
194 &self,
195 seed: u64,
196 mut model: impl FnMut(&[f64]) -> f64,
197 ) -> Result<SobolIndices, AnalysisError> {
198 let design = self.design(seed);
199 let outputs: Vec<f64> = design.points().iter().map(|x| model(x)).collect();
200 design.analyze(&outputs)
201 }
202}
203
204impl SobolDesign {
205 pub fn rows(&self) -> usize {
207 self.a.len() / self.factors.len()
208 }
209
210 pub fn len(&self) -> usize {
212 self.rows() * (self.factors.len() + 2)
213 }
214
215 pub fn is_empty(&self) -> bool {
217 self.len() == 0
218 }
219
220 pub fn points(&self) -> Vec<Vec<f64>> {
223 let k = self.factors.len();
224 let physical = |unit: &[f64]| -> Vec<f64> {
225 unit.iter()
226 .zip(&self.factors)
227 .map(|(&u, factor)| factor.at(u))
228 .collect()
229 };
230 let mut points = Vec::with_capacity(self.len());
231 for (a, b) in self.a.chunks_exact(k).zip(self.b.chunks_exact(k)) {
232 let a = physical(a);
233 let b = physical(b);
234 for i in 0..k {
235 let mut mixed = a.clone();
236 mixed[i] = b[i];
237 points.push(mixed);
238 }
239 let first = points.len() - k;
241 points.push(a);
242 points.push(b);
243 points[first..].rotate_right(2);
244 }
245 points
246 }
247
248 pub fn analyze(&self, outputs: &[f64]) -> Result<SobolIndices, AnalysisError> {
260 check_outputs(outputs, self.len())?;
261 let k = self.factors.len();
262 let rows: Vec<&[f64]> = outputs.chunks_exact(k + 2).collect();
263 let n = rows.len() as f64;
265 let shift = rows.iter().map(|r| r[0] + r[1]).sum::<f64>() / (2.0 * n);
266 let rows: Vec<Vec<f64>> = rows
268 .iter()
269 .map(|r| r.iter().map(|y| y - shift).collect())
270 .collect();
271 let m: Vec<f64> = rows.iter().map(|r| 0.5 * (r[0] + r[1])).collect();
272 let q: Vec<f64> = rows
273 .iter()
274 .map(|r| 0.5 * (r[0] * r[0] + r[1] * r[1]))
275 .collect();
276 let mean = |xs: &[f64]| xs.iter().sum::<f64>() / n;
277 let big_m = mean(&m);
278 let big_q = mean(&q);
279 let variance = big_q - big_m * big_m;
280 if !variance.is_finite() || variance <= 0.0 {
281 return Err(AnalysisError::Domain {
282 what: "output variance over A and B",
283 value: variance,
284 });
285 }
286 let on_variance: Vec<f64> = m
288 .iter()
289 .zip(&q)
290 .map(|(&mj, &qj)| ((qj - big_q) - 2.0 * big_m * (mj - big_m)) / variance)
291 .collect();
292 let standard_error = |psi: &mut dyn Iterator<Item = f64>| {
293 (psi.map(|x| x * x).sum::<f64>() / (n * (n - 1.0))).sqrt()
294 };
295 let factors: Vec<SobolIndex> = self
296 .factors
297 .iter()
298 .enumerate()
299 .map(|(i, factor)| {
300 let first: Vec<f64> = rows.iter().map(|r| r[1] * (r[2 + i] - r[0])).collect();
301 let differences: Vec<f64> = rows.iter().map(|r| r[2 + i] - r[0]).collect();
302 let total: Vec<f64> = differences.iter().map(|d| 0.5 * d * d).collect();
303 let big_p = mean(&first);
304 let big_d = mean(&differences);
305 let big_t = mean(&total);
306 let s = big_p / variance;
307 let st = big_t / variance;
308 let first_order_standard_error =
309 standard_error(&mut first.iter().zip(&m).zip(&on_variance).map(
310 |((&pj, &mj), &v)| ((pj - big_p) - big_d * (mj - big_m)) / variance - s * v,
311 ));
312 let total_standard_error = standard_error(
313 &mut total
314 .iter()
315 .zip(&on_variance)
316 .map(|(&tj, &v)| (tj - big_t) / variance - st * v),
317 );
318 SobolIndex {
319 name: factor.name().to_owned(),
320 first_order: s,
321 first_order_standard_error,
322 total: st,
323 total_standard_error,
324 }
325 })
326 .collect();
327 if let Some(value) = factors
328 .iter()
329 .flat_map(|f| {
330 [
331 f.first_order,
332 f.first_order_standard_error,
333 f.total,
334 f.total_standard_error,
335 ]
336 })
337 .find(|x| !x.is_finite())
338 {
339 return Err(AnalysisError::Domain {
340 what: "Sobol' index or standard error (an output too large)",
341 value,
342 });
343 }
344 Ok(SobolIndices {
345 rows: rows.len(),
346 mean: shift + big_m,
347 variance,
348 factors,
349 })
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 fn unit_factors(k: usize) -> Vec<Factor> {
358 (0..k)
359 .map(|i| Factor::new(format!("x{i}"), 0.0, 1.0).unwrap())
360 .collect()
361 }
362
363 #[test]
364 fn an_analysis_refuses_what_it_cant_lay_out() {
365 assert!(matches!(
366 Sobol::new(Vec::new(), 10),
367 Err(AnalysisError::TooFew {
368 what: "factors",
369 ..
370 })
371 ));
372 assert!(matches!(
373 Sobol::new(unit_factors(2), 1),
374 Err(AnalysisError::TooFew {
375 what: "Sobol' rows",
376 count: 1,
377 ..
378 })
379 ));
380 assert!(matches!(
381 Sobol::new(unit_factors(1), 1 << 61),
382 Err(AnalysisError::Count {
383 what: "Sobol' points",
384 limit: crate::sensitivity::MAX_DESIGN_POINTS,
385 ..
386 })
387 ));
388 assert!(matches!(
390 Sobol::new(unit_factors(100), 10_000),
391 Err(AnalysisError::Count {
392 what: "design coordinates (points times factors)",
393 count: 102_000_000,
394 ..
395 })
396 ));
397 let huge = r#"{"factors":[{"name":"x","low":0.0,"high":1.0}],"rows":2305843009213693952}"#;
398 let refused = serde_json::from_str::<Sobol>(huge).unwrap_err().to_string();
399 assert!(refused.contains("Sobol' points"), "{refused}");
400 }
401
402 #[test]
403 fn each_row_holds_a_b_and_a_with_one_column_of_b() {
404 let factors = vec![
405 Factor::new("a", 0.0, 1.0).unwrap(),
406 Factor::new("b", 10.0, 20.0).unwrap(),
407 Factor::new("c", -1.0, 1.0).unwrap(),
408 ];
409 let design = Sobol::new(factors.clone(), 6).unwrap().design(5);
410 let points = design.points();
411 assert_eq!((points.len(), design.len(), design.rows()), (30, 30, 6));
412 for row in points.as_chunks::<5>().0 {
413 let (a, b) = (&row[0], &row[1]);
414 for (x, f) in a.iter().chain(b).zip(factors.iter().cycle()) {
415 assert!(*x >= f.low() && *x < f.high());
416 }
417 assert!(a.iter().zip(b).all(|(x, y)| x != y));
418 for (i, mixed) in row[2..].iter().enumerate() {
419 for j in 0..3 {
420 assert_eq!(mixed[j], if i == j { b[j] } else { a[j] });
421 }
422 }
423 }
424 }
425
426 #[test]
427 fn row_j_is_the_same_however_many_are_drawn() {
428 let short = Sobol::new(unit_factors(3), 4).unwrap().design(9).points();
429 let long = Sobol::new(unit_factors(3), 40).unwrap().design(9).points();
430 assert_eq!(short[..], long[..short.len()]);
431 let other = Sobol::new(unit_factors(3), 4).unwrap().design(10).points();
432 assert_ne!(short, other);
433 }
434
435 #[test]
436 fn an_additive_model_has_equal_first_order_and_total_indices() {
437 let indices = Sobol::new(unit_factors(2), 20_000)
440 .unwrap()
441 .indices(1, |x| 2.0 * x[0] + x[1])
442 .unwrap();
443 for (index, share) in indices.factors.iter().zip([0.8, 0.2]) {
444 for (estimate, error) in [
445 (index.first_order, index.first_order_standard_error),
446 (index.total, index.total_standard_error),
447 ] {
448 assert!(error > 0.0 && error < 0.02, "{index:?}");
449 assert!((estimate - share).abs() < 4.0 * error, "{index:?}");
450 }
451 }
452 assert!((indices.mean - 1.5).abs() < 0.02);
453 assert!((indices.variance - 5.0 / 12.0).abs() < 0.01);
454 }
455
456 #[test]
457 fn adding_a_constant_changes_no_estimate_beyond_rounding() {
458 let sobol = Sobol::new(unit_factors(2), 500).unwrap();
459 let f = |x: &[f64]| x[0] * x[1] + x[0];
460 let plain = sobol.indices(3, f).unwrap();
461 let shifted = sobol.indices(3, |x| f(x) + 1.0e4).unwrap();
462 for (a, b) in plain.factors.iter().zip(&shifted.factors) {
463 assert!((a.first_order - b.first_order).abs() < 1e-9);
464 assert!((a.first_order_standard_error - b.first_order_standard_error).abs() < 1e-9);
465 assert!((a.total - b.total).abs() < 1e-9);
466 }
467 assert!((shifted.mean - plain.mean - 1.0e4).abs() < 1e-8);
468 }
469
470 #[test]
474 fn standard_errors_are_the_delta_method_on_raw_row_means() {
475 let f = |x: &[f64]| x[0] * x[1] + 2.0 * x[0] + x[2] * x[2] + 5.0;
476 let design = Sobol::new(unit_factors(3), 1000).unwrap().design(4);
477 let outputs: Vec<f64> = design.points().iter().map(|x| f(x)).collect();
478 let indices = design.analyze(&outputs).unwrap();
479 let rows: Vec<&[f64; 5]> = outputs.as_chunks::<5>().0.iter().collect();
480 let n = rows.len() as f64;
481 let mean = |v: &[f64]| v.iter().sum::<f64>() / n;
482 let covariance = |a: &[f64], b: &[f64]| {
483 let (ma, mb) = (mean(a), mean(b));
484 a.iter()
485 .zip(b)
486 .map(|(x, y)| (x - ma) * (y - mb))
487 .sum::<f64>()
488 / (n - 1.0)
489 };
490 let delta = |g: &[f64], zs: &[Vec<f64>]| {
492 let mut var = 0.0;
493 for (gi, zi) in g.iter().zip(zs) {
494 for (gj, zj) in g.iter().zip(zs) {
495 var += gi * gj * covariance(zi, zj);
496 }
497 }
498 (var / n).sqrt()
499 };
500 let m: Vec<f64> = rows.iter().map(|r| 0.5 * (r[0] + r[1])).collect();
501 let q: Vec<f64> = rows
502 .iter()
503 .map(|r| 0.5 * (r[0] * r[0] + r[1] * r[1]))
504 .collect();
505 let (big_m, big_q) = (mean(&m), mean(&q));
506 let v = big_q - big_m * big_m;
507 assert!((v - indices.variance).abs() < 1e-12 * v);
508 for (i, index) in indices.factors.iter().enumerate() {
509 let d: Vec<f64> = rows.iter().map(|r| r[2 + i] - r[0]).collect();
510 let r: Vec<f64> = rows.iter().zip(&d).map(|(row, d)| row[1] * d).collect();
511 let t: Vec<f64> = d.iter().map(|d| 0.5 * d * d).collect();
512 let (big_r, big_d, big_t) = (mean(&r), mean(&d), mean(&t));
513 let s = (big_r - big_m * big_d) / v;
514 let st = big_t / v;
515 let g = [1.0 / v, -big_m / v, (2.0 * big_m * s - big_d) / v, -s / v];
516 let zs = [r, d.clone(), m.clone(), q.clone()];
517 let se = delta(&g, &zs);
518 let gt = [1.0 / v, 2.0 * big_m * st / v, -st / v];
519 let se_t = delta(>, &[t, m.clone(), q.clone()]);
520 let close = |x: f64, y: f64| (x - y).abs() < 1e-9 * y.abs();
521 assert!(close(index.first_order, s), "{index:?} against {s}");
522 assert!(close(index.total, st), "{index:?} against {st}");
523 assert!(
524 close(index.first_order_standard_error, se),
525 "{index:?} against {se}"
526 );
527 assert!(
528 close(index.total_standard_error, se_t),
529 "{index:?} against {se_t}"
530 );
531 }
532 }
533
534 #[test]
535 fn outputs_whose_variance_overflows_are_refused() {
536 let sobol = Sobol::new(unit_factors(2), 10).unwrap();
537 let large = sobol.indices(1, |x| 1e150 * x[0]).unwrap();
538 assert!(large.factors.iter().all(|f| f.first_order.is_finite()
539 && f.total.is_finite()
540 && f.first_order_standard_error.is_finite()
541 && f.total_standard_error.is_finite()));
542 match sobol.indices(1, |x| 1e160 * x[0]) {
543 Err(AnalysisError::Domain { what, value }) => {
544 assert_eq!(what, "output variance over A and B");
545 assert!(value.is_infinite());
546 }
547 other => panic!("{other:?}"),
548 }
549 let design = Sobol::new(unit_factors(1), 2).unwrap().design(1);
551 match design.analyze(&[1.0, -1.0, 1e160, -1.0, 1.0, -1e160]) {
552 Err(AnalysisError::Domain { what, .. }) => {
553 assert_eq!(what, "Sobol' index or standard error (an output too large)");
554 }
555 other => panic!("{other:?}"),
556 }
557 }
558
559 #[test]
560 fn a_constant_output_is_refused() {
561 let sobol = Sobol::new(unit_factors(2), 10).unwrap();
562 assert!(matches!(
563 sobol.indices(1, |_| 3.0),
564 Err(AnalysisError::Domain {
565 what: "output variance over A and B",
566 ..
567 })
568 ));
569 }
570
571 #[test]
572 fn an_analysis_reads_back_through_its_checks() {
573 let s = Sobol::new(unit_factors(2), 64).unwrap();
574 let json = serde_json::to_string(&s).unwrap();
575 assert_eq!(serde_json::from_str::<Sobol>(&json).unwrap(), s);
576 let few = json.replace("\"rows\":64", "\"rows\":1");
577 let refused = serde_json::from_str::<Sobol>(&few).unwrap_err().to_string();
578 assert!(refused.contains("Sobol' rows: 1 given"), "{refused}");
579 }
580}