use std::collections::BTreeMap;
use ocas_atom::normalize::normalize;
use ocas_atom::{Atom, AtomArena, AtomNode, Symbol};
use super::{
contains_integral, int_pow, integrate_raw, inv, is_constant, linear_form, node_count, rat_atom,
};
const MAX_POW: i64 = 8;
const MAX_NODES: usize = 300;
const MAX_REENTRY_NODES: usize = 20_000;
const MAX_KERNEL_WORK: u64 = 5_000_000;
thread_local! {
static KERNEL_WORK: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
fn reset_kernel_budget() {
KERNEL_WORK.with(|c| c.set(0));
}
fn charge_kernel_work(units: u64) -> bool {
KERNEL_WORK.with(|c| {
let v = c.get().saturating_add(units);
c.set(v);
v > MAX_KERNEL_WORK
})
}
pub(crate) fn integrate_trig_kernel<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
) -> Option<Atom<'a>> {
reset_kernel_budget();
if is_constant(expr, var) || node_count(expr) > MAX_NODES {
return None;
}
try_single_kernel(ctx, expr, var)
.or_else(|| try_tan_sec_family(ctx, expr, var, Family::TanSec))
.or_else(|| try_tan_sec_family(ctx, expr, var, Family::CotCsc))
}
fn trig_pair(name: &str) -> Option<(i64, i64)> {
match name {
"sin" => Some((1, 0)),
"cos" => Some((0, 1)),
"sec" => Some((0, -1)),
"csc" => Some((-1, 0)),
"tan" => Some((1, -1)),
"cot" => Some((-1, 1)),
_ => None,
}
}
struct TrigPow<'a> {
name: &'static str,
exp: i64,
arg: Atom<'a>,
}
fn match_trig_pow<'a>(f: Atom<'a>) -> Option<TrigPow<'a>> {
let (name, args, exp) = match f.node() {
AtomNode::Fun(name, args) if args.len() == 1 => (name.as_str(), args, 1),
AtomNode::Pow(b, e) => match (b.node(), e.node()) {
(AtomNode::Fun(name, args), AtomNode::Num(k)) if args.len() == 1 => {
(name.as_str(), args, *k)
}
_ => return None,
},
_ => return None,
};
if trig_pair(name).is_none() || exp.abs() > MAX_POW {
return None;
}
Some(TrigPow {
name: match name {
"sin" => "sin",
"cos" => "cos",
"tan" => "tan",
"cot" => "cot",
"sec" => "sec",
"csc" => "csc",
_ => return None,
},
exp,
arg: args[0],
})
}
fn shared_linear_arg<'a>(
ctx: &'a AtomArena<'a>,
u: Atom<'a>,
var: Symbol,
) -> Option<(Atom<'a>, Atom<'a>)> {
let (f, _e) = linear_form(ctx, u, var)?;
if is_zero(ctx, f) {
return None;
}
Some((u, f))
}
fn unify_u<'a>(
ctx: &'a AtomArena<'a>,
slot: &mut Option<Atom<'a>>,
u: Atom<'a>,
) -> Option<Atom<'a>> {
let nu = normalize(ctx, u);
match *slot {
None => {
*slot = Some(nu);
Some(nu)
}
Some(u0) => {
if nu == u0 {
Some(u0)
} else {
None
}
}
}
}
fn is_zero<'a>(ctx: &'a AtomArena<'a>, a: Atom<'a>) -> bool {
matches!(normalize(ctx, a).node(), AtomNode::Num(0))
}
fn binom(n: i64, k: i64) -> i64 {
let mut r = 1i64;
for i in 0..k {
r = r.saturating_mul(n - i) / (i + 1);
}
r
}
struct K1Match<'a> {
sin: bool,
u: Atom<'a>,
consts: Vec<Atom<'a>>,
numer: Vec<(Atom<'a>, i64)>,
da: Atom<'a>,
db: Atom<'a>,
dn: i64,
}
fn add_as_kernel_binomial<'a>(
ctx: &'a AtomArena<'a>,
args: &[Atom<'a>],
sin: bool,
u: Atom<'a>,
var: Symbol,
) -> Option<(i64, Atom<'a>, Atom<'a>)> {
let mut terms: BTreeMap<i64, Vec<Atom<'a>>> = BTreeMap::new();
for t in args {
let (coeff, deg) = term_as_kernel_power(ctx, *t, sin, u, var)?;
terms.entry(deg).or_default().push(coeff);
}
let degrees: Vec<i64> = terms.keys().copied().collect();
if degrees.len() != 2 || degrees[1] != degrees[0] + 1 {
return None;
}
let d0 = degrees[0];
let a = normalize(ctx, ctx.add(&terms[&d0]));
let b = normalize(ctx, ctx.add(&terms[&(d0 + 1)]));
if is_zero(ctx, a) || is_zero(ctx, b) {
return None;
}
Some((d0, a, b))
}
fn term_as_kernel_power<'a>(
ctx: &'a AtomArena<'a>,
term: Atom<'a>,
sin: bool,
u: Atom<'a>,
var: Symbol,
) -> Option<(Atom<'a>, i64)> {
if is_constant(term, var) {
return Some((term, 0));
}
let sub: Vec<Atom<'a>> = match term.node() {
AtomNode::Mul(args) => args.to_vec(),
_ => vec![term],
};
let mut coeff: Vec<Atom<'a>> = Vec::new();
let mut deg: Option<i64> = None;
for f in sub {
if is_constant(f, var) {
coeff.push(f);
continue;
}
let tp = match_trig_pow(f)?;
if deg.is_some() {
return None;
}
if normalize(ctx, tp.arg) != normalize(ctx, u) {
return None;
}
let (se, ce) = trig_pair(tp.name)?;
let d = if sin { se } else { ce };
let other = if sin { ce } else { se };
if other != 0 {
return None;
}
deg = Some(d.checked_mul(tp.exp)?);
}
let coeff = if coeff.is_empty() {
ctx.num(1)
} else {
normalize(ctx, ctx.mul(&coeff))
};
Some((coeff, deg?))
}
fn try_single_kernel<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol) -> Option<Atom<'a>> {
let m = match_k1(ctx, expr, var)?;
if m.numer.iter().all(|(_, d)| *d == 0) {
return None;
}
let t_atom = ctx.fun(if m.sin { "sin" } else { "cos" }, &[m.u]);
let q_atom = normalize(ctx, ctx.add(&[m.da, ctx.mul(&[m.db, t_atom])]));
let mut terms: Vec<Atom<'a>> = Vec::new();
for (c, d) in &m.numer {
if *d >= 0 {
shift_expand(ctx, *c, *d, m.dn, m.da, m.db, t_atom, q_atom, &mut terms)?;
} else {
peel(ctx, *c, -*d, m.dn, m.da, m.db, t_atom, q_atom, &mut terms)?;
}
}
if terms.is_empty() {
return None;
}
let mut all = m.consts.clone();
all.push(ctx.add(&terms));
let sum = normalize(ctx, ctx.mul(&all));
if sum == expr {
return None;
}
if node_count(sum) > MAX_REENTRY_NODES {
return None;
}
let r = integrate_raw(ctx, sum, var, 0, true, 0, 0);
if contains_integral(r) {
return None;
}
Some(r)
}
#[allow(clippy::too_many_arguments)]
fn shift_expand<'a>(
ctx: &'a AtomArena<'a>,
c: Atom<'a>,
m: i64,
n: i64,
a: Atom<'a>,
b: Atom<'a>,
t_atom: Atom<'a>,
q_atom: Atom<'a>,
terms: &mut Vec<Atom<'a>>,
) -> Option<()> {
let b_inv_m = ctx.pow(b, ctx.num(-m));
for k in 0..=m {
if charge_kernel_work(1 + terms.len() as u64) {
return None;
}
let mut coeff = vec![c, ctx.num(binom(m, k)), int_pow(ctx, a, m - k), b_inv_m];
if (m - k) % 2 == 1 {
coeff.push(ctx.num(-1));
}
let coeff = normalize(ctx, ctx.mul(&coeff));
let j = k - n;
if j >= 0 {
for l in 0..=j {
let c3 = normalize(
ctx,
ctx.mul(&[
coeff,
ctx.num(binom(j, l)),
int_pow(ctx, a, j - l),
int_pow(ctx, b, l),
]),
);
terms.push(normalize(ctx, ctx.mul(&[c3, int_pow(ctx, t_atom, l)])));
}
} else {
terms.push(normalize(
ctx,
ctx.mul(&[coeff, ctx.pow(q_atom, ctx.num(j))]),
));
}
}
Some(())
}
#[allow(clippy::too_many_arguments)]
fn peel<'a>(
ctx: &'a AtomArena<'a>,
c: Atom<'a>,
s: i64,
n: i64,
a: Atom<'a>,
b: Atom<'a>,
t_atom: Atom<'a>,
q_atom: Atom<'a>,
terms: &mut Vec<Atom<'a>>,
) -> Option<()> {
if s + n > 2 * MAX_POW {
return None;
}
if charge_kernel_work(1 + terms.len() as u64) {
return None;
}
if n == 0 {
let (name, u) = match t_atom.node() {
AtomNode::Fun(name, args) => (name.as_str(), args[0]),
_ => return None,
};
let recip = ctx.fun(if name == "cos" { "sec" } else { "csc" }, &[u]);
terms.push(normalize(ctx, ctx.mul(&[c, int_pow(ctx, recip, s)])));
return Some(());
}
if s == 0 {
terms.push(normalize(ctx, ctx.mul(&[c, ctx.pow(q_atom, ctx.num(-n))])));
return Some(());
}
let a_inv = inv(ctx, a);
peel(
ctx,
normalize(ctx, ctx.mul(&[c, a_inv])),
s,
n - 1,
a,
b,
t_atom,
q_atom,
terms,
)?;
peel(
ctx,
normalize(ctx, ctx.mul(&[c, ctx.num(-1), b, a_inv])),
s - 1,
n,
a,
b,
t_atom,
q_atom,
terms,
)
}
fn match_k1<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol) -> Option<K1Match<'a>> {
let factors: Vec<Atom<'a>> = match expr.node() {
AtomNode::Mul(args) => args.to_vec(),
_ => vec![expr],
};
let mut consts: Vec<Atom<'a>> = Vec::new();
let mut numer_add: Vec<(Atom<'a>, i64)> = Vec::new();
let mut kernel_shift: i64 = 0;
let mut denom: Option<(Atom<'a>, Atom<'a>, i64)> = None;
let mut use_sin = false;
let mut use_cos = false;
let mut shared_u: Option<Atom<'a>> = None;
for f in factors {
if is_constant(f, var) {
consts.push(f);
continue;
}
match f.node() {
AtomNode::Pow(b, e) if matches!(b.node(), AtomNode::Add(_)) => {
let AtomNode::Num(n) = e.node() else {
return None;
};
let n = *n;
if n == 0 || n.abs() > MAX_POW {
return None;
}
let AtomNode::Add(add_args) = b.node() else {
return None;
};
let mut probe_sin = None;
for t in add_args.iter() {
if is_constant(*t, var) {
continue;
}
let sub: Vec<Atom<'a>> = match t.node() {
AtomNode::Mul(m) => m.to_vec(),
_ => vec![*t],
};
for sf in sub {
if is_constant(sf, var) {
continue;
}
let tp = match_trig_pow(sf)?;
let (se, ce) = trig_pair(tp.name)?;
if se != 0 && ce != 0 {
return None;
}
let is_sin = se != 0;
if let Some(prev) = probe_sin {
if prev != is_sin {
return None;
}
} else {
probe_sin = Some(is_sin);
}
unify_u(ctx, &mut shared_u, tp.arg)?;
if is_sin {
use_sin = true;
} else {
use_cos = true;
}
}
}
let sin = probe_sin?;
let u = shared_u?;
let (d0, a, bb) = add_as_kernel_binomial(ctx, add_args, sin, u, var)?;
if n < 0 {
if denom.is_some() {
return None;
}
denom = Some((a, bb, -n));
kernel_shift = kernel_shift.checked_add(d0.checked_mul(n)?)?;
} else {
if !numer_add.is_empty() {
return None;
}
for k in 0..=n {
let coeff = normalize(
ctx,
ctx.mul(&[
ctx.num(binom(n, k)),
int_pow(ctx, a, n - k),
int_pow(ctx, bb, k),
]),
);
let deg = d0.checked_mul(n)?.checked_add(k)?;
numer_add.push((coeff, deg));
}
}
}
AtomNode::Add(add_args) => {
let mut this_sin = None;
for t in add_args.iter() {
if is_constant(*t, var) {
continue;
}
let sub: Vec<Atom<'a>> = match t.node() {
AtomNode::Mul(m) => m.to_vec(),
_ => vec![*t],
};
for sf in &sub {
if is_constant(*sf, var) {
continue;
}
let tp = match_trig_pow(*sf)?;
let (se, ce) = trig_pair(tp.name)?;
if se != 0 && ce != 0 {
return None;
}
let is_sin = se != 0;
if let Some(prev) = this_sin {
if prev != is_sin {
return None;
}
} else {
this_sin = Some(is_sin);
}
unify_u(ctx, &mut shared_u, tp.arg)?;
}
}
let sin = this_sin?;
if sin {
use_sin = true;
} else {
use_cos = true;
}
let u = shared_u?;
if !numer_add.is_empty() {
return None;
}
for t in add_args.iter() {
let (coeff, deg) = term_as_kernel_power(ctx, *t, sin, u, var)?;
numer_add.push((coeff, deg));
}
}
_ => {
let tp = match_trig_pow(f)?;
let (se, ce) = trig_pair(tp.name)?;
if se != 0 && ce != 0 {
return None;
}
let is_sin = se != 0;
if is_sin {
use_sin = true;
} else {
use_cos = true;
}
unify_u(ctx, &mut shared_u, tp.arg)?;
let deg = if is_sin { se } else { ce }.checked_mul(tp.exp)?;
kernel_shift = kernel_shift.checked_add(deg)?;
}
}
}
if use_sin == use_cos {
return None;
}
let (da, db, dn) = denom?;
let u = shared_u?;
shared_linear_arg(ctx, u, var)?;
if numer_add.is_empty() {
numer_add.push((ctx.num(1), 0));
}
let shifted: Vec<(Atom<'a>, i64)> = numer_add
.into_iter()
.map(|(c, d)| Some((c, d.checked_add(kernel_shift)?)))
.collect::<Option<_>>()?;
let mut merged: BTreeMap<i64, Vec<Atom<'a>>> = BTreeMap::new();
for (c, d) in shifted {
merged.entry(d).or_default().push(c);
}
let numer: Vec<(Atom<'a>, i64)> = merged
.into_iter()
.map(|(d, cs)| (normalize(ctx, ctx.add(&cs)), d))
.collect();
Some(K1Match {
sin: use_sin,
u,
consts,
numer,
da,
db,
dn,
})
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Family {
TanSec,
CotCsc,
}
impl Family {
fn names(self) -> (&'static str, &'static str) {
match self {
Family::TanSec => ("tan", "sec"),
Family::CotCsc => ("cot", "csc"),
}
}
}
fn try_tan_sec_family<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
fam: Family,
) -> Option<Atom<'a>> {
let (tname, sname) = fam.names();
let factors: Vec<Atom<'a>> = match expr.node() {
AtomNode::Mul(args) => args.to_vec(),
_ => vec![expr],
};
let mut consts: Vec<Atom<'a>> = Vec::new();
let mut m = 0i64; let mut n = 0i64; let mut shared_u: Option<Atom<'a>> = None;
for f in factors {
if is_constant(f, var) {
consts.push(f);
continue;
}
let tp = match_trig_pow(f)?;
if tp.name != tname && tp.name != sname {
return None;
}
let nu = normalize(ctx, tp.arg);
match shared_u {
None => shared_u = Some(nu),
Some(u0) => {
if nu != u0 {
return None;
}
}
}
if tp.exp < 0 {
return None;
}
if tp.name == tname {
m = m.checked_add(tp.exp)?;
} else {
n = n.checked_add(tp.exp)?;
}
}
let u = shared_u?;
let (_, slope) = shared_linear_arg(ctx, u, var)?;
if m + n < 2 || m > MAX_POW || n > MAX_POW {
return None;
}
if m == 0 || n == 0 {
return None;
}
let tan = ctx.fun(tname, &[u]);
let sec = ctx.fun(sname, &[u]);
let f_inv = inv(ctx, slope);
let sign: i64 = match fam {
Family::TanSec => 1,
Family::CotCsc => -1,
};
let mut terms: Vec<Atom<'a>> = Vec::new();
if n >= 2 && n % 2 == 0 {
let h = (n - 2) / 2;
for k in 0..=h {
let j = m + 2 * k;
let c0 = normalize(
ctx,
ctx.mul(&[ctx.num(binom(h, k) * sign), f_inv, rat_atom(ctx, 1, j + 1)]),
);
terms.push(normalize(ctx, ctx.mul(&[c0, int_pow(ctx, tan, j + 1)])));
}
} else if m % 2 == 1 && n >= 1 {
let h = (m - 1) / 2;
for k in 0..=h {
let s2: i64 = if (h - k) % 2 == 0 { 1 } else { -1 };
let j = n - 1 + 2 * k;
let body = if j == 0 {
normalize(ctx, ctx.mul(&[f_inv, sec]))
} else {
normalize(
ctx,
ctx.mul(&[f_inv, rat_atom(ctx, 1, j + 1), int_pow(ctx, sec, j + 1)]),
)
};
terms.push(normalize(
ctx,
ctx.mul(&[ctx.num(binom(h, k) * s2 * sign), body]),
));
}
} else if m >= 2 && m % 2 == 0 && n % 2 == 1 {
let h = m / 2;
for k in 0..=h {
let s2: i64 = if (h - k) % 2 == 0 { 1 } else { -1 };
let j = n + 2 * k;
terms.push(normalize(
ctx,
ctx.mul(&[ctx.num(binom(h, k) * s2), int_pow(ctx, sec, j)]),
));
}
let mut all = consts.clone();
all.push(ctx.add(&terms));
let sum = normalize(ctx, ctx.mul(&all));
if sum == expr {
return None;
}
if node_count(sum) > MAX_REENTRY_NODES {
return None;
}
let r = integrate_raw(ctx, sum, var, 0, true, 0, 0);
if contains_integral(r) {
return None;
}
return Some(r);
} else {
return None;
}
if terms.is_empty() {
return None;
}
let mut all = consts;
all.push(ctx.add(&terms));
Some(normalize(ctx, ctx.mul(&all)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::integral::integrate;
use ocas_core::arena::Arena;
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(),
"sec" => v.cos().recip(),
"csc" => v.sin().recip(),
"cot" => v.tan().recip(),
"sinh" => v.sinh(),
"cosh" => v.cosh(),
"tanh" => v.tanh(),
"coth" => v.tanh().recip(),
"sech" => v.cosh().recip(),
"csch" => v.sinh().recip(),
"log" => v.ln(),
"sqrt" => v.sqrt(),
"atan" => v.atan(),
"atanh" => v.atanh(),
"asinh" => v.asinh(),
_ => return None,
})
}
}
}
fn parse_norm<'a>(ctx: &'a AtomArena<'a>, s: &str) -> Atom<'a> {
let e = ocas_parse::parse(ctx, s).unwrap();
normalize(ctx, e)
}
fn assert_module_solves(input: &str, consts: &[(Symbol, f64)], samples: &[f64]) {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, input);
let result = integrate_trig_kernel(&ctx, expr, Symbol::new("x"))
.unwrap_or_else(|| panic!("declined: {input}"));
assert!(
!result.to_string().contains("Integral"),
"residue for {input}: {result}"
);
let d = crate::diff(&ctx, result, Symbol::new("x"));
for &xv in samples {
let mut env = consts.to_vec();
env.push((Symbol::new("x"), xv));
let lhs = eval_f64(d, &env).expect("eval diff");
let rhs = eval_f64(expr, &env).expect("eval integrand");
let tol = 1e-5 * rhs.abs().max(1.0);
assert!(
(lhs - rhs).abs() < tol,
"{input} at x={xv}: diff={lhs} integrand={rhs} (result: {result})"
);
}
}
fn assert_module_no_residue(input: &str) {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, input);
let result = integrate_trig_kernel(&ctx, expr, Symbol::new("x"))
.unwrap_or_else(|| panic!("declined: {input}"));
assert!(
!result.to_string().contains("Integral"),
"residue for {input}: {result}"
);
}
fn assert_module_declined(input: &str) {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, input);
assert!(
integrate_trig_kernel(&ctx, expr, Symbol::new("x")).is_none(),
"expected None for {input}"
);
}
#[test]
fn k1_sec_over_linear_sec_squared() {
let env = [
(Symbol::new("a"), 2.0),
(Symbol::new("c"), 0.3),
(Symbol::new("d"), 0.7),
];
assert_module_solves(
"sec(c + d*x)/(a + a*sec(c + d*x))^2",
&env,
&[0.2, 0.5, 0.9],
);
}
#[test]
fn k1_linear_numerator_over_sin() {
assert_module_solves("(3 - 3*sin(2*x))^2/(1 + sin(2*x))", &[], &[0.2, 0.5, 0.9]);
}
#[test]
fn k1_corpus_sin_fourth_power() {
let env = [
(Symbol::new("a"), 2.0),
(Symbol::new("c"), 1.5),
(Symbol::new("e"), 0.4),
(Symbol::new("f"), 0.8),
];
assert_module_solves(
"(c - c*sin(e + f*x))^4/(a + a*sin(e + f*x))",
&env,
&[0.15, 0.35, 0.6],
);
}
#[test]
fn k1_sec_cubed_over_sec_squared_denom() {
assert_module_no_residue("sec(c + d*x)^3/(a + b*sec(c + d*x))^2");
}
#[test]
fn k1_abc_numerator_poly() {
assert_module_no_residue("(A + B*sec(c + d*x) + C*sec(c + d*x)^2)/(a + b*sec(c + d*x))^2");
}
#[test]
fn k2_tan2_sec2() {
assert_module_solves("tan(x)^2*sec(x)^2", &[], &[0.3, 0.6, 1.0]);
}
#[test]
fn k2_tan3_sec2_linear() {
let env = [(Symbol::new("c"), 0.2), (Symbol::new("d"), 0.9)];
assert_module_solves("tan(c + d*x)^3*sec(c + d*x)^2", &env, &[0.2, 0.4, 0.7]);
}
#[test]
fn k2_tan3_sec3() {
assert_module_solves("tan(x)^3*sec(x)^3", &[], &[0.3, 0.6, 1.0]);
}
#[test]
fn k2_tan2_sec3_chain_reentry() {
assert_module_solves("tan(x)^2*sec(x)^3", &[], &[0.3, 0.6, 1.0]);
}
#[test]
fn k2_cot_csc_mirror() {
assert_module_solves("cot(x)^2*csc(x)^2", &[], &[0.4, 0.8, 1.1]);
assert_module_solves("cot(x)^3*csc(x)^3", &[], &[0.4, 0.8, 1.1]);
}
#[test]
fn declines_out_of_scope() {
assert_module_declined("1/(2 + 3*cos(x))^2");
assert_module_declined("1/(2*sec(x))^(3/2)");
assert_module_declined("sin(x)^2*cos(x)/(1 + cos(x))");
assert_module_declined("sec(x)/(1 + sec(2*x))");
assert_module_declined("sec(x^2)/(1 + sec(x^2))");
}
#[test]
fn chain_end_to_end() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "(3 - 3*sin(2*x))^2/(1 + sin(2*x))");
let r = integrate(&ctx, expr, Symbol::new("x"));
assert!(
!r.to_string().contains("Integral"),
"chain left residue: {r}"
);
}
const HANG_SHAPES: &[&str] = &[
"sech(x)/(a + b*sinh(x))", "sech(x)/(a + b*csch(x))", "tanh(x)^3/(a + b*csch(x))", ];
#[test]
fn corpus_hang_shapes_decline() {
for input in HANG_SHAPES {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, input);
match integrate_trig_kernel(&ctx, expr, Symbol::new("x")) {
None => {}
Some(r) => assert!(
!r.to_string().contains("Integral"),
"{input} produced a residue: {r}"
),
}
}
}
#[test]
fn budget_keeps_solving_normal_inputs() {
assert_module_solves("tan(x)^2*sec(x)^2", &[], &[0.3, 0.6, 1.0]);
assert_module_solves("tan(x)^3*sec(x)^3", &[], &[0.3, 0.6, 1.0]);
assert_module_no_residue("sec(c + d*x)^3/(a + b*sec(c + d*x))^2");
}
#[test]
fn budget_does_not_leak_across_calls() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "sech(x)/(a + b*sinh(x))");
let start = std::time::Instant::now();
let first = integrate_trig_kernel(&ctx, expr, Symbol::new("x")).map(|a| a.to_string());
for i in 0..256 {
let r = integrate_trig_kernel(&ctx, expr, Symbol::new("x")).map(|a| a.to_string());
assert_eq!(r, first, "call {i} diverged");
}
assert!(
start.elapsed().as_secs() < 30,
"256 calls took {:?}; the budget is not containing the shape",
start.elapsed()
);
}
#[test]
fn peel_enumeration_is_charged_and_stable() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "sec(c + d*x)^7/(a + b*sec(c + d*x))^7");
let var = Symbol::new("x");
let first = integrate_trig_kernel(&ctx, expr, var).map(|a| a.to_string());
for i in 0..64 {
let r = integrate_trig_kernel(&ctx, expr, var).map(|a| a.to_string());
assert_eq!(r, first, "call {i} diverged");
}
}
}