openentropy_core/
verdict.rs1use serde::Serialize;
15
16use crate::analysis::RunsResult;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
24pub enum Verdict {
25 Pass,
27 Warn,
29 Fail,
31 #[serde(rename = "N/A")]
33 Na,
34}
35
36impl Verdict {
37 pub fn as_str(&self) -> &'static str {
39 match self {
40 Verdict::Pass => "PASS",
41 Verdict::Warn => "WARN",
42 Verdict::Fail => "FAIL",
43 Verdict::Na => "N/A",
44 }
45 }
46}
47
48impl std::fmt::Display for Verdict {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.write_str(self.as_str())
51 }
52}
53
54pub fn verdict_autocorr(max_abs: f64) -> Verdict {
60 if max_abs > 0.15 {
61 Verdict::Fail
62 } else if max_abs > 0.05 {
63 Verdict::Warn
64 } else {
65 Verdict::Pass
66 }
67}
68
69pub fn verdict_spectral(flatness: f64) -> Verdict {
71 if flatness < 0.5 {
72 Verdict::Fail
73 } else if flatness < 0.75 {
74 Verdict::Warn
75 } else {
76 Verdict::Pass
77 }
78}
79
80pub fn verdict_bias(overall: f64, has_significant: bool) -> Verdict {
82 if overall > 0.02 {
83 Verdict::Fail
84 } else if has_significant {
85 Verdict::Warn
86 } else {
87 Verdict::Pass
88 }
89}
90
91pub fn verdict_distribution(ks_p: f64) -> Verdict {
93 if ks_p < 0.001 {
94 Verdict::Fail
95 } else if ks_p < 0.01 {
96 Verdict::Warn
97 } else {
98 Verdict::Pass
99 }
100}
101
102pub fn verdict_stationarity(f_stat: f64, is_stationary: bool) -> Verdict {
104 if f_stat > 3.0 {
105 Verdict::Fail
106 } else if !is_stationary {
107 Verdict::Warn
108 } else {
109 Verdict::Pass
110 }
111}
112
113pub fn verdict_runs(ru: &RunsResult, _sample_size: usize) -> Verdict {
115 let longest_ratio = if ru.expected_longest_run > 0.0 {
116 ru.longest_run as f64 / ru.expected_longest_run
117 } else {
118 1.0
119 };
120 let runs_dev = if ru.expected_runs > 0.0 {
121 (ru.total_runs as f64 - ru.expected_runs).abs() / ru.expected_runs
122 } else {
123 0.0
124 };
125 if longest_ratio > 3.0 || runs_dev > 0.4 {
126 Verdict::Fail
127 } else if longest_ratio > 2.0 || runs_dev > 0.2 {
128 Verdict::Warn
129 } else {
130 Verdict::Pass
131 }
132}
133
134pub fn verdict_hurst(h: f64) -> Verdict {
140 if !h.is_finite() {
141 return Verdict::Na;
142 }
143 if (0.4..=0.6).contains(&h) {
144 Verdict::Pass
145 } else if (0.3..=0.7).contains(&h) {
146 Verdict::Warn
147 } else {
148 Verdict::Fail
149 }
150}
151
152pub fn verdict_lyapunov(l: f64) -> Verdict {
154 if !l.is_finite() {
155 return Verdict::Na;
156 }
157 if l.abs() < 0.1 {
158 Verdict::Pass
159 } else if l.abs() < 0.2 {
160 Verdict::Warn
161 } else {
162 Verdict::Fail
163 }
164}
165
166pub fn verdict_corrdim(d: f64) -> Verdict {
168 if !d.is_finite() {
169 return Verdict::Na;
170 }
171 if d > 3.0 {
172 Verdict::Pass
173 } else if d > 2.0 {
174 Verdict::Warn
175 } else {
176 Verdict::Fail
177 }
178}
179
180pub fn verdict_bientropy(b: f64) -> Verdict {
182 if !b.is_finite() {
183 return Verdict::Na;
184 }
185 if b > 0.95 {
186 Verdict::Pass
187 } else if b > 0.90 {
188 Verdict::Warn
189 } else {
190 Verdict::Fail
191 }
192}
193
194pub fn verdict_compression(c: f64) -> Verdict {
196 if !c.is_finite() {
197 return Verdict::Na;
198 }
199 if c > 0.99 {
200 Verdict::Pass
201 } else if c > 0.95 {
202 Verdict::Warn
203 } else {
204 Verdict::Fail
205 }
206}
207
208pub fn verdict_sampen(v: f64) -> Verdict {
215 if !v.is_finite() {
216 return Verdict::Na;
217 }
218 if v > 1.0 {
219 Verdict::Pass
220 } else if v >= 0.5 {
221 Verdict::Warn
222 } else {
223 Verdict::Fail
224 }
225}
226
227pub fn verdict_dfa(alpha: f64) -> Verdict {
230 if !alpha.is_finite() {
231 return Verdict::Na;
232 }
233 if alpha > 0.4 && alpha < 0.6 {
234 Verdict::Pass
235 } else if (0.3..=0.7).contains(&alpha) {
236 Verdict::Warn
237 } else {
238 Verdict::Fail
239 }
240}
241
242pub fn verdict_rqa_det(det: f64) -> Verdict {
245 if !det.is_finite() {
246 return Verdict::Na;
247 }
248 if det < 0.1 {
249 Verdict::Pass
250 } else if det <= 0.3 {
251 Verdict::Warn
252 } else {
253 Verdict::Fail
254 }
255}
256
257pub fn verdict_apen(v: f64) -> Verdict {
260 if !v.is_finite() {
261 return Verdict::Na;
262 }
263 if v > 1.0 {
264 Verdict::Pass
265 } else if v >= 0.5 {
266 Verdict::Warn
267 } else {
268 Verdict::Fail
269 }
270}
271
272pub fn verdict_permen(v: f64) -> Verdict {
275 if !v.is_finite() {
276 return Verdict::Na;
277 }
278 if v > 0.95 {
279 Verdict::Pass
280 } else if v >= 0.8 {
281 Verdict::Warn
282 } else {
283 Verdict::Fail
284 }
285}
286
287pub fn verdict_anderson_darling(p: f64) -> Verdict {
290 if !p.is_finite() {
291 return Verdict::Na;
292 }
293 if p > 0.05 {
294 Verdict::Pass
295 } else if p >= 0.01 {
296 Verdict::Warn
297 } else {
298 Verdict::Fail
299 }
300}
301
302pub fn verdict_ljung_box(p: f64) -> Verdict {
305 if !p.is_finite() {
306 return Verdict::Na;
307 }
308 if p > 0.05 {
309 Verdict::Pass
310 } else if p >= 0.01 {
311 Verdict::Warn
312 } else {
313 Verdict::Fail
314 }
315}
316
317pub fn verdict_cramer_von_mises(p: f64) -> Verdict {
320 if !p.is_finite() {
321 return Verdict::Na;
322 }
323 if p > 0.05 {
324 Verdict::Pass
325 } else if p >= 0.01 {
326 Verdict::Warn
327 } else {
328 Verdict::Fail
329 }
330}
331
332pub fn metric_or_na(value: f64, is_valid: bool) -> String {
338 if is_valid && value.is_finite() {
339 format!("{value:.4}")
340 } else {
341 "N/A".to_string()
342 }
343}
344
345#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[test]
354 fn verdict_enum_display() {
355 assert_eq!(Verdict::Pass.as_str(), "PASS");
356 assert_eq!(Verdict::Warn.as_str(), "WARN");
357 assert_eq!(Verdict::Fail.as_str(), "FAIL");
358 assert_eq!(Verdict::Na.as_str(), "N/A");
359 assert_eq!(format!("{}", Verdict::Pass), "PASS");
360 }
361
362 #[test]
363 fn autocorr_thresholds() {
364 assert_eq!(verdict_autocorr(0.01), Verdict::Pass);
365 assert_eq!(verdict_autocorr(0.10), Verdict::Warn);
366 assert_eq!(verdict_autocorr(0.20), Verdict::Fail);
367 }
368
369 #[test]
370 fn spectral_thresholds() {
371 assert_eq!(verdict_spectral(0.90), Verdict::Pass);
372 assert_eq!(verdict_spectral(0.60), Verdict::Warn);
373 assert_eq!(verdict_spectral(0.30), Verdict::Fail);
374 }
375
376 #[test]
377 fn hurst_thresholds() {
378 assert_eq!(verdict_hurst(0.50), Verdict::Pass);
379 assert_eq!(verdict_hurst(0.35), Verdict::Warn);
380 assert_eq!(verdict_hurst(0.10), Verdict::Fail);
381 assert_eq!(verdict_hurst(f64::NAN), Verdict::Na);
382 }
383
384 #[test]
385 fn lyapunov_thresholds() {
386 assert_eq!(verdict_lyapunov(0.05), Verdict::Pass);
387 assert_eq!(verdict_lyapunov(0.15), Verdict::Warn);
388 assert_eq!(verdict_lyapunov(0.50), Verdict::Fail);
389 assert_eq!(verdict_lyapunov(f64::NAN), Verdict::Na);
390 }
391
392 #[test]
393 fn corrdim_thresholds() {
394 assert_eq!(verdict_corrdim(5.0), Verdict::Pass);
395 assert_eq!(verdict_corrdim(2.5), Verdict::Warn);
396 assert_eq!(verdict_corrdim(1.5), Verdict::Fail);
397 assert_eq!(verdict_corrdim(f64::NAN), Verdict::Na);
398 }
399
400 #[test]
401 fn bientropy_thresholds() {
402 assert_eq!(verdict_bientropy(0.98), Verdict::Pass);
403 assert_eq!(verdict_bientropy(0.92), Verdict::Warn);
404 assert_eq!(verdict_bientropy(0.80), Verdict::Fail);
405 assert_eq!(verdict_bientropy(f64::NAN), Verdict::Na);
406 }
407
408 #[test]
409 fn compression_thresholds() {
410 assert_eq!(verdict_compression(1.00), Verdict::Pass);
411 assert_eq!(verdict_compression(0.97), Verdict::Warn);
412 assert_eq!(verdict_compression(0.90), Verdict::Fail);
413 assert_eq!(verdict_compression(f64::NAN), Verdict::Na);
414 }
415
416 #[test]
417 fn metric_display() {
418 assert_eq!(metric_or_na(0.1234, true), "0.1234");
419 assert_eq!(metric_or_na(0.1234, false), "N/A");
420 assert_eq!(metric_or_na(f64::NAN, true), "N/A");
421 }
422}