use ocas_atom::normalize::normalize;
use ocas_atom::{Atom, AtomArena, Symbol};
use ocas_domain::{Domain, Rational, RationalDomain};
use ocas_poly::DenseUnivariatePolynomial;
use ocas_rewrite::rules::default_rules;
use ocas_rewrite::simplify::simplify;
use super::rational::integrate_dpoly_structured;
use super::rde::rde_solve;
use crate::rules::calculus_rules;
use crate::tower::build::{GenKind, Tower, build_tower, tower_diff_kpoly};
use crate::tower::convert::{
GeneratorField, atom_to_rational_extended, rational_const_to_atom, rational_to_atom,
};
use crate::tower::elem::{KElem, KPoly, KRat};
type DPoly = DenseUnivariatePolynomial<RationalDomain>;
pub(crate) fn risch_integrate<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
) -> Option<Atom<'a>> {
let tower = build_tower(ctx, expr, var)?;
let level = tower.gens.len();
let rf = atom_to_rational_extended(expr, &tower.gen_atoms(), tower.n_vars())?;
let f = KRat::new(
KPoly::from_sparse(&rf.numerator, level),
KPoly::from_sparse(&rf.denominator, level),
);
let result = integrate_level(ctx, &tower, level, &f)?;
let raw = level_result_to_atom(ctx, &tower, level, &result)?;
let calc_rules = calculus_rules(ctx, &crate::pattern_alloc::VecAlloc);
let default_rules = default_rules(ctx, &crate::pattern_alloc::VecAlloc);
let after_default = simplify(ctx, raw, &default_rules, 20);
let after_calc = simplify(ctx, after_default, &calc_rules, 10);
Some(normalize(ctx, after_calc))
}
pub(crate) struct LevelResult<'a> {
pub rational: KRat,
pub poly: KPoly,
pub constant: KElem,
pub logs: Vec<(Rational, KElem)>,
pub extras: Vec<Atom<'a>>,
}
impl<'a> LevelResult<'a> {
fn empty(top: usize, n: usize) -> Self {
Self {
rational: KRat::new(KPoly::zero(top, n), KPoly::one(top, n)),
poly: KPoly::zero(top, n),
constant: KElem::zero(n),
logs: Vec::new(),
extras: Vec::new(),
}
}
fn elem_part(&self) -> KElem {
self.rational
.kelem()
.add(&self.poly.kelem())
.add(&self.constant)
}
}
fn integrate_level<'a>(
ctx: &'a AtomArena<'a>,
tower: &Tower<'a>,
level: usize,
f: &KRat,
) -> Option<LevelResult<'a>> {
if level == 0 {
return integrate_level_base(ctx, tower, f);
}
let top = level;
let n = tower.n_vars();
let tgen = &tower.gens[level - 1];
let (p, r) = f.num.div_rem(&f.den);
let (g, a1, d1) = hermite_tower(tower, level, &r, &f.den);
let mut out = LevelResult::empty(top, n);
out.rational = g;
if !a1.is_zero() {
let dd1 = tower_diff_kpoly(&d1, &tower.gens[..level - 1], &tgen.dt);
if let Some(c) = kpoly_scalar_multiple(&a1, &dd1) {
out.logs.push((c, d1.kelem()));
} else {
out.extras
.push(integral_fallback_atom(ctx, tower, level, &a1, &d1)?);
}
}
if !p.is_zero() {
let pk = integrate_kpoly(ctx, tower, level, &p);
if pk.is_none() {
eprintln!("DEBUG risch: integrate_kpoly(level={level}) returned None");
}
let (poly_ans, const_ans, logs_p, extras_p) = pk?;
out.poly = poly_ans;
out.constant = const_ans;
out.logs.extend(logs_p);
out.extras.extend(extras_p);
}
Some(out)
}
fn hermite_tower(tower: &Tower, level: usize, a: &KPoly, d: &KPoly) -> (KRat, KPoly, KPoly) {
let top = level;
let n = d.n_vars;
let factors = d.square_free();
let m = factors.iter().map(|&(_, k)| k).max().unwrap_or(1);
if m <= 1 {
return (
KRat::new(KPoly::zero(top, n), KPoly::one(top, n)),
a.clone(),
d.clone(),
);
}
let (v, _) = factors
.iter()
.rev()
.find(|&(_, k)| *k == m)
.expect("max multiplicity factor exists");
let vm = kpoly_pow(v, m as u64);
let (u, rem) = d.div_rem(&vm);
debug_assert!(rem.is_zero());
let tgen = &tower.gens[level - 1];
let dv = tower_diff_kpoly(v, &tower.gens[..level - 1], &tgen.dt);
let b = u.mul(&dv);
let (g0, s0, _t0) = b.eea(v);
debug_assert!(g0.is_one());
let (_, s) = a.mul(&s0).div_rem(v);
let (t, rem) = a.sub(&s.mul(&b)).div_rem(v);
debug_assert!(rem.is_zero());
let inv_m1 = Rational::new(1, (m - 1) as i64);
let s_scaled = s.mul_kelem(&KElem::from_rational(&inv_m1, n));
let vm1 = kpoly_pow(v, (m - 1) as u64);
let ds = tower_diff_kpoly(&s_scaled, &tower.gens[..level - 1], &tgen.dt);
let new_a = t.add(&u.mul(&ds));
let new_d = u.mul(&vm1);
let (g2, a1, d1) = hermite_tower(tower, level, &new_a, &new_d);
let g_term = KRat::new(s_scaled.neg(), vm1);
(krat_add(&g_term, &g2), a1, d1)
}
fn kpoly_pow(p: &KPoly, mut k: u64) -> KPoly {
let mut result = KPoly::one(p.top, p.n_vars);
let mut base = p.clone();
while k > 0 {
if k & 1 == 1 {
result = result.mul(&base);
}
base = base.mul(&base);
k >>= 1;
}
result
}
fn krat_add(a: &KRat, b: &KRat) -> KRat {
if a.num.is_zero() {
return b.clone();
}
if b.num.is_zero() {
return a.clone();
}
KRat::new(a.num.mul(&b.den).add(&b.num.mul(&a.den)), a.den.mul(&b.den))
}
fn kpoly_scalar_multiple(p: &KPoly, q: &KPoly) -> Option<Rational> {
if q.is_zero() {
return None;
}
if p.degree() != q.degree() {
return None;
}
let dom = RationalDomain;
let c = kelem_to_rational(&p.lc().div(&q.lc())?)?;
for i in 0..=p.degree()? {
let pc = p.coeff_at(i);
let qc = q.coeff_at(i).mul_rational(&c);
if !pc.eq_cross(&qc) {
return None;
}
}
let _ = dom;
Some(c)
}
fn kelem_to_rational(e: &KElem) -> Option<Rational> {
e.as_rational()
}
type KPolyIntegral<'a> = (KPoly, KElem, Vec<(Rational, KElem)>, Vec<Atom<'a>>);
fn integrate_kpoly<'a>(
ctx: &'a AtomArena<'a>,
tower: &Tower<'a>,
level: usize,
p: &KPoly,
) -> Option<KPolyIntegral<'a>> {
let top = level;
let n = tower.n_vars();
if p.is_zero() {
return Some((KPoly::zero(top, n), KElem::zero(n), Vec::new(), Vec::new()));
}
if level == 0 {
let dp = kpoly_to_dpoly(p)?;
let int = super::rational::poly_integrate(&dp);
return Some((
dpoly_to_kpoly(&int, top, n),
KElem::zero(n),
Vec::new(),
Vec::new(),
));
}
let tgen = &tower.gens[level - 1];
let m = p.degree()?;
match tgen.kind {
GenKind::Constant | GenKind::Log => integrate_kpoly_primitive(ctx, tower, level, p, m),
GenKind::Exp => integrate_kpoly_hyperexp(ctx, tower, level, p, m),
}
}
fn integrate_kpoly_primitive<'a>(
ctx: &'a AtomArena<'a>,
tower: &Tower<'a>,
level: usize,
p: &KPoly,
m: usize,
) -> Option<KPolyIntegral<'a>> {
let top = level;
let n = tower.n_vars();
let tgen = &tower.gens[level - 1];
let dt = &tgen.dt;
let prefix = tower.gen_atoms()[..level].to_vec();
let u_rf = atom_to_rational_extended(tgen.arg, &prefix, n)?;
let u_k = KElem::new(u_rf.numerator, u_rf.denominator);
let mut logs: Vec<(Rational, KElem)> = Vec::new();
let mut extras: Vec<Atom> = Vec::new();
let mut q = vec![KElem::zero(n); m + 2];
let int_pm = integrate_kelem_or_fallback(ctx, tower, level - 1, p.coeff_at(m))?;
let int_dt = integrate_kelem_or_fallback(ctx, tower, level - 1, dt.clone())?;
let (w_elem, w_logs, w_extras) = split_result(&int_dt);
if !w_elem.is_zero()
|| !w_extras.is_empty()
|| w_logs.len() != 1
|| !RationalDomain.is_one(&w_logs[0].0)
|| !w_logs[0].1.eq_cross(&u_k)
{
return None;
}
let (a_elem, a_logs, a_extras) = split_result(&int_pm);
let mut c = RationalDomain.zero();
let mut found = false;
for (d, v) in a_logs {
if v.eq_cross(&u_k) {
if found {
return None;
}
found = true;
c = RationalDomain.div(&d, &Rational::new((m + 1) as i64, 1))?;
} else {
logs.push((d, v));
}
}
extras.extend(a_extras);
q[m + 1] = KElem::from_rational(&c, n);
q[m] = a_elem;
for i in (0..m).rev() {
let h = p.coeff_at(i).sub(
&q[i + 1]
.mul_rational(&Rational::new((i + 1) as i64, 1))
.mul(dt),
);
let int_h = integrate_kelem_or_fallback(ctx, tower, level - 1, h)?;
let (e, ls, es) = split_result(&int_h);
q[i] = e;
for (d, v) in ls {
if v.eq_cross(&u_k) {
return None;
}
logs.push((d, v));
}
extras.extend(es);
}
Some((
KPoly {
top,
coeffs: q,
n_vars: n,
},
KElem::zero(n),
logs,
extras,
))
}
fn integrate_kpoly_hyperexp<'a>(
ctx: &'a AtomArena<'a>,
tower: &Tower<'a>,
level: usize,
p: &KPoly,
m: usize,
) -> Option<KPolyIntegral<'a>> {
let top = level;
let n = tower.n_vars();
let tgen = &tower.gens[level - 1];
let t = KElem::var(top, n);
let du = tgen.dt.div(&t)?;
let mut q = vec![KElem::zero(n); m + 1];
for i in (1..=m).rev() {
let f_i = du.mul_rational(&Rational::new(i as i64, 1));
q[i] = rde_solve(ctx, tower, level - 1, &f_i, &p.coeff_at(i))?;
}
let mut constant = KElem::zero(n);
let mut logs = Vec::new();
let mut extras = Vec::new();
let p0 = p.coeff_at(0);
if !p0.is_zero() {
let int_0 = integrate_kelem_or_fallback(ctx, tower, level - 1, p0)?;
let (e, ls, es) = split_result(&int_0);
constant = e;
logs = ls;
extras = es;
}
Some((
KPoly {
top,
coeffs: q,
n_vars: n,
},
constant,
logs,
extras,
))
}
fn integrate_kelem_or_fallback<'a>(
ctx: &'a AtomArena<'a>,
tower: &Tower<'a>,
level: usize,
h: KElem,
) -> Option<LevelResult<'a>> {
let top = level;
let n = tower.n_vars();
if let Some(res) = integrate_level(ctx, tower, level, &krat_const(h.clone(), top, n)) {
return Some(res);
}
let mut out = LevelResult::empty(top, n);
let gens = tower.gen_atoms();
let h_atom = kelem_to_atom(ctx, &h, &gens)?;
out.extras.push(ctx.fun("Integral", &[h_atom, tower.x]));
Some(out)
}
fn integrate_level_base<'a>(
ctx: &'a AtomArena<'a>,
tower: &Tower<'a>,
f: &KRat,
) -> Option<LevelResult<'a>> {
let n = tower.n_vars();
let elem = f.kelem();
let num = sparse_to_dpoly(&elem.num)?;
let den = sparse_to_dpoly(&elem.den)?;
let s = integrate_dpoly_structured(ctx, &num, &den, tower.x)?;
let mut out = LevelResult::empty(0, n);
out.poly = dpoly_to_kpoly(&s.poly, 0, n);
if let Some((g_num, g_den)) = &s.rational {
out.rational = KRat::new(dpoly_to_kpoly(g_num, 0, n), dpoly_to_kpoly(g_den, 0, n));
}
for (c, v) in &s.log_terms {
out.logs.push((c.clone(), dpoly_to_kelem(v, n)));
}
out.extras = s.extra_atoms;
Some(out)
}
fn krat_const(e: KElem, top: usize, n: usize) -> KRat {
KRat::new(KPoly::from_kelem(e, top), KPoly::one(top, n))
}
fn split_result<'a>(res: &LevelResult<'a>) -> (KElem, Vec<(Rational, KElem)>, Vec<Atom<'a>>) {
(res.elem_part(), res.logs.clone(), res.extras.clone())
}
fn kpoly_to_dpoly(p: &KPoly) -> Option<DPoly> {
let mut coeffs = Vec::with_capacity(p.coeffs.len());
for c in &p.coeffs {
coeffs.push(kelem_to_rational(c)?);
}
Some(DPoly::from_coeffs(RationalDomain, coeffs))
}
fn sparse_to_dpoly(p: &crate::tower::elem::Sparse) -> Option<DPoly> {
if p.terms_ref()
.keys()
.any(|ex| ex.iter().skip(1).any(|&k| k != 0))
{
return None;
}
let dom = RationalDomain;
let deg = p.degree_in(0);
let mut coeffs = vec![dom.zero(); deg + 1];
for (exp, c) in p.terms_ref() {
coeffs[exp[0]] = c.clone();
}
Some(DPoly::from_coeffs(RationalDomain, coeffs))
}
fn dpoly_to_kelem(p: &DPoly, n: usize) -> KElem {
let terms = p
.coeffs()
.iter()
.enumerate()
.filter(|&(_, c)| !RationalDomain.is_zero(c))
.map(|(i, c)| (vec_i(i, n), c.clone()))
.collect();
KElem::from_poly(crate::tower::elem::Sparse::from_terms(
RationalDomain,
n,
terms,
))
}
fn dpoly_to_kpoly(p: &DPoly, top: usize, n: usize) -> KPoly {
let coeffs = p
.coeffs()
.iter()
.map(|c| KElem::from_rational(c, n))
.collect();
let mut r = KPoly {
top,
coeffs,
n_vars: n,
};
while let Some(last) = r.coeffs.last() {
if last.is_zero() {
r.coeffs.pop();
} else {
break;
}
}
r
}
fn vec_i(i: usize, n: usize) -> Vec<usize> {
let mut v = vec![0; n];
v[0] = i;
v
}
fn integral_fallback_atom<'a>(
ctx: &'a AtomArena<'a>,
tower: &Tower<'a>,
level: usize,
a: &KPoly,
d: &KPoly,
) -> Option<Atom<'a>> {
let gens = tower.gen_atoms();
let num = kelem_to_atom(ctx, &a.kelem(), &gens)?;
let den = kelem_to_atom(ctx, &d.kelem(), &gens)?;
let expr = ctx.mul(&[num, ctx.pow(den, ctx.num(-1))]);
let _ = level;
Some(ctx.fun("Integral", &[expr, tower.x]))
}
fn kelem_to_atom<'a>(ctx: &'a AtomArena<'a>, e: &KElem, gens: &[Atom<'a>]) -> Option<Atom<'a>> {
rational_to_atom(
ctx,
&GeneratorField::from_num_den(e.num.clone(), e.den.clone()),
gens,
)
}
fn level_result_to_atom<'a>(
ctx: &'a AtomArena<'a>,
tower: &Tower<'a>,
level: usize,
res: &LevelResult<'a>,
) -> Option<Atom<'a>> {
let gens = tower.gen_atoms();
let t_atom = gens[level];
let n = tower.n_vars();
let mut parts: Vec<Atom> = Vec::new();
if !res.rational.num.is_zero() {
parts.push(kelem_to_atom(ctx, &res.rational.kelem(), &gens)?);
}
let mut poly_terms: Vec<Atom> = Vec::new();
for (i, c) in res.poly.coeffs.iter().enumerate() {
if c.is_zero() {
continue;
}
let c_atom = kelem_to_atom(ctx, c, &gens)?;
let tp = match i {
0 => c_atom,
1 => ctx.mul(&[c_atom, t_atom]),
_ => ctx.mul(&[c_atom, ctx.pow(t_atom, ctx.num(i as i64))]),
};
poly_terms.push(tp);
}
match poly_terms.len() {
0 => {}
1 => parts.push(poly_terms[0]),
_ => parts.push(ctx.add(&poly_terms)),
}
if !res.constant.is_zero() {
parts.push(kelem_to_atom(ctx, &res.constant, &gens)?);
}
for (c, v) in &res.logs {
let v_atom = kelem_to_atom(ctx, v, &gens)?;
let log = ctx.fun("log", &[v_atom]);
parts.push(if RationalDomain.is_one(c) {
log
} else {
ctx.mul(&[rational_const_to_atom(ctx, c)?, log])
});
}
parts.extend(res.extras.iter().copied());
let _ = n;
Some(match parts.len() {
0 => ctx.num(0),
1 => parts[0],
_ => ctx.add(&parts),
})
}
#[cfg(test)]
mod tests {
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;
use super::*;
use crate::derivative::diff;
use crate::tower::convert::atom_to_rational;
fn assert_risch_antiderivative<'a>(ctx: &'a AtomArena<'a>, integrand: Atom<'a>, var: Symbol) {
let result = risch_integrate(ctx, integrand, var).expect("risch result");
let tower = build_tower(ctx, integrand, var).expect("tower");
let gens = tower.gen_atoms();
let d = normalize(ctx, diff(ctx, result, var));
let lhs = atom_to_rational(d, &gens).expect("derivative converts");
let rhs = atom_to_rational(normalize(ctx, integrand), &gens).expect("integrand converts");
let lhs_e = KElem::new(lhs.numerator, lhs.denominator);
let rhs_e = KElem::new(rhs.numerator, rhs.denominator);
assert!(lhs_e.eq_cross(&rhs_e), "d/dx(result) != integrand");
}
#[test]
fn integrate_log_x() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
assert_risch_antiderivative(&ctx, ctx.fun("log", &[x]), Symbol::new("x"));
}
#[test]
fn integrate_log_x_over_x() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let expr = ctx.mul(&[ctx.fun("log", &[x]), ctx.pow(x, ctx.num(-1))]);
assert_risch_antiderivative(&ctx, expr, Symbol::new("x"));
}
#[test]
fn integrate_x_log_x() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let expr = ctx.mul(&[x, ctx.fun("log", &[x])]);
assert_risch_antiderivative(&ctx, expr, Symbol::new("x"));
}
#[test]
fn integrate_exp_x() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
assert_risch_antiderivative(&ctx, ctx.fun("exp", &[x]), Symbol::new("x"));
}
#[test]
fn integrate_x_exp_x() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let expr = ctx.mul(&[x, ctx.fun("exp", &[x])]);
assert_risch_antiderivative(&ctx, expr, Symbol::new("x"));
}
#[test]
fn integrate_x2_exp_x() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let expr = ctx.mul(&[ctx.pow(x, ctx.num(2)), ctx.fun("exp", &[x])]);
assert_risch_antiderivative(&ctx, expr, Symbol::new("x"));
}
#[test]
fn integrate_exp_x_over_x_fails() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let expr = ctx.mul(&[ctx.fun("exp", &[x]), ctx.pow(x, ctx.num(-1))]);
assert!(risch_integrate(&ctx, expr, Symbol::new("x")).is_none());
}
#[test]
fn integrate_log_x_plus_1() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let expr = ctx.fun("log", &[ctx.add(&[x, ctx.num(1)])]);
let _ = risch_integrate(&ctx, expr, Symbol::new("x"));
}
#[test]
fn integrate_exp_x2_times_x() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let expr = ctx.mul(&[x, ctx.fun("exp", &[ctx.pow(x, ctx.num(2))])]);
assert_risch_antiderivative(&ctx, expr, Symbol::new("x"));
}
#[test]
fn integrate_one_over_x_log_x_sq() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let expr = ctx.pow(
ctx.mul(&[x, ctx.pow(ctx.fun("log", &[x]), ctx.num(2))]),
ctx.num(-1),
);
let _ = risch_integrate(&ctx, expr, Symbol::new("x"));
}
#[test]
fn integrate_log_squared() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let expr = ctx.pow(ctx.fun("log", &[x]), ctx.num(2));
assert_risch_antiderivative(&ctx, expr, Symbol::new("x"));
}
#[test]
fn integrate_exp_plus_poly() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let expr = ctx.add(&[ctx.pow(x, ctx.num(2)), ctx.fun("exp", &[x])]);
assert_risch_antiderivative(&ctx, expr, Symbol::new("x"));
}
}