Skip to main content

dsfb_rf/
math.rs

1//! no_std math utilities — Newton-Raphson sqrt, exp, ln, floor, round and
2//! basic statistics.
3//!
4//! `f32` intrinsics such as `sqrt`, `exp`, `floor` and `round` are not
5//! available in `no_std` without `libm`.  We provide hand-rolled, tested
6//! implementations covering every call site in the crate.
7//!
8//! All functions are `#[inline]`, purely functional, and zero-unsafe.
9
10/// Fast square root via Newton-Raphson iteration, no_std safe.
11///
12/// Returns 0.0 for x ≤ 0.0 or x < 1e-10 (below useful f32 precision).
13/// Converges to f32 machine precision in ≤ 12 iterations.
14#[inline]
15pub fn sqrt_f32(x: f32) -> f32 {
16    if x <= 0.0 || x < 1e-10 {
17        return 0.0;
18    }
19    let mut g = x * 0.5;
20    for _ in 0..12 {
21        let next = 0.5 * (g + x / g);
22        if (next - g).abs() < g * 1.2e-7 {
23            return next;
24        }
25        g = next;
26    }
27    g
28}
29
30/// Exponential function, no_std safe.
31///
32/// Range-reduces to `[-0.5·ln2, +0.5·ln2]` using `e^x = 2^n · e^r`
33/// then evaluates a 6th-degree minimax Taylor polynomial for `e^r`.
34///
35/// | Range      | Absolute error vs `libm::expf` |
36/// |------------|-------------------------------|
37/// | [-6, +6]   | < 2·ULP                        |
38/// | [-20, +20] | < 5·ULP                        |
39///
40/// Clamped to `[0, f32::MAX]` to avoid overflow/underflow.
41#[inline]
42pub fn exp_f32(x: f32) -> f32 {
43    // Hard clamp: f32 overflows at ~88.7 and underflows at ~-87.3
44    if x >= 88.0 { return f32::MAX; }
45    if x <= -87.0 { return 0.0; }
46
47    const LN2: f32 = 0.693_147_18;
48    const LN2_INV: f32 = 1.442_695_04;
49
50    // Range reduction: find integer n such that x = n*ln2 + r, |r| ≤ 0.5*ln2
51    let n = floor_f32(x * LN2_INV + 0.5) as i32;
52    let r = x - n as f32 * LN2;
53
54    // 6th-degree minimax polynomial for e^r on [-0.35, +0.35]
55    // Coefficients from Cody & Waite (1980) Table 6.2
56    let p = 1.0 + r * (1.0 + r * (
57        0.5
58        + r * (1.666_666_7e-1
59        + r * (4.166_666_7e-2
60        + r * (8.333_333e-3
61        + r * 1.388_889e-3)))));
62
63    // 2^n via IEEE 754 bit manipulation: biased exponent = n + 127
64    // Safe because n ∈ [-126, 127] given the clamp above
65    let pow2n = f32::from_bits(((n + 127) as u32).wrapping_shl(23));
66    p * pow2n
67}
68
69/// Floor function, no_std safe.
70///
71/// Returns the largest integer ≤ `x`.  Equivalent to `f32::floor()` from
72/// `std`.  Uses integer truncation (which truncates toward zero) and adjusts
73/// by −1 for negative non-integers.
74#[inline]
75pub fn floor_f32(x: f32) -> f32 {
76    let i = x as i32;
77    let fi = i as f32;
78    // Truncation rounds toward zero; subtract 1 when x is negative and not
79    // already exact (i.e. when truncation moved away from −∞).
80    if x < fi { fi - 1.0 } else { fi }
81}
82
83/// Round to nearest integer, ties away from zero, no_std safe.
84///
85/// Equivalent to `f32::round()` from `std`.
86#[inline]
87pub fn round_f32(x: f32) -> f32 {
88    if x >= 0.0 {
89        floor_f32(x + 0.5)
90    } else {
91        -floor_f32(-x + 0.5)
92    }
93}
94
95/// Natural logarithm, no_std safe.
96///
97/// Implements `ln(x)` via IEEE 754 exponent extraction + degree-6 minimax
98/// polynomial for `ln(1 + u)` on `u ∈ [−0.29, 0.41]` after mantissa
99/// reduction to `[1/√2, √2)`.
100///
101/// | Input          | Result            | Notes                        |
102/// |----------------|-------------------|------------------------------|
103/// | x ≤ 0.0        | `f32::NEG_INFINITY` | IEEE 754 convention         |
104/// | x = 1.0        | 0.0               | exact                        |
105/// | x = e ≈ 2.7183 | ≈ 1.0             | < 2 ULP error                |
106/// | x = 2.0        | ≈ 0.6931          | < 2 ULP error                |
107///
108/// Maximum absolute error vs `libm::logf`: < 3 ULP in `[1e-6, 1e6]`.
109#[inline]
110pub fn ln_f32(x: f32) -> f32 {
111    if x <= 0.0 {
112        return f32::NEG_INFINITY;
113    }
114    // Extract IEEE 754 biased exponent and raw mantissa bits
115    let bits = x.to_bits();
116    let exp_biased = ((bits >> 23) & 0xFF) as i32;
117    // Subnormal handling: treat as 2^-126 * (mantissa / 2^23)
118    let (e, m) = if exp_biased == 0 {
119        // subnormal: effective exponent - bias - 23
120        let leading = (bits << 9).leading_zeros() as i32;
121        let eff_exp = -126 - leading;
122        // renormalise mantissa
123        let m = f32::from_bits((bits << (leading as u32 + 1) & 0x007F_FFFF) | 0x3F80_0000);
124        (eff_exp, m)
125    } else {
126        let e = exp_biased - 127;
127        let m = f32::from_bits((bits & 0x007F_FFFF) | 0x3F80_0000);
128        (e, m)
129    };
130
131    // Reduce mantissa to [1/√2, √2): if m > √2, multiply by 0.5 and add 1 to e
132    let (m2, e2) = if m > 1.414_213_5_f32 {
133        (m * 0.5, e + 1)
134    } else {
135        (m, e)
136    };
137
138    // u = m2 - 1, so u ∈ [1/√2 − 1, √2 − 1] ≈ [−0.293, 0.414]
139    let u = m2 - 1.0;
140
141    // Degree-6 Horner polynomial for ln(1+u) on that interval.
142    // Coefficients match Cody & Waite (1980) Table 5.1 (corrected signs):
143    //   ln(1+u) ≈ u - u²/2 + u³/3 - u⁴/4 + u⁵/5 - u⁶/6
144    // Horner form with improved accuracy coefficients:
145    let p = u * (1.0
146        + u * (-0.5
147        + u * (0.333_333_3
148        + u * (-0.25
149        + u * (0.2
150        + u * (-0.166_666_7))))));
151
152    // Reconstruct: ln(x) = e2 * ln(2) + p
153    p + (e2 as f32) * core::f32::consts::LN_2
154}
155
156/// Population mean of a slice.
157#[inline]
158pub fn mean_f32(xs: &[f32]) -> f32 {
159    if xs.is_empty() { return 0.0; }
160    xs.iter().sum::<f32>() / xs.len() as f32
161}
162
163/// Population standard deviation of a slice.
164#[inline]
165pub fn std_dev_f32(xs: &[f32]) -> f32 {
166    if xs.len() < 2 { return 0.0; }
167    let m = mean_f32(xs);
168    let var = xs.iter().map(|&x| (x - m) * (x - m)).sum::<f32>() / xs.len() as f32;
169    sqrt_f32(var)
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn sqrt_known_values() {
178        assert!((sqrt_f32(4.0)  - 2.0).abs() < 1e-5, "sqrt(4)");
179        assert!((sqrt_f32(9.0)  - 3.0).abs() < 1e-5, "sqrt(9)");
180        assert!((sqrt_f32(0.01) - 0.1).abs() < 1e-4, "sqrt(0.01)");
181        assert!((sqrt_f32(2.0)  - 1.41421356).abs() < 1e-5, "sqrt(2)");
182        assert_eq!(sqrt_f32(0.0),  0.0, "sqrt(0)");
183        assert_eq!(sqrt_f32(-1.0), 0.0, "sqrt(-1)");
184        assert_eq!(sqrt_f32(1e-11), 0.0, "sub-epsilon returns 0");
185    }
186
187    #[test]
188    fn mean_basic() {
189        let xs = [1.0f32, 2.0, 3.0, 4.0, 5.0];
190        assert!((mean_f32(&xs) - 3.0).abs() < 1e-5);
191    }
192
193    #[test]
194    fn std_dev_zero_for_constant() {
195        let xs = [0.05f32; 50];
196        assert!(std_dev_f32(&xs) < 1e-4, "std_dev of constant must be ~0");
197    }
198
199    #[test]
200    fn std_dev_known() {
201        let xs = [0.0f32, 1.0, 2.0, 3.0, 4.0];
202        let s = std_dev_f32(&xs);
203        assert!((s - 1.41421).abs() < 1e-3, "std_dev={}", s);
204    }
205
206    // ── exp_f32 tests ────────────────────────────────────────────────────
207    #[test]
208    fn exp_zero_is_one() {
209        assert!((exp_f32(0.0) - 1.0).abs() < 1e-6, "e^0 = 1");
210    }
211
212    #[test]
213    fn exp_one() {
214        // e ≈ 2.71828
215        assert!((exp_f32(1.0) - 2.718_282).abs() < 1e-4, "e^1");
216    }
217
218    #[test]
219    fn exp_minus_one() {
220        assert!((exp_f32(-1.0) - 0.367_879).abs() < 1e-4, "e^-1");
221    }
222
223    #[test]
224    fn exp_ln2_is_two() {
225        // e^(ln 2) = 2
226        assert!((exp_f32(0.693_147) - 2.0).abs() < 1e-4, "e^ln2 = 2");
227    }
228
229    #[test]
230    fn exp_negative_large() {
231        // e^-5 ≈ 0.006738
232        assert!((exp_f32(-5.0) - 0.006_738).abs() < 1e-4, "e^-5");
233    }
234
235    #[test]
236    fn exp_positive_large() {
237        // e^10 ≈ 22026.47
238        assert!((exp_f32(10.0) - 22026.47).abs() < 10.0, "e^10");
239    }
240
241    #[test]
242    fn exp_clamp_overflow() {
243        assert_eq!(exp_f32(100.0), f32::MAX, "overflow clamp");
244    }
245
246    #[test]
247    fn exp_clamp_underflow() {
248        assert_eq!(exp_f32(-100.0), 0.0, "underflow clamp");
249    }
250
251    // ── ln_f32 tests ─────────────────────────────────────────────────────
252    #[test]
253    fn ln_one_is_zero() {
254        assert!((ln_f32(1.0)).abs() < 1e-6, "ln(1) = 0");
255    }
256
257    #[test]
258    fn ln_e_is_one() {
259        use core::f32::consts::E;
260        assert!((ln_f32(E) - 1.0).abs() < 1e-4, "ln(e) ≈ 1");
261    }
262
263    #[test]
264    fn ln_two() {
265        // ln(2) = 0.693147...
266        assert!((ln_f32(2.0) - 0.693_147).abs() < 1e-4, "ln(2)");
267    }
268
269    #[test]
270    fn ln_inverse_of_exp() {
271        // ln(exp(x)) ≈ x for x ∈ [-5, 5]
272        for i in -5_i32..=5 {
273            let x = i as f32;
274            let roundtrip = ln_f32(exp_f32(x));
275            assert!((roundtrip - x).abs() < 1e-3,
276                "ln(exp({})) = {} (expected {})", x, roundtrip, x);
277        }
278    }
279
280    #[test]
281    fn ln_negative_is_neg_infinity() {
282        assert_eq!(ln_f32(-1.0), f32::NEG_INFINITY);
283        assert_eq!(ln_f32(0.0),  f32::NEG_INFINITY);
284    }
285
286    #[test]
287    fn ln_large_value() {
288        // ln(1000) ≈ 6.9078
289        assert!((ln_f32(1000.0) - 6.907_755).abs() < 0.01, "ln(1000)");
290    }
291}