1#[must_use]
22pub fn erf(x: f64) -> f64 {
23 const A1: f64 = 0.254_829_592;
25 const A2: f64 = -0.284_496_736;
26 const A3: f64 = 1.421_413_741;
27 const A4: f64 = -1.453_152_027;
28 const A5: f64 = 1.061_405_429;
29 const P: f64 = 0.327_591_1;
30
31 let sign = if x < 0.0 { -1.0 } else { 1.0 };
32 let x = x.abs();
33 let t = 1.0 / (1.0 + P * x);
34 let y = 1.0 - (((((A5 * t + A4) * t) + A3) * t + A2) * t + A1) * t * (-x * x).exp();
35
36 sign * y
37}
38
39#[must_use]
43pub fn erf_f32(x: f32) -> f32 {
44 erf(f64::from(x)) as f32
45}
46
47const SQRPI: f64 = 0.564_189_583_547_756_3;
71
72const CODY_THRESH: f64 = 0.468_75;
74
75const CODY_XBIG: f64 = 26.543;
77
78#[allow(clippy::unreadable_literal)]
88const CODY_A: [f64; 5] = [
89 3.1611237438705655,
90 113.86415415105016,
91 377.485237685302,
92 3209.3775891384694,
93 0.18577770618460315,
94];
95#[allow(clippy::unreadable_literal)]
96const CODY_B: [f64; 4] = [
97 23.601290952344122,
98 244.02463793444417,
99 1282.6165260773723,
100 2844.236833439171,
101];
102#[allow(clippy::unreadable_literal)]
103const CODY_C: [f64; 9] = [
104 0.5641884969886701,
105 8.883149794388377,
106 66.11919063714163,
107 298.6351381974001,
108 881.952221241769,
109 1712.0476126340707,
110 2051.0783778260716,
111 1230.3393547979972,
112 2.1531153547440383e-8,
113];
114#[allow(clippy::unreadable_literal)]
115const CODY_D: [f64; 8] = [
116 15.744926110709835,
117 117.6939508913125,
118 537.1811018620099,
119 1621.3895745666903,
120 3290.7992357334597,
121 4362.619090143247,
122 3439.3676741437216,
123 1230.3393548037495,
124];
125#[allow(clippy::unreadable_literal)]
126const CODY_P: [f64; 6] = [
127 0.30532663496123236,
128 0.36034489994980445,
129 0.12578172611122926,
130 0.016083785148742275,
131 0.0006587491615298378,
132 0.016315387137302097,
133];
134#[allow(clippy::unreadable_literal)]
135const CODY_Q: [f64; 5] = [
136 2.568520192289822,
137 1.8729528499234604,
138 0.5279051029514285,
139 0.06051834131244132,
140 0.0023352049762686918,
141];
142
143fn cody_erf_small(x: f64) -> f64 {
145 let z = x * x;
146 let mut xnum = CODY_A[4] * z;
147 let mut xden = z;
148 for i in 0..3 {
149 xnum = (xnum + CODY_A[i]) * z;
150 xden = (xden + CODY_B[i]) * z;
151 }
152 x * (xnum + CODY_A[3]) / (xden + CODY_B[3])
153}
154
155fn cody_erfc_pos(y: f64) -> f64 {
161 if y >= CODY_XBIG {
162 return 0.0;
163 }
164
165 let result = if y <= 4.0 {
166 let mut xnum = CODY_C[8] * y;
167 let mut xden = y;
168 for i in 0..7 {
169 xnum = (xnum + CODY_C[i]) * y;
170 xden = (xden + CODY_D[i]) * y;
171 }
172 (xnum + CODY_C[7]) / (xden + CODY_D[7])
173 } else {
174 let z = 1.0 / (y * y);
175 let mut xnum = CODY_P[5] * z;
176 let mut xden = z;
177 for i in 0..4 {
178 xnum = (xnum + CODY_P[i]) * z;
179 xden = (xden + CODY_Q[i]) * z;
180 }
181 let r = z * (xnum + CODY_P[4]) / (xden + CODY_Q[4]);
182 (SQRPI - r) / y
183 };
184
185 let ysq = (y * 16.0).trunc() / 16.0;
187 let del = (y - ysq) * (y + ysq);
188 (-ysq * ysq).exp() * (-del).exp() * result
189}
190
191#[must_use]
204pub fn erf_precise(x: f64) -> f64 {
205 if x.is_nan() {
206 return x;
207 }
208 let y = x.abs();
209 if y <= CODY_THRESH {
210 return cody_erf_small(x);
211 }
212 let v = 1.0 - cody_erfc_pos(y);
213 if x < 0.0 { -v } else { v }
214}
215
216#[must_use]
231pub fn erfc_precise(x: f64) -> f64 {
232 if x.is_nan() {
233 return x;
234 }
235 let y = x.abs();
236 if y <= CODY_THRESH {
237 return 1.0 - cody_erf_small(x);
238 }
239 if x > 0.0 {
240 cody_erfc_pos(y)
241 } else {
242 2.0 - cody_erfc_pos(y)
243 }
244}
245
246#[must_use]
263pub fn std_dev(samples: &[f64]) -> f64 {
264 if samples.len() < 2 {
265 return 0.0;
266 }
267 let n = samples.len() as f64;
268 let mean = samples.iter().sum::<f64>() / n;
269 let variance = samples.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (n - 1.0);
270 variance.sqrt()
271}
272
273#[must_use]
277pub fn std_dev_f32(samples: &[f32]) -> f32 {
278 if samples.len() < 2 {
279 return 0.0;
280 }
281 let n = samples.len() as f32;
282 let mean = samples.iter().sum::<f32>() / n;
283 let variance = samples.iter().map(|x| (x - mean).powi(2)).sum::<f32>() / (n - 1.0);
284 variance.sqrt()
285}
286
287#[must_use]
291pub fn std_dev_with_mean(samples: &[f64], mean: f64) -> f64 {
292 if samples.len() < 2 {
293 return 0.0;
294 }
295 let variance =
296 samples.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (samples.len() - 1) as f64;
297 variance.sqrt()
298}
299
300#[must_use]
302pub fn std_dev_f32_with_mean(samples: &[f32], mean: f32) -> f32 {
303 if samples.len() < 2 {
304 return 0.0;
305 }
306 let variance =
307 samples.iter().map(|x| (x - mean).powi(2)).sum::<f32>() / (samples.len() - 1) as f32;
308 variance.sqrt()
309}
310
311#[must_use]
330pub fn cosine_similarity_f32(a: &[f32], b: &[f32]) -> f32 {
331 let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
332 let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
333 let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
334
335 if norm_a == 0.0 || norm_b == 0.0 {
336 return 0.0;
337 }
338 dot / (norm_a * norm_b)
339}
340
341#[must_use]
345pub fn cosine_similarity(a: &[f64], b: &[f64]) -> f64 {
346 let dot: f64 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
347 let norm_a: f64 = a.iter().map(|x| x * x).sum::<f64>().sqrt();
348 let norm_b: f64 = b.iter().map(|x| x * x).sum::<f64>().sqrt();
349
350 if norm_a == 0.0 || norm_b == 0.0 {
351 return 0.0;
352 }
353 dot / (norm_a * norm_b)
354}
355
356#[must_use]
372pub fn usage_percent(used: u64, total: u64) -> f64 {
373 if total == 0 {
374 return 0.0;
375 }
376 (used as f64 / total as f64) * 100.0
377}
378
379#[cfg(test)]
384mod tests {
385 use super::*;
386
387 #[test]
390 fn test_erf_zero() {
391 assert!((erf(0.0) - 0.0).abs() < 1e-6);
392 }
393
394 #[test]
395 fn test_erf_positive() {
396 assert!((erf(1.0) - 0.842_700_793).abs() < 1e-6);
397 }
398
399 #[test]
400 fn test_erf_negative_symmetry() {
401 assert!((erf(-1.0) + erf(1.0)).abs() < 1e-10);
402 }
403
404 fn oracle_erf_series(t: f64) -> f64 {
411 let mut u = t;
412 let mut sum = t;
413 let mut n = 1.0_f64;
414 while n <= 200.0 {
415 u *= -(t * t) / n;
416 let add = u / (2.0 * n + 1.0);
417 sum += add;
418 if add == 0.0 || add.abs() < 1e-18 * sum.abs() {
419 break;
420 }
421 n += 1.0;
422 }
423 sum * 2.0 / std::f64::consts::PI.sqrt()
424 }
425
426 fn oracle_erfc_cf(t: f64) -> f64 {
427 let mut cf = 0.0_f64;
428 let mut k = 80_i32;
429 while k >= 1 {
430 cf = (f64::from(k) / 2.0) / (t + cf);
431 k -= 1;
432 }
433 (-t * t).exp() / std::f64::consts::PI.sqrt() / (t + cf)
434 }
435
436 fn oracle_erf(t: f64) -> f64 {
437 let a = t.abs();
438 let v = if a <= 2.0 {
439 oracle_erf_series(a)
440 } else {
441 1.0 - oracle_erfc_cf(a)
442 };
443 if t < 0.0 { -v } else { v }
444 }
445
446 #[test]
447 fn erf_precise_matches_independent_oracle_across_the_range() {
448 let mut worst = 0.0_f64;
449 let mut worst_at = 0.0_f64;
450 for i in 0..=1200 {
451 let x = -6.0 + 0.01 * f64::from(i);
452 let d = (erf_precise(x) - oracle_erf(x)).abs();
453 if d > worst {
454 worst = d;
455 worst_at = x;
456 }
457 }
458 assert!(
459 worst < 1e-14,
460 "erf_precise deviates from the independent oracle by {worst:.3e} at x={worst_at}"
461 );
462 }
463
464 #[test]
465 fn erf_precise_is_far_more_accurate_than_the_abramowitz_stegun_erf() {
466 let mut worst_as = 0.0_f64;
468 let mut worst_precise = 0.0_f64;
469 for i in 0..=1200 {
470 let x = -6.0 + 0.01 * f64::from(i);
471 let want = oracle_erf(x);
472 worst_as = worst_as.max((erf(x) - want).abs());
473 worst_precise = worst_precise.max((erf_precise(x) - want).abs());
474 }
475 assert!(
476 worst_as > 1e-9,
477 "A&S erf unexpectedly accurate ({worst_as:.3e}) — the premise for erf_precise changed"
478 );
479 assert!(
480 worst_precise * 1e6 < worst_as,
481 "erf_precise (worst {worst_precise:.3e}) must be orders of magnitude better \
482 than A&S erf (worst {worst_as:.3e})"
483 );
484 }
485
486 #[test]
487 fn erfc_precise_keeps_relative_accuracy_in_the_far_tail() {
488 for &(x, want) in &[
490 (2.0_f64, 4.677_734_981_047_265e-3_f64),
491 (3.0, 2.209_049_699_858_544e-5),
492 (4.0, 1.541_725_790_028_002_6e-8),
493 (5.0, 1.537_459_794_428_035_4e-12),
494 ] {
495 let got = erfc_precise(x);
496 let rel = (got - want).abs() / want;
497 assert!(
498 rel < 1e-13,
499 "erfc_precise({x}) = {got:e}, expected {want:e} (rel {rel:.3e})"
500 );
501 }
502 }
503
504 #[test]
505 fn erfc_precise_and_erf_precise_are_consistent_and_symmetric() {
506 for i in 0..=120 {
507 let x = -6.0 + 0.1 * f64::from(i);
508 assert!(
509 (erfc_precise(x) - (1.0 - erf_precise(x))).abs() < 1e-14,
510 "erfc_precise({x}) inconsistent with 1 - erf_precise({x})"
511 );
512 assert!(
513 (erf_precise(-x) + erf_precise(x)).abs() < 1e-15,
514 "erf_precise must be odd, failed at {x}"
515 );
516 }
517 assert_eq!(erf_precise(0.0), 0.0);
518 assert!((erfc_precise(0.0) - 1.0).abs() < 1e-15);
519 assert!(erf_precise(f64::NAN).is_nan());
520 assert!(erfc_precise(f64::NAN).is_nan());
521 assert_eq!(erfc_precise(30.0), 0.0, "erfc underflows to 0 past XBIG");
522 }
523
524 #[test]
525 fn test_erf_large() {
526 assert!((erf(5.0) - 1.0).abs() < 1e-6);
527 }
528
529 #[test]
530 fn test_erf_f32_matches() {
531 let f32_val = erf_f32(1.0_f32);
532 let f64_val = erf(1.0) as f32;
533 assert!((f32_val - f64_val).abs() < 1e-6);
534 }
535
536 #[test]
539 fn test_std_dev_known_value() {
540 let data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
543 assert!((std_dev(&data) - 2.138).abs() < 0.01);
544 }
545
546 #[test]
547 fn test_std_dev_single_element() {
548 assert_eq!(std_dev(&[42.0]), 0.0);
549 }
550
551 #[test]
552 fn test_std_dev_empty() {
553 assert_eq!(std_dev(&[]), 0.0);
554 }
555
556 #[test]
557 fn test_std_dev_identical_values() {
558 assert_eq!(std_dev(&[5.0, 5.0, 5.0, 5.0]), 0.0);
559 }
560
561 #[test]
562 fn test_std_dev_f32() {
563 let data: Vec<f32> = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
564 assert!((std_dev_f32(&data) - 2.138).abs() < 0.02);
565 }
566
567 #[test]
568 fn test_std_dev_with_mean_matches() {
569 let data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
570 let mean = data.iter().sum::<f64>() / data.len() as f64;
571 let sd1 = std_dev(&data);
572 let sd2 = std_dev_with_mean(&data, mean);
573 assert!((sd1 - sd2).abs() < 1e-10);
574 }
575
576 #[test]
579 fn test_cosine_identical() {
580 let a = [1.0, 2.0, 3.0];
581 assert!((cosine_similarity(&a, &a) - 1.0).abs() < 1e-10);
582 }
583
584 #[test]
585 fn test_cosine_orthogonal() {
586 let a = [1.0, 0.0, 0.0];
587 let b = [0.0, 1.0, 0.0];
588 assert!(cosine_similarity(&a, &b).abs() < 1e-10);
589 }
590
591 #[test]
592 fn test_cosine_opposite() {
593 let a = [1.0, 0.0];
594 let b = [-1.0, 0.0];
595 assert!((cosine_similarity(&a, &b) + 1.0).abs() < 1e-10);
596 }
597
598 #[test]
599 fn test_cosine_zero_vector() {
600 let a = [0.0, 0.0, 0.0];
601 let b = [1.0, 2.0, 3.0];
602 assert_eq!(cosine_similarity(&a, &b), 0.0);
603 }
604
605 #[test]
606 fn test_cosine_f32() {
607 let a = [1.0f32, 0.0, 0.0];
608 let b = [0.0f32, 1.0, 0.0];
609 assert!(cosine_similarity_f32(&a, &b).abs() < 1e-6);
610 }
611
612 #[test]
615 fn test_usage_percent_normal() {
616 assert!((usage_percent(750, 1000) - 75.0).abs() < 1e-10);
617 }
618
619 #[test]
620 fn test_usage_percent_zero_total() {
621 assert_eq!(usage_percent(0, 0), 0.0);
622 }
623
624 #[test]
625 fn test_usage_percent_full() {
626 assert!((usage_percent(1000, 1000) - 100.0).abs() < 1e-10);
627 }
628
629 #[test]
630 fn test_usage_percent_empty() {
631 assert!((usage_percent(0, 1000) - 0.0).abs() < 1e-10);
632 }
633}