Skip to main content

oxiz_math/polynomial/
advanced_ops.rs

1//! Advanced polynomial operations: factorization, root finding, and interpolation.
2
3use super::helpers::*;
4use super::types::*;
5#[allow(unused_imports)]
6use crate::prelude::*;
7use num_bigint::BigInt;
8use num_rational::BigRational;
9use num_traits::{One, Signed, Zero};
10
11type Polynomial = super::Polynomial;
12
13impl super::Polynomial {
14    /// Refine all roots in the given intervals using Newton-Raphson.
15    ///
16    /// Takes a list of isolating intervals (from `isolate_roots`) and refines
17    /// each root to higher precision using Newton-Raphson iteration.
18    ///
19    /// # Arguments
20    ///
21    /// * `var` - The variable to solve for
22    /// * `intervals` - List of isolating intervals from `isolate_roots`
23    /// * `max_iterations` - Maximum number of Newton-Raphson iterations per root
24    /// * `tolerance` - Convergence tolerance
25    ///
26    /// # Returns
27    ///
28    /// Vector of refined root approximations
29    ///
30    /// # Examples
31    ///
32    /// ```
33    /// use num_bigint::BigInt;
34    /// use num_rational::BigRational;
35    /// use oxiz_math::polynomial::Polynomial;
36    ///
37    /// // Solve x^2 - 2 = 0
38    /// let p = Polynomial::from_coeffs_int(&[
39    ///     (1, &[(0, 2)]),  // x^2
40    ///     (-2, &[]),       // -2
41    /// ]);
42    ///
43    /// let intervals = p.isolate_roots(0);
44    /// let tolerance = BigRational::new(BigInt::from(1), BigInt::from(1000000));
45    /// let refined_roots = p.refine_roots(0, &intervals, 10, &tolerance);
46    /// ```
47    pub fn refine_roots(
48        &self,
49        var: Var,
50        intervals: &[(BigRational, BigRational)],
51        max_iterations: usize,
52        tolerance: &BigRational,
53    ) -> Vec<BigRational> {
54        intervals
55            .iter()
56            .filter_map(|(lower, upper)| {
57                // Use midpoint as initial guess
58                let initial = (lower + upper) / BigRational::from_integer(BigInt::from(2));
59                self.newton_raphson(
60                    var,
61                    initial,
62                    lower.clone(),
63                    upper.clone(),
64                    max_iterations,
65                    tolerance,
66                )
67            })
68            .collect()
69    }
70
71    /// Compute the Taylor series expansion of a univariate polynomial around a point.
72    ///
73    /// For a polynomial p(x), computes the Taylor series:
74    /// p(x) = Σ_{k=0}^n (p^(k)(a) / k!) * (x - a)^k
75    ///
76    /// where p^(k) is the k-th derivative.
77    ///
78    /// # Arguments
79    ///
80    /// * `var` - The variable to expand around
81    /// * `point` - The point to expand around (typically 0 for Maclaurin series)
82    /// * `degree` - Maximum degree of the Taylor expansion
83    ///
84    /// # Returns
85    ///
86    /// A polynomial representing the Taylor series expansion
87    ///
88    /// # Examples
89    ///
90    /// ```
91    /// use num_bigint::BigInt;
92    /// use num_rational::BigRational;
93    /// use oxiz_math::polynomial::Polynomial;
94    ///
95    /// // Expand x^2 + 2x + 1 around x = 0
96    /// let p = Polynomial::from_coeffs_int(&[
97    ///     (1, &[(0, 2)]),  // x^2
98    ///     (2, &[(0, 1)]),  // 2x
99    ///     (1, &[]),        // 1
100    /// ]);
101    ///
102    /// let taylor = p.taylor_expansion(0, &BigRational::from_integer(BigInt::from(0)), 3);
103    /// // Should be the same polynomial since it's already a polynomial
104    /// ```
105    pub fn taylor_expansion(&self, var: Var, point: &BigRational, degree: u32) -> Polynomial {
106        use crate::rational::factorial;
107
108        let mut result = Polynomial::zero();
109        let mut derivative = self.clone();
110
111        // Compute successive derivatives and evaluate at the point
112        for k in 0..=degree {
113            // Evaluate derivative at the point
114            let mut assignment = FxHashMap::default();
115            assignment.insert(var, point.clone());
116            let coeff_at_point = derivative.eval(&assignment);
117
118            // Divide by k!
119            let factorial_k = factorial(k);
120            let taylor_coeff = coeff_at_point / BigRational::from_integer(factorial_k);
121
122            // Create term (x - point)^k
123            let shifted_term = if k == 0 {
124                Polynomial::constant(taylor_coeff)
125            } else {
126                // (x - a)^k = sum of binomial expansion
127                // For simplicity, compute it directly
128                let x_poly = Polynomial::from_var(var);
129                let point_poly = Polynomial::constant(point.clone());
130                let mut power = x_poly - point_poly;
131
132                for _ in 1..k {
133                    let x_poly = Polynomial::from_var(var);
134                    let point_poly = Polynomial::constant(point.clone());
135                    power = power * (x_poly - point_poly);
136                }
137
138                power * Polynomial::constant(taylor_coeff)
139            };
140
141            result = result + shifted_term;
142
143            // Compute next derivative if needed
144            if k < degree {
145                derivative = derivative.derivative(var);
146            }
147        }
148
149        result
150    }
151
152    /// Compute the Maclaurin series expansion (Taylor series around 0).
153    ///
154    /// This is a convenience method for Taylor expansion around 0.
155    ///
156    /// # Arguments
157    ///
158    /// * `var` - The variable to expand
159    /// * `degree` - Maximum degree of the expansion
160    ///
161    /// # Examples
162    ///
163    /// ```
164    /// use oxiz_math::polynomial::Polynomial;
165    ///
166    /// // Expand x^3 - 2x around x = 0
167    /// let p = Polynomial::from_coeffs_int(&[
168    ///     (1, &[(0, 3)]),   // x^3
169    ///     (-2, &[(0, 1)]),  // -2x
170    /// ]);
171    ///
172    /// let maclaurin = p.maclaurin_expansion(0, 4);
173    /// ```
174    pub fn maclaurin_expansion(&self, var: Var, degree: u32) -> Polynomial {
175        use num_bigint::BigInt;
176        self.taylor_expansion(var, &BigRational::from_integer(BigInt::from(0)), degree)
177    }
178
179    /// Find a root using the bisection method.
180    ///
181    /// The bisection method is a robust root-finding algorithm that works by repeatedly
182    /// bisecting an interval and selecting the subinterval where the function changes sign.
183    ///
184    /// # Arguments
185    ///
186    /// * `var` - The variable to solve for
187    /// * `lower` - Lower bound (must have f(lower) and f(upper) with opposite signs)
188    /// * `upper` - Upper bound (must have f(lower) and f(upper) with opposite signs)
189    /// * `max_iterations` - Maximum number of iterations
190    /// * `tolerance` - Convergence tolerance (stop when interval width < tolerance)
191    ///
192    /// # Returns
193    ///
194    /// The approximate root, or None if the initial bounds don't bracket a root
195    ///
196    /// # Examples
197    ///
198    /// ```
199    /// use num_bigint::BigInt;
200    /// use num_rational::BigRational;
201    /// use oxiz_math::polynomial::Polynomial;
202    ///
203    /// // Solve x^2 - 2 = 0 (find sqrt(2))
204    /// let p = Polynomial::from_coeffs_int(&[
205    ///     (1, &[(0, 2)]),  // x^2
206    ///     (-2, &[]),       // -2
207    /// ]);
208    ///
209    /// let lower = BigRational::from_integer(BigInt::from(1));
210    /// let upper = BigRational::from_integer(BigInt::from(2));
211    /// let tolerance = BigRational::new(BigInt::from(1), BigInt::from(1000000));
212    ///
213    /// let root = p.bisection(0, lower, upper, 100, &tolerance);
214    /// assert!(root.is_some());
215    /// ```
216    pub fn bisection(
217        &self,
218        var: Var,
219        lower: BigRational,
220        upper: BigRational,
221        max_iterations: usize,
222        tolerance: &BigRational,
223    ) -> Option<BigRational> {
224        use num_traits::{Signed, Zero};
225
226        let mut a = lower;
227        let mut b = upper;
228
229        // Evaluate at endpoints
230        let mut assignment_a = FxHashMap::default();
231        assignment_a.insert(var, a.clone());
232        let fa = self.eval(&assignment_a);
233
234        let mut assignment_b = FxHashMap::default();
235        assignment_b.insert(var, b.clone());
236        let fb = self.eval(&assignment_b);
237
238        // Check if endpoints bracket a root (opposite signs)
239        if fa.is_zero() {
240            return Some(a);
241        }
242        if fb.is_zero() {
243            return Some(b);
244        }
245        if (fa.is_positive() && fb.is_positive()) || (fa.is_negative() && fb.is_negative()) {
246            // Same sign, no root bracketed
247            return None;
248        }
249
250        for _ in 0..max_iterations {
251            // Check if interval is small enough
252            if (&b - &a).abs() < *tolerance {
253                return Some((&a + &b) / BigRational::from_integer(BigInt::from(2)));
254            }
255
256            // Compute midpoint
257            let mid = (&a + &b) / BigRational::from_integer(BigInt::from(2));
258
259            // Evaluate at midpoint
260            let mut assignment_mid = FxHashMap::default();
261            assignment_mid.insert(var, mid.clone());
262            let fmid = self.eval(&assignment_mid);
263
264            // Check if we found exact root
265            if fmid.is_zero() {
266                return Some(mid);
267            }
268
269            // Update interval
270            let mut assignment_a = FxHashMap::default();
271            assignment_a.insert(var, a.clone());
272            let fa = self.eval(&assignment_a);
273
274            if (fa.is_positive() && fmid.is_negative()) || (fa.is_negative() && fmid.is_positive())
275            {
276                // Root is in [a, mid]
277                b = mid;
278            } else {
279                // Root is in [mid, b]
280                a = mid;
281            }
282        }
283
284        // Return midpoint as best approximation
285        Some((&a + &b) / BigRational::from_integer(BigInt::from(2)))
286    }
287
288    /// Find a root using the secant method.
289    ///
290    /// The secant method is similar to Newton-Raphson but doesn't require computing derivatives.
291    /// It uses two previous points to approximate the derivative.
292    ///
293    /// # Arguments
294    ///
295    /// * `var` - The variable to solve for
296    /// * `x0` - First initial guess
297    /// * `x1` - Second initial guess (should be close to x0)
298    /// * `max_iterations` - Maximum number of iterations
299    /// * `tolerance` - Convergence tolerance
300    ///
301    /// # Returns
302    ///
303    /// The approximate root, or None if the method fails to converge
304    ///
305    /// # Examples
306    ///
307    /// ```
308    /// use num_bigint::BigInt;
309    /// use num_rational::BigRational;
310    /// use oxiz_math::polynomial::Polynomial;
311    ///
312    /// // Solve x^2 - 2 = 0
313    /// let p = Polynomial::from_coeffs_int(&[
314    ///     (1, &[(0, 2)]),  // x^2
315    ///     (-2, &[]),       // -2
316    /// ]);
317    ///
318    /// let x0 = BigRational::from_integer(BigInt::from(1));
319    /// let x1 = BigRational::new(BigInt::from(3), BigInt::from(2)); // 1.5
320    /// let tolerance = BigRational::new(BigInt::from(1), BigInt::from(1000000));
321    ///
322    /// let root = p.secant(0, x0, x1, 20, &tolerance);
323    /// assert!(root.is_some());
324    /// ```
325    pub fn secant(
326        &self,
327        var: Var,
328        x0: BigRational,
329        x1: BigRational,
330        max_iterations: usize,
331        tolerance: &BigRational,
332    ) -> Option<BigRational> {
333        use num_traits::{Signed, Zero};
334
335        let mut x_prev = x0;
336        let mut x_curr = x1;
337
338        for _ in 0..max_iterations {
339            // Evaluate at current and previous points
340            let mut assignment_prev = FxHashMap::default();
341            assignment_prev.insert(var, x_prev.clone());
342            let f_prev = self.eval(&assignment_prev);
343
344            let mut assignment_curr = FxHashMap::default();
345            assignment_curr.insert(var, x_curr.clone());
346            let f_curr = self.eval(&assignment_curr);
347
348            // Check if we're close enough
349            if f_curr.abs() < *tolerance {
350                return Some(x_curr);
351            }
352
353            // Check for zero denominator
354            let denom = &f_curr - &f_prev;
355            if denom.is_zero() {
356                return None;
357            }
358
359            // Secant method update: x_new = x_curr - f_curr * (x_curr - x_prev) / (f_curr - f_prev)
360            let x_new = &x_curr - &f_curr * (&x_curr - &x_prev) / denom;
361
362            x_prev = x_curr;
363            x_curr = x_new;
364        }
365
366        Some(x_curr)
367    }
368
369    /// Sign of the polynomial given signs of variables.
370    /// Returns Some(1) for positive, Some(-1) for negative, Some(0) for zero,
371    /// or None if undetermined.
372    pub fn sign_at(&self, var_signs: &FxHashMap<Var, i8>) -> Option<i8> {
373        if self.is_zero() {
374            return Some(0);
375        }
376        if self.is_constant() {
377            let c = &self.terms[0].coeff;
378            return Some(if c.is_positive() {
379                1
380            } else if c.is_negative() {
381                -1
382            } else {
383                0
384            });
385        }
386
387        // Try to determine sign from term signs
388        let mut all_positive = true;
389        let mut all_negative = true;
390
391        for term in &self.terms {
392            let mut term_sign: i8 = if term.coeff.is_positive() {
393                1
394            } else if term.coeff.is_negative() {
395                -1
396            } else {
397                continue;
398            };
399
400            for vp in term.monomial.vars() {
401                if let Some(&s) = var_signs.get(&vp.var) {
402                    if s == 0 {
403                        // Variable is zero; this term contributes 0
404                        term_sign = 0;
405                        break;
406                    }
407                    if vp.power % 2 == 1 {
408                        term_sign *= s;
409                    }
410                } else {
411                    // Unknown sign; can't determine overall sign
412                    return None;
413                }
414            }
415
416            if term_sign > 0 {
417                all_negative = false;
418            } else if term_sign < 0 {
419                all_positive = false;
420            }
421        }
422
423        if all_positive && !all_negative {
424            Some(1)
425        } else if all_negative && !all_positive {
426            Some(-1)
427        } else {
428            None
429        }
430    }
431
432    /// Check if a univariate polynomial is irreducible over rationals.
433    /// This is a heuristic test - returns false if definitely reducible,
434    /// true if likely irreducible (but not guaranteed).
435    pub fn is_irreducible(&self, var: Var) -> bool {
436        if self.is_zero() || self.is_constant() {
437            return false;
438        }
439
440        let deg = self.degree(var);
441        if deg == 0 {
442            return false;
443        }
444        if deg == 1 {
445            return true;
446        }
447
448        // Check if square-free
449        let sf = self.square_free();
450        if sf.degree(var) < deg {
451            return false; // Has repeated factors
452        }
453
454        // For degree 2 and 3, use discriminant-based checks
455        if deg == 2 {
456            return self.is_irreducible_quadratic(var);
457        }
458
459        // For higher degrees, we'd need more sophisticated tests
460        // For now, assume possibly irreducible
461        true
462    }
463
464    /// Check if a quadratic polynomial is irreducible over rationals.
465    fn is_irreducible_quadratic(&self, var: Var) -> bool {
466        let deg = self.degree(var);
467        if deg != 2 {
468            return false;
469        }
470
471        let a = self.univ_coeff(var, 2);
472        let b = self.univ_coeff(var, 1);
473        let c = self.univ_coeff(var, 0);
474
475        // Check discriminant: b^2 - 4ac
476        // If not a perfect square, polynomial is irreducible over rationals
477        let discriminant = &b * &b - (BigRational::from_integer(BigInt::from(4)) * &a * &c);
478
479        if discriminant.is_negative() {
480            return true; // No real roots
481        }
482
483        // Check if discriminant is a perfect square of a rational
484        let num_sqrt = is_perfect_square(discriminant.numer());
485        let den_sqrt = is_perfect_square(discriminant.denom());
486
487        !(num_sqrt && den_sqrt)
488    }
489
490    /// Factor a univariate polynomial into irreducible factors.
491    /// Returns a vector of (factor, multiplicity) pairs.
492    /// Each factor is monic and primitive.
493    pub fn factor(&self, var: Var) -> Vec<(Polynomial, u32)> {
494        if self.is_zero() {
495            return vec![];
496        }
497
498        if self.is_constant() {
499            return vec![(self.clone(), 1)];
500        }
501
502        let deg = self.degree(var);
503        if deg == 0 {
504            return vec![(self.clone(), 1)];
505        }
506
507        // Make monic and primitive
508        let p = self.primitive().make_monic();
509
510        // Handle degree 1 (linear) - always irreducible
511        if deg == 1 {
512            return vec![(p, 1)];
513        }
514
515        // Handle degree 2 (quadratic) - use quadratic formula
516        if deg == 2 {
517            return self.factor_quadratic(var);
518        }
519
520        // For degree > 2, use square-free factorization first
521        self.factor_square_free(var)
522    }
523
524    /// Factor a quadratic polynomial.
525    fn factor_quadratic(&self, var: Var) -> Vec<(Polynomial, u32)> {
526        let deg = self.degree(var);
527        if deg != 2 {
528            return vec![(self.primitive(), 1)];
529        }
530
531        let p = self.primitive().make_monic();
532        let a = p.univ_coeff(var, 2);
533        let b = p.univ_coeff(var, 1);
534        let c = p.univ_coeff(var, 0);
535
536        // Discriminant: b^2 - 4ac
537        let discriminant = &b * &b - (BigRational::from_integer(BigInt::from(4)) * &a * &c);
538
539        // Check if discriminant is a perfect square
540        let num_sqrt = integer_sqrt(discriminant.numer());
541        let den_sqrt = integer_sqrt(discriminant.denom());
542
543        if num_sqrt.is_none() || den_sqrt.is_none() {
544            // Irreducible over rationals
545            return vec![(p, 1)];
546        }
547
548        let disc_sqrt = BigRational::new(
549            num_sqrt.expect("num_sqrt should be valid"),
550            den_sqrt.expect("operation should succeed"),
551        );
552
553        // Roots: (-b ± sqrt(disc)) / (2a)
554        let two_a = BigRational::from_integer(BigInt::from(2)) * &a;
555        let root1 = (-&b + &disc_sqrt) / &two_a;
556        let root2 = (-&b - disc_sqrt) / two_a;
557
558        // Factor as (x - root1)(x - root2)
559        let factor1 = Polynomial::from_terms(
560            vec![
561                Term::new(BigRational::one(), Monomial::from_var(var)),
562                Term::new(-root1, Monomial::unit()),
563            ],
564            MonomialOrder::Lex,
565        );
566
567        let factor2 = Polynomial::from_terms(
568            vec![
569                Term::new(BigRational::one(), Monomial::from_var(var)),
570                Term::new(-root2, Monomial::unit()),
571            ],
572            MonomialOrder::Lex,
573        );
574
575        vec![(factor1, 1), (factor2, 1)]
576    }
577
578    /// Square-free factorization: decompose into coprime factors.
579    /// Returns factors with their multiplicities.
580    fn factor_square_free(&self, var: Var) -> Vec<(Polynomial, u32)> {
581        if self.is_zero() || self.is_constant() {
582            return vec![(self.clone(), 1)];
583        }
584
585        let mut result = Vec::new();
586        let p = self.primitive();
587        let mut multiplicity = 1u32;
588
589        // Yun's algorithm for square-free factorization
590        let deriv = p.derivative(var);
591
592        if deriv.is_zero() {
593            // All exponents are multiples of characteristic (0 for rationals)
594            // This shouldn't happen for polynomials over rationals
595            return vec![(p, 1)];
596        }
597
598        let gcd = p.gcd_univariate(&deriv);
599
600        if gcd.is_constant() {
601            // Already square-free
602            if p.is_irreducible(var) {
603                return vec![(p.make_monic(), 1)];
604            } else {
605                // Try to factor further (for now, return as-is)
606                return vec![(p.make_monic(), 1)];
607            }
608        }
609
610        let (quo, rem) = p.pseudo_div_univariate(&gcd);
611        if !rem.is_zero() {
612            return vec![(p, 1)];
613        }
614
615        let mut u = quo.primitive();
616        let (v_quo, v_rem) = deriv.pseudo_div_univariate(&gcd);
617        if v_rem.is_zero() {
618            let v = v_quo.primitive();
619            let mut w = Polynomial::sub(&v, &u.derivative(var));
620
621            while !w.is_zero() && !w.is_constant() {
622                let y = u.gcd_univariate(&w);
623                if !y.is_constant() {
624                    result.push((y.make_monic(), multiplicity));
625                    let (u_new, u_rem) = u.pseudo_div_univariate(&y);
626                    if u_rem.is_zero() {
627                        u = u_new.primitive();
628                    }
629                    let (w_new, w_rem) = w.pseudo_div_univariate(&y);
630                    if w_rem.is_zero() {
631                        w = Polynomial::sub(&w_new.primitive(), &u.derivative(var));
632                    } else {
633                        break;
634                    }
635                } else {
636                    break;
637                }
638                multiplicity += 1;
639            }
640
641            if !u.is_constant() {
642                result.push((u.make_monic(), multiplicity));
643            }
644        }
645
646        if result.is_empty() {
647            result.push((p.make_monic(), 1));
648        }
649
650        result
651    }
652
653    /// Content of a polynomial: GCD of all coefficients.
654    /// Returns the rational content.
655    pub fn content(&self) -> BigRational {
656        if self.terms.is_empty() {
657            return BigRational::one();
658        }
659
660        let mut num_gcd: Option<BigInt> = None;
661        let mut den_lcm: Option<BigInt> = None;
662
663        for term in &self.terms {
664            let coeff_num = term.coeff.numer().clone().abs();
665            let coeff_den = term.coeff.denom().clone();
666
667            num_gcd = Some(match num_gcd {
668                None => coeff_num,
669                Some(g) => gcd_bigint(g, coeff_num),
670            });
671
672            den_lcm = Some(match den_lcm {
673                None => coeff_den,
674                Some(l) => {
675                    let gcd = gcd_bigint(l.clone(), coeff_den.clone());
676                    (&l * &coeff_den) / gcd
677                }
678            });
679        }
680
681        BigRational::new(
682            num_gcd.unwrap_or_else(BigInt::one),
683            den_lcm.unwrap_or_else(BigInt::one),
684        )
685    }
686
687    /// Compose this polynomial with another: compute p(q(x)).
688    ///
689    /// This is an alias for `substitute` with clearer semantics for composition.
690    /// If `self` is p(x) and `other` is q(x), returns p(q(x)).
691    pub fn compose(&self, var: Var, other: &Polynomial) -> Polynomial {
692        self.substitute(var, other)
693    }
694
695    /// Lagrange polynomial interpolation.
696    ///
697    /// Given a set of points (x_i, y_i), constructs the unique polynomial of minimal degree
698    /// that passes through all the points.
699    ///
700    /// # Arguments
701    /// * `var` - The variable to use for the polynomial
702    /// * `points` - A slice of (x, y) coordinate pairs
703    ///
704    /// # Returns
705    /// The interpolating polynomial, or None if points is empty or contains duplicate x values
706    ///
707    /// # Reference
708    /// Standard Lagrange interpolation formula from numerical analysis textbooks
709    pub fn lagrange_interpolate(
710        var: Var,
711        points: &[(BigRational, BigRational)],
712    ) -> Option<Polynomial> {
713        if points.is_empty() {
714            return None;
715        }
716
717        if points.len() == 1 {
718            // Single point: constant polynomial
719            return Some(Polynomial::constant(points[0].1.clone()));
720        }
721
722        // Check for duplicate x values
723        for i in 0..points.len() {
724            for j in i + 1..points.len() {
725                if points[i].0 == points[j].0 {
726                    return None; // Duplicate x values
727                }
728            }
729        }
730
731        let mut result = Polynomial::zero();
732
733        // Lagrange basis polynomials
734        for i in 0..points.len() {
735            let (x_i, y_i) = &points[i];
736            let mut basis = Polynomial::one();
737
738            for (j, (x_j, _)) in points.iter().enumerate() {
739                if i == j {
740                    continue;
741                }
742
743                // basis *= (x - x_j) / (x_i - x_j)
744                let x_poly = Polynomial::from_var(var);
745                let const_poly = Polynomial::constant(x_j.clone());
746                let numerator = &x_poly - &const_poly;
747                let denominator = x_i - x_j;
748
749                if denominator.is_zero() {
750                    return None; // Should not happen after duplicate check
751                }
752
753                basis = &basis * &numerator;
754                basis = basis.scale(&(BigRational::one() / denominator));
755            }
756
757            // result += y_i * basis
758            let scaled_basis = basis.scale(y_i);
759            result = &result + &scaled_basis;
760        }
761
762        Some(result)
763    }
764
765    /// Newton polynomial interpolation using divided differences.
766    ///
767    /// Constructs the same interpolating polynomial as Lagrange interpolation,
768    /// but using Newton's divided difference form which can be more efficient
769    /// for incremental construction.
770    ///
771    /// # Arguments
772    /// * `var` - The variable to use for the polynomial
773    /// * `points` - A slice of (x, y) coordinate pairs
774    ///
775    /// # Returns
776    /// The interpolating polynomial, or None if points is empty or contains duplicate x values
777    pub fn newton_interpolate(
778        var: Var,
779        points: &[(BigRational, BigRational)],
780    ) -> Option<Polynomial> {
781        if points.is_empty() {
782            return None;
783        }
784
785        if points.len() == 1 {
786            return Some(Polynomial::constant(points[0].1.clone()));
787        }
788
789        // Check for duplicate x values
790        for i in 0..points.len() {
791            for j in i + 1..points.len() {
792                if points[i].0 == points[j].0 {
793                    return None;
794                }
795            }
796        }
797
798        let n = points.len();
799
800        // Compute divided differences table
801        let mut dd: Vec<Vec<BigRational>> = vec![vec![BigRational::zero(); n]; n];
802
803        // Initialize first column with y values
804        for i in 0..n {
805            dd[i][0] = points[i].1.clone();
806        }
807
808        // Compute divided differences
809        for j in 1..n {
810            for i in 0..n - j {
811                let numerator = &dd[i + 1][j - 1] - &dd[i][j - 1];
812                let denominator = &points[i + j].0 - &points[i].0;
813                if denominator.is_zero() {
814                    return None;
815                }
816                dd[i][j] = numerator / denominator;
817            }
818        }
819
820        // Build Newton polynomial: p(x) = a_0 + a_1(x-x_0) + a_2(x-x_0)(x-x_1) + ...
821        let mut result = Polynomial::constant(dd[0][0].clone());
822        let mut term = Polynomial::one();
823
824        for i in 1..n {
825            // term *= (x - x_{i-1})
826            let x_poly = Polynomial::from_var(var);
827            let const_poly = Polynomial::constant(points[i - 1].0.clone());
828            let factor = &x_poly - &const_poly;
829            term = &term * &factor;
830
831            // result += a_i * term
832            let scaled_term = term.scale(&dd[0][i]);
833            result = &result + &scaled_term;
834        }
835
836        Some(result)
837    }
838
839    /// Generate the nth Chebyshev polynomial of the first kind T_n(x).
840    ///
841    /// Chebyshev polynomials of the first kind are defined by:
842    /// - T_0(x) = 1
843    /// - T_1(x) = x
844    /// - T_{n+1}(x) = 2x T_n(x) - T_{n-1}(x)
845    ///
846    /// These polynomials are orthogonal on [-1, 1] with respect to the weight
847    /// function 1/√(1-x²) and are useful for polynomial approximation.
848    ///
849    /// # Arguments
850    /// * `var` - The variable to use for the polynomial
851    /// * `n` - The degree of the Chebyshev polynomial
852    ///
853    /// # Returns
854    /// The nth Chebyshev polynomial T_n(var)
855    pub fn chebyshev_first_kind(var: Var, n: u32) -> Polynomial {
856        match n {
857            0 => Polynomial::one(),
858            1 => Polynomial::from_var(var),
859            _ => {
860                // Use recurrence relation: T_{n+1}(x) = 2x T_n(x) - T_{n-1}(x)
861                let mut t_prev = Polynomial::one(); // T_0
862                let mut t_curr = Polynomial::from_var(var); // T_1
863
864                for _ in 2..=n {
865                    let x = Polynomial::from_var(var);
866                    let two_x_t_curr = Polynomial::mul(&t_curr, &x)
867                        .scale(&BigRational::from_integer(BigInt::from(2)));
868                    let t_next = Polynomial::sub(&two_x_t_curr, &t_prev);
869                    t_prev = t_curr;
870                    t_curr = t_next;
871                }
872
873                t_curr
874            }
875        }
876    }
877
878    /// Generate the nth Chebyshev polynomial of the second kind U_n(x).
879    ///
880    /// Chebyshev polynomials of the second kind are defined by:
881    /// - U_0(x) = 1
882    /// - U_1(x) = 2x
883    /// - U_{n+1}(x) = 2x U_n(x) - U_{n-1}(x)
884    ///
885    /// These polynomials are orthogonal on [-1, 1] with respect to the weight
886    /// function √(1-x²).
887    ///
888    /// # Arguments
889    /// * `var` - The variable to use for the polynomial
890    /// * `n` - The degree of the Chebyshev polynomial
891    ///
892    /// # Returns
893    /// The nth Chebyshev polynomial U_n(var)
894    pub fn chebyshev_second_kind(var: Var, n: u32) -> Polynomial {
895        match n {
896            0 => Polynomial::one(),
897            1 => {
898                // U_1(x) = 2x
899                Polynomial::from_var(var).scale(&BigRational::from_integer(BigInt::from(2)))
900            }
901            _ => {
902                // Use recurrence relation: U_{n+1}(x) = 2x U_n(x) - U_{n-1}(x)
903                let mut u_prev = Polynomial::one(); // U_0
904                let mut u_curr =
905                    Polynomial::from_var(var).scale(&BigRational::from_integer(BigInt::from(2))); // U_1
906
907                for _ in 2..=n {
908                    let x = Polynomial::from_var(var);
909                    let two_x_u_curr = Polynomial::mul(&u_curr, &x)
910                        .scale(&BigRational::from_integer(BigInt::from(2)));
911                    let u_next = Polynomial::sub(&two_x_u_curr, &u_prev);
912                    u_prev = u_curr;
913                    u_curr = u_next;
914                }
915
916                u_curr
917            }
918        }
919    }
920
921    /// Compute Chebyshev nodes (zeros of Chebyshev polynomial) for use in interpolation.
922    ///
923    /// The Chebyshev nodes minimize the Runge phenomenon in polynomial interpolation.
924    /// For degree n, returns n+1 nodes in [-1, 1].
925    ///
926    /// The nodes are: x_k = cos((2k+1)π / (2n+2)) for k = 0, 1, ..., n
927    ///
928    /// Note: This function returns approximate rational values. For exact values,
929    /// use algebraic numbers or symbolic computation.
930    ///
931    /// # Arguments
932    /// * `n` - The degree (returns n+1 nodes)
933    ///
934    /// # Returns
935    /// Approximations of the Chebyshev nodes as BigRational values
936    pub fn chebyshev_nodes(n: u32) -> Vec<BigRational> {
937        if n == 0 {
938            return vec![BigRational::zero()];
939        }
940
941        let mut nodes = Vec::with_capacity((n + 1) as usize);
942
943        // Compute nodes: cos((2k+1)π / (2n+2))
944        // We'll use a rational approximation
945        for k in 0..=n {
946            // Approximate cos using Taylor series or use exact rational bounds
947            // For now, use a simple rational approximation based on the angle
948
949            // Angle = (2k+1) / (2n+2) * π/2
950            // For small angles, we can use rational approximations
951            // This is a simplified version - ideally would use higher precision
952
953            let numerator = (2 * k + 1) as i64;
954            let denominator = (2 * n + 2) as i64;
955
956            // Simple linear approximation for demonstration
957            // In production, would use more accurate trigonometric approximation
958            let ratio = BigRational::new(BigInt::from(numerator), BigInt::from(denominator));
959
960            // Map to [-1, 1] range
961            // This is a placeholder - real implementation would compute cos accurately
962            let node = BigRational::one() - ratio * BigRational::from_integer(BigInt::from(2));
963            nodes.push(node);
964        }
965
966        nodes
967    }
968
969    /// Generate the nth Legendre polynomial P_n(x).
970    ///
971    /// Legendre polynomials are orthogonal polynomials on [-1, 1] with respect to
972    /// the weight function 1. They are defined by the recurrence:
973    /// - P_0(x) = 1
974    /// - P_1(x) = x
975    /// - (n+1) P_{n+1}(x) = (2n+1) x P_n(x) - n P_{n-1}(x)
976    ///
977    /// These polynomials are useful for Gaussian quadrature and least-squares
978    /// polynomial approximation.
979    ///
980    /// # Arguments
981    /// * `var` - The variable to use for the polynomial
982    /// * `n` - The degree of the Legendre polynomial
983    ///
984    /// # Returns
985    /// The nth Legendre polynomial P_n(var)
986    pub fn legendre(var: Var, n: u32) -> Polynomial {
987        match n {
988            0 => Polynomial::one(),
989            1 => Polynomial::from_var(var),
990            _ => {
991                // Recurrence: (n+1) P_{n+1}(x) = (2n+1) x P_n(x) - n P_{n-1}(x)
992                let mut p_prev = Polynomial::one(); // P_0
993                let mut p_curr = Polynomial::from_var(var); // P_1
994
995                for k in 1..n {
996                    let x = Polynomial::from_var(var);
997
998                    // (2k+1) x P_k(x)
999                    let coeff_2k_plus_1 = BigRational::from_integer(BigInt::from(2 * k + 1));
1000                    let term1 = Polynomial::mul(&p_curr, &x).scale(&coeff_2k_plus_1);
1001
1002                    // k P_{k-1}(x)
1003                    let coeff_k = BigRational::from_integer(BigInt::from(k));
1004                    let term2 = p_prev.scale(&coeff_k);
1005
1006                    // [(2k+1) x P_k(x) - k P_{k-1}(x)] / (k+1)
1007                    let numerator = Polynomial::sub(&term1, &term2);
1008                    let divisor = BigRational::from_integer(BigInt::from(k + 1));
1009                    let p_next = numerator.scale(&(BigRational::one() / divisor));
1010
1011                    p_prev = p_curr;
1012                    p_curr = p_next;
1013                }
1014
1015                p_curr
1016            }
1017        }
1018    }
1019
1020    /// Generate the nth Hermite polynomial H_n(x) (physicist's version).
1021    ///
1022    /// Hermite polynomials are orthogonal with respect to the weight function e^(-x²).
1023    /// The physicist's version is defined by:
1024    /// - H_0(x) = 1
1025    /// - H_1(x) = 2x
1026    /// - H_{n+1}(x) = 2x H_n(x) - 2n H_{n-1}(x)
1027    ///
1028    /// These polynomials are useful in quantum mechanics, probability theory,
1029    /// and numerical analysis.
1030    ///
1031    /// # Arguments
1032    /// * `var` - The variable to use for the polynomial
1033    /// * `n` - The degree of the Hermite polynomial
1034    ///
1035    /// # Returns
1036    /// The nth Hermite polynomial H_n(var)
1037    pub fn hermite(var: Var, n: u32) -> Polynomial {
1038        match n {
1039            0 => Polynomial::one(),
1040            1 => {
1041                // H_1(x) = 2x
1042                Polynomial::from_var(var).scale(&BigRational::from_integer(BigInt::from(2)))
1043            }
1044            _ => {
1045                // Recurrence: H_{n+1}(x) = 2x H_n(x) - 2n H_{n-1}(x)
1046                let mut h_prev = Polynomial::one(); // H_0
1047                let mut h_curr =
1048                    Polynomial::from_var(var).scale(&BigRational::from_integer(BigInt::from(2))); // H_1
1049
1050                for k in 1..n {
1051                    let x = Polynomial::from_var(var);
1052
1053                    // 2x H_k(x)
1054                    let two_x_h = Polynomial::mul(&h_curr, &x)
1055                        .scale(&BigRational::from_integer(BigInt::from(2)));
1056
1057                    // 2k H_{k-1}(x)
1058                    let coeff_2k = BigRational::from_integer(BigInt::from(2 * k));
1059                    let term2 = h_prev.scale(&coeff_2k);
1060
1061                    // 2x H_k(x) - 2k H_{k-1}(x)
1062                    let h_next = Polynomial::sub(&two_x_h, &term2);
1063
1064                    h_prev = h_curr;
1065                    h_curr = h_next;
1066                }
1067
1068                h_curr
1069            }
1070        }
1071    }
1072
1073    /// Generate the nth Laguerre polynomial L_n(x).
1074    ///
1075    /// Laguerre polynomials are orthogonal with respect to the weight function e^(-x)
1076    /// on [0, ∞). They are defined by:
1077    /// - L_0(x) = 1
1078    /// - L_1(x) = 1 - x
1079    /// - (n+1) L_{n+1}(x) = (2n+1-x) L_n(x) - n L_{n-1}(x)
1080    ///
1081    /// These polynomials are useful in quantum mechanics and numerical analysis.
1082    ///
1083    /// # Arguments
1084    /// * `var` - The variable to use for the polynomial
1085    /// * `n` - The degree of the Laguerre polynomial
1086    ///
1087    /// # Returns
1088    /// The nth Laguerre polynomial L_n(var)
1089    pub fn laguerre(var: Var, n: u32) -> Polynomial {
1090        match n {
1091            0 => Polynomial::one(),
1092            1 => {
1093                // L_1(x) = 1 - x
1094                let one = Polynomial::one();
1095                let x = Polynomial::from_var(var);
1096                Polynomial::sub(&one, &x)
1097            }
1098            _ => {
1099                // Recurrence: (n+1) L_{n+1}(x) = (2n+1-x) L_n(x) - n L_{n-1}(x)
1100                let mut l_prev = Polynomial::one(); // L_0
1101                let mut l_curr = {
1102                    let one = Polynomial::one();
1103                    let x = Polynomial::from_var(var);
1104                    Polynomial::sub(&one, &x)
1105                }; // L_1
1106
1107                for k in 1..n {
1108                    let x = Polynomial::from_var(var);
1109
1110                    // (2k+1-x) L_k(x) = (2k+1) L_k(x) - x L_k(x)
1111                    let coeff_2k_plus_1 = BigRational::from_integer(BigInt::from(2 * k + 1));
1112                    let term1 = l_curr.scale(&coeff_2k_plus_1);
1113                    let term2 = Polynomial::mul(&l_curr, &x);
1114                    let combined = Polynomial::sub(&term1, &term2);
1115
1116                    // k L_{k-1}(x)
1117                    let coeff_k = BigRational::from_integer(BigInt::from(k));
1118                    let term3 = l_prev.scale(&coeff_k);
1119
1120                    // [(2k+1-x) L_k(x) - k L_{k-1}(x)] / (k+1)
1121                    let numerator = Polynomial::sub(&combined, &term3);
1122                    let divisor = BigRational::from_integer(BigInt::from(k + 1));
1123                    let l_next = numerator.scale(&(BigRational::one() / divisor));
1124
1125                    l_prev = l_curr;
1126                    l_curr = l_next;
1127                }
1128
1129                l_curr
1130            }
1131        }
1132    }
1133}
1134
1135/// Check if a BigInt is a perfect square.
1136fn is_perfect_square(n: &BigInt) -> bool {
1137    if n.is_negative() {
1138        return false;
1139    }
1140    if n.is_zero() || n.is_one() {
1141        return true;
1142    }
1143
1144    let sqrt = integer_sqrt(n);
1145    sqrt.is_some()
1146}