Skip to main content

oxiz_math/algebraic/
number.rs

1//! Algebraic Number Representation.
2//!
3//! An algebraic number is a root of a polynomial with rational coefficients.
4//! We represent it as (polynomial, isolating_interval) where the interval
5//! contains exactly one root of the polynomial.
6//!
7//! ## Representation
8//!
9//! ```text
10//! α = (p(x), [a, b]) where p(α) = 0 and p has exactly one root in [a, b]
11//! ```
12//!
13//! ## References
14//!
15//! - "Algorithms in Real Algebraic Geometry" (Basu et al., 2006)
16//! - Z3's `math/polynomial/algebraic_numbers.h`
17
18use crate::polynomial::root_counting::Polynomial;
19#[allow(unused_imports)]
20use crate::prelude::*;
21use core::cmp::Ordering;
22use core::fmt;
23use num_bigint::BigInt;
24use num_rational::BigRational;
25use num_traits::{One, Signed, Zero};
26
27/// Algebraic number error types.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum AlgebraicNumberError {
30    /// Interval doesn't isolate a single root.
31    NonIsolatingInterval,
32    /// Polynomial has no roots in interval.
33    NoRootInInterval,
34    /// Polynomial is zero.
35    ZeroPolynomial,
36    /// Invalid operation.
37    InvalidOperation(String),
38}
39
40impl fmt::Display for AlgebraicNumberError {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match self {
43            Self::NonIsolatingInterval => write!(f, "Interval doesn't isolate a single root"),
44            Self::NoRootInInterval => write!(f, "No root in interval"),
45            Self::ZeroPolynomial => write!(f, "Polynomial is zero"),
46            Self::InvalidOperation(msg) => write!(f, "Invalid operation: {}", msg),
47        }
48    }
49}
50
51impl core::error::Error for AlgebraicNumberError {}
52
53/// Algebraic number represented as a root of a univariate polynomial over Q.
54#[derive(Debug, Clone)]
55pub struct AlgebraicNumber {
56    /// Minimal polynomial (irreducible, with rational coefficients).
57    pub minimal_poly: Polynomial,
58    /// Lower bound of the isolating interval.
59    pub lower: BigRational,
60    /// Upper bound of the isolating interval.
61    pub upper: BigRational,
62    /// Number of refinement steps performed (for termination detection).
63    refinement_level: usize,
64}
65
66impl AlgebraicNumber {
67    /// Create a new algebraic number.
68    ///
69    /// Validates that the interval `[lower, upper]` contains exactly one root
70    /// of `poly` using Sturm's theorem before accepting it.
71    ///
72    /// # Errors
73    ///
74    /// Returns an error if:
75    /// - `poly` is the zero polynomial,
76    /// - `lower > upper` (degenerate interval ordering), or
77    /// - the Sturm sequence shows ≠ 1 root inside `(lower, upper)`.
78    pub fn new(
79        poly: Polynomial,
80        lower: BigRational,
81        upper: BigRational,
82    ) -> Result<Self, AlgebraicNumberError> {
83        // Reject the zero polynomial — it has infinitely many roots.
84        if poly.degree() == 0 && poly.coeffs.first().is_none_or(Zero::is_zero) {
85            return Err(AlgebraicNumberError::ZeroPolynomial);
86        }
87
88        if lower > upper {
89            return Err(AlgebraicNumberError::NonIsolatingInterval);
90        }
91
92        // Degenerate (point) interval: check whether `lower` is a root.
93        if lower == upper {
94            if poly.eval(&lower).is_zero() {
95                return Ok(Self {
96                    minimal_poly: poly,
97                    lower,
98                    upper,
99                    refinement_level: 0,
100                });
101            }
102            return Err(AlgebraicNumberError::NoRootInInterval);
103        }
104
105        // Sturm sequence check: the interval must isolate exactly one root.
106        let root_count = sturm_root_count(&poly, &lower, &upper);
107        if root_count != 1 {
108            return Err(AlgebraicNumberError::NonIsolatingInterval);
109        }
110
111        Ok(Self {
112            minimal_poly: poly,
113            lower,
114            upper,
115            refinement_level: 0,
116        })
117    }
118
119    /// Create algebraic number from a rational.
120    ///
121    /// The rational `r` is represented as the root of the linear polynomial `(x - r)`.
122    pub fn from_rational(r: BigRational) -> Self {
123        // x - r  →  coeffs = [-r, 1]
124        let poly = Polynomial::new(vec![-r.clone(), BigRational::from(BigInt::from(1))]);
125
126        Self {
127            minimal_poly: poly,
128            lower: r.clone(),
129            upper: r,
130            refinement_level: 0,
131        }
132    }
133
134    /// Check if this algebraic number is rational.
135    pub fn is_rational(&self) -> bool {
136        self.minimal_poly.degree() == 1 || self.lower == self.upper
137    }
138
139    /// Convert to rational if the number is rational.
140    pub fn to_rational(&self) -> Option<BigRational> {
141        if self.is_rational() {
142            Some(self.lower.clone())
143        } else {
144            None
145        }
146    }
147
148    /// Refine the isolating interval by bisection.
149    pub fn refine(&mut self) {
150        let mid = (&self.lower + &self.upper) / BigRational::from(BigInt::from(2));
151        let mid_value = self.minimal_poly.eval(&mid);
152
153        if mid_value.is_zero() {
154            // Exact root — collapse to a point.
155            self.lower = mid.clone();
156            self.upper = mid;
157        } else {
158            let lower_value = self.minimal_poly.eval(&self.lower);
159            if lower_value.signum() != mid_value.signum() {
160                // Root is in [lower, mid].
161                self.upper = mid;
162            } else {
163                // Root is in [mid, upper].
164                self.lower = mid;
165            }
166        }
167
168        self.refinement_level += 1;
169    }
170
171    /// Refine until the interval width is at most `precision`.
172    pub fn refine_to_precision(&mut self, precision: &BigRational) {
173        while &self.upper - &self.lower > *precision {
174            self.refine();
175        }
176    }
177
178    /// Width of the current isolating interval.
179    pub fn interval_width(&self) -> BigRational {
180        &self.upper - &self.lower
181    }
182
183    /// Midpoint of the current isolating interval (an approximation of the number).
184    pub fn midpoint(&self) -> BigRational {
185        (&self.lower + &self.upper) / BigRational::from(BigInt::from(2))
186    }
187
188    /// Compare with another algebraic number by interval refinement.
189    pub fn compare(&mut self, other: &mut AlgebraicNumber) -> Ordering {
190        loop {
191            if self.upper < other.lower {
192                return Ordering::Less;
193            }
194            if self.lower > other.upper {
195                return Ordering::Greater;
196            }
197
198            // Identical representation → definitely equal.
199            if self.minimal_poly == other.minimal_poly
200                && self.lower == other.lower
201                && self.upper == other.upper
202            {
203                return Ordering::Equal;
204            }
205
206            // Intervals overlap; refine both.
207            self.refine();
208            other.refine();
209
210            if self.refinement_level > 1000 {
211                // Cannot separate after 1 000 steps — treat as equal.
212                return Ordering::Equal;
213            }
214        }
215    }
216
217    /// Add two algebraic numbers using resultant-based exact arithmetic.
218    ///
219    /// Given α (root of p) and β (root of q), computes α+β as a root of
220    /// `r(x) = Res_t(p(t), q(x−t))`, the polynomial in x whose roots are all
221    /// pairwise sums of roots of p and q.
222    ///
223    /// The resultant is computed via evaluation-interpolation: we evaluate
224    /// `Res_t(p(t), q(k−t))` for `deg(p)*deg(q)+1` integer points k, then
225    /// reconstruct `r` by Lagrange interpolation over Q.
226    ///
227    /// The correct root is isolated using the interval `[aₗ+bₗ, aᵤ+bᵤ]`.
228    ///
229    /// # Errors
230    ///
231    /// Returns [`AlgebraicNumberError::NonIsolatingInterval`] if the sum
232    /// interval does not isolate exactly one root of the result polynomial after
233    /// repeated halving, or [`AlgebraicNumberError::ZeroPolynomial`] if the
234    /// computed resultant is identically zero (only possible when p and q share
235    /// a common root, which cannot happen for irreducible min-polys over Q
236    /// unless they are identical).
237    pub fn add(&self, other: &AlgebraicNumber) -> Result<AlgebraicNumber, AlgebraicNumberError> {
238        // Fast path: both rational.
239        if self.is_rational() && other.is_rational() {
240            return Ok(AlgebraicNumber::from_rational(&self.lower + &other.lower));
241        }
242
243        // Compute the sum polynomial r(x) = Res_t(p(t), q(x-t)) via
244        // evaluation at deg(p)*deg(q)+1 integer points followed by Lagrange
245        // interpolation.
246        let deg_p = self.minimal_poly.degree();
247        let deg_q = other.minimal_poly.degree();
248        let result_deg = deg_p * deg_q;
249        let num_points = result_deg + 1;
250
251        let eval_points: Vec<BigRational> = (0..num_points as i64)
252            .map(|k| BigRational::from(BigInt::from(k)))
253            .collect();
254
255        let values: Vec<BigRational> = eval_points
256            .iter()
257            .map(|x_val| {
258                // Build q_x(t) = q(x_val - t) by substituting t -> (x_val - t).
259                // q(x_val - t) = sum_i coeff_i * (x_val - t)^i
260                // We expand each (x_val - t)^i using the binomial theorem.
261                let q_shifted = poly_substitute_affine_t(&other.minimal_poly, x_val);
262                // Now compute Res_t(p(t), q_shifted(t)) via subresultant PRS.
263                univariate_resultant(&self.minimal_poly, &q_shifted)
264            })
265            .collect();
266
267        // Reconstruct r(x) from the evaluations via Lagrange interpolation.
268        let raw_poly = lagrange_interpolate(&eval_points, &values);
269
270        if raw_poly.degree() == 0 && raw_poly.coeffs.first().is_none_or(Zero::is_zero) {
271            return Err(AlgebraicNumberError::ZeroPolynomial);
272        }
273
274        // Extract the square-free part so the Sturm-based isolation works correctly.
275        let result_poly = square_free_part(raw_poly);
276
277        // The correct root is in [lower_self + lower_other, upper_self + upper_other].
278        // Refine until the interval is isolating.
279        let sum_lower = &self.lower + &other.lower;
280        let sum_upper = &self.upper + &other.upper;
281
282        isolate_root_in_interval(result_poly, sum_lower, sum_upper)
283    }
284
285    /// Multiply two algebraic numbers using resultant-based exact arithmetic.
286    ///
287    /// Given α (root of p) and β (root of q), computes α·β as a root of
288    /// `r(x) = Res_t(p(t), t^deg(q) · q(x/t))`, the polynomial whose roots
289    /// are all pairwise products.
290    ///
291    /// For multiplication the evaluation-interpolation approach evaluates
292    /// `Res_t(p(t), t^m · q(k/t))` at nonzero points k (since division by t
293    /// is not polynomial at k=0; the value at 0 is handled as `p(0)^deg(q)` ·
294    /// appropriate sign correction).  We then isolate the product root using
295    /// interval `[min(aₗbₗ, aₗbᵤ, aᵤbₗ, aᵤbᵤ), max(…)]`.
296    ///
297    /// # Errors
298    ///
299    /// Returns [`AlgebraicNumberError::ZeroPolynomial`] if the computed
300    /// resultant is zero, or [`AlgebraicNumberError::NonIsolatingInterval`] if
301    /// isolation fails.
302    pub fn mul(&self, other: &AlgebraicNumber) -> Result<AlgebraicNumber, AlgebraicNumberError> {
303        // Fast path: both rational.
304        if self.is_rational() && other.is_rational() {
305            return Ok(AlgebraicNumber::from_rational(&self.lower * &other.lower));
306        }
307
308        let deg_p = self.minimal_poly.degree();
309        let deg_q = other.minimal_poly.degree();
310        let result_deg = deg_p * deg_q;
311        let num_points = result_deg + 1;
312
313        // For the product resultant we use points k = 1, 2, ..., num_points
314        // (avoid k=0 to dodge the t=0 singularity in q(x/t)).
315        let eval_points: Vec<BigRational> = (1..=(num_points as i64))
316            .map(|k| BigRational::from(BigInt::from(k)))
317            .collect();
318
319        let values: Vec<BigRational> = eval_points
320            .iter()
321            .map(|x_val| {
322                // Build h(t) = t^deg(q) * q(x_val / t).
323                // Since q(x_val / t) = sum_i c_i * (x_val/t)^i, we have
324                // t^deg(q) * q(x_val/t) = sum_i c_i * x_val^i * t^(deg_q - i).
325                // This is a polynomial in t with rational coefficients.
326                let h = poly_mul_pow_quot(&other.minimal_poly, x_val, deg_q);
327                univariate_resultant(&self.minimal_poly, &h)
328            })
329            .collect();
330
331        let raw_poly = lagrange_interpolate(&eval_points, &values);
332
333        if raw_poly.degree() == 0 && raw_poly.coeffs.first().is_none_or(Zero::is_zero) {
334            return Err(AlgebraicNumberError::ZeroPolynomial);
335        }
336
337        // The resultant polynomial may have repeated roots.  Extract the
338        // square-free part so that the Sturm-based root isolation works
339        // correctly (Sturm counts distinct roots).
340        let result_poly = square_free_part(raw_poly);
341
342        // Interval arithmetic: product interval (all four corners).
343        let corners = [
344            &self.lower * &other.lower,
345            &self.lower * &other.upper,
346            &self.upper * &other.lower,
347            &self.upper * &other.upper,
348        ];
349        let prod_lower = corners
350            .iter()
351            .min_by(|a, b| a.cmp(b))
352            .expect("4 elements")
353            .clone();
354        let prod_upper = corners
355            .iter()
356            .max_by(|a, b| a.cmp(b))
357            .expect("4 elements")
358            .clone();
359
360        isolate_root_in_interval(result_poly, prod_lower, prod_upper)
361    }
362
363    /// Negate an algebraic number.
364    ///
365    /// If α is a root of p(x), then −α is a root of p(−x).
366    pub fn negate(&self) -> AlgebraicNumber {
367        let neg_poly = poly_substitute_neg_x(&self.minimal_poly);
368        AlgebraicNumber {
369            minimal_poly: neg_poly,
370            lower: -&self.upper,
371            upper: -&self.lower,
372            refinement_level: self.refinement_level,
373        }
374    }
375
376    /// Return the sign of the algebraic number: −1, 0, or 1.
377    pub fn signum(&self) -> i32 {
378        if self.upper < BigRational::zero() {
379            -1
380        } else if self.lower > BigRational::zero() {
381            1
382        } else if self.lower == self.upper && self.lower.is_zero() {
383            0
384        } else {
385            // Interval straddles zero; refine until sign is clear.
386            let mut copy = self.clone();
387            copy.refine();
388            copy.signum()
389        }
390    }
391}
392
393// ─── private helpers ────────────────────────────────────────────────────────
394
395/// Substitute x → −x in a polynomial: p(−x).
396///
397/// For p(x) = a₀ + a₁x + a₂x² + …, we have p(−x) = a₀ − a₁x + a₂x² − …
398fn poly_substitute_neg_x(poly: &Polynomial) -> Polynomial {
399    let coeffs: Vec<BigRational> = poly
400        .coeffs
401        .iter()
402        .enumerate()
403        .map(|(i, c)| if i % 2 == 0 { c.clone() } else { -c })
404        .collect();
405    Polynomial::new(coeffs)
406}
407
408/// Count distinct real roots of `poly` in the open interval `(lower, upper)`
409/// using Sturm's theorem: count = V(lower) − V(upper) where V(x) is the
410/// number of sign changes in the Sturm sequence evaluated at x.
411fn sturm_root_count(poly: &Polynomial, lower: &BigRational, upper: &BigRational) -> usize {
412    let seq = build_sturm_sequence(poly);
413    let v_lower = sign_variations(&seq, lower);
414    let v_upper = sign_variations(&seq, upper);
415    (v_lower as isize - v_upper as isize).unsigned_abs()
416}
417
418/// Build the Sturm sequence of a polynomial.
419///
420/// - p₀ = p
421/// - p₁ = p′
422/// - pₖ₊₁ = −rem(pₖ₋₁, pₖ)
423fn build_sturm_sequence(poly: &Polynomial) -> Vec<Polynomial> {
424    let mut seq = vec![poly.clone(), poly.derivative()];
425
426    loop {
427        let n = seq.len();
428        let last = &seq[n - 1];
429
430        // Terminate when the tail is degree-0 (zero or non-zero constant): any polynomial
431        // mod a constant is 0, so the next negated remainder would be 0 anyway.
432        if last.degree() == 0 {
433            break;
434        }
435
436        let remainder = seq[n - 2].remainder(last);
437        let negated = Polynomial::new(remainder.coeffs.iter().map(|c| -c).collect());
438
439        if negated.degree() == 0 && negated.coeffs.first().is_none_or(Zero::is_zero) {
440            break;
441        }
442
443        seq.push(negated);
444
445        // Safety guard against pathological inputs.
446        if seq.len() > 1000 {
447            break;
448        }
449    }
450
451    seq
452}
453
454/// Count sign variations in the Sturm sequence evaluated at `point`.
455///
456/// Zero values are skipped (Sturm's theorem convention).
457fn sign_variations(seq: &[Polynomial], point: &BigRational) -> usize {
458    let signs: Vec<i32> = seq
459        .iter()
460        .map(|p| {
461            let val = p.eval(point);
462            if val.is_positive() {
463                1
464            } else if val.is_negative() {
465                -1
466            } else {
467                0
468            }
469        })
470        .filter(|&s| s != 0)
471        .collect();
472
473    signs.windows(2).filter(|w| w[0] != w[1]).count()
474}
475
476// ─── square-free helpers ─────────────────────────────────────────────────────
477
478/// Compute the square-free part of a univariate polynomial over Q:
479/// `sqfp(p) = p / gcd(p, p′)`.
480///
481/// The result has the same roots as `p` but each root appears exactly once,
482/// which is required for the Sturm-sequence root counter.
483fn square_free_part(p: Polynomial) -> Polynomial {
484    let deriv = p.derivative();
485
486    // If derivative is zero (constant polynomial), p is already square-free.
487    if deriv.degree() == 0 && deriv.coeffs.first().is_none_or(|c| c.is_zero()) {
488        return p;
489    }
490
491    // Compute gcd(p, p') via Euclidean PRS over Q.
492    let g = univariate_gcd(&p, &deriv);
493
494    // If gcd is degree 0 (i.e., a nonzero constant), p is already square-free.
495    if g.degree() == 0 {
496        return p;
497    }
498
499    // Exact division: p / g.
500    exact_poly_div(&p, &g)
501}
502
503/// Compute gcd of two univariate polynomials over Q via Euclidean algorithm.
504fn univariate_gcd(p: &Polynomial, q: &Polynomial) -> Polynomial {
505    let mut a = p.clone();
506    let mut b = q.clone();
507
508    loop {
509        // Normalise the zero polynomial check.
510        let b_is_zero = b.coeffs.iter().all(|c| c.is_zero());
511        if b_is_zero {
512            // Make monic and return.
513            return make_monic(a);
514        }
515        let r = a.remainder(&b);
516        a = b;
517        b = r;
518    }
519}
520
521/// Divide polynomial `a` by `b` exactly (remainder must be zero over Q).
522fn exact_poly_div(a: &Polynomial, b: &Polynomial) -> Polynomial {
523    let deg_b = b.degree();
524    let lead_b = b.coeffs[deg_b].clone();
525
526    if a.degree() < deg_b {
527        return Polynomial::new(vec![BigRational::zero()]);
528    }
529
530    let result_deg = a.degree() - deg_b;
531    let mut quotient_coeffs = vec![BigRational::zero(); result_deg + 1];
532    // Work on a mutable coefficient vector; rebuild Polynomial at end.
533    let mut rem_coeffs = a.coeffs.clone();
534
535    // Classic long division (high degree to low).
536    let mut rem_deg = a.degree();
537    while rem_deg >= deg_b {
538        // Check if the high-degree coefficient is essentially zero.
539        if rem_coeffs[rem_deg].is_zero() {
540            if rem_deg == 0 {
541                break;
542            }
543            rem_deg -= 1;
544            continue;
545        }
546        let factor = rem_coeffs[rem_deg].clone() / &lead_b;
547        let shift = rem_deg - deg_b;
548        quotient_coeffs[shift] = factor.clone();
549
550        for i in 0..=deg_b {
551            let upd = rem_coeffs[shift + i].clone() - &factor * &b.coeffs[i];
552            rem_coeffs[shift + i] = upd;
553        }
554        if rem_deg == 0 {
555            break;
556        }
557        rem_deg -= 1;
558    }
559
560    Polynomial::new(quotient_coeffs)
561}
562
563/// Normalise a polynomial: make it monic (leading coeff = 1).
564fn make_monic(p: Polynomial) -> Polynomial {
565    let deg = p.degree();
566    let lead = &p.coeffs[deg];
567    if lead.is_zero() {
568        return p;
569    }
570    let inv = lead.clone().recip();
571    let coeffs: Vec<BigRational> = p.coeffs.iter().map(|c| c * &inv).collect();
572    Polynomial::new(coeffs)
573}
574
575// ─── resultant helpers ──────────────────────────────────────────────────────
576
577/// Compute `q(k - t)` as a polynomial in `t`, where `q` is a univariate poly
578/// over Q and `k` is a rational constant.
579///
580/// We expand each power `(k - t)^i` via the binomial theorem:
581/// `(k - t)^i = Σ_{j=0}^{i} C(i,j) * k^(i-j) * (-t)^j = Σ_j C(i,j) * k^(i-j) * (-1)^j * t^j`
582///
583/// The coefficient of `t^j` in the full expansion of `q(k-t)` is then
584/// `Σ_{i ≥ j} q.coeffs[i] * C(i,j) * k^(i-j) * (-1)^j`.
585fn poly_substitute_affine_t(q: &Polynomial, k: &BigRational) -> Polynomial {
586    let n = q.coeffs.len();
587    if n == 0 {
588        return Polynomial::new(vec![BigRational::zero()]);
589    }
590
591    // result_coeffs[j] = coefficient of t^j in q(k - t)
592    let mut result_coeffs = vec![BigRational::zero(); n];
593
594    for (i, q_i) in q.coeffs.iter().enumerate() {
595        if q_i.is_zero() {
596            continue;
597        }
598        // Accumulate contributions to each t^j from this term: q_i * (k - t)^i
599        let binom_row = binomial_row(i);
600        for (j, binom_ij) in binom_row.iter().enumerate() {
601            // k^(i-j)
602            let k_pow = rational_pow(k, i - j);
603            // (-1)^j
604            let sign = if j % 2 == 0 {
605                BigRational::one()
606            } else {
607                -BigRational::one()
608            };
609            let contrib = q_i * binom_ij * k_pow * sign;
610            result_coeffs[j] = result_coeffs[j].clone() + contrib;
611        }
612    }
613
614    Polynomial::new(result_coeffs)
615}
616
617/// Build `h(t) = t^m * q(x_val / t)` for a fixed rational value `x_val` and
618/// integer `m = deg(q)`.
619///
620/// `q(x_val / t) = Σ_i q_i * (x_val/t)^i`, so
621/// `t^m * q(x_val/t) = Σ_i q_i * x_val^i * t^(m-i)`.
622///
623/// The coefficient of `t^(m-i)` (i.e., `t^j` where `j = m-i`) is `q_i * x_val^i`.
624fn poly_mul_pow_quot(q: &Polynomial, x_val: &BigRational, m: usize) -> Polynomial {
625    let mut result_coeffs = vec![BigRational::zero(); m + 1];
626    for (i, q_i) in q.coeffs.iter().enumerate() {
627        if q_i.is_zero() {
628            continue;
629        }
630        let j = m.saturating_sub(i);
631        let x_pow = rational_pow(x_val, i);
632        result_coeffs[j] = result_coeffs[j].clone() + q_i * x_pow;
633    }
634    Polynomial::new(result_coeffs)
635}
636
637/// Compute the resultant `Res_t(p, q)` of two univariate polynomials over Q
638/// via the Sylvester matrix determinant, using Bareiss fraction-free
639/// Gaussian elimination over the rationals.
640///
641/// Returns a rational number.  Zero if the polynomials share a common root.
642fn univariate_resultant(p: &Polynomial, q: &Polynomial) -> BigRational {
643    let m = p.degree();
644    let n = q.degree();
645
646    // Degenerate: one polynomial is constant.
647    if m == 0 {
648        let c = p.coeffs.first().cloned().unwrap_or_else(BigRational::zero);
649        return rational_pow(&c, n);
650    }
651    if n == 0 {
652        let c = q.coeffs.first().cloned().unwrap_or_else(BigRational::zero);
653        return rational_pow(&c, m);
654    }
655
656    // Build the (m+n) × (m+n) Sylvester matrix.
657    // Coefficients are stored low-to-high, but the Sylvester matrix convention
658    // is high-to-low. Reverse for row construction.
659    let p_rev: Vec<BigRational> = p.coeffs.iter().rev().cloned().collect();
660    let q_rev: Vec<BigRational> = q.coeffs.iter().rev().cloned().collect();
661
662    let size = m + n;
663    let mut mat: Vec<Vec<BigRational>> = vec![vec![BigRational::zero(); size]; size];
664
665    // First n rows: p coefficients shifted.
666    for r in 0..n {
667        mat[r][r..(m + r + 1)].clone_from_slice(&p_rev[..(m + 1)]);
668    }
669    // Last m rows: q coefficients shifted.
670    for r in 0..m {
671        mat[n + r][r..(n + r + 1)].clone_from_slice(&q_rev[..(n + 1)]);
672    }
673
674    // Bareiss fraction-free Gaussian elimination over Q.
675    bareiss_rat_det(mat)
676}
677
678/// Bareiss fraction-free determinant over Q.
679///
680/// Uses rational arithmetic so division is exact.  The Bareiss invariant
681/// guarantees every division step is exact (Sylvester's identity).
682fn bareiss_rat_det(mut mat: Vec<Vec<BigRational>>) -> BigRational {
683    let n = mat.len();
684    if n == 0 {
685        return BigRational::one();
686    }
687    if n == 1 {
688        return mat.remove(0).remove(0);
689    }
690
691    let mut sign = BigRational::one();
692    let neg_one = -BigRational::one();
693
694    for col in 0..n {
695        // Find a nonzero pivot.
696        let pivot_row = (col..n).find(|&r| !mat[r][col].is_zero());
697        let pivot_row = match pivot_row {
698            Some(r) => r,
699            None => return BigRational::zero(),
700        };
701
702        if pivot_row != col {
703            mat.swap(col, pivot_row);
704            sign *= &neg_one;
705        }
706
707        let pivot = mat[col][col].clone();
708
709        for row in (col + 1)..n {
710            for j in (col + 1)..n {
711                let prod1 = &pivot * &mat[row][j];
712                let prod2 = &mat[row][col] * &mat[col][j];
713                let diff = prod1 - prod2;
714                if col == 0 {
715                    mat[row][j] = diff;
716                } else {
717                    // Exact division by the previous pivot.
718                    let prev_pivot = mat[col - 1][col - 1].clone();
719                    if prev_pivot.is_zero() {
720                        mat[row][j] = BigRational::zero();
721                    } else {
722                        mat[row][j] = diff / prev_pivot;
723                    }
724                }
725            }
726            mat[row][col] = BigRational::zero();
727        }
728    }
729
730    sign * mat[n - 1][n - 1].clone()
731}
732
733/// Reconstruct a polynomial from its values at distinct rational points via
734/// Lagrange interpolation.
735///
736/// Given `n+1` pairs `(x_0, y_0), …, (x_n, y_n)` this returns the unique
737/// polynomial of degree ≤ n passing through all points.
738fn lagrange_interpolate(points: &[BigRational], values: &[BigRational]) -> Polynomial {
739    let n = points.len();
740    assert_eq!(n, values.len(), "points and values must have equal length");
741
742    if n == 0 {
743        return Polynomial::new(vec![BigRational::zero()]);
744    }
745    if n == 1 {
746        return Polynomial::new(vec![values[0].clone()]);
747    }
748
749    // Accumulate the interpolating polynomial term by term.
750    let mut result_coeffs = vec![BigRational::zero(); n];
751
752    for i in 0..n {
753        // Compute the i-th Lagrange basis polynomial Lᵢ(x) = Π_{j≠i} (x - x_j) / (x_i - x_j)
754        // and scale it by yᵢ.
755        let mut basis_coeffs = vec![BigRational::one()]; // start as constant 1
756        let mut denom = BigRational::one();
757
758        for j in 0..n {
759            if j == i {
760                continue;
761            }
762            // Multiply basis by (x - x_j): shift coefficients up and subtract x_j * current
763            let mut new_coeffs = vec![BigRational::zero(); basis_coeffs.len() + 1];
764            for (k, c) in basis_coeffs.iter().enumerate() {
765                new_coeffs[k + 1] = new_coeffs[k + 1].clone() + c;
766                new_coeffs[k] = new_coeffs[k].clone() - c * &points[j];
767            }
768            basis_coeffs = new_coeffs;
769
770            // Accumulate denominator: (x_i - x_j)
771            denom *= &points[i] - &points[j];
772        }
773
774        // Scale: yᵢ / denom
775        let scale = &values[i] / &denom;
776        for (k, c) in basis_coeffs.iter().enumerate() {
777            result_coeffs[k] = result_coeffs[k].clone() + c * &scale;
778        }
779    }
780
781    Polynomial::new(result_coeffs)
782}
783
784/// Return the k-th row of Pascal's triangle as BigRational values.
785fn binomial_row(k: usize) -> Vec<BigRational> {
786    let mut row = vec![BigRational::one(); k + 1];
787    for i in 1..k {
788        // In-place left-to-right: row[j] = row[j] + row[j-1] (before update)
789        for j in (1..i + 1).rev() {
790            let prev = row[j - 1].clone();
791            row[j] = row[j].clone() + prev;
792        }
793    }
794    row
795}
796
797/// Compute `base^exp` for `base ∈ Q` and `exp ∈ usize`.
798fn rational_pow(base: &BigRational, exp: usize) -> BigRational {
799    if exp == 0 {
800        return BigRational::one();
801    }
802    let mut result = BigRational::one();
803    let mut base = base.clone();
804    let mut exp = exp;
805    while exp > 0 {
806        if exp % 2 == 1 {
807            result *= &base;
808        }
809        let b = base.clone();
810        base *= &b;
811        exp /= 2;
812    }
813    result
814}
815
816/// Given a polynomial `r` and a candidate interval `[lo, hi]` containing α+β
817/// or α·β, refine until exactly one root of `r` is isolated in the interval.
818///
819/// Returns the algebraic number with that isolating interval, or an error if
820/// isolation cannot be achieved within the refinement budget.
821fn isolate_root_in_interval(
822    r: Polynomial,
823    lo: BigRational,
824    hi: BigRational,
825) -> Result<AlgebraicNumber, AlgebraicNumberError> {
826    let mut lo = lo;
827    let mut hi = hi;
828
829    // If lo == hi, test directly.
830    if lo == hi {
831        if r.eval(&lo).is_zero() {
832            return AlgebraicNumber::new(r, lo.clone(), hi);
833        }
834        return Err(AlgebraicNumberError::NoRootInInterval);
835    }
836
837    // Broaden the interval slightly to handle exact-endpoint roots.
838    // We expand by 1 ULP in BigRational terms (add/subtract a tiny epsilon).
839    let eps = BigRational::new(BigInt::from(1), BigInt::from(1_000_000_000_i64));
840    lo -= &eps;
841    hi += &eps;
842
843    // Count roots and refine until we have exactly one.
844    for _ in 0..200 {
845        let count = sturm_root_count(&r, &lo, &hi);
846        if count == 1 {
847            return AlgebraicNumber::new(r, lo, hi);
848        }
849        if count == 0 {
850            return Err(AlgebraicNumberError::NoRootInInterval);
851        }
852        // More than one root: bisect.
853        let mid = (&lo + &hi) / BigRational::from(BigInt::from(2));
854        let count_left = sturm_root_count(&r, &lo, &mid);
855        if count_left >= 1 {
856            hi = mid;
857        } else {
858            lo = mid;
859        }
860    }
861
862    Err(AlgebraicNumberError::NonIsolatingInterval)
863}
864
865// ─── trait impls ────────────────────────────────────────────────────────────
866
867impl PartialEq for AlgebraicNumber {
868    fn eq(&self, other: &Self) -> bool {
869        let mut a = self.clone();
870        let mut b = other.clone();
871        matches!(a.compare(&mut b), Ordering::Equal)
872    }
873}
874
875impl Eq for AlgebraicNumber {}
876
877impl PartialOrd for AlgebraicNumber {
878    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
879        Some(self.cmp(other))
880    }
881}
882
883impl Ord for AlgebraicNumber {
884    fn cmp(&self, other: &Self) -> Ordering {
885        let mut a = self.clone();
886        let mut b = other.clone();
887        a.compare(&mut b)
888    }
889}
890
891// ─── tests ──────────────────────────────────────────────────────────────────
892
893#[cfg(test)]
894mod tests {
895    use super::*;
896
897    fn rat(n: i64) -> BigRational {
898        BigRational::from(BigInt::from(n))
899    }
900
901    #[test]
902    fn test_from_rational() {
903        let alg = AlgebraicNumber::from_rational(rat(42));
904        assert!(alg.is_rational());
905        assert_eq!(alg.to_rational(), Some(rat(42)));
906    }
907
908    #[test]
909    fn test_rational_arithmetic() {
910        let a = AlgebraicNumber::from_rational(rat(3));
911        let b = AlgebraicNumber::from_rational(rat(5));
912
913        let sum = a.add(&b).expect("test operation should succeed");
914        assert!(sum.is_rational());
915        assert_eq!(sum.to_rational(), Some(rat(8)));
916
917        let prod = a.mul(&b).expect("test operation should succeed");
918        assert!(prod.is_rational());
919        assert_eq!(prod.to_rational(), Some(rat(15)));
920    }
921
922    #[test]
923    fn test_negate() {
924        let a = AlgebraicNumber::from_rational(rat(5));
925        let neg_a = a.negate();
926
927        assert!(neg_a.is_rational());
928        assert_eq!(neg_a.to_rational(), Some(rat(-5)));
929    }
930
931    #[test]
932    fn test_compare() {
933        let a = AlgebraicNumber::from_rational(rat(3));
934        let b = AlgebraicNumber::from_rational(rat(5));
935
936        assert!(a < b);
937        assert!(b > a);
938        assert_eq!(a, a.clone());
939    }
940
941    #[test]
942    fn test_signum() {
943        let pos = AlgebraicNumber::from_rational(rat(5));
944        let neg = AlgebraicNumber::from_rational(rat(-3));
945        let zero = AlgebraicNumber::from_rational(rat(0));
946
947        assert_eq!(pos.signum(), 1);
948        assert_eq!(neg.signum(), -1);
949        assert_eq!(zero.signum(), 0);
950    }
951
952    #[test]
953    fn test_refine() {
954        // x^2 - 2, root in [1, 2]
955        let poly = Polynomial::new(vec![rat(-2), rat(0), rat(1)]);
956        let mut alg =
957            AlgebraicNumber::new(poly, rat(1), rat(2)).expect("test operation should succeed");
958
959        let initial_width = alg.interval_width();
960        alg.refine();
961        let refined_width = alg.interval_width();
962
963        assert!(refined_width < initial_width);
964    }
965
966    #[test]
967    fn test_interval_width() {
968        let alg = AlgebraicNumber::from_rational(rat(5));
969        assert_eq!(alg.interval_width(), rat(0));
970    }
971
972    #[test]
973    fn test_new_rejects_non_isolating_interval() {
974        // x^2 - 1 has two roots in [-2, 2]
975        let poly = Polynomial::new(vec![rat(-1), rat(0), rat(1)]);
976        let result = AlgebraicNumber::new(poly, rat(-2), rat(2));
977        assert!(result.is_err());
978    }
979
980    #[test]
981    fn test_new_accepts_isolating_interval() {
982        // x^2 - 2 has exactly one root in [1, 2]
983        let poly = Polynomial::new(vec![rat(-2), rat(0), rat(1)]);
984        let result = AlgebraicNumber::new(poly, rat(1), rat(2));
985        assert!(result.is_ok());
986    }
987
988    #[test]
989    fn test_sturm_root_count() {
990        // x^2 - 2: one positive root between 1 and 2
991        let poly = Polynomial::new(vec![rat(-2), rat(0), rat(1)]);
992        let count = sturm_root_count(&poly, &rat(1), &rat(2));
993        assert_eq!(count, 1);
994    }
995
996    #[test]
997    fn test_poly_substitute_neg_x() {
998        // p(x) = x^2 - 2 → p(-x) = x^2 - 2 (even polynomial, unchanged)
999        let poly = Polynomial::new(vec![rat(-2), rat(0), rat(1)]);
1000        let neg = poly_substitute_neg_x(&poly);
1001        assert_eq!(neg.eval(&rat(2)), poly.eval(&rat(2)));
1002
1003        // p(x) = x^3 → p(-x) = -x^3
1004        let poly2 = Polynomial::new(vec![rat(0), rat(0), rat(0), rat(1)]);
1005        let neg2 = poly_substitute_neg_x(&poly2);
1006        assert_eq!(neg2.eval(&rat(2)), -poly2.eval(&rat(2)));
1007    }
1008
1009    // ── helpers ──────────────────────────────────────────────────────────────
1010
1011    /// Construct √2 as an `AlgebraicNumber` (root of x²−2 in [1,2]).
1012    fn sqrt2() -> AlgebraicNumber {
1013        let poly = Polynomial::new(vec![rat(-2), rat(0), rat(1)]);
1014        AlgebraicNumber::new(poly, rat(1), rat(2)).expect("√2 construction")
1015    }
1016
1017    /// Construct √3 as an `AlgebraicNumber` (root of x²−3 in [1,2]).
1018    fn sqrt3() -> AlgebraicNumber {
1019        let poly = Polynomial::new(vec![rat(-3), rat(0), rat(1)]);
1020        AlgebraicNumber::new(poly, rat(1), rat(2)).expect("√3 construction")
1021    }
1022
1023    // ── new tests for non-rational add / mul ─────────────────────────────────
1024
1025    /// √2 + √3 ≈ 3.1462…
1026    /// Verify the sum isolates exactly one root and the minimal poly
1027    /// x⁴ − 10x² + 1 has opposite signs at the interval endpoints.
1028    #[test]
1029    fn test_add_irrational_sum_polynomial_root() {
1030        let a = sqrt2();
1031        let b = sqrt3();
1032        let mut sum = a.add(&b).expect("√2 + √3 should succeed");
1033
1034        // The minimal polynomial of √2+√3 is p(x) = x⁴−10x²+1.
1035        // By construction the isolating interval contains exactly one root.
1036        // Verify: p changes sign across the interval.
1037        let p = |x: &num_rational::BigRational| {
1038            x.clone() * x.clone() * x.clone() * x.clone() - rat(10) * x.clone() * x.clone() + rat(1)
1039        };
1040
1041        // Refine until the interval is very tight (width < 2^{-20}).
1042        let tight = num_rational::BigRational::new(BigInt::from(1), BigInt::from(1 << 20));
1043        sum.refine_to_precision(&tight);
1044
1045        let val_lo = p(&sum.lower);
1046        let val_hi = p(&sum.upper);
1047
1048        // Sign must differ (Intermediate Value Theorem guarantees a root).
1049        assert!(
1050            val_lo.is_negative() != val_hi.is_negative() || val_lo.is_zero() || val_hi.is_zero(),
1051            "expected sign change: p(lo)={:?}, p(hi)={:?}",
1052            val_lo,
1053            val_hi
1054        );
1055
1056        // The interval must straddle √2+√3 ≈ 3.146 (in (3, 4)).
1057        assert!(
1058            sum.lower < rat(4) && sum.upper > rat(3),
1059            "interval [{:?}, {:?}] should contain 3.146",
1060            sum.lower,
1061            sum.upper
1062        );
1063    }
1064
1065    /// √2 * √3 = √6 ≈ 2.449…
1066    /// Verify it is a root of x²−6 and the interval contains √6.
1067    #[test]
1068    fn test_mul_irrational_product_polynomial_root() {
1069        let a = sqrt2();
1070        let b = sqrt3();
1071        let mut prod = a.mul(&b).expect("√2 * √3 should succeed");
1072
1073        // Verify the product is in (2, 3) — the correct isolating interval for √6.
1074        assert!(
1075            prod.lower < num_rational::BigRational::new(BigInt::from(3), BigInt::from(1)),
1076            "lower bound should be < 3"
1077        );
1078        assert!(
1079            prod.upper > num_rational::BigRational::new(BigInt::from(2), BigInt::from(1)),
1080            "upper bound should be > 2"
1081        );
1082
1083        // The minimal poly of √6 is x² − 6; verify it changes sign across the interval.
1084        let p = |x: &num_rational::BigRational| x.clone() * x.clone() - rat(6);
1085
1086        let tight = num_rational::BigRational::new(BigInt::from(1), BigInt::from(1 << 20));
1087        prod.refine_to_precision(&tight);
1088
1089        let val_lo = p(&prod.lower);
1090        let val_hi = p(&prod.upper);
1091        assert!(
1092            val_lo.is_negative() != val_hi.is_negative() || val_lo.is_zero() || val_hi.is_zero(),
1093            "expected sign change for √6: p(lo)={:?}, p(hi)={:?}",
1094            val_lo,
1095            val_hi
1096        );
1097    }
1098
1099    /// Rational + irrational: 1 + √2 should succeed and equal (√2+1).
1100    #[test]
1101    fn test_add_rational_plus_irrational() {
1102        let one = AlgebraicNumber::from_rational(rat(1));
1103        let s2 = sqrt2();
1104        let result = one.add(&s2).expect("1 + √2 should succeed");
1105        // 1 + √2 ≈ 2.414; should be in (2, 3).
1106        assert!(
1107            result.lower < rat(3) && result.upper > rat(2),
1108            "1 + √2 expected in (2,3), got [{:?}, {:?}]",
1109            result.lower,
1110            result.upper
1111        );
1112    }
1113
1114    /// Helpers: poly_substitute_affine_t and univariate_resultant.
1115    #[test]
1116    fn test_poly_substitute_affine_t() {
1117        // q(t) = t − 1 (coeffs = [-1, 1]);  q(3 − t) should be (3-t) - 1 = 2 - t
1118        // = coeffs [2, -1]
1119        let q = Polynomial::new(vec![rat(-1), rat(1)]);
1120        let shifted = poly_substitute_affine_t(&q, &rat(3));
1121        assert_eq!(shifted.eval(&rat(0)), rat(2), "q(3-0) = 2");
1122        assert_eq!(shifted.eval(&rat(1)), rat(1), "q(3-1) = 2 - 1 = 1");
1123    }
1124
1125    #[test]
1126    fn test_univariate_resultant_linear() {
1127        // Res(t - a, t - b) = b - a.
1128        let p = Polynomial::new(vec![rat(-3), rat(1)]); // t - 3
1129        let q = Polynomial::new(vec![rat(-5), rat(1)]); // t - 5
1130        let res = univariate_resultant(&p, &q);
1131        // Res(t-3, t-5) = 5 - 3 = 2 (or -2 depending on sign convention; nonzero)
1132        assert!(
1133            !res.is_zero(),
1134            "resultant of coprime linear polys should be nonzero"
1135        );
1136    }
1137
1138    #[test]
1139    fn test_lagrange_interpolate_linear() {
1140        use num_bigint::BigInt;
1141        let points = vec![
1142            num_rational::BigRational::from(BigInt::from(0)),
1143            num_rational::BigRational::from(BigInt::from(1)),
1144        ];
1145        let values = vec![
1146            num_rational::BigRational::from(BigInt::from(3)),
1147            num_rational::BigRational::from(BigInt::from(5)),
1148        ];
1149        // Line through (0,3) and (1,5): p(x) = 3 + 2x
1150        let poly = lagrange_interpolate(&points, &values);
1151        assert_eq!(poly.eval(&points[0]), values[0]);
1152        assert_eq!(poly.eval(&points[1]), values[1]);
1153    }
1154}