Skip to main content

adele_ring/
dispatch.rs

1//! Base analyzer and multimodular basis provisioner.
2//!
3//! The original module tried to "activate only the primes dividing the natural
4//! base" and leave the rest idle to save power. That is **mathematically
5//! unsound**: CRT reconstruction needs *all* channels, so dropping channels
6//! makes the result recoverable only modulo a tiny number — useless.
7//!
8//! This reframed dispatcher does the two jobs that are actually well-posed:
9//!
10//! 1. **Exactness / base analysis.** Report the natural base (LCM of the radicals
11//!    of the operands' denominators) and whether a result is exact in a base.
12//!    This *classifies* the answer; it never skips arithmetic.
13//! 2. **Basis provisioning.** Estimate the result's bit-height and grow the
14//!    [`Basis`] so the multimodular pipeline has enough primes to CRT +
15//!    rational-reconstruct without aliasing.
16//!
17//! It also exposes the **per-value channel validity mask**: the channels whose
18//! prime divides a value's denominator are non-integral at that prime and are
19//! excluded from *that value's* reconstruction. This is correctness bookkeeping
20//! for one number — never a global optimization.
21
22use num_bigint::BigInt;
23use num_traits::Zero;
24
25use crate::basis::Basis;
26use crate::primes::lcm;
27use crate::rational::RnsRational;
28
29/// A plan describing the base structure and basis requirements of an operation.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct DispatchPlan {
32    /// LCM of the operands' natural bases (the minimal exact base).
33    pub natural_base: u64,
34    /// A-priori bound on the result's bit-height (drives the basis size).
35    pub required_bits: u64,
36    /// Bits of range the current basis actually provides.
37    pub provisioned_bits: u64,
38}
39
40impl DispatchPlan {
41    /// Whether the current basis is large enough for the estimated result.
42    pub fn fits(&self) -> bool {
43        self.provisioned_bits >= self.required_bits
44    }
45
46    /// Bound-tightness diagnostic in `[0, ∞)`: `required / provisioned`. Values
47    /// `> 1` mean the basis must grow; values `≪ 1` mean it is oversized.
48    pub fn tightness(&self) -> f64 {
49        if self.provisioned_bits == 0 {
50            f64::INFINITY
51        } else {
52            self.required_bits as f64 / self.provisioned_bits as f64
53        }
54    }
55}
56
57/// Analyzes operands and provisions the adaptive basis.
58pub struct Dispatcher {
59    basis: Basis,
60}
61
62impl Dispatcher {
63    pub fn new(basis: Basis) -> Self {
64        Dispatcher { basis }
65    }
66
67    /// The basis this dispatcher currently manages.
68    pub fn basis(&self) -> &Basis {
69        &self.basis
70    }
71
72    /// Reconstruction needs `M > 2·max(|p|, |q|)²`, i.e. `~2·max_bits + 2` bits.
73    fn reconstruct_bits(p: &BigInt, q: &BigInt) -> u64 {
74        let max_bits = p.bits().max(q.bits());
75        2 * max_bits + 2
76    }
77
78    fn plan(&self, base: u64, p: &BigInt, q: &BigInt) -> DispatchPlan {
79        DispatchPlan {
80            natural_base: base,
81            required_bits: Self::reconstruct_bits(p, q),
82            provisioned_bits: self.basis.capacity_bits(),
83        }
84    }
85
86    /// Plan an addition: `(p1·q2 + p2·q1) / (q1·q2)`.
87    pub fn plan_add(&self, a: &RnsRational, b: &RnsRational) -> DispatchPlan {
88        let (p1, q1) = a.to_pair();
89        let (p2, q2) = b.to_pair();
90        let p = &p1 * &q2 + &p2 * &q1;
91        let q = &q1 * &q2;
92        self.plan(lcm(a.natural_base(), b.natural_base()), &p, &q)
93    }
94
95    /// Plan a multiplication: `(p1·p2) / (q1·q2)`.
96    pub fn plan_mul(&self, a: &RnsRational, b: &RnsRational) -> DispatchPlan {
97        let (p1, q1) = a.to_pair();
98        let (p2, q2) = b.to_pair();
99        let p = &p1 * &p2;
100        let q = &q1 * &q2;
101        self.plan(lcm(a.natural_base(), b.natural_base()), &p, &q)
102    }
103
104    /// Grow the basis so the (planned) result reconstructs without aliasing.
105    pub fn provision(&mut self, plan: &DispatchPlan) {
106        if !plan.fits() {
107            self.basis = self.basis.extend_to_bits(plan.required_bits + 2);
108        }
109    }
110
111    /// Execute an addition (exact regardless of the plan).
112    pub fn execute_add(&self, a: &RnsRational, b: &RnsRational) -> RnsRational {
113        a.add(b)
114    }
115
116    /// Execute a multiplication (exact regardless of the plan).
117    pub fn execute_mul(&self, a: &RnsRational, b: &RnsRational) -> RnsRational {
118        a.mul(b)
119    }
120
121    /// Per-value channel validity mask: indices of channels whose prime divides
122    /// `denom` (and are therefore excluded from *that value's* reconstruction).
123    pub fn invalid_channels(&self, denom: &BigInt) -> Vec<usize> {
124        (0..self.basis.len())
125            .filter(|&c| {
126                let m = BigInt::from(self.basis.modulus(c));
127                (denom % &m).is_zero()
128            })
129            .collect()
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    fn b() -> Basis {
138        Basis::standard()
139    }
140
141    #[test]
142    fn natural_base_is_reported() {
143        let d = Dispatcher::new(b());
144        let sixth = RnsRational::from_fraction(1, 6, b());
145        let quarter = RnsRational::from_fraction(1, 4, b());
146        let plan = d.plan_add(&sixth, &quarter);
147        // radical(6)=6, radical(4)=2, lcm=6
148        assert_eq!(plan.natural_base, 6);
149    }
150
151    #[test]
152    fn integers_need_minimal_range() {
153        let d = Dispatcher::new(b());
154        let a = RnsRational::from_int(3, b());
155        let c = RnsRational::from_int(5, b());
156        let plan = d.plan_add(&a, &c);
157        assert_eq!(plan.natural_base, 1);
158        assert!(plan.fits()); // standard basis dwarfs an 8-bit result
159        assert!(plan.tightness() < 1.0);
160    }
161
162    #[test]
163    fn small_basis_triggers_provisioning() {
164        let mut d = Dispatcher::new(Basis::with_bits(40));
165        // a large rational whose reconstruction needs > 40 bits
166        let big: BigInt = BigInt::from(1u64) << 100;
167        let a = RnsRational::new(big.clone(), BigInt::from(1), Basis::with_bits(40));
168        let c = RnsRational::from_int(1, Basis::with_bits(40));
169        let plan = d.plan_add(&a, &c);
170        assert!(!plan.fits());
171        d.provision(&plan);
172        assert!(d.basis().capacity_bits() >= plan.required_bits);
173    }
174
175    #[test]
176    fn execution_is_exact() {
177        let d = Dispatcher::new(b());
178        let sixth = RnsRational::from_fraction(1, 6, b());
179        let quarter = RnsRational::from_fraction(1, 4, b());
180        assert_eq!(
181            d.execute_add(&sixth, &quarter),
182            RnsRational::from_fraction(5, 12, b())
183        );
184    }
185}