mathhook_core/calculus/integrals/rational.rs
1//! Rational function integration via partial fraction decomposition
2//!
3//! Implements integration of rational functions `P(x)/Q(x)` using:
4//! 1. Polynomial long division if `deg(P) >= deg(Q)`
5//! 2. Factor denominator into linear and irreducible quadratic factors
6//! 3. Decompose into partial fractions using Heaviside's method
7//! 4. Integrate each term using standard formulas
8//!
9//! # Mathematical Background
10//!
11//! For `P(x)/Q(x)` where `deg(P) < deg(Q)`, factor `Q(x)` as:
12//! ```text
13//! Q(x) = (x-r₁)^n₁ · ... · (x-rₖ)^nₖ · (x²+p₁x+q₁)^m₁ · ...
14//! ```
15//!
16//! Then the partial fraction decomposition is:
17//! ```text
18//! P(x)/Q(x) = Σᵢ Σⱼ₌₁ⁿⁱ Aᵢⱼ/(x-rᵢ)ʲ + Σᵢ Σⱼ₌₁ᵐⁱ (Bᵢⱼx+Cᵢⱼ)/(x²+pᵢx+qᵢ)ʲ
19//! ```
20//!
21//! # Integration Formulas
22//!
23//! Linear terms:
24//! - `∫A/(x-r) dx = A·ln|x-r| + C`
25//! - `∫A/(x-r)ⁿ dx = -A/((n-1)(x-r)^(n-1)) + C` for `n > 1`
26//!
27//! Quadratic terms (irreducible `x²+px+q` with `p²-4q < 0`):
28//! - Complete the square: `x²+px+q = (x+p/2)² + a²` where `a² = q - p²/4`
29//! - `∫(Bx+C)/(x²+px+q) dx = (B/2)·ln|x²+px+q| + ((C-Bp/2)/a)·arctan((x+p/2)/a) + C`
30//!
31//! # Implementation Status
32//!
33//! **Fully Implemented:**
34//! - Simple linear factors `(x-r)` via cover-up method
35//! - Repeated linear factors `(x-r)^n` via Heaviside's method with derivatives
36//! - Simple irreducible quadratics `(x²+px+q)` with proper coefficient extraction
37//! - Repeated irreducible quadratics `(x²+px+q)²` via Ostrogradsky's reduction formula
38//!
39//! **Not Yet Implemented:**
40//! - Repeated irreducible quadratics `(x²+px+q)^m` with `m > 2`
41//! (Can be generalized using recursive Ostrogradsky reduction)
42//! - Automatic polynomial factorization (assumes factored form)
43//!
44//! # References
45//!
46//! This implementation follows the approaches in:
47//! - Heaviside's cover-up method and derivative technique for repeated poles
48//! - Ostrogradsky's reduction formula for repeated quadratics
49//! - Stewart, Calculus (8th ed), Chapter 7
50//! - Bronstein, "Symbolic Integration I"
51
52use crate::algebra::gcd::PolynomialGcd;
53use crate::core::constants::EPSILON;
54use crate::core::{Expression, Number, Symbol};
55use crate::simplify::Simplify;
56
57pub mod helpers;
58pub mod linear;
59pub mod quadratic;
60
61use helpers::{is_polynomial, polynomial_degree};
62use linear::integrate_linear_factor;
63use quadratic::{integrate_repeated_quadratic, integrate_simple_quadratic};
64
65#[derive(Debug, Clone)]
66pub struct LinearTerm {
67 pub coefficient: Expression,
68 pub root: Expression,
69 pub power: i64,
70}
71
72#[derive(Debug, Clone)]
73pub struct QuadraticTerm {
74 pub numerator_linear_coeff: Expression,
75 pub numerator_constant: Expression,
76 pub p_coeff: Expression,
77 pub q_coeff: Expression,
78 pub power: i64,
79}
80
81#[derive(Debug, Clone)]
82pub struct PartialFractionDecomposition {
83 pub polynomial_part: Expression,
84 pub linear_terms: Vec<LinearTerm>,
85 pub quadratic_terms: Vec<QuadraticTerm>,
86}
87
88#[derive(Debug, Clone)]
89enum Factor {
90 Linear {
91 root: Expression,
92 power: i64,
93 },
94 Quadratic {
95 p: Expression,
96 q: Expression,
97 power: i64,
98 },
99}
100
101/// Check if expression is a rational function `P(x)/Q(x)`
102///
103/// # Arguments
104///
105/// * `expr` - Expression to check
106/// * `var` - Variable
107///
108/// # Examples
109///
110/// ```
111/// use mathhook_core::{Expression, symbol};
112/// use mathhook_core::calculus::integrals::rational::is_rational_function;
113///
114/// let x = symbol!(x);
115/// let rational = Expression::mul(vec![
116/// Expression::symbol(x.clone()),
117/// Expression::pow(
118/// Expression::add(vec![
119/// Expression::symbol(x.clone()),
120/// Expression::integer(1),
121/// ]),
122/// Expression::integer(-1),
123/// ),
124/// ]);
125///
126/// assert!(is_rational_function(&rational, &x));
127/// ```
128pub fn is_rational_function(expr: &Expression, var: &Symbol) -> bool {
129 // Mathematically, a rational function is p(x)/q(x) where p and q are polynomials
130 // Polynomials are rational functions with denominator 1
131 // So we accept: polynomials OR expressions with polynomial numerator/denominator
132
133 // First check if it's just a polynomial (most common case)
134 if is_polynomial(expr, var) {
135 return true;
136 }
137
138 // Check for rational expressions (including sums of rational functions)
139 match expr {
140 // Sum of rational functions is also a rational function
141 Expression::Add(terms) => terms.iter().all(|term| is_rational_function(term, var)),
142
143 // Product form: p(x) * q(x)^(-1) or products of polynomials
144 Expression::Mul(factors) => {
145 // Check if all factors are either polynomials or powers of polynomials
146 factors.iter().all(|factor| {
147 match factor {
148 Expression::Pow(base, exp) => {
149 if let Expression::Number(Number::Integer(_e)) = exp.as_ref() {
150 is_polynomial(base, var) // Accept both positive and negative powers
151 } else {
152 false
153 }
154 }
155 _ => is_polynomial(factor, var),
156 }
157 })
158 }
159
160 // Power of polynomial
161 Expression::Pow(base, exp) => {
162 if let Expression::Number(Number::Integer(_e)) = exp.as_ref() {
163 is_polynomial(base, var) // Accept both x^2 and x^(-1)
164 } else {
165 false
166 }
167 }
168 _ => false,
169 }
170}
171
172/// Extract numerator and denominator from rational expression
173///
174/// # Arguments
175///
176/// * `expr` - Rational expression
177///
178/// # Returns
179///
180/// Tuple `(numerator, denominator)`
181///
182/// # Examples
183///
184/// ```
185/// use mathhook_core::{Expression, symbol};
186/// use mathhook_core::calculus::integrals::rational::extract_numerator_denominator;
187///
188/// let x = symbol!(x);
189/// let expr = Expression::mul(vec![
190/// Expression::integer(2),
191/// Expression::pow(
192/// Expression::symbol(x.clone()),
193/// Expression::integer(-1),
194/// ),
195/// ]);
196///
197/// let (num, den) = extract_numerator_denominator(&expr);
198/// ```
199pub fn extract_numerator_denominator(expr: &Expression) -> (Expression, Expression) {
200 match expr {
201 Expression::Mul(factors) => {
202 let mut numerator_factors = Vec::new();
203 let mut denominator_factors = Vec::new();
204
205 for factor in factors.iter() {
206 if let Expression::Pow(base, exp) = factor {
207 if let Expression::Number(Number::Integer(e)) = exp.as_ref() {
208 if *e < 0 {
209 let positive_exp = Expression::integer(-e);
210 denominator_factors
211 .push(Expression::pow((**base).clone(), positive_exp));
212 } else {
213 numerator_factors.push(factor.clone());
214 }
215 } else {
216 numerator_factors.push(factor.clone());
217 }
218 } else {
219 numerator_factors.push(factor.clone());
220 }
221 }
222
223 let numerator = if numerator_factors.is_empty() {
224 Expression::integer(1)
225 } else {
226 Expression::mul(numerator_factors)
227 };
228
229 let denominator = if denominator_factors.is_empty() {
230 Expression::integer(1)
231 } else {
232 Expression::mul(denominator_factors)
233 };
234
235 (numerator, denominator)
236 }
237 Expression::Pow(base, exp) => {
238 if let Expression::Number(Number::Integer(e)) = exp.as_ref() {
239 if *e < 0 {
240 (
241 Expression::integer(1),
242 Expression::pow((**base).clone(), Expression::integer(-e)),
243 )
244 } else {
245 (expr.clone(), Expression::integer(1))
246 }
247 } else {
248 (expr.clone(), Expression::integer(1))
249 }
250 }
251 _ => (expr.clone(), Expression::integer(1)),
252 }
253}
254
255/// Integrate rational function `P(x)/Q(x)` via partial fractions
256///
257/// # Arguments
258///
259/// * `expr` - Rational expression to integrate
260/// * `var` - Integration variable
261///
262/// # Returns
263///
264/// Integrated expression or `None` if not a rational function or unsupported
265///
266/// # Examples
267///
268/// ```
269/// use mathhook_core::{Expression, symbol};
270/// use mathhook_core::calculus::integrals::rational::integrate_rational;
271///
272/// let x = symbol!(x);
273/// let rational = Expression::mul(vec![
274/// Expression::integer(1),
275/// Expression::pow(
276/// Expression::add(vec![
277/// Expression::symbol(x.clone()),
278/// Expression::integer(-1),
279/// ]),
280/// Expression::integer(-1),
281/// ),
282/// ]);
283///
284/// let result = integrate_rational(&rational, &x);
285/// assert!(result.is_some());
286/// ```
287pub fn integrate_rational(expr: &Expression, var: &Symbol) -> Option<Expression> {
288 if !is_rational_function(expr, var) {
289 return None;
290 }
291
292 let (numerator, denominator) = extract_numerator_denominator(expr);
293
294 let num_degree = polynomial_degree(&numerator, var);
295 let den_degree = polynomial_degree(&denominator, var);
296
297 // Early return for simple constant/x^n cases ONLY if denominator is a pure monomial (just x^n)
298 // This prevents expensive partial fraction decomposition for trivial cases like 1/x^2
299 // BUT we must NOT early return for general polynomials like 1/(x^2 + 2x + 1)
300 let is_simple_monomial = match &denominator {
301 Expression::Symbol(_) => true,
302 Expression::Pow(base, _) => matches!(base.as_ref(), Expression::Symbol(_)),
303 _ => false,
304 };
305
306 if num_degree == 0 && den_degree >= 1 && is_simple_monomial {
307 // Numerator is constant, denominator is simple monomial: c/x^n pattern
308 // Let basic integration rules handle this via power rule
309 return None;
310 }
311
312 // Early return if denominator doesn't actually contain the variable
313 // This happens when expression is in wrong variable (e.g., contains 'x' but var is 'u')
314 // Return None to let other strategies handle it
315 if den_degree == 0 {
316 return None;
317 }
318
319 let (quotient, remainder) = if num_degree >= den_degree {
320 numerator.div_polynomial(&denominator, var)
321 } else {
322 (Expression::integer(0), numerator)
323 };
324
325 let polynomial_integral = if !quotient.is_zero() {
326 integrate_polynomial("ient, var)
327 } else {
328 Expression::integer(0)
329 };
330
331 if remainder.is_zero() {
332 return Some(polynomial_integral);
333 }
334
335 let factors = factor_simple_denominator(&denominator, var)?;
336
337 let mut result = polynomial_integral;
338
339 for factor in factors.iter() {
340 match factor {
341 Factor::Linear { root, power } => {
342 let factor_result =
343 integrate_linear_factor(&remainder, &denominator, root, *power, var)?;
344 result = Expression::add(vec![result, factor_result]).simplify();
345 }
346 Factor::Quadratic { p, q, power } => {
347 if *power == 1 {
348 let factor_result =
349 integrate_simple_quadratic(&remainder, &denominator, p, q, var)?;
350 result = Expression::add(vec![result, factor_result]).simplify();
351 } else {
352 let factor_result =
353 integrate_repeated_quadratic(&remainder, &denominator, p, q, *power, var)?;
354 result = Expression::add(vec![result, factor_result]).simplify();
355 }
356 }
357 }
358 }
359
360 Some(result.simplify())
361}
362
363fn integrate_polynomial(poly: &Expression, var: &Symbol) -> Expression {
364 match poly {
365 Expression::Number(_) => {
366 Expression::mul(vec![poly.clone(), Expression::symbol(var.clone())])
367 }
368 Expression::Symbol(s) if s == var => Expression::mul(vec![
369 Expression::rational(1, 2),
370 Expression::pow(Expression::symbol(var.clone()), Expression::integer(2)),
371 ]),
372 Expression::Pow(base, exp) => {
373 if let (Expression::Symbol(s), Expression::Number(Number::Integer(n))) =
374 (base.as_ref(), exp.as_ref())
375 {
376 if s == var {
377 let new_exp = n + 1;
378 return Expression::mul(vec![
379 Expression::rational(1, new_exp),
380 Expression::pow(
381 Expression::symbol(var.clone()),
382 Expression::integer(new_exp),
383 ),
384 ]);
385 }
386 }
387 Expression::mul(vec![poly.clone(), Expression::symbol(var.clone())])
388 }
389 Expression::Mul(factors) => {
390 let mut coeff = Expression::integer(1);
391 let mut var_power = 0i64;
392
393 for factor in factors.iter() {
394 if let Expression::Symbol(s) = factor {
395 if s == var {
396 var_power += 1;
397 continue;
398 }
399 }
400 if let Expression::Pow(base, exp) = factor {
401 if let (Expression::Symbol(s), Expression::Number(Number::Integer(e))) =
402 (base.as_ref(), exp.as_ref())
403 {
404 if s == var {
405 var_power += e;
406 continue;
407 }
408 }
409 }
410 coeff = Expression::mul(vec![coeff, factor.clone()]);
411 }
412
413 let new_power = var_power + 1;
414 Expression::mul(vec![
415 Expression::mul(vec![coeff, Expression::rational(1, new_power)]),
416 Expression::pow(
417 Expression::symbol(var.clone()),
418 Expression::integer(new_power),
419 ),
420 ])
421 }
422 Expression::Add(terms) => {
423 Expression::add(terms.iter().map(|t| integrate_polynomial(t, var)).collect())
424 }
425 _ => Expression::mul(vec![poly.clone(), Expression::symbol(var.clone())]),
426 }
427}
428
429fn factor_simple_denominator(denom: &Expression, var: &Symbol) -> Option<Vec<Factor>> {
430 let mut factors = Vec::new();
431
432 match denom {
433 Expression::Add(terms) => {
434 if terms.len() == 2 {
435 if let Expression::Symbol(s) = &terms[0] {
436 if s == var {
437 if let Expression::Number(_) = &terms[1] {
438 let root =
439 Expression::mul(vec![Expression::integer(-1), terms[1].clone()]);
440 factors.push(Factor::Linear { root, power: 1 });
441 return Some(factors);
442 }
443 }
444 }
445 }
446
447 if let Some((p, q)) = helpers::try_extract_quadratic(denom, var) {
448 // Check if this quadratic factors over the reals
449 // Discriminant Δ = p^2 - 4q determines factorability:
450 // Δ < 0: irreducible (complex roots)
451 // Δ = 0: perfect square (double root)
452 // Δ > 0: two distinct real roots
453
454 // For integer coefficients, check if discriminant is a perfect square
455 if let (
456 Expression::Number(Number::Integer(p_val)),
457 Expression::Number(Number::Integer(q_val)),
458 ) = (&p, &q)
459 {
460 let discriminant = p_val * p_val - 4 * q_val;
461
462 if discriminant == 0 {
463 let root = Expression::rational(-p_val, 2);
464 factors.push(Factor::Linear { root, power: 2 });
465 return Some(factors);
466 } else if discriminant > 0 {
467 // Check if discriminant is a perfect square (for rational roots)
468 let sqrt_disc = (discriminant as f64).sqrt();
469 if sqrt_disc.fract().abs() < EPSILON {
470 let sqrt_disc_int = sqrt_disc as i64;
471 let root1 = Expression::rational(-p_val + sqrt_disc_int, 2);
472 let root2 = Expression::rational(-p_val - sqrt_disc_int, 2);
473 factors.push(Factor::Linear {
474 root: root1,
475 power: 1,
476 });
477 factors.push(Factor::Linear {
478 root: root2,
479 power: 1,
480 });
481 return Some(factors);
482 }
483 }
484 }
485
486 // Irreducible quadratic (complex roots or irrational roots)
487 factors.push(Factor::Quadratic { p, q, power: 1 });
488 return Some(factors);
489 }
490
491 factors.push(Factor::Linear {
492 root: Expression::integer(0),
493 power: 1,
494 });
495 }
496 Expression::Mul(terms) => {
497 for term in terms.iter() {
498 if let Some(mut term_factors) = factor_simple_denominator(term, var) {
499 factors.append(&mut term_factors);
500 }
501 }
502 }
503 Expression::Pow(base, exp) => {
504 if let Expression::Number(Number::Integer(e)) = exp.as_ref() {
505 if let Some(base_factors) = factor_simple_denominator(base, var) {
506 for factor in base_factors {
507 match factor {
508 Factor::Linear { root, power } => {
509 factors.push(Factor::Linear {
510 root,
511 power: power * e,
512 });
513 }
514 Factor::Quadratic { p, q, power } => {
515 factors.push(Factor::Quadratic {
516 p,
517 q,
518 power: power * e,
519 });
520 }
521 }
522 }
523 return Some(factors);
524 }
525 }
526 }
527 Expression::Symbol(s) if s == var => {
528 factors.push(Factor::Linear {
529 root: Expression::integer(0),
530 power: 1,
531 });
532 }
533 _ => {
534 return None;
535 }
536 }
537
538 Some(factors)
539}