#![allow(dead_code)]
use ocas_atom::normalize::normalize;
use ocas_atom::{Atom, AtomArena, AtomNode, Symbol};
use super::rules::rat_of;
use super::{
affine_square_base, chain, contains_integral, contains_symbol, gcd_i64, int_pow, integrate_raw,
inv, is_constant, is_zero_expr, linear_form, node_count, rat_atom, replace_symbol,
};
const MAX_NODES: usize = 200;
const MAX_REENTRY_NODES: usize = 300;
const MAX_SR_WORK: u64 = 100_000;
thread_local! {
static SR_WORK: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
fn reset_sr_budget() {
SR_WORK.with(|c| c.set(0));
}
fn charge_sr_work(units: u64) -> bool {
SR_WORK.with(|c| {
let v = c.get().saturating_add(units);
c.set(v);
v > MAX_SR_WORK
})
}
fn rat_exponent(atom: Atom<'_>) -> Option<(i64, i64)> {
let (p, q) = rat_of(atom)?;
if q == 0 {
return None;
}
let (p, q) = if q < 0 {
(p.checked_neg()?, q.checked_neg()?)
} else {
(p, q)
};
let g = gcd_i64(p, q).max(1);
Some((p / g, q / g))
}
fn site_of(expr: Atom<'_>, var: Symbol) -> Option<(Atom<'_>, i64, i64)> {
match expr.node() {
AtomNode::Fun(name, args) if name.as_str() == "sqrt" && args.len() == 1 => {
let base = args[0];
if is_constant(base, var) {
None
} else {
Some((base, 1, 2))
}
}
AtomNode::Pow(b, e) => {
if is_constant(*b, var) {
return None;
}
let (p, q) = rat_exponent(*e)?;
if q >= 2 { Some((*b, p, q)) } else { None }
}
_ => None,
}
}
fn collect_site_bases<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
out: &mut Vec<Atom<'a>>,
) {
if let Some((b, _, _)) = site_of(expr, var) {
let nb = normalize(ctx, b);
if !out.contains(&nb) {
out.push(nb);
}
}
match expr.node() {
AtomNode::Num(_) | AtomNode::Var(_) => {}
AtomNode::Add(args) | AtomNode::Mul(args) => {
for a in args.iter() {
collect_site_bases(ctx, *a, var, out);
}
}
AtomNode::Pow(b, e) => {
collect_site_bases(ctx, *b, var, out);
collect_site_bases(ctx, *e, var, out);
}
AtomNode::Fun(_, args) => {
for a in args.iter() {
collect_site_bases(ctx, *a, var, out);
}
}
}
}
fn has_site(expr: Atom<'_>, var: Symbol) -> bool {
if site_of(expr, var).is_some() {
return true;
}
match expr.node() {
AtomNode::Num(_) | AtomNode::Var(_) => false,
AtomNode::Add(args) | AtomNode::Mul(args) => args.iter().any(|a| has_site(*a, var)),
AtomNode::Pow(b, e) => has_site(*b, var) || has_site(*e, var),
AtomNode::Fun(_, args) => args.iter().any(|a| has_site(*a, var)),
}
}
fn is_rational_in(expr: Atom<'_>, var: Symbol) -> bool {
match expr.node() {
AtomNode::Num(_) | AtomNode::Var(_) => true,
AtomNode::Add(args) | AtomNode::Mul(args) => args.iter().all(|a| is_rational_in(*a, var)),
AtomNode::Pow(b, e) => {
matches!(e.node(), AtomNode::Num(_))
&& is_rational_in(*b, var)
&& is_rational_in(*e, var)
}
AtomNode::Fun(_, args) => args.iter().all(|a| is_constant(*a, var)),
}
}
fn square_root_form<'a>(ctx: &'a AtomArena<'a>, base: Atom<'a>, var: Symbol) -> Option<Atom<'a>> {
let w = match base.node() {
AtomNode::Pow(b, e) if matches!(e.node(), AtomNode::Num(2)) => *b,
AtomNode::Add(_) => affine_square_base(ctx, base, var)?,
_ => return None,
};
let (slope, _) = linear_form(ctx, w, var)?;
if is_zero_expr(ctx, slope) {
return None;
}
Some(w)
}
fn slice_squares<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
targets: &[(Atom<'a>, Atom<'a>)],
sigmas: &mut Vec<Atom<'a>>,
) -> Option<Atom<'a>> {
if let Some((b, p, q)) = site_of(expr, var) {
let nb = normalize(ctx, b);
if q == 2
&& let Some((_, w)) = targets.iter().find(|(base, _)| *base == nb)
{
if p % 2 == 0 {
return Some(int_pow(ctx, *w, p));
}
sigmas.push(ctx.mul(&[*w, ctx.pow(nb, rat_atom(ctx, -1, 2))]));
return Some(int_pow(ctx, *w, p));
}
return Some(expr);
}
match expr.node() {
AtomNode::Num(_) | AtomNode::Var(_) => Some(expr),
AtomNode::Add(args) => {
let kids: Option<Vec<Atom<'a>>> = args
.iter()
.map(|a| slice_squares(ctx, *a, var, targets, sigmas))
.collect();
Some(ctx.add(&kids?))
}
AtomNode::Mul(args) => {
let kids: Option<Vec<Atom<'a>>> = args
.iter()
.map(|a| slice_squares(ctx, *a, var, targets, sigmas))
.collect();
Some(ctx.mul(&kids?))
}
AtomNode::Pow(b, e) => Some(ctx.pow(slice_squares(ctx, *b, var, targets, sigmas)?, *e)),
AtomNode::Fun(name, args) => {
let kids: Option<Vec<Atom<'a>>> = args
.iter()
.map(|a| slice_squares(ctx, *a, var, targets, sigmas))
.collect();
Some(ctx.fun(name.as_str(), &kids?))
}
}
}
fn try_square_slice<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol) -> Option<Atom<'a>> {
let mut bases = Vec::new();
collect_site_bases(ctx, expr, var, &mut bases);
let targets: Vec<(Atom<'a>, Atom<'a>)> = bases
.into_iter()
.filter_map(|b| square_root_form(ctx, b, var).map(|w| (b, w)))
.collect();
if targets.is_empty() {
return None;
}
if charge_sr_work(node_count(expr) as u64) {
return None;
}
let mut sigmas: Vec<Atom<'a>> = Vec::new();
let rest = slice_squares(ctx, expr, var, &targets, &mut sigmas)?;
if sigmas.is_empty() || has_site(rest, var) || !is_rational_in(rest, var) {
return None;
}
let rest = normalize(ctx, rest);
if node_count(rest) > MAX_REENTRY_NODES {
return None;
}
let anti = integrate_raw(ctx, rest, var, 0, true, 0, 0);
if contains_integral(anti) {
return None;
}
sigmas.push(anti);
Some(normalize(ctx, ctx.mul(&sigmas)))
}
fn strip_one_var<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol) -> Option<Atom<'a>> {
match expr.node() {
AtomNode::Var(v) if *v == var => Some(ctx.num(1)),
AtomNode::Pow(b, e) => match (b.node(), e.node()) {
(AtomNode::Var(v), AtomNode::Num(k)) if *v == var => {
if *k == 1 {
Some(ctx.num(1))
} else {
Some(ctx.pow(*b, ctx.num(*k - 1)))
}
}
(AtomNode::Mul(inner), AtomNode::Num(_)) => {
let kids: Vec<Atom<'a>> = inner.iter().map(|a| ctx.pow(*a, *e)).collect();
strip_one_var(ctx, ctx.mul(&kids), var)
}
_ => None,
},
AtomNode::Mul(_) => None,
AtomNode::Add(args) => {
let kids: Option<Vec<Atom<'a>>> =
args.iter().map(|a| strip_one_var(ctx, *a, var)).collect();
Some(ctx.add(&kids?))
}
_ => None,
}
}
fn to_even_form<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
v: Atom<'a>,
) -> Option<Atom<'a>> {
match expr.node() {
AtomNode::Num(_) | AtomNode::Var(_) => {
if matches!(expr.node(), AtomNode::Var(s) if *s == var) {
return None;
}
Some(expr)
}
AtomNode::Pow(b, e) => {
if let (AtomNode::Var(s), AtomNode::Num(k)) = (b.node(), e.node())
&& *s == var
{
if k % 2 != 0 {
return None;
}
return Some(if *k == 0 {
ctx.num(1)
} else {
int_pow(ctx, v, *k / 2)
});
}
if contains_symbol(*e, var) {
return None;
}
Some(ctx.pow(to_even_form(ctx, *b, var, v)?, *e))
}
AtomNode::Add(args) => {
let kids: Option<Vec<Atom<'a>>> =
args.iter().map(|a| to_even_form(ctx, *a, var, v)).collect();
Some(ctx.add(&kids?))
}
AtomNode::Mul(args) => {
let kids: Option<Vec<Atom<'a>>> =
args.iter().map(|a| to_even_form(ctx, *a, var, v)).collect();
Some(ctx.mul(&kids?))
}
AtomNode::Fun(name, args) => {
if name.as_str() == "sqrt" && args.len() == 1 {
return Some(ctx.fun("sqrt", &[to_even_form(ctx, args[0], var, v)?]));
}
if args.iter().all(|a| is_constant(*a, var)) {
Some(expr)
} else {
None
}
}
}
}
fn odd_split<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
v: Atom<'a>,
) -> Option<Atom<'a>> {
if let AtomNode::Mul(args) = expr.node() {
for i in 0..args.len() {
let Some(stripped) = strip_one_var(ctx, args[i], var) else {
continue;
};
let mut kids: Vec<Atom<'a>> = args.to_vec();
kids[i] = stripped;
let candidate = ctx.mul(&kids);
if let Some(even) = to_even_form(ctx, candidate, var, v) {
return Some(even);
}
}
return None;
}
let stripped = strip_one_var(ctx, expr, var)?;
to_even_form(ctx, stripped, var, v)
}
fn radicand_has_neg_power(expr: Atom<'_>, var: Symbol) -> bool {
if let Some((b, _, _)) = site_of(expr, var)
&& has_neg_power(b, var)
{
return true;
}
match expr.node() {
AtomNode::Num(_) | AtomNode::Var(_) => false,
AtomNode::Add(args) | AtomNode::Mul(args) => {
args.iter().any(|a| radicand_has_neg_power(*a, var))
}
AtomNode::Pow(b, e) => radicand_has_neg_power(*b, var) || radicand_has_neg_power(*e, var),
AtomNode::Fun(_, args) => args.iter().any(|a| radicand_has_neg_power(*a, var)),
}
}
fn has_neg_power(expr: Atom<'_>, var: Symbol) -> bool {
match expr.node() {
AtomNode::Pow(b, e) => {
if let (AtomNode::Var(s), AtomNode::Num(k)) = (b.node(), e.node())
&& *s == var
&& *k < 0
{
return true;
}
has_neg_power(*b, var) || has_neg_power(*e, var)
}
AtomNode::Add(args) | AtomNode::Mul(args) => args.iter().any(|a| has_neg_power(*a, var)),
AtomNode::Fun(_, args) => args.iter().any(|a| has_neg_power(*a, var)),
AtomNode::Num(_) | AtomNode::Var(_) => false,
}
}
fn fractional_bases_safe<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol) -> bool {
match expr.node() {
AtomNode::Pow(b, e) => {
let fractional = match rat_exponent(*e) {
Some((p, q)) => q >= 2 || p.abs() > 1_000,
None => true,
};
if fractional && !is_constant(*b, var) && !safe_power_base(ctx, *b, var) {
return false;
}
fractional_bases_safe(ctx, *b, var) && fractional_bases_safe(ctx, *e, var)
}
AtomNode::Fun(name, args) => {
if name.as_str() == "sqrt"
&& let Some(b) = args.first()
&& !is_constant(*b, var)
&& !safe_power_base(ctx, *b, var)
{
return false;
}
args.iter().all(|a| fractional_bases_safe(ctx, *a, var))
}
AtomNode::Add(args) | AtomNode::Mul(args) => {
args.iter().all(|a| fractional_bases_safe(ctx, *a, var))
}
AtomNode::Num(_) | AtomNode::Var(_) => true,
}
}
fn safe_power_base<'a>(ctx: &'a AtomArena<'a>, base: Atom<'a>, var: Symbol) -> bool {
let bare = match base.node() {
AtomNode::Var(s) => *s == var,
AtomNode::Mul(args) => {
let mut varcount = 0;
let mut others_ok = true;
for a in args.iter() {
match a.node() {
AtomNode::Var(s) if *s == var => varcount += 1,
AtomNode::Pow(b, e) => {
if let (AtomNode::Var(s), AtomNode::Num(_)) = (b.node(), e.node())
&& *s == var
{
varcount += 1;
} else if !is_constant(*a, var) {
others_ok = false;
}
}
_ if is_constant(*a, var) => {}
_ => others_ok = false,
}
}
varcount == 1 && others_ok
}
_ => false,
};
if bare {
return false;
}
if let Some((_, intercept)) = linear_form(ctx, base, var)
&& is_zero_expr(ctx, intercept)
{
return false;
}
true
}
fn split_int_powers<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>) -> Atom<'a> {
match expr.node() {
AtomNode::Pow(b, e) => {
if let (AtomNode::Mul(inner), AtomNode::Num(_)) = (b.node(), e.node()) {
let kids: Vec<Atom<'a>> = inner
.iter()
.map(|a| split_int_powers(ctx, ctx.pow(*a, *e)))
.collect();
return ctx.mul(&kids);
}
ctx.pow(split_int_powers(ctx, *b), split_int_powers(ctx, *e))
}
AtomNode::Add(args) => {
let kids: Vec<Atom<'a>> = args.iter().map(|a| split_int_powers(ctx, *a)).collect();
ctx.add(&kids)
}
AtomNode::Mul(args) => {
let kids: Vec<Atom<'a>> = args.iter().map(|a| split_int_powers(ctx, *a)).collect();
ctx.mul(&kids)
}
AtomNode::Fun(name, args) => {
let kids: Vec<Atom<'a>> = args.iter().map(|a| split_int_powers(ctx, *a)).collect();
ctx.fun(name.as_str(), &kids)
}
AtomNode::Num(_) | AtomNode::Var(_) => expr,
}
}
fn try_even_substitution<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
) -> Option<Atom<'a>> {
let v_sym = aux_symbol(expr, var, &["v", "w"])?;
let v = ctx.var(v_sym.as_str());
if charge_sr_work(node_count(expr) as u64) {
return None;
}
let expr = normalize(ctx, fold_pow_pow(ctx, split_int_powers(ctx, expr)));
let even = odd_split(ctx, expr, var, v)?;
let v_form = normalize(ctx, ctx.mul(&[rat_atom(ctx, 1, 2), even]));
if !has_site(v_form, v_sym)
|| radicand_has_neg_power(v_form, v_sym)
|| provably_nonpositive_radicand(ctx, v_form, v_sym)
{
return None;
}
if node_count(v_form) > MAX_REENTRY_NODES {
return None;
}
let anti = integrate_raw(ctx, v_form, v_sym, 0, true, 0, 0);
if contains_integral(anti) || !fractional_bases_safe(ctx, anti, v_sym) {
return None;
}
let back = replace_symbol(ctx, anti, v_sym, ctx.pow(ctx.var(var.as_str()), ctx.num(2)));
let back = normalize(ctx, back);
if node_count(back) > MAX_REENTRY_NODES {
return None;
}
Some(back)
}
fn has_inverse_radicand(expr: Atom<'_>, var: Symbol) -> bool {
if let Some((b, _, _)) = site_of(expr, var)
&& has_neg_power(b, var)
{
return true;
}
match expr.node() {
AtomNode::Num(_) | AtomNode::Var(_) => false,
AtomNode::Add(args) | AtomNode::Mul(args) => {
args.iter().any(|a| has_inverse_radicand(*a, var))
}
AtomNode::Pow(b, e) => has_inverse_radicand(*b, var) || has_inverse_radicand(*e, var),
AtomNode::Fun(_, args) => args.iter().any(|a| has_inverse_radicand(*a, var)),
}
}
fn fold_pow_pow<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>) -> Atom<'a> {
match expr.node() {
AtomNode::Pow(b, e) => {
let fb = fold_pow_pow(ctx, *b);
let fe = fold_pow_pow(ctx, *e);
if let AtomNode::Pow(inner, ie) = fb.node()
&& let (Some((p1, q1)), Some((p2, q2))) = (rat_exponent(*ie), rat_exponent(fe))
&& let (Some(p), Some(q)) = (p1.checked_mul(p2), q1.checked_mul(q2))
{
return ctx.pow(*inner, rat_atom(ctx, p, q));
}
ctx.pow(fb, fe)
}
AtomNode::Add(args) => {
let kids: Vec<Atom<'a>> = args.iter().map(|a| fold_pow_pow(ctx, *a)).collect();
ctx.add(&kids)
}
AtomNode::Mul(args) => {
let kids: Vec<Atom<'a>> = args.iter().map(|a| fold_pow_pow(ctx, *a)).collect();
ctx.mul(&kids)
}
AtomNode::Fun(name, args) => {
let kids: Vec<Atom<'a>> = args.iter().map(|a| fold_pow_pow(ctx, *a)).collect();
ctx.fun(name.as_str(), &kids)
}
AtomNode::Num(_) | AtomNode::Var(_) => expr,
}
}
fn try_reciprocal<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol) -> Option<Atom<'a>> {
if !has_inverse_radicand(expr, var) {
return None;
}
let t_sym = aux_symbol(expr, var, &["t", "u"])?;
let t = ctx.var(t_sym.as_str());
if charge_sr_work(node_count(expr) as u64) {
return None;
}
let subbed = fold_pow_pow(ctx, replace_symbol(ctx, expr, var, inv(ctx, t)));
let t_form = normalize(
ctx,
fold_pow_pow(
ctx,
split_int_powers(
ctx,
ctx.mul(&[subbed, ctx.num(-1), ctx.pow(t, ctx.num(-2))]),
),
),
);
if radicand_has_neg_power(t_form, t_sym)
|| !has_site(t_form, t_sym)
|| provably_nonpositive_radicand(ctx, t_form, t_sym)
{
return None;
}
if node_count(t_form) > MAX_REENTRY_NODES {
return None;
}
let anti = integrate_raw(ctx, t_form, t_sym, 0, true, 0, 0);
if contains_integral(anti) {
return None;
}
let back = fold_pow_pow(
ctx,
replace_symbol(ctx, anti, t_sym, inv(ctx, ctx.var(var.as_str()))),
);
let back = normalize(ctx, back);
if !fractional_bases_safe(ctx, back, var) || node_count(back) > MAX_REENTRY_NODES {
return None;
}
Some(back)
}
fn provably_nonpositive_radicand<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol) -> bool {
if let Some((b, _, _)) = site_of(expr, var)
&& let Some((slope, intercept)) = linear_form(ctx, b, var)
&& let (Some(p), Some(q)) = (
rat_of(normalize(ctx, intercept)),
rat_of(normalize(ctx, slope)),
)
&& p.1 > 0
&& q.1 > 0
&& p.0 <= 0
&& q.0 <= 0
&& (p.0 < 0 || q.0 < 0)
{
return true;
}
match expr.node() {
AtomNode::Num(_) | AtomNode::Var(_) => false,
AtomNode::Add(args) | AtomNode::Mul(args) => args
.iter()
.any(|a| provably_nonpositive_radicand(ctx, *a, var)),
AtomNode::Pow(b, e) => {
provably_nonpositive_radicand(ctx, *b, var)
|| provably_nonpositive_radicand(ctx, *e, var)
}
AtomNode::Fun(_, args) => args
.iter()
.any(|a| provably_nonpositive_radicand(ctx, *a, var)),
}
}
fn aux_symbol(expr: Atom<'_>, var: Symbol, candidates: &[&str]) -> Option<Symbol> {
for name in candidates {
let s = Symbol::new(name);
if s != var && !contains_symbol(expr, s) {
return Some(s);
}
}
None
}
pub(crate) fn integrate_single_radical<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
) -> Option<Atom<'a>> {
if chain::parts_active() {
return None;
}
if is_constant(expr, var) || node_count(expr) > MAX_NODES {
return None;
}
if !has_site(expr, var) {
return None;
}
reset_sr_budget();
if charge_sr_work(node_count(expr) as u64) {
return None;
}
if let Some(r) = try_square_slice(ctx, expr, var) {
return Some(r);
}
if let Some(r) = try_even_substitution(ctx, expr, var) {
return Some(r);
}
try_reciprocal(ctx, expr, var)
}
#[cfg(test)]
mod tests {
use super::*;
use ocas_core::arena::Arena;
use std::time::Instant;
fn parse<'a>(ctx: &'a AtomArena<'a>, s: &str) -> Atom<'a> {
ocas_parse::parse(ctx, s).unwrap_or_else(|e| panic!("parse {s}: {e:?}"))
}
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() {
"log" => v.abs().ln(),
"sqrt" => v.sqrt(),
"atan" => v.atan(),
"asin" => v.asin(),
"atanh" => v.atanh(),
"asinh" => 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_single_radical(ctx, integrand, var)
.unwrap_or_else(|| panic!("mechanism declined: {integrand}"));
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)
.unwrap_or_else(|| panic!("eval diff failed\n result: {result}\n diff: {d}"));
let rhs = eval_f64(integrand, &env)
.unwrap_or_else(|| panic!("eval integrand failed: {integrand}"));
let tol = 1e-5 * rhs.abs().max(1.0);
assert!(
(lhs - rhs).abs() < tol,
"integrand: {integrand}\nat x={xv}: diff={lhs} integrand={rhs}\nresult: {result}"
);
}
}
fn assert_declined<'a>(ctx: &'a AtomArena<'a>, integrand: Atom<'a>, var: Symbol) {
let start = Instant::now();
let out = integrate_single_radical(ctx, integrand, var);
let elapsed = start.elapsed();
assert!(out.is_none(), "expected a decline, got {:?}", out);
assert!(
elapsed.as_secs() < 60,
"decline took {elapsed:?} (hang): {integrand}"
);
}
fn assert_certified<'a>(ctx: &'a AtomArena<'a>, integrand: Atom<'a>, var: Symbol) {
let result = integrate_single_radical(ctx, integrand, var)
.unwrap_or_else(|| panic!("mechanism declined: {integrand}"));
let normalized = normalize(ctx, integrand);
if let Err(e) = super::super::certify::certify(ctx, normalized, result, var) {
panic!("certify declined ({e:?}) for {integrand}\n result: {result}");
}
}
#[test]
fn expanded_square_radicand_slices() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let var = Symbol::new("x");
for src in [
"(d + e*x)^4*sqrt(a^2 + 2*a*b*x + b^2*x^2)",
"x^3*(A + B*x)*(a^2 + 2*a*b*x + b^2*x^2)^(3/2)",
"(A + B*x)*(a^2 + 2*a*b*x + b^2*x^2)^(5/2)/x",
"(a + b*x)/((d + e*x)^5*sqrt(a^2 + 2*a*b*x + b^2*x^2))",
] {
let integrand = parse(&ctx, src);
let result = integrate_single_radical(&ctx, integrand, var)
.unwrap_or_else(|| panic!("declined: {src}"));
assert!(
!result.to_string().contains("Integral"),
"residue: {src} -> {result}"
);
assert_certified(&ctx, integrand, var);
}
}
#[test]
fn expanded_square_radicand_is_numerically_correct() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let var = Symbol::new("x");
let a = Symbol::new("a");
let b = Symbol::new("b");
let d = Symbol::new("d");
let e = Symbol::new("e");
for (src, extra) in [
("(d + e*x)^4*sqrt(a^2 + 2*a*b*x + b^2*x^2)", Vec::new()),
(
"x^3*(A + B*x)*(a^2 + 2*a*b*x + b^2*x^2)^(3/2)",
vec![(Symbol::new("A"), 1.0), (Symbol::new("B"), 1.0)],
),
] {
let integrand = parse(&ctx, src);
let mut env: Vec<(Symbol, f64)> = vec![(a, 2.0), (b, 3.0), (d, 1.0), (e, 1.0)];
env.extend(extra);
let result = integrate_single_radical(&ctx, integrand, var)
.unwrap_or_else(|| panic!("declined: {src}"));
let dv = crate::diff(&ctx, result, var);
for &xv in &[-1.5_f64, -0.8, 0.2, 0.5, 1.1] {
let mut env = env.clone();
env.push((var, xv));
let lhs = eval_f64(dv, &env).expect("eval diff");
let rhs = eval_f64(integrand, &env).expect("eval integrand");
let tol = 1e-5 * rhs.abs().max(1.0);
assert!(
(lhs - rhs).abs() < tol,
"{src} at x={xv}: diff={lhs} rhs={rhs}\nresult: {result}"
);
}
}
}
#[test]
fn even_substitution_handles_odd_quadratic_radicals() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let var = Symbol::new("x");
assert_antiderivative_num(
&ctx,
parse(&ctx, "x/((5 - x^2)*sqrt(3 - x^2))"),
var,
&[],
&[0.1, 0.4, 0.9],
);
assert_antiderivative_num(
&ctx,
parse(&ctx, "x*sqrt(2 - 3*x^2)/(1 + x^2)"),
var,
&[],
&[0.1, 0.4, 0.7],
);
}
#[test]
fn certificate_limitations_are_pre_existing() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let var = Symbol::new("x");
let a = ctx.var("a");
let b = ctx.var("b");
let base = ctx.add(&[a, ctx.mul(&[b, ctx.pow(ctx.var("x"), ctx.num(-1))])]);
let ig = ctx.fun("sqrt", &[base]);
let ans = ctx.mul(&[
rat_atom(&ctx, 2, 3),
inv(&ctx, b),
ctx.pow(base, rat_atom(&ctx, 3, 2)),
]);
assert!(
super::super::certify::certify(&ctx, ig, ans, var).is_err(),
"the certificate layer unexpectedly models x^(−1) radicands"
);
}
#[test]
fn even_substitution_handles_fractional_powers() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let var = Symbol::new("x");
assert_antiderivative_num(
&ctx,
parse(&ctx, "x^3*(1 + x^2)^(9/14)"),
var,
&[],
&[0.4, 0.9, 1.6],
);
assert_antiderivative_num(
&ctx,
parse(&ctx, "x*(2 + 3*x^2)^(1/3)"),
var,
&[],
&[0.4, 0.9, 1.6],
);
assert_certified(&ctx, parse(&ctx, "x*(2 + 3*x^2)^(1/3)"), var);
}
#[test]
fn inverse_radicand_substitution() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let var = Symbol::new("x");
let a = Symbol::new("a");
let b = Symbol::new("b");
let consts = [(a, 2.0), (b, 3.0)];
for src in ["1/(x^5*sqrt(a + b/x))", "sqrt(a + b/x)/x^3"] {
assert_antiderivative_num(&ctx, parse(&ctx, src), var, &consts, &[0.4, 0.9, 1.7]);
}
}
#[test]
fn declines_out_of_fragment_shapes() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let var = Symbol::new("x");
assert_declined(&ctx, parse(&ctx, "(b*x + c*x^2)^(2/3)"), var);
assert_declined(
&ctx,
parse(&ctx, "(5 - x)*(2 + 3*x^2)^(3/2)/(3 + 2*x)^5"),
var,
);
assert_declined(&ctx, parse(&ctx, "sqrt(a + c*x^2)/(d + e*x)"), var);
assert_declined(&ctx, parse(&ctx, "(a + b*x^2)^(4/3)/x"), var);
assert_declined(&ctx, parse(&ctx, "x^3*sqrt(a + b/x)"), var);
assert_declined(&ctx, parse(&ctx, "(a + b/x^2)/(x^4*sqrt(c + d/x^2))"), var);
assert_declined(&ctx, parse(&ctx, "sqrt(-1 + 1/x^2)/(x*(-1 + x^2)^2)"), var);
assert_declined(&ctx, parse(&ctx, "1/(x^3*sqrt(-9 - 4*x^2))"), var);
assert_declined(&ctx, parse(&ctx, "1/(1 + x^2)"), var);
}
#[test]
fn declines_inside_parts_scope() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let var = Symbol::new("x");
let integrand = parse(&ctx, "x/((5 - x^2)*sqrt(3 - x^2))");
let out = super::super::chain::with_parts_scope(|| {
integrate_single_radical(&ctx, integrand, var)
});
assert!(out.is_none(), "must decline inside a parts scope");
}
#[test]
fn stage_output_does_not_re_enter() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let var = Symbol::new("x");
let integrand = parse(&ctx, "x/((5 - x^2)*sqrt(3 - x^2))");
let first = integrate_single_radical(&ctx, integrand, var).expect("first pass");
let second = integrate_single_radical(&ctx, first, var);
assert!(
second.is_none(),
"stage re-entered on its own output: {second:?}"
);
}
}