Skip to main content

fixed_math_taylor/
lib.rs

1// Copyright (C) 2026 Jorge Andre Castro
2// This program is free software: you can redistribute it and/or modify
3// it under the terms of the GNU General Public License as published by
4// the Free Software Foundation, either version 2 or the License, or
5// (at your option) any later version.
6
7#![no_std]
8#![forbid(unsafe_code)]
9
10//! # Fixed-Math-Taylor
11//!
12//! A high-performance fixed-point trigonometry library optimized for embedded systems and microcontrollers
13//! without a floating-point unit (FPU).
14//!
15//! ## Overview
16//!
17//! Fixed-Math-Taylor eliminates the overhead of software floating-point emulation on resource-constrained
18//! devices by implementing trigonometric functions using only integer arithmetic and bit shifts. Designed
19//! specifically for the RP2040 and similar microcontrollers, it provides multiple calculation engines
20//! with different precision/speed trade-offs.
21//!
22//! ## Features & Calculation Engines
23//!
24//! The library provides three distinct calculation engines, selectable via Cargo features:
25//!
26//! - **`lut`** (default): Lookup table with linear interpolation. Ultra-fast, highest precision (~0.1% error),
27//!   640 bytes Flash. Ideal for control loops, motor control, and audio applications.
28//! - **`taylor`**: Taylor series expansion (order 5). Pure algorithmic computation with high precision.
29//!   Smaller memory footprint, better for algorithms-only use cases.
30//! - **`fast-sin`**: Bhaskara I approximation. Medium precision with exceptional speed. Optimal for
31//!   animations and graphics.
32//!
33//! ## Number Format: Q15 Fixed-Point
34//!
35//! All functions return results in **Q15 format** (16-bit signed integer):
36//! - `32767` represents `+1.0`
37//! - `0` represents `0.0`
38//! - `-32768` represents approximately `-1.0`
39//!
40//! To convert to a human-readable float:
41//! ```rust,ignore
42//! let fixed_value: i16 = /* ... */;
43//! let float_value: f32 = (fixed_value as f32) / 32767.0;
44//! ```
45//!
46//! ## Angle Representation
47//!
48//! Angles are represented as `u16` values (0..=65535):
49//! - `0` = 0 radians
50//! - `16384` ≈ π/2
51//! - `32768` ≈ π
52//! - `65535` ≈ 2π
53//!
54//! ## Quick Start
55//!
56//! Add to your `Cargo.toml`:
57//!
58//! ```toml
59//! [dependencies]
60//! fixed-math-taylor = { version = "0.3", features = ["lut"] }
61//! ```
62//!
63//! Basic usage:
64//!
65//! ```rust,ignore
66//! use fixed_math_taylor::{sin_fixed, cos_fixed, Angle, Fixed};
67//!
68//! let angle: Angle = 16384; // π/2
69//! let sine_value: Fixed = sin_fixed(angle);
70//! let cosine_value: Fixed = cos_fixed(angle);
71//!
72//! // Convert to float for display
73//! println!("sin(π/2) ≈ {}", (sine_value as f32) / 32767.0);
74//! ```
75//!
76//! ## Performance Characteristics
77//!
78//! | Engine    | Speed         | Precision  | Flash Cost | Best For                    |
79//! |-----------|---------------|------------|------------|-----------------------------|
80//! | LUT       | Fastest       | ~0.1% err  | 640 bytes  | Control loops, motor ctrl   |
81//! | Taylor    | Fast          | High       | ~500 bytes | Pure algorithms             |
82//! | Fast-Sin  | Fastest       | Medium     | ~300 bytes | Graphics, animations        |
83//!
84//! ## no_std Environment
85//!
86//! This crate is `#![no_std]` compatible and suitable for embedded environments with no standard library.
87//! No dynamic allocations are performed.
88//!
89//! ## Dependencies
90//!
91//! Zero external dependencies. Pure Rust with platform-independent implementation.
92
93// --- TYPES DE BASE ---
94
95/// An angle in the range [0, 65535], representing a full rotation [0, 2π).
96///
97/// The mapping is:
98/// - `0` → 0 radians
99/// - `16384` → π/2
100/// - `32768` → π
101/// - `49152` → 3π/2
102/// - `65535` → ~2π
103///
104/// This representation allows fast angle arithmetic using only bit shifts
105/// and avoids floating-point operations entirely.
106pub type Angle = u16;
107
108/// A fixed-point number in Q15 format, suitable for storing results from trigonometric functions.
109///
110/// Q15 maps the range [-32768, 32767] to the mathematical range [-1.0, 1.0):
111/// - `32767` represents `+1.0`
112/// - `0` represents `0.0`
113/// - `-32768` represents approximately `-1.0`
114///
115/// To convert to a float: `(fixed_value as f32) / 32767.0`
116///
117/// This format is native to 16-bit integer arithmetic and matches the output
118/// of all trigonometric functions in this library.
119pub type Fixed = i16;
120
121// ==========================================
122// MOTEUR LUT (FEATURE "lut")
123// ==========================================
124#[cfg(feature = "lut")]
125mod lut_impl {
126    use super::{Angle, Fixed};
127    const QUADRANT_BITS: u32 = 14;
128    const LUT_SIZE: usize = 256;
129    const LUT_BITS: u32 = 8;
130    const LUT_MASK: u32 = (1 << (QUADRANT_BITS - LUT_BITS)) - 1;
131
132    // Inclusion de la table de sinus (0 à PI/2)
133    static SIN_LUT: [Fixed; 257] = include!("sin_table.rs.inc");
134
135    #[inline(always)]
136    pub fn sin_fixed(angle: Angle) -> Fixed {
137        let quadrant = (angle >> QUADRANT_BITS) as usize;
138        let idx = (angle & 0x3FFF) as u32;
139        let lut_idx = (idx >> (QUADRANT_BITS - LUT_BITS)) as usize;
140        let frac = (idx & LUT_MASK) as i32;
141
142        match quadrant {
143            0 => interpolate(SIN_LUT[lut_idx], SIN_LUT[lut_idx + 1], frac),
144            1 => interpolate(
145                SIN_LUT[LUT_SIZE - lut_idx],
146                SIN_LUT[LUT_SIZE - lut_idx - 1],
147                frac,
148            ),
149            2 => -interpolate(SIN_LUT[lut_idx], SIN_LUT[lut_idx + 1], frac),
150            _ => -interpolate(
151                SIN_LUT[LUT_SIZE - lut_idx],
152                SIN_LUT[LUT_SIZE - lut_idx - 1],
153                frac,
154            ),
155        }
156    }
157
158    #[inline(always)]
159    fn interpolate(y0: Fixed, y1: Fixed, frac: i32) -> Fixed {
160        let y0_32 = y0 as i32;
161        let y1_32 = y1 as i32;
162        (y0_32 + (((y1_32 - y0_32) * frac) >> (QUADRANT_BITS - LUT_BITS))) as Fixed
163    }
164}
165
166// Ré-exportation et fonctions publiques liées à la LUT
167
168/// Computes the sine of an angle using the lookup table engine.
169///
170/// This function provides the fastest and most memory-efficient trigonometric
171/// computation with high accuracy (~0.1% error).
172///
173/// # Input
174/// - `angle`: An [`Angle`] value in the range [0, 65535] representing [0, 2π).
175///
176/// # Output
177/// A [`Fixed`] value in Q15 format, where 32767 ≈ 1.0 and -32768 ≈ -1.0.
178///
179/// # Algorithm
180/// Uses a 257-entry lookup table with linear interpolation. Exploits trigonometric
181/// symmetry to store only the first quadrant while computing all four quadrants.
182///
183/// # Example
184/// ```ignore
185/// use fixed_math_taylor::sin_fixed;
186///
187/// // π/2 in angle representation
188/// let angle = 16384u16;
189/// let result = sin_fixed(angle);
190/// // result ≈ 32767 (representing 1.0)
191/// ```
192///
193/// # Performance
194/// ~2.4 µs on RP2040 at 125 MHz
195#[cfg(feature = "lut")]
196pub use lut_impl::sin_fixed;
197
198/// Computes the cosine of an angle using the lookup table engine.
199///
200/// Equivalent to `sin(angle + π/2)`. Provides the same performance and accuracy
201/// as [`sin_fixed`].
202///
203/// # Input
204/// - `angle`: An [`Angle`] value in the range [0, 65535] representing [0, 2π).
205///
206/// # Output
207/// A [`Fixed`] value in Q15 format, where 32767 ≈ 1.0 and -32768 ≈ -1.0.
208///
209/// # Example
210/// ```ignore
211/// use fixed_math_taylor::cos_fixed;
212///
213/// // π in angle representation
214/// let angle = 32768u16;
215/// let result = cos_fixed(angle);
216/// // result ≈ -32768 (representing -1.0)
217/// ```
218///
219/// # Performance
220/// ~2.4 µs on RP2040 at 125 MHz
221#[cfg(feature = "lut")]
222#[inline(always)]
223pub fn cos_fixed(angle: Angle) -> Fixed {
224    sin_fixed(angle.wrapping_add(16384))
225}
226
227/// Computes both sine and cosine of an angle simultaneously.
228///
229/// More efficient than calling [`sin_fixed`] and [`cos_fixed`] separately,
230/// as some intermediate calculations are shared.
231///
232/// # Input
233/// - `angle`: An [`Angle`] value in the range [0, 65535] representing [0, 2π).
234///
235/// # Output
236/// A tuple `(sin_value, cos_value)`, both in Q15 [`Fixed`] format.
237///
238/// # Example
239/// ```ignore
240/// use fixed_math_taylor::sin_cos;
241///
242/// let angle = 16384u16;
243/// let (sin_val, cos_val) = sin_cos(angle);
244/// ```
245///
246/// # Performance
247/// ~3.5 µs on RP2040 at 125 MHz (faster than two separate calls)
248#[cfg(feature = "lut")]
249#[inline(always)]
250pub fn sin_cos(angle: Angle) -> (Fixed, Fixed) {
251    (sin_fixed(angle), cos_fixed(angle))
252}
253// ==========================================
254// MOTEUR TAYLOR (Q15 - 100% Entiers)
255// ==========================================
256
257/// Pure algorithmic sine and cosine using 5th-order Taylor series expansion.
258///
259/// This module provides trigonometric functions without requiring a lookup table,
260/// making it ideal for scenarios where Flash memory is constrained or where
261/// purely algorithmic computation is preferred.
262///
263/// Both functions use 100% integer arithmetic (Q15 fixed-point), with intermediate
264/// 64-bit calculations to prevent overflow.
265#[cfg(feature = "taylor")]
266pub mod taylor_impl {
267    use super::{Angle, Fixed};
268
269    /// Computes sine using 5th-order Taylor series expansion (Q15).
270    ///
271    /// # Performance
272    /// ~12.8 µs on RP2040 at 125 MHz
273    ///
274    /// # Precision
275    /// High accuracy (~0.01% error) with pure integer arithmetic.
276    ///
277    /// # Example
278    /// ```ignore
279    /// use fixed_math_taylor::taylor_impl;
280    ///
281    /// let angle = 16384u16; // π/2
282    /// let result = taylor_impl::sin_taylor(angle);
283    /// // result ≈ 32767 (representing 1.0)
284    /// ```
285    pub fn sin_taylor(angle: Angle) -> Fixed {
286        let x_input = if angle > 32768 {
287            65536 - angle as i32
288        } else {
289            angle as i32
290        };
291        let x = if x_input > 16384 {
292            32768 - x_input
293        } else {
294            x_input
295        };
296
297        // Calculs en i64 pour la précision
298        let x_rad = ((x as i64 * 51472) >> 14) as i32;
299        let x2 = ((x_rad as i64 * x_rad as i64) >> 15) as i32;
300        let x3 = ((x2 as i64 * x_rad as i64) >> 15) as i32;
301        let x5 = ((((x3 as i64 * x2 as i64) >> 15) as i64 * x2 as i64) >> 15) as i32;
302
303        let term3 = ((x3 as i64 * 5461) >> 15) as i32;
304        let term5 = ((x5 as i64 * 273) >> 15) as i32;
305
306        // --- SÉCURITÉ DE SATURATION ---
307        let res_full = x_rad - term3 + term5;
308        
309        // Si ça dépasse 1.0 (32767), on bloque au max au lieu de laisser l'i16 boucler
310        let res_clamped = if res_full > 32767 {
311            32767
312        } else if res_full < -32767 {
313            -32767
314        } else {
315            res_full
316        };
317
318        let res = res_clamped as Fixed;
319        
320        if angle > 32768 { -res } else { res }
321    }
322
323    /// Computes cosine using the identity cos(x) = sin(x + π/2).
324    ///
325    /// # Performance
326    /// ~12.8 µs on RP2040 at 125 MHz (same as sin_taylor)
327    pub fn cos_taylor(angle: super::Angle) -> super::Fixed {
328        sin_taylor(angle.wrapping_add(16384))
329    }
330}
331
332// ==========================================
333// MOTEUR FAST (Bhaskara I Q15)
334// ==========================================
335
336/// Ultra-fast approximation using Bhaskara I formula.
337///
338/// This module provides the speediest trigonometric computation at the cost
339/// of slightly lower precision. Ideal for graphics, animations, and scenarios
340/// where speed is critical.
341///
342/// Uses the Bhaskara I approximation formula in pure Q15 integer arithmetic.
343#[cfg(feature = "fast-sin")]
344pub mod fast_impl {
345    use super::{Angle, Fixed};
346
347    /// Computes sine using Bhaskara I approximation (Q15).
348    ///
349    /// # Performance
350    /// ~1.6 µs on RP2040 at 125 MHz (fastest option)
351    ///
352    /// # Precision
353    /// Medium accuracy (~0.5% error). Suitable for graphics and animations.
354    ///
355    /// # Example
356    /// ```ignore
357    /// use fixed_math_taylor::fast_impl;
358    ///
359    /// let angle = 16384u16; // π/2
360    /// let result = fast_impl::sin_fast(angle);
361    /// // result ≈ 32767 (representing 1.0)
362    /// ```
363    pub fn sin_fast(angle: Angle) -> Fixed {
364        // 0..PI (0..32768)
365        let x = (angle & 0x7FFF) as i32;
366        let pi = 32768i32;
367
368        // num = 4x(pi-x)
369        let x_pi_x = (x * (pi - x)) >> 15; // Reste en Q15
370
371        // Formule de Bhaskara simplifiée pour calcul entier :
372        // sin(x) ≈ (16x(pi-x)) / (5pi^2 - 4x(pi-x))
373        let num = (x_pi_x as i64) * 16;
374        let den = (5 * 32768) - (4 * x_pi_x); // Approximation du dénominateur
375
376        // On scale le numérateur pour la division Q15
377        let res = (num * 32767) / den as i64;
378
379        let val = res as Fixed;
380        if angle > 32768 {
381            -val
382        } else {
383            val
384        }
385    }
386
387    /// Computes cosine using the identity cos(x) = sin(x + π/2).
388    ///
389    /// # Performance
390    /// ~1.6 µs on RP2040 at 125 MHz (same as sin_fast)
391    pub fn cos_fast(angle: super::Angle) -> super::Fixed {
392        sin_fast(angle.wrapping_add(16384))
393    }
394}
395
396// ==========================================
397// UTILITAIRES COMMUNS
398// ==========================================
399
400/// Converts a float value to Q15 fixed-point format.
401///
402/// # Input
403/// - `x`: A floating-point value in the range [-1.0, 1.0]
404///
405/// # Output
406/// A [`Fixed`] value representing the input in Q15 format.
407///
408/// # Example
409/// ```ignore
410/// use fixed_math_taylor::to_fixed;
411///
412/// let f = to_fixed(0.5);
413/// assert_eq!(f, 16384); // 0.5 * 32767 ≈ 16384
414/// ```
415#[inline(always)]
416pub fn to_fixed(x: f32) -> Fixed {
417    (x * 32767.0) as Fixed
418}
419
420/// Converts a Q15 fixed-point value to a float.
421///
422/// # Input
423/// - `x`: A [`Fixed`] value in Q15 format
424///
425/// # Output
426/// A floating-point value representing the input (approximately in [-1.0, 1.0])
427///
428/// # Example
429/// ```ignore
430/// use fixed_math_taylor::from_fixed;
431///
432/// let f = from_fixed(32767);
433/// assert!((f - 1.0).abs() < 0.001); // Close to 1.0
434/// ```
435#[inline(always)]
436pub fn from_fixed(x: Fixed) -> f32 {
437    (x as f32) / 32767.0
438}
439
440/// Converts an angle in radians to the [`Angle`] representation used by this library.
441///
442/// # Input
443/// - `rads`: An angle in radians. Values outside [0, 2π) wrap around automatically.
444///
445/// # Output
446/// An [`Angle`] value suitable for the trigonometric functions.
447///
448/// # Example
449/// ```ignore
450/// use fixed_math_taylor::radians_to_angle;
451/// use core::f32::consts::PI;
452///
453/// let angle = radians_to_angle(PI / 2.0);
454/// assert_eq!(angle, 16384); // π/2
455/// ```
456#[inline(always)]
457pub fn radians_to_angle(rads: f32) -> Angle {
458    let scale = 65536.0 / (2.0 * core::f32::consts::PI);
459    (rads * scale) as i32 as u16
460}
461
462// ==========================================
463// TESTS UNITAIRES
464// ==========================================
465#[cfg(test)]
466mod tests {
467    extern crate std;
468    use super::*;
469    use core::f32::consts::PI;
470
471    #[cfg(feature = "lut")]
472    #[test]
473    fn test_sin_fixed_precision() {
474        // Points cardinaux : Précision exacte (tolérance 1 bit)
475        assert!((sin_fixed(0) - 0).abs() <= 1);
476        assert!((sin_fixed(16384) - 32767).abs() <= 1); // PI/2 (1.0)
477        assert!((sin_fixed(32768) - 0).abs() <= 1); // PI (0.0)
478        assert!((sin_fixed(49152) - (-32767)).abs() <= 1); // 3PI/2 (-1.0)
479
480        // Test à 45°
481        let res_raw = sin_fixed(8192);
482        let expected_raw = 23203;
483        assert_eq!(res_raw, expected_raw, "Erreur de précision à 45°");
484    }
485
486    #[cfg(feature = "lut")]
487    #[test]
488    fn test_cos_fixed() {
489        // CORRECTION : Appel de cos_fixed au lieu de cos
490        assert!((cos_fixed(0) - 32767).abs() <= 1);
491        assert!(cos_fixed(16384).abs() <= 1);
492        assert!((cos_fixed(32768) - (-32767)).abs() <= 1);
493    }
494
495    #[cfg(feature = "taylor")]
496    #[test]
497    fn test_taylor_accuracy() {
498        let res = taylor_impl::sin_taylor(8192); // 45°
499        let expected = 23170;
500        assert!((res - expected).abs() < 1000);
501    }
502
503    #[cfg(feature = "fast-sin")]
504    #[test]
505    fn test_fast_sin_approximation() {
506        let res = fast_impl::sin_fast(5461); // 30°
507        let expected = 16384;
508        assert!((res - expected).abs() < 1500);
509    }
510
511    #[test]
512    fn test_radians_to_angle_wrapping() {
513        assert_eq!(radians_to_angle(0.0), 0);
514        assert_eq!(radians_to_angle(2.0 * PI), 0);
515        let a = radians_to_angle(-PI / 2.0);
516        assert!(a == 49152 || a == 49151);
517    }
518
519    #[test]
520    fn test_fixed_conversion_roundtrip() {
521        let original = 0.5f32;
522        let fixed = to_fixed(original);
523        let back = from_fixed(fixed);
524        assert!((original - back).abs() < 0.0001);
525    }
526
527    #[test]
528    fn test_sin_cos_simultaneous() {
529        #[cfg(feature = "lut")]
530        {
531            let (s, c) = sin_cos(0);
532            assert_eq!(s, 0);
533            assert_eq!(c, 32767);
534        }
535    }
536
537    #[test]
538    fn test_cos_consistency() {
539        let _angle_45 = 8192;
540
541        #[cfg(feature = "lut")]
542        assert!((cos_fixed(0) - 32767).abs() <= 1);
543
544        #[cfg(feature = "taylor")]
545        {
546            // CORRECTION : S'assure que cos_taylor est bien appelé
547            let res = taylor_impl::cos_taylor(angle_45);
548            assert!((res - 23170).abs() < 1000);
549        }
550
551        #[cfg(feature = "fast-sin")]
552        {
553            // CORRECTION : S'assure que cos_fast est bien appelé
554            let res = fast_impl::cos_fast(0);
555            assert!((res - 32767).abs() < 1500);
556        }
557    }
558}