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