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}