use std::collections::HashMap;
use crate::deriv::log::{DerivationLog, DerivedExpr, RewriteStep};
use crate::integrate::risch::poly_rde::is_free_of_var;
use crate::kernel::{Domain, ExprData, ExprId, ExprPool};
use crate::simplify::engine::simplify;
const U_NAME: &str = "$pullback_u$";
const MAX_K: i64 = 12;
pub(super) fn try_power_pullback(
expr: ExprId,
var: ExprId,
pool: &ExprPool,
) -> Option<DerivedExpr<ExprId>> {
let u = pool.symbol(U_NAME, Domain::Real);
if var == u {
return None; }
if !has_variable_radical(expr, var, pool) {
return None;
}
for k in (2..=MAX_K).rev() {
let Some((g, r)) = split(expr, var, u, k, pool) else {
continue;
};
if r != k - 1 || !is_free_of_var(g, var, pool) {
continue;
}
let g = simplify(g, pool).value;
if is_free_of_var(g, u, pool) && !is_free_of_var(expr, var, pool) {
continue; }
let Ok(inner) = crate::integrate::engine::integrate(g, u, pool) else {
continue;
};
let mut map = HashMap::new();
map.insert(u, pool.pow(var, pool.integer(k as i32)));
let back = crate::kernel::subs::subs(inner.value, &map, pool);
let scaled = pool.mul(vec![
pool.pow(pool.integer(k as i32), pool.integer(-1_i32)),
back,
]);
let f = simplify(scaled, pool);
if !verify(f.value, expr, var, pool) {
continue;
}
let mut log = DerivationLog::new();
log = log.merge(inner.log);
log = log.merge(f.log.clone());
log.push(RewriteStep::simple("alg_power_pullback", expr, f.value));
return Some(DerivedExpr::with_log(f.value, log));
}
None
}
fn split(expr: ExprId, var: ExprId, u: ExprId, k: i64, pool: &ExprPool) -> Option<(ExprId, i64)> {
let one = pool.integer(1_i32);
if expr == var {
return Some((one, 1));
}
if is_free_of_var(expr, var, pool) {
return Some((expr, 0));
}
match pool.get(expr) {
ExprData::Add(args) => {
let mut parts = Vec::with_capacity(args.len());
let mut deg: Option<i64> = None;
for a in args.iter() {
let (g, r) = split(*a, var, u, k, pool)?;
if *deg.get_or_insert(r) != r {
return None; }
parts.push(g);
}
Some((pool.add(parts), deg.unwrap_or(0)))
}
ExprData::Mul(args) => {
let mut parts = Vec::with_capacity(args.len() + 1);
let mut total = 0_i64;
for a in args.iter() {
let (g, r) = split(*a, var, u, k, pool)?;
parts.push(g);
total = total.checked_add(r)?;
}
let (q, r) = divmod(total, k);
if q != 0 {
parts.push(pool.pow(u, pool.integer(i32::try_from(q).ok()?)));
}
Some((pool.mul(parts), r))
}
ExprData::Pow { base, exp } => {
let (g, rb) = split(base, var, u, k, pool)?;
match pool.get(exp) {
ExprData::Integer(n) => {
let n = n.0.to_i64()?;
let total = rb.checked_mul(n)?;
let (q, r) = divmod(total, k);
let mut parts = vec![pool.pow(g, exp)];
if q != 0 {
parts.push(pool.pow(u, pool.integer(i32::try_from(q).ok()?)));
}
Some((pool.mul(parts), r))
}
_ if rb == 0 => Some((pool.pow(g, exp), 0)),
_ => None,
}
}
ExprData::Func { ref name, ref args } => {
let mut parts = Vec::with_capacity(args.len());
for a in args.iter() {
let (g, r) = split(*a, var, u, k, pool)?;
if r != 0 {
return None; }
parts.push(g);
}
Some((pool.func(name.clone(), parts), 0))
}
_ => None,
}
}
fn has_variable_radical(expr: ExprId, var: ExprId, pool: &ExprPool) -> bool {
if is_free_of_var(expr, var, pool) {
return false;
}
match pool.get(expr) {
ExprData::Func { ref name, ref args }
if (name == "sqrt" || name == "cbrt") && args.len() == 1 =>
{
if !is_free_of_var(args[0], var, pool) {
return true;
}
}
ExprData::Pow { base, exp } => {
if let ExprData::Rational(r) = pool.get(exp) {
if *r.0.denom() != 1 && !is_free_of_var(base, var, pool) {
return true;
}
}
}
_ => {}
}
match pool.get(expr) {
ExprData::Add(args) | ExprData::Mul(args) | ExprData::Func { args, .. } => {
args.iter().any(|&c| has_variable_radical(c, var, pool))
}
ExprData::Pow { base, exp } => {
has_variable_radical(base, var, pool) || has_variable_radical(exp, var, pool)
}
_ => false,
}
}
fn divmod(n: i64, k: i64) -> (i64, i64) {
let mut q = n / k;
let mut r = n % k;
if r < 0 {
r += k;
q -= 1;
}
(q, r)
}
fn verify(f: ExprId, integrand: ExprId, var: ExprId, pool: &ExprPool) -> bool {
let Ok(df) = crate::diff::diff(f, var, pool) else {
return false;
};
let d = simplify(df.value, pool).value;
let mut checked = 0;
for &xv in &[
0.13_f64, 0.29, 0.41, 0.57, 0.68, 0.83, 1.15, 1.4, 1.7, 2.3, 3.1, 4.2,
] {
let mut env = HashMap::new();
env.insert(var, xv);
let (Some(lhs), Some(rhs)) = (
crate::jit::eval_interp(d, &env, pool),
crate::jit::eval_interp(integrand, &env, pool),
) else {
continue;
};
if !lhs.is_finite() || !rhs.is_finite() {
continue;
}
if (lhs - rhs).abs() > 1e-7 * (1.0 + rhs.abs()) {
return false;
}
checked += 1;
}
checked >= 3
}
#[cfg(test)]
mod tests {
use super::*;
use crate::integrate::engine::{integrate, IntegrationError};
fn parse(src: &str, pool: &ExprPool) -> ExprId {
let mut syms = HashMap::new();
let x = pool.symbol("x", Domain::Real);
syms.insert("x".to_string(), x);
crate::parse::parse(src, pool, &mut syms).expect("parse")
}
fn assert_antiderivative(src: &str, samples: &[f64]) -> String {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let f = parse(src, &pool);
let res = integrate(f, x, &pool).unwrap_or_else(|e| panic!("{src}: {e:?}"));
let d = simplify(crate::diff::diff(res.value, x, &pool).unwrap().value, &pool).value;
let mut checked = 0;
for &xv in samples {
let mut env = HashMap::new();
env.insert(x, xv);
let (Some(lhs), Some(rhs)) = (
crate::jit::eval_interp(d, &env, &pool),
crate::jit::eval_interp(f, &env, &pool),
) else {
continue;
};
assert!(
(lhs - rhs).abs() < 1e-7 * (1.0 + rhs.abs()),
"{src}: d/dx F = {lhs} ≠ {rhs} = f at x = {xv}"
);
checked += 1;
}
assert!(checked >= 3, "{src}: only {checked} samples evaluated");
pool.display(res.value).to_string()
}
#[test]
fn asin_pullback_family() {
for (k, src) in [
(2, "x/sqrt(1-x^4)"),
(3, "x^2/sqrt(1-x^6)"),
(4, "x^3/sqrt(1-x^8)"),
] {
let out = assert_antiderivative(src, &[0.2, 0.4, 0.6, 0.75]);
assert!(!out.is_empty(), "k = {k}");
}
}
#[test]
fn asin_pullback_scaled() {
assert_antiderivative("x/sqrt(4-x^4)", &[0.2, 0.5, 0.9, 1.2]);
assert_antiderivative("x/sqrt(9-x^4)", &[0.3, 0.7, 1.1, 1.5]);
}
#[test]
fn refuses_a_constant_radicand() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let f = parse("1/(x*sqrt(2))", &pool);
assert!(!has_variable_radical(f, x, &pool));
assert!(try_power_pullback(f, x, &pool).is_none());
}
#[test]
fn asinh_pullback() {
assert_antiderivative("x/sqrt(1+x^4)", &[0.2, 0.7, 1.3, 2.1]);
assert_antiderivative("x^2/sqrt(1+x^6)", &[0.2, 0.7, 1.3, 2.1]);
}
#[test]
fn second_kind_pullback() {
assert_antiderivative("x*sqrt(1-x^4)", &[0.2, 0.4, 0.6, 0.8]);
assert_antiderivative("x*sqrt(1+x^4)", &[0.2, 0.9, 1.4, 2.2]);
assert_antiderivative("x^3*sqrt(1-x^8)", &[0.2, 0.4, 0.6, 0.8]);
}
#[test]
fn split_recognises_and_refuses() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let u = pool.symbol(U_NAME, Domain::Real);
let f = parse("x/sqrt(1-x^4)", &pool);
let (g, r) = split(f, x, u, 2, &pool).expect("k = 2 splits");
assert_eq!(r, 1);
assert!(is_free_of_var(g, x, &pool));
assert_eq!(split(f, x, u, 4, &pool).map(|(_, r)| r), Some(1));
let bad = parse("x/sqrt(1-x^3)", &pool);
assert_eq!(split(bad, x, u, 2, &pool).map(|(_, r)| r), None);
let mixed = parse("x + x^2", &pool);
assert!(split(mixed, x, u, 2, &pool).is_none());
}
#[test]
fn split_handles_negative_exponents() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let u = pool.symbol(U_NAME, Domain::Real);
let f = parse("1/x", &pool);
let (g, r) = split(f, x, u, 2, &pool).expect("splits");
assert_eq!(r, 1);
assert!(is_free_of_var(g, x, &pool));
}
#[test]
fn refuses_to_nest() {
let pool = ExprPool::new();
let u = pool.symbol(U_NAME, Domain::Real);
let f = pool.mul(vec![u, pool.pow(u, pool.integer(2_i32))]);
assert!(try_power_pullback(f, u, &pool).is_none());
}
#[test]
fn does_not_override_a_certificate() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let f = parse("x/sqrt(1-x^6)", &pool);
let res = integrate(f, x, &pool);
assert!(
matches!(res, Err(IntegrationError::NonElementary(_))),
"∫x dx/√(1−x⁶) must stay NonElementary; got {res:?}"
);
}
}