Skip to main content

zenith_float_num/
poly.rs

1//! Dense univariate polynomials with [`ExactNum`] coefficients.
2
3use crate::defs::RoundingMode;
4use crate::defs::WORD_BIT_SIZE;
5use crate::Consts;
6use crate::ExactInt;
7use crate::ExactNum;
8use crate::ExactNumArray;
9use alloc::vec;
10use alloc::vec::Vec;
11
12/// Highest degree whose companion eigenvalues have a closed form on this type.
13///
14/// Degree 1 is linear. Degree 2 is the characteristic polynomial of the 2×2
15/// companion (quadratic formula). Higher-degree companions are not symmetric,
16/// so [`ExactNumArray::eigen_decomp`] cannot be used; those degrees return
17/// `None` from [`ExactNumPoly::roots_real`].
18pub const POLY_COMPANION_CLOSED_DEG: usize = 2;
19
20/// Dense univariate polynomial `c₀ + c₁ x + ⋯ + cₙ xⁿ`.
21///
22/// Coefficients are stored lowest degree first. Leading zeros are stripped.
23/// The zero polynomial has an empty coefficient vector. Arithmetic methods
24/// take an explicit precision `p` and rounding mode; stored `(p, rm)` are the
25/// values used when the polynomial was constructed.
26#[derive(Clone, Debug)]
27pub struct ExactNumPoly {
28    coeffs: Vec<ExactNum>,
29    p: usize,
30    rm: RoundingMode,
31}
32
33impl ExactNumPoly {
34    /// Zero polynomial at `(p, rm)`.
35    pub fn zero(p: usize, rm: RoundingMode) -> Self {
36        Self {
37            coeffs: Vec::new(),
38            p,
39            rm,
40        }
41    }
42
43    /// Constant `1` at `(p, rm)`.
44    pub fn one(p: usize, rm: RoundingMode) -> Self {
45        Self::from_coeffs(p, rm, &[ExactNum::from_u8(1, p)])
46    }
47
48    /// Build from coefficients, lowest degree first. Each coefficient is
49    /// rounded to `(p, rm)`. Leading zeros are stripped.
50    pub fn from_coeffs(p: usize, rm: RoundingMode, coeffs: &[ExactNum]) -> Self {
51        let mut out = Vec::with_capacity(coeffs.len());
52        for c in coeffs {
53            let mut y = c.clone();
54            let _ = y.set_precision(p, rm);
55            out.push(y);
56        }
57        let mut s = Self { coeffs: out, p, rm };
58        s.strip_leading();
59        s
60    }
61
62    /// Coefficients from `i64` values, lowest degree first.
63    pub fn from_i64_coeffs(p: usize, rm: RoundingMode, coeffs: &[i64]) -> Self {
64        let xs: Vec<ExactNum> = coeffs.iter().map(|&k| ExactNum::from_i64(k, p)).collect();
65        Self::from_coeffs(p, rm, &xs)
66    }
67
68    /// Stored construction precision.
69    pub fn precision(&self) -> usize {
70        self.p
71    }
72
73    /// Stored construction rounding mode.
74    pub fn rounding(&self) -> RoundingMode {
75        self.rm
76    }
77
78    /// Coefficients, lowest degree first. Empty if this is the zero polynomial.
79    pub fn coeffs(&self) -> &[ExactNum] {
80        &self.coeffs
81    }
82
83    /// Degree `n` of a nonzero polynomial. `None` if this is zero.
84    pub fn degree(&self) -> Option<usize> {
85        if self.coeffs.is_empty() {
86            None
87        } else {
88            Some(self.coeffs.len() - 1)
89        }
90    }
91
92    /// True if every coefficient is zero (or the vector is empty).
93    pub fn is_zero(&self) -> bool {
94        self.coeffs.is_empty() || self.coeffs.iter().all(|c| c.is_zero())
95    }
96
97    /// Coefficient of `x^k`, or zero if `k` exceeds the degree.
98    pub fn coeff(&self, k: usize) -> ExactNum {
99        self.coeffs
100            .get(k)
101            .cloned()
102            .unwrap_or_else(|| ExactNum::new(self.p))
103    }
104
105    fn leading(&self) -> ExactNum {
106        self.coeffs
107            .last()
108            .cloned()
109            .unwrap_or_else(|| ExactNum::new(self.p))
110    }
111
112    fn all_finite(&self) -> bool {
113        self.coeffs.iter().all(|c| !c.is_nan() && !c.is_inf())
114    }
115
116    fn strip_leading(&mut self) {
117        while self.coeffs.last().is_some_and(|c| c.is_zero()) {
118            self.coeffs.pop();
119        }
120    }
121
122    fn work_p(p: usize) -> usize {
123        p.saturating_add(WORD_BIT_SIZE)
124    }
125
126    /// Horner evaluation `c₀ + x(c₁ + x(c₂ + ⋯))` at `(p, rm)`.
127    ///
128    /// `cc` is accepted for signature uniformity with other numeric methods;
129    /// Horner uses only add and fused multiply-add.
130    pub fn eval(&self, x: &ExactNum, p: usize, rm: RoundingMode, _cc: &mut Consts) -> ExactNum {
131        ExactNum::polyval(&self.coeffs, x, p, rm)
132    }
133
134    /// Coefficient-wise sum at precision `p`.
135    pub fn add(&self, rhs: &Self, p: usize, rm: RoundingMode) -> Self {
136        let n = self.coeffs.len().max(rhs.coeffs.len());
137        let mut out = Vec::with_capacity(n);
138        for i in 0..n {
139            let a = self.coeff(i);
140            let b = rhs.coeff(i);
141            out.push(a.add(&b, p, rm));
142        }
143        Self::from_coeffs(p, rm, &out)
144    }
145
146    /// Coefficient-wise difference at precision `p`.
147    pub fn sub(&self, rhs: &Self, p: usize, rm: RoundingMode) -> Self {
148        let n = self.coeffs.len().max(rhs.coeffs.len());
149        let mut out = Vec::with_capacity(n);
150        for i in 0..n {
151            let a = self.coeff(i);
152            let b = rhs.coeff(i);
153            out.push(a.sub(&b, p, rm));
154        }
155        Self::from_coeffs(p, rm, &out)
156    }
157
158    /// Schoolbook product at precision `p`.
159    pub fn mul(&self, rhs: &Self, p: usize, rm: RoundingMode) -> Self {
160        if self.is_zero() || rhs.is_zero() {
161            return Self::zero(p, rm);
162        }
163        let n = self.coeffs.len() + rhs.coeffs.len() - 1;
164        let mut out = vec![ExactNum::new(p); n];
165        for (i, a) in self.coeffs.iter().enumerate() {
166            for (j, b) in rhs.coeffs.iter().enumerate() {
167                let t = a.mul(b, p, rm);
168                out[i + j] = out[i + j].add(&t, p, rm);
169            }
170        }
171        Self::from_coeffs(p, rm, &out)
172    }
173
174    /// Polynomial division: `self = q·g + r` with `deg r < deg g`.
175    ///
176    /// `None` if `g` is the zero polynomial or a coefficient is non-finite.
177    pub fn div_rem(&self, g: &Self, p: usize, rm: RoundingMode) -> Option<(Self, Self)> {
178        if g.is_zero() || !self.all_finite() || !g.all_finite() {
179            return None;
180        }
181        let deg_g = g.degree()?;
182        let lc_g = g.leading();
183        if lc_g.is_zero() {
184            return None;
185        }
186        let mut r = self.coeffs.clone();
187        for c in &mut r {
188            let _ = c.set_precision(p, rm);
189        }
190        while r.last().is_some_and(|c| c.is_zero()) {
191            r.pop();
192        }
193        if r.is_empty() {
194            return Some((Self::zero(p, rm), Self::zero(p, rm)));
195        }
196        let deg_f = r.len() - 1;
197        let mut q = if deg_f >= deg_g {
198            vec![ExactNum::new(p); deg_f - deg_g + 1]
199        } else {
200            Vec::new()
201        };
202        while r.len() > deg_g {
203            let deg_r = r.len() - 1;
204            let shift = deg_r - deg_g;
205            let t = r[deg_r].div(&lc_g, p, rm);
206            q[shift] = q[shift].add(&t, p, rm);
207            for i in 0..=deg_g {
208                let term = t.mul(&g.coeff(i), p, rm);
209                let idx = i + shift;
210                r[idx] = r[idx].sub(&term, p, rm);
211            }
212            while r.last().is_some_and(|c| c.is_zero()) {
213                r.pop();
214            }
215        }
216        Some((Self::from_coeffs(p, rm, &q), Self::from_coeffs(p, rm, &r)))
217    }
218
219    fn integer_content(&self) -> Option<ExactInt> {
220        if self.coeffs.is_empty() {
221            return Some(ExactInt::zero());
222        }
223        let mut g: Option<ExactInt> = None;
224        for c in &self.coeffs {
225            if c.is_nan() || c.is_inf() || !c.fract().is_zero() {
226                return None;
227            }
228            let Some(i) = ExactInt::from_exact_num(c) else {
229                return None;
230            };
231            g = Some(match g {
232                None => i,
233                Some(prev) => prev.gcd(&i),
234            });
235        }
236        g
237    }
238
239    /// Primitive part with positive leading coefficient, then monic.
240    ///
241    /// Integer content (GCD of integer coefficients) is divided out when every
242    /// coefficient is an integer. Otherwise only the leading coefficient is
243    /// removed (float monic form).
244    fn primitive_monic(&self, p: usize, rm: RoundingMode) -> Self {
245        if self.is_zero() {
246            return Self::zero(p, rm);
247        }
248        let mut s = if let Some(cont) = self.integer_content() {
249            if !cont.is_zero() && !cont.is_one() {
250                let d = cont.to_exact_num(p, rm);
251                let xs: Vec<ExactNum> = self.coeffs.iter().map(|c| c.div(&d, p, rm)).collect();
252                Self::from_coeffs(p, rm, &xs)
253            } else {
254                self.clone()
255            }
256        } else {
257            self.clone()
258        };
259        let lc = s.leading();
260        if lc.is_zero() || lc.is_nan() || lc.is_inf() {
261            return s;
262        }
263        if lc.is_negative() {
264            s.coeffs = s.coeffs.into_iter().map(|c| c.neg()).collect();
265        }
266        let lc = s.leading();
267        if lc.is_zero() {
268            return s;
269        }
270        let xs: Vec<ExactNum> = s.coeffs.iter().map(|c| c.div(&lc, p, rm)).collect();
271        Self::from_coeffs(p, rm, &xs)
272    }
273
274    /// Euclidean GCD with integer content removal, returned monic.
275    ///
276    /// Both zero → zero. One zero → monic of the other.
277    pub fn gcd(&self, other: &Self, p: usize, rm: RoundingMode) -> Self {
278        if self.is_zero() && other.is_zero() {
279            return Self::zero(p, rm);
280        }
281        let mut a = self.primitive_monic(p, rm);
282        let mut b = other.primitive_monic(p, rm);
283        let max_steps = a.degree().unwrap_or(0) + b.degree().unwrap_or(0) + 1;
284        let mut steps = 0;
285        while !b.is_zero() && steps < max_steps {
286            steps += 1;
287            let Some((_, r)) = a.div_rem(&b, p, rm) else {
288                break;
289            };
290            a = b;
291            b = r.primitive_monic(p, rm);
292        }
293        a.primitive_monic(p, rm)
294    }
295
296    /// Composition `self(g(x))` by Horner at precision `p`.
297    pub fn compose(&self, g: &Self, p: usize, rm: RoundingMode, _cc: &mut Consts) -> Self {
298        if self.is_zero() {
299            return Self::zero(p, rm);
300        }
301        let mut acc = Self::from_coeffs(p, rm, &[self.leading()]);
302        for c in self.coeffs.iter().rev().skip(1) {
303            acc = acc.mul(g, p, rm);
304            acc = acc.add(&Self::from_coeffs(p, rm, &[c.clone()]), p, rm);
305        }
306        acc
307    }
308
309    /// Formal derivative. The derivative of a constant is zero.
310    pub fn derivative(&self, p: usize, rm: RoundingMode) -> Self {
311        if self.coeffs.len() <= 1 {
312            return Self::zero(p, rm);
313        }
314        let mut out = Vec::with_capacity(self.coeffs.len() - 1);
315        for (k, c) in self.coeffs.iter().enumerate().skip(1) {
316            let n = ExactNum::from_u32(k as u32, p);
317            out.push(c.mul(&n, p, rm));
318        }
319        Self::from_coeffs(p, rm, &out)
320    }
321
322    /// Indefinite integral with constant term zero.
323    pub fn integral(&self, p: usize, rm: RoundingMode) -> Self {
324        if self.is_zero() {
325            return Self::zero(p, rm);
326        }
327        let mut out = Vec::with_capacity(self.coeffs.len() + 1);
328        out.push(ExactNum::new(p));
329        for (k, c) in self.coeffs.iter().enumerate() {
330            let den = ExactNum::from_u32((k + 1) as u32, p);
331            out.push(c.div(&den, p, rm));
332        }
333        Self::from_coeffs(p, rm, &out)
334    }
335
336    /// Monic companion matrix of this polynomial, or `None` if the degree is
337    /// zero or the polynomial is zero / non-finite.
338    ///
339    /// Last column is `(-c₀/cₙ, …, -cₙ₋₁/cₙ)`. Subdiagonal is ones.
340    pub fn companion_matrix(&self, p: usize, rm: RoundingMode) -> Option<ExactNumArray> {
341        let n = self.degree()?;
342        if n == 0 || !self.all_finite() {
343            return None;
344        }
345        let lc = self.leading();
346        if lc.is_zero() {
347            return None;
348        }
349        let mut vals = vec![ExactNum::new(p); n * n];
350        let one = ExactNum::from_u8(1, p);
351        for i in 1..n {
352            vals[i * n + (i - 1)] = one.clone();
353        }
354        for i in 0..n {
355            let a = self.coeff(i).div(&lc, p, rm);
356            vals[i * n + (n - 1)] = a.neg();
357        }
358        ExactNumArray::from_shape(p, n, n, &vals)
359    }
360
361    /// Real roots via the companion characteristic equation.
362    ///
363    /// Degree 1 and 2 use the closed-form eigenvalues of the companion
364    /// (linear solve / quadratic formula) at extra working precision, then
365    /// one round to `p`. Degree greater than [`POLY_COMPANION_CLOSED_DEG`]
366    /// returns `None` — those companions are not symmetric, so
367    /// [`ExactNumArray::eigen_decomp`] does not apply.
368    ///
369    /// The zero polynomial returns `None`. A nonzero constant returns an
370    /// empty vector. A negative discriminant returns an empty vector.
371    pub fn roots_real(
372        &self,
373        p: usize,
374        rm: RoundingMode,
375        _cc: &mut Consts,
376    ) -> Option<Vec<ExactNum>> {
377        if !self.all_finite() {
378            return None;
379        }
380        let n = match self.degree() {
381            None => return None,
382            Some(0) => return Some(Vec::new()),
383            Some(n) => n,
384        };
385        if n > POLY_COMPANION_CLOSED_DEG {
386            return None;
387        }
388        let wrk = Self::work_p(p);
389        let lc = self.leading();
390        if n == 1 {
391            let r = self.coeff(0).div(&lc, wrk, RoundingMode::None).neg();
392            let mut r = r;
393            let _ = r.set_precision(p, rm);
394            return Some(vec![r]);
395        }
396        // Degree 2: a x² + b x + c = 0. Companion eigenvalues.
397        let a = self.coeff(2);
398        let b = self.coeff(1);
399        let c = self.coeff(0);
400        let four = ExactNum::from_u8(4, wrk);
401        let disc = b.mul(&b, wrk, RoundingMode::None).sub(
402            &four
403                .mul(&a, wrk, RoundingMode::None)
404                .mul(&c, wrk, RoundingMode::None),
405            wrk,
406            RoundingMode::None,
407        );
408        if disc.is_negative() {
409            return Some(Vec::new());
410        }
411        let two_a = ExactNum::from_u8(2, wrk).mul(&a, wrk, RoundingMode::None);
412        if two_a.is_zero() {
413            return None;
414        }
415        let sqrt_d = disc.sqrt(wrk, RoundingMode::None);
416        let mut r0 =
417            b.neg()
418                .sub(&sqrt_d, wrk, RoundingMode::None)
419                .div(&two_a, wrk, RoundingMode::None);
420        let mut r1 =
421            b.neg()
422                .add(&sqrt_d, wrk, RoundingMode::None)
423                .div(&two_a, wrk, RoundingMode::None);
424        let _ = r0.set_precision(p, rm);
425        let _ = r1.set_precision(p, rm);
426        if disc.is_zero() {
427            Some(vec![r0])
428        } else {
429            Some(vec![r0, r1])
430        }
431    }
432}
433
434impl PartialEq for ExactNumPoly {
435    fn eq(&self, other: &Self) -> bool {
436        if self.coeffs.len() != other.coeffs.len() {
437            return false;
438        }
439        self.coeffs
440            .iter()
441            .zip(other.coeffs.iter())
442            .all(|(a, b)| a == b)
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449    use crate::Consts;
450
451    fn gold_p() -> (usize, RoundingMode) {
452        (256, RoundingMode::ToEven)
453    }
454
455    #[test]
456    fn exact_num_poly_div_rem_gcd_compose_deriv_roots() {
457        let (p, rm) = gold_p();
458        let mut cc = Consts::new().expect("consts");
459
460        let x2m1 = ExactNumPoly::from_i64_coeffs(p, rm, &[-1, 0, 1]);
461        let xm1 = ExactNumPoly::from_i64_coeffs(p, rm, &[-1, 1]);
462        let (q, r) = x2m1.div_rem(&xm1, p, rm).expect("div_rem");
463        assert_eq!(q, ExactNumPoly::from_i64_coeffs(p, rm, &[1, 1]));
464        assert!(r.is_zero());
465
466        let g = x2m1.gcd(&xm1, p, rm);
467        assert_eq!(g, xm1);
468
469        let x2 = ExactNumPoly::from_i64_coeffs(p, rm, &[0, 0, 1]);
470        let xp1 = ExactNumPoly::from_i64_coeffs(p, rm, &[1, 1]);
471        let composed = x2.compose(&xp1, p, rm, &mut cc);
472        assert_eq!(composed, ExactNumPoly::from_i64_coeffs(p, rm, &[1, 2, 1]));
473
474        let x3 = ExactNumPoly::from_i64_coeffs(p, rm, &[0, 0, 0, 1]);
475        let dx = x3.derivative(p, rm);
476        assert_eq!(dx, ExactNumPoly::from_i64_coeffs(p, rm, &[0, 0, 3]));
477
478        let x2m2 = ExactNumPoly::from_i64_coeffs(p, rm, &[-2, 0, 1]);
479        let roots = x2m2.roots_real(p, rm, &mut cc).expect("roots");
480        assert_eq!(roots.len(), 2);
481        let s2 = ExactNum::from_u8(2, p).sqrt(p, rm);
482        let ns2 = s2.neg();
483        let mut saw_pos = false;
484        let mut saw_neg = false;
485        for root in &roots {
486            if root.cmp(&s2) == Some(0) {
487                saw_pos = true;
488            }
489            if root.cmp(&ns2) == Some(0) {
490                saw_neg = true;
491            }
492        }
493        assert!(saw_pos && saw_neg);
494
495        let c = x2m2.companion_matrix(p, rm).expect("companion");
496        assert_eq!(c.shape(), (2, 2));
497        let two = ExactNum::from_u8(2, p);
498        let one = ExactNum::from_u8(1, p);
499        let z = ExactNum::new(p);
500        assert_eq!(c.get2(0, 0).map(|x| x.cmp(&z)), Some(Some(0)));
501        assert_eq!(c.get2(0, 1).map(|x| x.cmp(&two)), Some(Some(0)));
502        assert_eq!(c.get2(1, 0).map(|x| x.cmp(&one)), Some(Some(0)));
503        assert_eq!(c.get2(1, 1).map(|x| x.cmp(&z)), Some(Some(0)));
504
505        assert!(x2m1.div_rem(&ExactNumPoly::zero(p, rm), p, rm).is_none());
506        assert!(x3.roots_real(p, rm, &mut cc).is_none());
507    }
508}