use super::hyperexp::{fresh_outer_index, HyperExpTerm};
use super::rde::{solve_probe, verify, RdeSetup};
use super::{AzOpts, DiffTelescopingError};
use crate::deriv::log::{DerivationLog, DerivedExpr, RewriteStep};
use crate::holonomic::hyperterm::ratk_to_expr;
use crate::holonomic::qfield::RatK;
use crate::kernel::{ExprId, ExprPool};
pub fn dgosper(
f: ExprId,
x: ExprId,
pool: &ExprPool,
opts: &AzOpts,
) -> Result<DerivedExpr<ExprId>, DiffTelescopingError> {
let n = fresh_outer_index(pool);
if n == x {
return Err(DiffTelescopingError::InvalidInput(
"the integration variable collides with the internal outer index".into(),
));
}
let term = HyperExpTerm::parse(f, n, x, pool)?;
let r = dgosper_term(&term, opts)?;
let expr = ratk_to_expr(pool, n, x, &r);
let mut log = DerivationLog::new();
log.push(RewriteStep::simple("dgosper_certificate", f, expr));
Ok(DerivedExpr::with_log(expr, log))
}
pub fn dgosper_term(term: &HyperExpTerm, opts: &AzOpts) -> Result<RatK, DiffTelescopingError> {
let theta = term.theta()?;
let setup = RdeSetup::new(&theta, vec![RatK::one()]).ok_or_else(|| {
DiffTelescopingError::SearchExhausted(
"the logarithmic derivative has a degenerate denominator".into(),
)
})?;
let mut saw_candidate = false;
for kappa in 0..=opts.max_den_power {
for d in 0..=opts.max_degree {
let Some(cand) = solve_probe(&setup, kappa, d) else {
continue;
};
saw_candidate = true;
if verify(&setup, &cand.a, &cand.r) {
return Ok(cand.r);
}
}
}
Err(if saw_candidate {
DiffTelescopingError::CertificateVerificationFailed(
"every candidate R found by the ansatz search failed the exact R' + theta*R = 1 \
check in Q(n)(x)"
.into(),
)
} else {
DiffTelescopingError::SearchExhausted(format!(
"no rational R with (R*F)' = F was found with deg R numerator <= {} and \
certificate denominator power <= {}; this is a limit of the search, not a proof \
that no elementary antiderivative exists",
opts.max_degree, opts.max_den_power
))
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::holonomic::azeil::hyperexp::hyperexp_log_derivative;
use crate::holonomic::hyperterm::as_ratk;
use crate::kernel::Domain;
fn pool_x() -> (ExprPool, ExprId) {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
(pool, x)
}
fn assert_is_antiderivative(f: ExprId, r: ExprId, x: ExprId, pool: &ExprPool) {
let n = fresh_outer_index(pool);
let theta_expr = hyperexp_log_derivative(f, x, pool).expect("hyperexponential");
let theta = as_ratk(theta_expr, n, x, pool, 0).expect("theta is rational");
let rr = as_ratk(r, n, x, pool, 0).expect("R is rational");
let lhs = crate::holonomic::qfield::ratk_deriv_k(&rr).add(&theta.mul(&rr));
assert!(
lhs.eq_ratk(&RatK::one()),
"R' + theta*R must be 1; got {} for R = {}",
pool.display(ratk_to_expr(pool, n, x, &lhs)),
pool.display(r)
);
}
#[test]
fn exp_is_its_own_antiderivative() {
let (pool, x) = pool_x();
let f = pool.func("exp", vec![x]);
let r = dgosper(f, x, &pool, &AzOpts::default()).expect("R = 1");
assert_is_antiderivative(f, r.value, x, &pool);
}
#[test]
fn x_times_exp_x() {
let (pool, x) = pool_x();
let f = pool.mul(vec![x, pool.func("exp", vec![x])]);
let r = dgosper(f, x, &pool, &AzOpts::default()).expect("certificate");
assert_is_antiderivative(f, r.value, x, &pool);
}
#[test]
fn x_squared_times_exp_x_needs_a_double_pole() {
let (pool, x) = pool_x();
let f = pool.mul(vec![
pool.pow(x, pool.integer(2_i32)),
pool.func("exp", vec![x]),
]);
let r = dgosper(f, x, &pool, &AzOpts::default()).expect("certificate");
assert_is_antiderivative(f, r.value, x, &pool);
}
#[test]
fn gaussian_derivative() {
let (pool, x) = pool_x();
let f = pool.mul(vec![
x,
pool.func(
"exp",
vec![pool.mul(vec![pool.integer(-1_i32), pool.pow(x, pool.integer(2_i32))])],
),
]);
let r = dgosper(f, x, &pool, &AzOpts::default()).expect("certificate");
assert_is_antiderivative(f, r.value, x, &pool);
}
#[test]
fn rational_power() {
let (pool, x) = pool_x();
let f = pool.pow(x, pool.rational(-1, 2));
let r = dgosper(f, x, &pool, &AzOpts::default()).expect("certificate");
assert_is_antiderivative(f, r.value, x, &pool);
}
#[test]
fn a_polynomial_is_hyperexponential_too() {
let (pool, x) = pool_x();
let base = pool.add(vec![x, pool.integer(1_i32)]);
let f = pool.pow(base, pool.integer(3_i32));
let r = dgosper(f, x, &pool, &AzOpts::default()).expect("certificate");
assert_is_antiderivative(f, r.value, x, &pool);
}
#[test]
fn exp_over_x_has_no_hyperexponential_antiderivative() {
let (pool, x) = pool_x();
let f = pool.mul(vec![
pool.func("exp", vec![x]),
pool.pow(x, pool.integer(-1_i32)),
]);
let err = dgosper(f, x, &pool, &AzOpts::default()).expect_err("Ei is not R*F");
assert!(
matches!(err, DiffTelescopingError::SearchExhausted(_)),
"expected a search refusal, got {err:?}"
);
}
#[test]
fn erf_has_no_hyperexponential_antiderivative() {
let (pool, x) = pool_x();
let f = pool.func("exp", vec![pool.pow(x, pool.integer(2_i32))]);
let err = dgosper(f, x, &pool, &AzOpts::default()).expect_err("erfi is not R*F");
assert!(matches!(err, DiffTelescopingError::SearchExhausted(_)));
}
#[test]
fn non_hyperexponential_input_is_refused_cleanly() {
let (pool, x) = pool_x();
let f = pool.func("log", vec![x]);
let err = dgosper(f, x, &pool, &AzOpts::default()).expect_err("log is out of class");
assert!(matches!(err, DiffTelescopingError::NotHyperexponential(_)));
}
#[test]
fn certificate_is_exact_for_a_rational_integrand() {
let (pool, x) = pool_x();
let f = pool.pow(x, pool.integer(-2_i32));
let r = dgosper(f, x, &pool, &AzOpts::default()).expect("certificate");
assert_is_antiderivative(f, r.value, x, &pool);
}
}