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