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