mathhook_core/calculus/integrals/risch/
rational.rs

1//! Rational function integration using Hermite reduction algorithm
2//!
3//! Implements the Hermite reduction method for integrating rational functions.
4//! This is a core component of the Risch algorithm for symbolic integration.
5//!
6//! # Mathematical Background
7//!
8//! For a rational function R(x) = P(x)/Q(x), the integral decomposes as:
9//!
10//! ∫ R(x) dx = polynomial_part + ∑ cᵢ ln|qᵢ(x)| + ∫ remaining_rational
11//!
12//! where:
13//! - polynomial_part comes from polynomial long division if deg(P) ≥ deg(Q)
14//! - logarithmic terms arise from square-free factorization of denominator
15//! - remaining_rational is a proper rational function with square-free denominator
16//!
17//! # Algorithm Steps
18//!
19//! 1. **Polynomial Division**: If deg(P) ≥ deg(Q), divide to get quotient + remainder
20//! 2. **Square-Free Factorization**: Factor Q = q₁·q₂²·q₃³·... into square-free parts
21//! 3. **Hermite Reduction**: Extract logarithmic terms using GCD operations
22//! 4. **Partial Fractions**: Decompose remaining rational part
23//!
24//! # References
25//!
26//! - Bronstein, M. (2005). *Symbolic Integration I: Transcendental Functions*
27//! - Geddes, K. et al. (1992). *Algorithms for Computer Algebra*
28
29use crate::algebra::gcd::PolynomialGcd;
30use crate::algebra::polynomial_division::polynomial_div;
31use crate::calculus::derivatives::Derivative;
32use crate::core::{Expression, Symbol};
33use crate::simplify::Simplify;
34
35/// Result of rational function integration
36#[derive(Debug, Clone, PartialEq)]
37pub struct RationalIntegral {
38    /// Polynomial part from long division
39    pub polynomial_part: Expression,
40
41    /// Logarithmic terms: ∑ cᵢ ln|qᵢ(x)|
42    pub logarithmic_terms: Vec<(Expression, Expression)>,
43
44    /// Remaining rational part (if any)
45    pub remaining: Option<Expression>,
46}
47
48/// Assemble a RationalIntegral into a single Expression
49pub fn assemble_integral(integral: &RationalIntegral) -> Expression {
50    let mut terms = vec![integral.polynomial_part.clone()];
51
52    for (coeff, arg) in &integral.logarithmic_terms {
53        terms.push(Expression::mul(vec![
54            coeff.clone(),
55            Expression::function("ln", vec![Expression::function("abs", vec![arg.clone()])]),
56        ]));
57    }
58
59    if let Some(remaining) = &integral.remaining {
60        terms.push(Expression::function(
61            "integrate",
62            vec![remaining.clone(), Expression::symbol(Symbol::scalar("x"))],
63        ));
64    }
65
66    Expression::add(terms)
67}
68
69/// Integrate a rational function P(x)/Q(x)
70///
71/// Implements Hermite reduction algorithm for complete rational function integration.
72///
73/// # Arguments
74///
75/// * `numerator` - Polynomial P(x)
76/// * `denominator` - Polynomial Q(x) (must be non-zero)
77/// * `var` - Variable of integration
78///
79/// # Algorithm
80///
81/// 1. If deg(P) ≥ deg(Q): Perform polynomial long division
82///    - P/Q = quotient + remainder/Q
83///    - Integrate quotient using power rule
84/// 2. Apply Hermite reduction to remainder/Q:
85///    - Compute D = gcd(Q, Q') where Q' is derivative
86///    - Use extended GCD to extract logarithmic terms
87///    - Separate into algebraic + logarithmic parts
88/// 3. Return complete integral
89///
90/// # Examples
91///
92/// ```rust
93/// use mathhook_core::calculus::integrals::risch::rational::integrate_rational;
94/// use mathhook_core::{expr, symbol};
95///
96/// let x = symbol!(x);
97/// let num = expr!((x^2) + 1);
98/// let den = expr!(x - 1);
99/// let result = integrate_rational(&num, &den, &x);
100/// ```
101///
102/// # Returns
103///
104/// Returns `RationalIntegral` containing:
105/// - `polynomial_part`: Result of integrating quotient
106/// - `logarithmic_terms`: List of (coefficient, argument) for ln terms
107/// - `remaining`: Any unintegrated rational part (None if complete)
108pub fn integrate_rational(
109    numerator: &Expression,
110    denominator: &Expression,
111    var: &Symbol,
112) -> RationalIntegral {
113    if denominator.is_zero() {
114        return RationalIntegral {
115            polynomial_part: Expression::undefined(),
116            logarithmic_terms: vec![],
117            remaining: None,
118        };
119    }
120
121    if numerator.is_zero() {
122        return RationalIntegral {
123            polynomial_part: Expression::integer(0),
124            logarithmic_terms: vec![],
125            remaining: None,
126        };
127    }
128
129    let (quotient, remainder) = match polynomial_div(numerator, denominator, var) {
130        Ok(result) => result,
131        Err(_) => {
132            return RationalIntegral {
133                polynomial_part: Expression::integer(0),
134                logarithmic_terms: vec![],
135                remaining: Some(Expression::mul(vec![
136                    numerator.clone(),
137                    Expression::pow(denominator.clone(), Expression::integer(-1)),
138                ])),
139            }
140        }
141    };
142
143    let polynomial_part = integrate_polynomial(&quotient, var);
144
145    if remainder.is_zero() {
146        return RationalIntegral {
147            polynomial_part,
148            logarithmic_terms: vec![],
149            remaining: None,
150        };
151    }
152
153    let (log_terms, remaining_rational) = hermite_reduce(&remainder, denominator, var);
154
155    RationalIntegral {
156        polynomial_part,
157        logarithmic_terms: log_terms,
158        remaining: remaining_rational,
159    }
160}
161
162/// Integrate polynomial using power rule
163///
164/// For polynomial p(x) = ∑ aᵢ xⁱ, returns ∑ aᵢ xⁱ⁺¹/(i+1)
165fn integrate_polynomial(poly: &Expression, var: &Symbol) -> Expression {
166    match poly {
167        Expression::Number(n) => Expression::mul(vec![
168            Expression::Number(n.clone()),
169            Expression::symbol(var.clone()),
170        ]),
171        Expression::Symbol(s) if s == var => Expression::mul(vec![
172            Expression::rational(1, 2),
173            Expression::pow(Expression::symbol(var.clone()), Expression::integer(2)),
174        ]),
175        Expression::Pow(base, exp) => {
176            if let Expression::Symbol(s) = &**base {
177                if s == var {
178                    if let Expression::Number(num_exp) = &**exp {
179                        let new_exp = Expression::add(vec![
180                            Expression::Number(num_exp.clone()),
181                            Expression::integer(1),
182                        ]);
183                        return Expression::mul(vec![
184                            Expression::pow(new_exp.clone(), Expression::integer(-1)),
185                            Expression::pow(Expression::symbol(var.clone()), new_exp),
186                        ]);
187                    }
188                }
189            }
190            Expression::integral(poly.clone(), var.clone())
191        }
192        Expression::Add(terms) => {
193            Expression::add(terms.iter().map(|t| integrate_polynomial(t, var)).collect())
194        }
195        Expression::Mul(factors) => {
196            let (constants, variables): (Vec<_>, Vec<_>) =
197                factors.iter().partition(|f| !f.contains_variable(var));
198
199            if variables.is_empty() {
200                Expression::mul(vec![
201                    Expression::mul((**factors).clone()),
202                    Expression::symbol(var.clone()),
203                ])
204            } else if variables.len() == 1 {
205                let integrated_var = integrate_polynomial(variables[0], var);
206                if constants.is_empty() {
207                    integrated_var
208                } else {
209                    Expression::mul(vec![
210                        Expression::mul(constants.into_iter().cloned().collect()),
211                        integrated_var,
212                    ])
213                }
214            } else {
215                Expression::integral(poly.clone(), var.clone())
216            }
217        }
218        _ => Expression::integral(poly.clone(), var.clone()),
219    }
220}
221
222/// Hermite reduction for rational function integration
223///
224/// Reduces ∫ P/Q dx where Q is not square-free to:
225/// - Logarithmic part: ∑ cᵢ ln|qᵢ|
226/// - Remaining rational part with square-free denominator
227///
228/// # Algorithm
229///
230/// 1. Compute D = gcd(Q, Q')
231/// 2. Split Q = D·S where S is square-free
232/// 3. Use extended GCD to find A, B such that: P = A·D + B·Q'
233/// 4. Then: ∫ P/Q = -A/D·S + ∫ (B + A')/S
234fn hermite_reduce(
235    numerator: &Expression,
236    denominator: &Expression,
237    var: &Symbol,
238) -> (Vec<(Expression, Expression)>, Option<Expression>) {
239    let denom_deriv = denominator.derivative(var.clone()).simplify();
240
241    let gcd = PolynomialGcd::gcd(denominator, &denom_deriv);
242
243    if gcd.is_one() {
244        let log_terms = extract_logarithmic_terms(numerator, denominator, var);
245        return (log_terms, None);
246    }
247
248    let square_free_part = divide_polynomials(denominator, &gcd, var);
249
250    let algebraic_part = Expression::mul(vec![
251        Expression::integer(-1),
252        numerator.clone(),
253        Expression::pow(gcd.clone(), Expression::integer(-1)),
254        Expression::pow(square_free_part.clone(), Expression::integer(-1)),
255    ])
256    .simplify();
257
258    let numerator_deriv = numerator.derivative(var.clone());
259    let remaining_num = Expression::add(vec![
260        numerator_deriv,
261        Expression::mul(vec![
262            numerator.clone(),
263            Expression::pow(gcd, Expression::integer(-1)),
264            denom_deriv,
265        ]),
266    ])
267    .simplify();
268
269    let log_terms = extract_logarithmic_terms(&remaining_num, &square_free_part, var);
270
271    let remaining = if algebraic_part.is_zero() {
272        None
273    } else {
274        Some(algebraic_part)
275    };
276
277    (log_terms, remaining)
278}
279
280/// Extract logarithmic terms from a proper rational function
281///
282/// For P/Q where deg(P) < deg(Q) and Q is square-free,
283/// attempts to find coefficients cᵢ such that:
284/// P/Q = ∑ cᵢ · (qᵢ'/qᵢ)
285///
286/// Returns list of (coefficient, argument) pairs for ln terms
287fn extract_logarithmic_terms(
288    numerator: &Expression,
289    denominator: &Expression,
290    var: &Symbol,
291) -> Vec<(Expression, Expression)> {
292    if denominator.is_one() || numerator.is_zero() {
293        return vec![];
294    }
295
296    let denom_deriv = denominator.derivative(var.clone()).simplify();
297    let gcd = PolynomialGcd::gcd(denominator, &denom_deriv);
298
299    if !gcd.is_one() {
300        return vec![];
301    }
302
303    let coefficient = divide_polynomials(numerator, &denom_deriv, var);
304
305    if is_constant(&coefficient, var) {
306        vec![(coefficient, denominator.clone())]
307    } else {
308        vec![]
309    }
310}
311
312/// Divide two polynomial expressions
313///
314/// Returns quotient if division is exact, otherwise returns 0
315fn divide_polynomials(dividend: &Expression, divisor: &Expression, var: &Symbol) -> Expression {
316    if divisor.is_zero() {
317        return Expression::undefined();
318    }
319
320    if dividend.is_zero() {
321        return Expression::integer(0);
322    }
323
324    if divisor.is_one() {
325        return dividend.clone();
326    }
327
328    match polynomial_div(dividend, divisor, var) {
329        Ok((quotient, remainder)) => {
330            if remainder.is_zero() {
331                quotient
332            } else {
333                Expression::mul(vec![
334                    dividend.clone(),
335                    Expression::pow(divisor.clone(), Expression::integer(-1)),
336                ])
337            }
338        }
339        Err(_) => Expression::mul(vec![
340            dividend.clone(),
341            Expression::pow(divisor.clone(), Expression::integer(-1)),
342        ]),
343    }
344}
345
346/// Check if expression is constant with respect to variable
347fn is_constant(expr: &Expression, var: &Symbol) -> bool {
348    !expr.contains_variable(var)
349}