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}