Skip to main content

adele_ring/
algebraic.rs

1//! Level 2 — ℚ̄. Exact algebraic numbers as (minimal polynomial, isolating
2//! interval) pairs.
3//!
4//! We never store the decimal expansion of √2: we store that it is *the unique
5//! root of `x² - 2` in `(1, 2)`*. Arithmetic on algebraic numbers is done with
6//! **resultants** — the sum α+β is a root of `Res_y(p(y), q(x-y))` and the
7//! product is a root of `Res_y(p(y), yᵈ q(x/y))` — followed by factorization
8//! over ℚ and re-isolation of the relevant root.
9//!
10//! Resultants are computed as Sylvester-matrix determinants over the polynomial
11//! ring ℚ[x], using the fraction-free Bareiss algorithm so every intermediate
12//! division is exact.
13
14use num_bigint::BigInt;
15use num_integer::Integer;
16use num_traits::{One, Signed, ToPrimitive, Zero};
17
18use crate::basis::Basis;
19use crate::primes::{lcm as u64_lcm, mod_inverse};
20use crate::rational::RnsRational;
21use crate::rns::crt_balanced;
22
23/// A univariate polynomial with exact rational coefficients.
24///
25/// `coeffs[i]` is the coefficient of `xⁱ`. Trailing zero coefficients are
26/// trimmed so the last entry (if any) is the nonzero leading coefficient.
27#[derive(Clone, Debug)]
28pub struct Polynomial {
29    pub coeffs: Vec<RnsRational>,
30    pub channels: Basis,
31}
32
33impl Polynomial {
34    /// Build from coefficients (ascending powers), trimming trailing zeros.
35    pub fn new(coeffs: Vec<RnsRational>, channels: Basis) -> Self {
36        let mut p = Polynomial { coeffs, channels };
37        p.trim();
38        p
39    }
40
41    /// Build from integer coefficients (ascending powers).
42    pub fn from_int_coeffs(coeffs: &[i64], channels: Basis) -> Self {
43        let c = coeffs
44            .iter()
45            .map(|&v| RnsRational::from_int(v, channels.clone()))
46            .collect();
47        Self::new(c, channels)
48    }
49
50    fn r_zero(&self) -> RnsRational {
51        RnsRational::zero(self.channels.clone())
52    }
53    fn r_one(&self) -> RnsRational {
54        RnsRational::from_int(1, self.channels.clone())
55    }
56
57    fn trim(&mut self) {
58        while self.coeffs.len() > 0 && self.coeffs.last().unwrap().is_zero() {
59            self.coeffs.pop();
60        }
61    }
62
63    /// The zero polynomial.
64    pub fn zero(channels: Basis) -> Self {
65        Polynomial { coeffs: vec![], channels }
66    }
67
68    /// The constant polynomial `1`.
69    pub fn one(channels: Basis) -> Self {
70        Self::from_int_coeffs(&[1], channels)
71    }
72
73    /// A constant polynomial.
74    pub fn constant(c: RnsRational) -> Self {
75        let ch = c.channels.clone();
76        Self::new(vec![c], ch)
77    }
78
79    /// `true` iff this is the zero polynomial.
80    pub fn is_zero(&self) -> bool {
81        self.coeffs.is_empty()
82    }
83
84    /// Degree; the zero polynomial has degree 0 by convention here (callers
85    /// should also check [`Polynomial::is_zero`]).
86    pub fn degree(&self) -> usize {
87        self.coeffs.len().saturating_sub(1)
88    }
89
90    /// The leading (highest-power) coefficient, or zero for the zero polynomial.
91    pub fn leading(&self) -> RnsRational {
92        self.coeffs.last().cloned().unwrap_or_else(|| self.r_zero())
93    }
94
95    /// Evaluate at a rational point via Horner's scheme.
96    pub fn eval(&self, x: &RnsRational) -> RnsRational {
97        let mut acc = self.r_zero();
98        for c in self.coeffs.iter().rev() {
99            acc = acc.mul(x).add(c);
100        }
101        acc
102    }
103
104    /// Sign of `p(x)`: `-1`, `0`, or `+1`.
105    pub fn sign_at(&self, x: &RnsRational) -> i32 {
106        self.eval(x).signum()
107    }
108
109    /// Formal derivative.
110    pub fn derivative(&self) -> Self {
111        if self.coeffs.len() <= 1 {
112            return Self::zero(self.channels.clone());
113        }
114        let coeffs = self
115            .coeffs
116            .iter()
117            .enumerate()
118            .skip(1)
119            .map(|(i, c)| c.mul(&RnsRational::from_int(i as i64, self.channels.clone())))
120            .collect();
121        Self::new(coeffs, self.channels.clone())
122    }
123
124    /// Multiply by a rational scalar.
125    pub fn scalar_mul(&self, s: &RnsRational) -> Self {
126        Self::new(
127            self.coeffs.iter().map(|c| c.mul(s)).collect(),
128            self.channels.clone(),
129        )
130    }
131
132    /// Polynomial addition.
133    pub fn add(&self, other: &Self) -> Self {
134        let n = self.coeffs.len().max(other.coeffs.len());
135        let mut out = Vec::with_capacity(n);
136        for i in 0..n {
137            let a = self.coeffs.get(i).cloned().unwrap_or_else(|| self.r_zero());
138            let b = other.coeffs.get(i).cloned().unwrap_or_else(|| self.r_zero());
139            out.push(a.add(&b));
140        }
141        Self::new(out, self.channels.clone())
142    }
143
144    /// Polynomial subtraction.
145    pub fn sub(&self, other: &Self) -> Self {
146        self.add(&other.scalar_mul(&RnsRational::from_int(-1, self.channels.clone())))
147    }
148
149    /// Polynomial multiplication.
150    pub fn mul(&self, other: &Self) -> Self {
151        if self.is_zero() || other.is_zero() {
152            return Self::zero(self.channels.clone());
153        }
154        let mut out = vec![self.r_zero(); self.coeffs.len() + other.coeffs.len() - 1];
155        for (i, a) in self.coeffs.iter().enumerate() {
156            for (j, b) in other.coeffs.iter().enumerate() {
157                out[i + j] = out[i + j].add(&a.mul(b));
158            }
159        }
160        Self::new(out, self.channels.clone())
161    }
162
163    /// Long division returning `(quotient, remainder)` over the rational field.
164    /// Panics if `divisor` is zero.
165    pub fn divmod(&self, divisor: &Self) -> (Self, Self) {
166        assert!(!divisor.is_zero(), "polynomial division by zero");
167        let mut rem = self.clone();
168        let d_deg = divisor.degree();
169        let d_lead = divisor.leading();
170        if self.is_zero() || self.degree() < d_deg {
171            return (Self::zero(self.channels.clone()), rem);
172        }
173        let mut quot = vec![self.r_zero(); self.degree() - d_deg + 1];
174        while !rem.is_zero() && rem.degree() >= d_deg {
175            let shift = rem.degree() - d_deg;
176            let factor = rem.leading().div(&d_lead);
177            quot[shift] = factor.clone();
178            // rem -= factor * x^shift * divisor
179            let mut sub_coeffs = vec![self.r_zero(); shift];
180            for c in &divisor.coeffs {
181                sub_coeffs.push(c.mul(&factor));
182            }
183            let sub = Self::new(sub_coeffs, self.channels.clone());
184            rem = rem.sub(&sub);
185        }
186        (Self::new(quot, self.channels.clone()), rem)
187    }
188
189    /// Remainder of polynomial division.
190    pub fn rem(&self, divisor: &Self) -> Self {
191        self.divmod(divisor).1
192    }
193
194    /// Exact division (asserts the remainder is zero).
195    pub fn div_exact(&self, divisor: &Self) -> Self {
196        let (q, r) = self.divmod(divisor);
197        debug_assert!(r.is_zero(), "div_exact: non-zero remainder");
198        q
199    }
200
201    /// Make monic (leading coefficient 1). The zero polynomial is returned as-is.
202    pub fn monic(&self) -> Self {
203        if self.is_zero() {
204            return self.clone();
205        }
206        let inv = self.r_one().div(&self.leading());
207        self.scalar_mul(&inv)
208    }
209
210    /// Monic GCD of two polynomials (Euclidean algorithm over ℚ).
211    pub fn gcd(a: &Self, b: &Self) -> Self {
212        let mut x = a.clone();
213        let mut y = b.clone();
214        while !y.is_zero() {
215            let r = x.rem(&y);
216            x = y;
217            y = r;
218        }
219        x.monic()
220    }
221
222    /// Square-free part: `p / gcd(p, p')`, made monic.
223    pub fn squarefree(&self) -> Self {
224        if self.is_zero() || self.degree() == 0 {
225            return self.monic();
226        }
227        let g = Self::gcd(self, &self.derivative());
228        self.div_exact(&g).monic()
229    }
230
231    /// The Sturm sequence `s0 = p, s1 = p', s_{i+1} = -rem(s_{i-1}, s_i)`.
232    pub fn sturm_sequence(&self) -> Vec<Self> {
233        let mut seq = vec![self.clone(), self.derivative()];
234        while !seq.last().unwrap().is_zero() {
235            let n = seq.len();
236            let r = seq[n - 2].rem(&seq[n - 1]);
237            if r.is_zero() {
238                break;
239            }
240            seq.push(r.scalar_mul(&RnsRational::from_int(-1, self.channels.clone())));
241        }
242        seq
243    }
244
245    /// Count sign changes of a Sturm sequence evaluated at `x` (zeros ignored).
246    pub fn sign_changes(seq: &[Self], x: &RnsRational) -> usize {
247        let mut last = 0i32;
248        let mut changes = 0usize;
249        for s in seq {
250            let sign = s.sign_at(x);
251            if sign != 0 {
252                if last != 0 && sign != last {
253                    changes += 1;
254                }
255                last = sign;
256            }
257        }
258        changes
259    }
260
261    /// Number of distinct real roots in the half-open interval `(a, b]`.
262    pub fn sturm_root_count(&self, a: &RnsRational, b: &RnsRational) -> usize {
263        let seq = self.sturm_sequence();
264        let va = Self::sign_changes(&seq, a);
265        let vb = Self::sign_changes(&seq, b);
266        va.saturating_sub(vb)
267    }
268
269    /// A Cauchy bound `B` such that every real root lies in `(-B, B)`.
270    pub fn root_bound(&self) -> RnsRational {
271        if self.is_zero() || self.degree() == 0 {
272            return self.r_one();
273        }
274        let lead = self.leading();
275        let mut max_ratio = self.r_zero();
276        for c in &self.coeffs[..self.coeffs.len() - 1] {
277            let ratio = c.div(&lead).abs();
278            if ratio > max_ratio {
279                max_ratio = ratio;
280            }
281        }
282        self.r_one().add(&max_ratio)
283    }
284
285    /// Isolate every real root into its own interval `(lo, hi]`, sorted ascending.
286    /// `self` should be square-free for clean isolation.
287    pub fn isolate_real_roots(&self) -> Vec<(RnsRational, RnsRational)> {
288        let sf = self.squarefree();
289        if sf.degree() == 0 {
290            return Vec::new();
291        }
292        let seq = sf.sturm_sequence();
293        let b = sf.root_bound();
294        let lo = b.neg();
295        let hi = b;
296        let mut out = Vec::new();
297        // Minimum width guard to avoid pathological recursion (simple roots only).
298        let min_width = RnsRational::new(BigInt::one(), BigInt::one() << 80, sf.channels.clone());
299        Self::isolate_rec(&seq, &lo, &hi, &min_width, &mut out);
300        out.sort_by(|x, y| x.0.cmp(&y.0));
301        out
302    }
303
304    fn isolate_rec(
305        seq: &[Self],
306        lo: &RnsRational,
307        hi: &RnsRational,
308        min_width: &RnsRational,
309        out: &mut Vec<(RnsRational, RnsRational)>,
310    ) {
311        let cnt = Self::sign_changes(seq, lo).saturating_sub(Self::sign_changes(seq, hi));
312        if cnt == 0 {
313            return;
314        }
315        if cnt == 1 {
316            out.push((lo.clone(), hi.clone()));
317            return;
318        }
319        let width = hi.sub(lo);
320        if width < *min_width {
321            out.push((lo.clone(), hi.clone()));
322            return;
323        }
324        let mid = lo.midpoint(hi);
325        Self::isolate_rec(seq, lo, &mid, min_width, out);
326        Self::isolate_rec(seq, &mid, hi, min_width, out);
327    }
328
329    // ── Factorization over ℚ (sufficient for low-degree results) ─────────────
330
331    /// Clear denominators and the integer content, returning primitive integer
332    /// coefficients (ascending powers).
333    fn primitive_int_coeffs(&self) -> Vec<BigInt> {
334        if self.is_zero() {
335            return vec![BigInt::zero()];
336        }
337        // Common denominator across all coefficients.
338        let mut denom_lcm = 1u64;
339        let mut pairs = Vec::new();
340        for c in &self.coeffs {
341            let (p, q) = c.to_pair();
342            let qu = q.to_u64().unwrap_or(1);
343            denom_lcm = u64_lcm(denom_lcm, qu);
344            pairs.push((p, q));
345        }
346        let big_lcm = BigInt::from(denom_lcm);
347        let mut ints: Vec<BigInt> = pairs
348            .iter()
349            .map(|(p, q)| p * (&big_lcm / q))
350            .collect();
351        // Divide by integer content.
352        let mut content = BigInt::zero();
353        for v in &ints {
354            content = content.gcd(v);
355        }
356        if !content.is_zero() && content != BigInt::one() {
357            for v in &mut ints {
358                *v /= &content;
359            }
360        }
361        ints
362    }
363
364    /// Find one rational root via the rational-root theorem, or `None`.
365    pub fn find_rational_root(&self) -> Option<RnsRational> {
366        let ints = self.primitive_int_coeffs();
367        if ints.len() <= 1 {
368            return None;
369        }
370        let a0 = ints.first().unwrap().clone();
371        let an = ints.last().unwrap().clone();
372        // Zero is a root iff the constant term is zero.
373        if a0.is_zero() {
374            return Some(self.r_zero());
375        }
376        let p_divs = divisors(&a0.abs());
377        let q_divs = divisors(&an.abs());
378        for p in &p_divs {
379            for q in &q_divs {
380                for sign in [1i64, -1] {
381                    let cand = RnsRational::new(
382                        BigInt::from(sign) * p,
383                        q.clone(),
384                        self.channels.clone(),
385                    );
386                    if self.eval(&cand).is_zero() {
387                        return Some(cand);
388                    }
389                }
390            }
391        }
392        None
393    }
394
395    /// Factor into monic irreducible-over-ℚ factors (for the low degrees that
396    /// arise from resultants of quadratics/cubics, this is exact; higher-degree
397    /// factors that resist rational-root splitting are returned whole).
398    pub fn factor_over_q(&self) -> Vec<Self> {
399        let mut work = self.squarefree();
400        let mut factors = Vec::new();
401        loop {
402            if work.degree() == 0 {
403                break;
404            }
405            match work.find_rational_root() {
406                Some(r) => {
407                    // factor (x - r)
408                    let lin = Self::new(
409                        vec![r.neg(), work.r_one()],
410                        self.channels.clone(),
411                    );
412                    work = work.div_exact(&lin);
413                    factors.push(lin.monic());
414                }
415                None => break,
416            }
417        }
418        if work.degree() >= 1 {
419            factors.push(work.monic());
420        }
421        factors
422    }
423}
424
425impl PartialEq for Polynomial {
426    fn eq(&self, other: &Self) -> bool {
427        self.coeffs == other.coeffs
428    }
429}
430impl Eq for Polynomial {}
431
432/// Positive integer divisors of `n` (assumes `n` fits in `u128`).
433fn divisors(n: &BigInt) -> Vec<BigInt> {
434    let mut out = vec![BigInt::one()];
435    let n_u = match n.to_u128() {
436        Some(v) if v > 0 => v,
437        _ => return out,
438    };
439    let mut divs = Vec::new();
440    let mut d = 1u128;
441    while d * d <= n_u {
442        if n_u % d == 0 {
443            divs.push(d);
444            if d != n_u / d {
445                divs.push(n_u / d);
446            }
447        }
448        d += 1;
449    }
450    out.clear();
451    for v in divs {
452        out.push(BigInt::from(v));
453    }
454    out
455}
456
457// ── Bivariate machinery for resultants ───────────────────────────────────────
458//
459// A bivariate polynomial in `y` is represented as `Vec<Polynomial>`, where entry
460// `k` is the coefficient of `yᵏ` and is itself a polynomial in `x`.
461type BiPoly = Vec<Polynomial>;
462
463fn bi_degree(p: &BiPoly) -> usize {
464    let mut d = 0;
465    for (i, c) in p.iter().enumerate() {
466        if !c.is_zero() {
467            d = i;
468        }
469    }
470    d
471}
472
473/// Resultant of two polynomials in `y` (coefficients in ℚ[x]) w.r.t. `y`,
474/// returned as a polynomial in `x`, computed as the Sylvester determinant.
475///
476/// This is **multimodular** (refactor plan §3/§6): the Sylvester matrix is
477/// cleared of denominators to integers, an a-priori coefficient bound sizes an
478/// adaptive [`Basis`], then the determinant is computed modulo each prime *in
479/// parallel* — each prime's determinant via evaluation/interpolation in `x`
480/// (scalar Gaussian elimination over the field 𝔽_p, no fraction-free subtleties)
481/// — and the coefficients are CRT-reconstructed. No monolithic BigInt
482/// determinant is ever formed; `BigInt` appears only at the CRT boundary.
483///
484/// Row scaling during denominator-clearing multiplies the determinant by a
485/// nonzero constant, which does not change the resultant's *roots* — all the
486/// algebraic layer consumes (it factors / monicizes the result downstream).
487fn resultant_y(a: &BiPoly, b: &BiPoly, channels: &Basis) -> Polynomial {
488    let m = bi_degree(a);
489    let n = bi_degree(b);
490    let size = m + n;
491    if size == 0 {
492        return Polynomial::one(channels.clone());
493    }
494    let zero = Polynomial::zero(channels.clone());
495    let mut mat = vec![vec![zero.clone(); size]; size];
496
497    // Rows from `a`: coefficients high→low, shifted right by the row index.
498    for i in 0..n {
499        for j in 0..=m {
500            mat[i][i + j] = a[m - j].clone();
501        }
502    }
503    // Rows from `b`.
504    for i in 0..m {
505        for j in 0..=n {
506            mat[n + i][i + j] = b[n - j].clone();
507        }
508    }
509
510    let int_mat = clear_denominators(&mat);
511    let det = multimodular_det(&int_mat);
512    int_poly_to_polynomial(&det, channels)
513}
514
515/// An integer polynomial in `x`, ascending coefficients.
516type IntPoly = Vec<BigInt>;
517
518/// Clear denominators row-by-row, returning an integer Sylvester matrix. Each
519/// row is scaled by its denominator LCM (a constant ⇒ only a constant change to
520/// the determinant ⇒ identical roots).
521fn clear_denominators(mat: &[Vec<Polynomial>]) -> Vec<Vec<IntPoly>> {
522    mat.iter()
523        .map(|row| {
524            let mut l = BigInt::one();
525            for entry in row {
526                for c in &entry.coeffs {
527                    let (_, q) = c.to_pair();
528                    l = l.lcm(&q);
529                }
530            }
531            row.iter()
532                .map(|entry| {
533                    entry
534                        .coeffs
535                        .iter()
536                        .map(|c| {
537                            let (p, q) = c.to_pair();
538                            p * (&l / &q)
539                        })
540                        .collect::<IntPoly>()
541                })
542                .collect()
543        })
544        .collect()
545}
546
547/// Multimodular determinant of an `N×N` matrix of integer polynomials in `x`.
548fn multimodular_det(mat: &[Vec<IntPoly>]) -> IntPoly {
549    let n = mat.len();
550    if n == 0 {
551        return vec![BigInt::one()];
552    }
553
554    // Degree-in-x bound: the determinant's x-degree is at most the sum of the
555    // per-row maximum entry degrees.
556    let deg_x: usize = mat
557        .iter()
558        .map(|row| row.iter().map(|e| e.len().saturating_sub(1)).max().unwrap_or(0))
559        .sum();
560    let slots = deg_x + 1;
561
562    // Coefficient height bound: |det coeff| ≤ ∏_i (Σ_j ‖entry_ij‖₁) — the
563    // permanent of the 1-norm matrix, itself ≤ the product of row sums. Provision
564    // a basis whose product M exceeds twice that (balanced lift needs M > 2|c|).
565    let mut bits: u64 = 2; // sign + slack
566    for row in mat {
567        let row_sum: BigInt = row
568            .iter()
569            .map(|e| e.iter().map(|c| c.abs()).sum::<BigInt>())
570            .sum();
571        bits += row_sum.bits().max(1);
572    }
573    let basis = Basis::with_bits(bits);
574    let moduli = basis.moduli().to_vec();
575
576    // Determinant polynomial modulo each prime, in parallel.
577    use rayon::prelude::*;
578    let per_prime: Vec<Vec<u32>> = moduli
579        .par_iter()
580        .map(|&p| det_poly_mod_p(mat, deg_x, p as u64))
581        .collect();
582
583    // CRT each coefficient slot across all primes (balanced lift).
584    (0..slots)
585        .map(|s| {
586            let residues: Vec<u32> = per_prime.iter().map(|poly| poly[s]).collect();
587            crt_balanced(&residues, &moduli)
588        })
589        .collect()
590}
591
592/// One prime's determinant polynomial (mod `p`), via evaluation at `deg_x + 1`
593/// points and interpolation. Returns `deg_x + 1` coefficients in `[0, p)`.
594fn det_poly_mod_p(mat: &[Vec<IntPoly>], deg_x: usize, p: u64) -> Vec<u32> {
595    let n = mat.len();
596    let num_points = deg_x + 1;
597    let mut ys = Vec::with_capacity(num_points);
598    for t in 0..num_points {
599        let tt = (t as u64) % p;
600        let mut a = vec![vec![0u64; n]; n];
601        for (i, row) in mat.iter().enumerate() {
602            for (j, entry) in row.iter().enumerate() {
603                a[i][j] = eval_int_poly_mod(entry, tt, p);
604            }
605        }
606        ys.push(det_scalar_mod_p(a, p));
607    }
608    interpolate_mod_p(&ys, p).into_iter().map(|c| c as u32).collect()
609}
610
611/// Horner evaluation of an integer polynomial at `t`, modulo `p`.
612fn eval_int_poly_mod(poly: &[BigInt], t: u64, p: u64) -> u64 {
613    let pb = BigInt::from(p);
614    let mut acc = 0u64;
615    for c in poly.iter().rev() {
616        let cm = c.mod_floor(&pb).to_u64().unwrap_or(0);
617        acc = (acc * (t % p) + cm) % p;
618    }
619    acc
620}
621
622/// Determinant of a scalar matrix over the field 𝔽_p, by Gaussian elimination
623/// with partial pivoting. Returns a value in `[0, p)`.
624fn det_scalar_mod_p(mut a: Vec<Vec<u64>>, p: u64) -> u64 {
625    let n = a.len();
626    let mut det = 1u64;
627    for k in 0..n {
628        let pivot = (k..n).find(|&i| a[i][k] % p != 0);
629        let piv = match pivot {
630            Some(i) => i,
631            None => return 0, // singular mod p ⇒ det ≡ 0
632        };
633        if piv != k {
634            a.swap(piv, k);
635            det = (p - det) % p; // row swap flips the sign
636        }
637        let inv = mod_inverse(a[k][k], p).expect("nonzero element of a field is invertible");
638        det = det * (a[k][k] % p) % p;
639        let pivot_row = a[k].clone();
640        for row in a.iter_mut().skip(k + 1) {
641            let factor = row[k] * inv % p;
642            if factor == 0 {
643                continue;
644            }
645            for (j, &pj) in pivot_row.iter().enumerate().skip(k) {
646                let sub = factor * (pj % p) % p;
647                row[j] = (row[j] + p - sub) % p;
648            }
649        }
650    }
651    det % p
652}
653
654/// Interpolate the polynomial (mod `p`) through points `(0, ys[0]), (1, ys[1]),
655/// …` via Lagrange, returning monomial coefficients (ascending), `ys.len()` long.
656fn interpolate_mod_p(ys: &[u64], p: u64) -> Vec<u64> {
657    let np = ys.len();
658    let mut coeffs = vec![0u64; np];
659    for (s, &ys_s) in ys.iter().enumerate() {
660        // numerator poly ∏_{t≠s} (x - t), and denominator ∏_{t≠s} (s - t).
661        let mut num = vec![1u64];
662        let mut denom = 1u64;
663        for t in 0..np {
664            if t == s {
665                continue;
666            }
667            num = poly_mul_linear(&num, t as u64, p);
668            let diff = (s as i64 - t as i64).rem_euclid(p as i64) as u64;
669            denom = denom * diff % p;
670        }
671        let scale = ys_s % p * mod_inverse(denom, p).expect("distinct nodes are invertible") % p;
672        for (k, &nc) in num.iter().enumerate() {
673            coeffs[k] = (coeffs[k] + scale * nc) % p;
674        }
675    }
676    coeffs
677}
678
679/// Multiply an ascending-coefficient polynomial by the linear factor `(x - t)`,
680/// modulo `p`.
681fn poly_mul_linear(c: &[u64], t: u64, p: u64) -> Vec<u64> {
682    let mut r = vec![0u64; c.len() + 1];
683    for (k, &ck) in c.iter().enumerate() {
684        r[k + 1] = (r[k + 1] + ck) % p; // x · c[k]
685        let neg = (p - (t * ck % p)) % p; // -t · c[k]
686        r[k] = (r[k] + neg) % p;
687    }
688    r
689}
690
691/// Build a `Polynomial` (over `channels`) from integer coefficients.
692fn int_poly_to_polynomial(coeffs: &[BigInt], channels: &Basis) -> Polynomial {
693    let rcoeffs = coeffs
694        .iter()
695        .map(|c| RnsRational::new(c.clone(), BigInt::one(), channels.clone()))
696        .collect();
697    Polynomial::new(rcoeffs, channels.clone())
698}
699
700/// Lift a univariate `p(y)` to a `BiPoly` (constant-in-x coefficients).
701fn lift_const(p: &Polynomial) -> BiPoly {
702    p.coeffs.iter().map(|c| Polynomial::constant(c.clone())).collect()
703}
704
705/// Build `q(x - y)` as a `BiPoly` in `y`.
706fn shift_sub(q: &Polynomial, channels: &Basis) -> BiPoly {
707    // base = (x - y): as poly in y, coeff[0] = x, coeff[1] = -1.
708    let x_poly = Polynomial::from_int_coeffs(&[0, 1], channels.clone());
709    let neg_one = Polynomial::from_int_coeffs(&[-1], channels.clone());
710    let base: BiPoly = vec![x_poly, neg_one];
711
712    let mut acc: BiPoly = vec![Polynomial::zero(channels.clone())];
713    let mut power: BiPoly = vec![Polynomial::one(channels.clone())]; // (x - y)^0
714    for (j, c) in q.coeffs.iter().enumerate() {
715        if j > 0 {
716            power = bi_mul(&power, &base, channels);
717        }
718        let term = bi_scalar(&power, c);
719        acc = bi_add(&acc, &term, channels);
720    }
721    acc
722}
723
724/// Build `yᵈ · q(x/y)` as a `BiPoly` in `y` (for products).
725fn invert_scale(q: &Polynomial, channels: &Basis) -> BiPoly {
726    let d = q.degree();
727    let mut out: BiPoly = vec![Polynomial::zero(channels.clone()); d + 1];
728    for (j, c) in q.coeffs.iter().enumerate() {
729        // term c_j * x^j * y^(d-j)
730        let mut xj = vec![0i64; j + 1];
731        xj[j] = 1;
732        let x_pow = Polynomial::from_int_coeffs(&xj, channels.clone());
733        out[d - j] = x_pow.scalar_mul(c);
734    }
735    out
736}
737
738fn bi_add(a: &BiPoly, b: &BiPoly, channels: &Basis) -> BiPoly {
739    let n = a.len().max(b.len());
740    (0..n)
741        .map(|i| {
742            let za = a.get(i).cloned().unwrap_or_else(|| Polynomial::zero(channels.clone()));
743            let zb = b.get(i).cloned().unwrap_or_else(|| Polynomial::zero(channels.clone()));
744            za.add(&zb)
745        })
746        .collect()
747}
748
749fn bi_scalar(a: &BiPoly, s: &RnsRational) -> BiPoly {
750    a.iter().map(|c| c.scalar_mul(s)).collect()
751}
752
753fn bi_mul(a: &BiPoly, b: &BiPoly, channels: &Basis) -> BiPoly {
754    if a.is_empty() || b.is_empty() {
755        return vec![Polynomial::zero(channels.clone())];
756    }
757    let mut out = vec![Polynomial::zero(channels.clone()); a.len() + b.len() - 1];
758    for (i, ca) in a.iter().enumerate() {
759        for (j, cb) in b.iter().enumerate() {
760            out[i + j] = out[i + j].add(&ca.mul(cb));
761        }
762    }
763    out
764}
765
766/// An exact real algebraic number (Level 2).
767#[derive(Clone, Debug)]
768pub struct AlgebraicNumber {
769    /// An **annihilating** polynomial over ℚ: monic, with this number as a root.
770    ///
771    /// Resultants produce an annihilating polynomial that is not *necessarily*
772    /// minimal. [`AlgebraicNumber::try_minimize`] reduces it toward the minimal
773    /// polynomial (square-free + rational-root factorization); without full
774    /// factorization over ℚ this is best-effort, not guaranteed.
775    pub annihilating_poly: Polynomial,
776    /// Isolating interval `(lo, hi)` containing exactly one root of `annihilating_poly`.
777    pub interval: (RnsRational, RnsRational),
778    pub channels: Basis,
779}
780
781impl AlgebraicNumber {
782    /// The positive square root √n.
783    pub fn sqrt(n: u64, channels: Basis) -> Self {
784        // min poly x² - n, isolate the positive root in (0, n+1).
785        let annihilating_poly = Polynomial::from_int_coeffs(&[-(n as i64), 0, 1], channels.clone());
786        let lo = RnsRational::from_int(0, channels.clone());
787        let hi = RnsRational::from_int(n as i64 + 1, channels.clone());
788        Self::from_annihilating_poly_interval(annihilating_poly, lo, hi, channels)
789    }
790
791    /// The real cube root ∛n.
792    pub fn cbrt(n: u64, channels: Basis) -> Self {
793        let annihilating_poly = Polynomial::from_int_coeffs(&[-(n as i64), 0, 0, 1], channels.clone());
794        let lo = RnsRational::from_int(0, channels.clone());
795        let hi = RnsRational::from_int(n as i64 + 1, channels.clone());
796        Self::from_annihilating_poly_interval(annihilating_poly, lo, hi, channels)
797    }
798
799    /// A rational as a degree-1 algebraic number.
800    pub fn from_rational(r: RnsRational) -> Self {
801        let channels = r.channels.clone();
802        let annihilating_poly = Polynomial::new(
803            vec![r.neg(), RnsRational::from_int(1, channels.clone())],
804            channels.clone(),
805        );
806        let lo = r.sub(&RnsRational::from_int(1, channels.clone()));
807        let hi = r.add(&RnsRational::from_int(1, channels.clone()));
808        AlgebraicNumber { annihilating_poly, interval: (lo, hi), channels }
809    }
810
811    /// The `root_index`-th real root (ascending) of `p`.
812    pub fn from_poly_root(p: Polynomial, root_index: usize, channels: Basis) -> Self {
813        let roots = p.isolate_real_roots();
814        let (lo, hi) = roots
815            .get(root_index)
816            .cloned()
817            .expect("root_index out of range");
818        // Attach the irreducible factor that owns this root.
819        let factors = p.factor_over_q();
820        let annihilating_poly = Self::factor_for_interval(&factors, &lo, &hi).unwrap_or(p);
821        AlgebraicNumber { annihilating_poly, interval: (lo, hi), channels }
822    }
823
824    fn from_annihilating_poly_interval(
825        annihilating_poly: Polynomial,
826        lo: RnsRational,
827        hi: RnsRational,
828        channels: Basis,
829    ) -> Self {
830        let mut a = AlgebraicNumber { annihilating_poly, interval: (lo, hi), channels };
831        // Tighten a little so the interval cleanly isolates the root.
832        let target = RnsRational::new(BigInt::one(), BigInt::from(1_000_000), a.channels.clone());
833        a.refine_interval(&target);
834        a
835    }
836
837    /// Degree of the annihilating polynomial.
838    pub fn degree(&self) -> usize {
839        self.annihilating_poly.degree()
840    }
841
842    /// Reduce the annihilating polynomial toward the **minimal** polynomial by
843    /// square-free + rational-root factorization over ℚ, keeping the irreducible
844    /// factor whose real root matches this number. Best-effort: without full
845    /// factorization, high-degree irreducible factors are returned whole.
846    pub fn try_minimize(&self) -> AlgebraicNumber {
847        let factors = self.annihilating_poly.squarefree().factor_over_q();
848        let approx = self.to_f64();
849        let mut best: Option<(Polynomial, f64)> = None;
850        for f in &factors {
851            for (lo, hi) in f.isolate_real_roots() {
852                let mut cand = AlgebraicNumber {
853                    annihilating_poly: f.clone(),
854                    interval: (lo, hi),
855                    channels: self.channels.clone(),
856                };
857                let target = RnsRational::new(BigInt::one(), BigInt::from(1_000_000), self.channels.clone());
858                cand.refine_interval(&target);
859                let dist = (cand.to_f64() - approx).abs();
860                if best.as_ref().map(|(_, d)| dist < *d).unwrap_or(true) {
861                    best = Some((f.clone(), dist));
862                }
863            }
864        }
865        match best {
866            Some((f, _)) => AlgebraicNumber {
867                annihilating_poly: f,
868                interval: self.interval.clone(),
869                channels: self.channels.clone(),
870            },
871            None => self.clone(),
872        }
873    }
874
875    /// `Some(r)` iff this number is actually rational (degree-1 min poly).
876    pub fn to_rational(&self) -> Option<RnsRational> {
877        if self.annihilating_poly.degree() == 1 {
878            // c1 x + c0 = 0  =>  x = -c0/c1
879            let c0 = self.annihilating_poly.coeffs[0].clone();
880            let c1 = self.annihilating_poly.coeffs[1].clone();
881            Some(c0.neg().div(&c1))
882        } else {
883            None
884        }
885    }
886
887    /// Refine the isolating interval until its width is `< target_width`.
888    pub fn refine_interval(&mut self, target_width: &RnsRational) {
889        let (mut lo, mut hi) = self.interval.clone();
890        let sign_lo = self.annihilating_poly.sign_at(&lo);
891        // If an endpoint is exactly the root, collapse to it.
892        if sign_lo == 0 {
893            self.interval = (lo.clone(), lo);
894            return;
895        }
896        if self.annihilating_poly.sign_at(&hi) == 0 {
897            self.interval = (hi.clone(), hi);
898            return;
899        }
900        while hi.sub(&lo) >= *target_width {
901            let mid = lo.midpoint(&hi);
902            let sm = self.annihilating_poly.sign_at(&mid);
903            if sm == 0 {
904                lo = mid.clone();
905                hi = mid;
906                break;
907            } else if sm == sign_lo {
908                lo = mid;
909            } else {
910                hi = mid;
911            }
912        }
913        self.interval = (lo, hi);
914    }
915
916    /// An `f64` approximation (refines internally first).
917    pub fn to_f64(&self) -> f64 {
918        let mut clone = self.clone();
919        let target = RnsRational::new(BigInt::one(), BigInt::one() << 60, self.channels.clone());
920        clone.refine_interval(&target);
921        clone.interval.0.midpoint(&clone.interval.1).to_f64()
922    }
923
924    /// Exact sign of the number: `-1`, `0`, or `+1`.
925    pub fn sign(&self) -> i32 {
926        // Zero is a root iff the constant term of an irreducible min poly is 0
927        // (i.e. min poly is exactly x).
928        if self.annihilating_poly.degree() == 1 && self.annihilating_poly.coeffs[0].is_zero() {
929            return 0;
930        }
931        let mut clone = self.clone();
932        let zero = RnsRational::zero(self.channels.clone());
933        // Refine until the interval lies strictly on one side of 0.
934        let mut target = RnsRational::new(BigInt::one(), BigInt::from(1024), self.channels.clone());
935        for _ in 0..200 {
936            if clone.interval.0 > zero {
937                return 1;
938            }
939            if clone.interval.1 < zero {
940                return -1;
941            }
942            clone.refine_interval(&target);
943            target = target.mul(&RnsRational::from_fraction(1, 2, self.channels.clone()));
944        }
945        clone.interval.0.midpoint(&clone.interval.1).signum()
946    }
947
948    /// Additive inverse: negate the root (`x -> -x` in the min poly).
949    pub fn neg(&self) -> Self {
950        let coeffs = self
951            .annihilating_poly
952            .coeffs
953            .iter()
954            .enumerate()
955            .map(|(i, c)| if i % 2 == 1 { c.neg() } else { c.clone() })
956            .collect();
957        let annihilating_poly = Polynomial::new(coeffs, self.channels.clone()).monic();
958        AlgebraicNumber {
959            annihilating_poly,
960            interval: (self.interval.1.neg(), self.interval.0.neg()),
961            channels: self.channels.clone(),
962        }
963    }
964
965    /// Multiplicative inverse: reciprocal of the root (reverse the coefficients).
966    pub fn recip(&self) -> Self {
967        let mut coeffs = self.annihilating_poly.coeffs.clone();
968        coeffs.reverse();
969        let annihilating_poly = Polynomial::new(coeffs, self.channels.clone()).monic();
970        let v = self.to_f64();
971        Self::reconstruct(annihilating_poly, 1.0 / v, self.channels.clone())
972    }
973
974    /// α + β.
975    pub fn add(&self, other: &Self) -> Self {
976        let channels = self.channels.clone();
977        let a = lift_const(&self.annihilating_poly);
978        let b = shift_sub(&other.annihilating_poly, &channels);
979        let res = resultant_y(&a, &b, &channels);
980        let value = self.to_f64() + other.to_f64();
981        Self::reconstruct(res, value, channels)
982    }
983
984    /// α × β.
985    pub fn mul(&self, other: &Self) -> Self {
986        let channels = self.channels.clone();
987        let a = lift_const(&self.annihilating_poly);
988        let b = invert_scale(&other.annihilating_poly, &channels);
989        let res = resultant_y(&a, &b, &channels);
990        let value = self.to_f64() * other.to_f64();
991        Self::reconstruct(res, value, channels)
992    }
993
994    /// From a resultant polynomial plus an approximate value, pick the
995    /// irreducible factor and isolating interval that own the target root.
996    fn reconstruct(res: Polynomial, approx: f64, channels: Basis) -> Self {
997        let factors = res.factor_over_q();
998        // Choose the factor whose nearest real root is closest to `approx`.
999        let mut best: Option<(AlgebraicNumber, f64)> = None;
1000        for f in &factors {
1001            for (lo, hi) in f.isolate_real_roots() {
1002                let mut cand = AlgebraicNumber {
1003                    annihilating_poly: f.clone(),
1004                    interval: (lo, hi),
1005                    channels: channels.clone(),
1006                };
1007                let target = RnsRational::new(BigInt::one(), BigInt::from(1_000_000), channels.clone());
1008                cand.refine_interval(&target);
1009                let dist = (cand.to_f64() - approx).abs();
1010                if best.as_ref().map(|(_, d)| dist < *d).unwrap_or(true) {
1011                    best = Some((cand, dist));
1012                }
1013            }
1014        }
1015        best.map(|(a, _)| a).expect("resultant had no real root near target")
1016    }
1017
1018    fn factor_for_interval(
1019        factors: &[Polynomial],
1020        lo: &RnsRational,
1021        hi: &RnsRational,
1022    ) -> Option<Polynomial> {
1023        for f in factors {
1024            if f.sturm_root_count(lo, hi) >= 1 {
1025                return Some(f.monic());
1026            }
1027        }
1028        None
1029    }
1030}
1031
1032#[cfg(test)]
1033mod tests {
1034    use super::*;
1035
1036    fn ch() -> Basis {
1037        Basis::standard()
1038    }
1039
1040    #[test]
1041    fn sqrt2_annihilating_poly() {
1042        let s = AlgebraicNumber::sqrt(2, ch());
1043        assert_eq!(s.degree(), 2);
1044        // 1² - 2 < 0
1045        assert_eq!(
1046            s.annihilating_poly.sign_at(&RnsRational::from_int(1, ch())),
1047            -1
1048        );
1049        assert!(s.interval.0 < s.interval.1);
1050        assert!(s.to_rational().is_none());
1051        assert!((s.to_f64() - 2f64.sqrt()).abs() < 1e-9);
1052    }
1053
1054    #[test]
1055    fn from_rational_roundtrip() {
1056        let r = RnsRational::from_fraction(3, 5, ch());
1057        let a = AlgebraicNumber::from_rational(r.clone());
1058        assert_eq!(a.to_rational(), Some(r));
1059    }
1060
1061    #[test]
1062    fn sturm_counts() {
1063        // x² - 2 has one root in (-2,-1], one in (1,2], two in (-2,2].
1064        let p = Polynomial::from_int_coeffs(&[-2, 0, 1], ch());
1065        let r = |a: i64, b: i64| {
1066            p.sturm_root_count(
1067                &RnsRational::from_int(a, ch()),
1068                &RnsRational::from_int(b, ch()),
1069            )
1070        };
1071        assert_eq!(r(-2, -1), 1);
1072        assert_eq!(r(1, 2), 1);
1073        assert_eq!(r(-2, 2), 2);
1074    }
1075
1076    #[test]
1077    fn sqrt2_times_sqrt2_is_two() {
1078        let s = AlgebraicNumber::sqrt(2, ch());
1079        let p = s.mul(&s);
1080        // √2·√2 = 2  =>  degree-1 min poly, rational value 2.
1081        assert_eq!(p.degree(), 1);
1082        assert_eq!(p.to_rational(), Some(RnsRational::from_int(2, ch())));
1083    }
1084
1085    #[test]
1086    fn sqrt2_times_sqrt3_is_sqrt6() {
1087        let s2 = AlgebraicNumber::sqrt(2, ch());
1088        let s3 = AlgebraicNumber::sqrt(3, ch());
1089        let p = s2.mul(&s3);
1090        assert_eq!(p.degree(), 2);
1091        // min poly x² - 6
1092        let expected = Polynomial::from_int_coeffs(&[-6, 0, 1], ch()).monic();
1093        assert_eq!(p.annihilating_poly, expected);
1094    }
1095
1096    #[test]
1097    fn sqrt2_plus_sqrt2_is_2sqrt2() {
1098        let s = AlgebraicNumber::sqrt(2, ch());
1099        let p = s.add(&s);
1100        // 2√2 has min poly x² - 8.
1101        assert_eq!(p.degree(), 2);
1102        let expected = Polynomial::from_int_coeffs(&[-8, 0, 1], ch()).monic();
1103        assert_eq!(p.annihilating_poly, expected);
1104    }
1105
1106    #[test]
1107    fn refine_narrows() {
1108        let mut s = AlgebraicNumber::sqrt(2, ch());
1109        let target = RnsRational::new(BigInt::one(), BigInt::from(10).pow(20), ch());
1110        s.refine_interval(&target);
1111        assert!(s.interval.1.sub(&s.interval.0) < target);
1112    }
1113
1114    // ── Multimodular-determinant oracle tests (refactor plan §11) ────────────
1115
1116    /// Direct fraction-free (Bareiss) determinant over ℤ — the trusted oracle.
1117    fn det_bigint(mut m: Vec<Vec<BigInt>>) -> BigInt {
1118        let n = m.len();
1119        if n == 0 {
1120            return BigInt::one();
1121        }
1122        let mut sign = 1i32;
1123        let mut prev = BigInt::one();
1124        for k in 0..n - 1 {
1125            if m[k][k].is_zero() {
1126                let swap = (k + 1..n).find(|&i| !m[i][k].is_zero());
1127                match swap {
1128                    Some(i) => {
1129                        m.swap(k, i);
1130                        sign = -sign;
1131                    }
1132                    None => return BigInt::zero(),
1133                }
1134            }
1135            for i in k + 1..n {
1136                for j in k + 1..n {
1137                    let num = &m[i][j] * &m[k][k] - &m[i][k] * &m[k][j];
1138                    m[i][j] = num / &prev;
1139                }
1140                m[i][k] = BigInt::zero();
1141            }
1142            prev = m[k][k].clone();
1143        }
1144        let det = m[n - 1][n - 1].clone();
1145        if sign < 0 {
1146            -det
1147        } else {
1148            det
1149        }
1150    }
1151
1152    fn eval_int_poly(poly: &[BigInt], x: &BigInt) -> BigInt {
1153        let mut acc = BigInt::zero();
1154        for c in poly.iter().rev() {
1155            acc = acc * x + c;
1156        }
1157        acc
1158    }
1159
1160    /// Tiny deterministic LCG so the oracle tests need no `rand` dependency.
1161    struct Lcg(u64);
1162    impl Lcg {
1163        fn next(&mut self) -> u64 {
1164            self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
1165            self.0 >> 33
1166        }
1167        /// Signed integer in `[-range, range]`.
1168        fn coeff(&mut self, range: i64) -> i64 {
1169            (self.next() as i64).rem_euclid(2 * range + 1) - range
1170        }
1171    }
1172
1173    #[test]
1174    fn multimodular_det_matches_bigint_scalar() {
1175        let mut rng = Lcg(0x1234_5678_9abc_def0);
1176        for n in 1..=6usize {
1177            for _ in 0..20 {
1178                // A random scalar (constant-in-x) integer matrix.
1179                let scalar: Vec<Vec<BigInt>> = (0..n)
1180                    .map(|_| (0..n).map(|_| BigInt::from(rng.coeff(50))).collect())
1181                    .collect();
1182                let int_mat: Vec<Vec<IntPoly>> = scalar
1183                    .iter()
1184                    .map(|row| row.iter().map(|c| vec![c.clone()]).collect())
1185                    .collect();
1186
1187                let expected = det_bigint(scalar);
1188                let got = multimodular_det(&int_mat);
1189                // Constant polynomial: only the x⁰ slot is significant.
1190                let got0 = got.first().cloned().unwrap_or_else(BigInt::zero);
1191                assert_eq!(got0, expected, "n={n}");
1192                assert!(got.iter().skip(1).all(|c| c.is_zero()));
1193            }
1194        }
1195    }
1196
1197    #[test]
1198    fn multimodular_det_matches_bigint_polynomial() {
1199        let mut rng = Lcg(0xfeed_face_cafe_d00d);
1200        for n in 1..=5usize {
1201            for _ in 0..20 {
1202                // Each entry a random degree-≤1 integer polynomial in x.
1203                let int_mat: Vec<Vec<IntPoly>> = (0..n)
1204                    .map(|_| {
1205                        (0..n)
1206                            .map(|_| {
1207                                vec![
1208                                    BigInt::from(rng.coeff(20)),
1209                                    BigInt::from(rng.coeff(20)),
1210                                ]
1211                            })
1212                            .collect()
1213                    })
1214                    .collect();
1215
1216                let det_poly = multimodular_det(&int_mat);
1217
1218                // Certify by evaluating the determinant polynomial at several
1219                // integer points against a direct scalar BigInt determinant.
1220                for x in [-3i64, -1, 0, 2, 5, 11] {
1221                    let xb = BigInt::from(x);
1222                    let scalar: Vec<Vec<BigInt>> = int_mat
1223                        .iter()
1224                        .map(|row| row.iter().map(|e| eval_int_poly(e, &xb)).collect())
1225                        .collect();
1226                    let expected = det_bigint(scalar);
1227                    let got = eval_int_poly(&det_poly, &xb);
1228                    assert_eq!(got, expected, "n={n}, x={x}");
1229                }
1230            }
1231        }
1232    }
1233}