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