Skip to main content

oxiz_math/polynomial/
extended_ops.rs

1//! Extended polynomial operations: calculus, GCD, and evaluation.
2
3use super::helpers::*;
4use super::types::*;
5#[allow(unused_imports)]
6use crate::prelude::*;
7use crate::simd::polynomial_simd::{poly_add_coeffs, poly_dot_product, poly_mul_scalar};
8use num_bigint::BigInt;
9use num_rational::BigRational;
10use num_traits::{One, Signed, Zero};
11
12type Polynomial = super::Polynomial;
13
14impl super::Polynomial {
15    /// Compute the derivative with respect to a variable.
16    pub fn derivative(&self, var: Var) -> Polynomial {
17        let terms: Vec<Term> = self
18            .terms
19            .iter()
20            .filter_map(|t| {
21                let d = t.monomial.degree(var);
22                if d == 0 {
23                    return None;
24                }
25                let new_coeff = &t.coeff * BigRational::from_integer(BigInt::from(d));
26                let new_mon = if d == 1 {
27                    t.monomial
28                        .div(&Monomial::from_var(var))
29                        .unwrap_or_else(Monomial::unit)
30                } else {
31                    let new_powers: Vec<(Var, u32)> = t
32                        .monomial
33                        .vars()
34                        .iter()
35                        .map(|vp| {
36                            if vp.var == var {
37                                (vp.var, vp.power - 1)
38                            } else {
39                                (vp.var, vp.power)
40                            }
41                        })
42                        .filter(|(_, p)| *p > 0)
43                        .collect();
44                    Monomial::from_powers(new_powers)
45                };
46                Some(Term::new(new_coeff, new_mon))
47            })
48            .collect();
49        Polynomial::from_terms(terms, self.order)
50    }
51
52    /// Compute the nth derivative of the polynomial with respect to a variable.
53    ///
54    /// # Arguments
55    /// * `var` - The variable to differentiate with respect to
56    /// * `n` - The order of the derivative (n = 0 returns the polynomial itself)
57    ///
58    /// # Examples
59    /// ```
60    /// use oxiz_math::polynomial::Polynomial;
61    ///
62    /// // f(x) = x^3 = x³
63    /// let f = Polynomial::from_coeffs_int(&[(1, &[(0, 3)])]);
64    ///
65    /// // f'(x) = 3x^2
66    /// let f_prime = f.nth_derivative(0, 1);
67    /// assert_eq!(f_prime.total_degree(), 2);
68    ///
69    /// // f''(x) = 6x
70    /// let f_double_prime = f.nth_derivative(0, 2);
71    /// assert_eq!(f_double_prime.total_degree(), 1);
72    ///
73    /// // f'''(x) = 6 (constant)
74    /// let f_triple_prime = f.nth_derivative(0, 3);
75    /// assert_eq!(f_triple_prime.total_degree(), 0);
76    /// assert!(!f_triple_prime.is_zero());
77    ///
78    /// // f''''(x) = 0
79    /// let f_fourth = f.nth_derivative(0, 4);
80    /// assert!(f_fourth.is_zero());
81    /// ```
82    pub fn nth_derivative(&self, var: Var, n: u32) -> Polynomial {
83        if n == 0 {
84            return self.clone();
85        }
86
87        let mut result = self.clone();
88        for _ in 0..n {
89            result = result.derivative(var);
90            if result.is_zero() {
91                break;
92            }
93        }
94        result
95    }
96
97    /// Computes the gradient (vector of partial derivatives) with respect to all variables.
98    ///
99    /// For a multivariate polynomial f(x₁, x₂, ..., xₙ), returns the vector:
100    /// ∇f = [∂f/∂x₁, ∂f/∂x₂, ..., ∂f/∂xₙ]
101    ///
102    /// # Returns
103    /// A vector of polynomials, one for each variable, ordered by variable index.
104    ///
105    /// # Example
106    /// ```
107    /// use oxiz_math::polynomial::Polynomial;
108    /// // f(x,y) = x²y + 2xy + y²
109    /// let f = Polynomial::from_coeffs_int(&[
110    ///     (1, &[(0, 2), (1, 1)]), // x²y
111    ///     (2, &[(0, 1), (1, 1)]), // 2xy
112    ///     (1, &[(1, 2)]),          // y²
113    /// ]);
114    ///
115    /// let grad = f.gradient();
116    /// // ∂f/∂x = 2xy + 2y
117    /// // ∂f/∂y = x² + 2x + 2y
118    /// assert_eq!(grad.len(), 2);
119    /// ```
120    pub fn gradient(&self) -> Vec<Polynomial> {
121        let vars = self.vars();
122        if vars.is_empty() {
123            return vec![];
124        }
125
126        // `vars` was just confirmed non-empty above, so `max()` always
127        // yields `Some`; `unwrap_or(0)` avoids an `.expect()` panic path
128        // without changing behavior (the fallback is unreachable).
129        let max_var = *vars.iter().max().unwrap_or(&0);
130        let mut grad = Vec::new();
131
132        // Compute partial derivative for each variable from 0 to max_var
133        for var in 0..=max_var {
134            grad.push(self.derivative(var));
135        }
136
137        grad
138    }
139
140    /// Computes the Hessian matrix (matrix of second-order partial derivatives).
141    ///
142    /// For a multivariate polynomial f(x₁, x₂, ..., xₙ), returns the symmetric matrix:
143    /// `H[i,j] = ∂²f/(∂xᵢ∂xⱼ)`
144    ///
145    /// The Hessian is useful for:
146    /// - Optimization (finding local minima/maxima)
147    /// - Convexity analysis
148    /// - Second-order Taylor approximations
149    ///
150    /// # Returns
151    /// A vector of vectors representing the Hessian matrix.
152    /// The matrix is symmetric: `H[i][j] = H[j][i]`
153    ///
154    /// # Example
155    /// ```
156    /// use oxiz_math::polynomial::Polynomial;
157    /// // f(x,y) = x² + xy + y²
158    /// let f = Polynomial::from_coeffs_int(&[
159    ///     (1, &[(0, 2)]),          // x²
160    ///     (1, &[(0, 1), (1, 1)]), // xy
161    ///     (1, &[(1, 2)]),          // y²
162    /// ]);
163    ///
164    /// let hessian = f.hessian();
165    /// // H = [[2, 1],
166    /// //      [1, 2]]
167    /// assert_eq!(hessian.len(), 2);
168    /// assert_eq!(hessian[0].len(), 2);
169    /// ```
170    pub fn hessian(&self) -> Vec<Vec<Polynomial>> {
171        let vars = self.vars();
172        if vars.is_empty() {
173            return vec![];
174        }
175
176        // Same reasoning as `gradient` above: `vars` is confirmed
177        // non-empty by the guard above, so `unwrap_or` never triggers.
178        let max_var = *vars.iter().max().unwrap_or(&0);
179        let n = (max_var + 1) as usize;
180
181        let mut hessian = vec![vec![Polynomial::zero(); n]; n];
182
183        // Compute all second-order partial derivatives
184        for i in 0..=max_var {
185            for j in 0..=max_var {
186                // ∂²f/(∂xᵢ∂xⱼ) = ∂/∂xⱼ(∂f/∂xᵢ)
187                let first_deriv = self.derivative(i);
188                let second_deriv = first_deriv.derivative(j);
189                hessian[i as usize][j as usize] = second_deriv;
190            }
191        }
192
193        hessian
194    }
195
196    /// Computes the Jacobian matrix for a vector of polynomials.
197    ///
198    /// For a vector of polynomials f = (f₁, f₂, ..., fₘ) each depending on variables
199    /// x = (x₁, x₂, ..., xₙ), the Jacobian is the m×n matrix:
200    /// `J[i,j] = ∂fᵢ/∂xⱼ`
201    ///
202    /// # Arguments
203    /// * `polys` - Vector of polynomials representing the function components
204    ///
205    /// # Returns
206    /// A matrix where each row i contains the gradient of polynomial i
207    ///
208    /// # Example
209    /// ```
210    /// use oxiz_math::polynomial::Polynomial;
211    /// // f₁(x,y) = x² + y
212    /// // f₂(x,y) = x + y²
213    /// let f1 = Polynomial::from_coeffs_int(&[(1, &[(0, 2)]), (1, &[(1, 1)])]);
214    /// let f2 = Polynomial::from_coeffs_int(&[(1, &[(0, 1)]), (1, &[(1, 2)])]);
215    ///
216    /// let jacobian = Polynomial::jacobian(&[f1, f2]);
217    /// // J = [[2x, 1 ],
218    /// //      [1,  2y]]
219    /// assert_eq!(jacobian.len(), 2); // 2 functions
220    /// ```
221    pub fn jacobian(polys: &[Polynomial]) -> Vec<Vec<Polynomial>> {
222        if polys.is_empty() {
223            return vec![];
224        }
225
226        // Find the maximum variable across all polynomials
227        let max_var = polys.iter().flat_map(|p| p.vars()).max().unwrap_or(0);
228
229        let n_vars = (max_var + 1) as usize;
230        let mut jacobian = Vec::with_capacity(polys.len());
231
232        for poly in polys {
233            let mut row = Vec::with_capacity(n_vars);
234            for var in 0..=max_var {
235                row.push(poly.derivative(var));
236            }
237            jacobian.push(row);
238        }
239
240        jacobian
241    }
242
243    /// Compute the indefinite integral (antiderivative) of the polynomial with respect to a variable.
244    ///
245    /// For a polynomial p(x), returns ∫p(x)dx. The constant of integration is implicitly zero.
246    ///
247    /// # Arguments
248    /// * `var` - The variable to integrate with respect to
249    ///
250    /// # Examples
251    /// ```
252    /// use oxiz_math::polynomial::Polynomial;
253    /// use num_bigint::BigInt;
254    /// use num_rational::BigRational;
255    ///
256    /// // f(x) = 3x^2
257    /// let f = Polynomial::from_coeffs_int(&[(3, &[(0, 2)])]);
258    ///
259    /// // ∫f(x)dx = x^3
260    /// let integral = f.integrate(0);
261    ///
262    /// // Verify: derivative of integral should be original
263    /// let derivative = integral.derivative(0);
264    /// assert_eq!(derivative, f);
265    /// ```
266    pub fn integrate(&self, var: Var) -> Polynomial {
267        let terms: Vec<Term> = self
268            .terms
269            .iter()
270            .map(|t| {
271                let d = t.monomial.degree(var);
272                let new_power = d + 1;
273
274                // Divide coefficient by (power + 1)
275                let new_coeff = &t.coeff / BigRational::from_integer(BigInt::from(new_power));
276
277                // Increment the power of var
278                let new_powers: Vec<(Var, u32)> = if d == 0 {
279                    // Constant term: add var^1
280                    let mut powers = t
281                        .monomial
282                        .vars()
283                        .iter()
284                        .map(|vp| (vp.var, vp.power))
285                        .collect::<Vec<_>>();
286                    powers.push((var, 1));
287                    powers.sort_by_key(|(v, _)| *v);
288                    powers
289                } else {
290                    // Variable already exists: increment power
291                    t.monomial
292                        .vars()
293                        .iter()
294                        .map(|vp| {
295                            if vp.var == var {
296                                (vp.var, vp.power + 1)
297                            } else {
298                                (vp.var, vp.power)
299                            }
300                        })
301                        .collect()
302                };
303
304                let new_mon = Monomial::from_powers(new_powers);
305                Term::new(new_coeff, new_mon)
306            })
307            .collect();
308        Polynomial::from_terms(terms, self.order)
309    }
310
311    /// Compute the definite integral of a univariate polynomial over an interval (a, b).
312    ///
313    /// For a univariate polynomial p(x), returns ∫ₐᵇ p(x)dx = F(b) - F(a)
314    /// where F is the antiderivative of p.
315    ///
316    /// # Arguments
317    /// * `var` - The variable to integrate with respect to
318    /// * `lower` - Lower bound of integration
319    /// * `upper` - Upper bound of integration
320    ///
321    /// # Returns
322    /// The definite integral value, or None if the polynomial is not univariate in the given variable
323    ///
324    /// # Examples
325    /// ```
326    /// use oxiz_math::polynomial::Polynomial;
327    /// use num_bigint::BigInt;
328    /// use num_rational::BigRational;
329    ///
330    /// // ∫[0,2] x^2 dx = [x^3/3] from 0 to 2 = 8/3
331    /// let f = Polynomial::from_coeffs_int(&[(1, &[(0, 2)])]);
332    /// let result = f.definite_integral(0, &BigRational::from_integer(BigInt::from(0)),
333    ///                                     &BigRational::from_integer(BigInt::from(2)));
334    /// assert_eq!(result, Some(BigRational::new(BigInt::from(8), BigInt::from(3))));
335    /// ```
336    pub fn definite_integral(
337        &self,
338        var: Var,
339        lower: &BigRational,
340        upper: &BigRational,
341    ) -> Option<BigRational> {
342        // Get the antiderivative
343        let antideriv = self.integrate(var);
344
345        // Evaluate at upper and lower bounds
346        let mut upper_assignment = crate::prelude::FxHashMap::default();
347        upper_assignment.insert(var, upper.clone());
348        let upper_val = antideriv.eval(&upper_assignment);
349
350        let mut lower_assignment = crate::prelude::FxHashMap::default();
351        lower_assignment.insert(var, lower.clone());
352        let lower_val = antideriv.eval(&lower_assignment);
353
354        // Return F(b) - F(a)
355        Some(upper_val - lower_val)
356    }
357
358    /// Find critical points of a univariate polynomial by solving f'(x) = 0.
359    ///
360    /// Critical points are values where the derivative equals zero, which correspond
361    /// to local maxima, minima, or saddle points.
362    ///
363    /// # Arguments
364    /// * `var` - The variable to find critical points for
365    ///
366    /// # Returns
367    /// A vector of isolating intervals containing the critical points. Each interval
368    /// contains exactly one root of the derivative.
369    ///
370    /// # Examples
371    /// ```
372    /// use oxiz_math::polynomial::Polynomial;
373    ///
374    /// // f(x) = x^3 - 3x = x(x^2 - 3)
375    /// // f'(x) = 3x^2 - 3 = 3(x^2 - 1) = 3(x-1)(x+1)
376    /// // Critical points at x = -1 and x = 1
377    /// let f = Polynomial::from_coeffs_int(&[
378    ///     (1, &[(0, 3)]),  // x^3
379    ///     (-3, &[(0, 1)]), // -3x
380    /// ]);
381    ///
382    /// let critical_points = f.find_critical_points(0);
383    /// assert_eq!(critical_points.len(), 2); // Two critical points
384    /// ```
385    pub fn find_critical_points(&self, var: Var) -> Vec<(BigRational, BigRational)> {
386        // Compute the derivative
387        let deriv = self.derivative(var);
388
389        // Find roots of the derivative (where f'(x) = 0)
390        deriv.isolate_roots(var)
391    }
392
393    /// Numerically integrate using the trapezoidal rule.
394    ///
395    /// Approximates ∫ₐᵇ f(x)dx using the trapezoidal rule with n subintervals.
396    /// The trapezoidal rule approximates the integral by summing the areas of trapezoids.
397    ///
398    /// # Arguments
399    /// * `var` - The variable to integrate with respect to
400    /// * `lower` - Lower bound of integration
401    /// * `upper` - Upper bound of integration
402    /// * `n` - Number of subintervals (more subintervals = higher accuracy)
403    ///
404    /// # Examples
405    /// ```
406    /// use oxiz_math::polynomial::Polynomial;
407    /// use num_bigint::BigInt;
408    /// use num_rational::BigRational;
409    ///
410    /// // ∫[0,1] x^2 dx = 1/3
411    /// let f = Polynomial::from_coeffs_int(&[(1, &[(0, 2)])]);
412    /// let approx = f.trapezoidal_rule(0,
413    ///     &BigRational::from_integer(BigInt::from(0)),
414    ///     &BigRational::from_integer(BigInt::from(1)),
415    ///     100);
416    ///
417    /// let exact = BigRational::new(BigInt::from(1), BigInt::from(3));
418    /// // With 100 intervals, approximation should be very close
419    /// ```
420    pub fn trapezoidal_rule(
421        &self,
422        var: Var,
423        lower: &BigRational,
424        upper: &BigRational,
425        n: u32,
426    ) -> BigRational {
427        if n == 0 {
428            return BigRational::zero();
429        }
430
431        // Step size h = (b - a) / n
432        let h = (upper - lower) / BigRational::from_integer(BigInt::from(n));
433
434        let mut sum = BigRational::zero();
435
436        // First and last terms: f(a)/2 + f(b)/2
437        let mut assignment = crate::prelude::FxHashMap::default();
438        assignment.insert(var, lower.clone());
439        sum += &self.eval(&assignment) / BigRational::from_integer(BigInt::from(2));
440
441        assignment.insert(var, upper.clone());
442        sum += &self.eval(&assignment) / BigRational::from_integer(BigInt::from(2));
443
444        // Middle terms: sum of f(x_i) for i = 1 to n-1
445        for i in 1..n {
446            let x_i = lower + &h * BigRational::from_integer(BigInt::from(i));
447            assignment.insert(var, x_i);
448            sum += &self.eval(&assignment);
449        }
450
451        // Multiply by step size
452        sum * h
453    }
454
455    /// Numerically integrate using Simpson's rule.
456    ///
457    /// Approximates ∫ₐᵇ f(x)dx using Simpson's rule with n subintervals (n must be even).
458    /// Simpson's rule uses parabolic approximation and is generally more accurate than
459    /// the trapezoidal rule for smooth functions.
460    ///
461    /// # Arguments
462    /// * `var` - The variable to integrate with respect to
463    /// * `lower` - Lower bound of integration
464    /// * `upper` - Upper bound of integration
465    /// * `n` - Number of subintervals (must be even for Simpson's rule)
466    ///
467    /// # Examples
468    /// ```
469    /// use oxiz_math::polynomial::Polynomial;
470    /// use num_bigint::BigInt;
471    /// use num_rational::BigRational;
472    ///
473    /// // ∫[0,1] x^2 dx = 1/3
474    /// let f = Polynomial::from_coeffs_int(&[(1, &[(0, 2)])]);
475    /// let approx = f.simpsons_rule(0,
476    ///     &BigRational::from_integer(BigInt::from(0)),
477    ///     &BigRational::from_integer(BigInt::from(1)),
478    ///     100);
479    ///
480    /// let exact = BigRational::new(BigInt::from(1), BigInt::from(3));
481    /// // Simpson's rule should give very accurate results for polynomials
482    /// ```
483    pub fn simpsons_rule(
484        &self,
485        var: Var,
486        lower: &BigRational,
487        upper: &BigRational,
488        n: u32,
489    ) -> BigRational {
490        if n == 0 {
491            return BigRational::zero();
492        }
493
494        // Ensure n is even for Simpson's rule
495        let n = if n % 2 == 1 { n + 1 } else { n };
496
497        // Step size h = (b - a) / n
498        let h = (upper - lower) / BigRational::from_integer(BigInt::from(n));
499
500        let mut sum = BigRational::zero();
501        let mut assignment = crate::prelude::FxHashMap::default();
502
503        // First and last terms: f(a) + f(b)
504        assignment.insert(var, lower.clone());
505        sum += &self.eval(&assignment);
506
507        assignment.insert(var, upper.clone());
508        sum += &self.eval(&assignment);
509
510        // Odd-indexed terms (multiplied by 4): i = 1, 3, 5, ..., n-1
511        for i in (1..n).step_by(2) {
512            let x_i = lower + &h * BigRational::from_integer(BigInt::from(i));
513            assignment.insert(var, x_i);
514            sum += BigRational::from_integer(BigInt::from(4)) * &self.eval(&assignment);
515        }
516
517        // Even-indexed terms (multiplied by 2): i = 2, 4, 6, ..., n-2
518        for i in (2..n).step_by(2) {
519            let x_i = lower + &h * BigRational::from_integer(BigInt::from(i));
520            assignment.insert(var, x_i);
521            sum += BigRational::from_integer(BigInt::from(2)) * &self.eval(&assignment);
522        }
523
524        // Multiply by h/3
525        sum * h / BigRational::from_integer(BigInt::from(3))
526    }
527
528    /// Evaluate the polynomial at a point (substituting a value for a variable).
529    pub fn eval_at(&self, var: Var, value: &BigRational) -> Polynomial {
530        let terms: Vec<Term> = self
531            .terms
532            .iter()
533            .map(|t| {
534                let d = t.monomial.degree(var);
535                if d == 0 {
536                    t.clone()
537                } else {
538                    let new_coeff = &t.coeff * value.pow(d as i32);
539                    let new_mon = t
540                        .monomial
541                        .div(&Monomial::from_var_power(var, d))
542                        .unwrap_or_else(Monomial::unit);
543                    Term::new(new_coeff, new_mon)
544                }
545            })
546            .collect();
547        Polynomial::from_terms(terms, self.order)
548    }
549
550    /// Evaluate the polynomial completely (all variables assigned).
551    ///
552    /// (Numeric substitution of `assignment` into the polynomial's terms —
553    /// no code/expression execution of any kind is involved.)
554    ///
555    /// # Panics
556    ///
557    /// Panics if `assignment` does not cover every variable occurring in
558    /// `self`. Callers that cannot guarantee a complete assignment ahead of
559    /// time (e.g. because it was built incrementally, or comes from
560    /// untrusted/partial input) should use [`Self::try_eval`] instead, which
561    /// reports the same condition as `None` rather than aborting.
562    pub fn eval(&self, assignment: &FxHashMap<Var, BigRational>) -> BigRational {
563        match self.try_eval(assignment) {
564            Some(v) => v,
565            None => panic!(
566                "Polynomial::eval: assignment does not cover every variable in the polynomial \
567                 (use try_eval for a non-panicking partial-assignment check)"
568            ),
569        }
570    }
571
572    /// Evaluate the polynomial completely (all variables assigned),
573    /// returning `None` instead of panicking if `assignment` is missing a
574    /// variable that occurs in `self`.
575    pub fn try_eval(&self, assignment: &FxHashMap<Var, BigRational>) -> Option<BigRational> {
576        let mut result = BigRational::zero();
577
578        for term in &self.terms {
579            let mut val = term.coeff.clone();
580            for vp in term.monomial.vars() {
581                let v = assignment.get(&vp.var)?;
582                val *= v.pow(vp.power as i32);
583            }
584            result += val;
585        }
586
587        Some(result)
588    }
589
590    /// Evaluate a univariate polynomial using Horner's method.
591    ///
592    /// Horner's method evaluates a polynomial p(x) = a_n x^n + ... + a_1 x + a_0
593    /// as (((...((a_n) x + a_{n-1}) x + ...) x + a_1) x + a_0), which requires
594    /// only n multiplications instead of n(n+1)/2 for the naive method.
595    ///
596    /// This method is more efficient than eval() for univariate polynomials.
597    ///
598    /// # Arguments
599    /// * `var` - The variable to evaluate
600    /// * `value` - The value to substitute for the variable
601    ///
602    /// # Returns
603    /// The evaluated value
604    ///
605    /// # Panics
606    ///
607    /// Panics if the polynomial is genuinely multivariate in `var` — i.e. it
608    /// still contains a variable other than `var` after substituting `value`
609    /// for `var`, so it cannot be reduced to a scalar. Callers that cannot
610    /// guarantee univariate-in-`var` input (e.g. because it comes from
611    /// untrusted or incrementally built terms) should use
612    /// [`Self::try_eval_horner`] instead, which reports the same condition as
613    /// `None` rather than aborting.
614    pub fn eval_horner(&self, var: Var, value: &BigRational) -> BigRational {
615        match self.try_eval_horner(var, value) {
616            Some(v) => v,
617            None => panic!(
618                "Polynomial::eval_horner: polynomial is not univariate in variable x{var} \
619                 (use try_eval_horner for a non-panicking check)"
620            ),
621        }
622    }
623
624    /// Evaluate a univariate polynomial using Horner's method, returning `None`
625    /// instead of panicking when the polynomial is genuinely multivariate in
626    /// `var` (i.e. it still contains another variable after substituting
627    /// `value` for `var`, so no scalar result exists).
628    ///
629    /// This is the non-panicking counterpart of [`Self::eval_horner`]; see its
630    /// documentation for the numeric contract.
631    pub fn try_eval_horner(&self, var: Var, value: &BigRational) -> Option<BigRational> {
632        if self.is_zero() {
633            return Some(BigRational::zero());
634        }
635
636        // For polynomials that are not univariate *in `var`*, fall back to
637        // full substitution: this succeeds only when the substitution leaves a
638        // constant (every other variable, if any, cancelled out).
639        if !self.is_univariate() || self.max_var() != var {
640            let result = self.eval_at(var, value);
641            if result.is_constant() {
642                return Some(result.constant_value());
643            }
644            return None;
645        }
646
647        let deg = self.degree(var);
648        if deg == 0 {
649            return Some(self.constant_value());
650        }
651
652        // Collect coefficients in descending order of degree
653        let mut coeffs = vec![BigRational::zero(); (deg + 1) as usize];
654        for k in 0..=deg {
655            coeffs[k as usize] = self.univ_coeff(var, k);
656        }
657
658        // Apply Horner's method: start from highest degree
659        let mut result = coeffs[deg as usize].clone();
660        for k in (0..deg).rev() {
661            result = &result * value + &coeffs[k as usize];
662        }
663
664        Some(result)
665    }
666
667    /// Substitute a polynomial for a variable.
668    pub fn substitute(&self, var: Var, replacement: &Polynomial) -> Polynomial {
669        let mut result = Polynomial::zero();
670
671        for term in &self.terms {
672            let d = term.monomial.degree(var);
673            if d == 0 {
674                result = Polynomial::add(
675                    &result,
676                    &Polynomial::from_terms(vec![term.clone()], self.order),
677                );
678            } else {
679                let remainder = term
680                    .monomial
681                    .div(&Monomial::from_var_power(var, d))
682                    .unwrap_or_else(Monomial::unit);
683                let coeff_poly = Polynomial::from_terms(
684                    vec![Term::new(term.coeff.clone(), remainder)],
685                    self.order,
686                );
687                let rep_pow = replacement.pow(d);
688                result = Polynomial::add(&result, &Polynomial::mul(&coeff_poly, &rep_pow));
689            }
690        }
691
692        result
693    }
694
695    /// Integer content: GCD of all coefficients (as integers).
696    /// Assumes all coefficients are integers.
697    pub fn integer_content(&self) -> BigInt {
698        if self.terms.is_empty() {
699            return BigInt::one();
700        }
701
702        let mut gcd: Option<BigInt> = None;
703        for term in &self.terms {
704            let num = term.coeff.numer().clone();
705            gcd = Some(match gcd {
706                None => num.abs(),
707                Some(g) => gcd_bigint(g, num.abs()),
708            });
709        }
710        gcd.unwrap_or_else(BigInt::one)
711    }
712
713    /// Make the polynomial primitive (divide by integer content).
714    pub fn primitive(&self) -> Polynomial {
715        let content = self.integer_content();
716        if content.is_one() {
717            return self.clone();
718        }
719        let c = BigRational::from_integer(content);
720        self.scale(&(BigRational::one() / c))
721    }
722
723    /// GCD of two polynomials (univariate, using Euclidean algorithm).
724    pub fn gcd_univariate(&self, other: &Polynomial) -> Polynomial {
725        if self.is_zero() {
726            return other.primitive();
727        }
728        if other.is_zero() {
729            return self.primitive();
730        }
731
732        let var = self.max_var().max(other.max_var());
733        if var == NULL_VAR {
734            // Both are constants, GCD is 1
735            return Polynomial::one();
736        }
737
738        let mut a = self.primitive();
739        let mut b = other.primitive();
740
741        // Ensure deg(a) >= deg(b)
742        if a.degree(var) < b.degree(var) {
743            core::mem::swap(&mut a, &mut b);
744        }
745
746        // Limit iterations for safety
747        let mut iter_count = 0;
748        let max_iters = a.degree(var) as usize + b.degree(var) as usize + 10;
749
750        while !b.is_zero() && iter_count < max_iters {
751            iter_count += 1;
752            let r = a.pseudo_remainder(&b, var);
753            a = b;
754            b = if r.is_zero() { r } else { r.primitive() };
755        }
756
757        a.primitive()
758    }
759
760    /// Pseudo-remainder for univariate polynomials.
761    /// Returns r such that lc(b)^d * a = q * b + r for some q,
762    /// where d = max(deg(a) - deg(b) + 1, 0).
763    ///
764    /// Division by the zero polynomial is mathematically undefined; rather
765    /// than panicking on a crafted degenerate `divisor` (a soundness/DoS
766    /// concern for any caller reachable from untrusted input), this treats
767    /// it as "no reduction possible" and returns `self` unchanged, mirroring
768    /// `Self::exact_remainder_univariate`'s convention for the same case.
769    pub fn pseudo_remainder(&self, divisor: &Polynomial, var: Var) -> Polynomial {
770        if divisor.is_zero() {
771            return self.clone();
772        }
773
774        if self.is_zero() {
775            return Polynomial::zero();
776        }
777
778        let deg_a = self.degree(var);
779        let deg_b = divisor.degree(var);
780
781        if deg_a < deg_b {
782            return self.clone();
783        }
784
785        let lc_b = divisor.univ_coeff(var, deg_b);
786        let mut r = self.clone();
787
788        // Limit iterations
789        let max_iters = (deg_a - deg_b + 2) as usize;
790        let mut iters = 0;
791
792        while !r.is_zero() && r.degree(var) >= deg_b && iters < max_iters {
793            iters += 1;
794            let deg_r = r.degree(var);
795            let lc_r = r.univ_coeff(var, deg_r);
796            let shift = deg_r - deg_b;
797
798            // r = lc_b * r - lc_r * x^shift * divisor
799            r = r.scale(&lc_b);
800            let subtractor = divisor
801                .scale(&lc_r)
802                .mul_monomial(&Monomial::from_var_power(var, shift));
803            r = Polynomial::sub(&r, &subtractor);
804        }
805
806        r
807    }
808
809    /// Pseudo-division for univariate polynomials.
810    /// Returns (quotient, remainder) such that lc(b)^d * a = q * b + r
811    /// where d = deg(a) - deg(b) + 1.
812    ///
813    /// Division by the zero polynomial is mathematically undefined; rather
814    /// than panicking on a crafted degenerate `divisor`, this returns
815    /// `(0, self)` -- "no division performed" -- matching the convention
816    /// used for the `deg(a) < deg(b)` case below.
817    pub fn pseudo_div_univariate(&self, divisor: &Polynomial) -> (Polynomial, Polynomial) {
818        if divisor.is_zero() {
819            return (Polynomial::zero(), self.clone());
820        }
821
822        if self.is_zero() {
823            return (Polynomial::zero(), Polynomial::zero());
824        }
825
826        let var = self.max_var().max(divisor.max_var());
827        if var == NULL_VAR {
828            // Both are constants
829            return (Polynomial::zero(), self.clone());
830        }
831
832        let deg_a = self.degree(var);
833        let deg_b = divisor.degree(var);
834
835        if deg_a < deg_b {
836            return (Polynomial::zero(), self.clone());
837        }
838
839        let lc_b = divisor.univ_coeff(var, deg_b);
840        let mut q = Polynomial::zero();
841        let mut r = self.clone();
842
843        // Limit iterations
844        let max_iters = (deg_a - deg_b + 2) as usize;
845        let mut iters = 0;
846
847        while !r.is_zero() && r.degree(var) >= deg_b && iters < max_iters {
848            iters += 1;
849            let deg_r = r.degree(var);
850            let lc_r = r.univ_coeff(var, deg_r);
851            let shift = deg_r - deg_b;
852
853            let term = Polynomial::from_terms(
854                vec![Term::new(
855                    lc_r.clone(),
856                    Monomial::from_var_power(var, shift),
857                )],
858                self.order,
859            );
860
861            q = q.scale(&lc_b);
862            q = Polynomial::add(&q, &term);
863
864            r = r.scale(&lc_b);
865            let subtractor = Polynomial::mul(divisor, &term);
866            r = Polynomial::sub(&r, &subtractor);
867        }
868
869        (q, r)
870    }
871
872    /// Compute the subresultant polynomial remainder sequence (PRS).
873    ///
874    /// The subresultant PRS is a more efficient variant of the pseudo-remainder sequence
875    /// that avoids coefficient explosion through careful normalization. It's useful for
876    /// GCD computation and other polynomial algorithms.
877    ///
878    /// Returns a sequence of polynomials [p0, p1, ..., pk] where:
879    /// - p0 = self
880    /// - p1 = other
881    /// - Each subsequent polynomial is derived using the subresultant algorithm
882    ///
883    /// Reference: "Algorithms for Computer Algebra" by Geddes, Czapor, Labahn
884    pub fn subresultant_prs(&self, other: &Polynomial, var: Var) -> Vec<Polynomial> {
885        if self.is_zero() || other.is_zero() {
886            return vec![];
887        }
888
889        let mut prs = Vec::new();
890        let mut a = self.clone();
891        let mut b = other.clone();
892
893        // Ensure deg(a) >= deg(b)
894        if a.degree(var) < b.degree(var) {
895            core::mem::swap(&mut a, &mut b);
896        }
897
898        prs.push(a.clone());
899        prs.push(b.clone());
900
901        let mut g = Polynomial::one();
902        let mut h = Polynomial::one();
903
904        let max_iters = a.degree(var) as usize + b.degree(var) as usize + 10;
905        let mut iter_count = 0;
906
907        while !b.is_zero() && iter_count < max_iters {
908            iter_count += 1;
909
910            let delta = a.degree(var) as i32 - b.degree(var) as i32;
911            if delta < 0 {
912                break;
913            }
914
915            // Compute pseudo-remainder
916            let prem = a.pseudo_remainder(&b, var);
917
918            if prem.is_zero() {
919                break;
920            }
921
922            // Subresultant normalization to prevent coefficient explosion
923            let normalized = if delta == 0 {
924                // No adjustment needed for delta = 0
925                prem.scale(&(BigRational::one() / &h.constant_value()))
926            } else {
927                // For delta > 0, divide by g^delta * h
928                let g_pow = g.constant_value().pow(delta);
929                let divisor = &g_pow * &h.constant_value();
930                prem.scale(&(BigRational::one() / divisor))
931            };
932
933            // Update g and h for next iteration
934            let lc_b = b.leading_coeff_wrt(var);
935            g = lc_b;
936
937            h = if delta == 0 {
938                Polynomial::one()
939            } else {
940                let g_val = g.constant_value();
941                let h_val = h.constant_value();
942                let new_h = g_val.pow(delta) / h_val.pow(delta - 1);
943                Polynomial::constant(new_h)
944            };
945
946            // Move to next iteration
947            a = b;
948            b = normalized.primitive(); // Make primitive to keep coefficients manageable
949            prs.push(b.clone());
950        }
951
952        prs
953    }
954
955    /// Resultant of two polynomials with respect to a variable.
956    ///
957    /// The resultant `Res_var(p, q)` eliminates `var` from `{p = 0, q = 0}`,
958    /// returning a polynomial in the remaining variables that vanishes exactly
959    /// when `p` and `q` share a common root (over an algebraic closure) or
960    /// their leading coefficients in `var` both vanish.
961    ///
962    /// * **Univariate inputs** (both operands contain only `var`): computed via
963    ///   the exact subresultant PRS with rational scalar normalization.
964    /// * **Multivariate inputs** (either operand contains another variable):
965    ///   computed *exactly* as the determinant of the `(deg_p + deg_q)`-square
966    ///   Sylvester matrix over the polynomial coefficient ring, evaluated
967    ///   division-free via the Samuelson–Berkowitz algorithm (see
968    ///   `sylvester_resultant`). This replaces the former `primitive()`-based
969    ///   approximation, which could return a mathematically wrong (non-zero)
970    ///   value for genuinely multivariate operands.
971    ///
972    /// The sign/value convention is the classical
973    /// `Res(p, q) = lc(p)^{deg q} · Π_{p(α)=0} q(α)`; in particular
974    /// `Res(x - a, x - b) = a - b`.
975    pub fn resultant(&self, other: &Polynomial, var: Var) -> Polynomial {
976        if self.is_zero() || other.is_zero() {
977            return Polynomial::zero();
978        }
979
980        let deg_p = self.degree(var);
981        let deg_q = other.degree(var);
982
983        if deg_p == 0 {
984            return self.pow(deg_q);
985        }
986        if deg_q == 0 {
987            return other.pow(deg_p);
988        }
989
990        // Both operands have positive degree in `var`. Compute the resultant
991        // exactly as the Sylvester matrix determinant over the coefficient
992        // ring, evaluated division-free (Berkowitz). This is exact for both
993        // univariate and genuinely multivariate inputs; it replaces the former
994        // subresultant-PRS loop, whose leading-coefficient normalization was
995        // only correct for constant coefficients (a wrong `primitive()`
996        // approximation for multivariate operands) and could return a
997        // non-canonical scalar multiple even in the univariate case.
998        sylvester_resultant(self, other, var, deg_p as usize, deg_q as usize)
999    }
1000
1001    /// Discriminant of a polynomial with respect to a variable.
1002    /// discriminant(p) = resultant(p, dp/dx) / lc(p)
1003    pub fn discriminant(&self, var: Var) -> Polynomial {
1004        let deriv = self.derivative(var);
1005        self.resultant(&deriv, var)
1006    }
1007
1008    /// Check if the polynomial has a positive sign for all variable assignments.
1009    /// This is an incomplete check that only handles obvious cases.
1010    pub fn is_definitely_positive(&self) -> bool {
1011        // All terms with even total degree and positive coefficients
1012        if self.terms.is_empty() {
1013            return false;
1014        }
1015
1016        // Check if all monomials are even powers and coefficients are positive
1017        self.terms
1018            .iter()
1019            .all(|t| t.coeff.is_positive() && t.monomial.vars().iter().all(|vp| vp.power % 2 == 0))
1020    }
1021
1022    /// Check if the polynomial has a negative sign for all variable assignments.
1023    /// This is an incomplete check.
1024    pub fn is_definitely_negative(&self) -> bool {
1025        self.neg().is_definitely_positive()
1026    }
1027
1028    /// Make the polynomial monic (leading coefficient = 1).
1029    pub fn make_monic(&self) -> Polynomial {
1030        if self.is_zero() {
1031            return self.clone();
1032        }
1033        let lc = self.leading_coeff();
1034        if lc.is_one() {
1035            return self.clone();
1036        }
1037        self.scale(&(BigRational::one() / lc))
1038    }
1039
1040    /// Compute the square-free part of a polynomial (removes repeated factors).
1041    pub fn square_free(&self) -> Polynomial {
1042        if self.is_zero() || self.is_constant() {
1043            return self.clone();
1044        }
1045
1046        let var = self.max_var();
1047        if var == NULL_VAR {
1048            return self.clone();
1049        }
1050
1051        // Square-free: p / gcd(p, p')
1052        let deriv = self.derivative(var);
1053        if deriv.is_zero() {
1054            return self.clone();
1055        }
1056
1057        let g = self.gcd_univariate(&deriv);
1058        if g.is_constant() {
1059            self.primitive()
1060        } else {
1061            let (q, r) = self.pseudo_div_univariate(&g);
1062            if r.is_zero() {
1063                q.primitive().square_free()
1064            } else {
1065                self.primitive()
1066            }
1067        }
1068    }
1069
1070    /// Exact univariate polynomial division over the rationals.
1071    ///
1072    /// Returns `(q, r)` with `deg(r) < deg(divisor)` such that
1073    /// `self = q * divisor + r`, using exact rational coefficient division
1074    /// (no leading-coefficient scaling factor, unlike
1075    /// [`pseudo_div_univariate`](Self::pseudo_div_univariate)).
1076    ///
1077    /// This is exact and correct for polynomials that are univariate in
1078    /// `var` (or constant in the other variables); it is used by both
1079    /// `Self::exact_remainder_univariate` (Sturm sequences) and by
1080    /// [`super::gcd::PolynomialGcd`]'s Euclidean-algorithm GCD/LCM.
1081    ///
1082    /// # Multivariate limitation
1083    ///
1084    /// [`Self::univ_coeff`] only recognizes terms with at most one variable,
1085    /// so if `self` or `divisor` contains a term mixing `var` with another
1086    /// variable, that term is treated as contributing `0` to the relevant
1087    /// coefficient rather than being folded into a coefficient polynomial.
1088    /// Callers that need an exact result for genuinely multivariate
1089    /// polynomials should use [`super::gcd_multivariate`] /
1090    /// [`super::gcd_multivariate_advanced`] instead.
1091    pub(crate) fn exact_div_rem_univariate(
1092        &self,
1093        divisor: &Polynomial,
1094        var: Var,
1095    ) -> (Polynomial, Polynomial) {
1096        if divisor.is_zero() {
1097            // Degenerate divisor: nothing to reduce by, remainder is self.
1098            return (Polynomial::zero(), self.clone());
1099        }
1100        if self.is_zero() {
1101            return (Polynomial::zero(), Polynomial::zero());
1102        }
1103
1104        let deg_b = divisor.degree(var);
1105        let lc_b = divisor.univ_coeff(var, deg_b);
1106        if lc_b.is_zero() {
1107            return (Polynomial::zero(), self.clone());
1108        }
1109
1110        let mut q = Polynomial::zero();
1111        let mut r = self.clone();
1112        // Each iteration strictly lowers deg(r) by at least one; bound the loop
1113        // by the initial degree gap plus slack for safety.
1114        let deg_a = self.degree(var);
1115        let max_iters = deg_a.saturating_sub(deg_b) as usize + 2;
1116        let mut iters = 0;
1117
1118        while !r.is_zero() && r.degree(var) >= deg_b && iters < max_iters {
1119            iters += 1;
1120            let deg_r = r.degree(var);
1121            let lc_r = r.univ_coeff(var, deg_r);
1122            let shift = deg_r - deg_b;
1123
1124            // factor = lc_r / lc_b (exact rational)
1125            let factor = lc_r / &lc_b;
1126            let term = Polynomial::constant(factor.clone())
1127                .mul_monomial(&Monomial::from_var_power(var, shift));
1128            q = Polynomial::add(&q, &term);
1129
1130            let subtractor = divisor
1131                .scale(&factor)
1132                .mul_monomial(&Monomial::from_var_power(var, shift));
1133            r = Polynomial::sub(&r, &subtractor);
1134        }
1135
1136        (q, r)
1137    }
1138
1139    /// Exact univariate polynomial remainder over the rationals.
1140    ///
1141    /// Returns `r` with `deg(r) < deg(divisor)` such that
1142    /// `self = q * divisor + r` for some quotient `q`, using exact rational
1143    /// coefficient division (no leading-coefficient scaling factor).
1144    ///
1145    /// Unlike [`pseudo_remainder`](Self::pseudo_remainder), this introduces no
1146    /// `lc(divisor)^k` multiplier, so the sign of the result is exactly the
1147    /// sign of the true remainder. This sign fidelity is essential for Sturm
1148    /// sequences: a spurious negative scale factor (which arises whenever
1149    /// `lc(divisor)` is negative and `k` is odd) would corrupt sign-variation
1150    /// counts and hence real-root counts.
1151    fn exact_remainder_univariate(&self, divisor: &Polynomial, var: Var) -> Polynomial {
1152        self.exact_div_rem_univariate(divisor, var).1
1153    }
1154
1155    /// Compute the Sturm sequence for a univariate polynomial.
1156    /// The Sturm sequence is used for counting real roots in an interval.
1157    ///
1158    /// The chain is built as `p_{i+1} = -rem(p_{i-1}, p_i)` using the *exact*
1159    /// rational remainder. The pseudo-remainder must not be used here: it
1160    /// scales by `lc(p_i)^k`, whose sign flips when the leading coefficient is
1161    /// negative, which would break the Sturm sign invariant and yield wrong
1162    /// root counts (e.g. `-x^2 + 1` on `(-2, 2)` would report 0 roots instead
1163    /// of 2).
1164    pub fn sturm_sequence(&self, var: Var) -> Vec<Polynomial> {
1165        if self.is_zero() || self.degree(var) == 0 {
1166            return vec![self.clone()];
1167        }
1168
1169        let mut seq = Vec::new();
1170        seq.push(self.clone());
1171        seq.push(self.derivative(var));
1172
1173        // Build Sturm sequence: p_i+1 = -rem(p_i-1, p_i)
1174        let max_iterations = self.degree(var) as usize + 5;
1175        let mut iterations = 0;
1176
1177        // `seq` always has >= 2 elements by construction (two pushes above,
1178        // never popped), so `last()` is never `None`; `unwrap_or(false)`
1179        // documents that invariant without an `.expect()` panic path, and
1180        // degrades gracefully (stops the loop) instead of aborting even if
1181        // that invariant were ever violated by a future edit.
1182        while seq.last().map(|p| !p.is_zero()).unwrap_or(false) && iterations < max_iterations {
1183            iterations += 1;
1184            let n = seq.len();
1185            let rem = seq[n - 2].exact_remainder_univariate(&seq[n - 1], var);
1186            if rem.is_zero() {
1187                break;
1188            }
1189            seq.push(rem.neg());
1190        }
1191
1192        seq
1193    }
1194
1195    /// Count the number of real roots in an interval using Sturm's theorem.
1196    /// Returns the number of distinct real roots in (a, b).
1197    pub fn count_roots_in_interval(&self, var: Var, a: &BigRational, b: &BigRational) -> usize {
1198        if self.is_zero() {
1199            return 0;
1200        }
1201
1202        let sturm_seq = self.sturm_sequence(var);
1203        if sturm_seq.is_empty() {
1204            return 0;
1205        }
1206
1207        // Count sign variations at a and b
1208        let var_a = count_sign_variations(&sturm_seq, var, a);
1209        let var_b = count_sign_variations(&sturm_seq, var, b);
1210
1211        // Number of roots = var_a - var_b
1212        var_a.saturating_sub(var_b)
1213    }
1214
1215    /// Compute Cauchy's root bound for a univariate polynomial.
1216    /// Returns B such that all roots have absolute value <= B.
1217    ///
1218    /// Cauchy bound: 1 + max(|a_i| / |a_n|) for i < n
1219    ///
1220    /// This is a simple, conservative bound that's fast to compute.
1221    pub fn cauchy_bound(&self, var: Var) -> BigRational {
1222        cauchy_root_bound(self, var)
1223    }
1224
1225    /// Compute Fujiwara's root bound for a univariate polynomial.
1226    /// Returns B such that all roots have absolute value <= B.
1227    ///
1228    /// Fujiwara bound: 2 * max(|a_i/a_n|^(1/(n-i))) for i < n
1229    ///
1230    /// This bound is generally tighter than Cauchy's bound but more expensive to compute.
1231    ///
1232    /// Reference: Fujiwara, "Über die obere Schranke des absoluten Betrages
1233    /// der Wurzeln einer algebraischen Gleichung" (1916)
1234    pub fn fujiwara_bound(&self, var: Var) -> BigRational {
1235        fujiwara_root_bound(self, var)
1236    }
1237
1238    /// Compute Lagrange's bound for positive roots of a univariate polynomial.
1239    /// Returns B such that all positive roots are <= B.
1240    ///
1241    /// This can provide a tighter bound than general root bounds when analyzing
1242    /// only positive roots, useful for root isolation optimization.
1243    pub fn lagrange_positive_bound(&self, var: Var) -> BigRational {
1244        lagrange_positive_root_bound(self, var)
1245    }
1246
1247    /// Isolate real roots of a univariate polynomial.
1248    /// Returns a list of intervals, each containing exactly one root.
1249    /// Uses Descartes' rule of signs to optimize the search.
1250    pub fn isolate_roots(&self, var: Var) -> Vec<(BigRational, BigRational)> {
1251        if self.is_zero() || self.is_constant() {
1252            return vec![];
1253        }
1254
1255        // Make square-free first
1256        let p = self.square_free();
1257        if p.is_constant() {
1258            return vec![];
1259        }
1260
1261        // Find a bound for all real roots using Cauchy's bound
1262        let bound = cauchy_root_bound(&p, var);
1263
1264        // Use Descartes' rule to check if there are any roots at all
1265        let (_pos_lower, pos_upper) = descartes_positive_roots(&p, var);
1266        let (_neg_lower, neg_upper) = descartes_negative_roots(&p, var);
1267
1268        // Use interval bisection with Sturm's theorem
1269        let mut intervals = Vec::new();
1270        let mut queue = Vec::new();
1271
1272        // `count_roots_in_interval` counts roots in the *open* interval
1273        // (a, b) (standard Sturm's theorem convention). The search below
1274        // seeds (0, bound) for positive roots and (-bound, 0) for negative
1275        // roots, so a root at exactly x=0 — the shared boundary of both
1276        // ranges — falls outside both open intervals and would otherwise be
1277        // silently dropped. Check for it explicitly up front, the same way
1278        // an exact root found at a bisection midpoint is recorded below.
1279        if p.eval_at(var, &BigRational::zero())
1280            .constant_term()
1281            .is_zero()
1282        {
1283            intervals.push((BigRational::zero(), BigRational::zero()));
1284        }
1285
1286        // Only search in positive interval if there might be positive roots
1287        if pos_upper > 0 {
1288            queue.push((BigRational::zero(), bound.clone()));
1289        }
1290
1291        // Only search in negative interval if there might be negative roots
1292        if neg_upper > 0 {
1293            queue.push((-bound, BigRational::zero()));
1294        }
1295
1296        let max_iterations = 1000;
1297        let mut iterations = 0;
1298
1299        while let Some((a, b)) = queue.pop() {
1300            iterations += 1;
1301            if iterations > max_iterations {
1302                break;
1303            }
1304
1305            let num_roots = p.count_roots_in_interval(var, &a, &b);
1306
1307            if num_roots == 0 {
1308                // No roots in this interval
1309                continue;
1310            } else if num_roots == 1 {
1311                // Exactly one root
1312                intervals.push((a, b));
1313            } else {
1314                // Multiple roots, bisect
1315                let mid = (&a + &b) / BigRational::from_integer(BigInt::from(2));
1316
1317                // Check if mid is a root
1318                let val_mid = p.eval_at(var, &mid);
1319                if val_mid.constant_term().is_zero() {
1320                    // Found an exact root
1321                    intervals.push((mid.clone(), mid.clone()));
1322                    // Don't add intervals that would contain this root again
1323                    let left_roots = p.count_roots_in_interval(var, &a, &mid);
1324                    let right_roots = p.count_roots_in_interval(var, &mid, &b);
1325                    if left_roots > 0 {
1326                        queue.push((a, mid.clone()));
1327                    }
1328                    if right_roots > 0 {
1329                        queue.push((mid, b));
1330                    }
1331                } else {
1332                    queue.push((a, mid.clone()));
1333                    queue.push((mid, b));
1334                }
1335            }
1336        }
1337
1338        intervals
1339    }
1340
1341    /// Refine a root approximation using Newton-Raphson iteration.
1342    ///
1343    /// Given an initial approximation and bounds, refines the root using the formula:
1344    /// x_{n+1} = x_n - f(x_n) / f'(x_n)
1345    ///
1346    /// # Arguments
1347    ///
1348    /// * `var` - The variable to solve for
1349    /// * `initial` - Initial approximation of the root
1350    /// * `lower` - Lower bound for the root (for verification)
1351    /// * `upper` - Upper bound for the root (for verification)
1352    /// * `max_iterations` - Maximum number of iterations
1353    /// * `tolerance` - Convergence tolerance (stop when |f(x)| < tolerance)
1354    ///
1355    /// # Returns
1356    ///
1357    /// The refined root approximation, or None if the method fails to converge
1358    /// or if the derivative is zero.
1359    ///
1360    /// # Examples
1361    ///
1362    /// ```
1363    /// use num_bigint::BigInt;
1364    /// use num_rational::BigRational;
1365    /// use oxiz_math::polynomial::Polynomial;
1366    ///
1367    /// // Solve x^2 - 2 = 0 (find sqrt(2) ≈ 1.414...)
1368    /// let p = Polynomial::from_coeffs_int(&[
1369    ///     (1, &[(0, 2)]),  // x^2
1370    ///     (-2, &[]),       // -2
1371    /// ]);
1372    ///
1373    /// let initial = BigRational::new(BigInt::from(3), BigInt::from(2)); // 1.5
1374    /// let lower = BigRational::from_integer(BigInt::from(1));
1375    /// let upper = BigRational::from_integer(BigInt::from(2));
1376    ///
1377    /// let root = p.newton_raphson(0, initial, lower, upper, 10, &BigRational::new(BigInt::from(1), BigInt::from(1000000)));
1378    /// assert!(root.is_some());
1379    /// ```
1380    pub fn newton_raphson(
1381        &self,
1382        var: Var,
1383        initial: BigRational,
1384        lower: BigRational,
1385        upper: BigRational,
1386        max_iterations: usize,
1387        tolerance: &BigRational,
1388    ) -> Option<BigRational> {
1389        use num_traits::{Signed, Zero};
1390
1391        let derivative = self.derivative(var);
1392
1393        let mut x = initial;
1394
1395        for _ in 0..max_iterations {
1396            // Evaluate f(x)
1397            let mut assignment = FxHashMap::default();
1398            assignment.insert(var, x.clone());
1399            let fx = self.eval(&assignment);
1400
1401            // Check if we're close enough
1402            if fx.abs() < *tolerance {
1403                return Some(x);
1404            }
1405
1406            // Evaluate f'(x)
1407            let fpx = derivative.eval(&assignment);
1408
1409            // Check for zero derivative
1410            if fpx.is_zero() {
1411                return None;
1412            }
1413
1414            // Newton-Raphson update: x_new = x - f(x)/f'(x)
1415            let x_new = x - (fx / fpx);
1416
1417            // Verify the new point is within bounds
1418            if x_new < lower || x_new > upper {
1419                // If out of bounds, use bisection fallback
1420                return None;
1421            }
1422
1423            x = x_new;
1424        }
1425
1426        // Return the result even if not fully converged
1427        Some(x)
1428    }
1429
1430    // ── Dense i64 coefficient fast paths (SIMD-accelerated) ─────────────────
1431
1432    /// Extract the dense integer coefficient vector for a univariate polynomial.
1433    ///
1434    /// Returns `Some(coeffs)` where `coeffs[k]` is the coefficient of `var^k`,
1435    /// if and only if every coefficient is an integer that fits in `i64`.
1436    /// Returns `None` for multivariate, non-integer, or out-of-range coefficients.
1437    pub fn as_dense_i64(&self, var: Var) -> Option<Vec<i64>> {
1438        if self.is_zero() {
1439            return Some(vec![0i64]);
1440        }
1441        // Require the polynomial to be univariate *in `var`* specifically:
1442        // at most one variable overall (`is_univariate()`), and that
1443        // variable — if any (a nonzero constant has none, `max_var() ==
1444        // NULL_VAR`) — must be `var` itself. The previous `&&` accepted any
1445        // multivariate polynomial whose *highest-indexed* variable happened
1446        // to equal `var`, silently dropping every other variable's
1447        // contribution when the coefficients below are extracted purely by
1448        // `var`-degree.
1449        if !self.is_univariate() || (self.max_var() != var && self.max_var() != NULL_VAR) {
1450            return None;
1451        }
1452
1453        let deg = self.degree(var) as usize;
1454        let mut coeffs = vec![0i64; deg + 1];
1455
1456        for term in &self.terms {
1457            let d = term.monomial.degree(var) as usize;
1458            if !term.coeff.is_integer() {
1459                return None;
1460            }
1461            let numer = term.coeff.numer();
1462            match i64::try_from(numer.clone()) {
1463                Ok(v) => coeffs[d] = v,
1464                Err(_) => return None,
1465            }
1466        }
1467
1468        Some(coeffs)
1469    }
1470
1471    /// Add two univariate polynomials using the SIMD-accelerated i64 fast path.
1472    ///
1473    /// Falls back to the generic [`Polynomial::add`] when coefficients do not fit
1474    /// in `i64` or the polynomials are not univariate in the same variable.
1475    pub fn add_fast_i64(&self, other: &Polynomial, var: Var) -> Polynomial {
1476        let a_opt = self.as_dense_i64(var);
1477        let b_opt = other.as_dense_i64(var);
1478
1479        match (a_opt, b_opt) {
1480            (Some(a), Some(b)) => {
1481                // poly_add_coeffs pads the shorter slice to max(a.len(), b.len())
1482                let out = poly_add_coeffs(&a, &b);
1483                Polynomial::from_dense_i64(&out, var, self.order)
1484            }
1485            _ => Polynomial::add(self, other),
1486        }
1487    }
1488
1489    /// Scale a univariate polynomial by an integer scalar using the SIMD fast path.
1490    ///
1491    /// Falls back to generic scalar-multiply when coefficients do not fit in `i64`.
1492    pub fn scale_fast_i64(&self, scalar: i64, var: Var) -> Polynomial {
1493        match self.as_dense_i64(var) {
1494            Some(coeffs) => {
1495                let scaled = poly_mul_scalar(&coeffs, scalar);
1496                Polynomial::from_dense_i64(&scaled, var, self.order)
1497            }
1498            None => {
1499                let s = BigRational::from_integer(BigInt::from(scalar));
1500                let scaled_terms: Vec<Term> = self
1501                    .terms
1502                    .iter()
1503                    .map(|t| Term::new(&t.coeff * &s, t.monomial.clone()))
1504                    .collect();
1505                Polynomial::from_terms(scaled_terms, self.order)
1506            }
1507        }
1508    }
1509
1510    /// Compute the i64 dot product between two same-degree dense coefficient arrays.
1511    ///
1512    /// Returns `Some(value)` when both polynomials are expressible as dense univariate
1513    /// `i64` coefficient arrays of equal length, `None` otherwise.
1514    pub fn dot_product_i64(&self, other: &Polynomial, var: Var) -> Option<i64> {
1515        let a = self.as_dense_i64(var)?;
1516        let b = other.as_dense_i64(var)?;
1517        if a.len() != b.len() {
1518            return None;
1519        }
1520        poly_dot_product(&a, &b).ok()
1521    }
1522
1523    /// Reconstruct a [`Polynomial`] from a dense `i64` coefficient array.
1524    ///
1525    /// `coeffs[k]` is the coefficient of `var^k`.  Zero coefficients are omitted.
1526    fn from_dense_i64(coeffs: &[i64], var: Var, order: super::types::MonomialOrder) -> Polynomial {
1527        let terms: Vec<Term> = coeffs
1528            .iter()
1529            .enumerate()
1530            .filter(|(_, c)| **c != 0)
1531            .map(|(k, c)| {
1532                let coeff = BigRational::from_integer(BigInt::from(*c));
1533                let mon = if k == 0 {
1534                    Monomial::unit()
1535                } else {
1536                    Monomial::from_var_power(var, k as u32)
1537                };
1538                Term::new(coeff, mon)
1539            })
1540            .collect();
1541        Polynomial::from_terms(terms, order)
1542    }
1543}
1544
1545// ── Multivariate resultant via Sylvester matrix + Berkowitz determinant ─────
1546
1547/// Exact resultant `Res_var(p, q)` for polynomials whose coefficients with
1548/// respect to `var` may themselves be polynomials in other variables.
1549///
1550/// Builds the `(deg_p + deg_q)`-square Sylvester matrix in the classical
1551/// high-to-low coefficient layout (so the value/sign matches the standard
1552/// resultant `Res(p, q) = lc(p)^{deg q} · Π q(αᵢ)`), then evaluates its
1553/// determinant over the polynomial coefficient ring **division-free** via the
1554/// Samuelson–Berkowitz algorithm. Because Berkowitz uses only ring `+`, `-`,
1555/// `*` (never division), the result is exact for genuinely multivariate
1556/// inputs, where exact polynomial division is not generally available.
1557///
1558/// `deg_p` / `deg_q` are the degrees of `p` / `q` in `var` and must both be
1559/// `>= 1` (the constant-in-`var` cases are handled by the caller).
1560///
1561/// Reference: Z3's `math/polynomial/polynomial.cpp` resultant, computed here
1562/// division-free rather than via pseudo-remainder chains.
1563fn sylvester_resultant(
1564    p: &Polynomial,
1565    q: &Polynomial,
1566    var: Var,
1567    deg_p: usize,
1568    deg_q: usize,
1569) -> Polynomial {
1570    let size = deg_p + deg_q;
1571
1572    // High-to-low coefficient rows: `p_coeff[i]` is the coefficient of
1573    // `var^(deg_p - i)` in `p` (index 0 = leading coefficient).
1574    let p_coeff: Vec<Polynomial> = (0..=deg_p)
1575        .map(|i| p.coeff(var, (deg_p - i) as u32))
1576        .collect();
1577    let q_coeff: Vec<Polynomial> = (0..=deg_q)
1578        .map(|i| q.coeff(var, (deg_q - i) as u32))
1579        .collect();
1580
1581    let mut mat: Vec<Vec<Polynomial>> = (0..size)
1582        .map(|_| (0..size).map(|_| Polynomial::zero()).collect())
1583        .collect();
1584
1585    // First `deg_q` rows: shifted copies of `p`'s coefficients.
1586    for (r, row) in mat.iter_mut().take(deg_q).enumerate() {
1587        for (i, c) in p_coeff.iter().enumerate() {
1588            row[r + i] = c.clone();
1589        }
1590    }
1591    // Last `deg_p` rows: shifted copies of `q`'s coefficients.
1592    for (r, row) in mat.iter_mut().skip(deg_q).take(deg_p).enumerate() {
1593        for (i, c) in q_coeff.iter().enumerate() {
1594            row[r + i] = c.clone();
1595        }
1596    }
1597
1598    berkowitz_determinant(&mat)
1599}
1600
1601/// Determinant of a square matrix over the (commutative) polynomial ring,
1602/// computed division-free via the Samuelson–Berkowitz algorithm.
1603///
1604/// Returns the exact determinant using only ring `+`, `-`, `*`; it never
1605/// divides, so it is well-defined over the multivariate polynomial ring (an
1606/// integral domain but not a field). Singular matrices yield the zero
1607/// polynomial. `mat` is read but not mutated.
1608///
1609/// The algorithm accumulates the characteristic polynomial of each leading
1610/// principal submatrix as a product of lower-triangular Toeplitz matrices; the
1611/// determinant of the full `n × n` matrix is `(-1)^n` times the constant
1612/// coefficient of `det(λI - mat)`.
1613///
1614/// Reference: Berkowitz, S. J. (1984), "On computing the determinant in small
1615/// parallel time using a small number of processors", *Inf. Process. Lett.*
1616/// 18(3).
1617fn berkowitz_determinant(mat: &[Vec<Polynomial>]) -> Polynomial {
1618    let n = mat.len();
1619    if n == 0 {
1620        return Polynomial::one();
1621    }
1622
1623    // `char_vec` holds coefficients [c_0, c_1, …, c_k] of the characteristic
1624    // polynomial of the `k × k` leading principal submatrix:
1625    // `det(λI - M_k) = c_0 λ^k + c_1 λ^{k-1} + … + c_k`, with `c_0 = 1`.
1626    // Base case `k = 0`: the empty matrix's characteristic polynomial is `1`.
1627    let mut char_vec: Vec<Polynomial> = vec![Polynomial::one()];
1628
1629    for r in 1..=n {
1630        let a = mat[r - 1][r - 1].clone();
1631
1632        // Toeplitz first-column vector `t` (length `r + 1`):
1633        //   t[0] = 1
1634        //   t[1] = -a
1635        //   t[k] = -(row · M_{r-1}^{k-2} · col)   for k = 2..=r
1636        // where the leading `(r-1)`-square submatrix `M_{r-1}` is `mat[0..r-1]
1637        // [0..r-1]`, `col` its column `r-1`, and `row` its row `r-1`.
1638        let mut t: Vec<Polynomial> = Vec::with_capacity(r + 1);
1639        t.push(Polynomial::one());
1640        t.push(a.neg());
1641
1642        if r >= 2 {
1643            let dim = r - 1;
1644            let col: Vec<Polynomial> = (0..dim).map(|i| mat[i][r - 1].clone()).collect();
1645            let row: Vec<Polynomial> = (0..dim).map(|j| mat[r - 1][j].clone()).collect();
1646
1647            // `w` starts as `M^0 · col = col`, then `w ← M_{r-1} · w`.
1648            let mut w = col;
1649            for k in 0..dim {
1650                let mut dot = Polynomial::zero();
1651                for (rj, wj) in row.iter().zip(w.iter()) {
1652                    dot = Polynomial::add(&dot, &Polynomial::mul(rj, wj));
1653                }
1654                t.push(dot.neg());
1655
1656                if k + 1 < dim {
1657                    let mut w_next: Vec<Polynomial> = Vec::with_capacity(dim);
1658                    for mat_row in mat.iter().take(dim) {
1659                        let mut acc = Polynomial::zero();
1660                        for (j, wj) in w.iter().enumerate() {
1661                            acc = Polynomial::add(&acc, &Polynomial::mul(&mat_row[j], wj));
1662                        }
1663                        w_next.push(acc);
1664                    }
1665                    w = w_next;
1666                }
1667            }
1668        }
1669
1670        // `new_char = C · char_vec`, where `C` is the `(r+1) × r` lower-
1671        // triangular Toeplitz matrix with first column `t`:
1672        // `C[i][j] = t[i-j]` for `i >= j`, else `0`.
1673        let mut new_char: Vec<Polynomial> = Vec::with_capacity(r + 1);
1674        for i in 0..=r {
1675            let mut acc = Polynomial::zero();
1676            for (j, cv) in char_vec.iter().enumerate() {
1677                if i >= j && (i - j) < t.len() {
1678                    acc = Polynomial::add(&acc, &Polynomial::mul(&t[i - j], cv));
1679                }
1680            }
1681            new_char.push(acc);
1682        }
1683        char_vec = new_char;
1684    }
1685
1686    // det(M) = (-1)^n · c_n, where c_n is the trailing coefficient of
1687    // `det(λI - M)`.
1688    let c_n = char_vec[n].clone();
1689    if n.is_multiple_of(2) { c_n } else { c_n.neg() }
1690}