hpr_analysis/sensitivity/
benchmark.rs1use std::f64::consts::PI;
17
18use serde::{Deserialize, Serialize};
19
20use super::Factor;
21use crate::error::AnalysisError;
22
23#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
41pub struct Ishigami {
42 pub a: f64,
44 pub b: f64,
46}
47
48impl Ishigami {
49 pub const STANDARD: Self = Self { a: 7.0, b: 0.1 };
51
52 pub fn factors() -> Result<Vec<Factor>, AnalysisError> {
58 ["x1", "x2", "x3"]
59 .into_iter()
60 .map(|name| Factor::new(name, -PI, PI))
61 .collect()
62 }
63
64 pub fn evaluate(&self, x: &[f64]) -> f64 {
66 let &[x1, x2, x3] = x else {
67 return f64::NAN;
68 };
69 let s2 = x2.sin();
70 x1.sin() + self.a * s2 * s2 + self.b * x3.powi(4) * x1.sin()
71 }
72
73 pub fn mean(&self) -> f64 {
75 0.5 * self.a
76 }
77
78 pub fn variance(&self) -> f64 {
80 let (a, b) = (self.a, self.b);
81 0.5 + a * a / 8.0 + b * PI.powi(4) / 5.0 + b * b * PI.powi(8) / 18.0
82 }
83
84 fn v13(&self) -> f64 {
86 8.0 * self.b * self.b * PI.powi(8) / 225.0
87 }
88
89 pub fn first_order(&self) -> [f64; 3] {
91 let v = self.variance();
92 let v1 = 0.5 * (1.0 + self.b * PI.powi(4) / 5.0).powi(2);
93 let v2 = self.a * self.a / 8.0;
94 [v1 / v, v2 / v, 0.0]
95 }
96
97 pub fn total(&self) -> [f64; 3] {
99 let [s1, s2, _] = self.first_order();
100 let s13 = self.v13() / self.variance();
101 [s1 + s13, s2, s13]
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
116#[serde(try_from = "SobolGData")]
117pub struct SobolG {
118 a: Vec<f64>,
119}
120
121#[derive(Deserialize)]
123#[serde(deny_unknown_fields)]
124struct SobolGData {
125 a: Vec<f64>,
126}
127
128impl TryFrom<SobolGData> for SobolG {
129 type Error = AnalysisError;
130
131 fn try_from(data: SobolGData) -> Result<Self, AnalysisError> {
132 Self::new(data.a)
133 }
134}
135
136impl SobolG {
137 pub const MAX_A: f64 = 1e100;
140
141 pub fn new(a: Vec<f64>) -> Result<Self, AnalysisError> {
148 if a.is_empty() {
149 return Err(AnalysisError::TooFew {
150 what: "g function's factors",
151 count: 0,
152 minimum: 1,
153 });
154 }
155 if let Some(&value) = a.iter().find(|&&ai| !(0.0..=Self::MAX_A).contains(&ai)) {
156 return Err(AnalysisError::Domain {
157 what: "g function's a (from 0 to 1e100)",
158 value,
159 });
160 }
161 Ok(Self { a })
162 }
163
164 pub fn a(&self) -> &[f64] {
166 &self.a
167 }
168
169 pub fn factors(&self) -> Result<Vec<Factor>, AnalysisError> {
175 (1..=self.a.len())
176 .map(|i| Factor::new(format!("x{i}"), 0.0, 1.0))
177 .collect()
178 }
179
180 pub fn evaluate(&self, x: &[f64]) -> f64 {
182 if x.len() != self.a.len() {
183 return f64::NAN;
184 }
185 x.iter()
186 .zip(&self.a)
187 .map(|(&xi, &ai)| ((4.0 * xi - 2.0).abs() + ai) / (1.0 + ai))
188 .product()
189 }
190
191 pub fn mean(&self) -> f64 {
193 1.0
194 }
195
196 fn parts(&self) -> Vec<f64> {
198 self.a
199 .iter()
200 .map(|ai| 1.0 / (3.0 * (1.0 + ai) * (1.0 + ai)))
201 .collect()
202 }
203
204 pub fn variance(&self) -> f64 {
206 self.log_product().exp_m1()
207 }
208
209 fn log_product(&self) -> f64 {
211 self.parts().iter().map(|v| v.ln_1p()).sum()
212 }
213
214 pub fn first_order(&self) -> Vec<f64> {
216 let v = self.variance();
217 self.parts().iter().map(|vi| vi / v).collect()
218 }
219
220 pub fn total(&self) -> Vec<f64> {
222 let log_all = self.log_product();
223 let v = log_all.exp_m1();
224 self.parts()
225 .iter()
226 .map(|vi| vi * (log_all - vi.ln_1p()).exp() / v)
227 .collect()
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn ishigamis_indices_are_the_published_ones() {
237 let f = Ishigami { a: 7.0, b: 0.05 };
240 let [s1, s2, s3] = f.first_order();
241 let [t1, _, t3] = f.total();
242 assert!((s1 - 0.219).abs() < 5e-4, "{s1}");
243 assert!((s2 - 0.687).abs() < 5e-4, "{s2}");
244 assert_eq!(s3, 0.0);
245 assert!((t3 - 0.0946).abs() < 5e-5, "{t3}");
246 assert!((t1 - s1 - t3).abs() < 1e-15);
247 let [s1, s2, _] = Ishigami::STANDARD.first_order();
249 assert!((s1 - 0.3139).abs() < 5e-5 && (s2 - 0.4424).abs() < 5e-5);
250 let f = Ishigami::STANDARD;
252 let [s1, s2, _] = f.first_order();
253 let [_, _, t3] = f.total();
254 assert!((s1 + s2 + t3 - 1.0).abs() < 1e-15);
255 }
256
257 #[test]
258 fn ishigamis_mean_and_variance_are_its_integrals() {
259 let f = Ishigami::STANDARD;
262 let n = 120;
263 let h = 2.0 * PI / f64::from(n);
264 let (mut sum, mut squares) = (0.0, 0.0);
265 for i in 0..n {
266 for j in 0..n {
267 for k in 0..n {
268 let x = |m: i32| -PI + (f64::from(m) + 0.5) * h;
269 let y = f.evaluate(&[x(i), x(j), x(k)]);
270 sum += y;
271 squares += y * y;
272 }
273 }
274 }
275 let count = f64::from(n).powi(3);
276 let mean = sum / count;
277 let variance = squares / count - mean * mean;
278 assert!((mean - f.mean()).abs() < 1e-9, "{mean}");
279 assert!(
280 (variance - f.variance()).abs() / f.variance() < 1e-3,
281 "{variance}"
282 );
283 }
284
285 #[test]
286 fn the_g_functions_indices_add_up_and_order_by_a() {
287 let g = SobolG::new(vec![0.0, 1.0, 4.5, 9.0, 99.0, 99.0, 99.0, 99.0]).unwrap();
288 let s = g.first_order();
289 let t = g.total();
290 assert!(s.iter().sum::<f64>() < 1.0);
291 assert!(t.iter().sum::<f64>() > 1.0);
292 for i in 0..8 {
293 assert!(s[i] <= t[i]);
294 }
295 for i in 0..3 {
296 assert!(t[i] > t[i + 1]);
297 }
298 let tiny = SobolG::new(vec![1e9]).unwrap();
300 let part = 1.0 / (3.0 * (1.0 + 1e9) * (1.0 + 1e9));
301 assert!((tiny.variance() / part - 1.0).abs() < 1e-12);
302 assert!((tiny.first_order()[0] - 1.0).abs() < 1e-12);
303 let one = SobolG::new(vec![0.0]).unwrap();
305 assert!((one.variance() - 1.0 / 3.0).abs() < 1e-15);
306 assert!((one.first_order()[0] - 1.0).abs() < 1e-14);
309 assert!((one.total()[0] - 1.0).abs() < 1e-14);
310 }
311
312 #[test]
313 fn the_g_functions_mean_and_variance_are_its_integrals() {
314 let g = SobolG::new(vec![0.5, 2.0]).unwrap();
317 let n = 4000;
318 let h = 1.0 / f64::from(n);
319 let (mut sum, mut squares) = (0.0, 0.0);
320 for i in 0..n {
321 for j in 0..n {
322 let y = g.evaluate(&[(f64::from(i) + 0.5) * h, (f64::from(j) + 0.5) * h]);
323 sum += y;
324 squares += y * y;
325 }
326 }
327 let count = f64::from(n * n);
328 let mean = sum / count;
329 let variance = squares / count - mean * mean;
330 assert!((mean - 1.0).abs() < 1e-12, "{mean}");
331 assert!((variance - g.variance()).abs() < 1e-7, "{variance}");
332 }
333
334 #[test]
335 fn a_point_of_the_wrong_length_is_nan_and_bad_parameters_are_refused() {
336 assert!(Ishigami::STANDARD.evaluate(&[0.0, 0.0]).is_nan());
337 let g = SobolG::new(vec![1.0, 2.0]).unwrap();
338 assert!(g.evaluate(&[0.5]).is_nan());
339 assert_eq!(g.evaluate(&[0.5, 0.5]), (1.0 / 2.0) * (2.0 / 3.0));
340 assert!(matches!(
341 SobolG::new(vec![]),
342 Err(AnalysisError::TooFew {
343 what: "g function's factors",
344 count: 0,
345 minimum: 1
346 })
347 ));
348 match SobolG::new(vec![1.0, -0.5]) {
349 Err(AnalysisError::Domain { value, .. }) => assert_eq!(value, -0.5),
350 other => panic!("{other:?}"),
351 }
352 let refused = serde_json::from_str::<SobolG>(r#"{"a":[1.0,-1.0]}"#)
353 .unwrap_err()
354 .to_string();
355 assert!(refused.contains("g function's a"), "{refused}");
356 assert!(SobolG::new(vec![SobolG::MAX_A]).is_ok());
357 match SobolG::new(vec![1e101]) {
358 Err(AnalysisError::Domain { what, value }) => {
359 assert_eq!((what, value), ("g function's a (from 0 to 1e100)", 1e101));
360 }
361 other => panic!("{other:?}"),
362 }
363 let big = SobolG::new(vec![SobolG::MAX_A, SobolG::MAX_A]).unwrap();
364 assert!(big.first_order().iter().all(|s| (s - 0.5).abs() < 1e-12));
365 assert_eq!(
366 serde_json::from_str::<SobolG>(r#"{"a":[1.0,2.0]}"#).unwrap(),
367 g
368 );
369 }
370}