use super::DiffTelescopingError;
use crate::holonomic::hyperterm::{affine_parts, as_ratk, ratk_to_expr};
use crate::holonomic::qfield::{ratk_deriv_k, rn_add, rn_int, rn_mul, rn_rat, rn_var, RatK};
use crate::kernel::{Domain, ExprData, ExprId, ExprPool};
use rug::Rational;
const MAX_POW: i32 = 32;
const MAX_SHIFT_POW: i64 = 512;
const MAX_PARSE_DEPTH: usize = 64;
#[derive(Clone, Debug)]
pub struct PowerFactor {
pub base: RatK,
pub alpha: i64,
pub beta: Rational,
}
#[derive(Clone, Debug)]
pub struct HyperExpTerm {
pub rat: RatK,
pub w: Rational,
pub eta: RatK,
pub powers: Vec<PowerFactor>,
}
impl HyperExpTerm {
fn one() -> Self {
HyperExpTerm {
rat: RatK::one(),
w: Rational::from(1),
eta: RatK::zero(),
powers: Vec::new(),
}
}
fn from_ratk(r: RatK) -> Self {
HyperExpTerm {
rat: r,
w: Rational::from(1),
eta: RatK::zero(),
powers: Vec::new(),
}
}
fn mul(&self, other: &HyperExpTerm) -> HyperExpTerm {
let mut powers = self.powers.clone();
powers.extend(other.powers.iter().cloned());
HyperExpTerm {
rat: self.rat.mul(&other.rat),
w: self.w.clone() * other.w.clone(),
eta: self.eta.add(&other.eta),
powers,
}
}
fn pow(&self, e: i32) -> Result<HyperExpTerm, DiffTelescopingError> {
if e.unsigned_abs() > MAX_POW as u32 {
return Err(DiffTelescopingError::NotHyperexponential(format!(
"exponent {e} is outside the supported range (|e| ≤ {MAX_POW})"
)));
}
let rat = self.rat.pow_i32(e).ok_or_else(|| {
DiffTelescopingError::NotHyperexponential(
"a factor raised to a negative power is identically zero".into(),
)
})?;
let scale = RatK::from_rn(rn_int(e as i64));
let powers = self
.powers
.iter()
.map(|p| {
Ok(PowerFactor {
base: p.base.clone(),
alpha: p.alpha.checked_mul(e as i64).ok_or_else(|| {
DiffTelescopingError::NotHyperexponential(
"exponent overflow in a power factor".into(),
)
})?,
beta: p.beta.clone() * Rational::from(e),
})
})
.collect::<Result<Vec<_>, DiffTelescopingError>>()?;
Ok(HyperExpTerm {
rat,
w: rat_pow(&self.w, e as i64)?,
eta: self.eta.mul(&scale),
powers,
})
}
pub fn theta(&self) -> Result<RatK, DiffTelescopingError> {
if self.rat.is_zero() {
return Err(DiffTelescopingError::NotHyperexponential(
"the term is identically zero, so F'/F is undefined".into(),
));
}
let mut acc = log_deriv(&self.rat)?;
acc = acc.add(&ratk_deriv_k(&self.eta));
for p in &self.powers {
let e = RatK::from_rn(rn_add(
&rn_mul(&rn_int(p.alpha), &rn_var()),
&rn_rat(p.beta.clone()),
));
acc = acc.add(&e.mul(&log_deriv(&p.base)?));
}
Ok(acc)
}
pub fn ratio_n(&self, i: i64) -> Result<RatK, DiffTelescopingError> {
if i == 0 {
return Ok(RatK::one());
}
if self.rat.is_zero() {
return Err(DiffTelescopingError::NotHyperexponential(
"the term is identically zero".into(),
));
}
if !self.eta.shift_n(i).eq_ratk(&self.eta) {
return Err(DiffTelescopingError::NotHypergeometricInN(
"the argument of exp depends on n, so F(n+i,x)/F(n,x) contains \
exp(eta(n+i,x) - eta(n,x)), which is not a rational function of x"
.into(),
));
}
let mut acc = self.rat.shift_n(i).div(&self.rat).ok_or_else(|| {
DiffTelescopingError::NotHyperexponential("the term is identically zero".into())
})?;
acc = acc.mul(&RatK::from_rn(rn_rat(rat_pow(&self.w, i)?)));
for p in &self.powers {
if !p.base.shift_n(i).eq_ratk(&p.base) {
return Err(DiffTelescopingError::NotHypergeometricInN(
"the base of a symbolic power depends on n, so the n-shift ratio is not \
a rational function of x"
.into(),
));
}
let shift = p.alpha.checked_mul(i).ok_or_else(|| {
DiffTelescopingError::NotHypergeometricInN("exponent overflow".into())
})?;
if shift == 0 {
continue;
}
if shift.abs() > MAX_SHIFT_POW {
return Err(DiffTelescopingError::SearchExhausted(format!(
"n-shift exponent {shift} exceeds the supported limit of {MAX_SHIFT_POW}"
)));
}
let step = p.base.pow_i32(shift as i32).ok_or_else(|| {
DiffTelescopingError::NotHyperexponential(
"a power factor's base is identically zero".into(),
)
})?;
acc = acc.mul(&step);
}
Ok(acc)
}
pub fn parse(
expr: ExprId,
n: ExprId,
x: ExprId,
pool: &ExprPool,
) -> Result<HyperExpTerm, DiffTelescopingError> {
if n == x {
return Err(DiffTelescopingError::InvalidInput(
"the outer index n and the integration variable x must be distinct symbols".into(),
));
}
parse_rec(expr, n, x, pool, 0)
}
}
fn log_deriv(p: &RatK) -> Result<RatK, DiffTelescopingError> {
ratk_deriv_k(p).div(p).ok_or_else(|| {
DiffTelescopingError::NotHyperexponential(
"a factor of the term is identically zero, so its logarithmic derivative is \
undefined"
.into(),
)
})
}
fn rat_pow(q: &Rational, e: i64) -> Result<Rational, DiffTelescopingError> {
if e == 0 {
return Ok(Rational::from(1));
}
if e.unsigned_abs() > 1024 {
return Err(DiffTelescopingError::NotHyperexponential(
"exponential factor exponent exceeds the supported limit".into(),
));
}
if *q == 0 {
if e < 0 {
return Err(DiffTelescopingError::NotHyperexponential(
"zero base raised to a negative power".into(),
));
}
return Ok(Rational::from(0));
}
let base = if e < 0 { q.clone().recip() } else { q.clone() };
let mut acc = Rational::from(1);
for _ in 0..e.unsigned_abs() {
acc *= base.clone();
}
Ok(acc)
}
fn parse_rec(
expr: ExprId,
n: ExprId,
x: ExprId,
pool: &ExprPool,
depth: usize,
) -> Result<HyperExpTerm, DiffTelescopingError> {
if depth > MAX_PARSE_DEPTH {
return Err(DiffTelescopingError::NotHyperexponential(
"expression nests deeper than the parser supports".into(),
));
}
if let Some(r) = as_ratk(expr, n, x, pool, 0) {
return Ok(HyperExpTerm::from_ratk(r));
}
match pool.get(expr) {
ExprData::Mul(args) => {
let mut acc = HyperExpTerm::one();
for a in args {
acc = acc.mul(&parse_rec(a, n, x, pool, depth + 1)?);
}
Ok(acc)
}
ExprData::Pow { base, exp } => parse_pow(base, exp, n, x, pool, depth),
ExprData::Func { name, args } => parse_func(&name, &args, n, x, pool, depth),
ExprData::Add(_) => Err(DiffTelescopingError::NotHyperexponential(format!(
"a sum of hyperexponential terms is not itself hyperexponential (unless it is \
rational, which {} is not)",
pool.display(expr)
))),
other => Err(DiffTelescopingError::NotHyperexponential(format!(
"unsupported node {other:?} in {}",
pool.display(expr)
))),
}
}
fn parse_pow(
base: ExprId,
exp: ExprId,
n: ExprId,
x: ExprId,
pool: &ExprPool,
depth: usize,
) -> Result<HyperExpTerm, DiffTelescopingError> {
if let ExprData::Integer(i) = pool.get(exp) {
let Some(e) = i.0.to_i32() else {
return Err(DiffTelescopingError::NotHyperexponential(
"integer exponent does not fit in i32".into(),
));
};
return parse_rec(base, n, x, pool, depth + 1)?.pow(e);
}
let (alpha, x_coeff, beta) = affine_parts(exp, n, x, pool).ok_or_else(|| {
DiffTelescopingError::NotHyperexponential(format!(
"exponent {} is not of the form a*n + b with integer a and rational b",
pool.display(exp)
))
})?;
if x_coeff != 0 {
return Err(DiffTelescopingError::NotHyperexponential(format!(
"exponent {} depends on the integration variable; c**x is hyperexponential only \
over a field containing log(c), which Q(n)(x) is not",
pool.display(exp)
)));
}
if alpha == 0 {
if let Some(b) = rational_to_i32(&beta) {
let inner = parse_rec(base, n, x, pool, depth + 1)?;
return inner.pow(b);
}
}
let b = as_ratk(base, n, x, pool, 0).ok_or_else(|| {
DiffTelescopingError::NotHyperexponential(format!(
"a symbolic power needs a rational-function base, got {}",
pool.display(base)
))
})?;
if b.is_zero() {
return Err(DiffTelescopingError::NotHyperexponential(
"zero raised to a symbolic power".into(),
));
}
if let Some(c) = ratk_as_rational(&b) {
let bi = rational_to_i32(&beta).ok_or_else(|| {
DiffTelescopingError::NotHyperexponential(
"a rational base raised to a non-integer constant power leaves Q (e.g. 2**(1/2))"
.into(),
)
})?;
return Ok(HyperExpTerm {
rat: RatK::from_rn(rn_rat(rat_pow(&c, bi as i64)?)),
w: rat_pow(&c, alpha)?,
eta: RatK::zero(),
powers: Vec::new(),
});
}
Ok(HyperExpTerm {
rat: RatK::one(),
w: Rational::from(1),
eta: RatK::zero(),
powers: vec![PowerFactor {
base: b,
alpha,
beta,
}],
})
}
fn parse_func(
name: &str,
args: &[ExprId],
n: ExprId,
x: ExprId,
pool: &ExprPool,
depth: usize,
) -> Result<HyperExpTerm, DiffTelescopingError> {
match (name, args.len()) {
("exp", 1) => {
let eta = as_ratk(args[0], n, x, pool, 0).ok_or_else(|| {
DiffTelescopingError::NotHyperexponential(format!(
"exp(...) is hyperexponential only when its argument is a rational \
function of the indices; got exp({})",
pool.display(args[0])
))
})?;
Ok(HyperExpTerm {
rat: RatK::one(),
w: Rational::from(1),
eta,
powers: Vec::new(),
})
}
("sqrt", 1) => {
let inner = parse_rec(args[0], n, x, pool, depth + 1)?;
let mut out = HyperExpTerm {
rat: RatK::one(),
w: Rational::from(1),
eta: inner
.eta
.mul(&RatK::from_rn(rn_rat(Rational::from((1, 2))))),
powers: inner
.powers
.iter()
.map(|p| {
if p.alpha % 2 != 0 {
return Err(DiffTelescopingError::NotHyperexponential(
"sqrt of a factor with an odd n-exponent leaves the class \
(its n-shift ratio would be a square root)"
.into(),
));
}
Ok(PowerFactor {
base: p.base.clone(),
alpha: p.alpha / 2,
beta: p.beta.clone() / Rational::from(2),
})
})
.collect::<Result<Vec<_>, _>>()?,
};
if inner.w != 1 {
return Err(DiffTelescopingError::NotHyperexponential(
"sqrt of a w**n factor leaves Q".into(),
));
}
if !inner.rat.eq_ratk(&RatK::one()) {
out.powers.push(PowerFactor {
base: inner.rat.clone(),
alpha: 0,
beta: Rational::from((1, 2)),
});
}
Ok(out)
}
_ => Err(DiffTelescopingError::NotHyperexponential(format!(
"function `{name}/{}` is not part of the hyperexponential class \
(supported heads: exp, sqrt)",
args.len()
))),
}
}
fn rational_to_i32(q: &Rational) -> Option<i32> {
if *q.clone().denom() != 1 {
return None;
}
q.numer().to_i32()
}
fn ratk_as_rational(r: &RatK) -> Option<Rational> {
if r.num.degree() > 0 || r.den.degree() > 0 {
return None;
}
let num = r.num.coeff(0);
let den = r.den.coeff(0);
let q = crate::holonomic::qfield::rn_div(&num, &den)?;
if q.num.degree() > 0 || q.den.degree() > 0 {
return None;
}
let a = q.num.coeffs.first().cloned()?;
let b = q.den.coeffs.first().cloned()?;
if b == 0 {
return None;
}
Some(a / b)
}
pub fn hyperexp_log_derivative(
f: ExprId,
x: ExprId,
pool: &ExprPool,
) -> Result<ExprId, DiffTelescopingError> {
let n = fresh_outer_index(pool);
let term = HyperExpTerm::parse(f, n, x, pool)?;
let theta = term.theta()?;
Ok(ratk_to_expr(pool, n, x, &theta))
}
pub(super) fn fresh_outer_index(pool: &ExprPool) -> ExprId {
pool.symbol("__azeil_no_outer_index", Domain::Real)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::holonomic::hyperterm::ratk_to_expr;
use crate::kernel::Domain;
fn nx(pool: &ExprPool) -> (ExprId, ExprId) {
(
pool.symbol("n", Domain::Real),
pool.symbol("x", Domain::Real),
)
}
fn theta_str(expr: ExprId, pool: &ExprPool) -> String {
let (n, x) = nx(pool);
let t = HyperExpTerm::parse(expr, n, x, pool)
.expect("term parses")
.theta()
.expect("theta exists");
format!("{}", pool.display(ratk_to_expr(pool, n, x, &t)))
}
fn assert_theta_eq(expr: ExprId, expected: ExprId, pool: &ExprPool) {
let (n, x) = nx(pool);
let t = HyperExpTerm::parse(expr, n, x, pool)
.expect("term parses")
.theta()
.expect("theta exists");
let want = as_ratk(expected, n, x, pool, 0).expect("expected value is rational");
assert!(
t.eq_ratk(&want),
"theta mismatch: got {}, want {}",
pool.display(ratk_to_expr(pool, n, x, &t)),
pool.display(expected)
);
}
#[test]
fn power_of_x_with_rational_exponent() {
let pool = ExprPool::new();
let (_, x) = nx(&pool);
let f = pool.pow(x, pool.rational(3, 2));
let expected = pool.mul(vec![pool.rational(3, 2), pool.pow(x, pool.integer(-1_i32))]);
assert_theta_eq(f, expected, &pool);
}
#[test]
fn exp_of_a_polynomial() {
let pool = ExprPool::new();
let (_, x) = nx(&pool);
let arg = pool.add(vec![
pool.pow(x, pool.integer(2_i32)),
pool.mul(vec![pool.integer(-3_i32), x]),
]);
let f = pool.func("exp", vec![arg]);
let expected = pool.add(vec![
pool.mul(vec![pool.integer(2_i32), x]),
pool.integer(-3_i32),
]);
assert_theta_eq(f, expected, &pool);
}
#[test]
fn one_minus_x_to_the_n() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let base = pool.add(vec![
pool.integer(1_i32),
pool.mul(vec![pool.integer(-1_i32), x]),
]);
let f = pool.pow(base, n);
let expected = pool.mul(vec![
pool.integer(-1_i32),
n,
pool.pow(base, pool.integer(-1_i32)),
]);
assert_theta_eq(f, expected, &pool);
}
#[test]
fn x_to_the_n_times_exp_minus_x() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let f = pool.mul(vec![
pool.pow(x, n),
pool.func("exp", vec![pool.mul(vec![pool.integer(-1_i32), x])]),
]);
let expected = pool.add(vec![
pool.mul(vec![n, pool.pow(x, pool.integer(-1_i32))]),
pool.integer(-1_i32),
]);
assert_theta_eq(f, expected, &pool);
let term = HyperExpTerm::parse(f, n, x, &pool).expect("parses");
let r1 = term.ratio_n(1).expect("hypergeometric in n");
assert!(r1.eq_ratk(&RatK::k()), "F(n+1,x)/F(n,x) must be x");
}
#[test]
fn gaussian_moment_term() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let f = pool.mul(vec![
pool.pow(x, pool.mul(vec![pool.integer(2_i32), n])),
pool.func(
"exp",
vec![pool.mul(vec![pool.integer(-1_i32), pool.pow(x, pool.integer(2_i32))])],
),
]);
let expected = pool.add(vec![
pool.mul(vec![
pool.integer(2_i32),
n,
pool.pow(x, pool.integer(-1_i32)),
]),
pool.mul(vec![pool.integer(-2_i32), x]),
]);
assert_theta_eq(f, expected, &pool);
let term = HyperExpTerm::parse(f, n, x, &pool).expect("parses");
let r1 = term.ratio_n(1).expect("hypergeometric in n");
let want = RatK::k().mul(&RatK::k());
assert!(r1.eq_ratk(&want), "F(n+1,x)/F(n,x) must be x^2");
}
#[test]
fn rational_multiple_and_product() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let one_minus = pool.add(vec![
pool.integer(1_i32),
pool.mul(vec![pool.integer(-1_i32), x]),
]);
let f = pool.mul(vec![
x,
pool.pow(pool.add(vec![x, pool.integer(1_i32)]), pool.integer(-1_i32)),
pool.pow(x, n),
pool.pow(one_minus, n),
]);
let expected = pool.add(vec![
pool.pow(x, pool.integer(-1_i32)),
pool.mul(vec![
pool.integer(-1_i32),
pool.pow(pool.add(vec![x, pool.integer(1_i32)]), pool.integer(-1_i32)),
]),
pool.mul(vec![n, pool.pow(x, pool.integer(-1_i32))]),
pool.mul(vec![
pool.integer(-1_i32),
n,
pool.pow(one_minus, pool.integer(-1_i32)),
]),
]);
assert_theta_eq(f, expected, &pool);
}
#[test]
fn w_to_the_n_factor() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let f = pool.mul(vec![pool.pow(pool.integer(2_i32), n), pool.pow(x, n)]);
let expected = pool.mul(vec![n, pool.pow(x, pool.integer(-1_i32))]);
assert_theta_eq(f, expected, &pool);
let term = HyperExpTerm::parse(f, n, x, &pool).expect("parses");
let r1 = term.ratio_n(1).expect("hypergeometric in n");
let want = RatK::k().mul(&RatK::from_rn(rn_int(2)));
assert!(r1.eq_ratk(&want), "F(n+1,x)/F(n,x) must be 2x");
}
#[test]
fn sqrt_is_a_half_power() {
let pool = ExprPool::new();
let (_, x) = nx(&pool);
let inner = pool.add(vec![
pool.integer(1_i32),
pool.mul(vec![pool.integer(-1_i32), pool.pow(x, pool.integer(2_i32))]),
]);
let f = pool.func("sqrt", vec![inner]);
let expected = pool.mul(vec![
pool.integer(-1_i32),
x,
pool.pow(inner, pool.integer(-1_i32)),
]);
assert_theta_eq(f, expected, &pool);
}
#[test]
fn univariate_entry_point() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let f = pool.mul(vec![x, pool.func("exp", vec![x])]);
let theta = hyperexp_log_derivative(f, x, &pool).expect("hyperexponential");
let s = format!("{}", pool.display(theta));
assert!(s.contains('x'), "log derivative should mention x, got {s}");
}
#[test]
fn log_is_refused() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let f = pool.func("log", vec![x]);
let err = HyperExpTerm::parse(f, n, x, &pool).expect_err("log is not hyperexponential");
assert!(matches!(err, DiffTelescopingError::NotHyperexponential(_)));
}
#[test]
fn sum_of_hyperexponentials_is_refused() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let f = pool.add(vec![
pool.func("exp", vec![x]),
pool.func("exp", vec![pool.mul(vec![pool.integer(2_i32), x])]),
]);
let err = HyperExpTerm::parse(f, n, x, &pool).expect_err("a sum is refused");
assert!(matches!(err, DiffTelescopingError::NotHyperexponential(_)));
}
#[test]
fn x_in_the_exponent_of_a_constant_is_refused() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let f = pool.pow(pool.integer(2_i32), x);
let err = HyperExpTerm::parse(f, n, x, &pool).expect_err("2**x is refused");
assert!(matches!(err, DiffTelescopingError::NotHyperexponential(_)));
}
#[test]
fn exp_of_n_times_x_is_hyperexponential_but_not_hypergeometric_in_n() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let f = pool.func("exp", vec![pool.mul(vec![n, x])]);
let term = HyperExpTerm::parse(f, n, x, &pool).expect("hyperexponential in x");
let theta = term.theta().expect("theta exists");
assert!(
theta.eq_ratk(&RatK::from_rn(rn_var())),
"theta must be n, got {}",
pool.display(ratk_to_expr(&pool, n, x, &theta))
);
let err = term
.ratio_n(1)
.expect_err("ratio in n is exp(x), not rational");
assert!(matches!(err, DiffTelescopingError::NotHypergeometricInN(_)));
}
#[test]
fn sin_is_refused() {
let pool = ExprPool::new();
let (n, x) = nx(&pool);
let f = pool.func("sin", vec![x]);
let err = HyperExpTerm::parse(f, n, x, &pool).expect_err("sin is not hyperexponential");
assert!(matches!(err, DiffTelescopingError::NotHyperexponential(_)));
}
#[test]
fn identical_symbols_are_refused() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let err = HyperExpTerm::parse(x, x, x, &pool).expect_err("n and x must differ");
assert!(matches!(err, DiffTelescopingError::InvalidInput(_)));
}
#[test]
fn theta_of_a_rational_function_is_its_log_derivative() {
let pool = ExprPool::new();
let (_, x) = nx(&pool);
let f = pool.pow(pool.add(vec![x, pool.integer(1_i32)]), pool.integer(3_i32));
let expected = pool.mul(vec![
pool.integer(3_i32),
pool.pow(pool.add(vec![x, pool.integer(1_i32)]), pool.integer(-1_i32)),
]);
assert_theta_eq(f, expected, &pool);
let _ = theta_str(f, &pool);
}
}