Skip to main content

telltale_types/
fixed_q32.rs

1//! Safe fixed-point wrapper for deterministic fractional arithmetic.
2
3use fixed::types::I32F32;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use std::fmt;
6use std::iter::Sum;
7use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
8use std::str::FromStr;
9
10/// Parts-per-million scale for probability-style values.
11pub const PPM_SCALE: u32 = 1_000_000;
12
13/// Error type for safe fixed-point construction and conversion.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum FixedQ32Error {
16    /// Conversion or arithmetic exceeded representable range.
17    Overflow,
18    /// Division by zero was requested.
19    DivisionByZero,
20}
21
22impl fmt::Display for FixedQ32Error {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        match self {
25            Self::Overflow => write!(f, "fixed-point overflow"),
26            Self::DivisionByZero => write!(f, "fixed-point division by zero"),
27        }
28    }
29}
30
31impl std::error::Error for FixedQ32Error {}
32
33/// Signed Q32.32 fixed-point number.
34///
35/// This wrapper intentionally exposes only checked and explicit operations.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
37pub struct FixedQ32(I32F32);
38
39impl FixedQ32 {
40    /// Fractional bits in the Q32.32 encoding.
41    pub const FRACTIONAL_BITS: u32 = 32;
42    /// Raw scaling factor (2^32).
43    pub const SCALE: i64 = 1_i64 << Self::FRACTIONAL_BITS;
44
45    /// Construct from raw two's-complement bits.
46    #[must_use]
47    pub const fn from_bits(bits: i64) -> Self {
48        Self(I32F32::from_bits(bits))
49    }
50
51    /// Return the raw two's-complement bits.
52    #[must_use]
53    pub const fn to_bits(self) -> i64 {
54        self.0.to_bits()
55    }
56
57    /// Zero.
58    #[must_use]
59    pub const fn zero() -> Self {
60        Self::from_bits(0)
61    }
62
63    /// One.
64    #[must_use]
65    pub const fn one() -> Self {
66        Self::from_bits(Self::SCALE)
67    }
68
69    /// -1.
70    #[must_use]
71    pub const fn neg_one() -> Self {
72        Self::from_bits(-Self::SCALE)
73    }
74
75    /// 1/2.
76    #[must_use]
77    pub fn half() -> Self {
78        Self::from_bits(Self::SCALE / 2)
79    }
80
81    /// Construct from an integer, returning an error on overflow.
82    ///
83    /// # Errors
84    ///
85    /// Returns [`FixedQ32Error::Overflow`] if the value is out of range.
86    pub fn try_from_i64(value: i64) -> Result<Self, FixedQ32Error> {
87        I32F32::checked_from_num(value)
88            .map(Self)
89            .ok_or(FixedQ32Error::Overflow)
90    }
91
92    /// Construct from an unsigned integer, returning an error on overflow.
93    ///
94    /// # Errors
95    ///
96    /// Returns [`FixedQ32Error::Overflow`] if the value is out of range.
97    pub fn try_from_u64(value: u64) -> Result<Self, FixedQ32Error> {
98        I32F32::checked_from_num(value)
99            .map(Self)
100            .ok_or(FixedQ32Error::Overflow)
101    }
102
103    /// Construct from usize, returning an error on overflow.
104    ///
105    /// # Errors
106    ///
107    /// Returns [`FixedQ32Error::Overflow`] if the value is out of range.
108    pub fn try_from_usize(value: usize) -> Result<Self, FixedQ32Error> {
109        I32F32::checked_from_num(value)
110            .map(Self)
111            .ok_or(FixedQ32Error::Overflow)
112    }
113
114    /// Construct an exact fixed-point ratio `num / den`.
115    ///
116    /// # Errors
117    ///
118    /// Returns [`FixedQ32Error::DivisionByZero`] if `den` is zero, or
119    /// [`FixedQ32Error::Overflow`] if the result is out of range.
120    pub fn from_ratio(num: i64, den: i64) -> Result<Self, FixedQ32Error> {
121        if den == 0 {
122            return Err(FixedQ32Error::DivisionByZero);
123        }
124        let n = I32F32::checked_from_num(num).ok_or(FixedQ32Error::Overflow)?;
125        let d = I32F32::checked_from_num(den).ok_or(FixedQ32Error::Overflow)?;
126        n.checked_div(d).map(Self).ok_or(FixedQ32Error::Overflow)
127    }
128
129    /// Construct a probability-like value from parts-per-million.
130    ///
131    /// `0` maps to `0.0`, `1_000_000` maps to `1.0`.
132    ///
133    /// # Errors
134    ///
135    /// Returns [`FixedQ32Error::Overflow`] if the result is out of range.
136    pub fn from_ppm(ppm: u32) -> Result<Self, FixedQ32Error> {
137        let n = I32F32::checked_from_num(ppm).ok_or(FixedQ32Error::Overflow)?;
138        let d = I32F32::checked_from_num(PPM_SCALE).ok_or(FixedQ32Error::Overflow)?;
139        n.checked_div(d).map(Self).ok_or(FixedQ32Error::Overflow)
140    }
141
142    /// Parse from a decimal string.
143    ///
144    /// # Errors
145    ///
146    /// Returns [`FixedQ32Error::Overflow`] if parsing fails or the result is out of range.
147    pub fn from_decimal_str(s: &str) -> Result<Self, FixedQ32Error> {
148        s.parse::<I32F32>()
149            .map(Self)
150            .map_err(|_| FixedQ32Error::Overflow)
151    }
152
153    /// Checked addition.
154    #[must_use]
155    pub fn checked_add(self, rhs: Self) -> Option<Self> {
156        self.0.checked_add(rhs.0).map(Self)
157    }
158
159    /// Checked subtraction.
160    #[must_use]
161    pub fn checked_sub(self, rhs: Self) -> Option<Self> {
162        self.0.checked_sub(rhs.0).map(Self)
163    }
164
165    /// Checked multiplication.
166    #[must_use]
167    pub fn checked_mul(self, rhs: Self) -> Option<Self> {
168        self.0.checked_mul(rhs.0).map(Self)
169    }
170
171    /// Checked division.
172    #[must_use]
173    pub fn checked_div(self, rhs: Self) -> Option<Self> {
174        self.0.checked_div(rhs.0).map(Self)
175    }
176
177    /// Saturating addition.
178    #[must_use]
179    pub fn saturating_add(self, rhs: Self) -> Self {
180        Self(self.0.saturating_add(rhs.0))
181    }
182
183    /// Saturating subtraction.
184    #[must_use]
185    pub fn saturating_sub(self, rhs: Self) -> Self {
186        Self(self.0.saturating_sub(rhs.0))
187    }
188
189    /// Saturating multiplication.
190    #[must_use]
191    pub fn saturating_mul(self, rhs: Self) -> Self {
192        Self(self.0.saturating_mul(rhs.0))
193    }
194
195    /// Saturating division.
196    #[must_use]
197    pub fn saturating_div(self, rhs: Self) -> Self {
198        Self(self.0.saturating_div(rhs.0))
199    }
200
201    /// Saturating multiplication by an integer.
202    #[must_use]
203    pub fn saturating_mul_int(self, rhs: i64) -> Self {
204        Self(self.0.saturating_mul_int(rhs))
205    }
206
207    /// Saturating division by an integer.
208    #[must_use]
209    pub fn saturating_div_int(self, rhs: i64) -> Self {
210        Self(self.0.saturating_div_int(rhs))
211    }
212
213    /// Floor rounding.
214    #[must_use]
215    pub fn floor(self) -> Self {
216        Self(self.0.floor())
217    }
218
219    /// Ceiling rounding.
220    #[must_use]
221    pub fn ceil(self) -> Self {
222        Self(self.0.ceil())
223    }
224
225    /// Round to nearest with ties away from zero.
226    #[must_use]
227    pub fn round(self) -> Self {
228        Self(self.0.round())
229    }
230
231    /// Absolute value.
232    #[must_use]
233    pub fn abs(self) -> Self {
234        Self(self.0.abs())
235    }
236
237    /// Clamp to bounds.
238    #[must_use]
239    pub fn clamp(self, lo: Self, hi: Self) -> Self {
240        if self < lo {
241            lo
242        } else if self > hi {
243            hi
244        } else {
245            self
246        }
247    }
248
249    /// Minimum.
250    #[must_use]
251    pub fn min(self, rhs: Self) -> Self {
252        if self <= rhs {
253            self
254        } else {
255            rhs
256        }
257    }
258
259    /// Maximum.
260    #[must_use]
261    pub fn max(self, rhs: Self) -> Self {
262        if self >= rhs {
263            self
264        } else {
265            rhs
266        }
267    }
268
269    /// Whether the value is strictly positive.
270    #[must_use]
271    pub fn is_positive(self) -> bool {
272        self > Self::zero()
273    }
274
275    /// Whether the value is strictly negative.
276    #[must_use]
277    pub fn is_negative(self) -> bool {
278        self < Self::zero()
279    }
280
281    /// Square the value (saturating).
282    #[must_use]
283    pub fn square(self) -> Self {
284        self.saturating_mul(self)
285    }
286
287    /// Integer power using repeated multiplication.
288    #[must_use]
289    pub fn powi(self, exp: u32) -> Self {
290        let mut out = Self::one();
291        for _ in 0..exp {
292            out = out.saturating_mul(self);
293        }
294        out
295    }
296
297    /// Saturating square root for non-negative values; returns zero for negatives.
298    #[must_use]
299    pub fn sqrt(self) -> Self {
300        if self <= Self::zero() {
301            return Self::zero();
302        }
303        // Newton iteration over fixed point.
304        let two = Self::from_bits(2 * Self::SCALE);
305        let mut x = self.max(Self::one());
306        for _ in 0..16 {
307            let q = self.saturating_div(x);
308            x = x.saturating_add(q).saturating_div(two);
309        }
310        x
311    }
312
313    /// Approximate tanh using a rational approximation.
314    ///
315    /// tanh(x) ~= x * (27 + x^2) / (27 + 9x^2), clipped to [-1, 1].
316    #[must_use]
317    pub fn tanh_approx(self) -> Self {
318        let one = Self::one();
319        let three = Self::from_bits(3 * Self::SCALE);
320        if self >= three {
321            return one;
322        }
323        if self <= -three {
324            return -one;
325        }
326        let nine = Self::from_bits(9 * Self::SCALE);
327        let twenty_seven = Self::from_bits(27 * Self::SCALE);
328        let x2 = self.square();
329        let num = self.saturating_mul(twenty_seven.saturating_add(x2));
330        let den = twenty_seven.saturating_add(nine.saturating_mul(x2));
331        num.saturating_div(den).clamp(-one, one)
332    }
333
334    /// Approximate hyperbolic tangent.
335    #[must_use]
336    pub fn tanh(self) -> Self {
337        self.tanh_approx()
338    }
339
340    /// Fixed-point values are always finite.
341    #[must_use]
342    pub const fn is_finite(self) -> bool {
343        // Self is unused: fixed-point values are always finite by construction
344        let _ = self;
345        true
346    }
347
348    /// Convert to i64 after floor rounding.
349    ///
350    /// # Errors
351    ///
352    /// Returns [`FixedQ32Error::Overflow`] if the result is out of range.
353    pub fn to_i64_floor(self) -> Result<i64, FixedQ32Error> {
354        self.floor()
355            .0
356            .checked_to_num()
357            .ok_or(FixedQ32Error::Overflow)
358    }
359
360    /// Convert to i64 after ceiling rounding.
361    ///
362    /// # Errors
363    ///
364    /// Returns [`FixedQ32Error::Overflow`] if the result is out of range.
365    pub fn to_i64_ceil(self) -> Result<i64, FixedQ32Error> {
366        self.ceil()
367            .0
368            .checked_to_num()
369            .ok_or(FixedQ32Error::Overflow)
370    }
371
372    /// Convert to i64 after nearest rounding.
373    ///
374    /// # Errors
375    ///
376    /// Returns [`FixedQ32Error::Overflow`] if the result is out of range.
377    pub fn to_i64_round(self) -> Result<i64, FixedQ32Error> {
378        self.round()
379            .0
380            .checked_to_num()
381            .ok_or(FixedQ32Error::Overflow)
382    }
383
384    /// Convert to usize after nearest rounding.
385    ///
386    /// # Errors
387    ///
388    /// Returns [`FixedQ32Error::Overflow`] if the result is out of range or negative.
389    pub fn to_usize_round(self) -> Result<usize, FixedQ32Error> {
390        let i = self.to_i64_round()?;
391        if i < 0 {
392            return Err(FixedQ32Error::Overflow);
393        }
394        usize::try_from(i).map_err(|_| FixedQ32Error::Overflow)
395    }
396
397    /// Convert to u64 after nearest rounding.
398    ///
399    /// # Errors
400    ///
401    /// Returns [`FixedQ32Error::Overflow`] if the result is out of range or negative.
402    pub fn to_u64_round(self) -> Result<u64, FixedQ32Error> {
403        let i = self.to_i64_round()?;
404        if i < 0 {
405            return Err(FixedQ32Error::Overflow);
406        }
407        u64::try_from(i).map_err(|_| FixedQ32Error::Overflow)
408    }
409}
410
411impl fmt::Display for FixedQ32 {
412    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
413        write!(f, "{}", self.0)
414    }
415}
416
417impl Serialize for FixedQ32 {
418    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
419    where
420        S: Serializer,
421    {
422        serializer.serialize_i64(self.to_bits())
423    }
424}
425
426impl<'de> Deserialize<'de> for FixedQ32 {
427    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
428    where
429        D: Deserializer<'de>,
430    {
431        struct FixedQ32Visitor;
432
433        impl<'de> serde::de::Visitor<'de> for FixedQ32Visitor {
434            type Value = FixedQ32;
435
436            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
437                formatter.write_str(
438                    "a fixed-point number encoded as raw bits (i64/u64) or decimal string",
439                )
440            }
441
442            fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
443            where
444                E: serde::de::Error,
445            {
446                Ok(FixedQ32::from_bits(v))
447            }
448
449            fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
450            where
451                E: serde::de::Error,
452            {
453                let bits = i64::try_from(v).map_err(|_| E::custom(FixedQ32Error::Overflow))?;
454                Ok(FixedQ32::from_bits(bits))
455            }
456
457            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
458            where
459                E: serde::de::Error,
460            {
461                FixedQ32::from_decimal_str(v).map_err(E::custom)
462            }
463
464            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
465            where
466                E: serde::de::Error,
467            {
468                self.visit_str(&v)
469            }
470        }
471
472        deserializer.deserialize_any(FixedQ32Visitor)
473    }
474}
475
476impl TryFrom<i64> for FixedQ32 {
477    type Error = FixedQ32Error;
478
479    fn try_from(value: i64) -> Result<Self, Self::Error> {
480        Self::try_from_i64(value)
481    }
482}
483
484impl TryFrom<u64> for FixedQ32 {
485    type Error = FixedQ32Error;
486
487    fn try_from(value: u64) -> Result<Self, Self::Error> {
488        Self::try_from_u64(value)
489    }
490}
491
492impl From<f64> for FixedQ32 {
493    fn from(value: f64) -> Self {
494        I32F32::checked_from_num(value)
495            .map(Self)
496            .unwrap_or_else(|| {
497                if value.is_sign_negative() {
498                    Self(I32F32::MIN)
499                } else {
500                    Self(I32F32::MAX)
501                }
502            })
503    }
504}
505
506impl TryFrom<usize> for FixedQ32 {
507    type Error = FixedQ32Error;
508
509    fn try_from(value: usize) -> Result<Self, Self::Error> {
510        Self::try_from_usize(value)
511    }
512}
513
514impl TryFrom<FixedQ32> for i64 {
515    type Error = FixedQ32Error;
516
517    fn try_from(value: FixedQ32) -> Result<Self, Self::Error> {
518        value.to_i64_round()
519    }
520}
521
522impl TryFrom<FixedQ32> for u64 {
523    type Error = FixedQ32Error;
524
525    fn try_from(value: FixedQ32) -> Result<Self, Self::Error> {
526        let i = value.to_i64_round()?;
527        if i < 0 {
528            return Err(FixedQ32Error::Overflow);
529        }
530        u64::try_from(i).map_err(|_| FixedQ32Error::Overflow)
531    }
532}
533
534impl Add for FixedQ32 {
535    type Output = Self;
536
537    fn add(self, rhs: Self) -> Self::Output {
538        self.saturating_add(rhs)
539    }
540}
541
542impl AddAssign for FixedQ32 {
543    fn add_assign(&mut self, rhs: Self) {
544        *self = self.saturating_add(rhs);
545    }
546}
547
548impl Sub for FixedQ32 {
549    type Output = Self;
550
551    fn sub(self, rhs: Self) -> Self::Output {
552        self.saturating_sub(rhs)
553    }
554}
555
556impl SubAssign for FixedQ32 {
557    fn sub_assign(&mut self, rhs: Self) {
558        *self = self.saturating_sub(rhs);
559    }
560}
561
562impl Mul for FixedQ32 {
563    type Output = Self;
564
565    fn mul(self, rhs: Self) -> Self::Output {
566        self.saturating_mul(rhs)
567    }
568}
569
570impl MulAssign for FixedQ32 {
571    fn mul_assign(&mut self, rhs: Self) {
572        *self = self.saturating_mul(rhs);
573    }
574}
575
576impl Div for FixedQ32 {
577    type Output = Self;
578
579    fn div(self, rhs: Self) -> Self::Output {
580        self.saturating_div(rhs)
581    }
582}
583
584impl DivAssign for FixedQ32 {
585    fn div_assign(&mut self, rhs: Self) {
586        *self = self.saturating_div(rhs);
587    }
588}
589
590impl Neg for FixedQ32 {
591    type Output = Self;
592
593    fn neg(self) -> Self::Output {
594        Self::zero().saturating_sub(self)
595    }
596}
597
598include!("fixed_q32/iter_and_parse_impls.rs");
599
600#[cfg(test)]
601mod tests {
602    include!("fixed_q32/tests.rs");
603}