Skip to main content

adele_ring/
rational.rs

1//! Level 1 — ℚ. Exact rational arithmetic over the RNS substrate.
2//!
3//! An [`RnsRational`] stores a reduced numerator and a positive denominator,
4//! each as an [`RnsInt`]. Arithmetic reconstructs to `BigInt`, computes exactly,
5//! and re-normalizes — so results never drift the way floating point does.
6//!
7//! The module also exposes *base awareness*: which primes a fraction's
8//! denominator contains, whether it terminates in a given base, and the minimal
9//! ("natural") base in which it is exact.
10
11use num_bigint::BigInt;
12use num_integer::Integer;
13use num_traits::{One, Signed, ToPrimitive, Zero};
14
15use crate::primes::{distinct_primes, radical, termination_period};
16use crate::rns::{Channels, RnsInt};
17
18/// An exact rational number represented over RNS channels.
19///
20/// The canonical value is held as a reduced `BigInt` pair (`p`, `q`) so that
21/// precision is never capped by the channel capacity — essential for the
22/// arbitrary-precision rationals produced at Level 3. The `numer`/`denom`
23/// [`RnsInt`] fields are the RNS *view* of that value (exact while the value
24/// fits inside the channel capacity), kept for the lower-level channel-dispatch
25/// machinery.
26#[derive(Clone, Debug)]
27pub struct RnsRational {
28    /// Reduced numerator (carries the sign).
29    p: BigInt,
30    /// Reduced denominator (always positive).
31    q: BigInt,
32    /// RNS view of the numerator.
33    pub numer: RnsInt,
34    /// RNS view of the denominator.
35    pub denom: RnsInt,
36    pub channels: Channels,
37}
38
39impl RnsRational {
40    /// Build from a `p/q` pair, normalizing the sign and reducing by the GCD.
41    ///
42    /// Panics if `q == 0`.
43    pub fn new(p: BigInt, q: BigInt, channels: Channels) -> Self {
44        assert!(!q.is_zero(), "denominator must be non-zero");
45        let mut p = p;
46        let mut q = q;
47        if q.is_negative() {
48            p = -p;
49            q = -q;
50        }
51        let g = p.gcd(&q);
52        if !g.is_zero() {
53            p /= &g;
54            q /= &g;
55        }
56        let numer = RnsInt::from_bigint(&p, channels.clone());
57        let denom = RnsInt::from_bigint(&q, channels.clone());
58        RnsRational {
59            p,
60            q,
61            numer,
62            denom,
63            channels,
64        }
65    }
66
67    /// Convenience constructor from machine integers.
68    pub fn from_fraction(p: i64, q: i64, channels: Channels) -> Self {
69        Self::new(BigInt::from(p), BigInt::from(q), channels)
70    }
71
72    /// An integer rational `n/1`.
73    pub fn from_int(n: i64, channels: Channels) -> Self {
74        Self::from_fraction(n, 1, channels)
75    }
76
77    /// Zero.
78    pub fn zero(channels: Channels) -> Self {
79        Self::from_fraction(0, 1, channels)
80    }
81
82    /// The reduced `(p, q)` pair as `BigInt`s (exact, never capacity-bounded).
83    pub fn to_pair(&self) -> (BigInt, BigInt) {
84        (self.p.clone(), self.q.clone())
85    }
86
87    /// `true` iff the value is zero.
88    pub fn is_zero(&self) -> bool {
89        self.p.is_zero()
90    }
91
92    /// `true` iff the denominator is 1 (the value is an integer).
93    pub fn is_integer(&self) -> bool {
94        self.q == BigInt::one()
95    }
96
97    fn op(&self, other: &Self, f: impl Fn(&BigInt, &BigInt, &BigInt, &BigInt) -> (BigInt, BigInt)) -> Self {
98        let (p1, q1) = self.to_pair();
99        let (p2, q2) = other.to_pair();
100        let (p, q) = f(&p1, &q1, &p2, &q2);
101        Self::new(p, q, self.channels.clone())
102    }
103
104    /// `p1/q1 + p2/q2 = (p1·q2 + p2·q1) / (q1·q2)`.
105    pub fn add(&self, other: &Self) -> Self {
106        self.op(other, |p1, q1, p2, q2| (p1 * q2 + p2 * q1, q1 * q2))
107    }
108
109    /// Subtraction.
110    pub fn sub(&self, other: &Self) -> Self {
111        self.op(other, |p1, q1, p2, q2| (p1 * q2 - p2 * q1, q1 * q2))
112    }
113
114    /// Multiplication.
115    pub fn mul(&self, other: &Self) -> Self {
116        self.op(other, |p1, q1, p2, q2| (p1 * p2, q1 * q2))
117    }
118
119    /// Division. Panics if `other` is zero.
120    pub fn div(&self, other: &Self) -> Self {
121        assert!(!other.is_zero(), "division by zero");
122        self.op(other, |p1, q1, p2, q2| (p1 * q2, q1 * p2))
123    }
124
125    /// Additive inverse.
126    pub fn neg(&self) -> Self {
127        let (p, q) = self.to_pair();
128        Self::new(-p, q, self.channels.clone())
129    }
130
131    /// Multiplicative inverse. Panics if zero.
132    pub fn recip(&self) -> Self {
133        assert!(!self.is_zero(), "reciprocal of zero");
134        let (p, q) = self.to_pair();
135        Self::new(q, p, self.channels.clone())
136    }
137
138    // ── Base awareness ──────────────────────────────────────────────────────
139
140    /// The denominator value as a `u64` (denominators are small after reduction
141    /// for practical inputs). Returns `None` if it exceeds `u64`.
142    fn denom_u64(&self) -> Option<u64> {
143        self.denom.to_bigint().to_u64()
144    }
145
146    /// The distinct primes appearing in the reduced denominator.
147    pub fn denom_prime_signature(&self) -> Vec<u64> {
148        match self.denom_u64() {
149            Some(d) => distinct_primes(d),
150            None => Vec::new(),
151        }
152    }
153
154    /// `true` iff this fraction has a terminating expansion in `base`, i.e. every
155    /// prime in the denominator divides `base`.
156    pub fn exact_in_base(&self, base: u64) -> bool {
157        self.denom_prime_signature()
158            .into_iter()
159            .all(|p| base % p == 0)
160    }
161
162    /// Minimal base for an exact (terminating) representation = `radical(denom)`.
163    pub fn natural_base(&self) -> u64 {
164        self.denom_u64().map(radical).unwrap_or(1).max(1)
165    }
166
167    /// Repeating period in `base`: `0` if it terminates, else the period length.
168    pub fn termination_period_in_base(&self, base: u64) -> u64 {
169        match self.denom_u64() {
170            Some(d) => termination_period(d, base).unwrap_or(0),
171            None => 0,
172        }
173    }
174
175    // ── Floating-point bridge ───────────────────────────────────────────────
176
177    /// Lossy `f64` approximation.
178    ///
179    /// Computed via bit-shift scaling so it stays finite even when the numerator
180    /// and denominator are far too large to fit in an `f64` individually.
181    pub fn to_f64(&self) -> f64 {
182        let (p, q) = self.to_pair();
183        ratio_to_f64(&p, &q)
184    }
185
186    /// Exact dyadic rational equal to a finite `f64`.
187    pub fn from_f64(x: f64, channels: Channels) -> Self {
188        if x == 0.0 || !x.is_finite() {
189            return Self::zero(channels);
190        }
191        let bits = x.to_bits();
192        let sign = if bits >> 63 == 1 { -1i64 } else { 1 };
193        let exponent = ((bits >> 52) & 0x7ff) as i64;
194        let mantissa = bits & 0x000f_ffff_ffff_ffff;
195        // Reconstruct mantissa * 2^(e) with the implicit leading bit.
196        let (m, e) = if exponent == 0 {
197            (mantissa, -1074i64) // subnormal
198        } else {
199            (mantissa | 0x0010_0000_0000_0000, exponent - 1075)
200        };
201        let m = BigInt::from(sign) * BigInt::from(m);
202        if e >= 0 {
203            Self::new(m * (BigInt::one() << (e as usize)), BigInt::one(), channels)
204        } else {
205            Self::new(m, BigInt::one() << ((-e) as usize), channels)
206        }
207    }
208
209    /// `f64` approximation together with the exact representation error
210    /// `self - f64(self)` as an `RnsRational`.
211    pub fn to_f64_with_error(&self) -> (f64, RnsRational) {
212        let approx = self.to_f64();
213        let approx_exact = Self::from_f64(approx, self.channels.clone());
214        let error = self.sub(&approx_exact);
215        (approx, error)
216    }
217
218    /// Sign of the value: `-1`, `0`, or `+1`.
219    pub fn signum(&self) -> i32 {
220        match self.p.sign() {
221            num_bigint::Sign::Minus => -1,
222            num_bigint::Sign::NoSign => 0,
223            num_bigint::Sign::Plus => 1,
224        }
225    }
226
227    /// Absolute value.
228    pub fn abs(&self) -> Self {
229        if self.signum() < 0 {
230            self.neg()
231        } else {
232            self.clone()
233        }
234    }
235
236    /// Midpoint `(self + other) / 2`.
237    pub fn midpoint(&self, other: &Self) -> Self {
238        self.add(other).mul(&Self::from_fraction(1, 2, self.channels.clone()))
239    }
240
241    /// Human-readable form: `"3/7"`, or just `"5"` when the denominator is 1.
242    pub fn display(&self) -> String {
243        let (p, q) = self.to_pair();
244        if q == BigInt::one() {
245            p.to_string()
246        } else {
247            format!("{p}/{q}")
248        }
249    }
250}
251
252/// Convert `p/q` to `f64`, scaling by powers of two so neither operand
253/// overflows the `f64` range during the division.
254fn ratio_to_f64(p: &BigInt, q: &BigInt) -> f64 {
255    if q.is_zero() {
256        return f64::NAN;
257    }
258    if p.is_zero() {
259        return 0.0;
260    }
261    let negative = (p.sign() == num_bigint::Sign::Minus) ^ (q.sign() == num_bigint::Sign::Minus);
262    let mut pm = p.magnitude().clone();
263    let mut qm = q.magnitude().clone();
264    let pb = pm.bits() as i64;
265    let qb = qm.bits() as i64;
266    // Aim for the quotient to carry ~60 significant bits.
267    let shift = 60 + qb - pb;
268    if shift > 0 {
269        pm <<= shift as u64;
270    } else {
271        qm <<= (-shift) as u64;
272    }
273    let quo = &pm / &qm;
274    let mag = quo.to_f64().unwrap_or(f64::INFINITY);
275    let value = mag * 2f64.powi(-(shift as i32));
276    if negative {
277        -value
278    } else {
279        value
280    }
281}
282
283impl PartialEq for RnsRational {
284    fn eq(&self, other: &Self) -> bool {
285        self.to_pair() == other.to_pair()
286    }
287}
288impl Eq for RnsRational {}
289
290impl PartialOrd for RnsRational {
291    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
292        Some(self.cmp(other))
293    }
294}
295
296impl Ord for RnsRational {
297    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
298        // p1/q1 ? p2/q2  <=>  p1*q2 ? p2*q1  (both denominators positive)
299        let (p1, q1) = self.to_pair();
300        let (p2, q2) = other.to_pair();
301        (p1 * q2).cmp(&(p2 * q1))
302    }
303}
304
305impl std::fmt::Display for RnsRational {
306    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307        f.write_str(&self.display())
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    fn ch() -> Channels {
316        Channels::standard(32)
317    }
318
319    fn frac(p: i64, q: i64) -> RnsRational {
320        RnsRational::from_fraction(p, q, ch())
321    }
322
323    #[test]
324    fn sixths_tenths_fifteenths() {
325        // 1/6 + 1/10 + 1/15 == 1/3
326        let r = frac(1, 6).add(&frac(1, 10)).add(&frac(1, 15));
327        assert_eq!(r, frac(1, 3));
328    }
329
330    #[test]
331    fn third_times_three() {
332        assert_eq!(frac(1, 3).mul(&frac(3, 1)), frac(1, 1));
333        assert_eq!(frac(1, 7).mul(&frac(7, 1)), frac(1, 1));
334    }
335
336    #[test]
337    fn point_one_plus_point_two() {
338        // 0.1 + 0.2 == 3/10  (not 0.30000000000000004)
339        assert_eq!(frac(1, 10).add(&frac(1, 5)), frac(3, 10));
340    }
341
342    #[test]
343    fn eighths() {
344        assert_eq!(frac(7, 8).sub(&frac(3, 8)), frac(1, 2));
345    }
346
347    #[test]
348    fn base_awareness() {
349        assert_eq!(frac(1, 6).natural_base(), 6);
350        assert_eq!(frac(1, 12).natural_base(), 6); // radical(12) = 6
351        assert!(frac(1, 6).exact_in_base(6));
352        assert!(!frac(1, 6).exact_in_base(10));
353        assert!(frac(1, 6).exact_in_base(30));
354        assert_eq!(frac(1, 8).termination_period_in_base(10), 0); // terminates
355        assert_eq!(frac(1, 3).termination_period_in_base(10), 1);
356    }
357
358    #[test]
359    fn f64_error_is_exact() {
360        // 1/10 is not exact in binary; the error must be non-zero and exact.
361        let (approx, err) = frac(1, 10).to_f64_with_error();
362        assert!((approx - 0.1).abs() < 1e-17);
363        assert!(!err.is_zero());
364        // approx + err reconstructs the original exactly.
365        let reconstructed = RnsRational::from_f64(approx, ch()).add(&err);
366        assert_eq!(reconstructed, frac(1, 10));
367    }
368
369    #[test]
370    fn display_form() {
371        assert_eq!(frac(3, 7).display(), "3/7");
372        assert_eq!(frac(10, 2).display(), "5");
373    }
374}