Skip to main content

adele_ring/
rns.rs

1//! Level 0 — ℤ. RNS integers over the adaptive [`Basis`], and Garner CRT.
2//!
3//! # Balanced residues (no sign-magnitude)
4//!
5//! Residues are stored canonically in `[0, p)` for each prime `p`, but the value
6//! they encode is the **balanced** (symmetric) integer in `(-M/2, M/2]`, where
7//! `M = ∏ p`. Reconstruction folds `u ∈ [0, M)` to `u - M` when `2u > M`.
8//!
9//! There is **no `negative` flag**: subtraction is the channel-parallel
10//! `(a + p - b) % p`, with no magnitude comparison and no `BigInt` in the loop.
11//! The *sign* of a value is an Archimedean question — it is answered by the
12//! [`crate::ball::Ball`] in the [`crate::adelic::Adelic`] carrier, never by the
13//! residues (which are constitutionally blind to size).
14//!
15//! All primes are GPU-eligible (`(2^15, 2^16)`), so every channel op — add, sub,
16//! and even `(a*b)` — fits in a `u32` with no `u128` intermediate.
17
18use num_bigint::{BigInt, BigUint, Sign};
19use num_traits::Zero;
20
21use crate::basis::Basis;
22use crate::primes::mod_inverse;
23use crate::RAYON_CHANNEL_THRESHOLD;
24
25/// An exact integer in balanced RNS form (Level 0 of the tower).
26#[derive(Clone, Debug)]
27pub struct RnsInt {
28    /// `residues[i] = value mod basis[i]`, stored canonically in `[0, p)`.
29    pub residues: Vec<u32>,
30    pub basis: Basis,
31}
32
33impl RnsInt {
34    /// Construct from an arbitrary `BigInt` (reduced into each channel).
35    pub fn from_bigint(n: &BigInt, basis: Basis) -> Self {
36        let residues = basis
37            .moduli()
38            .iter()
39            .map(|&m| {
40                let mm = BigInt::from(m);
41                let r = ((n % &mm) + &mm) % &mm;
42                r.to_biguint().unwrap().try_into().unwrap()
43            })
44            .collect();
45        RnsInt { residues, basis }
46    }
47
48    /// Construct from a machine integer.
49    pub fn from_i64(n: i64, basis: Basis) -> Self {
50        Self::from_bigint(&BigInt::from(n), basis)
51    }
52
53    /// Additive identity.
54    pub fn zero(basis: Basis) -> Self {
55        RnsInt { residues: vec![0; basis.len()], basis }
56    }
57
58    /// Build directly from raw channel residues (already reduced into `[0, p)`).
59    pub fn from_residues(residues: Vec<u32>, basis: Basis) -> Self {
60        RnsInt { residues, basis }
61    }
62
63    /// Reconstruct the exact signed value via Garner CRT + balanced folding.
64    pub fn to_bigint(&self) -> BigInt {
65        let u = garner_crt(&self.residues, self.basis.moduli());
66        let m = self.basis.modulus_product();
67        if &u * 2u8 > m {
68            BigInt::from_biguint(Sign::Plus, u) - BigInt::from_biguint(Sign::Plus, m)
69        } else {
70            BigInt::from_biguint(Sign::Plus, u)
71        }
72    }
73
74    /// `true` iff every residue is zero.
75    pub fn is_zero(&self) -> bool {
76        self.residues.iter().all(|&r| r == 0)
77    }
78
79    /// Channel-wise modular addition.
80    pub fn add(&self, other: &Self) -> Self {
81        let out = channel_map(&self.residues, &other.residues, self.basis.moduli(), add_channel);
82        Self::from_residues(out, self.basis.clone())
83    }
84
85    /// Channel-wise modular subtraction (`(a + p - b) % p`, no sign-magnitude).
86    pub fn sub(&self, other: &Self) -> Self {
87        let out = channel_map(&self.residues, &other.residues, self.basis.moduli(), sub_channel);
88        Self::from_residues(out, self.basis.clone())
89    }
90
91    /// Channel-wise modular multiplication.
92    pub fn mul(&self, other: &Self) -> Self {
93        let out = channel_map(&self.residues, &other.residues, self.basis.moduli(), mul_channel);
94        Self::from_residues(out, self.basis.clone())
95    }
96
97    /// Additive inverse (channel-wise `(p - r) % p`).
98    pub fn neg(&self) -> Self {
99        let out: Vec<u32> = self
100            .residues
101            .iter()
102            .zip(self.basis.moduli())
103            .map(|(&r, &m)| (m - r) % m)
104            .collect();
105        Self::from_residues(out, self.basis.clone())
106    }
107}
108
109/// Apply `f(a, b, m)` channel-wise, parallelizing only above the threshold.
110fn channel_map(
111    a: &[u32],
112    b: &[u32],
113    moduli: &[u32],
114    f: impl Fn(u32, u32, u32) -> u32 + Sync + Send,
115) -> Vec<u32> {
116    use rayon::prelude::*;
117    if a.len() >= RAYON_CHANNEL_THRESHOLD {
118        a.par_iter()
119            .zip(b.par_iter())
120            .zip(moduli.par_iter())
121            .map(|((&av, &bv), &m)| f(av, bv, m))
122            .collect()
123    } else {
124        a.iter()
125            .zip(b.iter())
126            .zip(moduli.iter())
127            .map(|((&av, &bv), &m)| f(av, bv, m))
128            .collect()
129    }
130}
131
132/// One channel's add: `(a + b) % m`. Fits `u32` (`a, b < 2^16`).
133#[inline]
134pub fn add_channel(a: u32, b: u32, m: u32) -> u32 {
135    (a + b) % m
136}
137
138/// One channel's subtract: `(a + m - b) % m`. No magnitude comparison.
139#[inline]
140pub fn sub_channel(a: u32, b: u32, m: u32) -> u32 {
141    (a + m - b) % m
142}
143
144/// One channel's multiply: `(a * b) % m`. Fits `u32` (`a, b < 2^16`).
145#[inline]
146pub fn mul_channel(a: u32, b: u32, m: u32) -> u32 {
147    (a * b) % m
148}
149
150/// Garner's algorithm: reconstruct the unsigned integer in `[0, M)` from its
151/// balanced-canonical residues. All intermediates stay within `u64`.
152///
153/// The mixed-radix step uses the **underflow-safe** form
154/// `c[i] = ((c[i] + m_i - (c[j] % m_i)) % m_i) * inv % m_i`.
155pub fn garner_crt(residues: &[u32], moduli: &[u32]) -> BigUint {
156    let k = residues.len();
157    assert_eq!(k, moduli.len(), "residue/moduli length mismatch");
158    if k == 0 {
159        return BigUint::zero();
160    }
161
162    let mut c: Vec<u64> = residues.iter().map(|&r| r as u64).collect();
163    for i in 0..k {
164        let mi = moduli[i] as u64;
165        for j in 0..i {
166            let inv = mod_inverse(moduli[j] as u64 % mi, mi)
167                .expect("basis primes must be pairwise coprime for CRT");
168            let diff = (c[i] + mi - (c[j] % mi)) % mi;
169            c[i] = (diff * inv) % mi;
170        }
171    }
172
173    let mut result = BigUint::from(c[k - 1]);
174    for i in (0..k - 1).rev() {
175        result = result * BigUint::from(moduli[i]) + BigUint::from(c[i]);
176    }
177    result
178}
179
180/// Garner CRT followed by balanced (symmetric) folding into `(-M/2, M/2]`.
181pub fn crt_balanced(residues: &[u32], moduli: &[u32]) -> BigInt {
182    let u = garner_crt(residues, moduli);
183    let m: BigUint = moduli.iter().map(|&p| BigUint::from(p)).product();
184    if &u * 2u8 > m {
185        BigInt::from_biguint(Sign::Plus, u) - BigInt::from_biguint(Sign::Plus, m)
186    } else {
187        BigInt::from_biguint(Sign::Plus, u)
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    fn b() -> Basis {
196        Basis::standard()
197    }
198
199    #[test]
200    fn roundtrip_positive() {
201        let a = RnsInt::from_i64(123_456_789, b());
202        assert_eq!(a.to_bigint(), BigInt::from(123_456_789));
203    }
204
205    #[test]
206    fn roundtrip_negative() {
207        let a = RnsInt::from_i64(-42, b());
208        assert_eq!(a.to_bigint(), BigInt::from(-42));
209    }
210
211    #[test]
212    fn add_sub_mul() {
213        let a = RnsInt::from_i64(1000, b());
214        let bb = RnsInt::from_i64(337, b());
215        assert_eq!(a.add(&bb).to_bigint(), BigInt::from(1337));
216        assert_eq!(a.sub(&bb).to_bigint(), BigInt::from(663));
217        assert_eq!(bb.sub(&a).to_bigint(), BigInt::from(-663));
218        assert_eq!(a.mul(&bb).to_bigint(), BigInt::from(337_000));
219    }
220
221    #[test]
222    fn garner_classic() {
223        // x ≡ 2 (3), 3 (5), 2 (7)  =>  x = 23
224        assert_eq!(garner_crt(&[2, 3, 2], &[3, 5, 7]), BigUint::from(23u8));
225        // x ≡ 0 (2), 1 (3), 0 (5)  =>  x = 10
226        assert_eq!(garner_crt(&[0, 1, 0], &[2, 3, 5]), BigUint::from(10u8));
227    }
228
229    #[test]
230    fn is_zero_works() {
231        assert!(RnsInt::zero(b()).is_zero());
232        assert!(!RnsInt::from_i64(1, b()).is_zero());
233    }
234}