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::primes::lcm as u64_lcm;
19use crate::rational::RnsRational;
20use crate::rns::Channels;
21
22/// A univariate polynomial with exact rational coefficients.
23///
24/// `coeffs[i]` is the coefficient of `xⁱ`. Trailing zero coefficients are
25/// trimmed so the last entry (if any) is the nonzero leading coefficient.
26#[derive(Clone, Debug)]
27pub struct Polynomial {
28    pub coeffs: Vec<RnsRational>,
29    pub channels: Channels,
30}
31
32impl Polynomial {
33    /// Build from coefficients (ascending powers), trimming trailing zeros.
34    pub fn new(coeffs: Vec<RnsRational>, channels: Channels) -> Self {
35        let mut p = Polynomial { coeffs, channels };
36        p.trim();
37        p
38    }
39
40    /// Build from integer coefficients (ascending powers).
41    pub fn from_int_coeffs(coeffs: &[i64], channels: Channels) -> Self {
42        let c = coeffs
43            .iter()
44            .map(|&v| RnsRational::from_int(v, channels.clone()))
45            .collect();
46        Self::new(c, channels)
47    }
48
49    fn r_zero(&self) -> RnsRational {
50        RnsRational::zero(self.channels.clone())
51    }
52    fn r_one(&self) -> RnsRational {
53        RnsRational::from_int(1, self.channels.clone())
54    }
55
56    fn trim(&mut self) {
57        while self.coeffs.len() > 0 && self.coeffs.last().unwrap().is_zero() {
58            self.coeffs.pop();
59        }
60    }
61
62    /// The zero polynomial.
63    pub fn zero(channels: Channels) -> Self {
64        Polynomial { coeffs: vec![], channels }
65    }
66
67    /// The constant polynomial `1`.
68    pub fn one(channels: Channels) -> Self {
69        Self::from_int_coeffs(&[1], channels)
70    }
71
72    /// A constant polynomial.
73    pub fn constant(c: RnsRational) -> Self {
74        let ch = c.channels.clone();
75        Self::new(vec![c], ch)
76    }
77
78    /// `true` iff this is the zero polynomial.
79    pub fn is_zero(&self) -> bool {
80        self.coeffs.is_empty()
81    }
82
83    /// Degree; the zero polynomial has degree 0 by convention here (callers
84    /// should also check [`Polynomial::is_zero`]).
85    pub fn degree(&self) -> usize {
86        self.coeffs.len().saturating_sub(1)
87    }
88
89    /// The leading (highest-power) coefficient, or zero for the zero polynomial.
90    pub fn leading(&self) -> RnsRational {
91        self.coeffs.last().cloned().unwrap_or_else(|| self.r_zero())
92    }
93
94    /// Evaluate at a rational point via Horner's scheme.
95    pub fn eval(&self, x: &RnsRational) -> RnsRational {
96        let mut acc = self.r_zero();
97        for c in self.coeffs.iter().rev() {
98            acc = acc.mul(x).add(c);
99        }
100        acc
101    }
102
103    /// Sign of `p(x)`: `-1`, `0`, or `+1`.
104    pub fn sign_at(&self, x: &RnsRational) -> i32 {
105        self.eval(x).signum()
106    }
107
108    /// Formal derivative.
109    pub fn derivative(&self) -> Self {
110        if self.coeffs.len() <= 1 {
111            return Self::zero(self.channels.clone());
112        }
113        let coeffs = self
114            .coeffs
115            .iter()
116            .enumerate()
117            .skip(1)
118            .map(|(i, c)| c.mul(&RnsRational::from_int(i as i64, self.channels.clone())))
119            .collect();
120        Self::new(coeffs, self.channels.clone())
121    }
122
123    /// Multiply by a rational scalar.
124    pub fn scalar_mul(&self, s: &RnsRational) -> Self {
125        Self::new(
126            self.coeffs.iter().map(|c| c.mul(s)).collect(),
127            self.channels.clone(),
128        )
129    }
130
131    /// Polynomial addition.
132    pub fn add(&self, other: &Self) -> Self {
133        let n = self.coeffs.len().max(other.coeffs.len());
134        let mut out = Vec::with_capacity(n);
135        for i in 0..n {
136            let a = self.coeffs.get(i).cloned().unwrap_or_else(|| self.r_zero());
137            let b = other.coeffs.get(i).cloned().unwrap_or_else(|| self.r_zero());
138            out.push(a.add(&b));
139        }
140        Self::new(out, self.channels.clone())
141    }
142
143    /// Polynomial subtraction.
144    pub fn sub(&self, other: &Self) -> Self {
145        self.add(&other.scalar_mul(&RnsRational::from_int(-1, self.channels.clone())))
146    }
147
148    /// Polynomial multiplication.
149    pub fn mul(&self, other: &Self) -> Self {
150        if self.is_zero() || other.is_zero() {
151            return Self::zero(self.channels.clone());
152        }
153        let mut out = vec![self.r_zero(); self.coeffs.len() + other.coeffs.len() - 1];
154        for (i, a) in self.coeffs.iter().enumerate() {
155            for (j, b) in other.coeffs.iter().enumerate() {
156                out[i + j] = out[i + j].add(&a.mul(b));
157            }
158        }
159        Self::new(out, self.channels.clone())
160    }
161
162    /// Long division returning `(quotient, remainder)` over the rational field.
163    /// Panics if `divisor` is zero.
164    pub fn divmod(&self, divisor: &Self) -> (Self, Self) {
165        assert!(!divisor.is_zero(), "polynomial division by zero");
166        let mut rem = self.clone();
167        let d_deg = divisor.degree();
168        let d_lead = divisor.leading();
169        if self.is_zero() || self.degree() < d_deg {
170            return (Self::zero(self.channels.clone()), rem);
171        }
172        let mut quot = vec![self.r_zero(); self.degree() - d_deg + 1];
173        while !rem.is_zero() && rem.degree() >= d_deg {
174            let shift = rem.degree() - d_deg;
175            let factor = rem.leading().div(&d_lead);
176            quot[shift] = factor.clone();
177            // rem -= factor * x^shift * divisor
178            let mut sub_coeffs = vec![self.r_zero(); shift];
179            for c in &divisor.coeffs {
180                sub_coeffs.push(c.mul(&factor));
181            }
182            let sub = Self::new(sub_coeffs, self.channels.clone());
183            rem = rem.sub(&sub);
184        }
185        (Self::new(quot, self.channels.clone()), rem)
186    }
187
188    /// Remainder of polynomial division.
189    pub fn rem(&self, divisor: &Self) -> Self {
190        self.divmod(divisor).1
191    }
192
193    /// Exact division (asserts the remainder is zero).
194    pub fn div_exact(&self, divisor: &Self) -> Self {
195        let (q, r) = self.divmod(divisor);
196        debug_assert!(r.is_zero(), "div_exact: non-zero remainder");
197        q
198    }
199
200    /// Make monic (leading coefficient 1). The zero polynomial is returned as-is.
201    pub fn monic(&self) -> Self {
202        if self.is_zero() {
203            return self.clone();
204        }
205        let inv = self.r_one().div(&self.leading());
206        self.scalar_mul(&inv)
207    }
208
209    /// Monic GCD of two polynomials (Euclidean algorithm over ℚ).
210    pub fn gcd(a: &Self, b: &Self) -> Self {
211        let mut x = a.clone();
212        let mut y = b.clone();
213        while !y.is_zero() {
214            let r = x.rem(&y);
215            x = y;
216            y = r;
217        }
218        x.monic()
219    }
220
221    /// Square-free part: `p / gcd(p, p')`, made monic.
222    pub fn squarefree(&self) -> Self {
223        if self.is_zero() || self.degree() == 0 {
224            return self.monic();
225        }
226        let g = Self::gcd(self, &self.derivative());
227        self.div_exact(&g).monic()
228    }
229
230    /// The Sturm sequence `s0 = p, s1 = p', s_{i+1} = -rem(s_{i-1}, s_i)`.
231    pub fn sturm_sequence(&self) -> Vec<Self> {
232        let mut seq = vec![self.clone(), self.derivative()];
233        while !seq.last().unwrap().is_zero() {
234            let n = seq.len();
235            let r = seq[n - 2].rem(&seq[n - 1]);
236            if r.is_zero() {
237                break;
238            }
239            seq.push(r.scalar_mul(&RnsRational::from_int(-1, self.channels.clone())));
240        }
241        seq
242    }
243
244    /// Count sign changes of a Sturm sequence evaluated at `x` (zeros ignored).
245    pub fn sign_changes(seq: &[Self], x: &RnsRational) -> usize {
246        let mut last = 0i32;
247        let mut changes = 0usize;
248        for s in seq {
249            let sign = s.sign_at(x);
250            if sign != 0 {
251                if last != 0 && sign != last {
252                    changes += 1;
253                }
254                last = sign;
255            }
256        }
257        changes
258    }
259
260    /// Number of distinct real roots in the half-open interval `(a, b]`.
261    pub fn sturm_root_count(&self, a: &RnsRational, b: &RnsRational) -> usize {
262        let seq = self.sturm_sequence();
263        let va = Self::sign_changes(&seq, a);
264        let vb = Self::sign_changes(&seq, b);
265        va.saturating_sub(vb)
266    }
267
268    /// A Cauchy bound `B` such that every real root lies in `(-B, B)`.
269    pub fn root_bound(&self) -> RnsRational {
270        if self.is_zero() || self.degree() == 0 {
271            return self.r_one();
272        }
273        let lead = self.leading();
274        let mut max_ratio = self.r_zero();
275        for c in &self.coeffs[..self.coeffs.len() - 1] {
276            let ratio = c.div(&lead).abs();
277            if ratio > max_ratio {
278                max_ratio = ratio;
279            }
280        }
281        self.r_one().add(&max_ratio)
282    }
283
284    /// Isolate every real root into its own interval `(lo, hi]`, sorted ascending.
285    /// `self` should be square-free for clean isolation.
286    pub fn isolate_real_roots(&self) -> Vec<(RnsRational, RnsRational)> {
287        let sf = self.squarefree();
288        if sf.degree() == 0 {
289            return Vec::new();
290        }
291        let seq = sf.sturm_sequence();
292        let b = sf.root_bound();
293        let lo = b.neg();
294        let hi = b;
295        let mut out = Vec::new();
296        // Minimum width guard to avoid pathological recursion (simple roots only).
297        let min_width = RnsRational::new(BigInt::one(), BigInt::one() << 80, sf.channels.clone());
298        Self::isolate_rec(&seq, &lo, &hi, &min_width, &mut out);
299        out.sort_by(|x, y| x.0.cmp(&y.0));
300        out
301    }
302
303    fn isolate_rec(
304        seq: &[Self],
305        lo: &RnsRational,
306        hi: &RnsRational,
307        min_width: &RnsRational,
308        out: &mut Vec<(RnsRational, RnsRational)>,
309    ) {
310        let cnt = Self::sign_changes(seq, lo).saturating_sub(Self::sign_changes(seq, hi));
311        if cnt == 0 {
312            return;
313        }
314        if cnt == 1 {
315            out.push((lo.clone(), hi.clone()));
316            return;
317        }
318        let width = hi.sub(lo);
319        if width < *min_width {
320            out.push((lo.clone(), hi.clone()));
321            return;
322        }
323        let mid = lo.midpoint(hi);
324        Self::isolate_rec(seq, lo, &mid, min_width, out);
325        Self::isolate_rec(seq, &mid, hi, min_width, out);
326    }
327
328    // ── Factorization over ℚ (sufficient for low-degree results) ─────────────
329
330    /// Clear denominators and the integer content, returning primitive integer
331    /// coefficients (ascending powers).
332    fn primitive_int_coeffs(&self) -> Vec<BigInt> {
333        if self.is_zero() {
334            return vec![BigInt::zero()];
335        }
336        // Common denominator across all coefficients.
337        let mut denom_lcm = 1u64;
338        let mut pairs = Vec::new();
339        for c in &self.coeffs {
340            let (p, q) = c.to_pair();
341            let qu = q.to_u64().unwrap_or(1);
342            denom_lcm = u64_lcm(denom_lcm, qu);
343            pairs.push((p, q));
344        }
345        let big_lcm = BigInt::from(denom_lcm);
346        let mut ints: Vec<BigInt> = pairs
347            .iter()
348            .map(|(p, q)| p * (&big_lcm / q))
349            .collect();
350        // Divide by integer content.
351        let mut content = BigInt::zero();
352        for v in &ints {
353            content = content.gcd(v);
354        }
355        if !content.is_zero() && content != BigInt::one() {
356            for v in &mut ints {
357                *v /= &content;
358            }
359        }
360        ints
361    }
362
363    /// Find one rational root via the rational-root theorem, or `None`.
364    pub fn find_rational_root(&self) -> Option<RnsRational> {
365        let ints = self.primitive_int_coeffs();
366        if ints.len() <= 1 {
367            return None;
368        }
369        let a0 = ints.first().unwrap().clone();
370        let an = ints.last().unwrap().clone();
371        // Zero is a root iff the constant term is zero.
372        if a0.is_zero() {
373            return Some(self.r_zero());
374        }
375        let p_divs = divisors(&a0.abs());
376        let q_divs = divisors(&an.abs());
377        for p in &p_divs {
378            for q in &q_divs {
379                for sign in [1i64, -1] {
380                    let cand = RnsRational::new(
381                        BigInt::from(sign) * p,
382                        q.clone(),
383                        self.channels.clone(),
384                    );
385                    if self.eval(&cand).is_zero() {
386                        return Some(cand);
387                    }
388                }
389            }
390        }
391        None
392    }
393
394    /// Factor into monic irreducible-over-ℚ factors (for the low degrees that
395    /// arise from resultants of quadratics/cubics, this is exact; higher-degree
396    /// factors that resist rational-root splitting are returned whole).
397    pub fn factor_over_q(&self) -> Vec<Self> {
398        let mut work = self.squarefree();
399        let mut factors = Vec::new();
400        loop {
401            if work.degree() == 0 {
402                break;
403            }
404            match work.find_rational_root() {
405                Some(r) => {
406                    // factor (x - r)
407                    let lin = Self::new(
408                        vec![r.neg(), work.r_one()],
409                        self.channels.clone(),
410                    );
411                    work = work.div_exact(&lin);
412                    factors.push(lin.monic());
413                }
414                None => break,
415            }
416        }
417        if work.degree() >= 1 {
418            factors.push(work.monic());
419        }
420        factors
421    }
422}
423
424impl PartialEq for Polynomial {
425    fn eq(&self, other: &Self) -> bool {
426        self.coeffs == other.coeffs
427    }
428}
429impl Eq for Polynomial {}
430
431/// Positive integer divisors of `n` (assumes `n` fits in `u128`).
432fn divisors(n: &BigInt) -> Vec<BigInt> {
433    let mut out = vec![BigInt::one()];
434    let n_u = match n.to_u128() {
435        Some(v) if v > 0 => v,
436        _ => return out,
437    };
438    let mut divs = Vec::new();
439    let mut d = 1u128;
440    while d * d <= n_u {
441        if n_u % d == 0 {
442            divs.push(d);
443            if d != n_u / d {
444                divs.push(n_u / d);
445            }
446        }
447        d += 1;
448    }
449    out.clear();
450    for v in divs {
451        out.push(BigInt::from(v));
452    }
453    out
454}
455
456// ── Bivariate machinery for resultants ───────────────────────────────────────
457//
458// A bivariate polynomial in `y` is represented as `Vec<Polynomial>`, where entry
459// `k` is the coefficient of `yᵏ` and is itself a polynomial in `x`.
460type BiPoly = Vec<Polynomial>;
461
462fn bi_degree(p: &BiPoly) -> usize {
463    let mut d = 0;
464    for (i, c) in p.iter().enumerate() {
465        if !c.is_zero() {
466            d = i;
467        }
468    }
469    d
470}
471
472/// Resultant of two polynomials in `y` (coefficients in ℚ[x]) w.r.t. `y`,
473/// returned as a polynomial in `x`. Computed as the Sylvester determinant.
474fn resultant_y(a: &BiPoly, b: &BiPoly, channels: &Channels) -> Polynomial {
475    let m = bi_degree(a);
476    let n = bi_degree(b);
477    let size = m + n;
478    if size == 0 {
479        return Polynomial::one(channels.clone());
480    }
481    let zero = Polynomial::zero(channels.clone());
482    let mut mat = vec![vec![zero.clone(); size]; size];
483
484    // Rows from `a`: coefficients high→low, shifted right by the row index.
485    for i in 0..n {
486        for j in 0..=m {
487            mat[i][i + j] = a[m - j].clone();
488        }
489    }
490    // Rows from `b`.
491    for i in 0..m {
492        for j in 0..=n {
493            mat[n + i][i + j] = b[n - j].clone();
494        }
495    }
496
497    bareiss_det(mat, channels)
498}
499
500/// Fraction-free (Bareiss) determinant over the polynomial ring ℚ[x].
501fn bareiss_det(mut m: Vec<Vec<Polynomial>>, channels: &Channels) -> Polynomial {
502    let n = m.len();
503    if n == 0 {
504        return Polynomial::one(channels.clone());
505    }
506    let mut sign = 1i32;
507    let mut prev = Polynomial::one(channels.clone());
508    for k in 0..n - 1 {
509        if m[k][k].is_zero() {
510            // Pivot: find a row below with a nonzero entry in column k.
511            let mut swap = None;
512            for i in (k + 1)..n {
513                if !m[i][k].is_zero() {
514                    swap = Some(i);
515                    break;
516                }
517            }
518            match swap {
519                Some(i) => {
520                    m.swap(k, i);
521                    sign = -sign;
522                }
523                None => return Polynomial::zero(channels.clone()),
524            }
525        }
526        for i in (k + 1)..n {
527            for j in (k + 1)..n {
528                let term1 = m[i][j].mul(&m[k][k]);
529                let term2 = m[i][k].mul(&m[k][j]);
530                let numer = term1.sub(&term2);
531                m[i][j] = numer.div_exact(&prev);
532            }
533            m[i][k] = Polynomial::zero(channels.clone());
534        }
535        prev = m[k][k].clone();
536    }
537    let det = m[n - 1][n - 1].clone();
538    if sign < 0 {
539        det.scalar_mul(&RnsRational::from_int(-1, channels.clone()))
540    } else {
541        det
542    }
543}
544
545/// Lift a univariate `p(y)` to a `BiPoly` (constant-in-x coefficients).
546fn lift_const(p: &Polynomial) -> BiPoly {
547    p.coeffs.iter().map(|c| Polynomial::constant(c.clone())).collect()
548}
549
550/// Build `q(x - y)` as a `BiPoly` in `y`.
551fn shift_sub(q: &Polynomial, channels: &Channels) -> BiPoly {
552    // base = (x - y): as poly in y, coeff[0] = x, coeff[1] = -1.
553    let x_poly = Polynomial::from_int_coeffs(&[0, 1], channels.clone());
554    let neg_one = Polynomial::from_int_coeffs(&[-1], channels.clone());
555    let base: BiPoly = vec![x_poly, neg_one];
556
557    let mut acc: BiPoly = vec![Polynomial::zero(channels.clone())];
558    let mut power: BiPoly = vec![Polynomial::one(channels.clone())]; // (x - y)^0
559    for (j, c) in q.coeffs.iter().enumerate() {
560        if j > 0 {
561            power = bi_mul(&power, &base, channels);
562        }
563        let term = bi_scalar(&power, c);
564        acc = bi_add(&acc, &term, channels);
565    }
566    acc
567}
568
569/// Build `yᵈ · q(x/y)` as a `BiPoly` in `y` (for products).
570fn invert_scale(q: &Polynomial, channels: &Channels) -> BiPoly {
571    let d = q.degree();
572    let mut out: BiPoly = vec![Polynomial::zero(channels.clone()); d + 1];
573    for (j, c) in q.coeffs.iter().enumerate() {
574        // term c_j * x^j * y^(d-j)
575        let mut xj = vec![0i64; j + 1];
576        xj[j] = 1;
577        let x_pow = Polynomial::from_int_coeffs(&xj, channels.clone());
578        out[d - j] = x_pow.scalar_mul(c);
579    }
580    out
581}
582
583fn bi_add(a: &BiPoly, b: &BiPoly, channels: &Channels) -> BiPoly {
584    let n = a.len().max(b.len());
585    (0..n)
586        .map(|i| {
587            let za = a.get(i).cloned().unwrap_or_else(|| Polynomial::zero(channels.clone()));
588            let zb = b.get(i).cloned().unwrap_or_else(|| Polynomial::zero(channels.clone()));
589            za.add(&zb)
590        })
591        .collect()
592}
593
594fn bi_scalar(a: &BiPoly, s: &RnsRational) -> BiPoly {
595    a.iter().map(|c| c.scalar_mul(s)).collect()
596}
597
598fn bi_mul(a: &BiPoly, b: &BiPoly, channels: &Channels) -> BiPoly {
599    if a.is_empty() || b.is_empty() {
600        return vec![Polynomial::zero(channels.clone())];
601    }
602    let mut out = vec![Polynomial::zero(channels.clone()); a.len() + b.len() - 1];
603    for (i, ca) in a.iter().enumerate() {
604        for (j, cb) in b.iter().enumerate() {
605            out[i + j] = out[i + j].add(&ca.mul(cb));
606        }
607    }
608    out
609}
610
611/// An exact real algebraic number (Level 2).
612#[derive(Clone, Debug)]
613pub struct AlgebraicNumber {
614    /// Minimal polynomial over ℚ (monic, irreducible for the cases we build).
615    pub min_poly: Polynomial,
616    /// Isolating interval `(lo, hi)` containing exactly one root of `min_poly`.
617    pub interval: (RnsRational, RnsRational),
618    pub channels: Channels,
619}
620
621impl AlgebraicNumber {
622    /// The positive square root √n.
623    pub fn sqrt(n: u64, channels: Channels) -> Self {
624        // min poly x² - n, isolate the positive root in (0, n+1).
625        let min_poly = Polynomial::from_int_coeffs(&[-(n as i64), 0, 1], channels.clone());
626        let lo = RnsRational::from_int(0, channels.clone());
627        let hi = RnsRational::from_int(n as i64 + 1, channels.clone());
628        Self::from_min_poly_interval(min_poly, lo, hi, channels)
629    }
630
631    /// The real cube root ∛n.
632    pub fn cbrt(n: u64, channels: Channels) -> Self {
633        let min_poly = Polynomial::from_int_coeffs(&[-(n as i64), 0, 0, 1], channels.clone());
634        let lo = RnsRational::from_int(0, channels.clone());
635        let hi = RnsRational::from_int(n as i64 + 1, channels.clone());
636        Self::from_min_poly_interval(min_poly, lo, hi, channels)
637    }
638
639    /// A rational as a degree-1 algebraic number.
640    pub fn from_rational(r: RnsRational) -> Self {
641        let channels = r.channels.clone();
642        let min_poly = Polynomial::new(
643            vec![r.neg(), RnsRational::from_int(1, channels.clone())],
644            channels.clone(),
645        );
646        let lo = r.sub(&RnsRational::from_int(1, channels.clone()));
647        let hi = r.add(&RnsRational::from_int(1, channels.clone()));
648        AlgebraicNumber { min_poly, interval: (lo, hi), channels }
649    }
650
651    /// The `root_index`-th real root (ascending) of `p`.
652    pub fn from_poly_root(p: Polynomial, root_index: usize, channels: Channels) -> Self {
653        let roots = p.isolate_real_roots();
654        let (lo, hi) = roots
655            .get(root_index)
656            .cloned()
657            .expect("root_index out of range");
658        // Attach the irreducible factor that owns this root.
659        let factors = p.factor_over_q();
660        let min_poly = Self::factor_for_interval(&factors, &lo, &hi).unwrap_or(p);
661        AlgebraicNumber { min_poly, interval: (lo, hi), channels }
662    }
663
664    fn from_min_poly_interval(
665        min_poly: Polynomial,
666        lo: RnsRational,
667        hi: RnsRational,
668        channels: Channels,
669    ) -> Self {
670        let mut a = AlgebraicNumber { min_poly, interval: (lo, hi), channels };
671        // Tighten a little so the interval cleanly isolates the root.
672        let target = RnsRational::new(BigInt::one(), BigInt::from(1_000_000), a.channels.clone());
673        a.refine_interval(&target);
674        a
675    }
676
677    /// Degree of the minimal polynomial.
678    pub fn degree(&self) -> usize {
679        self.min_poly.degree()
680    }
681
682    /// `Some(r)` iff this number is actually rational (degree-1 min poly).
683    pub fn to_rational(&self) -> Option<RnsRational> {
684        if self.min_poly.degree() == 1 {
685            // c1 x + c0 = 0  =>  x = -c0/c1
686            let c0 = self.min_poly.coeffs[0].clone();
687            let c1 = self.min_poly.coeffs[1].clone();
688            Some(c0.neg().div(&c1))
689        } else {
690            None
691        }
692    }
693
694    /// Refine the isolating interval until its width is `< target_width`.
695    pub fn refine_interval(&mut self, target_width: &RnsRational) {
696        let (mut lo, mut hi) = self.interval.clone();
697        let sign_lo = self.min_poly.sign_at(&lo);
698        // If an endpoint is exactly the root, collapse to it.
699        if sign_lo == 0 {
700            self.interval = (lo.clone(), lo);
701            return;
702        }
703        if self.min_poly.sign_at(&hi) == 0 {
704            self.interval = (hi.clone(), hi);
705            return;
706        }
707        while hi.sub(&lo) >= *target_width {
708            let mid = lo.midpoint(&hi);
709            let sm = self.min_poly.sign_at(&mid);
710            if sm == 0 {
711                lo = mid.clone();
712                hi = mid;
713                break;
714            } else if sm == sign_lo {
715                lo = mid;
716            } else {
717                hi = mid;
718            }
719        }
720        self.interval = (lo, hi);
721    }
722
723    /// An `f64` approximation (refines internally first).
724    pub fn to_f64(&self) -> f64 {
725        let mut clone = self.clone();
726        let target = RnsRational::new(BigInt::one(), BigInt::one() << 60, self.channels.clone());
727        clone.refine_interval(&target);
728        clone.interval.0.midpoint(&clone.interval.1).to_f64()
729    }
730
731    /// Exact sign of the number: `-1`, `0`, or `+1`.
732    pub fn sign(&self) -> i32 {
733        // Zero is a root iff the constant term of an irreducible min poly is 0
734        // (i.e. min poly is exactly x).
735        if self.min_poly.degree() == 1 && self.min_poly.coeffs[0].is_zero() {
736            return 0;
737        }
738        let mut clone = self.clone();
739        let zero = RnsRational::zero(self.channels.clone());
740        // Refine until the interval lies strictly on one side of 0.
741        let mut target = RnsRational::new(BigInt::one(), BigInt::from(1024), self.channels.clone());
742        for _ in 0..200 {
743            if clone.interval.0 > zero {
744                return 1;
745            }
746            if clone.interval.1 < zero {
747                return -1;
748            }
749            clone.refine_interval(&target);
750            target = target.mul(&RnsRational::from_fraction(1, 2, self.channels.clone()));
751        }
752        clone.interval.0.midpoint(&clone.interval.1).signum()
753    }
754
755    /// Additive inverse: negate the root (`x -> -x` in the min poly).
756    pub fn neg(&self) -> Self {
757        let coeffs = self
758            .min_poly
759            .coeffs
760            .iter()
761            .enumerate()
762            .map(|(i, c)| if i % 2 == 1 { c.neg() } else { c.clone() })
763            .collect();
764        let min_poly = Polynomial::new(coeffs, self.channels.clone()).monic();
765        AlgebraicNumber {
766            min_poly,
767            interval: (self.interval.1.neg(), self.interval.0.neg()),
768            channels: self.channels.clone(),
769        }
770    }
771
772    /// Multiplicative inverse: reciprocal of the root (reverse the coefficients).
773    pub fn recip(&self) -> Self {
774        let mut coeffs = self.min_poly.coeffs.clone();
775        coeffs.reverse();
776        let min_poly = Polynomial::new(coeffs, self.channels.clone()).monic();
777        let v = self.to_f64();
778        Self::reconstruct(min_poly, 1.0 / v, self.channels.clone())
779    }
780
781    /// α + β.
782    pub fn add(&self, other: &Self) -> Self {
783        let channels = self.channels.clone();
784        let a = lift_const(&self.min_poly);
785        let b = shift_sub(&other.min_poly, &channels);
786        let res = resultant_y(&a, &b, &channels);
787        let value = self.to_f64() + other.to_f64();
788        Self::reconstruct(res, value, channels)
789    }
790
791    /// α × β.
792    pub fn mul(&self, other: &Self) -> Self {
793        let channels = self.channels.clone();
794        let a = lift_const(&self.min_poly);
795        let b = invert_scale(&other.min_poly, &channels);
796        let res = resultant_y(&a, &b, &channels);
797        let value = self.to_f64() * other.to_f64();
798        Self::reconstruct(res, value, channels)
799    }
800
801    /// From a resultant polynomial plus an approximate value, pick the
802    /// irreducible factor and isolating interval that own the target root.
803    fn reconstruct(res: Polynomial, approx: f64, channels: Channels) -> Self {
804        let factors = res.factor_over_q();
805        // Choose the factor whose nearest real root is closest to `approx`.
806        let mut best: Option<(AlgebraicNumber, f64)> = None;
807        for f in &factors {
808            for (lo, hi) in f.isolate_real_roots() {
809                let mut cand = AlgebraicNumber {
810                    min_poly: f.clone(),
811                    interval: (lo, hi),
812                    channels: channels.clone(),
813                };
814                let target = RnsRational::new(BigInt::one(), BigInt::from(1_000_000), channels.clone());
815                cand.refine_interval(&target);
816                let dist = (cand.to_f64() - approx).abs();
817                if best.as_ref().map(|(_, d)| dist < *d).unwrap_or(true) {
818                    best = Some((cand, dist));
819                }
820            }
821        }
822        best.map(|(a, _)| a).expect("resultant had no real root near target")
823    }
824
825    fn factor_for_interval(
826        factors: &[Polynomial],
827        lo: &RnsRational,
828        hi: &RnsRational,
829    ) -> Option<Polynomial> {
830        for f in factors {
831            if f.sturm_root_count(lo, hi) >= 1 {
832                return Some(f.monic());
833            }
834        }
835        None
836    }
837}
838
839#[cfg(test)]
840mod tests {
841    use super::*;
842
843    fn ch() -> Channels {
844        Channels::standard(32)
845    }
846
847    #[test]
848    fn sqrt2_min_poly() {
849        let s = AlgebraicNumber::sqrt(2, ch());
850        assert_eq!(s.degree(), 2);
851        // 1² - 2 < 0
852        assert_eq!(
853            s.min_poly.sign_at(&RnsRational::from_int(1, ch())),
854            -1
855        );
856        assert!(s.interval.0 < s.interval.1);
857        assert!(s.to_rational().is_none());
858        assert!((s.to_f64() - 2f64.sqrt()).abs() < 1e-9);
859    }
860
861    #[test]
862    fn from_rational_roundtrip() {
863        let r = RnsRational::from_fraction(3, 5, ch());
864        let a = AlgebraicNumber::from_rational(r.clone());
865        assert_eq!(a.to_rational(), Some(r));
866    }
867
868    #[test]
869    fn sturm_counts() {
870        // x² - 2 has one root in (-2,-1], one in (1,2], two in (-2,2].
871        let p = Polynomial::from_int_coeffs(&[-2, 0, 1], ch());
872        let r = |a: i64, b: i64| {
873            p.sturm_root_count(
874                &RnsRational::from_int(a, ch()),
875                &RnsRational::from_int(b, ch()),
876            )
877        };
878        assert_eq!(r(-2, -1), 1);
879        assert_eq!(r(1, 2), 1);
880        assert_eq!(r(-2, 2), 2);
881    }
882
883    #[test]
884    fn sqrt2_times_sqrt2_is_two() {
885        let s = AlgebraicNumber::sqrt(2, ch());
886        let p = s.mul(&s);
887        // √2·√2 = 2  =>  degree-1 min poly, rational value 2.
888        assert_eq!(p.degree(), 1);
889        assert_eq!(p.to_rational(), Some(RnsRational::from_int(2, ch())));
890    }
891
892    #[test]
893    fn sqrt2_times_sqrt3_is_sqrt6() {
894        let s2 = AlgebraicNumber::sqrt(2, ch());
895        let s3 = AlgebraicNumber::sqrt(3, ch());
896        let p = s2.mul(&s3);
897        assert_eq!(p.degree(), 2);
898        // min poly x² - 6
899        let expected = Polynomial::from_int_coeffs(&[-6, 0, 1], ch()).monic();
900        assert_eq!(p.min_poly, expected);
901    }
902
903    #[test]
904    fn sqrt2_plus_sqrt2_is_2sqrt2() {
905        let s = AlgebraicNumber::sqrt(2, ch());
906        let p = s.add(&s);
907        // 2√2 has min poly x² - 8.
908        assert_eq!(p.degree(), 2);
909        let expected = Polynomial::from_int_coeffs(&[-8, 0, 1], ch()).monic();
910        assert_eq!(p.min_poly, expected);
911    }
912
913    #[test]
914    fn refine_narrows() {
915        let mut s = AlgebraicNumber::sqrt(2, ch());
916        let target = RnsRational::new(BigInt::one(), BigInt::from(10).pow(20), ch());
917        s.refine_interval(&target);
918        assert!(s.interval.1.sub(&s.interval.0) < target);
919    }
920}