Skip to main content

kinavis_kernel/
math.rs

1//! Floating-point primitives routed to `std` or `libm`.
2//!
3//! Routing every transcendental call through this module lets the crate build
4//! as `no_std`: with `default-features = false, features = ["libm"]` the same
5//! code links against pure-Rust `libm`.
6//!
7//! Public because the rule applies to the whole crate family: magnetic models
8//! and estimators call these and thereby build for bare metal and produce the
9//! same results on every target. Also provides the conversions and guarded
10//! casts numeric code needs.
11
12#[cfg(feature = "std")]
13mod imp {
14    pub(crate) fn sin(x: f64) -> f64 {
15        x.sin()
16    }
17    pub(crate) fn cos(x: f64) -> f64 {
18        x.cos()
19    }
20    pub(crate) fn asin(x: f64) -> f64 {
21        x.asin()
22    }
23    pub(crate) fn atan2(y: f64, x: f64) -> f64 {
24        y.atan2(x)
25    }
26    pub(crate) fn sqrt(x: f64) -> f64 {
27        x.sqrt()
28    }
29    pub(crate) fn abs(x: f64) -> f64 {
30        x.abs()
31    }
32    pub(crate) fn copysign(magnitude: f64, sign: f64) -> f64 {
33        magnitude.copysign(sign)
34    }
35    pub(crate) fn tan(x: f64) -> f64 {
36        x.tan()
37    }
38    pub(crate) fn atan(x: f64) -> f64 {
39        x.atan()
40    }
41    pub(crate) fn acos(x: f64) -> f64 {
42        x.acos()
43    }
44    pub(crate) fn ln(x: f64) -> f64 {
45        x.ln()
46    }
47    pub(crate) fn exp(x: f64) -> f64 {
48        x.exp()
49    }
50    pub(crate) fn hypot(x: f64, y: f64) -> f64 {
51        x.hypot(y)
52    }
53    pub(crate) fn round(x: f64) -> f64 {
54        x.round()
55    }
56    pub(crate) fn ceil(x: f64) -> f64 {
57        x.ceil()
58    }
59    pub(crate) fn trunc(x: f64) -> f64 {
60        x.trunc()
61    }
62}
63
64#[cfg(all(not(feature = "std"), feature = "libm"))]
65mod imp {
66    pub(crate) fn sin(x: f64) -> f64 {
67        libm::sin(x)
68    }
69    pub(crate) fn cos(x: f64) -> f64 {
70        libm::cos(x)
71    }
72    pub(crate) fn asin(x: f64) -> f64 {
73        libm::asin(x)
74    }
75    pub(crate) fn atan2(y: f64, x: f64) -> f64 {
76        libm::atan2(y, x)
77    }
78    pub(crate) fn sqrt(x: f64) -> f64 {
79        libm::sqrt(x)
80    }
81    pub(crate) fn abs(x: f64) -> f64 {
82        libm::fabs(x)
83    }
84    pub(crate) fn copysign(magnitude: f64, sign: f64) -> f64 {
85        libm::copysign(magnitude, sign)
86    }
87    pub(crate) fn tan(x: f64) -> f64 {
88        libm::tan(x)
89    }
90    pub(crate) fn atan(x: f64) -> f64 {
91        libm::atan(x)
92    }
93    pub(crate) fn acos(x: f64) -> f64 {
94        libm::acos(x)
95    }
96    pub(crate) fn ln(x: f64) -> f64 {
97        libm::log(x)
98    }
99    pub(crate) fn exp(x: f64) -> f64 {
100        libm::exp(x)
101    }
102    pub(crate) fn hypot(x: f64, y: f64) -> f64 {
103        libm::hypot(x, y)
104    }
105    pub(crate) fn round(x: f64) -> f64 {
106        libm::round(x)
107    }
108    pub(crate) fn ceil(x: f64) -> f64 {
109        libm::ceil(x)
110    }
111    pub(crate) fn trunc(x: f64) -> f64 {
112        libm::trunc(x)
113    }
114}
115
116#[cfg(not(any(feature = "std", feature = "libm")))]
117compile_error!(
118    "kinavis-kernel needs floating point math: enable the default `std` feature, \
119     or build with `--no-default-features --features libm` for `no_std` targets"
120);
121
122// One function per primitive, delegating to the feature-selected
123// implementation. Downstream code calls these, never `f64` methods, so a stray
124// `x.sin()` cannot break `no_std` builds.
125
126/// Sine, radians.
127#[must_use]
128#[inline]
129pub fn sin(x: f64) -> f64 {
130    imp::sin(x)
131}
132
133/// Cosine, radians.
134#[must_use]
135#[inline]
136pub fn cos(x: f64) -> f64 {
137    imp::cos(x)
138}
139
140/// Tangent, radians.
141#[must_use]
142#[inline]
143pub fn tan(x: f64) -> f64 {
144    imp::tan(x)
145}
146
147/// Arcsine in `[-π/2, π/2]`; `NaN` outside `[-1, 1]`.
148#[must_use]
149#[inline]
150pub fn asin(x: f64) -> f64 {
151    imp::asin(x)
152}
153
154/// Arccosine in `[0, π]`; `NaN` outside `[-1, 1]`.
155#[must_use]
156#[inline]
157pub fn acos(x: f64) -> f64 {
158    imp::acos(x)
159}
160
161/// Arctangent in `[-π/2, π/2]`.
162#[must_use]
163#[inline]
164pub fn atan(x: f64) -> f64 {
165    imp::atan(x)
166}
167
168/// Four-quadrant arctangent of `y / x`, in `[-π, π]`.
169///
170/// Mathematical argument order `atan2(y, x)`: for a course from north/east
171/// components, pass east first.
172#[must_use]
173#[inline]
174pub fn atan2(y: f64, x: f64) -> f64 {
175    imp::atan2(y, x)
176}
177
178/// Square root; `NaN` for negative input.
179#[must_use]
180#[inline]
181pub fn sqrt(x: f64) -> f64 {
182    imp::sqrt(x)
183}
184
185/// Absolute value.
186#[must_use]
187#[inline]
188pub fn abs(x: f64) -> f64 {
189    imp::abs(x)
190}
191
192/// `magnitude` with the sign of `sign`.
193#[must_use]
194#[inline]
195pub fn copysign(magnitude: f64, sign: f64) -> f64 {
196    imp::copysign(magnitude, sign)
197}
198
199/// Natural logarithm; `NaN` for negative input, `-∞` for zero.
200#[must_use]
201#[inline]
202pub fn ln(x: f64) -> f64 {
203    imp::ln(x)
204}
205
206/// `eˣ`.
207#[must_use]
208#[inline]
209pub fn exp(x: f64) -> f64 {
210    imp::exp(x)
211}
212
213/// `√(x² + y²)` without intermediate overflow.
214#[must_use]
215#[inline]
216pub fn hypot(x: f64, y: f64) -> f64 {
217    imp::hypot(x, y)
218}
219
220/// Round to nearest, halves away from zero.
221#[must_use]
222#[inline]
223pub fn round(x: f64) -> f64 {
224    imp::round(x)
225}
226
227/// Ceiling.
228#[must_use]
229#[inline]
230pub fn ceil(x: f64) -> f64 {
231    imp::ceil(x)
232}
233
234/// Truncate towards zero.
235#[must_use]
236#[inline]
237pub fn trunc(x: f64) -> f64 {
238    imp::trunc(x)
239}
240
241/// Degrees per radian (`to_radians` without `std`).
242const DEGREES_PER_RADIAN: f64 = 180.0 / core::f64::consts::PI;
243
244/// Degrees to radians.
245#[must_use]
246pub fn to_radians(degrees: f64) -> f64 {
247    degrees / DEGREES_PER_RADIAN
248}
249
250/// Radians to degrees.
251#[must_use]
252pub fn to_degrees(radians: f64) -> f64 {
253    radians * DEGREES_PER_RADIAN
254}
255
256/// Whether a value is an integer.
257///
258/// Exact comparison is intended: the question is whether there is any
259/// fractional part.
260#[allow(clippy::float_cmp)]
261#[must_use]
262pub fn is_integral(value: f64) -> bool {
263    value == trunc(value)
264}
265
266/// Relative tolerance for comparing computed results.
267///
268/// Larger than `f64::EPSILON`: chained transcendental calls lose several ULP,
269/// and cancellation leaves a residue proportional to the operands.
270pub const RELATIVE_TOLERANCE: f64 = 1e-12;
271
272/// Whether a value is zero relative to `scale`, the magnitude of its operands.
273///
274/// Two cancelling 5 kn vectors leave a residue well above `f64::EPSILON` that
275/// still means "stationary"; the same residue from 5000 kn vectors is noise of
276/// another order. No fixed constant handles both.
277#[must_use]
278pub fn is_effectively_zero(value: f64, scale: f64) -> bool {
279    abs(value) <= abs(scale) * RELATIVE_TOLERANCE
280}
281
282/// Rounds a value known to be within `i32` range. Callers pass angles bounded
283/// by 360.
284#[allow(clippy::cast_possible_truncation)]
285#[must_use]
286pub fn round_to_i32(value: f64) -> i32 {
287    let rounded = round(value);
288    if rounded > f64::from(i32::MAX) || rounded < f64::from(i32::MIN) {
289        return 0;
290    }
291    rounded as i32
292}
293
294/// Truncates a small non-negative value to `usize`. Callers bound it first;
295/// out-of-range input yields zero.
296#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
297#[must_use]
298pub fn to_usize(value: f64) -> usize {
299    // NaN fails the range test and hits the guard.
300    if !(0.0..=1e9).contains(&value) {
301        return 0;
302    }
303    value as usize
304}
305
306/// Count as `f64`. Counts here are far below 2^53, so the conversion is exact.
307#[allow(clippy::cast_precision_loss)]
308#[must_use]
309pub fn count_to_f64(count: usize) -> f64 {
310    count as f64
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn radian_conversion_round_trips() {
319        for degrees in [0.0, 1.0, 45.0, 90.0, 180.0, 359.9] {
320            let back = to_degrees(to_radians(degrees));
321            assert!((back - degrees).abs() < 1e-12);
322        }
323    }
324
325    #[test]
326    fn trig_matches_known_values() {
327        assert!(abs(sin(to_radians(90.0)) - 1.0) < 1e-12);
328        assert!(abs(cos(to_radians(180.0)) + 1.0) < 1e-12);
329        assert!(abs(to_degrees(atan2(1.0, 0.0)) - 90.0) < 1e-12);
330        assert!(abs(hypot(3.0, 4.0) - 5.0) < 1e-12);
331        assert!(abs(sqrt(9.0) - 3.0) < 1e-12);
332        assert!(abs(to_degrees(asin(0.5)) - 30.0) < 1e-12);
333    }
334}