use ocas_atom::normalize::normalize;
use ocas_atom::{Atom, AtomArena, AtomNode, Symbol};
use super::rules::rat_of;
use super::{
contains_integral, contains_symbol, int_pow, integrate_raw, inv, is_constant, linear_form,
node_count, pick_subst_symbol, rat_atom, replace_symbol,
};
const MAX_NODES: usize = 200;
const MAX_SUBST_NODES: usize = 300;
const MAX_REDUCED_NODES: usize = 600;
const MAX_POWER_K: i64 = 6;
const MAX_SUBST_K: i64 = 4;
const MAX_POLY_DEG: usize = 4;
const MAX_HYP_EXP: usize = 8;
const MAX_CANCEL_BUDGET: usize = 800;
const MAX_KERNEL_POW: i64 = 6;
const MAX_COFACTOR_DEG: usize = 6;
const MAX_RECIP_EXP: i64 = 8;
const MAX_KERNEL_NODES: usize = 400;
const MAX_RAT_Q: i64 = 4;
#[derive(Clone, Copy, PartialEq, Eq)]
enum InvFun {
Asin,
Acos,
Atan,
Asinh,
Acosh,
Atanh,
Acsc,
Asec,
Acoth,
Asech,
Acsch,
}
impl InvFun {
fn name(self) -> &'static str {
match self {
InvFun::Asin => "asin",
InvFun::Acos => "acos",
InvFun::Atan => "atan",
InvFun::Asinh => "asinh",
InvFun::Acosh => "acosh",
InvFun::Atanh => "atanh",
InvFun::Acsc => "acsc",
InvFun::Asec => "asec",
InvFun::Acoth => "acoth",
InvFun::Asech => "asech",
InvFun::Acsch => "acsch",
}
}
}
fn inv_fun(name: &str) -> Option<InvFun> {
Some(match name {
"asin" => InvFun::Asin,
"acos" => InvFun::Acos,
"atan" => InvFun::Atan,
"asinh" => InvFun::Asinh,
"acosh" => InvFun::Acosh,
"atanh" => InvFun::Atanh,
"acsc" => InvFun::Acsc,
"asec" => InvFun::Asec,
"acoth" => InvFun::Acoth,
"asech" => InvFun::Asech,
"acsch" => InvFun::Acsch,
_ => return None,
})
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum KernelExp {
InvSqrt,
Recip,
}
fn family_kernel_exp(f: InvFun) -> Option<KernelExp> {
Some(match f {
InvFun::Asin | InvFun::Acos | InvFun::Asinh | InvFun::Acosh => KernelExp::InvSqrt,
InvFun::Atan | InvFun::Atanh => KernelExp::Recip,
InvFun::Acsc | InvFun::Asec | InvFun::Acoth | InvFun::Asech | InvFun::Acsch => {
return None;
}
})
}
fn family_sign(f: InvFun) -> Option<i64> {
Some(match f {
InvFun::Acos => -1,
InvFun::Asin | InvFun::Atan | InvFun::Asinh | InvFun::Acosh | InvFun::Atanh => 1,
InvFun::Acsc | InvFun::Asec | InvFun::Acoth | InvFun::Asech | InvFun::Acsch => {
return None;
}
})
}
fn kernel_sum<'a>(
ctx: &'a AtomArena<'a>,
f: InvFun,
sigma: Atom<'a>,
var: Symbol,
) -> (Atom<'a>, i64) {
let x = ctx.var(var.as_str());
let s2x2 = ctx.mul(&[int_pow(ctx, sigma, 2), int_pow(ctx, x, 2)]);
match f {
InvFun::Asin | InvFun::Acos | InvFun::Atanh | InvFun::Asech => (
normalize(ctx, ctx.add(&[ctx.num(1), ctx.mul(&[ctx.num(-1), s2x2])])),
1,
),
InvFun::Atan | InvFun::Asinh | InvFun::Acsch => {
(normalize(ctx, ctx.add(&[ctx.num(1), s2x2])), 1)
}
InvFun::Acosh | InvFun::Acsc | InvFun::Asec | InvFun::Acoth => {
(normalize(ctx, ctx.add(&[ctx.num(-1), s2x2])), -1)
}
}
}
pub(crate) fn folds_to_zero<'a>(ctx: &'a AtomArena<'a>, e: Atom<'a>) -> bool {
let n = normalize(ctx, e);
if matches!(n.node(), AtomNode::Num(0)) {
return true;
}
matches!(
crate::ode::util::collect_terms(ctx, n).node(),
AtomNode::Num(0)
)
}
fn is_zero_atom<'a>(ctx: &'a AtomArena<'a>, e: Atom<'a>) -> bool {
folds_to_zero(ctx, e)
}
struct InvPow<'a> {
f: InvFun,
arg: Atom<'a>,
#[allow(dead_code)] a: Atom<'a>,
b: Atom<'a>,
k: i64,
base: Atom<'a>,
}
fn match_inv_base<'a>(
ctx: &'a AtomArena<'a>,
base: Atom<'a>,
var: Symbol,
) -> Option<(InvFun, Atom<'a>, Atom<'a>, Atom<'a>)> {
match base.node() {
AtomNode::Fun(name, args) if args.len() == 1 => {
let f = inv_fun(name.as_str())?;
Some((f, args[0], ctx.num(0), ctx.num(1)))
}
AtomNode::Mul(args) => {
let mut coeff: Vec<Atom<'a>> = Vec::new();
let mut found: Option<(InvFun, Atom<'a>)> = None;
for fa in args.iter() {
match fa.node() {
AtomNode::Fun(name, fargs) if fargs.len() == 1 => {
let f = inv_fun(name.as_str())?;
if found.is_some() {
return None;
}
found = Some((f, fargs[0]));
}
_ if is_constant(*fa, var) => coeff.push(*fa),
_ => return None,
}
}
let (f, arg) = found?;
let b = if coeff.is_empty() {
ctx.num(1)
} else {
normalize(ctx, ctx.mul(&coeff))
};
Some((f, arg, ctx.num(0), b))
}
AtomNode::Add(args) => {
let mut consts: Vec<Atom<'a>> = Vec::new();
let mut found: Option<(InvFun, Atom<'a>, Atom<'a>, Atom<'a>)> = None;
for t in args.iter() {
if is_constant(*t, var) {
consts.push(*t);
continue;
}
let term = match_inv_base(ctx, *t, var)?;
if found.is_some() {
return None;
}
found = Some(term);
}
let (f, arg, _zero, b) = found?;
let a = if consts.is_empty() {
ctx.num(0)
} else {
normalize(ctx, ctx.add(&consts))
};
Some((f, arg, a, b))
}
_ => None,
}
}
fn match_inv_power<'a>(
ctx: &'a AtomArena<'a>,
factor: Atom<'a>,
var: Symbol,
k_cap: i64,
) -> Option<InvPow<'a>> {
if let AtomNode::Pow(b, e) = factor.node()
&& let AtomNode::Num(k) = e.node()
{
if !(1..=k_cap).contains(k) {
return None;
}
let (f, arg, a, bb) = match_inv_base(ctx, *b, var)?;
return Some(InvPow {
f,
arg,
a,
b: bb,
k: *k,
base: *b,
});
}
let (f, arg, a, bb) = match_inv_base(ctx, factor, var)?;
Some(InvPow {
f,
arg,
a,
b: bb,
k: 1,
base: factor,
})
}
fn match_kernel(factor: Atom<'_>) -> Option<(Atom<'_>, KernelExp)> {
let AtomNode::Pow(b, e) = factor.node() else {
return None;
};
if let AtomNode::Fun(name, args) = b.node()
&& name.as_str() == "sqrt"
&& args.len() == 1
&& matches!(e.node(), AtomNode::Num(-1))
{
return Some((args[0], KernelExp::InvSqrt));
}
match rat_of(*e)? {
(-1, 2) => Some((*b, KernelExp::InvSqrt)),
(-1, 1) => Some((*b, KernelExp::Recip)),
_ => None,
}
}
fn radicand_normalization<'a>(
ctx: &'a AtomArena<'a>,
r: Atom<'a>,
e_sum: Atom<'a>,
e0: i64,
var: Symbol,
) -> Option<Atom<'a>> {
let r0 = normalize(ctx, replace_symbol(ctx, r, var, ctx.num(0)));
let s_atom = if e0 == 1 {
r0
} else {
normalize(ctx, ctx.mul(&[ctx.num(-1), r0]))
};
if is_zero_atom(ctx, s_atom) {
return None;
}
let diff = ctx.add(&[r, ctx.mul(&[ctx.num(-1), s_atom, e_sum])]);
if !folds_to_zero(ctx, diff) {
return None;
}
Some(s_atom)
}
fn sqrt_norm_positive(s_atom: Atom<'_>) -> bool {
if mentions_imaginary(s_atom) {
return false;
}
match rat_of(s_atom) {
Some((p, q)) => (p > 0) == (q > 0),
None => true,
}
}
pub(crate) fn integrate_kernel_power<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
) -> Option<Atom<'a>> {
if is_constant(expr, var) || matches!(expr.node(), AtomNode::Add(_)) {
return None;
}
let factors: Vec<Atom<'a>> = match expr.node() {
AtomNode::Mul(args) => args.to_vec(),
_ => vec![expr],
};
let mut rest: Vec<Atom<'a>> = Vec::new();
let mut inv_f: Option<InvPow<'a>> = None;
let mut kern: Option<(Atom<'a>, KernelExp)> = None;
for f in factors {
if is_constant(f, var) {
rest.push(f);
continue;
}
if inv_f.is_none()
&& let Some(m) = match_inv_power(ctx, f, var, MAX_POWER_K)
{
inv_f = Some(m);
continue;
}
if kern.is_none()
&& let Some(kd) = match_kernel(f)
{
kern = Some(kd);
continue;
}
return None;
}
let m = inv_f?;
let (r, ke) = kern?;
if Some(ke) != family_kernel_exp(m.f) {
return None;
}
let (sigma, intercept) = linear_form(ctx, m.arg, var)?;
if !is_zero_atom(ctx, intercept) || is_zero_atom(ctx, sigma) {
return None;
}
let sign = family_sign(m.f)?;
let (e_sum, e0) = kernel_sum(ctx, m.f, sigma, var);
let s_atom = radicand_normalization(ctx, r, e_sum, e0, var)?;
if ke == KernelExp::InvSqrt && !sqrt_norm_positive(s_atom) {
return None;
}
let s_factor = match ke {
KernelExp::Recip => inv(ctx, s_atom),
KernelExp::InvSqrt => ctx.pow(s_atom, rat_atom(ctx, -1, 2)),
};
let mut out = rest;
if sign < 0 {
out.push(ctx.num(-1));
}
out.push(s_factor);
out.push(int_pow(ctx, m.base, m.k + 1));
let denom = normalize(ctx, ctx.mul(&[m.b, sigma, ctx.num(m.k + 1)]));
out.push(inv(ctx, denom));
Some(normalize(ctx, ctx.mul(&out)))
}
fn poly_deg(expr: Atom<'_>, var: Symbol) -> Option<usize> {
match expr.node() {
AtomNode::Num(_) => Some(0),
AtomNode::Var(v) => Some(if *v == var { 1 } else { 0 }),
AtomNode::Add(args) => args
.iter()
.try_fold(0usize, |d, a| Some(d.max(poly_deg(*a, var)?))),
AtomNode::Mul(args) => args
.iter()
.try_fold(0usize, |d, a| Some(d + poly_deg(*a, var)?)),
AtomNode::Pow(b, e) => {
let AtomNode::Num(n) = e.node() else {
return None;
};
if *n < 0 {
return None;
}
Some(poly_deg(*b, var)?.checked_mul(*n as usize)?)
}
AtomNode::Fun(_, _) => None,
}
}
fn match_sqrt_pow(factor: Atom<'_>) -> Option<(Atom<'_>, i64)> {
match factor.node() {
AtomNode::Fun(name, args) if name.as_str() == "sqrt" && args.len() == 1 => {
Some((args[0], 1))
}
AtomNode::Pow(b, e) => {
if let AtomNode::Fun(name, args) = b.node()
&& name.as_str() == "sqrt"
&& args.len() == 1
&& let AtomNode::Num(n) = e.node()
{
return match n {
1 | 3 => Some((args[0], *n)),
_ => None,
};
}
let (p, q) = rat_of(*e)?;
if q == 2 && matches!(p, 1 | 3) {
Some((*b, p))
} else {
None
}
}
_ => None,
}
}
pub(crate) fn integrate_invhyp_subst<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
) -> Option<Atom<'a>> {
if is_constant(expr, var) || matches!(expr.node(), AtomNode::Add(_)) {
return None;
}
let factors: Vec<Atom<'a>> = match expr.node() {
AtomNode::Mul(args) => args.to_vec(),
_ => vec![expr],
};
let mut rest: Vec<Atom<'a>> = Vec::new();
let mut inv_f: Option<InvPow<'a>> = None;
let mut polys: Vec<Atom<'a>> = Vec::new();
let mut sqrt_f: Option<(Atom<'a>, i64)> = None;
let mut total_deg = 0usize;
for f in factors {
if is_constant(f, var) {
rest.push(f);
continue;
}
if inv_f.is_none()
&& let Some(m) = match_inv_power(ctx, f, var, MAX_SUBST_K)
{
if !matches!(m.f, InvFun::Asinh | InvFun::Acosh) {
return None;
}
inv_f = Some(m);
continue;
}
if sqrt_f.is_none()
&& let Some(sp) = match_sqrt_pow(f)
{
sqrt_f = Some(sp);
continue;
}
if let Some(d) = poly_deg(f, var)
&& d >= 1
&& total_deg + d <= MAX_POLY_DEG
{
total_deg += d;
polys.push(f);
continue;
}
return None;
}
let m = inv_f?;
let (sigma, rho) = linear_form(ctx, m.arg, var)?;
if is_zero_atom(ctx, sigma) {
return None;
}
if sqrt_f.is_some() && !is_zero_atom(ctx, rho) {
return None;
}
let t_sym = pick_subst_symbol(expr, var)?;
let t = ctx.var(t_sym.as_str());
let (g_name, gp_name) = match m.f {
InvFun::Asinh => ("sinh", "cosh"),
InvFun::Acosh => ("cosh", "sinh"),
_ => return None,
};
let g_t = ctx.fun(g_name, &[t]);
let gp_t = ctx.fun(gp_name, &[t]);
let x_of_t = {
let num_t = if is_zero_atom(ctx, rho) {
g_t
} else {
ctx.add(&[g_t, ctx.mul(&[ctx.num(-1), rho])])
};
ctx.mul(&[num_t, inv(ctx, sigma)])
};
let mut t_factors = rest;
let base_t = normalize(ctx, ctx.add(&[m.a, ctx.mul(&[m.b, t])]));
t_factors.push(int_pow(ctx, base_t, m.k));
for p in &polys {
t_factors.push(replace_symbol(ctx, *p, var, x_of_t));
}
t_factors.push(ctx.mul(&[gp_t, inv(ctx, sigma)]));
if let Some((r, mpow)) = sqrt_f {
let (e_sum, e0) = kernel_sum(ctx, m.f, sigma, var);
let s_atom = radicand_normalization(ctx, r, e_sum, e0, var)?;
if !sqrt_norm_positive(s_atom) {
return None;
}
t_factors.push(ctx.pow(s_atom, rat_atom(ctx, mpow, 2)));
t_factors.push(int_pow(ctx, gp_t, mpow));
}
let product = normalize(ctx, ctx.mul(&t_factors));
let expanded = crate::expand::expand_bounded(ctx, product).unwrap_or(product);
let folded = crate::ode::util::collect_terms(ctx, normalize(ctx, expanded));
if node_count(folded) > MAX_SUBST_NODES {
return None;
}
let terms: Vec<Atom<'a>> = match folded.node() {
AtomNode::Add(args) => args.to_vec(),
_ => vec![folded],
};
let mut reduced: Vec<Atom<'a>> = Vec::new();
for term in terms {
let (coeff, j, na, nb) = scan_hyp_term(ctx, term, t_sym, MAX_SUBST_K)?;
reduce_hyp_term(ctx, &coeff, j, na, nb, t, &mut reduced)?;
}
let t_form = normalize(ctx, ctx.add(&reduced));
if node_count(t_form) > MAX_REDUCED_NODES {
return None;
}
let back = ctx.fun(m.f.name(), &[m.arg]);
let chain = integrate_raw(ctx, t_form, t_sym, 0, true, 0, 0);
if !contains_integral(chain) {
return Some(normalize(ctx, replace_symbol(ctx, chain, t_sym, back)));
}
let rterms: Vec<Atom<'a>> = match t_form.node() {
AtomNode::Add(args) => args.to_vec(),
_ => vec![t_form],
};
let mut out = Vec::with_capacity(rterms.len());
for term in rterms {
out.push(integrate_hyp_term(ctx, term, t_sym, MAX_SUBST_K)?);
}
let res_t = normalize(ctx, ctx.add(&out));
Some(normalize(ctx, replace_symbol(ctx, res_t, t_sym, back)))
}
fn binom(n: i64, mut k: i64) -> i64 {
if k < 0 || k > n {
return 0;
}
if k > n - k {
k = n - k;
}
let mut r: i64 = 1;
for i in 0..k {
r = r.saturating_mul(n - i) / (i + 1);
}
r
}
fn is_t_var(e: Atom<'_>, t_sym: Symbol) -> bool {
matches!(e.node(), AtomNode::Var(v) if *v == t_sym)
}
fn scan_hyp_term<'a>(
ctx: &'a AtomArena<'a>,
term: Atom<'a>,
t_sym: Symbol,
j_cap: i64,
) -> Option<(Vec<Atom<'a>>, i64, usize, usize)> {
let _ = ctx;
let factors: Vec<Atom<'a>> = match term.node() {
AtomNode::Mul(args) => args.to_vec(),
_ => vec![term],
};
let mut coeff: Vec<Atom<'a>> = Vec::new();
let mut j = 0i64;
let mut na = 0usize;
let mut nb = 0usize;
for f in factors {
if is_constant(f, t_sym) {
coeff.push(f);
continue;
}
match f.node() {
AtomNode::Var(v) if *v == t_sym => j += 1,
AtomNode::Pow(b, e) if matches!(b.node(), AtomNode::Var(v) if *v == t_sym) => {
let AtomNode::Num(n) = e.node() else {
return None;
};
if *n < 0 {
return None;
}
j += n;
}
AtomNode::Fun(name, args) if args.len() == 1 && is_t_var(args[0], t_sym) => {
match name.as_str() {
"cosh" => na += 1,
"sinh" => nb += 1,
_ => return None,
}
}
AtomNode::Pow(b, e) => {
let AtomNode::Fun(name, args) = b.node() else {
return None;
};
if args.len() != 1 || !is_t_var(args[0], t_sym) {
return None;
}
let AtomNode::Num(n) = e.node() else {
return None;
};
if *n <= 0 {
return None;
}
match name.as_str() {
"cosh" => na += *n as usize,
"sinh" => nb += *n as usize,
_ => return None,
}
}
_ => return None,
}
}
if j > j_cap || na + nb > MAX_HYP_EXP {
return None;
}
Some((coeff, j, na, nb))
}
fn reduce_hyp_term<'a>(
ctx: &'a AtomArena<'a>,
coeff: &[Atom<'a>],
j: i64,
na: usize,
nb: usize,
t: Atom<'a>,
out: &mut Vec<Atom<'a>>,
) -> Option<()> {
if na == 0 && nb == 0 {
let mut fs = coeff.to_vec();
if j > 0 {
fs.push(int_pow(ctx, t, j));
}
out.push(normalize(ctx, ctx.mul(&fs)));
return Some(());
}
let nsum = na + nb;
if nsum > MAX_HYP_EXP {
return None;
}
let mut m = nsum % 2;
while m <= nsum {
let target = ((m + nsum) / 2) as i64;
let mut sum: i64 = 0;
for i in 0..=(na as i64) {
let jj = target - i;
if jj < 0 || jj > nb as i64 {
continue;
}
let c = binom(na as i64, i) * binom(nb as i64, jj);
sum += if (nb as i64 - jj) % 2 == 0 { c } else { -c };
}
if sum != 0 {
let num = if m == 0 { sum } else { 2 * sum };
let r = rat_atom(ctx, num, 1i64 << nsum);
let mut fs = coeff.to_vec();
fs.push(r);
if j > 0 {
fs.push(int_pow(ctx, t, j));
}
if m > 0 {
let marg = if m == 1 {
t
} else {
ctx.mul(&[ctx.num(m as i64), t])
};
fs.push(ctx.fun(if nb.is_multiple_of(2) { "cosh" } else { "sinh" }, &[marg]));
}
out.push(normalize(ctx, ctx.mul(&fs)));
}
m += 2;
}
Some(())
}
fn hyp_multiple(arg: Atom<'_>, t_sym: Symbol) -> Option<i64> {
match arg.node() {
AtomNode::Var(v) if *v == t_sym => Some(1),
AtomNode::Mul(args) if args.len() == 2 => {
let (a, b) = (args[0], args[1]);
match (a.node(), b.node()) {
(AtomNode::Num(m), AtomNode::Var(v)) if *v == t_sym && *m > 0 => Some(*m),
(AtomNode::Var(v), AtomNode::Num(m)) if *v == t_sym && *m > 0 => Some(*m),
_ => None,
}
}
_ => None,
}
}
fn integrate_hyp_term<'a>(
ctx: &'a AtomArena<'a>,
term: Atom<'a>,
t_sym: Symbol,
j_cap: i64,
) -> Option<Atom<'a>> {
let factors: Vec<Atom<'a>> = match term.node() {
AtomNode::Mul(args) => args.to_vec(),
_ => vec![term],
};
let mut coeff: Vec<Atom<'a>> = Vec::new();
let mut j = 0i64;
let mut hyp: Option<(bool, i64)> = None;
for f in factors {
if is_constant(f, t_sym) {
coeff.push(f);
continue;
}
match f.node() {
AtomNode::Var(v) if *v == t_sym => j += 1,
AtomNode::Pow(b, e) if matches!(b.node(), AtomNode::Var(v) if *v == t_sym) => {
let AtomNode::Num(n) = e.node() else {
return None;
};
if *n < 0 {
return None;
}
j += n;
}
AtomNode::Fun(name, args) if args.len() == 1 => {
let is_sinh = match name.as_str() {
"sinh" => true,
"cosh" => false,
_ => return None,
};
if hyp.is_some() {
return None;
}
hyp = Some((is_sinh, hyp_multiple(args[0], t_sym)?));
}
_ => return None,
}
}
if j > j_cap {
return None;
}
let t = ctx.var(t_sym.as_str());
let (is_sinh, m) = match hyp {
None => {
let mut fs = coeff;
fs.push(int_pow(ctx, t, j + 1));
fs.push(inv(ctx, ctx.num(j + 1)));
return Some(normalize(ctx, ctx.mul(&fs)));
}
Some(h) => h,
};
let mut terms = Vec::new();
let mut fall: i64 = 1; for i in 0..=j {
if i > 0 {
fall = fall.checked_mul(j - i + 1)?;
}
let mpow = m.checked_pow((i + 1) as u32)?;
let out_sinh = if is_sinh { i % 2 == 1 } else { i % 2 == 0 };
let marg = if m == 1 { t } else { ctx.mul(&[ctx.num(m), t]) };
let mut fs = coeff.clone();
if i % 2 == 1 {
fs.push(ctx.num(-1));
}
if fall != 1 {
fs.push(ctx.num(fall));
}
let deg = j - i;
if deg > 0 {
fs.push(int_pow(ctx, t, deg));
}
fs.push(ctx.pow(ctx.num(mpow), ctx.num(-1)));
fs.push(ctx.fun(if out_sinh { "sinh" } else { "cosh" }, &[marg]));
terms.push(normalize(ctx, ctx.mul(&fs)));
}
Some(normalize(ctx, ctx.add(&terms)))
}
pub(crate) fn integrate_bare_linear<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
) -> Option<Atom<'a>> {
if is_constant(expr, var) || matches!(expr.node(), AtomNode::Add(_)) {
return None;
}
let factors: Vec<Atom<'a>> = match expr.node() {
AtomNode::Mul(args) => args.to_vec(),
_ => vec![expr],
};
let mut rest: Vec<Atom<'a>> = Vec::new();
let mut bare: Option<(InvFun, Atom<'a>)> = None;
for f in factors {
if is_constant(f, var) {
rest.push(f);
continue;
}
if bare.is_none()
&& let AtomNode::Fun(name, args) = f.node()
&& args.len() == 1
&& inv_fun(name.as_str()).is_some()
{
bare = Some((inv_fun(name.as_str())?, args[0]));
continue;
}
return None;
}
let (f, u) = bare?;
let (slope, _intercept) = linear_form(ctx, u, var)?;
if is_zero_atom(ctx, slope) {
return None;
}
let u2 = int_pow(ctx, u, 2);
let one_minus_u2 = || ctx.add(&[ctx.num(1), ctx.mul(&[ctx.num(-1), u2])]);
let fu = ctx.fun(f.name(), &[u]);
let core = match f {
InvFun::Asin => ctx.add(&[ctx.mul(&[u, fu]), ctx.fun("sqrt", &[one_minus_u2()])]),
InvFun::Acos => ctx.add(&[
ctx.mul(&[u, fu]),
ctx.mul(&[ctx.num(-1), ctx.fun("sqrt", &[one_minus_u2()])]),
]),
InvFun::Atan => ctx.add(&[
ctx.mul(&[u, fu]),
ctx.mul(&[
rat_atom(ctx, -1, 2),
ctx.fun("log", &[ctx.add(&[ctx.num(1), u2])]),
]),
]),
InvFun::Asinh => ctx.add(&[
ctx.mul(&[u, fu]),
ctx.mul(&[ctx.num(-1), ctx.fun("sqrt", &[ctx.add(&[ctx.num(1), u2])])]),
]),
InvFun::Acosh => ctx.add(&[
ctx.mul(&[u, fu]),
ctx.mul(&[ctx.num(-1), ctx.fun("sqrt", &[ctx.add(&[ctx.num(-1), u2])])]),
]),
InvFun::Atanh => ctx.add(&[
ctx.mul(&[u, fu]),
ctx.mul(&[rat_atom(ctx, 1, 2), ctx.fun("log", &[one_minus_u2()])]),
]),
InvFun::Acsc | InvFun::Asec | InvFun::Acoth | InvFun::Asech | InvFun::Acsch => {
return None;
}
};
let mut out = rest;
out.push(inv(ctx, slope));
out.push(core);
Some(normalize(ctx, ctx.mul(&out)))
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum CancelGuard {
Unconditional,
AtanBand,
AcoshHalf,
}
fn cancel_guard(outer: &str, inner: &str) -> Option<CancelGuard> {
Some(match (outer, inner) {
("atanh", "tanh") | ("asinh", "sinh") | ("acoth", "coth") | ("log", "exp") => {
CancelGuard::Unconditional
}
("atan", "tan") => CancelGuard::AtanBand,
("acosh", "cosh") => CancelGuard::AcoshHalf,
_ => return None,
})
}
fn guard_certified(g: CancelGuard, u: Atom<'_>, var: Symbol) -> bool {
match g {
CancelGuard::Unconditional => true,
CancelGuard::AtanBand => {
is_constant(u, var)
&& rat_of(u).is_some_and(|(p, q)| {
q > 0 && p.saturating_abs().saturating_mul(2) <= q.saturating_mul(3)
})
}
CancelGuard::AcoshHalf => {
is_constant(u, var) && rat_of(u).is_some_and(|(p, q)| q > 0 && p >= 0)
}
}
}
fn rewrite_children<'a>(
ctx: &'a AtomArena<'a>,
args: &[Atom<'a>],
var: Symbol,
budget: &mut usize,
) -> (bool, Vec<Atom<'a>>) {
let mut changed = false;
let mut out: Vec<Atom<'a>> = Vec::with_capacity(args.len());
for a in args {
match cancel_compositions(ctx, *a, var, budget) {
Some(r) => {
changed = true;
out.push(r);
}
None => out.push(*a),
}
}
(changed, out)
}
fn cancel_compositions<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
budget: &mut usize,
) -> Option<Atom<'a>> {
if *budget == 0 {
return None;
}
*budget -= 1;
match expr.node() {
AtomNode::Num(_) | AtomNode::Var(_) => None,
AtomNode::Add(args) => {
let (changed, out) = rewrite_children(ctx, args, var, budget);
if changed { Some(ctx.add(&out)) } else { None }
}
AtomNode::Mul(args) => {
let (changed, out) = rewrite_children(ctx, args, var, budget);
if changed { Some(ctx.mul(&out)) } else { None }
}
AtomNode::Pow(b, e) => {
let nb = cancel_compositions(ctx, *b, var, budget);
let ne = cancel_compositions(ctx, *e, var, budget);
match (nb, ne) {
(None, None) => None,
(nb, ne) => Some(ctx.pow(nb.unwrap_or(*b), ne.unwrap_or(*e))),
}
}
AtomNode::Fun(name, args) => {
let (changed, new_args) = rewrite_children(ctx, args, var, budget);
if new_args.len() == 1
&& let AtomNode::Fun(inner, iargs) = new_args[0].node()
&& iargs.len() == 1
&& let Some(g) = cancel_guard(name.as_str(), inner.as_str())
&& guard_certified(g, iargs[0], var)
{
return Some(iargs[0]);
}
if changed {
Some(ctx.fun(name.as_str(), &new_args))
} else {
None
}
}
}
}
fn factor_var_power(factor: Atom<'_>, var: Symbol) -> Option<(i64, i64)> {
match factor.node() {
AtomNode::Var(v) if *v == var => Some((1, 1)),
AtomNode::Pow(b, e) => {
let (p, q) = rat_of(*e)?;
if q <= 0 {
return None;
}
let (bp, bq) = factor_var_power(*b, var)?;
Some((bp.checked_mul(p)?, bq.checked_mul(q)?))
}
AtomNode::Fun(name, args) if name.as_str() == "sqrt" && args.len() == 1 => {
let (p, q) = factor_var_power(args[0], var)?;
Some((p, q.checked_mul(2)?))
}
_ => None,
}
}
fn add_var_power(acc: &mut Option<(i64, i64)>, add: (i64, i64)) -> Option<()> {
match acc {
None => {
*acc = Some(add);
Some(())
}
Some((p, q)) => {
let nq = q.checked_mul(add.1)?;
if nq <= 0 || nq > MAX_RAT_Q * MAX_RAT_Q {
return None;
}
let np = p.checked_mul(add.1)?.checked_add(add.0.checked_mul(*q)?)?;
*acc = Some((np, nq));
Some(())
}
}
}
fn split_monomial<'a>(term: Atom<'a>, var: Symbol) -> Option<(Vec<Atom<'a>>, i64, i64)> {
let factors: Vec<Atom<'a>> = match term.node() {
AtomNode::Mul(args) => args.to_vec(),
_ => vec![term],
};
let mut coeff: Vec<Atom<'a>> = Vec::new();
let mut power: Option<(i64, i64)> = None;
for f in factors {
if let Some(pq) = factor_var_power(f, var) {
add_var_power(&mut power, pq)?;
continue;
}
if is_constant(f, var) {
coeff.push(f);
continue;
}
return None;
}
let (p, q) = power?;
if q <= 0 {
return None;
}
Some((coeff, p, q))
}
fn integrate_expanded_terms<'a>(
ctx: &'a AtomArena<'a>,
folded: Atom<'a>,
var: Symbol,
) -> Option<Atom<'a>> {
let terms: Vec<Atom<'a>> = match folded.node() {
AtomNode::Add(args) => args.to_vec(),
_ => vec![folded],
};
let x = ctx.var(var.as_str());
let mut out: Vec<Atom<'a>> = Vec::with_capacity(terms.len());
for term in terms {
if is_constant(term, var) {
out.push(ctx.mul(&[term, x]));
continue;
}
if let Some((coeff, p, q)) = split_monomial(term, var) {
let shifted = p.checked_add(q)?;
let mut fs = coeff;
if shifted == 0 {
fs.push(ctx.fun("log", &[x]));
} else {
let r = rat_atom(ctx, shifted, q);
fs.push(ctx.pow(x, r));
fs.push(inv(ctx, r));
}
out.push(normalize(ctx, ctx.mul(&fs)));
continue;
}
let r = integrate_raw(ctx, term, var, 0, true, 0, 0);
if contains_integral(r) {
return None;
}
out.push(r);
}
Some(normalize(ctx, ctx.add(&out)))
}
fn affine_kernel_pair(outer: &str, inner: &str) -> bool {
matches!(
(outer, inner),
("atanh", "tanh")
| ("asinh", "sinh")
| ("acoth", "coth")
| ("acoth", "tanh")
| ("atan", "tan")
| ("log", "exp")
)
}
struct AffineKernel<'a> {
base: Atom<'a>,
sigma: Atom<'a>,
}
fn match_affine_kernel<'a>(
ctx: &'a AtomArena<'a>,
factor: Atom<'a>,
var: Symbol,
) -> Option<AffineKernel<'a>> {
let AtomNode::Fun(name, args) = factor.node() else {
return None;
};
if args.len() != 1 {
return None;
}
let AtomNode::Fun(inner, iargs) = args[0].node() else {
return None;
};
if iargs.len() != 1 || !affine_kernel_pair(name.as_str(), inner.as_str()) {
return None;
}
let (sigma, _rho) = linear_form(ctx, iargs[0], var)?;
if is_zero_atom(ctx, sigma) {
return None;
}
Some(AffineKernel {
base: factor,
sigma,
})
}
fn reciprocal_power(factor: Atom<'_>, var: Symbol) -> Option<i64> {
let AtomNode::Pow(b, e) = factor.node() else {
return None;
};
if !matches!(b.node(), AtomNode::Var(v) if *v == var) {
return None;
}
let AtomNode::Num(j) = e.node() else {
return None;
};
if *j <= -1 && *j >= -MAX_RECIP_EXP {
Some(-*j)
} else {
None
}
}
fn pick_proxy_symbol(expr: Atom<'_>, var: Symbol) -> Option<Symbol> {
for name in ["w", "k", "r", "z"] {
let s = Symbol::new(name);
if s != var && !contains_symbol(expr, s) {
return Some(s);
}
}
None
}
pub(crate) fn integrate_affine_kernel<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
) -> Option<Atom<'a>> {
if is_constant(expr, var) || matches!(expr.node(), AtomNode::Add(_)) {
return None;
}
let factors: Vec<Atom<'a>> = match expr.node() {
AtomNode::Mul(args) => args.to_vec(),
_ => vec![expr],
};
let mut rest: Vec<Atom<'a>> = Vec::new();
let mut kern: Option<(AffineKernel<'a>, i64)> = None;
let mut cofactor: Vec<Atom<'a>> = Vec::new();
let mut cofactor_deg = 0usize;
let mut recip = 0i64;
for f in factors {
if is_constant(f, var) {
rest.push(f);
continue;
}
if kern.is_none() {
if let Some(k) = match_affine_kernel(ctx, f, var) {
kern = Some((k, 1));
continue;
}
if let AtomNode::Pow(b, e) = f.node()
&& let AtomNode::Num(m) = e.node()
&& *m != 0
&& (-MAX_KERNEL_POW..=MAX_KERNEL_POW).contains(m)
&& let Some(k) = match_affine_kernel(ctx, *b, var)
{
kern = Some((k, *m));
continue;
}
}
if recip == 0
&& let Some(j) = reciprocal_power(f, var)
{
recip = j;
cofactor.push(f);
continue;
}
if let Some(d) = poly_deg(f, var)
&& d >= 1
&& cofactor_deg + d <= MAX_COFACTOR_DEG
{
cofactor_deg += d;
cofactor.push(f);
continue;
}
return None;
}
let (k, m) = kern?;
let w_sym = pick_proxy_symbol(expr, var)?;
let x = ctx.var(var.as_str());
let w = ctx.var(w_sym.as_str());
let lin = normalize(ctx, ctx.add(&[ctx.mul(&[k.sigma, x]), w]));
let mut rewritten: Vec<Atom<'a>> = rest;
rewritten.push(int_pow(ctx, lin, m));
rewritten.extend(cofactor);
let product = normalize(ctx, ctx.mul(&rewritten));
if node_count(product) > MAX_KERNEL_NODES {
return None;
}
let expanded = crate::expand::expand_bounded(ctx, product).unwrap_or(product);
let folded = crate::ode::util::collect_terms(ctx, normalize(ctx, expanded));
if node_count(folded) > MAX_KERNEL_NODES {
return None;
}
let chain = integrate_expanded_terms(ctx, folded, var)?;
if contains_integral(chain) {
return None;
}
let back = normalize(ctx, ctx.add(&[k.base, ctx.mul(&[ctx.num(-1), k.sigma, x])]));
let res = normalize(ctx, replace_symbol(ctx, chain, w_sym, back));
if contains_symbol(res, w_sym) {
return None;
}
Some(res)
}
fn family_kernel_of_u<'a>(ctx: &'a AtomArena<'a>, f: InvFun, u: Atom<'a>) -> Atom<'a> {
let u2 = int_pow(ctx, u, 2);
match f {
InvFun::Asin | InvFun::Acos | InvFun::Atanh | InvFun::Asech => {
normalize(ctx, ctx.add(&[ctx.num(1), ctx.mul(&[ctx.num(-1), u2])]))
}
InvFun::Atan | InvFun::Asinh | InvFun::Acsch => normalize(ctx, ctx.add(&[ctx.num(1), u2])),
InvFun::Acosh | InvFun::Acsc | InvFun::Asec | InvFun::Acoth => {
normalize(ctx, ctx.add(&[ctx.num(-1), u2]))
}
}
}
fn match_inv_power_rational<'a>(
ctx: &'a AtomArena<'a>,
factor: Atom<'a>,
var: Symbol,
) -> Option<(InvFun, Atom<'a>, Atom<'a>, Atom<'a>, i64, i64)> {
if let AtomNode::Fun(name, args) = factor.node()
&& name.as_str() == "sqrt"
&& args.len() == 1
&& let Some((f, arg, a, b)) = match_inv_base(ctx, args[0], var)
{
return Some((f, arg, a, b, 1, 2));
}
if let AtomNode::Pow(b, e) = factor.node()
&& let Some((p, q)) = rat_of(*e)
&& q > 0
&& q <= MAX_RAT_Q
&& p >= 1
&& p <= MAX_POWER_K * q
&& let Some((f, arg, a, bb)) = match_inv_base(ctx, *b, var)
{
return Some((f, arg, a, bb, p, q));
}
let (f, arg, a, b) = match_inv_base(ctx, factor, var)?;
Some((f, arg, a, b, 1, 1))
}
fn flatten_powers<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, budget: &mut usize) -> Atom<'a> {
if *budget == 0 {
return expr;
}
*budget -= 1;
match expr.node() {
AtomNode::Num(_) | AtomNode::Var(_) => expr,
AtomNode::Add(args) => {
let out: Vec<Atom<'a>> = args
.iter()
.map(|a| flatten_powers(ctx, *a, budget))
.collect();
ctx.add(&out)
}
AtomNode::Mul(args) => {
let out: Vec<Atom<'a>> = args
.iter()
.map(|a| flatten_powers(ctx, *a, budget))
.collect();
ctx.mul(&out)
}
AtomNode::Pow(b, e) => {
let nb = flatten_powers(ctx, *b, budget);
let ne = flatten_powers(ctx, *e, budget);
if let AtomNode::Num(n) = ne.node()
&& *n >= 2
&& let AtomNode::Mul(fs) = nb.node()
{
let out: Vec<Atom<'a>> = fs
.iter()
.map(|f| ctx.pow(flatten_powers(ctx, *f, budget), ne))
.collect();
return ctx.mul(&out);
}
ctx.pow(nb, ne)
}
AtomNode::Fun(name, args) => {
let out: Vec<Atom<'a>> = args
.iter()
.map(|a| flatten_powers(ctx, *a, budget))
.collect();
ctx.fun(name.as_str(), &out)
}
}
}
fn companion_normal_form<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>) -> Atom<'a> {
let expanded = crate::expand::expand_bounded(ctx, expr).unwrap_or(expr);
let mut budget = MAX_CANCEL_BUDGET;
let flat = flatten_powers(ctx, expanded, &mut budget);
crate::ode::util::collect_terms(ctx, normalize(ctx, flat))
}
fn radicand_scale<'a>(
ctx: &'a AtomArena<'a>,
r: Atom<'a>,
e_u: Atom<'a>,
x_root: Atom<'a>,
var: Symbol,
) -> Option<Atom<'a>> {
let re = companion_normal_form(ctx, r);
let ee = companion_normal_form(ctx, e_u);
for point in [x_root, ctx.num(0)] {
let rp = companion_normal_form(ctx, replace_symbol(ctx, re, var, point));
let ep = companion_normal_form(ctx, replace_symbol(ctx, ee, var, point));
if !is_constant(rp, var) || is_zero_atom(ctx, ep) {
continue;
}
let s = normalize(ctx, ctx.mul(&[rp, inv(ctx, ep)]));
if is_zero_atom(ctx, s) {
continue;
}
let diff = ctx.add(&[re, ctx.mul(&[ctx.num(-1), s, ee])]);
let folded = companion_normal_form(ctx, diff);
if folds_to_zero(ctx, folded) {
return Some(s);
}
}
None
}
pub(crate) fn integrate_companion_form<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
) -> Option<Atom<'a>> {
if is_constant(expr, var) || matches!(expr.node(), AtomNode::Add(_)) {
return None;
}
let factors: Vec<Atom<'a>> = match expr.node() {
AtomNode::Mul(args) => args.to_vec(),
_ => vec![expr],
};
let mut rest: Vec<Atom<'a>> = Vec::new();
let mut inv_f: Option<(InvFun, Atom<'a>, Atom<'a>, Atom<'a>, i64, i64)> = None;
let mut kern: Option<(Atom<'a>, KernelExp)> = None;
for f in factors {
if is_constant(f, var) {
rest.push(f);
continue;
}
if inv_f.is_none()
&& let Some(m) = match_inv_power_rational(ctx, f, var)
{
inv_f = Some(m);
continue;
}
if kern.is_none()
&& let Some(kd) = match_kernel(f)
{
kern = Some(kd);
continue;
}
return None;
}
let (f, arg, a, b, p, q) = inv_f?;
let (r, ke) = kern?;
if Some(ke) != family_kernel_exp(f) {
return None;
}
let (sigma, rho) = linear_form(ctx, arg, var)?;
if is_zero_atom(ctx, sigma) {
return None;
}
let e_u = family_kernel_of_u(ctx, f, arg);
let x_root = normalize(ctx, ctx.mul(&[ctx.num(-1), rho, inv(ctx, sigma)]));
let s_atom = radicand_scale(ctx, r, e_u, x_root, var)?;
if ke == KernelExp::InvSqrt && !sqrt_norm_positive(s_atom) {
return None;
}
let s_factor = match ke {
KernelExp::Recip => inv(ctx, s_atom),
KernelExp::InvSqrt => ctx.pow(s_atom, rat_atom(ctx, -1, 2)),
};
let base = normalize(ctx, ctx.add(&[a, ctx.mul(&[b, ctx.fun(f.name(), &[arg])])]));
let mut out = rest;
if family_sign(f)? < 0 {
out.push(ctx.num(-1));
}
out.push(s_factor);
out.push(ctx.pow(base, rat_atom(ctx, p + q, q)));
let denom = normalize(ctx, ctx.mul(&[b, sigma, rat_atom(ctx, p + q, q)]));
out.push(inv(ctx, denom));
Some(normalize(ctx, ctx.mul(&out)))
}
const MAX_IS_K: i64 = 6;
const MAX_IS_POLY_DEG: usize = 6;
const MAX_IS_SUBST_NODES: usize = 400;
const MAX_IS_MU: i64 = 8;
const MAX_IS_RAD_M: i64 = 5;
const MAX_IS_WORK: u64 = 100_000;
fn is_work_exceeded(work: &mut u64, nodes: usize) -> bool {
*work = work.saturating_add(nodes as u64);
*work > MAX_IS_WORK
}
fn family_g_head(f: InvFun) -> &'static str {
match f {
InvFun::Asin => "sin",
InvFun::Acos => "cos",
InvFun::Atan => "tan",
InvFun::Asinh => "sinh",
InvFun::Acosh => "cosh",
InvFun::Atanh => "tanh",
InvFun::Acsc => "csc",
InvFun::Asec => "sec",
InvFun::Acoth => "coth",
InvFun::Asech => "sech",
InvFun::Acsch => "csch",
}
}
fn family_gp_terms(f: InvFun) -> (i64, &'static [(&'static str, i64)]) {
match f {
InvFun::Asin => (1, &[("cos", 1)]),
InvFun::Acos => (-1, &[("sin", 1)]),
InvFun::Atan => (1, &[("sec", 2)]),
InvFun::Asinh => (1, &[("cosh", 1)]),
InvFun::Acosh => (1, &[("sinh", 1)]),
InvFun::Atanh => (1, &[("sech", 2)]),
InvFun::Acsc => (-1, &[("csc", 1), ("cot", 1)]),
InvFun::Asec => (1, &[("sec", 1), ("tan", 1)]),
InvFun::Acoth => (-1, &[("csch", 2)]),
InvFun::Asech => (-1, &[("sech", 1), ("tanh", 1)]),
InvFun::Acsch => (-1, &[("csch", 1), ("coth", 1)]),
}
}
fn family_kernel_head(f: InvFun) -> &'static str {
match f {
InvFun::Asin => "cos",
InvFun::Acos => "sin",
InvFun::Atan => "sec",
InvFun::Asinh => "cosh",
InvFun::Acosh => "sinh",
InvFun::Atanh => "sech",
InvFun::Acsc => "cot",
InvFun::Asec => "tan",
InvFun::Acoth => "csch",
InvFun::Asech => "tanh",
InvFun::Acsch => "coth",
}
}
struct IsBackSub<'a> {
t_sym: Symbol,
f: InvFun,
fw: Atom<'a>,
w: Atom<'a>,
e_w: Atom<'a>,
sqrt_e: Atom<'a>,
}
fn bs_power<'a>(ctx: &'a AtomArena<'a>, bs: &IsBackSub<'a>, c: Atom<'a>, k: i64) -> Atom<'a> {
if c == bs.sqrt_e {
if k % 2 == 0 {
int_pow(ctx, bs.e_w, k / 2)
} else {
normalize(
ctx,
ctx.mul(&[int_pow(ctx, bs.e_w, (k - 1) / 2), bs.sqrt_e]),
)
}
} else {
int_pow(ctx, c, k)
}
}
fn is_head_map<'a>(ctx: &'a AtomArena<'a>, bs: &IsBackSub<'a>, name: &str) -> Option<Atom<'a>> {
let w = bs.w;
let r = bs.sqrt_e;
let w_over_r = |ctx: &'a AtomArena<'a>| normalize(ctx, ctx.mul(&[w, inv(ctx, r)]));
let r_over_w = |ctx: &'a AtomArena<'a>| normalize(ctx, ctx.mul(&[r, inv(ctx, w)]));
Some(match bs.f {
InvFun::Asin => match name {
"sin" => w,
"cos" => r,
"tan" => w_over_r(ctx),
"sec" => inv(ctx, r),
"cot" => r_over_w(ctx),
"csc" => inv(ctx, w),
_ => return None,
},
InvFun::Acos => match name {
"cos" => w,
"sin" => r,
"tan" => r_over_w(ctx),
"sec" => inv(ctx, w),
"cot" => w_over_r(ctx),
"csc" => inv(ctx, r),
_ => return None,
},
InvFun::Atan => match name {
"tan" => w,
"sec" => r,
"sin" => w_over_r(ctx),
"cos" => inv(ctx, r),
"cot" => inv(ctx, w),
"csc" => r_over_w(ctx),
_ => return None,
},
InvFun::Asinh => match name {
"sinh" => w,
"cosh" => r,
"tanh" => w_over_r(ctx),
"sech" => inv(ctx, r),
"coth" => r_over_w(ctx),
"csch" => inv(ctx, w),
_ => return None,
},
InvFun::Acosh => match name {
"cosh" => w,
"sinh" => r,
"tanh" => r_over_w(ctx),
"sech" => inv(ctx, w),
"coth" => w_over_r(ctx),
"csch" => inv(ctx, r),
_ => return None,
},
InvFun::Atanh => match name {
"tanh" => w,
"sech" => r,
"sinh" => w_over_r(ctx),
"cosh" => inv(ctx, r),
"coth" => inv(ctx, w),
"csch" => r_over_w(ctx),
_ => return None,
},
InvFun::Acsc => match name {
"csc" => w,
"cot" => r,
"sin" => inv(ctx, w),
"cos" => r_over_w(ctx),
"tan" => inv(ctx, r),
"sec" => w_over_r(ctx),
_ => return None,
},
InvFun::Asec => match name {
"sec" => w,
"tan" => r,
"cos" => inv(ctx, w),
"sin" => r_over_w(ctx),
"cot" => inv(ctx, r),
"csc" => w_over_r(ctx),
_ => return None,
},
InvFun::Acoth => match name {
"coth" => w,
"csch" => r,
"sinh" => inv(ctx, r),
"cosh" => w_over_r(ctx),
"tanh" => inv(ctx, w),
"sech" => r_over_w(ctx),
_ => return None,
},
InvFun::Asech => match name {
"sech" => w,
"tanh" => r,
"cosh" => inv(ctx, w),
"sinh" => r_over_w(ctx),
"coth" => inv(ctx, r),
"csch" => w_over_r(ctx),
_ => return None,
},
InvFun::Acsch => match name {
"csch" => w,
"coth" => r,
"sinh" => inv(ctx, w),
"cosh" => r_over_w(ctx),
"tanh" => inv(ctx, r),
"sech" => w_over_r(ctx),
_ => return None,
},
})
}
fn u_multiple(arg: Atom<'_>, t_sym: Symbol) -> Option<i64> {
match arg.node() {
AtomNode::Num(n) if *n >= 0 => Some(*n),
AtomNode::Var(v) if *v == t_sym => Some(1),
AtomNode::Mul(args) => {
let mut mu: Option<i64> = None;
let mut seen_t = false;
for a in args.iter() {
match a.node() {
AtomNode::Num(n) if *n >= 0 && mu.is_none() => mu = Some(*n),
AtomNode::Var(v) if *v == t_sym && !seen_t => seen_t = true,
_ => return None,
}
}
if seen_t { Some(mu.unwrap_or(1)) } else { None }
}
_ => None,
}
}
fn chebyshev_t_coeffs(n: i64) -> Vec<i64> {
if n == 0 {
return vec![1];
}
let mut prev = vec![1];
let mut cur = vec![0, 1];
for _ in 1..n {
let mut next = vec![0i64; cur.len() + 1];
for (k, a) in cur.iter().enumerate() {
next[k + 1] += 2 * a;
}
for (k, a) in prev.iter().enumerate() {
next[k] -= a;
}
prev = cur;
cur = next;
}
cur
}
fn chebyshev_u_coeffs(n: i64) -> Vec<i64> {
if n == 0 {
return vec![1];
}
let mut prev = vec![1];
let mut cur = vec![0, 2];
for _ in 1..n {
let mut next = vec![0i64; cur.len() + 1];
for (k, a) in cur.iter().enumerate() {
next[k + 1] += 2 * a;
}
for (k, a) in prev.iter().enumerate() {
next[k] -= a;
}
prev = cur;
cur = next;
}
cur
}
fn chebyshev_eval<'a>(
ctx: &'a AtomArena<'a>,
bs: &IsBackSub<'a>,
coeffs: &[i64],
c: Atom<'a>,
) -> Atom<'a> {
let mut terms: Vec<Atom<'a>> = Vec::new();
for (k, a) in coeffs.iter().enumerate() {
if *a == 0 {
continue;
}
terms.push(normalize(
ctx,
ctx.mul(&[ctx.num(*a), bs_power(ctx, bs, c, k as i64)]),
));
}
match terms.len() {
0 => ctx.num(0),
1 => terms[0],
_ => normalize(ctx, ctx.add(&terms)),
}
}
fn is_trig_back_map<'a>(
ctx: &'a AtomArena<'a>,
bs: &IsBackSub<'a>,
name: &str,
mu: i64,
) -> Option<Atom<'a>> {
if mu > MAX_IS_MU {
return None;
}
if mu == 0 {
return Some(match name {
"cos" | "cosh" | "sec" | "sech" => ctx.num(1),
_ => ctx.num(0),
});
}
if mu == 1 {
return is_head_map(ctx, bs, name);
}
let (s_name, c_name) = match name {
"sin" | "cos" => ("sin", "cos"),
"sinh" | "cosh" => ("sinh", "cosh"),
_ => return None,
};
let c = is_head_map(ctx, bs, c_name)?;
if matches!(name, "cos" | "cosh") {
return Some(chebyshev_eval(ctx, bs, &chebyshev_t_coeffs(mu), c));
}
let s = is_head_map(ctx, bs, s_name)?;
let u_poly = chebyshev_eval(ctx, bs, &chebyshev_u_coeffs(mu - 1), c);
Some(normalize(ctx, ctx.mul(&[s, u_poly])))
}
fn is_inv_pair(f: &str, g: &str) -> bool {
matches!(
(f, g),
("asin", "sin")
| ("acos", "cos")
| ("atan", "tan")
| ("asinh", "sinh")
| ("acosh", "cosh")
| ("atanh", "tanh")
| ("acsc", "csc")
| ("asec", "sec")
| ("acoth", "coth")
| ("asech", "sech")
| ("acsch", "csch")
)
}
fn is_back_sub<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, bs: &IsBackSub<'a>) -> Option<Atom<'a>> {
match expr.node() {
AtomNode::Num(_) => Some(expr),
AtomNode::Var(v) => Some(if *v == bs.t_sym { bs.fw } else { expr }),
AtomNode::Add(args) => {
let out: Vec<Atom<'a>> = args
.iter()
.map(|a| is_back_sub(ctx, *a, bs))
.collect::<Option<_>>()?;
Some(ctx.add(&out))
}
AtomNode::Mul(args) => {
let out: Vec<Atom<'a>> = args
.iter()
.map(|a| is_back_sub(ctx, *a, bs))
.collect::<Option<_>>()?;
Some(ctx.mul(&out))
}
AtomNode::Pow(b, e) => {
let nb = is_back_sub(ctx, *b, bs)?;
let ne = is_back_sub(ctx, *e, bs)?;
if let AtomNode::Num(k) = ne.node() {
return Some(bs_power(ctx, bs, nb, *k));
}
Some(ctx.pow(nb, ne))
}
AtomNode::Fun(name, args) => {
let n = name.as_str();
if args.len() == 1 {
if matches!(
n,
"sin"
| "cos"
| "tan"
| "sec"
| "cot"
| "csc"
| "sinh"
| "cosh"
| "tanh"
| "sech"
| "coth"
| "csch"
) {
return match u_multiple(args[0], bs.t_sym) {
Some(mu) => is_trig_back_map(ctx, bs, n, mu),
None if contains_symbol(args[0], bs.t_sym) => None,
None => {
let a0 = is_back_sub(ctx, args[0], bs)?;
Some(ctx.fun(n, &[a0]))
}
};
}
if inv_fun(n).is_some() && contains_symbol(args[0], bs.t_sym) {
if bs.f == InvFun::Asech && n == "asin" {
if let AtomNode::Fun(inner, iargs) = args[0].node()
&& iargs.len() == 1
&& inner.as_str() == "tanh"
&& let Some(mu) = u_multiple(iargs[0], bs.t_sym)
&& mu == 1
{
return Some(ctx.fun("acos", &[bs.w]));
}
}
if let AtomNode::Fun(inner, iargs) = args[0].node()
&& iargs.len() == 1
&& !is_inv_pair(n, inner.as_str())
&& matches!(
inner.as_str(),
"sin" | "cos" | "tan" | "sec" | "sinh" | "cosh" | "tanh" | "sech"
)
&& let Some(mu) = u_multiple(iargs[0], bs.t_sym)
{
let v = is_trig_back_map(ctx, bs, inner.as_str(), mu)?;
return Some(ctx.fun(n, &[v]));
}
return None;
}
}
let out: Vec<Atom<'a>> = args
.iter()
.map(|a| is_back_sub(ctx, *a, bs))
.collect::<Option<_>>()?;
Some(ctx.fun(n, &out))
}
}
}
fn contains_inv_composite(expr: Atom<'_>) -> bool {
match expr.node() {
AtomNode::Num(_) | AtomNode::Var(_) => false,
AtomNode::Add(args) | AtomNode::Mul(args) => {
args.iter().any(|a| contains_inv_composite(*a))
}
AtomNode::Pow(b, e) => contains_inv_composite(*b) || contains_inv_composite(*e),
AtomNode::Fun(name, args) => {
if args.len() == 1
&& inv_fun(name.as_str()).is_some()
&& let AtomNode::Fun(inner, _) = args[0].node()
&& is_inv_pair(name.as_str(), inner.as_str())
{
return true;
}
args.iter().any(|a| contains_inv_composite(*a))
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum RatioFam {
TanSec,
TanhSech,
CotCsc,
CothCsch,
}
impl RatioFam {
fn heads(self) -> (&'static str, &'static str) {
match self {
RatioFam::TanSec => ("tan", "sec"),
RatioFam::TanhSech => ("tanh", "sech"),
RatioFam::CotCsc => ("cot", "csc"),
RatioFam::CothCsch => ("coth", "csch"),
}
}
fn eps(self) -> i64 {
match self {
RatioFam::TanSec | RatioFam::TanhSech => 1,
RatioFam::CotCsc | RatioFam::CothCsch => -1,
}
}
fn delta(self) -> i64 {
match self {
RatioFam::TanSec => 1,
RatioFam::TanhSech | RatioFam::CotCsc | RatioFam::CothCsch => -1,
}
}
fn eta(self) -> i64 {
match self {
RatioFam::TanSec | RatioFam::TanhSech | RatioFam::CotCsc => 1,
RatioFam::CothCsch => -1,
}
}
fn gamma(self) -> i64 {
match self {
RatioFam::TanSec | RatioFam::CotCsc => 1,
RatioFam::TanhSech | RatioFam::CothCsch => -1,
}
}
fn beta(self) -> i64 {
self.eta() * self.gamma()
}
fn hyper(self) -> bool {
matches!(self, RatioFam::TanhSech | RatioFam::CothCsch)
}
fn s2_coeffs(self, k: i64) -> Vec<i64> {
let (eta, beta) = (self.eta(), self.beta());
(0..=k)
.map(|i| binom(k, i) * eta.pow((k - i) as u32) * beta.pow(i as u32))
.collect()
}
fn r2_coeffs(self, k: i64) -> Vec<i64> {
let (eta, beta) = (self.eta(), self.beta());
(0..=k)
.map(|i| binom(k, i) * beta.pow(k as u32) * (-eta).pow((k - i) as u32))
.collect()
}
}
fn sc_mul<'a>(ctx: &'a AtomArena<'a>, s: i64, a: Atom<'a>) -> Atom<'a> {
match s {
1 => a,
-1 => normalize(ctx, ctx.mul(&[ctx.num(-1), a])),
_ => normalize(ctx, ctx.mul(&[ctx.num(s), a])),
}
}
fn scan_ratio_term<'a>(
term: Atom<'a>,
t_sym: Symbol,
fam: RatioFam,
) -> Option<(Vec<Atom<'a>>, i64, i64, i64)> {
let (tan, sec) = fam.heads();
let factors: Vec<Atom<'a>> = match term.node() {
AtomNode::Mul(args) => args.to_vec(),
_ => vec![term],
};
let mut coeff: Vec<Atom<'a>> = Vec::new();
let mut j = 0i64;
let mut tan_pow = 0i64;
let mut sec_pow = 0i64;
for f in factors {
if is_constant(f, t_sym) {
coeff.push(f);
continue;
}
match f.node() {
AtomNode::Var(v) if *v == t_sym => j += 1,
AtomNode::Pow(b, e) if matches!(b.node(), AtomNode::Var(v) if *v == t_sym) => {
let AtomNode::Num(n) = e.node() else {
return None;
};
if *n < 0 {
return None;
}
j += n;
}
AtomNode::Fun(name, args)
if args.len() == 1 && matches!(args[0].node(), AtomNode::Var(v) if *v == t_sym) =>
{
if name.as_str() == tan {
tan_pow += 1;
} else if name.as_str() == sec {
sec_pow += 1;
} else {
return None;
}
}
AtomNode::Pow(b, e) => {
let AtomNode::Fun(name, args) = b.node() else {
return None;
};
if args.len() != 1 || !matches!(args[0].node(), AtomNode::Var(v) if *v == t_sym) {
return None;
}
let AtomNode::Num(n) = e.node() else {
return None;
};
if name.as_str() == tan && *n > 0 {
tan_pow += n;
} else if name.as_str() == sec && *n != 0 {
sec_pow += n;
} else {
return None;
}
}
_ => return None,
}
}
if j > MAX_IS_K || tan_pow > MAX_IS_POLY_DEG as i64 || sec_pow.abs() > 2 + MAX_IS_RAD_M {
return None;
}
Some((coeff, j, tan_pow, sec_pow))
}
fn ratio_i<'a>(
ctx: &'a AtomArena<'a>,
j: i64,
a: i64,
t: Atom<'a>,
fam: RatioFam,
) -> Option<Atom<'a>> {
if !(0..=MAX_IS_K).contains(&j) || !(0..=2 * MAX_IS_MU).contains(&a) {
return None;
}
let (tan, _) = fam.heads();
let eps = fam.eps();
let r_t = ctx.fun(tan, &[t]);
let boundary = normalize(
ctx,
ctx.mul(&[
ctx.num(eps),
int_pow(ctx, t, j),
int_pow(ctx, r_t, a + 1),
inv(ctx, ctx.num(a + 1)),
]),
);
if j == 0 {
return Some(boundary);
}
let inner = ratio_j(ctx, j - 1, a + 1, t, fam)?;
let coeff = rat_atom(ctx, -j * eps, a + 1);
Some(normalize(
ctx,
ctx.add(&[boundary, ctx.mul(&[coeff, inner])]),
))
}
fn ratio_j<'a>(
ctx: &'a AtomArena<'a>,
j: i64,
c: i64,
t: Atom<'a>,
fam: RatioFam,
) -> Option<Atom<'a>> {
if !(0..=MAX_IS_K).contains(&j) || !(0..=2 * MAX_IS_MU).contains(&c) {
return None;
}
match c {
0 => Some(normalize(
ctx,
ctx.mul(&[int_pow(ctx, t, j + 1), inv(ctx, ctx.num(j + 1))]),
)),
1 => {
if j >= 1 {
return None;
}
let (head, sign) = match fam {
RatioFam::TanSec => ("cos", -1),
RatioFam::TanhSech => ("cosh", 1),
RatioFam::CotCsc => ("sin", 1),
RatioFam::CothCsch => ("sinh", 1),
};
let log = ctx.fun("log", &[ctx.fun(head, &[t])]);
Some(if sign < 0 {
normalize(ctx, ctx.mul(&[ctx.num(-1), log]))
} else {
log
})
}
_ => {
let jm = ratio_j(ctx, j, c - 2, t, fam)?;
let im = ratio_i(ctx, j, c - 2, t, fam)?;
let (beta, be) = (fam.beta(), fam.beta() * fam.eta());
Some(normalize(
ctx,
ctx.add(&[sc_mul(ctx, beta, im), sc_mul(ctx, -be, jm)]),
))
}
}
}
fn integrate_ratio_term<'a>(
ctx: &'a AtomArena<'a>,
term: Atom<'a>,
t_sym: Symbol,
fam: RatioFam,
) -> Option<Atom<'a>> {
let (coeff, j, a, b) = scan_ratio_term(term, t_sym, fam)?;
let t = ctx.var(t_sym.as_str());
if b < 0 {
let c_pow = -a - b;
if c_pow < 0 {
return None;
}
let (s_name, c_name) = match fam {
RatioFam::TanSec => ("sin", "cos"),
RatioFam::TanhSech => ("sinh", "cosh"),
RatioFam::CotCsc => ("cos", "sin"),
RatioFam::CothCsch => ("cosh", "sinh"),
};
let mut fs = coeff;
if j > 0 {
fs.push(int_pow(ctx, t, j));
}
if a > 0 {
fs.push(int_pow(ctx, ctx.fun(s_name, &[t]), a));
}
if c_pow > 0 {
fs.push(int_pow(ctx, ctx.fun(c_name, &[t]), c_pow));
}
let conv = normalize(ctx, ctx.mul(&fs));
return integrate_sincos_term(ctx, conv, t_sym, fam.hyper());
}
let core = ratio_rs(ctx, j, a, b, t, fam)?;
let mut fs = coeff;
fs.push(core);
Some(normalize(ctx, ctx.mul(&fs)))
}
fn integrate_sincos_term<'a>(
ctx: &'a AtomArena<'a>,
term: Atom<'a>,
t_sym: Symbol,
hyper: bool,
) -> Option<Atom<'a>> {
if hyper {
let t = ctx.var(t_sym.as_str());
let (coeff, j, na, nb) = scan_hyp_term(ctx, term, t_sym, MAX_IS_K)?;
let mut reduced: Vec<Atom<'a>> = Vec::new();
reduce_hyp_term(ctx, &coeff, j, na, nb, t, &mut reduced)?;
let t_form = normalize(ctx, ctx.add(&reduced));
if node_count(t_form) > MAX_REDUCED_NODES {
return None;
}
let rterms: Vec<Atom<'a>> = match t_form.node() {
AtomNode::Add(args) => args.to_vec(),
_ => vec![t_form],
};
let mut out = Vec::with_capacity(rterms.len());
for rt in rterms {
out.push(integrate_hyp_term(ctx, rt, t_sym, MAX_IS_K)?);
}
return Some(normalize(ctx, ctx.add(&out)));
}
let reduced = match super::trig_reduce::trig_reduce_products(ctx, term, t_sym) {
Some(r) => crate::ode::util::collect_terms(ctx, normalize(ctx, r)),
None => term,
};
if node_count(reduced) > MAX_REDUCED_NODES {
return None;
}
let rterms: Vec<Atom<'a>> = match reduced.node() {
AtomNode::Add(args) => args.to_vec(),
_ => vec![reduced],
};
let mut out = Vec::with_capacity(rterms.len());
for rt in rterms {
out.push(integrate_trig_term(ctx, rt, t_sym)?);
}
Some(normalize(ctx, ctx.add(&out)))
}
fn ratio_rs<'a>(
ctx: &'a AtomArena<'a>,
j: i64,
a: i64,
b: i64,
t: Atom<'a>,
fam: RatioFam,
) -> Option<Atom<'a>> {
if !(0..=MAX_IS_K).contains(&j) || !(0..=2 * MAX_IS_MU).contains(&a) {
return None;
}
if b == 0 {
return ratio_j(ctx, j, a, t, fam);
}
if !(1..=2 * MAX_IS_MU).contains(&b) {
return None;
}
let (_, sec) = fam.heads();
let s_t = ctx.fun(sec, &[t]);
if b % 2 == 0 {
let k = (b - 2) / 2;
let mut terms = Vec::new();
for (i, c) in fam.s2_coeffs(k).iter().enumerate() {
let core = ratio_i(ctx, j, a + 2 * i as i64, t, fam)?;
terms.push(sc_mul(ctx, *c, core));
}
return Some(normalize(ctx, ctx.add(&terms)));
}
if a == 0 {
return ratio_s_pow(ctx, j, b, t, fam);
}
let sgn = fam.delta();
if a % 2 == 1 {
let k = (a - 1) / 2;
let v_terms: Vec<(i64, i64)> = fam
.r2_coeffs(k)
.iter()
.enumerate()
.map(|(i, c)| (*c, b + 2 * i as i64))
.collect();
let mut v_atoms = Vec::with_capacity(v_terms.len());
let mut inner = Vec::with_capacity(v_terms.len());
for (c, p) in v_terms {
let coeff = rat_atom(ctx, c, sgn * p);
v_atoms.push(normalize(ctx, ctx.mul(&[coeff, int_pow(ctx, s_t, p)])));
if j > 0 {
let red = ratio_s_pow(ctx, j - 1, p, t, fam)?;
inner.push(normalize(ctx, ctx.mul(&[coeff, red])));
}
}
let v = normalize(ctx, ctx.add(&v_atoms));
let boundary = normalize(ctx, ctx.mul(&[int_pow(ctx, t, j), v]));
if j == 0 {
return Some(boundary);
}
let inner = normalize(ctx, ctx.add(&inner));
return Some(normalize(
ctx,
ctx.add(&[boundary, ctx.mul(&[ctx.num(-j), inner])]),
));
}
let k = a / 2;
let mut terms = Vec::new();
for (i, c) in fam.r2_coeffs(k).iter().enumerate() {
let red = ratio_s_pow(ctx, j, b + 2 * i as i64, t, fam)?;
terms.push(sc_mul(ctx, *c, red));
}
Some(normalize(ctx, ctx.add(&terms)))
}
fn ratio_s_pow<'a>(
ctx: &'a AtomArena<'a>,
j: i64,
b: i64,
t: Atom<'a>,
fam: RatioFam,
) -> Option<Atom<'a>> {
if !(0..=MAX_IS_K).contains(&j) || !(1..=2 * MAX_IS_MU).contains(&b) {
return None;
}
let (tan, sec) = fam.heads();
let r_t = ctx.fun(tan, &[t]);
let s_t = ctx.fun(sec, &[t]);
if b == 1 {
if j >= 1 {
return None;
}
return Some(match fam {
RatioFam::TanhSech => ctx.fun("asin", &[r_t]),
RatioFam::TanSec => ctx.fun("log", &[normalize(ctx, ctx.add(&[s_t, r_t]))]),
RatioFam::CotCsc | RatioFam::CothCsch => normalize(
ctx,
ctx.mul(&[
ctx.num(-1),
ctx.fun("log", &[normalize(ctx, ctx.add(&[s_t, r_t]))]),
]),
),
});
}
if j >= 1 {
return None;
}
let db = fam.delta() * fam.beta();
let den = fam.eps() + (b - 2) * db;
if den == 0 {
return None;
}
let num2 = (b - 2) * db * fam.eta();
let rec = ratio_s_pow(ctx, 0, b - 2, t, fam)?;
Some(normalize(
ctx,
ctx.mul(&[
inv(ctx, ctx.num(den)),
ctx.add(&[
ctx.mul(&[r_t, int_pow(ctx, s_t, b - 2)]),
sc_mul(ctx, num2, rec),
]),
]),
))
}
fn signed_u_multiple(arg: Atom<'_>, t_sym: Symbol) -> Option<i64> {
match arg.node() {
AtomNode::Num(n) => Some(*n),
AtomNode::Var(v) if *v == t_sym => Some(1),
AtomNode::Mul(args) => {
let mut mu: Option<i64> = None;
let mut seen_t = false;
for a in args.iter() {
match a.node() {
AtomNode::Num(n) if mu.is_none() => mu = Some(*n),
AtomNode::Var(v) if *v == t_sym && !seen_t => seen_t = true,
_ => return None,
}
}
if seen_t { Some(mu.unwrap_or(1)) } else { None }
}
_ => None,
}
}
fn integrate_trig_term<'a>(
ctx: &'a AtomArena<'a>,
term: Atom<'a>,
t_sym: Symbol,
) -> Option<Atom<'a>> {
let factors: Vec<Atom<'a>> = match term.node() {
AtomNode::Mul(args) => args.to_vec(),
_ => vec![term],
};
let mut coeff: Vec<Atom<'a>> = Vec::new();
let mut j = 0i64;
let mut trig: Option<(bool, i64)> = None;
for f in factors {
if is_constant(f, t_sym) {
coeff.push(f);
continue;
}
match f.node() {
AtomNode::Var(v) if *v == t_sym => j += 1,
AtomNode::Pow(b, e) if matches!(b.node(), AtomNode::Var(v) if *v == t_sym) => {
let AtomNode::Num(n) = e.node() else {
return None;
};
if *n < 0 {
return None;
}
j += n;
}
AtomNode::Fun(name, args) if args.len() == 1 => {
let is_sin = match name.as_str() {
"sin" => true,
"cos" => false,
_ => return None,
};
if trig.is_some() {
return None;
}
trig = Some((is_sin, signed_u_multiple(args[0], t_sym)?));
}
_ => return None,
}
}
if j > MAX_IS_K {
return None;
}
let t = ctx.var(t_sym.as_str());
let (is_sin, mu) = match trig {
None => {
let mut fs = coeff;
fs.push(int_pow(ctx, t, j + 1));
fs.push(inv(ctx, ctx.num(j + 1)));
return Some(normalize(ctx, ctx.mul(&fs)));
}
Some(h) => h,
};
let (neg, mu) = if mu < 0 { (is_sin, -mu) } else { (false, mu) };
if mu > MAX_IS_MU {
return None;
}
if mu == 0 {
if is_sin {
return Some(ctx.num(0));
}
let mut fs = coeff;
if neg {
fs.push(ctx.num(-1));
}
fs.push(int_pow(ctx, t, j + 1));
fs.push(inv(ctx, ctx.num(j + 1)));
return Some(normalize(ctx, ctx.mul(&fs)));
}
let mut terms = Vec::new();
let mut fall: i64 = 1; for i in 0..=j {
if i > 0 {
fall = fall.checked_mul(j - i + 1)?;
}
let mpow = mu.checked_pow((i + 1) as u32)?;
let (h_name, h_sign) = match (is_sin, i % 4) {
(true, 0) => ("cos", -1),
(true, 1) => ("sin", -1),
(true, 2) => ("cos", 1),
(true, _) => ("sin", 1),
(false, 0) => ("sin", 1),
(false, 1) => ("cos", -1),
(false, 2) => ("sin", -1),
(false, _) => ("cos", 1),
};
let marg = if mu == 1 {
t
} else {
ctx.mul(&[ctx.num(mu), t])
};
let mut fs = coeff.clone();
if ((i % 2 == 1) != (h_sign < 0)) != neg {
fs.push(ctx.num(-1));
}
if fall != 1 {
fs.push(ctx.num(fall));
}
let deg = j - i;
if deg > 0 {
fs.push(int_pow(ctx, t, deg));
}
fs.push(ctx.pow(ctx.num(mpow), ctx.num(-1)));
fs.push(ctx.fun(h_name, &[marg]));
terms.push(normalize(ctx, ctx.mul(&fs)));
}
Some(normalize(ctx, ctx.add(&terms)))
}
fn match_radical_pow(factor: Atom<'_>) -> Option<(Atom<'_>, i64)> {
match factor.node() {
AtomNode::Fun(name, args) if name.as_str() == "sqrt" && args.len() == 1 => {
Some((args[0], 1))
}
AtomNode::Pow(b, e) => {
if let AtomNode::Fun(name, args) = b.node()
&& name.as_str() == "sqrt"
&& args.len() == 1
&& let AtomNode::Num(n) = e.node()
{
if *n != 0 && n.unsigned_abs() <= MAX_IS_RAD_M as u64 {
return Some((args[0], *n));
}
return None;
}
let (p, q) = rat_of(*e)?;
let m = match (p, q) {
(p, 2) if p != 0 && p.unsigned_abs() <= MAX_IS_RAD_M as u64 => p,
(p, 1) if (-2..=-1).contains(&p) => 2 * p,
_ => return None,
};
Some((*b, m))
}
_ => None,
}
}
fn mentions_imaginary(expr: Atom<'_>) -> bool {
match expr.node() {
AtomNode::Num(_) => false,
AtomNode::Var(v) => matches!(v.as_str(), "i" | "I"),
AtomNode::Add(args) | AtomNode::Mul(args) | AtomNode::Fun(_, args) => {
args.iter().any(|a| mentions_imaginary(*a))
}
AtomNode::Pow(b, e) => mentions_imaginary(*b) || mentions_imaginary(*e),
}
}
pub(crate) fn integrate_inv_subst<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
) -> Option<Atom<'a>> {
if super::chain::parts_active() {
return None;
}
if is_constant(expr, var) || matches!(expr.node(), AtomNode::Add(_)) {
return None;
}
if node_count(expr) > MAX_NODES {
return None;
}
let factors: Vec<Atom<'a>> = match expr.node() {
AtomNode::Mul(args) => args.to_vec(),
_ => vec![expr],
};
let mut rest: Vec<Atom<'a>> = Vec::new();
let mut inv_f: Option<InvPow<'a>> = None;
let mut polys: Vec<Atom<'a>> = Vec::new();
let mut rad: Option<(Atom<'a>, i64)> = None;
let mut total_deg = 0usize;
for f in factors {
if is_constant(f, var) {
rest.push(f);
continue;
}
if inv_f.is_none()
&& let Some(m) = match_inv_power(ctx, f, var, MAX_IS_K)
{
inv_f = Some(m);
continue;
}
if let Some(d) = poly_deg(f, var)
&& d >= 1
&& total_deg + d <= MAX_IS_POLY_DEG
{
total_deg += d;
polys.push(f);
continue;
}
if rad.is_none()
&& let Some(rm) = match_radical_pow(f)
{
rad = Some(rm);
continue;
}
return None;
}
let m = inv_f?;
let (sigma, rho) = linear_form(ctx, m.arg, var)?;
if is_zero_atom(ctx, sigma) {
return None;
}
let mut work: u64 = node_count(expr) as u64;
let mut rad_data: Option<(i64, Atom<'a>)> = None;
if let Some((r, rm)) = rad {
let e_u = family_kernel_of_u(ctx, m.f, m.arg);
let x_root = normalize(ctx, ctx.mul(&[ctx.num(-1), rho, inv(ctx, sigma)]));
let s_atom = radicand_scale(ctx, r, e_u, x_root, var)?;
if rm % 2 != 0 && !sqrt_norm_positive(s_atom) {
return None;
}
if mentions_imaginary(r) || mentions_imaginary(s_atom) {
return None;
}
if is_work_exceeded(&mut work, 2 * node_count(r)) {
return None;
}
rad_data = Some((rm, s_atom));
}
let t_sym = pick_subst_symbol(expr, var)?;
let t = ctx.var(t_sym.as_str());
let g_t = ctx.fun(family_g_head(m.f), &[t]);
let x_of_t = {
let num_t = if is_zero_atom(ctx, rho) {
g_t
} else {
normalize(ctx, ctx.add(&[g_t, ctx.mul(&[ctx.num(-1), rho])]))
};
normalize(ctx, ctx.mul(&[num_t, inv(ctx, sigma)]))
};
let mut t_factors = rest;
let base_t = normalize(ctx, ctx.add(&[m.a, ctx.mul(&[m.b, t])]));
t_factors.push(int_pow(ctx, base_t, m.k));
for p in &polys {
t_factors.push(normalize(ctx, replace_symbol(ctx, *p, var, x_of_t)));
}
let (gp_sign, gp_terms) = family_gp_terms(m.f);
if gp_sign < 0 {
t_factors.push(ctx.num(-1));
}
let mut head_powers: Vec<(&'static str, i64)> = gp_terms.to_vec();
if let Some((rm, s_atom)) = &rad_data {
let kernel_head = family_kernel_head(m.f);
match head_powers.iter_mut().find(|(h, _)| *h == kernel_head) {
Some(slot) => slot.1 += rm,
None => head_powers.push((kernel_head, *rm)),
}
t_factors.push(ctx.pow(*s_atom, rat_atom(ctx, *rm, 2)));
if std::env::var_os("OCAS_M4_DEBUG").is_some() {
eprintln!(
"[m4] companion radical (s·E)^({rm}/2) absorbed; sheet factor σ folded to +1 (principal band)"
);
}
}
{
let mut gp_factors: Vec<Atom<'a>> = Vec::with_capacity(head_powers.len() + 1);
for (head, pow) in &head_powers {
if *pow != 0 {
gp_factors.push(int_pow(ctx, ctx.fun(head, &[t]), *pow));
}
}
gp_factors.push(inv(ctx, sigma));
t_factors.push(normalize(ctx, ctx.mul(&gp_factors)));
}
let product = normalize(ctx, ctx.mul(&t_factors));
if is_work_exceeded(&mut work, node_count(product)) {
return None;
}
let expanded = crate::expand::expand_bounded(ctx, product).unwrap_or(product);
let mut flat_budget = MAX_CANCEL_BUDGET;
let flattened = flatten_powers(ctx, expanded, &mut flat_budget);
let folded = crate::ode::util::collect_terms(ctx, normalize(ctx, flattened));
if node_count(folded) > MAX_IS_SUBST_NODES || is_work_exceeded(&mut work, node_count(folded)) {
return None;
}
let terms: Vec<Atom<'a>> = match folded.node() {
AtomNode::Add(args) => args.to_vec(),
_ => vec![folded],
};
let t_answer = match m.f {
InvFun::Asinh | InvFun::Acosh => {
let mut reduced: Vec<Atom<'a>> = Vec::new();
for term in terms {
if is_work_exceeded(&mut work, node_count(term)) {
return None;
}
let (coeff, j, na, nb) = scan_hyp_term(ctx, term, t_sym, MAX_IS_K)?;
reduce_hyp_term(ctx, &coeff, j, na, nb, t, &mut reduced)?;
}
let t_form = normalize(ctx, ctx.add(&reduced));
if node_count(t_form) > MAX_REDUCED_NODES
|| is_work_exceeded(&mut work, node_count(t_form))
{
return None;
}
let rterms: Vec<Atom<'a>> = match t_form.node() {
AtomNode::Add(args) => args.to_vec(),
_ => vec![t_form],
};
let mut out = Vec::with_capacity(rterms.len());
for term in rterms {
if is_work_exceeded(&mut work, node_count(term)) {
return None;
}
out.push(integrate_hyp_term(ctx, term, t_sym, MAX_IS_K)?);
}
normalize(ctx, ctx.add(&out))
}
InvFun::Atan
| InvFun::Atanh
| InvFun::Acsc
| InvFun::Asec
| InvFun::Acoth
| InvFun::Asech
| InvFun::Acsch => {
let fam = match m.f {
InvFun::Atan | InvFun::Asec => RatioFam::TanSec,
InvFun::Atanh | InvFun::Asech => RatioFam::TanhSech,
InvFun::Acsc => RatioFam::CotCsc,
InvFun::Acoth | InvFun::Acsch => RatioFam::CothCsch,
_ => return None,
};
let mut out = Vec::with_capacity(terms.len());
for term in terms {
if is_work_exceeded(&mut work, node_count(term)) {
return None;
}
out.push(integrate_ratio_term(ctx, term, t_sym, fam)?);
}
normalize(ctx, ctx.add(&out))
}
InvFun::Asin | InvFun::Acos => {
let mut out: Vec<Atom<'a>> = Vec::new();
for term in terms {
if is_work_exceeded(&mut work, node_count(term)) {
return None;
}
let reduced = match super::trig_reduce::trig_reduce_products(ctx, term, t_sym) {
Some(r) => crate::ode::util::collect_terms(ctx, normalize(ctx, r)),
None => term,
};
if node_count(reduced) > MAX_REDUCED_NODES
|| is_work_exceeded(&mut work, node_count(reduced))
{
return None;
}
let rterms: Vec<Atom<'a>> = match reduced.node() {
AtomNode::Add(args) => args.to_vec(),
_ => vec![reduced],
};
for rt in rterms {
match integrate_trig_term(ctx, rt, t_sym) {
Some(v) => out.push(v),
None => {
if std::env::var_os("OCAS_M4_DEBUG").is_some() {
eprintln!("[m4] trig term declined: {rt} (from {term})");
}
return None;
}
}
}
}
normalize(ctx, ctx.add(&out))
}
};
let w = m.arg;
let e_w = family_kernel_of_u(ctx, m.f, w);
let sqrt_e = ctx.fun("sqrt", &[e_w]);
let fw = ctx.fun(m.f.name(), &[w]);
let bs = IsBackSub {
t_sym,
f: m.f,
fw,
w,
e_w,
sqrt_e,
};
let ans = is_back_sub(ctx, t_answer, &bs)?;
if is_work_exceeded(&mut work, node_count(ans)) {
return None;
}
let ans = normalize(ctx, ans);
if contains_symbol(ans, t_sym) || contains_integral(ans) || contains_inv_composite(ans) {
return None;
}
Some(ans)
}
pub(crate) fn integrate_inverse_trig<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
) -> Option<Atom<'a>> {
if is_constant(expr, var) || node_count(expr) > MAX_NODES {
return None;
}
let mut budget = MAX_CANCEL_BUDGET;
if let Some(rewritten) = cancel_compositions(ctx, expr, var, &mut budget) {
let rewritten = normalize(ctx, rewritten);
if node_count(rewritten) <= MAX_NODES {
let expanded = crate::expand::expand_bounded(ctx, rewritten).unwrap_or(rewritten);
let folded = crate::ode::util::collect_terms(ctx, normalize(ctx, expanded));
if node_count(folded) <= MAX_KERNEL_NODES
&& let Some(r) = integrate_expanded_terms(ctx, folded, var)
{
return Some(normalize(ctx, r));
}
}
}
integrate_bare_linear(ctx, expr, var)
.or_else(|| integrate_kernel_power(ctx, expr, var))
.or_else(|| integrate_companion_form(ctx, expr, var))
.or_else(|| integrate_affine_kernel(ctx, expr, var))
.or_else(|| integrate_invhyp_subst(ctx, expr, var))
.or_else(|| integrate_inv_subst(ctx, expr, var))
}
#[cfg(test)]
mod tests {
use super::*;
use ocas_core::arena::Arena;
fn parse_norm<'a>(ctx: &'a AtomArena<'a>, s: &str) -> Atom<'a> {
normalize(ctx, ocas_parse::parse(ctx, s).unwrap())
}
fn eval_f64(expr: Atom<'_>, env: &[(Symbol, f64)]) -> Option<f64> {
match expr.node() {
AtomNode::Num(n) => Some(*n as f64),
AtomNode::Var(v) => env.iter().find(|(s, _)| s == v).map(|(_, val)| *val),
AtomNode::Add(args) => args
.iter()
.try_fold(0.0, |acc, a| Some(acc + eval_f64(*a, env)?)),
AtomNode::Mul(args) => args
.iter()
.try_fold(1.0, |acc, a| Some(acc * eval_f64(*a, env)?)),
AtomNode::Pow(b, e) => Some(eval_f64(*b, env)?.powf(eval_f64(*e, env)?)),
AtomNode::Fun(name, args) => {
let v = eval_f64(*args.first()?, env)?;
Some(match name.as_str() {
"sin" => v.sin(),
"cos" => v.cos(),
"tan" => v.tan(),
"cot" => 1.0 / v.tan(),
"sec" => 1.0 / v.cos(),
"csc" => 1.0 / v.sin(),
"exp" => v.exp(),
"log" => v.ln(),
"sqrt" => v.sqrt(),
"sinh" => v.sinh(),
"cosh" => v.cosh(),
"tanh" => v.tanh(),
"coth" => 1.0 / v.tanh(),
"sech" => 1.0 / v.cosh(),
"csch" => 1.0 / v.sinh(),
"asin" => v.asin(),
"acos" => v.acos(),
"atan" => v.atan(),
"acot" => std::f64::consts::FRAC_PI_2 - v.atan(),
"asinh" => v.asinh(),
"acosh" => v.acosh(),
"atanh" => v.atanh(),
"acoth" => (1.0 / v).atanh(),
"acsc" => (1.0 / v).asin(),
"asec" => (1.0 / v).acos(),
"asech" => (1.0 / v).acosh(),
"acsch" => (1.0 / v).asinh(),
_ => return None,
})
}
}
}
fn assert_antiderivative_num<'a>(
ctx: &'a AtomArena<'a>,
integrand: Atom<'a>,
var: Symbol,
consts: &[(Symbol, f64)],
samples: &[f64],
) {
let result = integrate_inverse_trig(ctx, integrand, var).expect("mechanism declined");
assert!(
!result.to_string().contains("Integral"),
"residue: {result}"
);
let d = crate::diff(ctx, result, var);
for &xv in samples {
let mut env = consts.to_vec();
env.push((var, xv));
let lhs = eval_f64(d, &env).expect("eval diff");
let rhs = eval_f64(integrand, &env).expect("eval integrand");
let tol = 1e-6 * rhs.abs().max(1.0);
assert!(
(lhs - rhs).abs() < tol,
"at x={xv}: diff={lhs} integrand={rhs} (result: {result})"
);
}
}
fn assert_declined<'a>(ctx: &'a AtomArena<'a>, input: &str) {
let expr = parse_norm(ctx, input);
assert!(
integrate_inverse_trig(ctx, expr, Symbol::new("x")).is_none(),
"expected None for {input}"
);
}
fn replace_atom<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
target: Atom<'a>,
repl: Atom<'a>,
) -> Atom<'a> {
if expr == target {
return repl;
}
match expr.node() {
AtomNode::Num(_) | AtomNode::Var(_) => expr,
AtomNode::Add(args) => {
let out: Vec<Atom<'a>> = args
.iter()
.map(|a| replace_atom(ctx, *a, target, repl))
.collect();
ctx.add(&out)
}
AtomNode::Mul(args) => {
let out: Vec<Atom<'a>> = args
.iter()
.map(|a| replace_atom(ctx, *a, target, repl))
.collect();
ctx.mul(&out)
}
AtomNode::Pow(b, e) => {
let nb = replace_atom(ctx, *b, target, repl);
let ne = replace_atom(ctx, *e, target, repl);
ctx.pow(nb, ne)
}
AtomNode::Fun(name, args) => {
let out: Vec<Atom<'a>> = args
.iter()
.map(|a| replace_atom(ctx, *a, target, repl))
.collect();
ctx.fun(name.as_str(), &out)
}
}
}
fn assert_antiderivative_branch_shifted<'a>(
ctx: &'a AtomArena<'a>,
integrand: Atom<'a>,
kernel: Atom<'a>,
proxy: Atom<'a>,
var: Symbol,
consts: &[(Symbol, f64)],
samples: &[f64],
) {
let result = integrate_inverse_trig(ctx, integrand, var).expect("mechanism declined");
assert!(
!result.to_string().contains("Integral"),
"residue: {result}"
);
for leaked in ["w", "k", "r", "z"] {
assert!(
!contains_symbol(result, Symbol::new(leaked)),
"proxy symbol {leaked} leaked: {result}"
);
}
let lhs_expr = replace_atom(ctx, result, kernel, proxy);
let rhs_expr = replace_atom(ctx, integrand, kernel, proxy);
let d = crate::diff(ctx, lhs_expr, var);
for &xv in samples {
let mut env = consts.to_vec();
env.push((var, xv));
let lhs = eval_f64(d, &env).expect("eval diff");
let rhs = eval_f64(rhs_expr, &env).expect("eval integrand");
let tol = 1e-6 * rhs.abs().max(1.0);
assert!(
(lhs - rhs).abs() < tol,
"at x={xv}: diff={lhs} integrand={rhs} (result: {result})"
);
}
}
fn m4_assert_solved<'a>(
ctx: &'a AtomArena<'a>,
input: &str,
consts: &[(Symbol, f64)],
samples: &[f64],
) -> Atom<'a> {
let x = Symbol::new("x");
let expr = parse_norm(ctx, input);
let r = integrate_inverse_trig(ctx, expr, x).expect("M4 declined");
assert!(!r.to_string().contains("Integral"), "{input}: residue: {r}");
assert!(
!contains_inv_composite(r),
"{input}: F(G(·)) composite left: {r}"
);
let d = crate::diff(ctx, r, x);
for &xv in samples {
let mut env = consts.to_vec();
env.push((x, xv));
let lhs = eval_f64(d, &env).expect("eval diff");
let rhs = eval_f64(expr, &env).expect("eval integrand");
let tol = 1e-6 * rhs.abs().max(1.0);
assert!(
(lhs - rhs).abs() < tol,
"{input} at x={xv}: diff={lhs} integrand={rhs} (result: {r})"
);
}
r
}
#[test]
fn m4_symbolic_slope_bare_power() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let env = [(Symbol::new("a"), 1.4)];
m4_assert_solved(&ctx, "x^2*acos(a*x)^2", &env, &[0.2, 0.4, 0.6]);
}
#[test]
fn m4_z1_zero_intercept_asin() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
m4_assert_solved(&ctx, "x^2*asin(x)", &[], &[0.2, 0.5, 0.8]);
}
#[test]
fn m4_z2_zero_intercept_asinh() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
m4_assert_solved(&ctx, "x^2*asinh(x)", &[], &[0.3, 0.8, 1.4]);
}
#[test]
fn m4_z3_zero_intercept_acos() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
m4_assert_solved(&ctx, "x*acos(x)", &[], &[0.2, 0.5, 0.8]);
}
#[test]
fn m4_k1_kernel_power_trig() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let env = [(Symbol::new("a"), 0.7), (Symbol::new("b"), 1.1)];
m4_assert_solved(&ctx, "(a + b*asin(x))^2", &env, &[0.2, 0.5, 0.8]);
}
#[test]
fn m4_k2_kernel_power_times_poly() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
m4_assert_solved(&ctx, "x^3*(1 + 2*asin(x))^2", &[], &[0.2, 0.5, 0.8]);
}
#[test]
fn m4_k3_kernel_power_hyp() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let env = [
(Symbol::new("a"), 0.9),
(Symbol::new("b"), -0.8),
(Symbol::new("c"), 1.4),
];
m4_assert_solved(&ctx, "x*(a + b*asinh(c*x))^2", &env, &[0.3, 0.7, 1.1]);
}
#[test]
fn m4_t3_bare_power_atan() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
m4_assert_solved(&ctx, "x*atan(x)^2", &[], &[0.3, 0.8, 1.5]);
}
#[test]
fn m4_b2_bare_power_acos() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
m4_assert_solved(&ctx, "x^2*acos(x)^2", &[], &[0.2, 0.5, 0.8]);
}
#[test]
fn m4_h2_m1_first_claim_regression() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let r = m4_assert_solved(&ctx, "atanh(x)^2/(1 - x^2)", &[], &[0.2, 0.5, 0.8]);
assert!(
r.to_string().contains("(atanh(x))^3"),
"expected M1's atanh^3/3 shape: {r}"
);
}
#[test]
fn m4_a1_m2_first_claim_regression() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let env = [
(Symbol::new("a"), 1.1),
(Symbol::new("b"), 0.7),
(Symbol::new("c"), 0.4),
(Symbol::new("d"), 1.2),
];
m4_assert_solved(&ctx, "(a + b*asinh(c + d*x))^2", &env, &[0.3, 0.8, 1.5]);
}
#[test]
fn m4_atah_tabular_weight() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
m4_assert_solved(&ctx, "x*atanh(x)", &[], &[0.2, 0.5, 0.8]);
m4_assert_solved(&ctx, "x^2*atanh(x)", &[], &[0.2, 0.5, 0.8]);
}
#[test]
fn m4_affine_argument_and_hyp_k5() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let env = [(Symbol::new("c"), 1.3)];
m4_assert_solved(&ctx, "(1 + asinh(c*x))^5", &env, &[0.2, 0.6, 1.0]);
let env2 = [(Symbol::new("c"), 0.5), (Symbol::new("d"), 0.8)];
m4_assert_solved(&ctx, "x*asin(c + d*x)", &env2, &[0.1, 0.3, 0.45]);
}
#[test]
fn m4_declines_spec_r_group() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
assert_declined(&ctx, "1/(1 + asin(x))");
assert_declined(&ctx, "1/acos(x)^3");
assert_declined(&ctx, "(1 + asin(x))^(-5/2)");
assert_declined(&ctx, "atanh(1 + x)/(2 + 2*x)");
assert_declined(&ctx, "x*(a + b*asinh(x))^2/(1 + x^2)");
assert_declined(&ctx, "x^2*(a + b*atanh(c*x))^2/(1 - c^2*x^2)^(-1)");
}
#[test]
fn m4_idempotent_reentry() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = Symbol::new("x");
for input in ["x^2*asin(x)", "(a + b*asin(x))^2"] {
let first = m4_assert_solved(
&ctx,
input,
&[(Symbol::new("a"), 0.7), (Symbol::new("b"), 1.1)],
&[0.3],
);
let second = crate::integral::integrate(&ctx, first, x);
assert!(
!contains_integral(second),
"{input}: answer re-entry left a residue: {second}"
);
}
let t3 = m4_assert_solved(&ctx, "x*atan(x)^2", &[], &[0.3]);
let terms: Vec<Atom> = match t3.node() {
AtomNode::Add(args) => args.to_vec(),
_ => vec![t3],
};
for term in terms {
if let Some(r) = integrate_inverse_trig(&ctx, term, x) {
assert!(!contains_integral(r), "partial guess on {term}: {r}");
}
}
}
#[test]
fn m4_certificate_gate() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = Symbol::new("x");
for input in ["x^2*asin(x)", "x*atan(x)^2", "x*acos(x)", "x^2*acos(x)^2"] {
let expr = parse_norm(&ctx, input);
let r = integrate_inverse_trig(&ctx, expr, x).expect("M4 declined");
crate::integral::certify::certify(&ctx, expr, r, x)
.unwrap_or_else(|e| panic!("{input}: certificate declined ({e:?}): {r}"));
}
let expr = parse_norm(&ctx, "(a + b*asin(x))^2");
let r = integrate_inverse_trig(&ctx, expr, x).expect("M4 declined");
crate::integral::certify::certify(&ctx, expr, r, x)
.unwrap_or_else(|e| panic!("K1: certificate declined ({e:?}): {r}"));
for input in [
"x*asin(x)/sqrt(1 - x^2)",
"x^3*asin(x)/sqrt(1 - x^2)",
"x*(1 - x^2)^(3/2)*atanh(x)",
] {
let expr = parse_norm(&ctx, input);
let r = integrate_inverse_trig(&ctx, expr, x).expect("M4 declined");
crate::integral::certify::certify(&ctx, expr, r, x)
.unwrap_or_else(|e| panic!("{input}: certificate declined ({e:?}): {r}"));
}
}
#[test]
fn m4_cr1_companion_sqrt_inverse() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let r = m4_assert_solved(&ctx, "x*asin(x)/sqrt(1 - x^2)", &[], &[0.2, 0.5, 0.8]);
let rs = r.to_string();
assert!(
rs.contains("sqrt") && rs.contains("asin(x)"),
"expected the √E literal × asin(x) in the answer: {r}"
);
}
#[test]
fn m4_cr2_companion_with_cubic_weight() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
m4_assert_solved(&ctx, "x^3*asin(x)/sqrt(1 - x^2)", &[], &[0.2, 0.5, 0.8]);
}
#[test]
fn m4_cr3_atanh_companion_half3() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let r = m4_assert_solved(&ctx, "x*(1 - x^2)^(3/2)*atanh(x)", &[], &[0.2, 0.5, 0.8]);
assert!(
r.to_string().contains("asin(x)"),
"expected the cross-family asin(w) term: {r}"
);
}
#[test]
fn m4_w5b_symbolic_companion_scale() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let env = [
(Symbol::new("a"), 0.9),
(Symbol::new("b"), 1.1),
(Symbol::new("c"), 0.8),
(Symbol::new("d"), 1.7),
];
m4_assert_solved(
&ctx,
"x^3*(d - c^2*d*x^2)^(1/2)*(a + b*asin(c*x))",
&env,
&[0.2, 0.5, 0.8],
);
m4_assert_solved(
&ctx,
"x^3*(a + b*asin(c*x))^2/(d - c^2*d*x^2)^(1/2)",
&env,
&[0.2, 0.5, 0.8],
);
}
#[test]
fn m4_w5b_companion_all_families() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
m4_assert_solved(&ctx, "x*acos(x)/sqrt(1 - x^2)", &[], &[0.2, 0.5, 0.8]);
m4_assert_solved(&ctx, "x*asinh(x)/sqrt(1 + x^2)", &[], &[0.3, 0.8, 1.4]);
m4_assert_solved(&ctx, "x*sqrt(1 + x^2)*asinh(x)", &[], &[0.3, 0.8, 1.4]);
m4_assert_solved(&ctx, "x*acosh(x)/sqrt(x^2 - 1)", &[], &[1.4, 1.9, 2.4]);
m4_assert_solved(&ctx, "x*(1 + x^2)^(3/2)*atan(x)", &[], &[0.3, 0.8, 1.5]);
}
#[test]
fn m4_w5b_negative_s_power_conversion() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let env = [(Symbol::new("a"), 0.9), (Symbol::new("c"), 1.7)];
m4_assert_solved(
&ctx,
"atanh(a*x)/(c - a^2*c*x^2)^(5/2)",
&env,
&[0.2, 0.5, 0.8],
);
m4_assert_solved(&ctx, "atanh(x)/(1 - x^2)^(3/2)", &[], &[0.2, 0.5, 0.8]);
}
#[test]
fn m4_w5b_declines_non_companion_radicals() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
assert_declined(&ctx, "x*asin(x)*sqrt(d + e*x^2)");
assert_declined(&ctx, "x*asin(x)*sqrt(1 - x^2)*sqrt(1 + x^2)");
assert_declined(&ctx, "x*asinh(x)*sqrt(i + i*x^2)");
assert_declined(&ctx, "x*asin(x)/(1 - x^2)");
assert_declined(&ctx, "x*asinh(x)*sqrt(-4 - 4*x^2)");
assert_declined(&ctx, "x*asin(x)*(1 - x^2)^(n/2)");
}
#[test]
fn m4_w5b_idempotent_reentry() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = Symbol::new("x");
for input in ["x*asin(x)/sqrt(1 - x^2)", "x*(1 - x^2)^(3/2)*atanh(x)"] {
let first = m4_assert_solved(&ctx, input, &[], &[0.3]);
let terms: Vec<Atom> = match first.node() {
AtomNode::Add(args) => args.to_vec(),
_ => vec![first],
};
for term in terms {
if let Some(r) = integrate_inverse_trig(&ctx, term, x) {
assert!(
!contains_integral(r),
"{input}: partial guess on {term}: {r}"
);
}
}
}
}
fn simpson<F: Fn(f64) -> f64>(f: F, a: f64, b: f64, n: usize) -> f64 {
let h = (b - a) / n as f64;
let mut acc = f(a) + f(b);
for i in 1..n {
let x = a + h * i as f64;
acc += if i % 2 == 1 { 4.0 * f(x) } else { 2.0 * f(x) };
}
acc * h / 3.0
}
#[test]
fn m4_w5c_ratio_tabular_numeric() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let t_sym = Symbol::new("t");
let t = ctx.var("t");
let fams = [
("tan/sec", RatioFam::TanSec),
("tanh/sech", RatioFam::TanhSech),
("cot/csc", RatioFam::CotCsc),
("coth/csch", RatioFam::CothCsch),
];
let (x0, x1) = (0.7f64, 1.3f64);
let mut accepted = 0usize;
for (name, fam) in fams {
let (r_name, s_name) = fam.heads();
for j in 0..=3i64 {
for a in 0..=4i64 {
for b in 0..=5i64 {
let Some(cand) = ratio_rs(&ctx, j, a, b, t, fam) else {
continue;
};
accepted += 1;
let integrand = |u: f64| {
let e = vec![(t_sym, u)];
let rv = eval_f64(ctx.fun(r_name, &[t]), &e).unwrap();
let sv = eval_f64(ctx.fun(s_name, &[t]), &e).unwrap();
u.powi(j as i32) * rv.powi(a as i32) * sv.powi(b as i32)
};
let numeric = simpson(integrand, x0, x1, 2000);
let f0 = eval_f64(cand, &[(t_sym, x0)]).unwrap();
let f1 = eval_f64(cand, &[(t_sym, x1)]).unwrap();
assert!(
f0.is_finite() && f1.is_finite(),
"{name}: non-finite {cand}"
);
let got = f1 - f0;
let tol = 1e-6 * numeric.abs().max(1.0);
assert!(
(got - numeric).abs() < tol,
"{name}: ∫u^{j}·{r_name}^{a}·{s_name}^{b} on [{x0},{x1}]: \
tabular {got} vs numeric {numeric} (cand {cand})"
);
}
}
}
}
assert!(accepted > 150, "only {accepted} tabular cases accepted");
}
type W5cProfile = (f64, f64, f64, [f64; 3]);
type W5cProbe = (&'static str, &'static str, W5cProfile, bool);
const W5C_GT1: W5cProfile = (1.2, 0.8, 1.6, [0.8, 1.2, 1.6]);
const W5C_LT1: W5cProfile = (0.2, 0.4, 0.4, [0.5, 0.9, 1.4]);
const W5C_POS: W5cProfile = (0.5, 0.4, 1.2, [0.4, 0.8, 1.2]);
fn w5c_env(profile: W5cProfile) -> ([(Symbol, f64); 3], [f64; 3]) {
(
[
(Symbol::new("a"), profile.0),
(Symbol::new("b"), profile.1),
(Symbol::new("c"), profile.2),
],
profile.3,
)
}
fn m4_w5c_solved<'a>(
ctx: &'a AtomArena<'a>,
input: &str,
consts: &[(Symbol, f64)],
samples: &[f64],
) -> Atom<'a> {
let x = Symbol::new("x");
let expr = parse_norm(ctx, input);
let r = integrate_inverse_trig(ctx, expr, x).expect("W5c declined");
assert!(!r.to_string().contains("Integral"), "{input}: residue: {r}");
assert!(
!contains_inv_composite(r),
"{input}: F(G(·)) composite left: {r}"
);
for leaked in ["t", "u"] {
assert!(
!contains_symbol(r, Symbol::new(leaked)),
"{input}: substitution symbol {leaked} leaked: {r}"
);
}
let d = crate::diff(ctx, r, x);
for &xv in samples {
let mut env = consts.to_vec();
env.push((x, xv));
let lhs = eval_f64(d, &env).expect("eval diff");
let rhs = eval_f64(expr, &env).expect("eval integrand");
assert!(
lhs.is_finite() && rhs.is_finite(),
"{input} at x={xv}: sample outside the head's real domain: \
diff={lhs} integrand={rhs} (result: {r})"
);
let tol = 1e-6 * rhs.abs().max(1.0);
assert!(
(lhs - rhs).abs() < tol,
"{input} at x={xv}: diff={lhs} integrand={rhs} (result: {r})"
);
}
r
}
fn m4_w5c_certifies<'a>(ctx: &'a AtomArena<'a>, input: &str) {
let x = Symbol::new("x");
let expr = parse_norm(ctx, input);
let r = integrate_inverse_trig(ctx, expr, x).expect("W5c declined");
crate::integral::certify::certify(ctx, expr, r, x)
.unwrap_or_else(|e| panic!("{input}: certificate declined ({e:?}): {r}"));
}
#[test]
fn m4_w5c_bare_and_affine_heads() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let bare: &[(&str, &[f64])] = &[
("acsc(x)", &[1.2, 1.8, 2.4]),
("asec(x)", &[1.2, 1.8, 2.4]),
("acoth(x)", &[1.2, 1.8, 2.4]),
("asech(x)", &[0.2, 0.5, 0.8]),
("acsch(x)", &[0.2, 0.6, 1.2]),
];
for (input, samples) in bare {
let r = m4_w5c_solved(&ctx, input, &[], samples);
let head = &input[..input.find('(').unwrap()];
assert!(
r.to_string().contains(&format!("{head}(")),
"{input}: answer lost its own head: {r}"
);
}
let affine: &[(&str, W5cProfile)] = &[
("acsc(a + b*x)", W5C_GT1),
("asec(a + b*x)", W5C_GT1),
("acoth(a + b*x)", W5C_GT1),
("asech(a + b*x)", W5C_LT1),
("acsch(a + b*x)", W5C_POS),
];
for (input, profile) in affine {
let (env, samples) = w5c_env(*profile);
let r = m4_w5c_solved(&ctx, input, &env, &samples);
let head = &input[..input.find('(').unwrap()];
assert!(
r.to_string().contains(&format!("{head}(")),
"{input}: answer lost its own head: {r}"
);
}
}
#[test]
fn m4_w5c_weighted_heads_and_bucket_rows() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let heads: &[(&str, W5cProfile)] = &[
("x^2*(a + b*acsc(c*x))", W5C_GT1),
("x^2*(a + b*asec(c*x))", W5C_GT1),
("x^2*(a + b*acoth(c*x))", W5C_GT1),
("x*(a + b*acoth(c*x))", W5C_GT1),
("x^2*(a + b*asech(c*x))", W5C_LT1),
("x*(a + b*asech(c*x))", W5C_LT1),
("x^2*(a + b*acsch(c*x))", W5C_POS),
("x*(a + b*acsch(c*x))", W5C_POS),
];
for (input, profile) in heads {
let (env, samples) = w5c_env(*profile);
m4_w5c_solved(&ctx, input, &env, &samples);
}
let env = [(Symbol::new("a"), 0.9), (Symbol::new("b"), 1.1)];
m4_w5c_solved(&ctx, "x^3*acsc(a + b*x)", &env, &[0.2, 0.5, 0.8]);
m4_w5c_solved(&ctx, "x^4*asec(a + b*x)", &env, &[0.2, 0.5, 0.8]);
for (input, profile) in [
("x*(a + b*acsc(c*x))^2", W5C_GT1),
("x*(a + b*asec(c*x))^2", W5C_GT1),
("x*(a + b*asech(c*x))^2", W5C_LT1),
("x*(a + b*acsch(c*x))^2", W5C_POS),
] {
let (env, samples) = w5c_env(profile);
m4_w5c_solved(&ctx, input, &env, &samples);
}
}
#[test]
fn m4_w5c_companion_radicals() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let cases: &[(&str, W5cProfile)] = &[
("x*acsc(c*x)*sqrt(c^2*x^2 - 1)", W5C_GT1),
("x*asec(c*x)*sqrt(c^2*x^2 - 1)", W5C_GT1),
("x*acoth(c*x)*sqrt(c^2*x^2 - 1)", W5C_GT1),
("x*asech(c*x)*sqrt(1 - c^2*x^2)", W5C_LT1),
("x*acsch(c*x)*sqrt(1 + c^2*x^2)", W5C_POS),
];
for (input, profile) in cases {
let (env, samples) = w5c_env(*profile);
m4_w5c_solved(&ctx, input, &env, &samples);
}
}
#[test]
fn m4_w5c_declines_reciprocal_shapes() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
assert_declined(&ctx, "(f*x)^m*(a + b*asec(c*x))"); assert_declined(&ctx, "(d + e*x)^m*(a + b*asech(c*x))"); assert_declined(&ctx, "(d*x)^m*(a + b*asec(c*x))"); assert_declined(&ctx, "x^n*(a + b*acsc(c*x))");
assert_declined(&ctx, "acoth(a + b*x)/x^3"); assert_declined(&ctx, "(d + e*x^2)*(a + b*acsch(c*x))/x^6"); assert_declined(&ctx, "x^4*(a + b*asech(c*x))/(d + e*x^2)^3"); assert_declined(&ctx, "(a + b*acsch(c + d*x))^2/(e + f*x)^3"); assert_declined(&ctx, "x*(d + e*x^2)^(3/2)*(a + b*acsc(c*x))"); assert_declined(&ctx, "(d + e*x)^(3/2)*(a + b*acsc(c*x))"); assert_declined(&ctx, "x*(a + b*acsch(c*x))/(d + e*x)^(3/2)"); assert_declined(&ctx, "(a + b*acsch(c*x))/(x^3*(d + e*x^2)^(5/2))"); assert_declined(&ctx, "x^2*(a + b*acsc(c*x))/(d + e*x^2)^(5/2)"); assert_declined(&ctx, "(f*x)^m*(a + b*asec(c*x))/(d + e*x^2)^(3/2)"); assert_declined(&ctx, "x^5*(d + e*x^2)*(a + b*acsc(c*x))"); assert_declined(&ctx, "acoth(a + b*f^((c + d*x)))"); assert_declined(&ctx, "(a + b*acoth(c*x))^2");
assert_declined(&ctx, "(a + b*acoth(c + d*x))^3"); assert_declined(&ctx, "x^2*asech(a*x)^3"); assert_declined(&ctx, "x^3*(a + b*acsc(c*x))^3"); assert_declined(&ctx, "x*(a + b*asec(c*x))^3"); assert_declined(&ctx, "x^2*(a + b*acsch(c*x))^2"); assert_declined(&ctx, "(a + b*asec(c*x))^2/(c^2*x^2 - 1)");
assert_declined(&ctx, "x*(a + b*asec(c*x))^2/(c^2*x^2 - 1)");
assert_declined(&ctx, "x*(a + b*acsch(c*x))^2/(1 + c^2*x^2)");
}
#[test]
fn m4_w5c_suppressed_inside_parts_scope() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = Symbol::new("x");
let cases: &[(&str, &[f64])] = &[
("acsc(x)", &[1.2, 1.8, 2.4]),
("asech(x)", &[0.2, 0.5, 0.8]),
("x^2*(a + b*asec(c*x))", &[0.8, 1.2, 1.6]),
];
for (input, samples) in cases {
let expr = parse_norm(&ctx, input);
let inside =
super::super::chain::with_parts_scope(|| integrate_inverse_trig(&ctx, expr, x));
assert!(
inside.is_none(),
"{input}: emitted inside a parts scope: {inside:?}"
);
m4_w5c_solved(&ctx, input, &w5c_env(W5C_GT1).0, samples);
}
}
#[test]
fn m4_w5c_certificate_gate() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
for input in [
"acsc(x)",
"asec(x)",
"acoth(x)",
"asech(x)",
"acsch(x)",
"acsc(a + b*x)",
"asec(a + b*x)",
"acoth(a + b*x)",
"asech(a + b*x)",
"acsch(a + b*x)",
"x^2*(a + b*acsc(c*x))",
"x^2*(a + b*asec(c*x))",
"x*(a + b*acoth(c*x))",
"x^2*(a + b*asech(c*x))",
"x^2*(a + b*acsch(c*x))",
"x^3*acsc(a + b*x)",
"x^4*asec(a + b*x)",
] {
m4_w5c_certifies(&ctx, input);
}
for input in [
"x*asec(x)*sqrt(x^2 - 1)",
"x*acsc(x)*sqrt(x^2 - 1)",
"x*acsch(x)*sqrt(1 + x^2)",
"x*asech(x)*sqrt(1 - x^2)",
"x*acoth(x)*sqrt(x^2 - 1)",
] {
m4_w5c_certifies(&ctx, input);
}
}
#[test]
fn m4_w5c_idempotent_reentry() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = Symbol::new("x");
let cases: &[(&str, W5cProfile)] = &[
("acsc(x)", (1.2, 0.8, 1.0, [1.2, 1.8, 2.4])),
("asec(x)", (1.2, 0.8, 1.0, [1.2, 1.8, 2.4])),
("acoth(x)", (1.2, 0.8, 1.0, [1.2, 1.8, 2.4])),
("asech(x)", (0.2, 0.4, 0.4, [0.2, 0.5, 0.8])),
("acsch(x)", W5C_POS),
("x^2*(a + b*acsc(c*x))", W5C_GT1),
("x^2*(a + b*asec(c*x))", W5C_GT1),
("x*(a + b*acoth(c*x))", W5C_GT1),
("x^2*(a + b*asech(c*x))", W5C_LT1),
("x^2*(a + b*acsch(c*x))", W5C_POS),
];
for (input, profile) in cases {
let (env, samples) = w5c_env(*profile);
let first = m4_w5c_solved(&ctx, input, &env, &samples);
let terms: Vec<Atom> = match first.node() {
AtomNode::Add(args) => args.to_vec(),
_ => vec![first],
};
for term in terms {
if let Some(r) = integrate_inverse_trig(&ctx, term, x) {
assert!(
!contains_integral(r),
"{input}: partial guess on {term}: {r}"
);
assert!(
!contains_inv_composite(r),
"{input}: composite on {term}: {r}"
);
let d = crate::diff(&ctx, r, x);
for &xv in &samples {
let mut e = env.to_vec();
e.push((x, xv));
let (Some(lhs), Some(rhs)) = (eval_f64(d, &e), eval_f64(term, &e)) else {
continue;
};
if !lhs.is_finite() || !rhs.is_finite() {
continue;
}
assert!(
(lhs - rhs).abs() < 1e-6 * rhs.abs().max(1.0),
"{input}: re-claim of {term} is not an antiderivative: {r}"
);
}
}
}
let second = crate::integral::integrate(&ctx, first, x);
assert!(
!contains_symbol(second, Symbol::new("t")),
"{input}: substitution symbol leaked through re-entry"
);
}
}
#[test]
fn m4_w5c_bucket_probe_lock() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = Symbol::new("x");
let csv = |c: f64| (0.9, 1.1, c, [0.2, 0.5, 0.8]);
let probes: &[W5cProbe] = &[
("00026", "acoth(a + b*x)/x^3", csv(0.8), false),
(
"00091",
"x*(d + e*x^2)^(3/2)*(a + b*acsc(c*x))",
csv(0.8),
false,
),
(
"00167",
"x^5*(d + e*x^2)*(a + b*acsc(c*x))",
csv(0.8),
false,
),
("00372", "(a + b*acoth(c + d*x))^3", csv(0.8), false),
(
"00441",
"(f*x)^m*(a + b*asec(c*x))/(d + e*x^2)^2",
csv(0.8),
false,
),
("00511", "x^3*acsc(a + b*x)", csv(0.8), true),
(
"00544",
"x*(a + b*acsch(c*x))/(d + e*x)^(3/2)",
csv(0.8),
false,
),
(
"00635",
"x^4*(a + b*asech(c*x))/(d + e*x^2)^3",
csv(0.8),
false,
),
(
"00781",
"(a + b*acsch(c + d*x))^2/(e + f*x)^3",
csv(0.8),
false,
),
(
"00821",
"(d + e*x)^(3/2)*(a + b*acsc(c*x))",
csv(0.8),
false,
),
("01132", "x^3*(a + b*acsc(c*x))^3", csv(0.8), false),
("01218", "x*(a + b*asec(c*x))^3", csv(0.8), false),
(
"01304",
"(a + b*acsch(c*x))/(x^3*(d + e*x^2)^(5/2))",
csv(0.8),
false,
),
("01317", "(d + e*x)^m*(a + b*asech(c*x))", csv(0.8), false),
("01347", "x^2*asech(a*x)^3", W5C_LT1, false),
(
"01351",
"x^2*(a + b*acsc(c*x))/(d + e*x^2)^(5/2)",
csv(0.8),
false,
),
(
"01376",
"(d + e*x^2)*(a + b*acsch(c*x))/x^6",
csv(0.8),
false,
),
("01467", "x^2*(a + b*acsch(c*x))^2", W5C_POS, false),
("01681", "acoth(a + b*f^((c + d*x)))", csv(0.8), false),
("01708", "x^4*asec(a + b*x)", csv(0.8), true),
("01786", "(d*x)^m*(a + b*asec(c*x))", csv(0.8), false),
(
"01793",
"(f*x)^m*(a + b*asec(c*x))/(d + e*x^2)^(3/2)",
csv(0.8),
false,
),
("w5c-acsc", "x^2*(a + b*acsc(c*x))", W5C_GT1, true),
("w5c-asec", "x^2*(a + b*asec(c*x))", W5C_GT1, true),
("w5c-acoth", "x*(a + b*acoth(c*x))", W5C_GT1, true),
("w5c-acoth2", "x^2*(a + b*acoth(c*x))", W5C_GT1, true),
("w5c-asech", "x^2*(a + b*asech(c*x))", W5C_LT1, true),
("w5c-acsch", "x^2*(a + b*acsch(c*x))", W5C_POS, true),
("w5c-acsc-k2", "x*(a + b*acsc(c*x))^2", W5C_GT1, true),
("w5c-asec-k2", "x*(a + b*asec(c*x))^2", W5C_GT1, true),
("w5c-asech-k2", "x*(a + b*asech(c*x))^2", W5C_LT1, true),
("w5c-acsch-k2", "x*(a + b*acsch(c*x))^2", W5C_POS, true),
(
"w5c-asec-den",
"(a + b*asec(c*x))^2/(c^2*x^2 - 1)",
W5C_GT1,
false,
),
("w5c-acsc-sympow", "x^n*(a + b*acsc(c*x))", W5C_GT1, false),
("w5c-asech-bare-k3", "x^2*asech(a*x)^3", W5C_LT1, false),
];
for (id, input, profile, expect_solved) in probes {
let expr = parse_norm(&ctx, input);
let t0 = std::time::Instant::now();
let r = integrate_inverse_trig(&ctx, expr, x);
let dt = t0.elapsed();
assert!(
dt.as_secs_f64() < 2.0,
"{id}: M4 took {dt:?} (refusal budget is 2 s in debug)"
);
let solved = r.is_some_and(|r| !contains_integral(r));
if std::env::var_os("OCAS_PROBE").is_some() {
let shape = match &r {
Some(a) => a.to_string(),
None => "<declined>".to_string(),
};
eprintln!("[probe] rubi-{id}: solved={solved} {dt:?} {input} => {shape}");
}
assert_eq!(solved, *expect_solved, "rubi-{id}: {input}");
if solved {
let e2e = crate::integral::integrate(&ctx, expr, x);
assert!(
!contains_integral(e2e),
"rubi-{id}: full chain left a residue: {e2e}"
);
}
if let Some(ans) = r {
assert!(!contains_inv_composite(ans), "rubi-{id}: composite: {ans}");
let (env, samples) = w5c_env(*profile);
let d = crate::diff(&ctx, ans, x);
for &xv in &samples {
let mut e = env.to_vec();
e.push((x, xv));
let lhs = eval_f64(d, &e).expect("eval diff");
let rhs = eval_f64(expr, &e).expect("eval integrand");
assert!(
lhs.is_finite() && rhs.is_finite(),
"rubi-{id} at x={xv}: non-finite sample"
);
let tol = 1e-6 * rhs.abs().max(1.0);
assert!(
(lhs - rhs).abs() < tol,
"rubi-{id} at x={xv}: diff={lhs} integrand={rhs} (result: {ans})"
);
}
}
}
}
#[test]
fn m4_w5b_bucket_probe_lock() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = Symbol::new("x");
let env = [
(Symbol::new("a"), 0.9),
(Symbol::new("b"), 1.1),
(Symbol::new("c"), 0.8),
(Symbol::new("d"), 1.7),
(Symbol::new("f"), 1.3),
(Symbol::new("g"), 0.7),
];
let samples = [0.2, 0.5, 0.8];
let probes: &[(&str, &str, bool)] = &[
("01568", "x^3*(d - c^2*d*x^2)^(1/2)*(a + b*asin(c*x))", true),
(
"01823",
"x^3*(a + b*asin(c*x))^2/(d - c^2*d*x^2)^(1/2)",
true,
),
("01733", "x*(1 - a^2*x^2)^(3/2)*atanh(a*x)", true),
("00689", "atanh(a*x)/(c - a^2*c*x^2)^(5/2)", true),
(
"00320",
"(f + g*x)^2*(d - c^2*d*x^2)^(3/2)*(a + b*asin(c*x))^2",
true,
),
(
"00459",
"x^5*(a + b*asin(c*x))/(d - c^2*d*x^2)^(3/2)",
false,
),
(
"00228",
"x^3*(a + b*asin(c*x))/(d - c^2*d*x^2)^(5/2)",
false,
),
("00811", "asin(a*x)^2/(c - a^2*c*x^2)^(3/2)", false),
(
"01666",
"x^2*(d - c^2*d*x^2)^(3/2)*(a + b*asin(c*x))^n",
false,
),
("00610", "(1 - a^2*x^2)^(1/2)/atanh(a*x)^2", false),
("01571", "(c - a^2*c*x^2)^(5/2)/acosh(a*x)^(3/2)", false),
(
"00408",
"(f - i*c*f*x)^(5/2)*(a + b*asinh(c*x))/(d + i*c*d*x)^(5/2)",
false,
),
];
for (id, input, expect_solved) in probes {
let expr = parse_norm(&ctx, input);
let r = integrate_inverse_trig(&ctx, expr, x);
let solved = r.is_some_and(|r| !contains_integral(r));
if std::env::var_os("OCAS_PROBE").is_some() {
let shape = match &r {
Some(a) => a.to_string(),
None => "<declined>".to_string(),
};
eprintln!("[probe] rubi-{id}: solved={solved} {input} => {shape}");
}
assert_eq!(solved, *expect_solved, "rubi-{id}: {input}");
if let Some(ans) = r {
assert!(!contains_inv_composite(ans), "rubi-{id}: composite: {ans}");
let d = crate::diff(&ctx, ans, x);
for &xv in &samples {
let mut e = env.to_vec();
e.push((x, xv));
let lhs = eval_f64(d, &e).expect("eval diff");
let rhs = eval_f64(expr, &e).expect("eval integrand");
let tol = 1e-6 * rhs.abs().max(1.0);
assert!(
(lhs - rhs).abs() < tol,
"rubi-{id} at x={xv}: diff={lhs} integrand={rhs} (result: {ans})"
);
}
}
}
}
#[test]
fn m1_asinh_kernel_sqrt_fun_form() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "(a + b*asinh(c*x))^2/sqrt(1 + c^2*x^2)");
let env = [
(Symbol::new("a"), 1.2),
(Symbol::new("b"), -0.7),
(Symbol::new("c"), 1.5),
];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.2, 0.5, 0.9]);
}
#[test]
fn m1_asin_kernel_pow_form() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "(a + b*asin(c*x))^3*(1 - c^2*x^2)^(-1/2)");
let env = [
(Symbol::new("a"), 0.8),
(Symbol::new("b"), 1.1),
(Symbol::new("c"), 2.0),
];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.1, 0.3, 0.45]);
}
#[test]
fn m1_acos_kernel_negative_sign() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "(a + b*acos(c*x))/sqrt(1 - c^2*x^2)");
let env = [
(Symbol::new("a"), 0.9),
(Symbol::new("b"), -1.2),
(Symbol::new("c"), 1.6),
];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.1, 0.3, 0.55]);
}
#[test]
fn m1_atanh_kernel_reciprocal() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "(a + b*atanh(c*x))^2/(1 - c^2*x^2)");
let env = [
(Symbol::new("a"), 1.3),
(Symbol::new("b"), 0.7),
(Symbol::new("c"), 1.9),
];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.1, 0.25, 0.5]);
}
#[test]
fn m1_atan_kernel_symbolic_norm() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "(a + b*atan(c*x))^4/(d + c^2*d*x^2)");
let env = [
(Symbol::new("a"), 1.1),
(Symbol::new("b"), -0.8),
(Symbol::new("c"), 1.7),
(Symbol::new("d"), 2.3),
];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.2, 0.7, 1.3]);
}
#[test]
fn m1_acosh_kernel() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "(a + b*acosh(c*x))^2/sqrt(c^2*x^2 - 1)");
let env = [
(Symbol::new("a"), 0.6),
(Symbol::new("b"), 1.4),
(Symbol::new("c"), 0.8),
];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[1.5, 2.0, 3.0]);
}
#[test]
fn m1_numeric_norm_factor() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "(a + b*asinh(c*x))^2/sqrt(4 + 4*c^2*x^2)");
let env = [
(Symbol::new("a"), 1.2),
(Symbol::new("b"), -0.7),
(Symbol::new("c"), 1.5),
];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.2, 0.5, 0.9]);
}
#[test]
fn m2_corpus_acosh_poly() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "(d + e*x)*(a + b*acosh(c*x))^2");
let env = [
(Symbol::new("a"), 0.9),
(Symbol::new("b"), -1.3),
(Symbol::new("c"), 0.7),
(Symbol::new("d"), 1.1),
(Symbol::new("e"), 0.6),
];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[1.6, 2.2, 3.0]);
}
#[test]
fn m2_corpus_asinh_sqrt_pow() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "(d + c^2*d*x^2)^(3/2)*(a + b*asinh(c*x))");
let env = [
(Symbol::new("a"), 0.8),
(Symbol::new("b"), 1.1),
(Symbol::new("c"), 1.3),
(Symbol::new("d"), 1.7),
];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.2, 0.6, 1.1]);
}
#[test]
fn m2_corpus_acosh_linear_arg_k4() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "(c*e + d*e*x)*(a + b*acosh(c + d*x))^4");
let env = [
(Symbol::new("a"), 0.7),
(Symbol::new("b"), -1.1),
(Symbol::new("c"), 0.4),
(Symbol::new("d"), 0.9),
(Symbol::new("e"), 1.4),
];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.8, 1.2, 1.8]);
}
#[test]
fn m2_asinh_linear_arg_bare_power() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "(a + b*asinh(c + d*x))^2");
let env = [
(Symbol::new("a"), 1.1),
(Symbol::new("b"), 0.7),
(Symbol::new("c"), 0.4),
(Symbol::new("d"), 1.2),
];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.3, 0.8, 1.5]);
}
#[test]
fn m3_trig_family_linear_arg() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let env = [(Symbol::new("a"), 0.3), (Symbol::new("b"), 1.2)];
for input in ["asin(a + b*x)", "acos(a + b*x)", "atan(a + b*x)"] {
let expr = parse_norm(&ctx, input);
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.1, 0.3, 0.5]);
}
}
#[test]
fn m3_hyper_family_linear_arg() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let env = [(Symbol::new("a"), 0.3), (Symbol::new("b"), 1.2)];
for input in ["asinh(a + b*x)", "atanh(a + b*x)"] {
let expr = parse_norm(&ctx, input);
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.1, 0.3, 0.5]);
}
}
#[test]
fn m3_acosh_linear_arg_domain() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let env = [(Symbol::new("a"), 1.4), (Symbol::new("b"), 0.9)];
let expr = parse_norm(&ctx, "acosh(a + b*x)");
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.1, 0.4, 0.8]);
}
#[test]
fn m3_constant_multiple() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let env = [(Symbol::new("a"), 0.3), (Symbol::new("b"), 1.2)];
let expr = parse_norm(&ctx, "q*asin(a + b*x)");
let mut env2 = env.to_vec();
env2.push((Symbol::new("q"), 2.5));
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env2, &[0.1, 0.3, 0.5]);
}
#[test]
fn declines_out_of_scope() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
assert_declined(&ctx, "x/acosh(a*x)^4");
assert_declined(&ctx, "(a + b*atanh(c*x))^2/(d + c*d*x)");
assert_declined(&ctx, "x^2/((c + a^2*c*x^2)^(5/2)*atan(a*x)^2)");
assert_declined(&ctx, "a + b*asin(1 + d*x^2)");
assert_declined(&ctx, "asin(1 + d*x^2)");
{
let expr = parse_norm(&ctx, "(d + e*x)*(a + b*acosh(c*x))^6");
let env = [
(Symbol::new("a"), 0.6),
(Symbol::new("b"), 1.1),
(Symbol::new("c"), 0.8),
(Symbol::new("d"), 1.3),
(Symbol::new("e"), 0.7),
];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[1.6, 2.2, 3.0]);
}
assert_declined(&ctx, "(-4 - 4*c^2*x^2)^(3/2)*(a + b*asinh(c*x))");
assert_declined(&ctx, "(a + b*asinh(c*x))^2/sqrt(1 - c^2*x^2)");
assert_declined(&ctx, "(1 + c*x)^3*(a + b*atanh(c*x))^3");
}
#[test]
fn p1_atanh_tanh_numeric_and_symbolic() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = Symbol::new("x");
let expr = parse_norm(&ctx, "atanh(tanh(b*x))^2");
let env = [(Symbol::new("b"), 1.2)];
assert_antiderivative_num(&ctx, expr, x, &env, &[0.1, 0.4, 0.9]);
let expr = parse_norm(&ctx, "atanh(tanh(a + b*x))^2");
let env = [(Symbol::new("a"), 0.4), (Symbol::new("b"), 1.1)];
assert_antiderivative_num(&ctx, expr, x, &env, &[0.1, 0.3, 0.5]);
}
#[test]
fn p1_corpus_atanh_tanh_shapes() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = Symbol::new("x");
let env = [(Symbol::new("a"), 0.4), (Symbol::new("b"), 0.9)];
for input in ["atanh(tanh(a + b*x))^3", "atanh(tanh(a + b*x))^2"] {
let expr = parse_norm(&ctx, input);
assert_antiderivative_num(&ctx, expr, x, &env, &[0.1, 0.3, 0.5]);
}
let expr = parse_norm(&ctx, "atanh(tanh(a + b*x))^4/x^4");
assert_antiderivative_num(&ctx, expr, x, &env, &[0.3, 0.6, 1.1]);
let expr = parse_norm(&ctx, "atanh(tanh(a + b*x))^2*sqrt(x)");
assert_antiderivative_num(&ctx, expr, x, &env, &[0.2, 0.5, 0.9]);
}
#[test]
fn p1_acoth_coth_numeric_and_symbolic() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = Symbol::new("x");
let expr = parse_norm(&ctx, "acoth(coth(b*x))^2");
let env = [(Symbol::new("b"), 1.1)];
assert_antiderivative_num(&ctx, expr, x, &env, &[0.3, 0.7, 1.1]);
let expr = parse_norm(&ctx, "acoth(coth(a + b*x))^3");
let env = [(Symbol::new("a"), 0.5), (Symbol::new("b"), 1.1)];
assert_antiderivative_num(&ctx, expr, x, &env, &[0.3, 0.6, 0.9]);
}
#[test]
fn p1_asinh_sinh_numeric_and_symbolic() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = Symbol::new("x");
let expr = parse_norm(&ctx, "asinh(sinh(b*x))^3");
let env = [(Symbol::new("b"), 1.3)];
assert_antiderivative_num(&ctx, expr, x, &env, &[0.2, 0.6, 1.0]);
let expr = parse_norm(&ctx, "asinh(sinh(a + b*x))^2/x^2");
let env = [(Symbol::new("a"), -0.3), (Symbol::new("b"), 0.8)];
assert_antiderivative_num(&ctx, expr, x, &env, &[0.4, 0.9, 1.4]);
}
#[test]
fn p1_log_exp_numeric_and_symbolic() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = Symbol::new("x");
let expr = parse_norm(&ctx, "log(exp(b*x))^2");
let env = [(Symbol::new("b"), 1.4)];
assert_antiderivative_num(&ctx, expr, x, &env, &[0.2, 0.5, 0.9]);
let expr = parse_norm(&ctx, "log(exp(a + b*x))^3/x");
let env = [(Symbol::new("a"), 0.6), (Symbol::new("b"), -1.1)];
assert_antiderivative_num(&ctx, expr, x, &env, &[0.3, 0.8, 1.5]);
}
#[test]
fn p1_band_guards_certify_only_inside_the_band() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = Symbol::new("x");
for input in ["atan(tan(1/2))", "acosh(cosh(1/2))"] {
let e = parse_norm(&ctx, input);
let mut budget = MAX_CANCEL_BUDGET;
let r = cancel_compositions(&ctx, e, x, &mut budget).expect("certified band");
assert_eq!(r, parse_norm(&ctx, "1/2"), "{input}");
}
for input in ["atan(tan(2))", "atan(tan(a + b*x))", "acosh(cosh(a + b*x))"] {
let e = parse_norm(&ctx, input);
let mut budget = MAX_CANCEL_BUDGET;
assert!(
cancel_compositions(&ctx, e, x, &mut budget).is_none(),
"expected no cancellation for {input}"
);
}
}
#[test]
fn p1_nested_pairs_reach_the_fixpoint_in_one_pass() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = Symbol::new("x");
let e = parse_norm(&ctx, "atanh(tanh(asinh(sinh(x))))");
let mut budget = MAX_CANCEL_BUDGET;
let r = cancel_compositions(&ctx, e, x, &mut budget).expect("nested pair");
assert_eq!(r, ctx.var("x"));
let mut budget = MAX_CANCEL_BUDGET;
assert!(cancel_compositions(&ctx, r, x, &mut budget).is_none());
}
#[test]
fn p2_acoth_tanh_corpus_shapes() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = Symbol::new("x");
let kernel = parse_norm(&ctx, "acoth(tanh(a + b*x))");
let proxy = parse_norm(&ctx, "a + b*x");
let env = [(Symbol::new("a"), 0.4), (Symbol::new("b"), 1.1)];
for input in ["acoth(tanh(a + b*x))^3/x^3", "x*acoth(tanh(a + b*x))^3"] {
let expr = parse_norm(&ctx, input);
assert_antiderivative_branch_shifted(
&ctx,
expr,
kernel,
proxy,
x,
&env,
&[0.4, 0.8, 1.3],
);
}
let expr = parse_norm(&ctx, "acoth(tanh(a + b*x))^3/x^6");
assert_antiderivative_branch_shifted(&ctx, expr, kernel, proxy, x, &env, &[0.4, 0.8, 1.3]);
let expr = parse_norm(&ctx, "acoth(tanh(2*x))^2/x^2");
let kernel = parse_norm(&ctx, "acoth(tanh(2*x))");
let proxy = parse_norm(&ctx, "2*x");
assert_antiderivative_branch_shifted(&ctx, expr, kernel, proxy, x, &[], &[0.5, 1.0, 1.6]);
}
#[test]
fn p2_atan_tan_is_branch_independent() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = Symbol::new("x");
let kernel = parse_norm(&ctx, "atan(tan(a + b*x))");
let proxy = parse_norm(&ctx, "a + b*x");
let env = [(Symbol::new("a"), 0.7), (Symbol::new("b"), 1.3)];
for input in ["x^2*atan(tan(a + b*x))^2", "atan(tan(a + b*x))"] {
let expr = parse_norm(&ctx, input);
assert_antiderivative_branch_shifted(
&ctx,
expr,
kernel,
proxy,
x,
&env,
&[1.0, 2.0, 3.0],
);
}
}
#[test]
fn p3_companion_radical_with_intercept() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = Symbol::new("x");
let expr = parse_norm(&ctx, "asin(a + b*x)/sqrt(1 - (a + b*x)^2)");
let env = [(Symbol::new("a"), 0.2), (Symbol::new("b"), 0.7)];
assert_antiderivative_num(&ctx, expr, x, &env, &[0.1, 0.4, 0.8]);
let expr = parse_norm(&ctx, "asin(c*x)/sqrt(1 - c^2*x^2)");
let env = [(Symbol::new("c"), 0.8)];
assert_antiderivative_num(&ctx, expr, x, &env, &[0.1, 0.5, 1.0]);
}
#[test]
fn p3_rational_inverse_power() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = Symbol::new("x");
let expr = parse_norm(&ctx, "sqrt(asin(a*x))/(c - a^2*c*x^2)^(1/2)");
let env = [(Symbol::new("a"), 0.6), (Symbol::new("c"), 1.4)];
assert_antiderivative_num(&ctx, expr, x, &env, &[0.2, 0.5, 0.9]);
let expr = parse_norm(&ctx, "sqrt(acosh(x))/(x^2 - 1)^(1/2)");
assert_antiderivative_num(&ctx, expr, x, &[], &[1.4, 1.9, 2.4]);
}
#[test]
fn p3_declines_non_companion_radicals() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
assert_declined(&ctx, "atan(a + b*x)/sqrt(1 + a^2 + 2*a*b*x + b^2*x^2)");
assert_declined(&ctx, "atanh(a + b*x)/sqrt(1 - (a + b*x)^2)");
assert_declined(&ctx, "asinh(a + b*x)/sqrt(1 - (a + b*x)^2)");
}
#[test]
fn declines_out_of_family_compositions() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
assert_declined(&ctx, "atan(x^2)");
assert_declined(&ctx, "atanh(sin(x))");
assert_declined(&ctx, "sqrt(asin(x))");
assert_declined(&ctx, "asin(x)*exp(x)");
assert_declined(&ctx, "acosh(cosh(a + b*x))");
assert_declined(&ctx, "x^2*atanh(a + b*f^((c + d*x)))");
assert_declined(&ctx, "acoth(a + b*f^((c + d*x)))");
}
#[test]
fn declines_hang_attributed_shapes_quickly() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
assert_declined(&ctx, "(c + d*x)^(1/2)/(a + b*x)^2");
assert_declined(&ctx, "(e + f*x)^2*cos(c + d*x)/(a + b*sin(c + d*x))^3");
}
#[test]
fn stress_repeat_is_stable_and_budget_bounded() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = Symbol::new("x");
let shapes = [
"atanh(tanh(a + b*x))^2",
"atanh(tanh(a + b*x))^4/x^4",
"atanh(tanh(a + b*x))^2*sqrt(x)",
"x*acoth(tanh(a + b*x))^3",
"acoth(tanh(a + b*x))^3/x^3",
"atan(x^2)",
"atanh(sin(x))",
"(c + d*x)^(1/2)/(a + b*x)^2",
];
let solved = [true, true, true, true, true, false, false, false];
let mut first: Vec<Option<(String, usize)>> = Vec::with_capacity(shapes.len());
for round in 0..300 {
for (i, input) in shapes.iter().enumerate() {
let expr = parse_norm(&ctx, input);
let observed =
integrate_inverse_trig(&ctx, expr, x).map(|r| (r.to_string(), node_count(r)));
if round == 0 {
first.push(observed);
} else {
assert_eq!(observed, first[i], "{input} diverged at round {round}");
}
}
}
for (i, input) in shapes.iter().enumerate() {
if solved[i] {
let r = first[i].as_ref().expect("expected a closed form");
assert!(!r.0.contains("Integral"), "{input} left a residue: {}", r.0);
} else {
assert!(first[i].is_none(), "{input}: {:?}", first[i]);
}
}
}
}