use ocas_atom::normalize::normalize;
use ocas_atom::{Atom, AtomArena, AtomNode, Symbol};
use super::{
contains_integral, integrate_raw, is_constant, is_fallback, linear_form, node_count, rat_atom,
};
const MAX_DENOM_POW: i64 = 6;
const MAX_POLY_DEG: i64 = 6;
const MAX_LIN_POW: i64 = 4;
const MAX_RATIO_DEG: i64 = 4;
const MAX_NODES: usize = 200;
const PHASE_RATIO_ENABLED: bool = false;
const MAX_RESULT_NODES: usize = 4000;
const MAX_SUB_CALLS: u32 = 2;
pub(crate) fn integrate_trig_reduction<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
) -> Option<Atom<'a>> {
if !PHASE_RATIO_ENABLED {
return integrate_trig_denom(ctx, expr, var)
.or_else(|| integrate_trig_num_linear(ctx, expr, var))
.or_else(|| integrate_poly_trig(ctx, expr, var));
}
integrate_trig_denom(ctx, expr, var)
.or_else(|| integrate_phase_shift(ctx, expr, var))
.or_else(|| integrate_trig_num_linear(ctx, expr, var))
.or_else(|| integrate_poly_kernel_ratio(ctx, expr, var))
.or_else(|| integrate_poly_trig(ctx, expr, var))
}
struct DenomMatch<'a> {
rest: Vec<Atom<'a>>,
other: Vec<Atom<'a>>,
base: Atom<'a>,
a: Atom<'a>,
b: Atom<'a>,
sin: bool,
u: Atom<'a>,
du: Atom<'a>,
n: i64,
disc: Atom<'a>,
}
fn match_trig_denom<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
) -> Option<DenomMatch<'a>> {
let factors: Vec<Atom<'a>> = match expr.node() {
AtomNode::Mul(args) => args.to_vec(),
_ => vec![expr],
};
let mut rest = Vec::new();
let mut other = Vec::new();
let mut found: Option<(Atom<'a>, Atom<'a>, Atom<'a>, bool, Atom<'a>, Atom<'a>, i64)> = None;
for f in factors {
if is_constant(f, var) {
rest.push(f);
continue;
}
if let Some((base, n)) = as_recip_pow(f)
&& let Some((a, b, sin, u, du)) = split_trig_base(ctx, base, var)
{
if found.is_some() {
return None;
}
found = Some((base, a, b, sin, u, du, n));
continue;
}
other.push(f);
}
let (base, a, b, sin, u, du, n) = found?;
let other: Vec<Atom<'a>> = if other.is_empty() {
Vec::new()
} else {
let folded = normalize(ctx, ctx.mul(&other));
if is_constant(folded, var) {
Vec::new()
} else {
vec![folded]
}
};
let disc = discriminant(ctx, a, b)?;
Some(DenomMatch {
rest,
other,
base,
a,
b,
sin,
u,
du,
n,
disc,
})
}
fn as_recip_pow(f: Atom<'_>) -> Option<(Atom<'_>, i64)> {
let AtomNode::Pow(b, e) = f.node() else {
return None;
};
if let (AtomNode::Pow(bb, ee), AtomNode::Num(-1)) = (b.node(), e.node())
&& let AtomNode::Num(n) = ee.node()
&& *n >= 1
{
return Some((*bb, *n));
}
if let AtomNode::Num(n) = e.node()
&& *n <= -1
{
return Some((*b, n.checked_neg()?));
}
None
}
enum TermSplit<'a> {
Constant,
Trig {
coeff: Atom<'a>,
sin: bool,
u: Atom<'a>,
},
Other,
}
fn split_trig_term<'a>(ctx: &'a AtomArena<'a>, term: Atom<'a>, var: Symbol) -> TermSplit<'a> {
if is_constant(term, var) {
return TermSplit::Constant;
}
let factors: Vec<Atom<'a>> = match term.node() {
AtomNode::Mul(args) => args.to_vec(),
AtomNode::Fun(name, args)
if args.len() == 1 && (name.as_str() == "sin" || name.as_str() == "cos") =>
{
return TermSplit::Trig {
coeff: ctx.num(1),
sin: name.as_str() == "sin",
u: args[0],
};
}
_ => vec![term],
};
let mut coeff: Vec<Atom<'a>> = Vec::new();
let mut trig: Option<(bool, Atom<'a>)> = None;
for f in factors {
if is_constant(f, var) {
coeff.push(f);
continue;
}
match f.node() {
AtomNode::Fun(name, args)
if args.len() == 1 && (name.as_str() == "sin" || name.as_str() == "cos") =>
{
if trig.is_some() {
return TermSplit::Other;
}
trig = Some((name.as_str() == "sin", args[0]));
}
_ => return TermSplit::Other,
}
}
let Some((sin, u)) = trig else {
return TermSplit::Other;
};
let coeff = if coeff.is_empty() {
ctx.num(1)
} else {
normalize(ctx, ctx.mul(&coeff))
};
TermSplit::Trig { coeff, sin, u }
}
type TrigTerm<'a> = (Atom<'a>, bool, Atom<'a>);
fn split_base_trigs<'a>(
ctx: &'a AtomArena<'a>,
base: Atom<'a>,
var: Symbol,
) -> Option<(Atom<'a>, Vec<TrigTerm<'a>>)> {
let AtomNode::Add(args) = base.node() else {
return None;
};
let mut consts: Vec<Atom<'a>> = Vec::new();
let mut trigs: Vec<TrigTerm<'a>> = Vec::new();
for t in args.iter() {
match split_trig_term(ctx, *t, var) {
TermSplit::Constant => consts.push(*t),
TermSplit::Trig { coeff, sin, u } => trigs.push((coeff, sin, u)),
TermSplit::Other => return None,
}
}
let a = if consts.is_empty() {
ctx.num(0)
} else {
normalize(ctx, ctx.add(&consts))
};
Some((a, trigs))
}
fn split_trig_base<'a>(
ctx: &'a AtomArena<'a>,
base: Atom<'a>,
var: Symbol,
) -> Option<(Atom<'a>, Atom<'a>, bool, Atom<'a>, Atom<'a>)> {
let (a, trigs) = split_base_trigs(ctx, base, var)?;
if trigs.len() != 1 {
return None;
}
let (b, sin, u) = trigs[0];
let (du, _phase) = linear_form(ctx, u, var)?;
if matches!(du.node(), AtomNode::Num(0)) {
return None;
}
Some((a, b, sin, u, du))
}
fn split_mixed_base<'a>(
ctx: &'a AtomArena<'a>,
base: Atom<'a>,
var: Symbol,
) -> Option<(Atom<'a>, Atom<'a>, Atom<'a>, Atom<'a>, Atom<'a>)> {
let (a, trigs) = split_base_trigs(ctx, base, var)?;
if trigs.len() != 2 {
return None;
}
let (mut b, mut c) = (None, None);
let mut u_norm = None;
for (coeff, sin, u) in trigs {
if !is_constant(coeff, var) {
return None;
}
let slot = if sin { &mut c } else { &mut b };
if slot.is_some() {
return None;
}
*slot = Some(coeff);
let un = normalize(ctx, u);
match u_norm {
None => u_norm = Some(un),
Some(prev) if prev == un => {}
Some(_) => return None,
}
}
let (b, c) = (b?, c?);
let u = u_norm?;
if matches!(b.node(), AtomNode::Num(0)) && matches!(c.node(), AtomNode::Num(0)) {
return None;
}
let (du, _phase) = linear_form(ctx, u, var)?;
if matches!(du.node(), AtomNode::Num(0)) {
return None;
}
Some((a, b, c, u, du))
}
fn discriminant<'a>(ctx: &'a AtomArena<'a>, a: Atom<'a>, b: Atom<'a>) -> Option<Atom<'a>> {
if let (Some(av), Some(bv)) = (numeric(a), numeric(b)) {
let d = (av as i128)
.checked_mul(av as i128)?
.checked_sub((bv as i128).checked_mul(bv as i128)?)?;
if d == 0 {
return None;
}
return i64::try_from(d).ok().map(|v| ctx.num(v));
}
let sq_a = square(ctx, a)?;
let sq_b = square(ctx, b)?;
let sum = normalize(
ctx,
fold_sqrt_squares(ctx, ctx.add(&[sq_a, ctx.mul(&[ctx.num(-1), sq_b])])),
);
let d = crate::ode::util::collect_terms(ctx, sum);
if matches!(d.node(), AtomNode::Num(0)) {
return None;
}
Some(d)
}
fn fold_sqrt_squares<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>) -> Atom<'a> {
match expr.node() {
AtomNode::Num(_) | AtomNode::Var(_) => expr,
AtomNode::Add(args) => {
let kids: Vec<Atom<'a>> = args.iter().map(|a| fold_sqrt_squares(ctx, *a)).collect();
normalize(ctx, ctx.add(&kids))
}
AtomNode::Mul(args) => {
let kids: Vec<Atom<'a>> = args.iter().map(|a| fold_sqrt_squares(ctx, *a)).collect();
normalize(ctx, ctx.mul(&kids))
}
AtomNode::Fun(_, _) => expr,
AtomNode::Pow(b, e) => {
let base = fold_sqrt_squares(ctx, *b);
let exp = fold_sqrt_squares(ctx, *e);
if let AtomNode::Num(k) = exp.node()
&& *k % 2 == 0
&& let AtomNode::Fun(name, fargs) = base.node()
&& name.as_str() == "sqrt"
&& fargs.len() == 1
&& let Some(folded) = int_pow(ctx, fargs[0], *k)
{
return folded;
}
normalize(ctx, ctx.pow(base, exp))
}
}
}
fn square<'a>(ctx: &'a AtomArena<'a>, e: Atom<'a>) -> Option<Atom<'a>> {
match e.node() {
AtomNode::Num(v) => Some(ctx.num(v.checked_mul(*v)?)),
_ => Some(ctx.pow(e, ctx.num(2))),
}
}
pub(crate) fn integrate_trig_denom<'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 m = match_trig_denom(ctx, expr, var)?;
if !(2..=MAX_DENOM_POW).contains(&m.n) || !m.other.is_empty() {
return None;
}
let core = denom_integral(ctx, &m, m.n, var)?;
let mut factors = m.rest.clone();
factors.push(core);
Some(normalize(ctx, ctx.mul(&factors)))
}
fn denom_integral<'a>(
ctx: &'a AtomArena<'a>,
m: &DenomMatch<'a>,
n: i64,
var: Symbol,
) -> Option<Atom<'a>> {
debug_assert!((1..=MAX_DENOM_POW).contains(&n));
if n == 1 {
let g = ctx.pow(m.base, ctx.num(-1));
let r = integrate_raw(ctx, g, var, 0, true, 0, 0);
if contains_integral(r) || is_fallback(&r) {
return weierstrass_base(ctx, m, var);
}
return Some(r);
}
let j1 = denom_integral(ctx, m, n - 1, var)?;
let j2 = if n == 2 {
None
} else {
Some(denom_integral(ctx, m, n - 2, var)?)
};
let (sign, other_sin) = if m.sin { (1, false) } else { (-1, true) };
let boundary = ctx.mul(&[
ctx.num(sign),
m.b,
ctx.fun(if other_sin { "sin" } else { "cos" }, &[m.u]),
ctx.pow(m.base, ctx.num(-(n - 1))),
ctx.pow(m.du, ctx.num(-1)),
]);
let mut terms = vec![boundary, ctx.mul(&[m.a, ctx.num(2 * n - 3), j1])];
if let Some(j2) = j2 {
terms.push(ctx.mul(&[ctx.num(-(n - 2)), j2]));
}
let bracket = normalize(ctx, ctx.add(&terms));
let denom = normalize(ctx, ctx.mul(&[ctx.num(n - 1), m.disc]));
Some(normalize(
ctx,
ctx.mul(&[bracket, ctx.pow(denom, ctx.num(-1))]),
))
}
fn weierstrass_base<'a>(
ctx: &'a AtomArena<'a>,
m: &DenomMatch<'a>,
var: Symbol,
) -> Option<Atom<'a>> {
let (d, e) = linear_form(ctx, m.u, var)?;
if matches!(d.node(), AtomNode::Num(0)) {
return None;
}
if matches!(m.disc.node(), AtomNode::Num(v) if *v < 0) {
return None;
}
let sign_a = if m.sin {
ctx.num(1)
} else {
match numeric(m.a) {
Some(v) if v > 0 => ctx.num(1),
Some(v) if v < 0 => ctx.num(-1),
Some(_) => return None,
None => return None,
}
};
let half = rat_atom(ctx, 1, 2);
let inv_half = rat_atom(ctx, -1, 2);
let v = ctx.add(&[ctx.mul(&[d, ctx.var(var.as_str())]), e]);
let t = ctx.fun("tan", &[normalize(ctx, ctx.mul(&[half, v]))]);
let inv_root = ctx.pow(m.disc, inv_half);
let atan_arg = if m.sin {
ctx.mul(&[ctx.add(&[m.b, ctx.mul(&[m.a, t])]), inv_root])
} else {
let ratio = normalize(
ctx,
ctx.mul(&[
ctx.add(&[m.a, ctx.mul(&[ctx.num(-1), m.b])]),
inv(ctx, ctx.add(&[m.a, m.b])),
]),
);
ctx.mul(&[t, ctx.fun("sqrt", &[ratio])])
};
let atan = ctx.fun("atan", &[normalize(ctx, atan_arg)]);
Some(normalize(
ctx,
ctx.mul(&[ctx.num(2), sign_a, inv_root, atan]),
))
}
pub(crate) fn integrate_phase_shift<'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 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 found: Option<(Atom<'a>, Atom<'a>, Atom<'a>, Atom<'a>)> = None;
for f in &factors {
if is_constant(*f, var) {
rest.push(*f);
continue;
}
let Some((base, _n)) = as_recip_pow(*f) else {
continue;
};
let Some((a, b, c, u, _du)) = split_mixed_base(ctx, base, var) else {
continue;
};
if found.is_some() {
return None;
}
found = Some((a, b, c, u));
}
let (a, b, c, u) = found?;
let phi = ctx.fun("atan", &[normalize(ctx, ctx.mul(&[c, inv(ctx, b)]))]);
let r_sq = normalize(ctx, ctx.add(&[square(ctx, b)?, square(ctx, c)?]));
let r = ctx.fun("sqrt", &[r_sq]);
discriminant(ctx, a, r)?;
for flip in [false, true] {
let base = shifted_base(ctx, a, r, u, phi, flip);
let shifted = rebuild(ctx, &factors, &rest, base, var);
let Some(out) = integrate_trig_reduction(ctx, shifted, var) else {
continue;
};
if contains_integral(out) {
continue;
}
return Some(out);
}
None
}
fn shifted_base<'a>(
ctx: &'a AtomArena<'a>,
a: Atom<'a>,
r: Atom<'a>,
u: Atom<'a>,
phi: Atom<'a>,
flip: bool,
) -> Atom<'a> {
let r_signed = if flip { ctx.mul(&[ctx.num(-1), r]) } else { r };
let v = normalize(ctx, ctx.add(&[u, ctx.mul(&[ctx.num(-1), phi])]));
let kernel = ctx.mul(&[r_signed, ctx.fun("cos", &[v])]);
if matches!(a.node(), AtomNode::Num(0)) {
normalize(ctx, kernel)
} else {
normalize(ctx, ctx.add(&[a, kernel]))
}
}
fn rebuild<'a>(
ctx: &'a AtomArena<'a>,
factors: &[Atom<'a>],
rest: &[Atom<'a>],
base: Atom<'a>,
var: Symbol,
) -> Atom<'a> {
let mut out: Vec<Atom<'a>> = rest.to_vec();
for f in factors {
if is_constant(*f, var) {
continue;
}
if let Some((old_base, n)) = as_recip_pow(*f)
&& split_mixed_base(ctx, old_base, var).is_some()
{
out.push(ctx.pow(base, ctx.num(-n)));
continue;
}
out.push(*f);
}
if out.is_empty() {
return ctx.num(1);
}
normalize(ctx, ctx.mul(&out))
}
pub(crate) fn integrate_poly_kernel_ratio<'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 m = match_trig_denom(ctx, expr, var)?;
if !(1..=MAX_DENOM_POW).contains(&m.n) {
return None;
}
let (coef, deg) = split_numerator_monomial(ctx, *m.other.first()?, var, m.sin, m.u)?;
if deg == 0 || deg > MAX_RATIO_DEG || deg >= m.n {
return None;
}
let mut calls = 0u32;
let core = reduce_ratio(ctx, &m, coef, deg, var, &mut calls)?;
let result = multiply_all(ctx, &m.rest, core);
if contains_integral(result) || node_count(result) > MAX_RESULT_NODES {
return None;
}
Some(result)
}
fn multiply_all<'a>(ctx: &'a AtomArena<'a>, factors: &[Atom<'a>], extra: Atom<'a>) -> Atom<'a> {
match factors {
[] => extra,
[f] => normalize(ctx, ctx.mul(&[*f, extra])),
_ => {
let mut all: Vec<Atom<'a>> = factors.to_vec();
all.push(extra);
normalize(ctx, ctx.mul(&all))
}
}
}
fn reduce_ratio<'a>(
ctx: &'a AtomArena<'a>,
m: &DenomMatch<'a>,
coef: Atom<'a>,
deg: i64,
var: Symbol,
calls: &mut u32,
) -> Option<Atom<'a>> {
debug_assert!(deg >= 0 && deg < m.n);
let mut p = coef;
let mut power = m.n;
let mut out: Vec<Atom<'a>> = Vec::new();
loop {
debug_assert!(power >= 1);
out.push(ratio_boundary(ctx, m, p, power)?);
if power == 1 {
*calls += 1;
if *calls > MAX_SUB_CALLS {
return None;
}
let j1 = denom_integral(ctx, m, 1, var)?;
if contains_integral(j1) || is_fallback(&j1) {
return None;
}
out.push(ctx.mul(&[ratio_const(ctx, m)?, p, j1]));
return Some(normalize(ctx, ctx.add(&out)));
}
p = ratio_next(ctx, m, &p, power, var)?;
if !is_polynomial_in_var(p, var) {
return None;
}
power -= 1;
}
}
fn ratio_const<'a>(ctx: &'a AtomArena<'a>, m: &DenomMatch<'a>) -> Option<Atom<'a>> {
let eps = if m.sin { 1 } else { -1 };
let signed = ctx.mul(&[ctx.num(eps), inv(ctx, m.b)]);
Some(normalize(ctx, ctx.mul(&[ctx.num(-1), signed])))
}
fn ratio_boundary<'a>(
ctx: &'a AtomArena<'a>,
m: &DenomMatch<'a>,
p: Atom<'a>,
n: i64,
) -> Option<Atom<'a>> {
let c = ratio_const(ctx, m)?;
Some(normalize(
ctx,
ctx.mul(&[c, p, ctx.pow(m.base, ctx.num(1 - n))]),
))
}
fn ratio_next<'a>(
ctx: &'a AtomArena<'a>,
m: &DenomMatch<'a>,
p: &Atom<'a>,
_n: i64,
var: Symbol,
) -> Option<Atom<'a>> {
let shift = normalize(ctx, ctx.mul(&[m.a, inv(ctx, m.b), *p]));
let derivative = crate::diff(ctx, *p, var);
let slope = normalize(ctx, ctx.mul(&[inv(ctx, m.b), inv(ctx, m.du), derivative]));
Some(normalize(ctx, ctx.add(&[shift, slope])))
}
fn split_numerator_monomial<'a>(
ctx: &'a AtomArena<'a>,
num: Atom<'a>,
var: Symbol,
sin: bool,
u_den: Atom<'a>,
) -> Option<(Atom<'a>, i64)> {
let factors: Vec<Atom<'a>> = match num.node() {
AtomNode::Mul(args) => args.to_vec(),
_ => vec![num],
};
let mut coef: Vec<Atom<'a>> = Vec::new();
let mut deg: Option<i64> = None;
let mut kernel_seen = false;
for f in factors {
if let AtomNode::Fun(name, args) = f.node()
&& args.len() == 1
&& (name.as_str() == "sin" || name.as_str() == "cos")
{
let is_sin = name.as_str() == "sin";
let arg_eq = normalize(ctx, args[0]) == normalize(ctx, u_den);
eprintln!();
if kernel_seen || is_sin != sin || !arg_eq {
return None;
}
kernel_seen = true;
continue;
}
if deg.is_none() {
match f.node() {
AtomNode::Var(v) if *v == var => {
deg = Some(1);
continue;
}
AtomNode::Pow(b, e)
if matches!(b.node(), AtomNode::Var(v) if *v == var)
&& matches!(e.node(), AtomNode::Num(k) if *k >= 1) =>
{
let AtomNode::Num(k) = e.node() else {
return None;
};
deg = Some(*k);
continue;
}
_ => {}
}
}
if !is_constant(f, var) {
return None;
}
coef.push(f);
}
if !kernel_seen {
return None;
}
let deg = deg?;
let c = if coef.is_empty() {
ctx.num(1)
} else {
normalize(ctx, ctx.mul(&coef))
};
Some((c, deg))
}
fn is_polynomial_in_var(expr: Atom<'_>, var: Symbol) -> bool {
let terms: Vec<Atom<'_>> = match expr.node() {
AtomNode::Add(args) => args.to_vec(),
_ => vec![expr],
};
terms.iter().all(|t| {
let factors: Vec<Atom<'_>> = match t.node() {
AtomNode::Mul(args) => args.to_vec(),
_ => vec![*t],
};
let mut deg = 0i64;
for f in factors {
match f.node() {
AtomNode::Var(v) if *v == var => deg += 1,
AtomNode::Pow(b, e) => {
if matches!(b.node(), AtomNode::Var(v) if *v == var)
&& let AtomNode::Num(k) = e.node()
{
deg += *k;
} else if !is_constant(f, var) {
return false;
}
}
_ => {
if !is_constant(f, var) {
return false;
}
}
}
}
deg <= MAX_RATIO_DEG
})
}
pub(crate) fn integrate_trig_num_linear<'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 m = match_trig_denom(ctx, expr, var)?;
if !(2..=MAX_DENOM_POW).contains(&m.n) || m.other.len() != 1 {
return None;
}
let u_norm = normalize(ctx, m.u);
let (num_a, num_b) = split_numerator(ctx, m.other[0], var, m.sin, u_norm)?;
if matches!(num_b.node(), AtomNode::Num(0)) {
return None; }
let jn = denom_integral(ctx, &m, m.n, var)?;
let jn1 = denom_integral(ctx, &m, m.n - 1, var)?;
let term_a = ctx.mul(&[num_a, jn]);
let inner = normalize(ctx, ctx.add(&[jn1, ctx.mul(&[ctx.num(-1), m.a, jn])]));
let term_b = ctx.mul(&[num_b, inv(ctx, m.b), inner]);
let core = normalize(ctx, ctx.add(&[term_a, term_b]));
let mut factors = m.rest.clone();
factors.push(core);
Some(normalize(ctx, ctx.mul(&factors)))
}
fn split_numerator<'a>(
ctx: &'a AtomArena<'a>,
num: Atom<'a>,
var: Symbol,
sin: bool,
u_norm: Atom<'a>,
) -> Option<(Atom<'a>, Atom<'a>)> {
let terms: Vec<Atom<'a>> = match num.node() {
AtomNode::Add(args) => args.to_vec(),
_ => vec![num],
};
let mut a_terms = Vec::new();
let mut b_terms = Vec::new();
for t in terms {
match split_trig_term(ctx, t, var) {
TermSplit::Constant => a_terms.push(t),
TermSplit::Trig { coeff, sin: s2, u } => {
if s2 != sin || normalize(ctx, u) != u_norm {
return None;
}
b_terms.push(coeff);
}
TermSplit::Other => return None,
}
}
let a = if a_terms.is_empty() {
ctx.num(0)
} else {
normalize(ctx, ctx.add(&a_terms))
};
let b = if b_terms.is_empty() {
ctx.num(0)
} else {
normalize(ctx, ctx.add(&b_terms))
};
Some((a, b))
}
pub(crate) fn integrate_poly_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 factors: Vec<Atom<'a>> = match expr.node() {
AtomNode::Mul(args) => args.to_vec(),
_ => vec![expr],
};
let mut rest = Vec::new();
let mut poly: Option<Vec<(Atom<'a>, i64)>> = None;
let mut trig: Option<(bool, Atom<'a>, Atom<'a>)> = None;
for f in factors {
if is_constant(f, var) {
rest.push(f);
continue;
}
match f.node() {
AtomNode::Fun(name, args)
if args.len() == 1 && (name.as_str() == "sin" || name.as_str() == "cos") =>
{
if trig.is_some() {
return None;
}
let (slope, _phase) = linear_form(ctx, args[0], var)?;
if matches!(slope.node(), AtomNode::Num(0)) {
return None;
}
trig = Some((name.as_str() == "sin", args[0], slope));
}
AtomNode::Var(v) if *v == var => {
if poly.is_some() {
return None;
}
poly = Some(vec![(ctx.num(1), 1)]);
}
AtomNode::Pow(b, e) => {
if poly.is_some() {
return None;
}
let AtomNode::Num(k) = e.node() else {
return None;
};
if matches!(b.node(), AtomNode::Var(v) if *v == var) {
if !(1..=MAX_POLY_DEG).contains(k) {
return None;
}
poly = Some(vec![(ctx.num(1), *k)]);
} else {
if !(1..=MAX_LIN_POW).contains(k) {
return None;
}
poly = Some(expand_linear_pow(ctx, *b, *k, var)?);
}
}
_ => return None,
}
}
let poly = poly?;
let (sin, u, slope) = trig?;
let mut terms = Vec::new();
for (coef, deg) in poly {
let cf = closed_form(ctx, deg, sin, u, slope, var)?;
match cf.node() {
AtomNode::Add(args) => {
for t in args.iter() {
terms.push(ctx.mul(&[coef, *t]));
}
}
_ => terms.push(ctx.mul(&[coef, cf])),
}
}
let core = normalize(ctx, ctx.add(&terms));
let mut factors = rest;
factors.push(core);
Some(normalize(ctx, ctx.mul(&factors)))
}
fn expand_linear_pow<'a>(
ctx: &'a AtomArena<'a>,
base: Atom<'a>,
k: i64,
var: Symbol,
) -> Option<Vec<(Atom<'a>, i64)>> {
let (d, c) = linear_form(ctx, base, var)?;
if matches!(d.node(), AtomNode::Num(0)) {
return None;
}
let mut out = Vec::new();
for j in 0..=k {
let coef = normalize(
ctx,
ctx.mul(&[
ctx.num(binom(k, j)),
int_pow(ctx, c, k - j)?,
int_pow(ctx, d, j)?,
]),
);
if matches!(coef.node(), AtomNode::Num(0)) {
continue;
}
out.push((coef, j));
}
if out.is_empty() { None } else { Some(out) }
}
fn closed_form<'a>(
ctx: &'a AtomArena<'a>,
m: i64,
sin: bool,
u: Atom<'a>,
slope: Atom<'a>,
var: Symbol,
) -> Option<Atom<'a>> {
debug_assert!((0..=MAX_POLY_DEG).contains(&m));
let x = ctx.var(var.as_str());
let mut terms = Vec::new();
let mut fall: i64 = 1; for k in 0..=m {
if k > 0 {
fall = fall.checked_mul(m - k + 1)?;
}
let out_sin = if sin { k % 2 == 1 } else { k % 2 == 0 };
let neg = if sin {
matches!(k % 4, 0 | 3)
} else {
k % 4 >= 2
};
let acoef = match slope.node() {
AtomNode::Num(av) => {
let p = av.checked_pow((k + 1) as u32)?;
ctx.pow(ctx.num(p), ctx.num(-1))
}
_ => ctx.pow(slope, ctx.num(-(k + 1))),
};
let mut fs: Vec<Atom<'a>> = Vec::new();
if neg {
fs.push(ctx.num(-1));
}
if fall != 1 {
fs.push(ctx.num(fall));
}
let deg = m - k;
if deg > 0 {
fs.push(int_pow(ctx, x, deg)?);
}
fs.push(acoef);
fs.push(ctx.fun(if out_sin { "sin" } else { "cos" }, &[u]));
terms.push(normalize(ctx, ctx.mul(&fs)));
}
Some(normalize(ctx, ctx.add(&terms)))
}
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 numeric(e: Atom<'_>) -> Option<i64> {
if let AtomNode::Num(n) = e.node() {
Some(*n)
} else {
None
}
}
fn inv<'a>(ctx: &'a AtomArena<'a>, a: Atom<'a>) -> Atom<'a> {
match a.node() {
AtomNode::Num(1) => ctx.num(1),
AtomNode::Num(-1) => ctx.num(-1),
_ => ctx.pow(a, ctx.num(-1)),
}
}
fn int_pow<'a>(ctx: &'a AtomArena<'a>, base: Atom<'a>, e: i64) -> Option<Atom<'a>> {
if e < 0 {
return Some(ctx.pow(base, ctx.num(e)));
}
match e {
0 => Some(ctx.num(1)),
1 => Some(base),
_ => {
if let AtomNode::Num(v) = base.node() {
Some(ctx.num(v.checked_pow(e as u32)?))
} else {
Some(ctx.pow(base, ctx.num(e)))
}
}
}
}
#[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(),
"sec" => v.cos().recip(),
"csc" => v.sin().recip(),
"cot" => v.tan().recip(),
"exp" => v.exp(),
"log" => v.ln(),
"sqrt" => v.sqrt(),
"atan" => v.atan(),
_ => return None,
})
}
}
}
fn assert_antiderivative_num<'a>(
ctx: &'a AtomArena<'a>,
integrand: Atom<'a>,
var: Symbol,
consts: &[(Symbol, f64)],
samples: &[f64],
) {
let result = integrate_trig_reduction(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_trig_reduction(ctx, expr, Symbol::new("x")).is_none(),
"expected None for {input}"
);
}
#[test]
fn t1_cos_n2_numeric() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "1/(2 + cos(x))^2");
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &[], &[0.3, 0.7, 1.1]);
}
#[test]
fn t1_cos_n3_corpus() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "1/(-5 + 3*cos(c + d*x))^3");
let env = [(Symbol::new("c"), 0.4), (Symbol::new("d"), 1.3)];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.2, 0.5, 0.9]);
}
#[test]
fn t1_sin_n2_numeric() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "1/(2 + sin(x))^2");
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &[], &[0.3, 0.7, 1.1]);
}
#[test]
fn t1_cos_n4_symbolic() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "1/(a + b*cos(c + d*x))^4");
let env = [
(Symbol::new("a"), 2.0),
(Symbol::new("b"), 0.5),
(Symbol::new("c"), 0.3),
(Symbol::new("d"), 0.8),
];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.2, 0.6, 1.0]);
}
#[test]
fn t1_singular_declines() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
assert_declined(&ctx, "1/(2 + 2*cos(x))^2");
assert_declined(&ctx, "1/(a + a*sin(c + d*x))^2");
}
#[test]
fn t1_out_of_scope_declines() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
assert_declined(&ctx, "1/(2 + cos(x))");
assert_declined(&ctx, "1/(2 + cos(x))^7");
assert_declined(&ctx, "1/(2*cos(x))^2");
}
#[test]
fn t2_cos_linear_num() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "(1 + 3*cos(x))/(2 + cos(x))^2");
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &[], &[0.3, 0.7, 1.1]);
}
#[test]
fn t2_sin_symbolic() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "(A + B*cos(c + d*x))/(a + b*cos(c + d*x))^3");
let env = [
(Symbol::new("A"), 1.5),
(Symbol::new("B"), -0.5),
(Symbol::new("a"), 2.0),
(Symbol::new("b"), 0.5),
(Symbol::new("c"), 0.3),
(Symbol::new("d"), 1.1),
];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.2, 0.5, 0.8]);
}
#[test]
fn t2_bare_trig_numerator() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "sin(x)/(2 + sin(x))^2");
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &[], &[0.3, 0.7, 1.1]);
}
#[test]
fn t2_two_denominator_powers_decline() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
assert_declined(
&ctx,
"(A + B*sin(e + f*x))/((a + a*sin(e + f*x))^3*(c - c*sin(e + f*x))^2)",
);
}
#[test]
fn t2_mismatched_trig_declines() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
assert_declined(&ctx, "sin(x)/(2 + cos(x))^2");
}
#[test]
#[ignore = "P1/P2 atom construction still being debugged"]
fn p1_numeric_two_kernels_n2() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "1/(3 + 2*cos(x) + sin(x))^2");
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &[], &[0.3, 0.7, 1.1]);
}
#[test]
#[ignore = "P1/P2 atom construction still being debugged"]
fn p1_symbolic_coeff_n2() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "1/(a + 2*cos(x) + 3*sin(x))^2");
let env = [(Symbol::new("a"), 4.0)];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.3, 0.7, 1.1]);
}
#[test]
#[ignore = "P1/P2 atom construction still being debugged"]
fn p1_fully_symbolic_n1() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "1/(a + b*cos(x) + c*sin(x))");
let env = [
(Symbol::new("a"), 3.0),
(Symbol::new("b"), 1.5),
(Symbol::new("c"), 0.7),
];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.3, 0.7, 1.1]);
}
#[test]
#[ignore = "P1/P2 atom construction still being debugged"]
fn p1_two_kernels_n3() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "1/(5 + 2*cos(x) + 2*sin(x))^3");
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &[], &[0.3, 0.7, 1.1]);
}
#[test]
#[ignore = "P1/P2 atom construction still being debugged"]
fn p1_negative_and_swapped_kernels() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "1/(4 + 3*sin(1 + 2*x) - 4*cos(1 + 2*x))^2");
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &[], &[0.1, 0.4, 0.8]);
}
#[test]
#[ignore = "P1/P2 atom construction still being debugged"]
fn p1_bare_kernels_no_constant() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "1/(cos(x) + sin(x))^2");
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &[], &[0.3, 0.7, 1.1]);
}
#[test]
fn p1_singular_declines() {
assert_declined_ctx("1/(sqrt(2)*cos(x) + sqrt(2)*sin(x))^2");
assert_declined_ctx("1/(2*cos(x))^2");
}
#[test]
#[ignore = "P1/P2 atom construction still being debugged"]
fn p2_x2_sin_over_sin_cubed() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "x^2*sin(x)/(a + b*sin(x))^3");
let env = [(Symbol::new("a"), 2.0), (Symbol::new("b"), 0.7)];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.3, 0.7, 1.1]);
}
#[test]
#[ignore = "P1/P2 atom construction still being debugged"]
fn p2_x2_cos_over_cos_cubed() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "x^2*cos(x)/(a + b*cos(x))^3");
let env = [(Symbol::new("a"), 2.0), (Symbol::new("b"), 0.7)];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.3, 0.7, 1.1]);
}
#[test]
#[ignore = "P1/P2 atom construction still being debugged"]
fn p2_linear_poly_corpus_shape() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "(e + f*x)^2*cos(c + d*x)/(a + b*cos(c + d*x))^3");
let env = [
(Symbol::new("e"), 1.2),
(Symbol::new("f"), 0.5),
(Symbol::new("c"), 0.4),
(Symbol::new("d"), 1.3),
(Symbol::new("a"), 2.0),
(Symbol::new("b"), 0.7),
];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.2, 0.5, 0.9]);
}
#[test]
#[ignore = "P1/P2 atom construction still being debugged"]
fn p2_linear_over_quadratic() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "x*cos(x)/(3 + 2*cos(x))^2");
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &[], &[0.3, 0.7, 1.1]);
}
#[test]
fn p2_out_of_scope_declines() {
assert_declined_ctx("x^2*sin(x)/(2 + sin(x))^2");
assert_declined_ctx("x^5*sin(x)/(2 + sin(x))^6");
assert_declined_ctx("x*sin(x)/(2 + cos(x))^3");
assert_declined_ctx("x*cos(x)/(2 + cos(x))^(3/2)");
}
#[test]
fn declines_owned_by_sibling_agents() {
assert_declined_ctx("1/(a + b*tanh(x))");
assert_declined_ctx("cos(x)^(7/2)/(a + b*cos(x)^2)^(3/2)");
assert_declined_ctx("exp(x)*cos(x)");
assert_declined_ctx("1/(a + b*cosh(x) + c*sinh(x))^2");
}
fn assert_declined_ctx(input: &str) {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
assert_declined(&ctx, input);
}
#[test]
fn stress_repeat_is_stable() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let var = Symbol::new("x");
let env = [
(Symbol::new("a"), 2.0),
(Symbol::new("b"), 0.7),
(Symbol::new("A"), 1.5),
(Symbol::new("B"), -0.5),
];
let cases = [
"1/(3 + 2*cos(x) + sin(x))^2",
"1/(5 + 2*cos(x) + 2*sin(x))^3",
"1/(a + b*cos(x) + c*sin(x))",
"x^2*cos(x)/(a + b*sin(x))^3",
"(1 + 2*x)^2*sin(x)/(3 + cos(x))^3",
"1/(2 + cos(x))^2",
"1/(3 + 2*sin(x))^2",
"(A + B*cos(x))/(2 + cos(x))^3",
"x^2*cos(x)",
"sin(x)/(2 + cos(x))^2",
"1/(a + b*tanh(x))",
"cos(x)^(7/2)/(a + b*cos(x)^2)^(3/2)",
];
let exprs: Vec<Atom<'_>> = cases.iter().map(|s| parse_norm(&ctx, s)).collect();
let mut kinds: Vec<bool> = Vec::with_capacity(exprs.len());
for (i, e) in exprs.iter().enumerate() {
let r = integrate_trig_reduction(&ctx, *e, var);
let nodes = r.map(node_count).unwrap_or(0);
assert!(
nodes <= MAX_RESULT_NODES,
"case {i} produced {nodes} nodes (> {MAX_RESULT_NODES})"
);
kinds.push(r.is_some());
}
for round in 0..300 {
for (i, e) in exprs.iter().enumerate() {
let r = integrate_trig_reduction(&ctx, *e, var);
assert_eq!(
r.is_some(),
kinds[i],
"case {i} flipped solved/declined at round {round}"
);
if let Some(a) = r {
let nodes = node_count(a);
assert!(nodes <= MAX_RESULT_NODES, "case {i} grew to {nodes} nodes");
let d = crate::diff(&ctx, a, var);
for &xv in &[0.37, 0.91] {
let mut sample = env.to_vec();
sample.push((var, xv));
if let (Some(lhs), Some(rhs)) =
(eval_f64(d, &sample), eval_f64(*e, &sample))
{
assert!(
(lhs - rhs).abs() < 1e-5 * rhs.abs().max(1.0),
"case {i} round {round} wrong at x={xv}: {lhs} vs {rhs}"
);
}
}
}
}
}
}
#[test]
fn t3_x2_cos() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "x^2*cos(x)");
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &[], &[0.3, 0.7, 1.1]);
}
#[test]
fn t3_x4_sin_symbolic_slope() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "x^4*sin(a + b*x)");
let env = [(Symbol::new("a"), 0.4), (Symbol::new("b"), 1.3)];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.3, 0.7, 1.1]);
}
#[test]
fn t3_x6_cos_numeric() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "x^6*cos(2*x - 1)");
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &[], &[0.3, 0.7, 1.1]);
}
#[test]
fn t3_linpow4_cos() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "(c + d*x)^4*cos(a + b*x)");
let env = [
(Symbol::new("a"), 1.1),
(Symbol::new("b"), 0.6),
(Symbol::new("c"), 0.7),
(Symbol::new("d"), -0.4),
];
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &env, &[0.3, 0.7, 1.1]);
}
#[test]
fn t3_linpow3_sin_numeric() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "(1 + 2*x)^3*sin(3*x)");
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &[], &[0.3, 0.7, 1.1]);
}
#[test]
fn t3_out_of_scope_declines() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
assert_declined(&ctx, "x*sin(x)*cos(x)");
assert_declined(&ctx, "x*sin(x)^2");
assert_declined(&ctx, "x^7*sin(x)");
}
#[test]
fn t1_closed_form_sin_arm() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "1/(3 + 2*sin(x))^2");
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &[], &[0.3, 0.7, 1.1]);
}
#[test]
fn t1_closed_form_cos_arm() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "1/(3 + 2*cos(x))^2");
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &[], &[0.3, 0.7, 1.1]);
}
#[test]
fn t1_closed_form_declines_where_unverified() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let var = Symbol::new("x");
let e = parse_norm(&ctx, "1/(2 + 3*sin(x))^2");
let m = match_trig_denom(&ctx, e, var).unwrap();
assert!(weierstrass_base(&ctx, &m, var).is_none());
}
#[test]
fn t1_closed_form_negative_a_uses_the_sign_rule() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = parse_norm(&ctx, "1/(-5 + 3*cos(x))^2");
assert_antiderivative_num(&ctx, expr, Symbol::new("x"), &[], &[0.3, 0.7, 1.1]);
}
#[test]
fn t1_closed_form_symbolic_a() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let var = Symbol::new("x");
let env = [(Symbol::new("a"), 3.0), (Symbol::new("b"), 2.0)];
let es = parse_norm(&ctx, "1/(a + b*sin(x))^2");
assert_antiderivative_num(&ctx, es, var, &env, &[0.3, 0.7, 1.1]);
let ec = parse_norm(&ctx, "1/(a + b*cos(x))^2");
let m = match_trig_denom(&ctx, ec, var).unwrap();
assert!(weierstrass_base(&ctx, &m, var).is_none());
}
}