Skip to main content

sciforge_hub/tools/
utils.rs

1//! Numeric utilities: scientific / SI formatting, `linspace`, `logspace`,
2//! normalization, moving average, and approximate equality.
3
4/// Formats `value` in scientific notation with `precision` decimal places.
5pub fn format_scientific(value: f64, precision: usize) -> String {
6    if value == 0.0 {
7        return "0.0".to_string();
8    }
9    let exp = value.abs().log10().floor() as i32;
10    let mantissa = value / 10f64.powi(exp);
11    format!("{mantissa:.precision$}e{exp}")
12}
13
14/// Formats `value` with an SI prefix (k, M, G, … or m, µ, n, …).
15pub fn format_si(value: f64) -> String {
16    let prefixes = [
17        (1e24, "Y"),
18        (1e21, "Z"),
19        (1e18, "E"),
20        (1e15, "P"),
21        (1e12, "T"),
22        (1e9, "G"),
23        (1e6, "M"),
24        (1e3, "k"),
25        (1.0, ""),
26        (1e-3, "m"),
27        (1e-6, "µ"),
28        (1e-9, "n"),
29        (1e-12, "p"),
30        (1e-15, "f"),
31        (1e-18, "a"),
32    ];
33    let abs = value.abs();
34    for &(threshold, prefix) in &prefixes {
35        if abs >= threshold {
36            return format!("{:.3}{prefix}", value / threshold);
37        }
38    }
39    format_scientific(value, 3)
40}
41
42/// Generates `n` evenly spaced values between `start` and `end` inclusive.
43pub fn linspace(start: f64, end: f64, n: usize) -> Vec<f64> {
44    if n <= 1 {
45        return vec![start];
46    }
47    let step = (end - start) / (n - 1) as f64;
48    (0..n).map(|i| start + i as f64 * step).collect()
49}
50
51/// Generates `n` logarithmically spaced values from $10^{\text{start\_exp}}$ to $10^{\text{end\_exp}}$.
52pub fn logspace(start_exp: f64, end_exp: f64, n: usize) -> Vec<f64> {
53    linspace(start_exp, end_exp, n)
54        .into_iter()
55        .map(|e| 10f64.powf(e))
56        .collect()
57}
58
59/// Clamps each element of `data` to `[min, max]` in place.
60pub fn clamp_vec(data: &mut [f64], min: f64, max: f64) {
61    for v in data.iter_mut() {
62        *v = v.clamp(min, max);
63    }
64}
65
66/// Normalizes `data` to [0, 1] (min-max scaling).
67pub fn normalize(data: &[f64]) -> Vec<f64> {
68    let min = data.iter().copied().fold(f64::INFINITY, f64::min);
69    let max = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
70    let range = max - min;
71    if range == 0.0 {
72        return vec![0.0; data.len()];
73    }
74    data.iter().map(|&v| (v - min) / range).collect()
75}
76
77/// Returns the cumulative sum of `data`.
78pub fn cumulative_sum(data: &[f64]) -> Vec<f64> {
79    let mut result = Vec::with_capacity(data.len());
80    let mut sum = 0.0;
81    for &v in data {
82        sum += v;
83        result.push(sum);
84    }
85    result
86}
87
88/// Computes a centered moving average with a window of `window` elements.
89pub fn moving_average(data: &[f64], window: usize) -> Vec<f64> {
90    if window == 0 || data.is_empty() {
91        return data.to_vec();
92    }
93    let n = data.len();
94    (0..n)
95        .map(|i| {
96            let lo = i.saturating_sub(window / 2);
97            let hi = (i + window / 2 + 1).min(n);
98            data[lo..hi].iter().sum::<f64>() / (hi - lo) as f64
99        })
100        .collect()
101}
102
103/// Computes the relative error between `computed` and `exact`.
104pub fn relative_error(computed: f64, exact: f64) -> f64 {
105    if exact == 0.0 {
106        return computed.abs();
107    }
108    ((computed - exact) / exact).abs()
109}
110
111/// Returns `true` if `a` and `b` are equal within `tolerance`.
112pub fn approx_equal(a: f64, b: f64, tolerance: f64) -> bool {
113    (a - b).abs() <= tolerance
114}