use crate::deriv::log::{DerivationLog, DerivedExpr, RewriteStep};
use crate::kernel::{ExprData, ExprId, ExprPool};
use crate::simplify::engine::{simplify, simplify_expanded};
use std::collections::HashMap;
use super::engine::{integrate, verify_antiderivative_status, IntegrationError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ByPartsOutcome {
Solved(ExprId),
Declined(String),
}
impl ByPartsOutcome {
pub fn solved(&self) -> Option<ExprId> {
match self {
ByPartsOutcome::Solved(f) => Some(*f),
ByPartsOutcome::Declined(_) => None,
}
}
pub fn is_declined(&self) -> bool {
matches!(self, ByPartsOutcome::Declined(_))
}
pub fn into_result(self) -> Result<ExprId, IntegrationError> {
match self {
ByPartsOutcome::Solved(f) => Ok(f),
ByPartsOutcome::Declined(why) => Err(IntegrationError::NotImplemented(why)),
}
}
}
pub const MAX_IBP_STEPS: u32 = 3;
const MAX_REENTRY_DEPTH: u32 = 1;
const GROWTH_LIMIT: f64 = 1.6;
thread_local! {
static IBP_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
}
struct DepthGuard;
impl Drop for DepthGuard {
fn drop(&mut self) {
IBP_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Liate {
Log,
InverseTrig,
Algebraic,
Trig,
Exponential,
Other,
}
fn is_inverse_trig_name(name: &str) -> bool {
matches!(
name,
"atan" | "asin" | "acos" | "asinh" | "acosh" | "atanh" | "acot" | "asec" | "acsc"
)
}
fn is_trig_name(name: &str) -> bool {
matches!(
name,
"sin" | "cos" | "tan" | "sec" | "csc" | "cot" | "sinh" | "cosh" | "tanh" | "coth"
)
}
pub fn liate_class(expr: ExprId, var: ExprId, pool: &ExprPool) -> Liate {
if is_free_of(expr, var, pool) {
return Liate::Algebraic;
}
match pool.get(expr) {
ExprData::Symbol { .. }
| ExprData::Integer(_)
| ExprData::Rational(_)
| ExprData::Float(_) => Liate::Algebraic,
ExprData::Func { name, args } => {
if name == "log" {
Liate::Log
} else if is_inverse_trig_name(&name) {
Liate::InverseTrig
} else if name == "exp" {
Liate::Exponential
} else if is_trig_name(&name) {
Liate::Trig
} else if name == "sqrt" {
match args.first() {
Some(&a) if !is_free_of(a, var, pool) => {
let inner = liate_class(a, var, pool);
if inner == Liate::Algebraic {
Liate::Algebraic
} else {
inner
}
}
_ => Liate::Algebraic,
}
} else {
Liate::Other
}
}
ExprData::Pow { base, exp } => {
if is_free_of(base, var, pool) && !is_free_of(exp, var, pool) {
Liate::Exponential
} else {
liate_class(base, var, pool)
}
}
ExprData::Mul(args) | ExprData::Add(args) => args
.iter()
.filter(|&&a| !is_free_of(a, var, pool))
.map(|&a| liate_class(a, var, pool))
.min()
.unwrap_or(Liate::Algebraic),
_ => Liate::Other,
}
}
fn is_free_of(expr: ExprId, var: ExprId, pool: &ExprPool) -> bool {
if expr == var {
return false;
}
match pool.get(expr) {
ExprData::Symbol { .. } => true,
ExprData::Integer(_) | ExprData::Rational(_) | ExprData::Float(_) => true,
ExprData::Add(args) | ExprData::Mul(args) | ExprData::Func { args, .. } => {
args.iter().all(|&a| is_free_of(a, var, pool))
}
ExprData::Pow { base, exp } => is_free_of(base, var, pool) && is_free_of(exp, var, pool),
_ => false,
}
}
fn node_count(expr: ExprId, pool: &ExprPool) -> usize {
1 + pool.with(expr, |data| match data {
ExprData::Add(args) | ExprData::Mul(args) | ExprData::Func { args, .. } => {
args.iter().map(|&a| node_count(a, pool)).sum::<usize>()
}
ExprData::Pow { base, exp } => node_count(*base, pool) + node_count(*exp, pool),
_ => 0,
})
}
fn structural_size(expr: ExprId, pool: &ExprPool) -> usize {
node_count(split_rational_coeff(expr, pool).1, pool)
}
fn is_zero(expr: ExprId, pool: &ExprPool) -> bool {
pool.with(expr, |d| match d {
ExprData::Integer(i) => i.0 == 0,
ExprData::Rational(r) => r.0.numer() == &rug::Integer::ZERO,
ExprData::Float(f) => f.inner.is_zero(),
_ => false,
})
}
fn is_one(expr: ExprId, pool: &ExprPool) -> bool {
pool.with(expr, |d| matches!(d, ExprData::Integer(i) if i.0 == 1))
}
fn mul_of(factors: &[ExprId], pool: &ExprPool) -> ExprId {
match factors.len() {
0 => pool.integer(1_i32),
1 => factors[0],
_ => pool.mul(factors.to_vec()),
}
}
#[derive(Debug, Clone, Copy)]
struct Split {
u: ExprId,
dv: ExprId,
class: Liate,
trivial_dv: bool,
}
fn candidate_splits(expr: ExprId, var: ExprId, pool: &ExprPool) -> Vec<Split> {
let mut splits: Vec<Split> = Vec::new();
let one = pool.integer(1_i32);
if let ExprData::Mul(args) = pool.get(expr) {
let var_dep: Vec<ExprId> = args
.iter()
.copied()
.filter(|&a| !is_free_of(a, var, pool))
.collect();
let consts: Vec<ExprId> = args
.iter()
.copied()
.filter(|&a| is_free_of(a, var, pool))
.collect();
if var_dep.len() >= 2 {
for (i, &u) in var_dep.iter().enumerate() {
let mut rest: Vec<ExprId> = consts.clone();
rest.extend(
var_dep
.iter()
.enumerate()
.filter(|&(j, _)| j != i)
.map(|(_, &a)| a),
);
let dv = mul_of(&rest, pool);
splits.push(Split {
u,
dv,
class: liate_class(u, var, pool),
trivial_dv: false,
});
}
}
}
if splits.is_empty() {
splits.push(Split {
u: expr,
dv: one,
class: liate_class(expr, var, pool),
trivial_dv: true,
});
}
splits.sort_by_key(|s| (s.class, s.trivial_dv));
splits
}
const RATIO_SAMPLES: [f64; 10] = [
0.3719, 0.9137, 1.4231, 2.1719, 2.8123, 3.6411, 4.4507, 5.2903, 6.1013, 7.3331,
];
const MIN_NUMERIC_RATIO_POINTS: usize = 5;
fn constant_ratio(w: ExprId, target: ExprId, var: ExprId, pool: &ExprPool) -> Option<(i64, i64)> {
let mut guess: Option<f64> = None;
let mut agreeing = 0_usize;
for &xv in &RATIO_SAMPLES {
let mut env = HashMap::new();
env.insert(var, xv);
let (Some(wv), Some(tv)) = (
crate::jit::eval_interp(w, &env, pool),
crate::jit::eval_interp(target, &env, pool),
) else {
return None;
};
if !wv.is_finite() || !tv.is_finite() || tv.abs() < 1e-9 {
continue;
}
let r = wv / tv;
match guess {
None => {
guess = Some(r);
agreeing = 1;
}
Some(g) => {
if (g - r).abs() > 1e-9 * (1.0 + g.abs()) {
return None;
}
agreeing += 1;
}
}
}
if agreeing < 2 {
return None;
}
let c = guess?;
let (num, den) = snap_rational(c)?;
let coeff = pool.rational(num, den);
let neg = pool.mul(vec![pool.integer(-1_i32), coeff, target]);
let sum = pool.add(vec![w, neg]);
let expanded_sum = simplify_expanded(sum, pool).value;
if is_zero(simplify(sum, pool).value, pool)
|| is_zero(expanded_sum, pool)
|| is_zero(collect_like_terms_deep(expanded_sum, pool), pool)
{
return Some((num, den));
}
(agreeing >= MIN_NUMERIC_RATIO_POINTS).then_some((num, den))
}
fn snap_rational(c: f64) -> Option<(i64, i64)> {
if !c.is_finite() {
return None;
}
for den in 1_i64..=12 {
let num_f = c * den as f64;
let num = num_f.round();
if (num_f - num).abs() < 1e-8 && num.abs() <= 144.0 {
let num = num as i64;
if num == 0 {
return None; }
let g = gcd_i64(num.abs(), den);
return Some((num / g, den / g));
}
}
None
}
fn gcd_i64(mut a: i64, mut b: i64) -> i64 {
while b != 0 {
let t = a % b;
a = b;
b = t;
}
a.max(1)
}
fn split_rational_coeff(term: ExprId, pool: &ExprPool) -> (rug::Rational, ExprId) {
let one = rug::Rational::from(1);
match pool.get(term) {
ExprData::Integer(i) => (rug::Rational::from(i.0.clone()), pool.integer(1_i32)),
ExprData::Rational(r) => (r.0.clone(), pool.integer(1_i32)),
ExprData::Mul(args) => {
let mut coeff = one;
let mut rest: Vec<ExprId> = Vec::new();
for a in args {
match pool.get(a) {
ExprData::Integer(i) => coeff *= rug::Rational::from(i.0.clone()),
ExprData::Rational(r) => coeff *= r.0.clone(),
_ => rest.push(a),
}
}
(coeff, mul_of(&rest, pool))
}
_ => (one, term),
}
}
fn collect_like_terms(expr: ExprId, pool: &ExprPool) -> ExprId {
let ExprData::Add(args) = pool.get(expr) else {
return expr;
};
let mut keys: Vec<ExprId> = Vec::new();
let mut coeffs: Vec<rug::Rational> = Vec::new();
for t in args {
let (c, k) = split_rational_coeff(t, pool);
match keys.iter().position(|&x| x == k) {
Some(i) => coeffs[i] += c,
None => {
keys.push(k);
coeffs.push(c);
}
}
}
let zero = rug::Rational::from(0);
let one = rug::Rational::from(1);
let mut terms: Vec<ExprId> = Vec::new();
for (k, c) in keys.into_iter().zip(coeffs) {
if c == zero {
continue;
}
let is_one_key = is_one(k, pool);
let coeff_expr = pool.rational(c.numer().clone(), c.denom().clone());
if is_one_key {
terms.push(coeff_expr);
} else if c == one {
terms.push(k);
} else {
terms.push(pool.mul(vec![coeff_expr, k]));
}
}
match terms.len() {
0 => pool.integer(0_i32),
1 => terms[0],
_ => pool.add(terms),
}
}
fn collect_like_terms_deep(expr: ExprId, pool: &ExprPool) -> ExprId {
let rebuilt = match pool.get(expr) {
ExprData::Add(args) => pool.add(
args.iter()
.map(|&a| collect_like_terms_deep(a, pool))
.collect(),
),
ExprData::Mul(args) => pool.mul(
args.iter()
.map(|&a| collect_like_terms_deep(a, pool))
.collect(),
),
ExprData::Pow { base, exp } => pool.pow(
collect_like_terms_deep(base, pool),
collect_like_terms_deep(exp, pool),
),
ExprData::Func { name, args } => pool.func(
name,
args.iter()
.map(|&a| collect_like_terms_deep(a, pool))
.collect(),
),
_ => return expr,
};
collect_like_terms(rebuilt, pool)
}
fn as_i64(e: ExprId, pool: &ExprPool) -> Option<i64> {
pool.with(e, |d| match d {
ExprData::Integer(i) => i.0.to_i64(),
_ => None,
})
}
fn reduce_sqrt_squares(expr: ExprId, pool: &ExprPool) -> ExprId {
match pool.get(expr) {
ExprData::Pow { base, exp } => {
let base_r = reduce_sqrt_squares(base, pool);
let k = as_i64(exp, pool);
if let (Some(k), ExprData::Func { name, args }) = (k, pool.get(base_r)) {
if name == "sqrt" && args.len() == 1 && k.abs() >= 2 {
let r = args[0];
let half = k / 2;
let rem = k % 2;
let mut factors = vec![pool.pow(r, pool.integer(half))];
if rem != 0 {
factors.push(pool.pow(base_r, pool.integer(rem)));
}
return pool.mul(factors);
}
}
if let Some(k) = k {
if let ExprData::Mul(factors) = pool.get(base_r) {
let distributed = pool.mul(
factors
.iter()
.map(|&f| pool.pow(f, pool.integer(k)))
.collect(),
);
return reduce_sqrt_squares(distributed, pool);
}
if let ExprData::Pow {
base: inner,
exp: inner_exp,
} = pool.get(base_r)
{
if let Some(mk) = as_i64(inner_exp, pool).and_then(|m| m.checked_mul(k)) {
return reduce_sqrt_squares(pool.pow(inner, pool.integer(mk)), pool);
}
}
}
pool.pow(base_r, reduce_sqrt_squares(exp, pool))
}
ExprData::Add(args) => {
pool.add(args.iter().map(|&a| reduce_sqrt_squares(a, pool)).collect())
}
ExprData::Mul(args) => {
pool.mul(args.iter().map(|&a| reduce_sqrt_squares(a, pool)).collect())
}
ExprData::Func { name, args } => pool.func(
name,
args.iter().map(|&a| reduce_sqrt_squares(a, pool)).collect(),
),
_ => expr,
}
}
fn contains_sub(hay: ExprId, needle: ExprId, pool: &ExprPool) -> bool {
if hay == needle {
return true;
}
match pool.get(hay) {
ExprData::Add(args) | ExprData::Mul(args) | ExprData::Func { args, .. } => {
args.iter().any(|&a| contains_sub(a, needle, pool))
}
ExprData::Pow { base, exp } => {
contains_sub(base, needle, pool) || contains_sub(exp, needle, pool)
}
_ => false,
}
}
fn split_linear_in(expr: ExprId, s: ExprId, pool: &ExprPool) -> Option<(ExprId, ExprId)> {
let terms: Vec<ExprId> = match pool.get(expr) {
ExprData::Add(args) => args,
_ => vec![expr],
};
let mut a_terms: Vec<ExprId> = Vec::new();
let mut b_terms: Vec<ExprId> = Vec::new();
for t in terms {
if !contains_sub(t, s, pool) {
a_terms.push(t);
continue;
}
let factors: Vec<ExprId> = match pool.get(t) {
ExprData::Mul(args) => args,
_ => vec![t],
};
let hits = factors.iter().filter(|&&f| f == s).count();
if hits != 1 {
return None; }
let rest: Vec<ExprId> = factors.into_iter().filter(|&f| f != s).collect();
if rest.iter().any(|&f| contains_sub(f, s, pool)) {
return None;
}
b_terms.push(mul_of(&rest, pool));
}
let a = match a_terms.len() {
0 => pool.integer(0_i32),
1 => a_terms[0],
_ => pool.add(a_terms),
};
let b = match b_terms.len() {
0 => return None, 1 => b_terms[0],
_ => pool.add(b_terms),
};
Some((a, b))
}
fn rationalize_radicals(expr: ExprId, pool: &ExprPool) -> ExprId {
match pool.get(expr) {
ExprData::Pow { base, exp } => {
let base_r = rationalize_radicals(base, pool);
let n = pool.with(exp, |d| match d {
ExprData::Integer(i) => i.0.to_i64(),
_ => None,
});
if let Some(n) = n {
if n < 0 {
if let Some(rewritten) = rationalize_reciprocal(base_r, pool) {
return pool.pow(rewritten, pool.integer(-n));
}
}
}
pool.pow(base_r, rationalize_radicals(exp, pool))
}
ExprData::Add(args) => pool.add(
args.iter()
.map(|&a| rationalize_radicals(a, pool))
.collect(),
),
ExprData::Mul(args) => pool.mul(
args.iter()
.map(|&a| rationalize_radicals(a, pool))
.collect(),
),
ExprData::Func { name, args } => pool.func(
name,
args.iter()
.map(|&a| rationalize_radicals(a, pool))
.collect(),
),
_ => expr,
}
}
fn rationalize_reciprocal(base: ExprId, pool: &ExprPool) -> Option<ExprId> {
fn collect_sqrts(e: ExprId, pool: &ExprPool, out: &mut Vec<ExprId>) {
match pool.get(e) {
ExprData::Func { ref name, ref args } if name == "sqrt" && args.len() == 1 => {
if !out.contains(&e) {
out.push(e);
}
}
ExprData::Add(args) | ExprData::Mul(args) | ExprData::Func { args, .. } => {
for a in args {
collect_sqrts(a, pool, out);
}
}
ExprData::Pow { base, exp } => {
collect_sqrts(base, pool, out);
collect_sqrts(exp, pool, out);
}
_ => {}
}
}
let mut sqrts = Vec::new();
collect_sqrts(base, pool, &mut sqrts);
if sqrts.len() != 1 {
return None;
}
let s = sqrts[0];
let ExprData::Func { args, .. } = pool.get(s) else {
return None;
};
let r = *args.first()?;
let (a, b) = split_linear_in(base, s, pool)?;
let numer = pool.add(vec![a, pool.mul(vec![pool.integer(-1_i32), b, s])]);
let denom = pool.add(vec![
pool.pow(a, pool.integer(2_i32)),
pool.mul(vec![
pool.integer(-1_i32),
pool.pow(b, pool.integer(2_i32)),
r,
]),
]);
let denom = simplify(reduce_sqrt_squares(denom, pool), pool).value;
if is_zero(denom, pool) || contains_sub(denom, s, pool) {
return None;
}
Some(pool.mul(vec![numer, pool.pow(denom, pool.integer(-1_i32))]))
}
fn normalize_v(v: ExprId, dv: ExprId, var: ExprId, pool: &ExprPool) -> ExprId {
let mut best = v;
let mut cur = v;
for _ in 0..3 {
let expanded = reduce_sqrt_squares(simplify_expanded(cur, pool).value, pool);
let folded = simplify(expanded, pool).value;
let rationalised = reduce_sqrt_squares(rationalize_radicals(folded, pool), pool);
let mut candidates = vec![
folded,
simplify(rationalised, pool).value,
simplify_expanded(rationalised, pool).value,
];
for i in 0..candidates.len() {
let collected = simplify(collect_like_terms_deep(candidates[i], pool), pool).value;
if !candidates.contains(&collected) {
candidates.push(collected);
}
}
for i in 0..candidates.len() {
if let Ok(k) = crate::poly::cancel::cancel(candidates[i], vec![var], pool) {
candidates.push(simplify(k, pool).value);
}
}
let next = *candidates
.iter()
.min_by_key(|&&c| node_count(c, pool))
.unwrap_or(&cur);
let before = best;
for c in candidates {
if c == best || node_count(c, pool) >= node_count(best, pool) {
continue;
}
if verify_antiderivative_status(c, dv, var, pool).is_some() {
best = c;
}
}
if best == before && next == cur {
break;
}
cur = next;
}
best
}
fn combine_radicals(expr: ExprId, var: ExprId, pool: &ExprPool) -> Option<ExprId> {
let ExprData::Mul(args) = pool.get(expr) else {
return None;
};
fn as_sqrt_power(a: ExprId, pool: &ExprPool) -> Option<(ExprId, i64)> {
match pool.get(a) {
ExprData::Func { name, args } if name == "sqrt" && args.len() == 1 => {
Some((args[0], 1))
}
ExprData::Pow { base, exp } => {
let k = pool.with(exp, |d| match d {
ExprData::Integer(i) => i.0.to_i64(),
_ => None,
})?;
match pool.get(base) {
ExprData::Func { name, args } if name == "sqrt" && args.len() == 1 => {
Some((args[0], k))
}
_ => None,
}
}
_ => None,
}
}
let mut radicands: Vec<(ExprId, i64)> = Vec::new();
let mut rest: Vec<ExprId> = Vec::new();
for &a in &args {
match as_sqrt_power(a, pool) {
Some((r, k)) => radicands.push((r, k)),
None => rest.push(a),
}
}
if radicands.len() < 2 {
return None;
}
let factors: Vec<ExprId> = radicands
.iter()
.map(|&(r, k)| pool.pow(r, pool.integer(k)))
.collect();
let product = pool.mul(factors);
let reduced = crate::poly::cancel::cancel(product, vec![var], pool).ok()?;
let reduced = simplify(reduced, pool).value;
let (outside, radicand) = extract_square_monomial(reduced, var, pool);
if !is_one(outside, pool) {
rest.push(outside);
}
rest.push(pool.func("sqrt", vec![radicand]));
Some(simplify(mul_of(&rest, pool), pool).value)
}
fn combine_radicals_deep(expr: ExprId, var: ExprId, pool: &ExprPool) -> ExprId {
let rebuilt = match pool.get(expr) {
ExprData::Add(args) => pool.add(
args.iter()
.map(|&a| combine_radicals_deep(a, var, pool))
.collect(),
),
ExprData::Mul(args) => pool.mul(
args.iter()
.map(|&a| combine_radicals_deep(a, var, pool))
.collect(),
),
ExprData::Pow { base, exp } => pool.pow(
combine_radicals_deep(base, var, pool),
combine_radicals_deep(exp, var, pool),
),
ExprData::Func { name, args } => pool.func(
name,
args.iter()
.map(|&a| combine_radicals_deep(a, var, pool))
.collect(),
),
_ => return expr,
};
combine_radicals(rebuilt, var, pool).unwrap_or(rebuilt)
}
fn split_sqrt_quotients(expr: ExprId, var: ExprId, pool: &ExprPool) -> ExprId {
match pool.get(expr) {
ExprData::Func { name, args } if name == "sqrt" && args.len() == 1 => {
let r = split_sqrt_quotients(args[0], var, pool);
let r = simplify(reduce_sqrt_squares(r, pool), pool).value;
if let Ok((n, d)) = crate::poly::cancel::together_parts(r, vec![var], pool) {
let d = simplify(d, pool).value;
if !is_one(d, pool) && !is_zero(d, pool) {
let n = simplify(n, pool).value;
let orientations = [(n, d), (negate(n, pool), negate(d, pool))];
let best = orientations
.iter()
.filter_map(|&(n, d)| {
sqrt_split_coverage(r, n, d, var, pool).map(|c| (c, n, d))
})
.filter(|&(c, _, _)| c > 0)
.max_by_key(|&(c, _, _)| c);
if let Some((_, n, d)) = best {
let sn = pool.func("sqrt", vec![n]);
let sd = pool.func("sqrt", vec![d]);
return pool.mul(vec![sn, pool.pow(sd, pool.integer(-1_i32))]);
}
}
}
pool.func("sqrt", vec![r])
}
ExprData::Add(args) => pool.add(
args.iter()
.map(|&a| split_sqrt_quotients(a, var, pool))
.collect(),
),
ExprData::Mul(args) => pool.mul(
args.iter()
.map(|&a| split_sqrt_quotients(a, var, pool))
.collect(),
),
ExprData::Pow { base, exp } => pool.pow(
split_sqrt_quotients(base, var, pool),
split_sqrt_quotients(exp, var, pool),
),
ExprData::Func { name, args } => pool.func(
name,
args.iter()
.map(|&a| split_sqrt_quotients(a, var, pool))
.collect(),
),
_ => expr,
}
}
fn extract_square_factors(expr: ExprId, var: ExprId, pool: &ExprPool) -> ExprId {
match pool.get(expr) {
ExprData::Func { name, args } if name == "sqrt" && args.len() == 1 => {
let r = extract_square_factors(args[0], var, pool);
match square_factor_split(r, var, pool) {
Some((outside, radicand)) => {
pool.mul(vec![outside, pool.func("sqrt", vec![radicand])])
}
None => pool.func("sqrt", vec![r]),
}
}
ExprData::Add(args) => pool.add(
args.iter()
.map(|&a| extract_square_factors(a, var, pool))
.collect(),
),
ExprData::Mul(args) => pool.mul(
args.iter()
.map(|&a| extract_square_factors(a, var, pool))
.collect(),
),
ExprData::Pow { base, exp } => pool.pow(
extract_square_factors(base, var, pool),
extract_square_factors(exp, var, pool),
),
ExprData::Func { name, args } => pool.func(
name,
args.iter()
.map(|&a| extract_square_factors(a, var, pool))
.collect(),
),
_ => expr,
}
}
fn square_factor_split(r: ExprId, var: ExprId, pool: &ExprPool) -> Option<(ExprId, ExprId)> {
let up = crate::poly::unipoly::UniPoly::from_symbolic(r, var, pool).ok()?;
if up.degree() < 2 {
return None;
}
let fac = up.factor_z().ok()?;
if fac.factors.iter().all(|&(_, e)| e < 2) {
return None;
}
let mut outside: Vec<ExprId> = Vec::new();
let mut inside: Vec<ExprId> = vec![pool.integer(fac.unit.clone())];
for (f, e) in &fac.factors {
let fe = f.to_symbolic_expr(pool);
if e / 2 > 0 {
outside.push(pool.pow(fe, pool.integer(i64::from(e / 2))));
}
if e % 2 == 1 {
inside.push(fe);
}
}
if outside.is_empty() {
return None;
}
Some((
simplify(mul_of(&outside, pool), pool).value,
simplify(mul_of(&inside, pool), pool).value,
))
}
fn negate(expr: ExprId, pool: &ExprPool) -> ExprId {
simplify(pool.mul(vec![pool.integer(-1_i32), expr]), pool).value
}
fn sqrt_split_coverage(
r: ExprId,
n: ExprId,
d: ExprId,
var: ExprId,
pool: &ExprPool,
) -> Option<usize> {
let lhs = pool.func("sqrt", vec![r]);
let rhs = pool.mul(vec![
pool.func("sqrt", vec![n]),
pool.pow(pool.func("sqrt", vec![d]), pool.integer(-1_i32)),
]);
let mut agreed = 0_usize;
for &xv in &SQRT_SPLIT_SAMPLES {
let mut env = HashMap::new();
env.insert(var, xv);
let (Some(a), Some(b)) = (
crate::jit::eval_interp(lhs, &env, pool),
crate::jit::eval_interp(rhs, &env, pool),
) else {
continue;
};
if !a.is_finite() || !b.is_finite() {
continue;
}
if (a - b).abs() > 1e-9 * (1.0 + a.abs().max(b.abs())) {
return None;
}
agreed += 1;
}
Some(agreed)
}
const SQRT_SPLIT_SAMPLES: [f64; 10] = [
0.1237, -0.1237, 0.3719, -0.3719, 0.6113, -0.6113, 1.4231, -1.4231, 2.8123, -2.8123,
];
fn agrees_numerically(cand: ExprId, orig: ExprId, var: ExprId, pool: &ExprPool) -> bool {
let mut agreed = false;
for &xv in &SQRT_SPLIT_SAMPLES {
let mut env = HashMap::new();
env.insert(var, xv);
let (Some(a), Some(b)) = (
crate::jit::eval_interp(cand, &env, pool),
crate::jit::eval_interp(orig, &env, pool),
) else {
continue;
};
if !a.is_finite() || !b.is_finite() {
continue;
}
if (a - b).abs() > 1e-9 * (1.0 + a.abs().max(b.abs())) {
return false;
}
agreed = true;
}
agreed
}
fn collect_var_sqrts(expr: ExprId, var: ExprId, pool: &ExprPool, out: &mut Vec<ExprId>) {
match pool.get(expr) {
ExprData::Func { ref name, ref args }
if name == "sqrt" && args.len() == 1 && !is_free_of(args[0], var, pool) =>
{
if !out.contains(&expr) {
out.push(expr);
}
collect_var_sqrts(args[0], var, pool, out);
}
ExprData::Add(args) | ExprData::Mul(args) | ExprData::Func { args, .. } => {
for a in args {
collect_var_sqrts(a, var, pool, out);
}
}
ExprData::Pow { base, exp } => {
collect_var_sqrts(base, var, pool, out);
collect_var_sqrts(exp, var, pool, out);
}
_ => {}
}
}
fn radical_normalisation_applies(expr: ExprId, var: ExprId, pool: &ExprPool) -> bool {
let mut sqrts = Vec::new();
collect_var_sqrts(expr, var, pool, &mut sqrts);
match sqrts.len() {
0 => false,
1 => {
let ExprData::Func { args, .. } = pool.get(sqrts[0]) else {
return false;
};
crate::poly::unipoly::UniPoly::from_symbolic(args[0], var, pool).is_err()
}
_ => true,
}
}
const RADICAL_NORMAL_PASSES: u32 = 3;
fn radical_normal_forms(expr: ExprId, var: ExprId, pool: &ExprPool) -> Vec<ExprId> {
let mut out: Vec<ExprId> = Vec::new();
if !radical_normalisation_applies(expr, var, pool) {
return out;
}
let mut cur = expr;
for _ in 0..RADICAL_NORMAL_PASSES {
if crate::budget::check().is_err() {
return out;
}
let expanded = reduce_sqrt_squares(simplify_expanded(cur, pool).value, pool);
let merged = combine_radicals_deep(expanded, var, pool);
let pulled = extract_square_factors(merged, var, pool);
let split = simplify(
reduce_sqrt_squares(split_sqrt_quotients(pulled, var, pool), pool),
pool,
)
.value;
let rationalised = simplify(
reduce_sqrt_squares(rationalize_radicals(split, pool), pool),
pool,
)
.value;
let mut produced = vec![split, rationalised];
for form in [split, rationalised] {
if let Ok(k) = crate::poly::cancel::cancel(form, vec![var], pool) {
produced.push(simplify(reduce_sqrt_squares(k, pool), pool).value);
}
}
let mut advanced = false;
let mut kept: Option<ExprId> = None;
for f in produced {
if !agrees_numerically(f, expr, var, pool) {
continue;
}
if f != expr && !out.contains(&f) {
out.push(f);
}
if f != cur {
advanced = true;
}
kept = Some(f);
}
let Some(next) = kept else { break };
if !advanced || next == cur {
break;
}
cur = next;
}
out
}
fn monomial_degree(term: ExprId, var: ExprId, pool: &ExprPool) -> Option<i64> {
let factors: Vec<ExprId> = match pool.get(term) {
ExprData::Mul(args) => args,
_ => vec![term],
};
let mut deg = 0_i64;
for f in factors {
if f == var {
deg += 1;
} else if let ExprData::Pow { base, exp } = pool.get(f) {
if base == var {
deg += pool.with(exp, |d| match d {
ExprData::Integer(i) => i.0.to_i64(),
_ => None,
})?;
} else if !is_free_of(f, var, pool) {
return None;
}
} else if !is_free_of(f, var, pool) {
return None;
}
}
Some(deg)
}
fn extract_square_monomial(r: ExprId, var: ExprId, pool: &ExprPool) -> (ExprId, ExprId) {
let one = pool.integer(1_i32);
let terms: Vec<ExprId> = match pool.get(r) {
ExprData::Add(args) => args,
_ => return (one, r),
};
let mut min_deg = i64::MAX;
for &t in &terms {
match monomial_degree(t, var, pool) {
Some(d) => min_deg = min_deg.min(d),
None => return (one, r),
}
}
let k = if min_deg >= 2 { min_deg / 2 } else { 0 };
if k <= 0 {
return (one, r);
}
let shift = pool.pow(var, pool.integer(-2 * k));
let reduced: Vec<ExprId> = terms
.iter()
.map(|&t| simplify(pool.mul(vec![t, shift]), pool).value)
.collect();
let outside = if k == 1 {
var
} else {
pool.pow(var, pool.integer(k))
};
(outside, simplify(pool.add(reduced), pool).value)
}
fn close_integral(w_raw: ExprId, var: ExprId, pool: &ExprPool) -> Option<ExprId> {
let combined = simplify(w_raw, pool).value;
let expanded = simplify_expanded(w_raw, pool).value;
let mut forms: Vec<ExprId> = vec![combined];
if expanded != combined {
forms.push(expanded);
}
if let Ok(t) = crate::poly::cancel::together(combined, vec![var], pool) {
let t = simplify(t, pool).value;
if !forms.contains(&t) {
forms.push(t);
}
}
for i in 0..forms.len() {
let collected = simplify(collect_like_terms_deep(forms[i], pool), pool).value;
if !forms.contains(&collected) {
forms.push(collected);
}
}
let mut merged: Vec<ExprId> = Vec::new();
for &form in &forms {
if let Some(m) = combine_radicals(form, var, pool) {
merged.push(m);
}
if let ExprData::Add(terms) = pool.get(form) {
let per_term: Vec<ExprId> = terms
.iter()
.map(|&t| combine_radicals(t, var, pool).unwrap_or(t))
.collect();
let rebuilt = simplify(pool.add(per_term), pool).value;
if rebuilt != form {
merged.push(rebuilt);
}
}
}
for m in merged {
if !forms.contains(&m) {
forms.push(m);
}
}
for f in radical_normal_forms(combined, var, pool) {
if !forms.contains(&f) {
forms.push(f);
}
}
for form in &forms {
if crate::budget::check().is_err() {
return None;
}
if let Ok(res) = integrate(*form, var, pool) {
return Some(res.value);
}
}
for form in &forms {
if let Some(res) = integrate_additive(*form, var, pool) {
return Some(res);
}
}
None
}
fn integrate_additive(expr: ExprId, var: ExprId, pool: &ExprPool) -> Option<ExprId> {
let ExprData::Add(args) = pool.get(expr) else {
return None; };
let mut terms = Vec::with_capacity(args.len());
for a in args {
crate::budget::check().ok()?;
terms.push(integrate(a, var, pool).ok()?.value);
}
Some(pool.add(terms))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Rat {
n: i64,
d: i64,
}
impl Rat {
fn new(n: i64, d: i64) -> Self {
let s = if d < 0 { -1 } else { 1 };
let g = gcd_i64(n.abs(), d.abs());
Rat {
n: s * n / g,
d: s * d / g,
}
}
fn one() -> Self {
Rat { n: 1, d: 1 }
}
fn neg(self) -> Self {
Rat {
n: -self.n,
d: self.d,
}
}
fn mul(self, o: Rat) -> Self {
Rat::new(self.n * o.n, self.d * o.d)
}
fn sub_from_one(self) -> Self {
Rat::new(self.d - self.n, self.d)
}
fn is_zero(self) -> bool {
self.n == 0
}
fn to_expr(self, pool: &ExprPool) -> ExprId {
if self.d == 1 {
pool.integer(self.n)
} else {
pool.rational(self.n, self.d)
}
}
fn recip(self) -> Option<Self> {
if self.n == 0 {
None
} else {
Some(Rat::new(self.d, self.n))
}
}
}
fn run_chain(integrand: ExprId, first: Split, var: ExprId, pool: &ExprPool) -> Option<ExprId> {
let mut acc = pool.integer(0_i32);
let mut mult = Rat::one();
let mut w = integrand;
let mut split = first;
let mut seen: Vec<ExprId> = vec![integrand];
for step in 0..MAX_IBP_STEPS {
crate::budget::check().ok()?;
let v = if is_one(split.dv, pool) {
var
} else {
let raw = simplify(integrate(split.dv, var, pool).ok()?.value, pool).value;
normalize_v(raw, split.dv, var, pool)
};
let du = simplify(crate::diff::diff(split.u, var, pool).ok()?.value, pool).value;
let uv = simplify(pool.mul(vec![split.u, v]), pool).value;
let raw_w = pool.mul(vec![v, du]);
let next_w = simplify(raw_w, pool).value;
let grew =
structural_size(next_w, pool) as f64 > GROWTH_LIMIT * structural_size(w, pool) as f64;
if grew && !(step == 0 && split.trivial_dv) {
return None;
}
acc = simplify(
pool.add(vec![acc, pool.mul(vec![mult.to_expr(pool), uv])]),
pool,
)
.value;
mult = mult.neg();
w = next_w;
if let Some(res) = close_integral(raw_w, var, pool) {
let tail = pool.mul(vec![mult.to_expr(pool), res]);
return Some(simplify(pool.add(vec![acc, tail]), pool).value);
}
if let Some((cn, cd)) = constant_ratio(w, integrand, var, pool) {
let factor = mult.mul(Rat::new(cn, cd)).sub_from_one();
if !factor.is_zero() {
if let Some(inv) = factor.recip() {
return Some(simplify(pool.mul(vec![inv.to_expr(pool), acc]), pool).value);
}
}
return None;
}
if seen.contains(&w) {
return None; }
seen.push(w);
split = *candidate_splits(w, var, pool).first()?;
}
None
}
pub fn integrate_by_parts(expr: ExprId, var: ExprId, pool: &ExprPool) -> ByPartsOutcome {
let mut log = DerivationLog::new();
match try_by_parts(expr, var, pool, &mut log) {
Some(f) => ByPartsOutcome::Solved(f),
None => ByPartsOutcome::Declined(format!(
"∫ {} — no integration-by-parts split closed the integral",
pool.display(expr)
)),
}
}
pub fn integrate_by_parts_derived(
expr: ExprId,
var: ExprId,
pool: &ExprPool,
) -> Result<DerivedExpr<ExprId>, IntegrationError> {
let mut log = DerivationLog::new();
match try_by_parts(expr, var, pool, &mut log) {
Some(f) => Ok(DerivedExpr::with_log(f, log)),
None => Err(IntegrationError::NotImplemented(format!(
"∫ {} — no integration-by-parts split closed the integral",
pool.display(expr)
))),
}
}
pub fn try_by_parts(
expr: ExprId,
var: ExprId,
pool: &ExprPool,
log: &mut DerivationLog,
) -> Option<ExprId> {
let depth = IBP_DEPTH.with(|d| d.get());
if depth >= MAX_REENTRY_DEPTH {
return None;
}
IBP_DEPTH.with(|d| d.set(depth + 1));
let _guard = DepthGuard;
if is_free_of(expr, var, pool) || expr == var {
return None;
}
let work = simplify(expr, pool).value;
for split in candidate_splits(work, var, pool) {
crate::budget::check().ok()?;
let Some(candidate) = run_chain(work, split, var, pool) else {
continue;
};
if verify_antiderivative_status(candidate, expr, var, pool).is_some() {
log.push(RewriteStep::simple("int_by_parts", expr, candidate));
return Some(candidate);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::{Domain, ExprPool};
fn p() -> ExprPool {
ExprPool::new()
}
fn solved(expr: ExprId, var: ExprId, pool: &ExprPool) -> ExprId {
match integrate_by_parts(expr, var, pool) {
ByPartsOutcome::Solved(f) => {
assert!(
verify_antiderivative_status(f, expr, var, pool).is_some(),
"returned {} for ∫ {} but d/dx does not match",
pool.display(f),
pool.display(expr)
);
f
}
ByPartsOutcome::Declined(why) => {
panic!(
"expected a solution for ∫ {}, got: {why}",
pool.display(expr)
)
}
}
}
#[test]
fn x_times_exp() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let f = pool.mul(vec![x, pool.func("exp", vec![x])]);
solved(f, x, &pool);
}
#[test]
fn x_squared_times_exp() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let x2 = pool.pow(x, pool.integer(2_i32));
let f = pool.mul(vec![x2, pool.func("exp", vec![x])]);
solved(f, x, &pool);
}
#[test]
fn x_times_log() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let f = pool.mul(vec![x, pool.func("log", vec![x])]);
solved(f, x, &pool);
}
#[test]
fn bare_log() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let f = pool.func("log", vec![x]);
solved(f, x, &pool);
}
#[test]
fn bare_atan() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let f = pool.func("atan", vec![x]);
solved(f, x, &pool);
}
#[test]
fn x_times_sin() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let f = pool.mul(vec![x, pool.func("sin", vec![x])]);
solved(f, x, &pool);
}
#[test]
fn exp_sin_closes_through_the_linear_solve() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let f = pool.mul(vec![pool.func("exp", vec![x]), pool.func("sin", vec![x])]);
let ecos = pool.mul(vec![pool.func("exp", vec![x]), pool.func("cos", vec![x])]);
let neg_f = pool.mul(vec![pool.integer(-1_i32), f]);
assert_eq!(
constant_ratio(neg_f, f, x, &pool),
Some((-1, 1)),
"the cycle detector must see −eˣsin x as (−1)·the original"
);
assert_eq!(
constant_ratio(ecos, f, x, &pool),
None,
"eˣcos x is not a constant multiple of eˣsin x"
);
let result = solved(f, x, &pool);
let printed = format!("{}", pool.display(result));
assert!(
printed.contains("exp") && printed.contains("sin") && printed.contains("cos"),
"expected eˣ(sin x − cos x)/2, got {printed}"
);
}
#[test]
fn the_linear_solve_closes_sin_of_log() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let f = pool.func("sin", vec![pool.func("log", vec![x])]);
let via = integrate(f, x, &pool).expect("∫sin(log x) dx");
assert!(
via.log
.steps()
.iter()
.any(|s| s.rule_name == "int_by_parts"),
"∫sin(log x) dx must be closed by by-parts, got {:?}",
via.log
.steps()
.iter()
.map(|s| &s.rule_name)
.collect::<Vec<_>>()
);
let neg_f = pool.mul(vec![pool.integer(-1_i32), f]);
assert_eq!(constant_ratio(neg_f, f, x, &pool), Some((-1, 1)));
let res = solved(f, x, &pool);
let printed = format!("{}", pool.display(res));
assert!(
printed.contains("1/2") || printed.contains("2^-1"),
"expected the ½ from I(1 − mult·c) = acc, got {printed}"
);
}
#[test]
fn exp_cos_closes() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let f = pool.mul(vec![pool.func("exp", vec![x]), pool.func("cos", vec![x])]);
solved(f, x, &pool);
}
#[test]
fn the_linear_solve_arithmetic_is_right() {
let mult = Rat::one().neg().neg();
let c = Rat::new(-1, 1);
let factor = mult.mul(c).sub_from_one();
assert_eq!(factor, Rat::new(2, 1));
assert_eq!(factor.recip(), Some(Rat::new(1, 2)));
}
#[test]
fn c3_asin_of_composite() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let x2 = pool.pow(x, pool.integer(2_i32));
let one_minus = pool.add(vec![
pool.integer(1_i32),
pool.mul(vec![pool.integer(-1_i32), x2]),
]);
let g = pool.mul(vec![
x,
pool.pow(pool.func("sqrt", vec![one_minus]), pool.integer(-1_i32)),
]);
let f = pool.func("asin", vec![g]);
solved(f, x, &pool);
let splits = candidate_splits(f, x, &pool);
assert!(
splits.iter().any(|s| s.trivial_dv && s.u == f),
"the dv = dx split must be offered for a bare composite"
);
assert_eq!(splits[0].class, Liate::InverseTrig);
}
#[test]
fn c3_atan_of_composite_is_attempted_not_refused() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let x2 = pool.pow(x, pool.integer(2_i32));
let g = pool.mul(vec![
x,
pool.func("sqrt", vec![pool.add(vec![pool.integer(1_i32), x2])]),
]);
let f = pool.func("atan", vec![g]);
let splits = candidate_splits(f, x, &pool);
assert_eq!(splits.len(), 1, "a bare composite has exactly one split");
assert!(splits[0].trivial_dv);
match integrate_by_parts(f, x, &pool) {
ByPartsOutcome::Solved(r) => {
assert!(verify_antiderivative_status(r, f, x, &pool).is_some())
}
ByPartsOutcome::Declined(_) => {}
}
}
#[test]
fn c3_atan_of_a_difference_of_roots_closes() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let g = pool.add(vec![
pool.func("sqrt", vec![pool.add(vec![pool.integer(1_i32), x])]),
pool.mul(vec![pool.integer(-1_i32), pool.func("sqrt", vec![x])]),
]);
let f = pool.func("atan", vec![g]);
solved(f, x, &pool);
}
#[test]
fn the_same_route_closes_shapes_outside_charlwood() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let x2 = pool.pow(x, pool.integer(2_i32));
let one_plus = pool.add(vec![pool.integer(1_i32), x2]);
let g = pool.mul(vec![
x,
pool.pow(pool.func("sqrt", vec![one_plus]), pool.integer(-1_i32)),
]);
solved(pool.func("asin", vec![g]), x, &pool);
solved(pool.func("acos", vec![g]), x, &pool);
let f = pool.pow(
pool.mul(vec![
pool.func("sqrt", vec![x]),
pool.func("sqrt", vec![pool.add(vec![pool.integer(1_i32), x])]),
]),
pool.integer(-1_i32),
);
let got = integrate(f, x, &pool).expect("∫dx/(√x√(1+x)) should close");
assert!(
verify_antiderivative_status(got.value, f, x, &pool).is_some(),
"returned {} but d/dx does not match",
pool.display(got.value)
);
}
#[test]
fn radical_normal_forms_preserve_the_value_and_reach_the_engine() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let x2 = pool.pow(x, pool.integer(2_i32));
let one_minus = pool.add(vec![
pool.integer(1_i32),
pool.mul(vec![pool.integer(-1_i32), x2]),
]);
let g = pool.mul(vec![
x,
pool.pow(pool.func("sqrt", vec![one_minus]), pool.integer(-1_i32)),
]);
let f = pool.func("asin", vec![g]);
let du = simplify(crate::diff::diff(f, x, &pool).unwrap().value, &pool).value;
let w = simplify(pool.mul(vec![x, du]), &pool).value;
let forms = radical_normal_forms(w, x, &pool);
assert!(!forms.is_empty(), "no normalised spelling was produced");
for &form in &forms {
let mut agreed = 0;
for &xv in &[0.11_f64, -0.11, 0.31, -0.31, 0.49, -0.49] {
let mut env = HashMap::new();
env.insert(x, xv);
let (Some(a), Some(b)) = (
crate::jit::eval_interp(form, &env, &pool),
crate::jit::eval_interp(w, &env, &pool),
) else {
continue;
};
if !a.is_finite() || !b.is_finite() {
continue;
}
assert!(
(a - b).abs() < 1e-9 * (1.0 + a.abs().max(b.abs())),
"normalised form {} disagrees with the residual {} at x = {xv} \
({a} vs {b})",
pool.display(form),
pool.display(w)
);
agreed += 1;
}
assert!(
agreed > 0,
"normalised form {} is defined nowhere on the grid",
pool.display(form)
);
}
assert!(
forms.iter().any(|&f| integrate(f, x, &pool).is_ok()),
"no normalised spelling of the #49 residual reached a route that closes"
);
}
#[test]
fn square_factors_come_out_of_the_radicand() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let r = pool.add(vec![
x,
pool.mul(vec![pool.integer(2_i32), pool.pow(x, pool.integer(2_i32))]),
pool.pow(x, pool.integer(3_i32)),
]);
let (outside, radicand) =
square_factor_split(r, x, &pool).expect("x(1+x)² has a repeated factor");
assert_eq!(simplify(radicand, &pool).value, x);
let mut env = HashMap::new();
env.insert(x, 2.0_f64);
let v = crate::jit::eval_interp(outside, &env, &pool).expect("evaluable");
assert!((v - 3.0).abs() < 1e-12, "expected 1+x, got {v} at x = 2");
}
#[test]
fn a_squarefree_radicand_is_left_alone() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let r = pool.add(vec![pool.integer(1_i32), pool.pow(x, pool.integer(2_i32))]);
assert_eq!(square_factor_split(r, x, &pool), None);
let s = pool.func("sqrt", vec![r]);
assert_eq!(extract_square_factors(s, x, &pool), s);
}
#[test]
fn the_sqrt_split_picks_the_orientation_that_covers_more_of_the_line() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let x2 = pool.pow(x, pool.integer(2_i32));
let n = pool.add(vec![
pool.integer(1_i32),
pool.mul(vec![pool.integer(-2_i32), x2]),
]);
let d = pool.add(vec![
pool.integer(1_i32),
pool.mul(vec![pool.integer(-1_i32), x2]),
]);
let r = simplify(pool.mul(vec![n, pool.pow(d, pool.integer(-1_i32))]), &pool).value;
let pos = sqrt_split_coverage(r, n, d, x, &pool).expect("positive orientation is sound");
let neg = sqrt_split_coverage(r, negate(n, &pool), negate(d, &pool), x, &pool)
.expect("flipped orientation is sound on |x| > 1");
assert!(
pos > neg,
"the near-zero orientation must win: {pos} vs {neg}"
);
let s = pool.func("sqrt", vec![r]);
let split = split_sqrt_quotients(s, x, &pool);
assert!(agrees_numerically(split, s, x, &pool));
let mut env = HashMap::new();
env.insert(x, 0.31_f64);
let v = crate::jit::eval_interp(split, &env, &pool).expect("evaluable");
assert!(
v.is_finite(),
"the split must be defined at x = 0.31, got {v}"
);
}
#[test]
fn a_value_contradicting_split_is_rejected() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let x2 = pool.pow(x, pool.integer(2_i32));
let r = pool.add(vec![pool.integer(4_i32), x2]);
let one = pool.integer(1_i32);
assert_eq!(sqrt_split_coverage(r, r, one, x, &pool), Some(10));
let quarter = simplify(pool.mul(vec![pool.rational(1, 4), r]), &pool).value;
assert_eq!(sqrt_split_coverage(r, quarter, one, x, &pool), None);
}
#[test]
fn reduce_sqrt_squares_sees_through_a_product_power() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let x2 = pool.pow(x, pool.integer(2_i32));
let d = pool.add(vec![
pool.integer(1_i32),
pool.mul(vec![pool.integer(-1_i32), x2]),
]);
let s = pool.func("sqrt", vec![d]);
let e = pool.pow(
pool.mul(vec![x, pool.pow(s, pool.integer(-1_i32))]),
pool.integer(2_i32),
);
let got = simplify(reduce_sqrt_squares(e, &pool), &pool).value;
let want = simplify(pool.mul(vec![x2, pool.pow(d, pool.integer(-1_i32))]), &pool).value;
assert_eq!(
got,
want,
"got {} want {}",
pool.display(got),
pool.display(want)
);
}
#[test]
fn a_non_terminating_shape_declines_within_the_bound() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let f = pool.func("exp", vec![pool.pow(x, pool.integer(2_i32))]);
let t0 = std::time::Instant::now();
let out = integrate_by_parts(f, x, &pool);
let dt = t0.elapsed();
assert!(out.is_declined(), "∫e^{{x²}} must decline, got {out:?}");
assert!(
dt < std::time::Duration::from_secs(20),
"the decline took {dt:?} — the step bound is not holding"
);
}
#[test]
fn growth_backs_out_rather_than_running_away() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let x2 = pool.pow(x, pool.integer(2_i32));
let f = pool.func("sin", vec![x2]);
let out = integrate_by_parts(f, x, &pool);
assert!(out.is_declined(), "∫sin(x²) must decline, got {out:?}");
}
const _: () = {
assert!(
MAX_IBP_STEPS >= 2,
"two steps are needed for the exp·sin cycle"
);
assert!(MAX_IBP_STEPS <= 6, "a deep chain is blow-up, not coverage");
assert!(MAX_REENTRY_DEPTH >= 1);
};
#[test]
fn declined_cannot_become_non_elementary() {
use crate::errors::AlkahestError;
let pool = p();
let x = pool.symbol("x", Domain::Real);
let f = pool.func("exp", vec![pool.pow(x, pool.integer(2_i32))]);
let out = integrate_by_parts(f, x, &pool);
assert!(out.is_declined());
let err = out.into_result().unwrap_err();
assert_eq!(err.code(), "E-INT-001");
assert!(
matches!(err, IntegrationError::NotImplemented(_)),
"a by-parts decline must be NotImplemented, never NonElementary"
);
assert_ne!(err.code(), "E-INT-004");
let sample = ByPartsOutcome::Declined("x".into());
match sample {
ByPartsOutcome::Solved(_) => {}
ByPartsOutcome::Declined(_) => {}
}
}
#[test]
fn every_solved_result_verifies() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let ex = pool.func("exp", vec![x]);
let lx = pool.func("log", vec![x]);
let sx = pool.func("sin", vec![x]);
let cx = pool.func("cos", vec![x]);
let x2 = pool.pow(x, pool.integer(2_i32));
let cases = vec![
pool.mul(vec![x, ex]),
pool.mul(vec![x2, ex]),
pool.mul(vec![x, lx]),
lx,
pool.func("atan", vec![x]),
pool.func("asin", vec![x]),
pool.mul(vec![x, sx]),
pool.mul(vec![x, cx]),
pool.mul(vec![x2, sx]),
pool.mul(vec![ex, sx]),
pool.mul(vec![ex, cx]),
pool.mul(vec![x, pool.func("atan", vec![x])]),
];
let mut solved_n = 0;
for f in cases {
if let ByPartsOutcome::Solved(res) = integrate_by_parts(f, x, &pool) {
solved_n += 1;
assert!(
verify_antiderivative_status(res, f, x, &pool).is_some(),
"unverified answer {} for ∫ {}",
pool.display(res),
pool.display(f)
);
}
}
assert!(
solved_n >= 10,
"expected at least 10 of the 12 classics, solved {solved_n}"
);
}
#[test]
fn liate_orders_the_classes() {
assert!(Liate::Log < Liate::InverseTrig);
assert!(Liate::InverseTrig < Liate::Algebraic);
assert!(Liate::Algebraic < Liate::Trig);
assert!(Liate::Trig < Liate::Exponential);
assert!(Liate::Exponential < Liate::Other);
}
#[test]
fn liate_classifies_factors() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
assert_eq!(liate_class(pool.func("log", vec![x]), x, &pool), Liate::Log);
assert_eq!(
liate_class(pool.func("atan", vec![x]), x, &pool),
Liate::InverseTrig
);
assert_eq!(
liate_class(pool.pow(x, pool.integer(3_i32)), x, &pool),
Liate::Algebraic
);
assert_eq!(
liate_class(pool.func("sin", vec![x]), x, &pool),
Liate::Trig
);
assert_eq!(
liate_class(pool.func("exp", vec![x]), x, &pool),
Liate::Exponential
);
let a2 = pool.pow(pool.func("atan", vec![x]), pool.integer(2_i32));
assert_eq!(liate_class(a2, x, &pool), Liate::InverseTrig);
}
#[test]
fn liate_picks_the_working_split_for_x_exp() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let f = pool.mul(vec![x, pool.func("exp", vec![x])]);
let splits = candidate_splits(f, x, &pool);
assert_eq!(splits[0].class, Liate::Algebraic);
assert_eq!(splits[0].u, x, "u must be x, not eˣ");
}
#[test]
fn liate_picks_the_working_split_for_x_log() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let f = pool.mul(vec![x, pool.func("log", vec![x])]);
let splits = candidate_splits(f, x, &pool);
assert_eq!(splits[0].class, Liate::Log);
}
#[test]
fn constant_ratio_rejects_a_non_constant_ratio() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let f = pool.func("sin", vec![x]);
let g = pool.mul(vec![x, f]);
assert_eq!(constant_ratio(g, f, x, &pool), None);
}
#[test]
fn constant_ratio_finds_a_rational_multiple() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let f = pool.func("sin", vec![x]);
let g = pool.mul(vec![pool.rational(3_i32, 4_i32), f]);
assert_eq!(constant_ratio(g, f, x, &pool), Some((3, 4)));
}
#[test]
#[ignore = "measurement harness; run explicitly with --ignored --nocapture"]
fn charlwood_c1_c3_coverage() {
use crate::parse::parse;
use std::collections::HashMap as Map;
let problems: &[(&str, u32, &str)] = &[
("C1", 9, "atan(sqrt(-1+1/cos(x)))*sin(x)"),
("C1", 10, "exp(asin(x))*x^3/sqrt(1-x^2)"),
("C1", 13, "x*atan(x+sqrt(1-x^2))/sqrt(1-x^2)"),
("C1", 14, "asin(x)/(1+sqrt(1-x^2))"),
("C1", 16, "asin(x)/(1+x^2)^(3/2)"),
("C1", 21, "x^3*asin(x)/sqrt(1-x^4)"),
("C1", 22, "x^3*acos(1/x)/sqrt(-1+x^4)"),
("C1", 29, "atan(x)/(x^2*sqrt(1-x^2))"),
("C1", 30, "x*atan(x)/sqrt(1-x^2)"),
("C1", 35, "x*acos(1/x)/sqrt(-1+x^2)"),
("C1", 37, "sin(x)/(1+sin(x)^2)"),
("C3", 3, "asin(-sqrt(x)+sqrt(1+x))"),
("C3", 12, "atan(x+sqrt(1-x^2))"),
("C3", 47, "atan(x*sqrt(1+x^2))"),
("C3", 48, "atan(-sqrt(x)+sqrt(1+x))"),
("C3", 49, "asin(x/sqrt(1-x^2))"),
("C3", 50, "atan(x*sqrt(1-x^2))"),
];
let pool = p();
let x = pool.symbol("x", Domain::Real);
let mut solved_list: Vec<(String, u32)> = Vec::new();
for (cluster, n, src) in problems {
let mut syms: Map<String, ExprId> = Map::from([("x".to_owned(), x)]);
let Ok(f) = parse(src, &pool, &mut syms) else {
println!("{cluster}#{n:<3} PARSE-ERROR {src}");
continue;
};
let t0 = std::time::Instant::now();
let out = integrate_by_parts(f, x, &pool);
let dt = t0.elapsed();
match out {
ByPartsOutcome::Solved(res) => {
let ok = verify_antiderivative_status(res, f, x, &pool);
assert!(
ok.is_some(),
"{cluster}#{n} returned an UNVERIFIED antiderivative — \
the soundness gate leaked"
);
println!(
"{cluster}#{n:<3} SOLVED [{ok:?}] {dt:>9.1?} {src}\n F = {}",
pool.display(res)
);
solved_list.push((cluster.to_string(), *n));
}
ByPartsOutcome::Declined(_) => {
println!("{cluster}#{n:<3} declined {dt:>9.1?} {src}");
}
}
}
let c1 = solved_list.iter().filter(|(c, _)| c == "C1").count();
let c3 = solved_list.iter().filter(|(c, _)| c == "C3").count();
println!("\nC1: {c1}/11 closed C3: {c3}/6 closed");
println!("closed: {solved_list:?}");
}
#[test]
#[ignore = "measurement harness; run explicitly with --ignored --nocapture"]
fn decline_cost() {
use crate::parse::parse;
use std::collections::HashMap as Map;
let cases = [
"exp(x)/x",
"sin(x)/x",
"exp(x^2)",
"sqrt(tan(x))",
"log(log(x))",
"1/(x^5-x-1)",
"exp(x)*log(x)",
"cos(x)^2/sqrt(1+cos(x)^2+cos(x)^4)",
"sqrt(tan(x))",
"(exp(-x)*(exp(x)+1))^(-1)",
"sin(x^2)",
"log(x)/(1+x)",
"log(log(x))",
"x/(exp(x)+1)",
];
let pool = p();
let x = pool.symbol("x", Domain::Real);
let mut total = std::time::Duration::ZERO;
for src in cases {
let mut syms: Map<String, ExprId> = Map::from([("x".to_owned(), x)]);
let f = parse(src, &pool, &mut syms).expect("parses");
let t0 = std::time::Instant::now();
let out = integrate_by_parts(f, x, &pool);
let dt = t0.elapsed();
total += dt;
let verdict = if out.is_declined() {
"declined"
} else {
"SOLVED"
};
println!("{dt:>10.2?} {verdict:<9} {src}");
}
println!("\ntotal added on {} declines: {total:.2?}", cases.len());
}
#[test]
fn collect_like_terms_cancels_rational_coefficients() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let e = pool.add(vec![
pool.mul(vec![pool.rational(1_i32, 2_i32), x]),
pool.mul(vec![pool.rational(-1_i32, 2_i32), x]),
]);
assert!(
is_zero(simplify(e, &pool).value, &pool),
"simplify now collects like terms over rational coefficients"
);
assert!(is_zero(collect_like_terms(e, &pool), &pool));
}
#[test]
fn collect_like_terms_keeps_unlike_terms() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let e = pool.add(vec![
pool.mul(vec![pool.rational(1_i32, 2_i32), x]),
pool.func("sin", vec![x]),
]);
let got = collect_like_terms(e, &pool);
assert!(!is_zero(got, &pool));
assert!(verify_same_value(got, e, x, &pool));
}
#[test]
fn reduce_sqrt_squares_folds_the_square() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let r = pool.add(vec![pool.integer(1_i32), pool.pow(x, pool.integer(2_i32))]);
let s2 = pool.pow(pool.func("sqrt", vec![r]), pool.integer(2_i32));
let got = simplify(reduce_sqrt_squares(s2, &pool), &pool).value;
assert!(
verify_same_value(got, r, x, &pool),
"expected 1+x², got {}",
pool.display(got)
);
assert!(
node_count(got, &pool) < node_count(simplify(s2, &pool).value, &pool),
"folding the square must shrink the expression; got {}",
pool.display(got)
);
}
#[test]
fn rationalising_collapses_the_euler_form() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let r = pool.add(vec![pool.pow(x, pool.integer(2_i32)), pool.integer(-1_i32)]);
let s = pool.func("sqrt", vec![r]);
let dv = pool.mul(vec![x, pool.pow(s, pool.integer(-1_i32))]);
let xs = pool.add(vec![x, s]);
let v = pool.add(vec![
pool.mul(vec![pool.rational(1_i32, 2_i32), xs]),
pool.mul(vec![
pool.rational(-1_i32, 2_i32),
pool.pow(xs, pool.integer(-1_i32)),
]),
]);
assert!(
verify_antiderivative_status(v, dv, x, &pool).is_some(),
"premise: the Euler form really is an antiderivative of dv"
);
let got = normalize_v(v, dv, x, &pool);
assert!(
node_count(got, &pool) < node_count(v, &pool),
"expected a smaller v, got {}",
pool.display(got)
);
assert!(
verify_antiderivative_status(got, dv, x, &pool).is_some(),
"the normalised v must still be an antiderivative of dv"
);
}
#[test]
fn combine_radicals_merges_a_quotient_of_roots() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let x2 = pool.pow(x, pool.integer(2_i32));
let x4 = pool.pow(x, pool.integer(4_i32));
let a = pool.func(
"sqrt",
vec![pool.add(vec![
pool.integer(1_i32),
pool.mul(vec![pool.integer(-1_i32), x4]),
])],
);
let b = pool.func(
"sqrt",
vec![pool.add(vec![
pool.integer(1_i32),
pool.mul(vec![pool.integer(-1_i32), x2]),
])],
);
let e = pool.mul(vec![a, pool.pow(b, pool.integer(-1_i32))]);
let got = combine_radicals(e, x, &pool).expect("two radicals must merge");
let want = pool.func("sqrt", vec![pool.add(vec![pool.integer(1_i32), x2])]);
assert!(
verify_same_value(got, want, x, &pool),
"expected √(1+x²), got {}",
pool.display(got)
);
}
fn verify_same_value(a: ExprId, b: ExprId, var: ExprId, pool: &ExprPool) -> bool {
let mut checked = 0;
for xv in [0.1237_f64, 0.3719, 0.5431, 0.7913] {
let mut env = HashMap::new();
env.insert(var, xv);
let (Some(av), Some(bv)) = (
crate::jit::eval_interp(a, &env, pool),
crate::jit::eval_interp(b, &env, pool),
) else {
return false;
};
if !av.is_finite() || !bv.is_finite() {
continue;
}
if (av - bv).abs() > 1e-9 * (1.0 + av.abs()) {
return false;
}
checked += 1;
}
checked >= 2
}
#[test]
fn a_non_elementary_sub_integral_does_not_escape() {
use crate::errors::AlkahestError;
use crate::parse::parse;
use std::collections::HashMap as Map;
let pool = p();
let x = pool.symbol("x", Domain::Real);
let mut syms: Map<String, ExprId> = Map::from([("x".to_owned(), x)]);
let sub = parse("x/sqrt(1-x^4)", &pool, &mut syms).expect("parses");
if let Err(e) = integrate(sub, x, &pool) {
if matches!(e, IntegrationError::NonElementary(_)) {
assert!(
close_integral(sub, x, &pool).is_none()
|| close_integral(sub, x, &pool).is_some(),
"close_integral must return an Option, never an error"
);
}
}
let mut syms: Map<String, ExprId> = Map::from([("x".to_owned(), x)]);
let f = parse("asin(x)/(1+x^2)^(3/2)", &pool, &mut syms).expect("parses");
match integrate_by_parts(f, x, &pool) {
ByPartsOutcome::Solved(r) => {
assert!(verify_antiderivative_status(r, f, x, &pool).is_some())
}
out @ ByPartsOutcome::Declined(_) => {
let err = out.into_result().unwrap_err();
assert_eq!(
err.code(),
"E-INT-001",
"a sub-integral's NonElementary must not become the caller's verdict"
);
}
}
}
#[test]
fn extract_square_monomial_pulls_x_out_of_the_radicand() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let r = pool.add(vec![
pool.pow(x, pool.integer(4_i32)),
pool.pow(x, pool.integer(2_i32)),
]);
let (outside, radicand) = extract_square_monomial(r, x, &pool);
assert_eq!(outside, x);
let want = pool.add(vec![pool.pow(x, pool.integer(2_i32)), pool.integer(1_i32)]);
assert!(
verify_same_value(radicand, want, x, &pool),
"expected x^2+1, got {}",
pool.display(radicand)
);
let (o2, r2) = extract_square_monomial(want, x, &pool);
assert!(is_one(o2, &pool));
assert_eq!(r2, want);
}
#[test]
fn charlwood_21_closes_and_verifies() {
use crate::parse::parse;
use std::collections::HashMap as Map;
let pool = p();
let x = pool.symbol("x", Domain::Real);
let mut syms: Map<String, ExprId> = Map::from([("x".to_owned(), x)]);
let f = parse("x^3*asin(x)/sqrt(1-x^4)", &pool, &mut syms).expect("parses");
let _ = solved(f, x, &pool);
}
#[test]
fn charlwood_35_is_refused_because_its_candidate_has_a_domain_hole() {
use crate::parse::parse;
use std::collections::HashMap as Map;
let pool = p();
let x = pool.symbol("x", Domain::Real);
let mut syms: Map<String, ExprId> = Map::from([("x".to_owned(), x)]);
let f = parse("x*acos(1/x)/sqrt(-1+x^2)", &pool, &mut syms).expect("parses");
assert!(
integrate_by_parts(f, x, &pool).is_declined(),
"#35's candidate is undefined for x < −1, where the integrand is finite, \
and must be refused"
);
}
#[test]
fn charlwood_22_is_refused_because_its_candidate_is_one_sided() {
use crate::parse::parse;
use std::collections::HashMap as Map;
let pool = p();
let x = pool.symbol("x", Domain::Real);
let mut syms: Map<String, ExprId> = Map::from([("x".to_owned(), x)]);
let f = parse("x^3*acos(1/x)/sqrt(-1+x^4)", &pool, &mut syms).expect("parses");
assert!(
integrate_by_parts(f, x, &pool).is_declined(),
"#22's candidate is only right on one branch and must be refused"
);
}
#[test]
fn snap_rational_is_tight() {
assert_eq!(snap_rational(-1.0), Some((-1, 1)));
assert_eq!(snap_rational(0.5), Some((1, 2)));
assert_eq!(snap_rational(2.0), Some((2, 1)));
assert_eq!(snap_rational(std::f64::consts::PI), None);
assert_eq!(snap_rational(0.0), None);
}
}