#![allow(clippy::excessive_precision)]
use symplex::prelude::*;
const POINTS: [(i64, i64); 4] = [(1, 3), (-5, 7), (13, 11), (-17, 5)];
fn close(actual: f64, expected: f64, rel: f64, label: &str) {
if expected.abs() < 1e-15 && actual.abs() < 1e-15 {
return;
}
let scale = expected.abs().max(1e-300);
assert!(
(actual - expected).abs() <= rel * scale,
"{label}: got {actual:e}, expected {expected:e} (relative error {:e})",
(actual - expected).abs() / scale
);
}
fn assert_antiderivative(f: &Ex, x: &Ex, f_ref: &[f64; 4], label: &str) -> Ex {
let big_f = f.integrate(x);
assert!(
!big_f.has_unevaluated(),
"{label}: expected a closed form, got {big_f}"
);
let d_big_f = big_f.diff(x);
let ctx = x.context();
for (&(n, d), &expected) in POINTS.iter().zip(f_ref) {
let p = ctx.rational(n, d);
let f_at = f
.subs(x, &p)
.eval_f64()
.unwrap_or_else(|e| panic!("{label}: f({n}/{d}) did not evaluate: {e}"));
close(
f_at,
expected,
1e-12,
&format!("{label}: f({n}/{d}) vs sympy"),
);
let df_at = d_big_f
.subs(x, &p)
.eval_f64()
.unwrap_or_else(|e| panic!("{label}: F'({n}/{d}) did not evaluate: {e} (F = {big_f})"));
close(
df_at,
expected,
1e-10,
&format!("{label}: F'({n}/{d}) vs f, F = {big_f}"),
);
}
big_f
}
fn assert_derivative_simplifies_to_f(f: &Ex, big_f: &Ex, x: &Ex, label: &str) {
let residual = (big_f.diff(x) - f).simplify();
assert_eq!(
residual.to_string(),
"0",
"{label}: (F' - f).simplify() should be 0, F = {big_f}"
);
}
#[test]
fn sqrt_of_x_squared_integrates_as_abs_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = x.pow(&ctx.int(2)).sqrt();
let big_f = assert_antiderivative(
&f,
&x,
&[
0.33333333333333333,
0.71428571428571429,
1.1818181818181818,
3.4000000000000000,
],
"∫ sqrt(x^2)",
);
assert_ne!(big_f.to_string(), "1/2*x^2", "∫ sqrt(x^2) must not be ∫ x");
}
#[test]
fn neg_sqrt_of_x_squared_integrates_as_neg_abs_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = -x.pow(&ctx.int(2)).sqrt();
assert_antiderivative(
&f,
&x,
&[
-0.33333333333333333,
-0.71428571428571429,
-1.1818181818181818,
-3.4000000000000000,
],
"∫ -sqrt(x^2)",
);
}
#[test]
fn x_squared_to_three_halves_is_abs_x_cubed() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = x.pow(&ctx.int(2)).pow(&ctx.rational(3, 2));
assert_antiderivative(
&f,
&x,
&[
0.037037037037037037,
0.36443148688046647,
1.6506386175807663,
39.304000000000000,
],
"∫ (x^2)^(3/2)",
);
}
#[test]
fn sqrt_of_x_fourth_flattens_to_x_squared() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = x.pow(&ctx.int(4)).sqrt();
let big_f = assert_antiderivative(
&f,
&x,
&[
0.11111111111111111,
0.51020408163265306,
1.3966942148760331,
11.560000000000000,
],
"∫ sqrt(x^4)",
);
assert_eq!(big_f.to_string(), "1/3*x^3");
}
#[test]
fn reciprocal_sqrt_flattening_still_reaches_asinh() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = ctx.int(1) / (x.pow(&ctx.int(2)) + 1).sqrt();
let big_f = assert_antiderivative(
&f,
&x,
&[
0.94868329805051380,
0.81373347120673496,
0.64594224146617384,
0.28216632399155017,
],
"∫ 1/sqrt(x^2+1)",
);
assert_eq!(big_f.to_string(), "asinh(x)");
}
#[test]
fn neg_of_product_sin_plus_one_times_cos() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = -((x.sin() + 1) * x.cos());
let big_f = assert_antiderivative(
&f,
&x,
&[
-1.2541418478496062,
-0.26060980851463465,
-0.73015561900524185,
1.2138548681487652,
],
"∫ -(sin x + 1) cos x",
);
assert_derivative_simplifies_to_f(&f, &big_f, &x, "∫ -(sin x + 1) cos x");
}
#[test]
fn three_times_cos_minus_four_times_sin() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = (x.cos() - 4) * x.sin() * 3;
let big_f = assert_antiderivative(
&f,
&x,
&[
-2.9987816569492214,
6.3760801515840370,
-10.050827316564941,
-3.8076632510298883,
],
"∫ 3 (cos x - 4) sin x",
);
assert_derivative_simplifies_to_f(&f, &big_f, &x, "∫ 3 (cos x - 4) sin x");
}
#[test]
fn minus_two_times_cos_minus_four_times_sin() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = (x.cos() - 4) * x.sin() * -2;
let big_f = assert_antiderivative(
&f,
&x,
&[
1.9991877712994809,
-4.2507201010560247,
6.7005515443766276,
2.5384421673532589,
],
"∫ -2 (cos x - 4) sin x",
);
assert_derivative_simplifies_to_f(&f, &big_f, &x, "∫ -2 (cos x - 4) sin x");
}
#[test]
fn three_cos_over_sin_squared_plus_one() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = x.cos() * 3 / (x.sin().pow(&ctx.int(2)) + 1);
let big_f = assert_antiderivative(
&f,
&x,
&[
2.5607285380951193,
1.5860619515432745,
0.61294300386480045,
-2.7226050514833956,
],
"∫ 3 cos x/(sin²x + 1)",
);
assert_eq!(big_f.to_string(), "3*atan(sin(x))");
}
#[test]
fn cos_over_sin_squared_plus_one_is_atan_sin() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = x.cos() / (x.sin().pow(&ctx.int(2)) + 1);
let big_f = assert_antiderivative(
&f,
&x,
&[
0.85357617936503976,
0.52868731718109150,
0.20431433462160015,
-0.90753501716113186,
],
"∫ cos x/(sin²x + 1)",
);
assert_eq!(big_f.to_string(), "atan(sin(x))");
assert_derivative_simplifies_to_f(&f, &big_f, &x, "∫ cos x/(sin²x + 1)");
}
#[test]
fn minus_two_cos_over_sin_squared_plus_four() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = x.cos() * -2 / (x.sin().pow(&ctx.int(2)) + 4);
let big_f = assert_antiderivative(
&f,
&x,
&[
-0.46016263779896368,
-0.34117844800441081,
-0.15619005457761024,
0.47563421846387038,
],
"∫ -2 cos x/(sin²x + 4)",
);
assert_eq!(big_f.to_string(), "-atan(1/2*sin(x))");
}
#[test]
fn three_sin_over_scaled_cos_squared_plus_four() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = x.sin() * 3 / (x.cos().pow(&ctx.int(2)) * ctx.rational(9, 4) + 4);
let big_f = assert_antiderivative(
&f,
&x,
&[
0.16334897170264714,
-0.37188892601949924,
0.64203134933413098,
0.12561268880521519,
],
"∫ 3 sin x/(9/4 cos²x + 4)",
);
assert_eq!(big_f.to_string(), "-atan(3/4*cos(x))");
}
#[test]
fn one_over_sin_squared_plus_four() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = ctx.int(1) / (x.sin().pow(&ctx.int(2)) + 4);
assert_antiderivative(
&f,
&x,
&[
0.24348338810226434,
0.22577812476394662,
0.20592339995226665,
0.24598423027398144,
],
"∫ 1/(sin²x + 4)",
);
}
#[test]
fn sin_over_cos_squared_plus_one_unchanged() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = x.sin() / (x.cos().pow(&ctx.int(2)) + 1);
let big_f = assert_antiderivative(
&f,
&x,
&[
0.17284967790034225,
-0.41701520021118986,
0.80894963134186558,
0.13208314868872689,
],
"∫ sin x/(cos²x + 1)",
);
assert_eq!(big_f.to_string(), "-atan(cos(x))");
}
#[test]
fn one_over_x_squared_minus_four_squared_plus_one() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = ctx.int(1) / ((x.pow(&ctx.int(2)) - 4).pow(&ctx.int(2)) + 1);
let big_f = assert_antiderivative(
&f,
&x,
&[
0.062021439509954058,
0.075880159281967006,
0.12858096358877979,
0.017195839982391460,
],
"∫ 1/((x²-4)²+1)",
);
let s = big_f.to_string();
assert!(
!s.contains("re(") && !s.contains("im("),
"closed form must not contain opaque re/im constants: {s}"
);
}
#[test]
fn ln_of_x_squared_minus_four_squared_plus_one() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = ((x.pow(&ctx.int(2)) - 4).pow(&ctx.int(2)) + 1).ln();
let big_f = assert_antiderivative(
&f,
&x,
&[
2.7802751551639376,
2.5786000347877560,
2.0511965062168838,
4.0630877859048051,
],
"∫ ln((x²-4)²+1)",
);
let s = big_f.to_string();
assert!(
!s.contains("re(") && !s.contains("im("),
"closed form must not contain opaque re/im constants: {s}"
);
}
#[test]
fn biquadratic_with_real_negative_roots_in_x_squared() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = ctx.int(1) / (x.pow(&ctx.int(4)) + x.pow(&ctx.int(2)) * 6 + 1);
assert_antiderivative(
&f,
&x,
&[
0.59558823529411765,
0.23139938319198150,
0.088254086897815499,
0.0049021145761435653,
],
"∫ 1/(x⁴+6x²+1)",
);
}
#[test]
fn biquadratic_with_cubic_numerator() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = (x.pow(&ctx.int(3)) + 1) / (x.pow(&ctx.int(4)) + x.pow(&ctx.int(2)) * 3 + 1);
assert_antiderivative(
&f,
&x,
&[
0.77064220183486239,
0.22772720489479182,
0.37119436819099178,
-0.22623108834730347,
],
"∫ (x³+1)/(x⁴+3x²+1)",
);
}
#[test]
fn one_over_x_fourth_plus_one_unchanged() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = ctx.int(1) / (x.pow(&ctx.int(4)) + 1);
let big_f = assert_antiderivative(
&f,
&x,
&[
0.98780487804878049,
0.79345670852610707,
0.33889634739132448,
0.0074275663727331067,
],
"∫ 1/(x⁴+1)",
);
let s = big_f.to_string();
assert!(
s.contains("atan") && s.contains("ln"),
"expected atan/ln form: {s}"
);
}
#[test]
fn x_cubed_over_shifted_biquadratic() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = x.pow(&ctx.int(3)) / (x.pow(&ctx.int(4)) - x.pow(&ctx.int(2)) * 8 + 17);
assert_antiderivative(
&f,
&x,
&[
0.0022970903522205207,
-0.027653119271853865,
0.21224070398538633,
-0.67586529466791394,
],
"∫ x³/(x⁴-8x²+17)",
);
}
#[test]
fn general_quartic_is_never_silently_wrong() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = ctx.int(1) / (x.pow(&ctx.int(4)) + &x + 1);
let big_f = f.integrate(&x);
if big_f.has_unevaluated() {
return;
}
let d_big_f = big_f.diff(&x);
let f_ref = [
0.74311926605504587,
1.8314263920671243,
0.24198000165275597,
0.0076199997561600078,
];
for (&(n, d), &expected) in POINTS.iter().zip(&f_ref) {
let p = ctx.rational(n, d);
let df_at = d_big_f
.subs(&x, &p)
.eval_f64()
.unwrap_or_else(|e| panic!("F'({n}/{d}) did not evaluate: {e} (F = {big_f})"));
close(
df_at,
expected,
1e-10,
&format!("∫ 1/(x⁴+x+1): F'({n}/{d}), F = {big_f}"),
);
}
}
#[test]
fn u_substitution_rejects_factor_with_bare_var() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = (&x + x.sin().pow(&ctx.int(2))) * x.cos();
let big_f = f.integrate(&x);
if big_f.has_unevaluated() {
return;
}
let d_big_f = big_f.diff(&x);
let f_ref = [
0.41614930888322871,
-0.21545486337458458,
0.77289471651060822,
3.2239807196321021,
];
for (&(n, d), &expected) in POINTS.iter().zip(&f_ref) {
let p = ctx.rational(n, d);
let df_at = d_big_f
.subs(&x, &p)
.eval_f64()
.unwrap_or_else(|e| panic!("F'({n}/{d}) did not evaluate: {e} (F = {big_f})"));
close(
df_at,
expected,
1e-10,
&format!("∫ (x + sin²x) cos x: F'({n}/{d}), F = {big_f}"),
);
}
}
#[test]
fn abs_of_sqrt_is_not_integrated_as_a_sign_product() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = x.sqrt().abs();
let big_f = f.integrate(&x);
if !big_f.has_unevaluated() {
let df = big_f.diff(&x);
for v in [ctx.rational(-5, 7), ctx.rational(1, 3), ctx.rational(13, 4)] {
let fv = f.subs(&x, &v).eval_f64().unwrap();
let dv = df.subs(&x, &v).eval_f64().unwrap();
assert!(
(fv - dv).abs() < 1e-12,
"∫|√x| = {big_f}: F′({v}) = {dv}, f = {fv}"
);
}
}
let g = x.abs().integrate(&x);
assert!(!g.has_unevaluated(), "{g}");
let dg = g
.diff(&x)
.subs(&x, &ctx.rational(-5, 7))
.eval_f64()
.unwrap();
assert!((dg - 5.0 / 7.0).abs() < 1e-12, "{g}");
}
fn assert_differentiates_back(f: &Ex, big_f: &Ex, x: &Ex) {
assert!(!big_f.has_unevaluated(), "∫ {f} returned {big_f}");
let ctx = x.context();
let df = big_f.diff(x);
let mut checked = 0;
for v in [
ctx.rational(1, 3),
ctx.rational(7, 5),
ctx.rational(13, 4),
ctx.int(5),
] {
let Ok(fv) = f.subs(x, &v).eval_f64() else {
continue;
};
let dv = df.subs(x, &v).eval_f64().unwrap();
assert!(
(fv - dv).abs() <= 1e-12 * fv.abs().max(1.0),
"∫ {f} = {big_f}: F′({v}) = {dv}, f = {fv}"
);
checked += 1;
}
assert!(checked >= 2, "∫ {f}: only {checked} real sample points");
}
#[test]
fn polynomial_over_symbolic_linear_denominator() {
let ctx = Context::new();
let x = ctx.symbol("x");
let a = ctx.symbol_with("a", &[Assumption::Positive]);
for k in [ctx.pi(), ctx.int(2).sqrt(), ctx.int(2).sin()] {
for f in [&x / (&x + &k), x.powi(3) / (2 * &x + &k), 1 / (&x - &k)] {
let big_f = f.integrate(&x);
assert_differentiates_back(&f, &big_f, &x);
}
}
for f in [&x / (&x + &a), x.powi(3) / (2 * &x + &a)] {
let big_f = f.integrate(&x);
let at = |e: &Ex| e.subs(&a, &ctx.rational(5, 3));
assert_differentiates_back(&at(&f), &at(&big_f), &x);
}
let f = &x * (&x + ctx.int(2).sin()).ln();
assert_differentiates_back(&f, &f.integrate(&x), &x);
let f = (x.sqrt() + ctx.int(-2).sin()).ln();
assert_differentiates_back(&f, &f.integrate(&x), &x);
}
#[test]
fn failing_integration_search_is_bounded() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = (x.sin() + x.ln()).sqrt() * x.exp().atan();
let started = std::time::Instant::now();
let big_f = f.integrate(&x);
assert!(big_f.has_unevaluated(), "{big_f}");
assert!(
started.elapsed().as_secs() < 10,
"took {:?}",
started.elapsed()
);
}
#[test]
fn abs_of_a_function_with_poles_is_not_a_constant_sign() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = (ctx.int(-1).atan() / x.cos()).abs();
let big_f = f.integrate(&x);
if !big_f.has_unevaluated() {
assert_differentiates_back(&f, &big_f, &x);
}
let g = (&x - 2).abs();
assert_differentiates_back(&g, &g.integrate(&x), &x);
let h = x.powi(2) * (2 * &x + 1).abs();
assert_differentiates_back(&h, &h.integrate(&x), &x);
}
#[test]
fn reciprocals_inside_sums_are_rational_functions() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = x.powi(2) * (&x + x.powi(-1));
assert_eq!(f.integrate(&x), x.powi(4) / 4 + x.powi(2) / 2);
let g = &x * (&x + x.powi(-2));
assert_differentiates_back(&g, &g.integrate(&x), &x);
let p = x.powi(2) * (&x - x.powi(-3) - ctx.rational(1, 4));
let h = &p * x.abs();
assert_differentiates_back(&h, &h.integrate(&x), &x);
let k = x.abs() / x.powi(2);
let big_k = k.integrate(&x);
assert!(!format!("{big_k}").contains("nan"), "{big_k}");
if !big_k.has_unevaluated() {
assert_differentiates_back(&k, &big_k, &x);
}
}
#[test]
fn log_polynomial_with_repeated_powers_keeps_every_term() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = (&x + x.ln() - ctx.rational(3, 4)) * x.ln();
let big_f = f.integrate(&x);
assert_differentiates_back(&f, &big_f, &x);
let sympy = x.powi(2) * x.ln() / 2 - x.powi(2) / 4 + &x * x.ln().powi(2)
- ctx.rational(11, 4) * &x * x.ln()
+ ctx.rational(11, 4) * &x;
assert_eq!((big_f - sympy).expand(), ctx.int(0));
}
#[test]
fn radical_substitution_keeps_analytic_logarithms() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = x.pow(&ctx.rational(-5, 2)) * x.sqrt().atan();
let big_f = f.integrate(&x);
assert!(!big_f.has_unevaluated(), "{big_f}");
let df = big_f.diff(&x);
for v in [ctx.rational(-5, 7), ctx.rational(1, 3), ctx.rational(13, 4)] {
let fv = f.subs(&x, &v).eval_f64().unwrap();
let dv = df.subs(&x, &v).eval_f64().unwrap();
assert!(
(fv - dv).abs() <= 1e-12 * fv.abs().max(1.0),
"F′({v}) = {dv}, f = {fv}: {big_f}"
);
}
}
#[test]
fn rootof_is_a_constant_under_differentiation() {
let ctx = Context::new();
let x = ctx.symbol("x");
let roots = (x.powi(5) - &x - 1).solve(&x).unwrap();
assert_eq!(roots.len(), 5);
let r = &roots[0];
assert!(format!("{r}").contains("RootOf"), "{r}");
assert_eq!(r.diff(&x), ctx.int(0));
assert_eq!((r * &x).diff(&x), r.clone());
assert_eq!((r * x.sin()).diff(&x), r * x.cos());
}
#[test]
fn arctangent_of_a_radical_polynomial_is_fast_and_compact() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = (x.sqrt() - x.powi(3)).atan();
let started = std::time::Instant::now();
let big_f = f.integrate(&x);
let elapsed = started.elapsed();
assert!(!big_f.has_unevaluated(), "{big_f}");
assert!(
big_f.to_string().len() < 2_000,
"{} chars",
big_f.to_string().len()
);
let d = big_f.diff(&x);
for v in [ctx.rational(1, 3), ctx.rational(7, 5)] {
let fv = f.subs(&x, &v).eval_f64().unwrap();
let dv = d.subs(&x, &v).eval_complex64().unwrap();
assert!(
(dv.re - fv).abs() < 1e-10 && dv.im.abs() < 1e-10,
"F′({v}) = {dv}, f = {fv}"
);
}
assert!(elapsed.as_secs() < 20, "took {elapsed:?}");
}