Skip to main content

qualia_core_db/specialized_libs/
polynomial_algebra.rs

1//! Dense univariate polynomial algebra over `f64` coefficients.
2//!
3//! A [`Polynomial`] stores its dense coefficient vector little-endian: index `i`
4//! holds the coefficient of `x^i`. The vector is kept *trimmed* so the leading
5//! (highest-index) coefficient is non-zero, except for the zero polynomial which
6//! is represented by an empty vector (degree reported as `None`).
7//!
8//! Provided: add, sub, mul, long division (quotient + remainder), Euclidean
9//! `gcd`, `derivative`, Horner `eval`, and the `resultant` of two polynomials
10//! via the Euclidean (subresultant-style) remainder sequence.
11//!
12//! Fallible operations (dividing by the zero polynomial) **fail closed** with
13//! `Option` and never fabricate a result.
14
15/// Tolerance below which a coefficient is treated as exactly zero when trimming
16/// leading terms. Floating-point polynomial GCD/division is inherently
17/// approximate; this keeps the leading-coefficient bookkeeping robust.
18const EPS: f64 = 1e-9;
19
20/// Dense univariate polynomial with `f64` coefficients, little-endian.
21#[derive(Clone, Debug, PartialEq)]
22pub struct Polynomial {
23    /// `coeffs[i]` is the coefficient of `x^i`. Trimmed: last element non-zero
24    /// (unless the vector is empty, i.e. the zero polynomial).
25    coeffs: Vec<f64>,
26}
27
28impl Polynomial {
29    /// Build from coefficients (index `i` = coefficient of `x^i`), trimming any
30    /// near-zero high-order terms.
31    pub fn new(coeffs: Vec<f64>) -> Self {
32        let mut p = Polynomial { coeffs };
33        p.trim();
34        p
35    }
36
37    /// The zero polynomial.
38    pub fn zero() -> Self {
39        Polynomial { coeffs: Vec::new() }
40    }
41
42    /// The constant polynomial `c`.
43    pub fn constant(c: f64) -> Self {
44        Polynomial::new(vec![c])
45    }
46
47    /// Borrow the (trimmed) coefficient slice, little-endian.
48    pub fn coeffs(&self) -> &[f64] {
49        &self.coeffs
50    }
51
52    /// True if this is the zero polynomial.
53    pub fn is_zero(&self) -> bool {
54        self.coeffs.is_empty()
55    }
56
57    /// Degree of the polynomial, or `None` for the zero polynomial.
58    pub fn degree(&self) -> Option<usize> {
59        if self.coeffs.is_empty() {
60            None
61        } else {
62            Some(self.coeffs.len() - 1)
63        }
64    }
65
66    /// Leading coefficient (highest-order). `0.0` for the zero polynomial.
67    pub fn leading(&self) -> f64 {
68        *self.coeffs.last().unwrap_or(&0.0)
69    }
70
71    /// Drop near-zero high-order coefficients so the leading term is non-zero.
72    fn trim(&mut self) {
73        while let Some(&c) = self.coeffs.last() {
74            if c.abs() <= EPS {
75                self.coeffs.pop();
76            } else {
77                break;
78            }
79        }
80    }
81
82    /// Evaluate at `x` using Horner's method.
83    pub fn eval(&self, x: f64) -> f64 {
84        let mut acc = 0.0;
85        for &c in self.coeffs.iter().rev() {
86            acc = acc * x + c;
87        }
88        acc
89    }
90
91    /// Sum `self + other`.
92    pub fn add(&self, other: &Polynomial) -> Polynomial {
93        let n = self.coeffs.len().max(other.coeffs.len());
94        let mut out = vec![0.0; n];
95        for (i, c) in self.coeffs.iter().enumerate() {
96            out[i] += c;
97        }
98        for (i, c) in other.coeffs.iter().enumerate() {
99            out[i] += c;
100        }
101        Polynomial::new(out)
102    }
103
104    /// Difference `self - other`.
105    pub fn sub(&self, other: &Polynomial) -> Polynomial {
106        let n = self.coeffs.len().max(other.coeffs.len());
107        let mut out = vec![0.0; n];
108        for (i, c) in self.coeffs.iter().enumerate() {
109            out[i] += c;
110        }
111        for (i, c) in other.coeffs.iter().enumerate() {
112            out[i] -= c;
113        }
114        Polynomial::new(out)
115    }
116
117    /// Scale every coefficient by `s`.
118    pub fn scale(&self, s: f64) -> Polynomial {
119        Polynomial::new(self.coeffs.iter().map(|c| c * s).collect())
120    }
121
122    /// Product `self * other` (schoolbook convolution).
123    pub fn mul(&self, other: &Polynomial) -> Polynomial {
124        if self.is_zero() || other.is_zero() {
125            return Polynomial::zero();
126        }
127        let mut out = vec![0.0; self.coeffs.len() + other.coeffs.len() - 1];
128        for (i, a) in self.coeffs.iter().enumerate() {
129            for (j, b) in other.coeffs.iter().enumerate() {
130                out[i + j] += a * b;
131            }
132        }
133        Polynomial::new(out)
134    }
135
136    /// Polynomial long division: returns `(quotient, remainder)` with
137    /// `self == quotient * divisor + remainder` and `deg(remainder) <
138    /// deg(divisor)`. Fails closed (`None`) when dividing by the zero polynomial.
139    pub fn div_rem(&self, divisor: &Polynomial) -> Option<(Polynomial, Polynomial)> {
140        if divisor.is_zero() {
141            return None; // fail closed — division by zero polynomial undefined
142        }
143        // deg(self) < deg(divisor) → quotient 0, remainder self
144        let div_deg = divisor.degree().unwrap();
145        if self.is_zero() || self.degree().unwrap() < div_deg {
146            return Some((Polynomial::zero(), self.clone()));
147        }
148        let mut rem = self.coeffs.clone();
149        let div_lead = divisor.leading();
150        let quot_len = self.coeffs.len() - divisor.coeffs.len() + 1;
151        let mut quot = vec![0.0; quot_len];
152
153        // Work from the highest-order coefficient of the remainder downward.
154        for i in (0..quot_len).rev() {
155            let rem_idx = i + div_deg; // current leading term of `rem`
156            let factor = rem[rem_idx] / div_lead;
157            quot[i] = factor;
158            if factor != 0.0 {
159                for (j, dc) in divisor.coeffs.iter().enumerate() {
160                    rem[i + j] -= factor * dc;
161                }
162            }
163        }
164        Some((Polynomial::new(quot), Polynomial::new(rem)))
165    }
166
167    /// First derivative.
168    pub fn derivative(&self) -> Polynomial {
169        if self.coeffs.len() <= 1 {
170            return Polynomial::zero();
171        }
172        let mut out = Vec::with_capacity(self.coeffs.len() - 1);
173        for (i, c) in self.coeffs.iter().enumerate().skip(1) {
174            out.push(c * i as f64);
175        }
176        Polynomial::new(out)
177    }
178
179    /// Make the polynomial monic (leading coefficient 1). Zero polynomial maps
180    /// to itself.
181    pub fn monic(&self) -> Polynomial {
182        if self.is_zero() {
183            return Polynomial::zero();
184        }
185        let lead = self.leading();
186        self.scale(1.0 / lead)
187    }
188
189    /// Greatest common divisor via the Euclidean algorithm, returned *monic*
190    /// (so it is unique up to the normalisation `gcd` of the zero polynomial
191    /// with `p` is `monic(p)`).
192    pub fn gcd(&self, other: &Polynomial) -> Polynomial {
193        let mut a = self.clone();
194        let mut b = other.clone();
195        while !b.is_zero() {
196            // a mod b — divisor is non-zero so div_rem is Some
197            let (_, r) = a.div_rem(&b).expect("b non-zero in gcd loop");
198            a = b;
199            b = r;
200        }
201        a.monic()
202    }
203
204    /// Resultant of `self` and `other` via the Euclidean remainder sequence.
205    ///
206    /// The resultant is zero **iff** the two polynomials share a common root
207    /// (over the complex numbers / have a non-constant gcd). It is computed by
208    /// running the Euclidean algorithm and accumulating the standard
209    /// degree/leading-coefficient factors that relate `res(a, b)` to
210    /// `res(b, a mod b)`:
211    ///
212    /// `res(a, b) = (-1)^(deg a · deg b) · lc(b)^(deg a − deg r) · res(b, r)`
213    ///
214    /// with base cases `res(a, const c) = c^(deg a)` and a zero result whenever
215    /// a remainder vanishes with positive remaining degree (a common factor).
216    pub fn resultant(&self, other: &Polynomial) -> f64 {
217        // Degenerate cases.
218        if self.is_zero() || other.is_zero() {
219            return 0.0;
220        }
221        let mut a = self.clone();
222        let mut b = other.clone();
223        let mut result = 1.0_f64;
224
225        loop {
226            let deg_a = a.degree().unwrap();
227            let deg_b = b.degree().unwrap();
228
229            // Base case: b is a constant.
230            if deg_b == 0 {
231                // res(a, c) = c^(deg a)
232                result *= b.leading().powi(deg_a as i32);
233                return result;
234            }
235
236            // a mod b
237            let (_, r) = a.div_rem(&b).expect("b non-constant ⇒ non-zero");
238
239            // Sign factor from swapping the Euclidean step.
240            if (deg_a % 2 == 1) && (deg_b % 2 == 1) {
241                result = -result;
242            }
243
244            if r.is_zero() {
245                // Common factor of positive degree ⇒ resultant is zero.
246                return 0.0;
247            }
248
249            let deg_r = r.degree().unwrap();
250            // lc(b)^(deg a − deg r)
251            result *= b.leading().powi((deg_a as i32) - (deg_r as i32));
252
253            a = b;
254            b = r;
255        }
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    /// Helper: `x - r` as a polynomial.
264    fn linear(r: f64) -> Polynomial {
265        Polynomial::new(vec![-r, 1.0])
266    }
267
268    fn approx(a: &Polynomial, b: &Polynomial) -> bool {
269        if a.coeffs().len() != b.coeffs().len() {
270            return false;
271        }
272        a.coeffs()
273            .iter()
274            .zip(b.coeffs())
275            .all(|(x, y)| (x - y).abs() < 1e-6)
276    }
277
278    #[test]
279    fn eval_horner() {
280        // p(x) = 2 + 3x + x^2, p(2) = 2 + 6 + 4 = 12
281        let p = Polynomial::new(vec![2.0, 3.0, 1.0]);
282        assert!((p.eval(2.0) - 12.0).abs() < 1e-12);
283    }
284
285    #[test]
286    fn add_sub() {
287        let a = Polynomial::new(vec![1.0, 2.0, 3.0]);
288        let b = Polynomial::new(vec![0.0, 1.0, -3.0]);
289        assert_eq!(a.add(&b), Polynomial::new(vec![1.0, 3.0, 0.0]));
290        // leading cancels → trimmed to degree 1
291        assert_eq!(a.add(&b).degree(), Some(1));
292        assert_eq!(a.sub(&a), Polynomial::zero());
293    }
294
295    #[test]
296    fn mul_factors() {
297        // (x-1)(x+1) = x^2 - 1
298        let prod = linear(1.0).mul(&Polynomial::new(vec![1.0, 1.0]));
299        assert!(approx(&prod, &Polynomial::new(vec![-1.0, 0.0, 1.0])));
300    }
301
302    #[test]
303    fn division_exact_x2_minus_1() {
304        // (x^2 - 1) / (x - 1) = (x + 1), remainder 0
305        let num = Polynomial::new(vec![-1.0, 0.0, 1.0]);
306        let den = linear(1.0); // x - 1
307        let (q, r) = num.div_rem(&den).unwrap();
308        assert!(approx(&q, &Polynomial::new(vec![1.0, 1.0])), "q = {:?}", q);
309        assert!(r.is_zero(), "remainder = {:?}", r);
310    }
311
312    #[test]
313    fn division_with_remainder_reconstructs() {
314        // (x^3 + 2x + 1) / (x^2 + 1)
315        let num = Polynomial::new(vec![1.0, 2.0, 0.0, 1.0]);
316        let den = Polynomial::new(vec![1.0, 0.0, 1.0]);
317        let (q, r) = num.div_rem(&den).unwrap();
318        // q*den + r == num
319        let recon = q.mul(&den).add(&r);
320        assert!(approx(&recon, &num), "recon = {:?}", recon);
321        assert!(r.degree().unwrap_or(0) < den.degree().unwrap());
322    }
323
324    #[test]
325    fn divide_by_zero_poly_fails_closed() {
326        let num = Polynomial::new(vec![1.0, 1.0]);
327        assert!(num.div_rem(&Polynomial::zero()).is_none());
328    }
329
330    #[test]
331    fn gcd_x2_minus_1_and_x_minus_1() {
332        // gcd(x^2 - 1, x - 1) = x - 1 (up to scale → monic x - 1)
333        let a = Polynomial::new(vec![-1.0, 0.0, 1.0]);
334        let b = linear(1.0);
335        let g = a.gcd(&b);
336        // monic(x - 1) = x - 1
337        assert!(
338            approx(&g, &Polynomial::new(vec![-1.0, 1.0])),
339            "gcd = {:?}",
340            g
341        );
342    }
343
344    #[test]
345    fn gcd_shared_quadratic_factor() {
346        // a = (x-1)(x-2), b = (x-2)(x-3) → gcd = (x-2) monic
347        let a = linear(1.0).mul(&linear(2.0));
348        let b = linear(2.0).mul(&linear(3.0));
349        let g = a.gcd(&b);
350        assert!(approx(&g, &linear(2.0)), "gcd = {:?}", g);
351    }
352
353    #[test]
354    fn derivative_basic() {
355        // d/dx (x^3 + 2x^2 + 5x + 7) = 3x^2 + 4x + 5
356        let p = Polynomial::new(vec![7.0, 5.0, 2.0, 1.0]);
357        assert_eq!(p.derivative(), Polynomial::new(vec![5.0, 4.0, 3.0]));
358        assert!(Polynomial::constant(4.0).derivative().is_zero());
359    }
360
361    #[test]
362    fn resultant_zero_iff_common_root() {
363        // Share root x=2 ⇒ resultant 0.
364        let a = linear(1.0).mul(&linear(2.0)); // (x-1)(x-2)
365        let b = linear(2.0).mul(&linear(3.0)); // (x-2)(x-3)
366        assert!(
367            a.resultant(&b).abs() < 1e-6,
368            "expected ~0, got {}",
369            a.resultant(&b)
370        );
371
372        // No common root ⇒ resultant non-zero.
373        let c = linear(1.0).mul(&linear(2.0)); // (x-1)(x-2)
374        let d = linear(3.0).mul(&linear(4.0)); // (x-3)(x-4)
375        assert!(
376            c.resultant(&d).abs() > 1e-6,
377            "expected non-zero, got {}",
378            c.resultant(&d)
379        );
380    }
381
382    #[test]
383    fn resultant_known_value() {
384        // res(x-1, x-2): the product of differences of roots = (1 - 2) = -1.
385        // res(a,b) = lc(a)^deg(b) * prod over roots α of a of b(α)
386        //          = 1 * b(1) = (1 - 2) = -1
387        let a = linear(1.0); // x - 1
388        let b = linear(2.0); // x - 2
389        let r = a.resultant(&b);
390        assert!((r - (-1.0)).abs() < 1e-9, "resultant = {}", r);
391    }
392}