Skip to main content

concinnity_core/math/
scalar.rs

1// The f32 transcendentals `core` does not carry, forwarded to libm.
2//
3// Free functions rather than an extension trait: `cargo test` links the test
4// harness against std, whose inherent f32 methods win method resolution over
5// any trait, so a trait would go unused in exactly the build that checks it.
6
7/// Square root.
8pub fn sqrt(x: f32) -> f32 {
9    libm::sqrtf(x)
10}
11
12/// Length of the hypotenuse of a right triangle with legs `x` and `y`,
13/// without the intermediate overflow of `sqrt(x * x + y * y)`.
14pub fn hypot(x: f32, y: f32) -> f32 {
15    libm::hypotf(x, y)
16}
17
18/// Sine of an angle in radians.
19pub fn sin(x: f32) -> f32 {
20    libm::sinf(x)
21}
22
23/// Cosine of an angle in radians.
24pub fn cos(x: f32) -> f32 {
25    libm::cosf(x)
26}
27
28/// Sine and cosine of an angle in radians.
29pub fn sin_cos(x: f32) -> (f32, f32) {
30    libm::sincosf(x)
31}
32
33/// Tangent of an angle in radians.
34pub fn tan(x: f32) -> f32 {
35    libm::tanf(x)
36}
37
38/// Arc sine, in radians.
39pub fn asin(x: f32) -> f32 {
40    libm::asinf(x)
41}
42
43/// Arc cosine, in radians.
44pub fn acos(x: f32) -> f32 {
45    libm::acosf(x)
46}
47
48/// Arc tangent of `y / x`, in radians, using both signs to pick the quadrant.
49pub fn atan2(y: f32, x: f32) -> f32 {
50    libm::atan2f(y, x)
51}
52
53/// Largest integer at or below `x`.
54pub fn floor(x: f32) -> f32 {
55    libm::floorf(x)
56}
57
58/// Smallest integer at or above `x`.
59pub fn ceil(x: f32) -> f32 {
60    libm::ceilf(x)
61}
62
63/// Nearest integer, with halves rounded away from zero.
64pub fn round(x: f32) -> f32 {
65    libm::roundf(x)
66}
67
68/// Integer part, discarding the fraction and keeping the sign of `x`.
69pub fn trunc(x: f32) -> f32 {
70    libm::truncf(x)
71}
72
73/// Fractional part, keeping the sign of `x`.
74pub fn fract(x: f32) -> f32 {
75    x - trunc(x)
76}
77
78/// `e` raised to `x`.
79pub fn exp(x: f32) -> f32 {
80    libm::expf(x)
81}
82
83/// 2 raised to `x`.
84pub fn exp2(x: f32) -> f32 {
85    libm::exp2f(x)
86}
87
88/// Natural logarithm.
89pub fn ln(x: f32) -> f32 {
90    libm::logf(x)
91}
92
93/// Base-2 logarithm.
94pub fn log2(x: f32) -> f32 {
95    libm::log2f(x)
96}
97
98/// `x` raised to `n`.
99pub fn powf(x: f32, n: f32) -> f32 {
100    libm::powf(x, n)
101}
102
103/// `x` raised to the integer power `n`, by squaring. Matches the expansion
104/// `f32::powi` lowers to rather than routing through [`powf`], which would
105/// round differently.
106pub fn powi(x: f32, n: i32) -> f32 {
107    let mut base = x;
108    let mut exp = n;
109    let mut acc = 1.0;
110    loop {
111        if exp & 1 != 0 {
112            acc *= base;
113        }
114        // Truncating division walks the magnitude's bits for a negative `n`
115        // too, so the reciprocal below is the only place the sign is read.
116        exp /= 2;
117        if exp == 0 {
118            break;
119        }
120        base *= base;
121    }
122    if n < 0 { 1.0 / acc } else { acc }
123}
124
125/// `x * y + z` with a single rounding.
126pub fn mul_add(x: f32, y: f32, z: f32) -> f32 {
127    libm::fmaf(x, y, z)
128}
129
130/// Least nonnegative remainder of `x (mod rhs)`.
131pub fn rem_euclid(x: f32, rhs: f32) -> f32 {
132    let r = libm::fmodf(x, rhs);
133    if r < 0.0 { r + libm::fabsf(rhs) } else { r }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    // Each must agree with the std implementation it stands in for: the compute
141    // crate above this one is std-linked and calls std's version on the same
142    // values, so a divergence would be a seam between the two halves of a split
143    // that is supposed to be behaviour-preserving.
144    #[track_caller]
145    fn approx(got: f32, want: f32) {
146        assert!((got - want).abs() < 1e-6, "got {got}, want {want}");
147    }
148
149    // Relative form, for the functions whose range runs well past the absolute
150    // tolerance above (powi of a large base, hypot of large legs).
151    #[track_caller]
152    fn approx_rel(got: f32, want: f32) {
153        let scale = want.abs().max(1.0);
154        assert!((got - want).abs() <= 1e-6 * scale, "got {got}, want {want}");
155    }
156
157    #[test]
158    fn transcendentals_match_std() {
159        for &x in &[0.0f32, 0.5, 1.0, 2.5, 7.0] {
160            approx(sqrt(x), f32::sqrt(x));
161            approx(exp(x), f32::exp(x));
162            approx(exp2(x), f32::exp2(x));
163            approx(powf(x, 1.5), f32::powf(x, 1.5));
164            approx_rel(hypot(x, 3.0), f32::hypot(x, 3.0));
165        }
166        for &x in &[0.25f32, 0.5, 1.0, 2.5, 7.0, 1000.0] {
167            approx(ln(x), f32::ln(x));
168            approx(log2(x), f32::log2(x));
169        }
170        for &x in &[-2.5f32, -0.75, 0.0, 0.3, 1.2, 3.0] {
171            approx(sin(x), f32::sin(x));
172            approx(cos(x), f32::cos(x));
173            approx(tan(x), f32::tan(x));
174            approx(floor(x), f32::floor(x));
175            approx(ceil(x), f32::ceil(x));
176            approx(round(x), f32::round(x));
177            approx(trunc(x), f32::trunc(x));
178            approx(fract(x), f32::fract(x));
179            approx(atan2(x, 2.0), f32::atan2(x, 2.0));
180            approx(mul_add(x, 2.5, -1.25), f32::mul_add(x, 2.5, -1.25));
181            let (s, c) = sin_cos(x);
182            approx(s, f32::sin(x));
183            approx(c, f32::cos(x));
184        }
185        for &x in &[-1.0f32, -0.5, 0.0, 0.5, 1.0] {
186            approx(asin(x), f32::asin(x));
187            approx(acos(x), f32::acos(x));
188        }
189    }
190
191    // The rounding family splits on the half and on the sign, which is exactly
192    // where floor / ceil / round / trunc stop agreeing with one another.
193    #[test]
194    fn rounding_matches_std_at_the_halves_and_across_signs() {
195        for &x in &[-2.5f32, -1.5, -0.5, -0.25, 0.0, 0.25, 0.5, 1.5, 2.5] {
196            approx(floor(x), f32::floor(x));
197            approx(ceil(x), f32::ceil(x));
198            approx(round(x), f32::round(x));
199            approx(trunc(x), f32::trunc(x));
200            approx(fract(x), f32::fract(x));
201        }
202    }
203
204    // powi has no libm counterpart, so the squaring loop is ours: it must agree
205    // with std's across both signs of the exponent and at the zero exponent,
206    // where the accumulator alone decides the answer.
207    #[test]
208    fn powi_matches_std_across_exponent_signs() {
209        for &x in &[-3.0f32, -0.5, 0.5, 1.0, 2.0, 7.5] {
210            for n in -6i32..=6 {
211                approx_rel(powi(x, n), f32::powi(x, n));
212            }
213        }
214        assert_eq!(powi(0.0, 0), 1.0);
215        assert_eq!(powi(5.0, 1), 5.0);
216    }
217
218    // mul_add must round once, not twice. `(2^23 + 1)^2` needs 47 bits, so the
219    // rounded product loses the trailing 1 and the unfused expression cancels
220    // to zero; only the fused form keeps it.
221    #[test]
222    fn mul_add_rounds_once() {
223        let x = 8_388_609.0f32;
224        let z = -(x * x);
225        assert_eq!(mul_add(x, x, z), f32::mul_add(x, x, z));
226        assert_eq!(mul_add(x, x, z), 1.0);
227        assert_eq!(x * x + z, 0.0);
228    }
229
230    // The one function with no libm counterpart, so the formula is ours: a
231    // negative dividend must come back in [0, |rhs|), not negative like fmod.
232    #[test]
233    fn rem_euclid_matches_std_across_signs() {
234        for &(a, b) in &[
235            (7.5f32, 2.0f32),
236            (-7.5, 2.0),
237            (7.5, -2.0),
238            (-7.5, -2.0),
239            (0.0, 3.0),
240            (-0.25, 1.0),
241        ] {
242            approx(rem_euclid(a, b), f32::rem_euclid(a, b));
243            assert!(rem_euclid(a, b) >= 0.0, "{a} rem_euclid {b}");
244        }
245    }
246}