use crate::deriv::log::{DerivationLog, DerivedExpr, RewriteStep};
use crate::integrate::engine::{integrate_raw, IntegrationError};
use crate::kernel::{ExprId, ExprPool};
use crate::simplify::engine::simplify;
use super::alg_field::AlgExtension;
use super::exp_case::{build_rational, decompose_over_alg_generator, detect_radical_generator};
use super::poly_rde::{
degree, poly_deriv, poly_mul, poly_one, poly_scale, qpoly_to_expr, trim, QPoly,
};
use super::rational_integrate::yun;
use super::rational_rde::{poly_gcd, poly_pow, poly_sub, solve_rational_rde_generalized};
pub fn try_integrate_simple_radical(
expr: ExprId,
var: ExprId,
pool: &ExprPool,
) -> Option<Result<DerivedExpr<ExprId>, IntegrationError>> {
let (n, p) = detect_radical_generator(expr, var, pool)?;
if n < 3 {
return None; }
let p = trim(p);
if degree(&p) < 1 {
return None;
}
let p_prime = poly_deriv(&p);
if degree(&poly_gcd(&p, &p_prime)) >= 1 {
return integrate_general_radical(expr, n, &p, var, pool);
}
let e = AlgExtension::radical(n, &p);
let elem = decompose_over_alg_generator(expr, n, &p, &e, var, pool)?;
let mut log = DerivationLog::new();
let mut terms: Vec<ExprId> = Vec::new();
for j in 0..n {
let (num, den) = match elem.get(j) {
Some(r) => (r.numer().clone(), r.denom().clone()),
None => (QPoly::new(), vec![rug::Rational::from(1)]),
};
if trim(num.clone()).is_empty() {
continue; }
if j == 0 {
let b0 = build_rational(&num, &den, var, pool);
match integrate_raw(b0, var, pool, &mut log) {
Ok(v0) => terms.push(v0),
Err(err) => return Some(Err(err)),
}
} else {
let f_num = poly_scale(&p_prime, &rug::Rational::from(j as i64));
let f_den = poly_scale(&p, &rug::Rational::from(n as i64));
let (vn, vd) = match solve_rational_rde_generalized(&f_num, &f_den, &num, &den) {
Some(sol) => sol,
None => return Some(Err(non_elementary(expr, pool))),
};
if trim(vn.clone()).is_empty() {
continue;
}
let v_expr = build_rational(&vn, &vd, var, pool);
let p_expr = qpoly_to_expr(&p, var, pool);
let yj = pool.pow(p_expr, pool.rational(j as i32, n as i32));
terms.push(pool.mul(vec![v_expr, yj]));
}
}
let raw = match terms.len() {
0 => pool.integer(0_i32),
1 => terms[0],
_ => pool.add(terms),
};
let simplified = simplify(raw, pool);
log = log.merge(simplified.log);
log.push(RewriteStep::simple(
"simple_radical_risch",
expr,
simplified.value,
));
Some(Ok(DerivedExpr::with_log(simplified.value, log)))
}
fn integrate_general_radical(
expr: ExprId,
n: usize,
a: &QPoly,
var: ExprId,
pool: &ExprPool,
) -> Option<Result<DerivedExpr<ExprId>, IntegrationError>> {
if a.last().map(|c| *c != 1).unwrap_or(true) {
return None;
}
let a_factors = yun(a)?;
let mut big_f = poly_one();
let mut big_h = poly_one();
for (ai, i) in &a_factors {
let q = i / n;
let r = i % n;
if q > 0 {
big_f = poly_mul(&big_f, &poly_pow(ai, q as u32));
}
if r > 0 {
big_h = poly_mul(&big_h, &poly_pow(ai, r as u32));
}
}
let h_factors = yun(&big_h)?;
let dk = |k: usize| -> QPoly {
let mut d = poly_one();
for (hj, j) in &h_factors {
let e = (k * j) / n;
if e > 0 {
d = poly_mul(&d, &poly_pow(hj, e as u32));
}
}
d
};
let e = AlgExtension::radical(n, a);
let elem = decompose_over_alg_generator(expr, n, a, &e, var, pool)?;
let h_prime = poly_deriv(&big_h);
let a_expr = qpoly_to_expr(a, var, pool);
let mut log = DerivationLog::new();
let mut terms: Vec<ExprId> = Vec::new();
for k in 0..n {
let (c_num, c_den) = match elem.get(k) {
Some(r) => (r.numer().clone(), r.denom().clone()),
None => (QPoly::new(), poly_one()),
};
if trim(c_num.clone()).is_empty() {
continue;
}
let d_k = dk(k);
let f_pow_k = poly_pow(&big_f, k as u32);
let a_num = poly_mul(&poly_mul(&c_num, &f_pow_k), &d_k);
let a_den = c_den;
let v = if k == 0 {
let a0 = build_rational(&a_num, &a_den, var, pool);
match integrate_raw(a0, var, pool, &mut log) {
Ok(v0) => terms.push(v0),
Err(err) => return Some(Err(err)),
}
continue;
} else {
let d_k_prime = poly_deriv(&d_k);
let f_num = poly_sub(
&poly_scale(&poly_mul(&h_prime, &d_k), &rug::Rational::from(k as i64)),
&poly_scale(
&poly_mul(&big_h, &d_k_prime),
&rug::Rational::from(n as i64),
),
);
let f_den = poly_scale(&poly_mul(&big_h, &d_k), &rug::Rational::from(n as i64));
match solve_rational_rde_generalized(&f_num, &f_den, &a_num, &a_den) {
Some(sol) => sol,
None => return Some(Err(non_elementary(expr, pool))),
}
};
let (vn, vd) = v;
if trim(vn.clone()).is_empty() {
continue;
}
let denom = poly_mul(&vd, &poly_mul(&f_pow_k, &d_k));
let coeff = build_rational(&vn, &denom, var, pool);
let yk = pool.pow(a_expr, pool.rational(k as i32, n as i32));
terms.push(pool.mul(vec![coeff, yk]));
}
let raw = match terms.len() {
0 => pool.integer(0_i32),
1 => terms[0],
_ => pool.add(terms),
};
let simplified = simplify(raw, pool);
log = log.merge(simplified.log);
log.push(RewriteStep::simple(
"simple_radical_risch_general",
expr,
simplified.value,
));
Some(Ok(DerivedExpr::with_log(simplified.value, log)))
}
fn non_elementary(expr: ExprId, pool: &ExprPool) -> IntegrationError {
IntegrationError::NonElementary(format!(
"∫ {} dx: the Risch differential equation over ℚ(x) for a simple-radical \
component has no rational solution",
pool.display(expr),
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::{Domain, ExprData};
fn pool() -> ExprPool {
ExprPool::new()
}
fn eval(expr: ExprId, x: ExprId, xv: f64, pool: &ExprPool) -> f64 {
if expr == x {
return xv;
}
match pool.get(expr) {
ExprData::Integer(n) => n.0.to_f64(),
ExprData::Rational(r) => r.0.to_f64(),
ExprData::Add(args) => args.iter().map(|&a| eval(a, x, xv, pool)).sum(),
ExprData::Mul(args) => args.iter().map(|&a| eval(a, x, xv, pool)).product(),
ExprData::Pow { base, exp } => eval(base, x, xv, pool).powf(eval(exp, x, xv, pool)),
ExprData::Func { ref name, ref args } if args.len() == 1 => {
let a = eval(args[0], x, xv, pool);
match name.as_str() {
"cbrt" => a.cbrt(),
"sqrt" => a.sqrt(),
"log" => a.ln(),
"exp" => a.exp(),
other => panic!("eval: unsupported func {other}"),
}
}
other => panic!("eval: unsupported node {other:?}"),
}
}
fn verify(integrand: ExprId, x: ExprId, pool: &ExprPool) {
let res = try_integrate_simple_radical(integrand, x, pool)
.expect("recognized as a simple radical")
.expect("should be elementary");
let f = res.value;
let df = crate::diff::diff(f, x, pool).expect("F is differentiable");
let ds = simplify(df.value, pool).value;
for &xv in &[0.6_f64, 1.4, 2.7] {
let lhs = eval(ds, x, xv, pool);
let rhs = eval(integrand, x, xv, pool);
assert!(
(lhs - rhs).abs() < 1e-9,
"d/dx F ≠ f at x={xv}: {lhs} vs {rhs}\n F = {}",
pool.display(f)
);
}
}
#[test]
fn integral_of_cbrt_x() {
let pool = pool();
let x = pool.symbol("x", Domain::Real);
verify(pool.func("cbrt", vec![x]), x, &pool);
}
#[test]
fn integral_of_x_times_cbrt_x() {
let pool = pool();
let x = pool.symbol("x", Domain::Real);
let integrand = pool.mul(vec![x, pool.func("cbrt", vec![x])]);
verify(integrand, x, &pool);
}
#[test]
fn integral_of_x_pow_two_thirds() {
let pool = pool();
let x = pool.symbol("x", Domain::Real);
let integrand = pool.pow(x, pool.rational(2_i32, 3_i32));
verify(integrand, x, &pool);
}
#[test]
fn integral_of_cbrt_x_over_x() {
let pool = pool();
let x = pool.symbol("x", Domain::Real);
let integrand = pool.mul(vec![
pool.func("cbrt", vec![x]),
pool.pow(x, pool.integer(-1_i32)),
]);
verify(integrand, x, &pool);
}
#[test]
fn fifth_root_integral() {
let pool = pool();
let x = pool.symbol("x", Domain::Real);
let integrand = pool.pow(x, pool.rational(2_i32, 5_i32));
verify(integrand, x, &pool);
}
#[test]
fn cbrt_over_x2_plus_1_is_nonelementary() {
let pool = pool();
let x = pool.symbol("x", Domain::Real);
let x2p1 = pool.add(vec![pool.pow(x, pool.integer(2_i32)), pool.integer(1_i32)]);
let integrand = pool.mul(vec![
pool.func("cbrt", vec![x]),
pool.pow(x2p1, pool.integer(-1_i32)),
]);
let res = try_integrate_simple_radical(integrand, x, &pool).expect("recognized");
assert!(
matches!(res, Err(IntegrationError::NonElementary(_))),
"expected NonElementary, got {res:?}"
);
}
#[test]
fn routed_through_algebraic_engine() {
let pool = pool();
let x = pool.symbol("x", Domain::Real);
let res =
crate::integrate::algebraic::integrate_algebraic(pool.func("cbrt", vec![x]), x, &pool)
.expect("∫∛x dx is elementary");
let f = res.value;
let h = 1e-6;
let dnum = (eval(f, x, 1.4 + h, &pool) - eval(f, x, 1.4 - h, &pool)) / (2.0 * h);
assert!(
(dnum - 1.4_f64.cbrt()).abs() < 1e-4,
"F = {}",
pool.display(f)
);
}
#[test]
fn cbrt_func_routes_through_public_engine() {
let pool = pool();
let x = pool.symbol("x", Domain::Real);
let f = pool.func("cbrt", vec![pool.pow(x, pool.integer(2_i32))]);
let res = crate::integrate::engine::integrate(f, x, &pool)
.expect("∫cbrt(x²) dx via public engine");
let g = res.value;
let h = 1e-6;
let dnum = (eval(g, x, 1.4 + h, &pool) - eval(g, x, 1.4 - h, &pool)) / (2.0 * h);
assert!(
(dnum - 1.4_f64.powf(2.0 / 3.0)).abs() < 1e-4,
"F = {}",
pool.display(g)
);
}
#[test]
fn cbrt_of_constant_still_rule_engine() {
let pool = pool();
let x = pool.symbol("x", Domain::Real);
let f = pool.func("cbrt", vec![pool.integer(5_i32)]);
let res =
crate::integrate::engine::integrate(f, x, &pool).expect("∫cbrt(5) dx = cbrt(5)·x");
let g = res.value;
let h = 1e-6;
let dnum = (eval(g, x, 2.0 + h, &pool) - eval(g, x, 2.0 - h, &pool)) / (2.0 * h);
assert!(
(dnum - 5.0_f64.cbrt()).abs() < 1e-4,
"F = {}",
pool.display(g)
);
}
#[test]
fn degree_two_returns_none() {
let pool = pool();
let x = pool.symbol("x", Domain::Real);
assert!(try_integrate_simple_radical(pool.func("sqrt", vec![x]), x, &pool).is_none());
}
#[test]
fn integral_of_cbrt_x_squared_general_basis() {
let pool = pool();
let x = pool.symbol("x", Domain::Real);
let cbrt_x2 = pool.func("cbrt", vec![pool.pow(x, pool.integer(2_i32))]);
verify(cbrt_x2, x, &pool);
}
#[test]
fn integral_using_basis_denominator() {
let pool = pool();
let x = pool.symbol("x", Domain::Real);
let cbrt_x2 = pool.func("cbrt", vec![pool.pow(x, pool.integer(2_i32))]);
let integrand = pool.mul(vec![
pool.pow(cbrt_x2, pool.integer(2_i32)),
pool.pow(x, pool.integer(-1_i32)),
]);
verify(integrand, x, &pool);
}
#[test]
fn integral_of_cbrt_linear_squared() {
let pool = pool();
let x = pool.symbol("x", Domain::Real);
let rad = pool.add(vec![
pool.pow(x, pool.integer(2_i32)),
pool.mul(vec![pool.integer(2_i32), x]),
pool.integer(1_i32),
]);
verify(pool.func("cbrt", vec![rad]), x, &pool);
}
#[test]
fn non_monic_non_squarefree_returns_none() {
let pool = pool();
let x = pool.symbol("x", Domain::Real);
let two_x = pool.mul(vec![pool.integer(2_i32), x]);
let rad = pool.pow(two_x, pool.integer(2_i32)); let cbrt = pool.func("cbrt", vec![rad]);
assert!(try_integrate_simple_radical(cbrt, x, &pool).is_none());
}
}