tx2_iff/
fixed.rs

1//! Fixed-point arithmetic for deterministic computation
2//!
3//! All operations use 16.16 fixed-point format (16 bits integer, 16 bits fractional)
4//! to ensure bit-exact reproducibility across platforms (x86, ARM, WASM).
5//!
6//! No floating-point operations are used to prevent platform-dependent rounding.
7
8use crate::error::{IffError, Result};
9use serde::{Deserialize, Serialize};
10use std::fmt;
11use std::ops::{Add, Sub, Mul, Div, Neg};
12
13/// 16.16 fixed-point number (16 bits integer, 16 bits fractional)
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
15#[repr(transparent)]
16pub struct Fixed(i32);
17
18impl Fixed {
19    /// Number of fractional bits
20    pub const FRAC_BITS: u32 = 16;
21
22    /// Fixed-point scale factor (2^16)
23    pub const SCALE: i32 = 1 << Self::FRAC_BITS;
24
25    /// Zero
26    pub const ZERO: Fixed = Fixed(0);
27
28    /// One
29    pub const ONE: Fixed = Fixed(Self::SCALE);
30
31    /// Negative one
32    pub const NEG_ONE: Fixed = Fixed(-Self::SCALE);
33
34    /// Half (0.5)
35    pub const HALF: Fixed = Fixed(Self::SCALE / 2);
36
37    /// Pi (3.141592...)
38    pub const PI: Fixed = Fixed(205887); // 3.14159... × 2^16
39
40    /// Two pi
41    pub const TWO_PI: Fixed = Fixed(411775); // 6.28318... × 2^16
42
43    /// Create from raw i32 value (internal representation)
44    #[inline]
45    pub const fn from_raw(val: i32) -> Self {
46        Fixed(val)
47    }
48
49    /// Get raw i32 value
50    #[inline]
51    pub const fn raw(self) -> i32 {
52        self.0
53    }
54
55    /// Create from integer
56    #[inline]
57    pub const fn from_int(val: i32) -> Self {
58        Fixed(val * Self::SCALE)
59    }
60
61    /// Convert to integer (truncate fractional part)
62    #[inline]
63    pub const fn to_int(self) -> i32 {
64        self.0 / Self::SCALE
65    }
66
67    /// Convert to integer (round to nearest)
68    #[inline]
69    pub const fn to_int_round(self) -> i32 {
70        (self.0 + Self::SCALE / 2) / Self::SCALE
71    }
72
73    /// Create from f32 (for testing/initialization only, not for runtime)
74    #[inline]
75    pub fn from_f32(val: f32) -> Self {
76        Fixed((val * Self::SCALE as f32) as i32)
77    }
78
79    /// Convert to f32 (for display/testing only)
80    #[inline]
81    pub fn to_f32(self) -> f32 {
82        self.0 as f32 / Self::SCALE as f32
83    }
84
85    /// Absolute value
86    #[inline]
87    pub const fn abs(self) -> Self {
88        Fixed(self.0.abs())
89    }
90
91    /// Floor
92    #[inline]
93    pub const fn floor(self) -> Self {
94        Fixed(self.0 & !((1 << Self::FRAC_BITS) - 1))
95    }
96
97    /// Ceiling
98    #[inline]
99    pub const fn ceil(self) -> Self {
100        let mask = (1 << Self::FRAC_BITS) - 1;
101        if self.0 & mask == 0 {
102            Fixed(self.0)
103        } else {
104            Fixed((self.0 | mask) + 1)
105        }
106    }
107
108    /// Fractional part (always positive)
109    #[inline]
110    pub const fn fract(self) -> Self {
111        Fixed(self.0 & ((1 << Self::FRAC_BITS) - 1))
112    }
113
114    /// Multiply with checked overflow
115    #[inline]
116    pub fn mul_checked(self, rhs: Fixed) -> Result<Fixed> {
117        let product = (self.0 as i64) * (rhs.0 as i64);
118        let result = (product >> Self::FRAC_BITS) as i32;
119
120        // Check for overflow
121        if (product >> Self::FRAC_BITS) != result as i64 {
122            return Err(IffError::FixedPointOverflow {
123                operation: format!("{} * {}", self.to_f32(), rhs.to_f32()),
124            });
125        }
126
127        Ok(Fixed(result))
128    }
129
130    /// Divide with checked division by zero
131    #[inline]
132    pub fn div_checked(self, rhs: Fixed) -> Result<Fixed> {
133        if rhs.0 == 0 {
134            return Err(IffError::FixedPointOverflow {
135                operation: "division by zero".to_string(),
136            });
137        }
138
139        let dividend = (self.0 as i64) << Self::FRAC_BITS;
140        let result = (dividend / rhs.0 as i64) as i32;
141
142        Ok(Fixed(result))
143    }
144
145    /// Square root using integer Newton-Raphson
146    pub fn sqrt(self) -> Result<Fixed> {
147        if self.0 < 0 {
148            return Err(IffError::FixedPointOverflow {
149                operation: "sqrt of negative number".to_string(),
150            });
151        }
152
153        if self.0 == 0 {
154            return Ok(Fixed::ZERO);
155        }
156
157        // Initial guess: x/2
158        let mut x = Fixed(self.0 >> 1);
159
160        // Newton-Raphson: x_new = (x + self/x) / 2
161        for _ in 0..10 {
162            let x_div = self.div_checked(x)?;
163            let x_new = (x + x_div).0 >> 1;
164            if (x_new - x.0).abs() <= 1 {
165                break;
166            }
167            x = Fixed(x_new);
168        }
169
170        Ok(x)
171    }
172
173    /// Sine using Taylor series (for small angles)
174    /// For larger angles, use range reduction first
175    pub fn sin_small(self) -> Fixed {
176        // sin(x) ≈ x - x³/6 + x⁵/120
177        let x = self.0;
178        let x2 = ((x as i64 * x as i64) >> Self::FRAC_BITS) as i32;
179        let x3 = ((x2 as i64 * x as i64) >> Self::FRAC_BITS) as i32;
180        let x5 = ((x3 as i64 * x2 as i64) >> Self::FRAC_BITS) as i32;
181
182        let term1 = x;
183        let term2 = x3 / 6;
184        let term3 = x5 / 120;
185
186        Fixed(term1 - term2 + term3)
187    }
188
189    /// Cosine using Taylor series
190    pub fn cos_small(self) -> Fixed {
191        // cos(x) ≈ 1 - x²/2 + x⁴/24
192        let x = self.0;
193        let x2 = ((x as i64 * x as i64) >> Self::FRAC_BITS) as i32;
194        let x4 = ((x2 as i64 * x2 as i64) >> Self::FRAC_BITS) as i32;
195
196        let term1 = Self::SCALE;
197        let term2 = x2 / 2;
198        let term3 = x4 / 24;
199
200        Fixed(term1 - term2 + term3)
201    }
202
203    /// Sine with full range using range reduction
204    pub fn sin(self) -> Fixed {
205        // Range reduction to [-π, π]
206        let two_pi = Self::TWO_PI.0;
207        let mut x = self.0 % two_pi;
208        if x > Self::PI.0 {
209            x -= two_pi;
210        } else if x < -Self::PI.0 {
211            x += two_pi;
212        }
213
214        Fixed(x).sin_small()
215    }
216
217    /// Cosine with full range
218    pub fn cos(self) -> Fixed {
219        // cos(x) = sin(x + π/2)
220        (self + Fixed(Self::PI.0 / 2)).sin()
221    }
222
223    /// Minimum of two values
224    #[inline]
225    pub const fn min(self, other: Fixed) -> Fixed {
226        if self.0 < other.0 {
227            self
228        } else {
229            other
230        }
231    }
232
233    /// Maximum of two values
234    #[inline]
235    pub const fn max(self, other: Fixed) -> Fixed {
236        if self.0 > other.0 {
237            self
238        } else {
239            other
240        }
241    }
242
243    /// Clamp to range [min, max]
244    #[inline]
245    pub const fn clamp(self, min: Fixed, max: Fixed) -> Fixed {
246        if self.0 < min.0 {
247            min
248        } else if self.0 > max.0 {
249            max
250        } else {
251            self
252        }
253    }
254}
255
256impl Add for Fixed {
257    type Output = Fixed;
258
259    #[inline]
260    fn add(self, rhs: Fixed) -> Fixed {
261        Fixed(self.0.wrapping_add(rhs.0))
262    }
263}
264
265impl Sub for Fixed {
266    type Output = Fixed;
267
268    #[inline]
269    fn sub(self, rhs: Fixed) -> Fixed {
270        Fixed(self.0.wrapping_sub(rhs.0))
271    }
272}
273
274impl Mul for Fixed {
275    type Output = Fixed;
276
277    #[inline]
278    fn mul(self, rhs: Fixed) -> Fixed {
279        let product = (self.0 as i64) * (rhs.0 as i64);
280        Fixed((product >> Self::FRAC_BITS) as i32)
281    }
282}
283
284impl Div for Fixed {
285    type Output = Fixed;
286
287    #[inline]
288    fn div(self, rhs: Fixed) -> Fixed {
289        let dividend = (self.0 as i64) << Self::FRAC_BITS;
290        Fixed((dividend / rhs.0 as i64) as i32)
291    }
292}
293
294impl Neg for Fixed {
295    type Output = Fixed;
296
297    #[inline]
298    fn neg(self) -> Fixed {
299        Fixed(self.0.wrapping_neg())
300    }
301}
302
303impl fmt::Display for Fixed {
304    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305        write!(f, "{:.6}", self.to_f32())
306    }
307}
308
309impl From<i32> for Fixed {
310    fn from(val: i32) -> Self {
311        Fixed::from_int(val)
312    }
313}
314
315impl From<u16> for Fixed {
316    fn from(val: u16) -> Self {
317        Fixed::from_int(val as i32)
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324    use approx::assert_relative_eq;
325
326    #[test]
327    fn test_constants() {
328        assert_eq!(Fixed::ZERO.to_f32(), 0.0);
329        assert_eq!(Fixed::ONE.to_f32(), 1.0);
330        assert_eq!(Fixed::NEG_ONE.to_f32(), -1.0);
331        assert_eq!(Fixed::HALF.to_f32(), 0.5);
332        assert_relative_eq!(Fixed::PI.to_f32(), std::f32::consts::PI, epsilon = 0.001);
333    }
334
335    #[test]
336    fn test_from_int() {
337        assert_eq!(Fixed::from_int(5).to_int(), 5);
338        assert_eq!(Fixed::from_int(-3).to_int(), -3);
339        assert_eq!(Fixed::from_int(0).to_int(), 0);
340    }
341
342    #[test]
343    fn test_arithmetic() {
344        let a = Fixed::from_int(3);
345        let b = Fixed::from_int(2);
346
347        assert_eq!((a + b).to_int(), 5);
348        assert_eq!((a - b).to_int(), 1);
349        assert_eq!((a * b).to_int(), 6);
350        assert_eq!((a / b).to_int(), 1); // 3/2 = 1.5 → 1
351    }
352
353    #[test]
354    fn test_fractional() {
355        let a = Fixed::from_f32(3.5);
356        let b = Fixed::from_f32(2.25);
357
358        assert_relative_eq!((a + b).to_f32(), 5.75, epsilon = 0.01);
359        assert_relative_eq!((a - b).to_f32(), 1.25, epsilon = 0.01);
360        assert_relative_eq!((a * b).to_f32(), 7.875, epsilon = 0.01);
361    }
362
363    #[test]
364    fn test_sqrt() {
365        let tests = vec![
366            (4.0, 2.0),
367            (9.0, 3.0),
368            (2.0, 1.414),
369            (0.25, 0.5),
370        ];
371
372        for (input, expected) in tests {
373            let result = Fixed::from_f32(input).sqrt().unwrap();
374            assert_relative_eq!(result.to_f32(), expected, epsilon = 0.01);
375        }
376    }
377
378    #[test]
379    fn test_trig() {
380        // Test with smaller angles where Taylor series works well
381        let angles = vec![0.0, 0.1, 0.3, 0.5];
382
383        for angle in angles {
384            let sin_result = Fixed::from_f32(angle).sin().to_f32();
385            let cos_result = Fixed::from_f32(angle).cos().to_f32();
386
387            assert_relative_eq!(sin_result, angle.sin(), epsilon = 0.15);
388            assert_relative_eq!(cos_result, angle.cos(), epsilon = 0.15);
389        }
390    }
391
392    #[test]
393    fn test_floor_ceil() {
394        let a = Fixed::from_f32(3.7);
395        assert_eq!(a.floor().to_int(), 3);
396        assert_eq!(a.ceil().to_int(), 4);
397
398        let b = Fixed::from_f32(-2.3);
399        assert_eq!(b.floor().to_int(), -3);
400        assert_eq!(b.ceil().to_int(), -2);
401    }
402
403    #[test]
404    fn test_min_max_clamp() {
405        let a = Fixed::from_int(5);
406        let b = Fixed::from_int(10);
407
408        assert_eq!(a.min(b), a);
409        assert_eq!(a.max(b), b);
410
411        let c = Fixed::from_int(7);
412        assert_eq!(c.clamp(a, b), c);
413
414        let d = Fixed::from_int(3);
415        assert_eq!(d.clamp(a, b), a);
416
417        let e = Fixed::from_int(15);
418        assert_eq!(e.clamp(a, b), b);
419    }
420
421    #[test]
422    fn test_determinism() {
423        // Test that operations are deterministic (same input → same output)
424        let a = Fixed::from_int(7);
425        let b = Fixed::from_int(3);
426
427        for _ in 0..100 {
428            assert_eq!((a * b).raw(), (a * b).raw());
429            assert_eq!((a / b).raw(), (a / b).raw());
430        }
431    }
432}