use symplex::prelude::*;
fn eval_at_int(expr: &Ex, var: &Ex, val: i64) -> Option<f64> {
expr.subs_i64(var, val).eval().eval_f64().ok()
}
#[test]
fn apart_1_over_x2_plus_1_unchanged() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = 1 / (&x.powi(2) + 1);
let decomposed = expr.partial_fractions(&x);
let orig_s = format!("{expr}");
let dec_s = format!("{decomposed}");
let orig_val = eval_at_int(&expr, &x, 2).unwrap();
let dec_val = eval_at_int(&decomposed, &x, 2).unwrap();
assert!(
(orig_val - dec_val).abs() < 1e-12,
"apart(1/(x²+1)) should be numerically equivalent: orig={orig_val}, dec={dec_val}, orig_s={orig_s}, dec_s={dec_s}"
);
}
#[test]
fn apart_1_over_x2_minus_1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = 1 / (&x.powi(2) - 1);
let decomposed = expr.partial_fractions(&x);
let s = format!("{decomposed}");
assert_ne!(s, format!("{expr}"), "should decompose: {s}");
let orig_val = eval_at_int(&expr, &x, 2).unwrap();
let dec_val = eval_at_int(&decomposed, &x, 2).unwrap();
assert!(
(orig_val - dec_val).abs() < 1e-12,
"decomposition must match at x=2: {orig_val} vs {dec_val}"
);
let orig_val = eval_at_int(&expr, &x, 3).unwrap();
let dec_val = eval_at_int(&decomposed, &x, 3).unwrap();
assert!(
(orig_val - dec_val).abs() < 1e-12,
"decomposition must match at x=3: {orig_val} vs {dec_val}"
);
}
#[test]
fn apart_1_over_x3_minus_1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = 1 / (&x.powi(3) - 1);
let decomposed = expr.partial_fractions(&x);
let s = format!("{decomposed}");
assert_ne!(s, format!("{expr}"), "should decompose 1/(x³-1): {s}");
let orig_val = eval_at_int(&expr, &x, 2).unwrap();
let dec_val = eval_at_int(&decomposed, &x, 2).unwrap();
assert!(
(orig_val - dec_val).abs() < 1e-10,
"1/(x³-1) decomposition must match at x=2: {orig_val} vs {dec_val}"
);
let orig_val = eval_at_int(&expr, &x, 3).unwrap();
let dec_val = eval_at_int(&decomposed, &x, 3).unwrap();
assert!(
(orig_val - dec_val).abs() < 1e-10,
"1/(x³-1) decomposition must match at x=3: {orig_val} vs {dec_val}"
);
}
#[test]
fn apart_1_over_x5_plus_1_decomposes() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = 1 / (&x.powi(5) + 1);
let decomposed = expr.partial_fractions(&x);
let s = format!("{decomposed}");
assert_ne!(
s,
format!("{expr}"),
"should decompose 1/(x⁵+1) into at least linear + quartic: {s}"
);
let orig_val = eval_at_int(&expr, &x, 2).unwrap();
let dec_val = eval_at_int(&decomposed, &x, 2).unwrap();
assert!(
(orig_val - dec_val).abs() < 1e-10,
"1/(x⁵+1) decomposition must match at x=2: {orig_val} vs {dec_val}"
);
let orig_val = eval_at_int(&expr, &x, 0).unwrap();
let dec_val = eval_at_int(&decomposed, &x, 0).unwrap();
assert!(
(orig_val - dec_val).abs() < 1e-10,
"1/(x⁵+1) decomposition must match at x=0: {orig_val} vs {dec_val}"
);
}
#[test]
fn apart_x_over_x4_plus_x2_plus_1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let denom = &x.powi(4) + &x.powi(2) + 1;
let expr = &x / &denom;
let decomposed = expr.partial_fractions(&x);
let s = format!("{decomposed}");
assert_ne!(s, format!("{expr}"), "x/(x⁴+x²+1) should decompose: {s}");
let orig_val = eval_at_int(&expr, &x, 1).unwrap();
let dec_val = eval_at_int(&decomposed, &x, 1).unwrap();
assert!(
(orig_val - dec_val).abs() < 1e-10,
"x/(x⁴+x²+1) must match at x=1: {orig_val} vs {dec_val}"
);
let orig_val = eval_at_int(&expr, &x, 2).unwrap();
let dec_val = eval_at_int(&decomposed, &x, 2).unwrap();
assert!(
(orig_val - dec_val).abs() < 1e-10,
"x/(x⁴+x²+1) must match at x=2: {orig_val} vs {dec_val}"
);
}
#[test]
fn apart_2x_plus_3_over_x2_plus_x_plus_1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let numer = &x * 2 + 3;
let denom = &x.powi(2) + &x + 1;
let expr = &numer / &denom;
let decomposed = expr.partial_fractions(&x);
let orig_val = eval_at_int(&expr, &x, 2).unwrap();
let dec_val = eval_at_int(&decomposed, &x, 2).unwrap();
assert!(
(orig_val - dec_val).abs() < 1e-10,
"(2x+3)/(x²+x+1) should be equivalent: {orig_val} vs {dec_val}"
);
}
#[test]
fn apart_repeated_linear_factor() {
let ctx = Context::new();
let x = ctx.symbol("x");
let numer = &x * 2 + 3;
let denom = (&x + 1).powi(2);
let expr = &numer / &denom;
let decomposed = expr.partial_fractions(&x);
let s = format!("{decomposed}");
assert_ne!(s, format!("{expr}"), "(2x+3)/(x+1)² should decompose: {s}");
let orig_val = eval_at_int(&expr, &x, 2).unwrap();
let dec_val = eval_at_int(&decomposed, &x, 2).unwrap();
assert!(
(orig_val - dec_val).abs() < 1e-10,
"(2x+3)/(x+1)² must match at x=2: {orig_val} vs {dec_val}"
);
let orig_val = eval_at_int(&expr, &x, 0).unwrap();
let dec_val = eval_at_int(&decomposed, &x, 0).unwrap();
assert!(
(orig_val - dec_val).abs() < 1e-10,
"(2x+3)/(x+1)² must match at x=0: {orig_val} vs {dec_val}"
);
}
#[test]
fn apart_1_over_x3_minus_1_three_point_verification() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = 1 / (&x.powi(3) - 1);
let decomposed = expr.partial_fractions(&x);
for val in [2, 3, 4, 5, 10] {
let orig = eval_at_int(&expr, &x, val).unwrap();
let dec = eval_at_int(&decomposed, &x, val).unwrap();
assert!(
(orig - dec).abs() < 1e-10,
"1/(x³-1) at x={val}: orig={orig}, dec={dec}"
);
}
}
#[test]
fn apart_x6_minus_1_factored() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = 1 / (&x.powi(6) - 1);
let decomposed = expr.partial_fractions(&x);
let s = format!("{decomposed}");
assert_ne!(s, format!("{expr}"), "1/(x⁶-1) should decompose: {s}");
let orig_val = eval_at_int(&expr, &x, 2).unwrap();
let dec_val = eval_at_int(&decomposed, &x, 2).unwrap();
assert!(
(orig_val - dec_val).abs() < 1e-10,
"1/(x⁶-1) must match at x=2: {orig_val} vs {dec_val}"
);
}
#[test]
fn apart_repeated_and_coprime() {
let ctx = Context::new();
let x = ctx.symbol("x");
let denom = (&x + 1).powi(2) * (&x - 1);
let expr = 1 / &denom;
let decomposed = expr.partial_fractions(&x);
let s = format!("{decomposed}");
assert_ne!(
s,
format!("{expr}"),
"1/((x+1)²(x-1)) should decompose: {s}"
);
let orig_val = eval_at_int(&expr, &x, 2).unwrap();
let dec_val = eval_at_int(&decomposed, &x, 2).unwrap();
assert!(
(orig_val - dec_val).abs() < 1e-10,
"1/((x+1)²(x-1)) must match at x=2: {orig_val} vs {dec_val}"
);
let orig_val = eval_at_int(&expr, &x, 3).unwrap();
let dec_val = eval_at_int(&decomposed, &x, 3).unwrap();
assert!(
(orig_val - dec_val).abs() < 1e-10,
"1/((x+1)²(x-1)) must match at x=3: {orig_val} vs {dec_val}"
);
}
#[test]
fn integrate_1_over_x2_plus_1_is_atan() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = 1 / (&x.powi(2) + 1);
let anti = expr.integrate(&x);
let s = format!("{anti}");
assert!(s.contains("atan"), "∫ 1/(x²+1) dx should be atan(x): {s}");
let f1 = eval_at_int(&anti, &x, 1);
let f0 = eval_at_int(&anti, &x, 0);
if let (Some(f1), Some(f0)) = (f1, f0) {
let val = f1 - f0;
assert!(
(val - std::f64::consts::FRAC_PI_4).abs() < 1e-10,
"∫₀¹ 1/(x²+1) dx should be π/4 ≈ 0.7854: got {val}"
);
}
}
#[test]
fn integrate_1_over_x2_minus_1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = 1 / (&x.powi(2) - 1);
let anti = expr.integrate(&x);
let s = format!("{anti}");
assert!(s.contains("ln"), "∫ 1/(x²-1) dx should contain ln: {s}");
let f3 = eval_at_int(&anti, &x, 3);
let f2 = eval_at_int(&anti, &x, 2);
if let (Some(f3), Some(f2)) = (f3, f2) {
let val = f3 - f2;
let expected = 0.5 * (1.5_f64).ln();
assert!(
(val - expected).abs() < 1e-8,
"∫₂³ 1/(x²-1) dx should be ≈ {expected}: got {val}"
);
}
}
#[test]
fn integrate_1_over_x3_minus_1_numerically() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = 1 / (&x.powi(3) - 1);
let anti = expr.integrate(&x);
let s = format!("{anti}");
let has_ln = s.contains("ln");
let has_atan = s.contains("atan");
let f3 = eval_at_int(&anti, &x, 3);
let f2 = eval_at_int(&anti, &x, 2);
if let (Some(f3), Some(f2)) = (f3, f2) {
let val = f3 - f2;
let expected = 0.07539; assert!(
(val - expected).abs() < 0.01,
"∫₂³ 1/(x³-1) dx ≈ {expected}: got {val} (antiderivative: {s})"
);
} else {
assert!(
has_ln || has_atan,
"∫ 1/(x³-1) dx should have ln or atan: {s}"
);
}
}
#[test]
fn integrate_x_over_x4_plus_x2_plus_1_numerically() {
let ctx = Context::new();
let x = ctx.symbol("x");
let denom = &x.powi(4) + &x.powi(2) + 1;
let expr = &x / &denom;
let anti = expr.integrate(&x);
let s = format!("{anti}");
let f1 = eval_at_int(&anti, &x, 1);
let f0 = eval_at_int(&anti, &x, 0);
if let (Some(f1), Some(f0)) = (f1, f0) {
let val = f1 - f0;
let expected = std::f64::consts::PI / (6.0 * 3.0_f64.sqrt());
assert!(
(val - expected).abs() < 0.01,
"∫₀¹ x/(x⁴+x²+1) dx ≈ {expected}: got {val} (form: {s})"
);
} else {
assert!(
s.contains("atan") || s.contains("ln"),
"∫ x/(x⁴+x²+1) dx should contain atan/ln: {s}"
);
}
}
#[test]
fn integrate_2x_plus_3_over_x2_plus_x_plus_1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let numer = &x * 2 + 3;
let denom = &x.powi(2) + &x + 1;
let expr = &numer / &denom;
let anti = expr.integrate(&x);
let s = format!("{anti}");
assert!(
!s.contains("Integral"),
"∫ (2x+3)/(x²+x+1) dx should be evaluated: {s}"
);
let f1 = eval_at_int(&anti, &x, 1);
let f0 = eval_at_int(&anti, &x, 0);
if let (Some(f1), Some(f0)) = (f1, f0) {
let val = f1 - f0;
assert!(
val > 1.5 && val < 3.5,
"∫₀¹ (2x+3)/(x²+x+1) dx should be ≈ 2.3: got {val}"
);
}
}
#[test]
fn integrate_1_over_x_plus_1_squared() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = 1 / (&x + 1).powi(2);
let anti = expr.integrate(&x);
let f3 = eval_at_int(&anti, &x, 3);
let f1 = eval_at_int(&anti, &x, 1);
if let (Some(f3), Some(f1)) = (f3, f1) {
let val = f3 - f1;
assert!(
(val - 0.25).abs() < 1e-10,
"∫₁³ 1/(x+1)² dx = 0.25: got {val}"
);
}
}
#[test]
fn integrate_repeated_and_coprime_1_over_x_plus_1_sq_times_x_minus_1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let denom = (&x + 1).powi(2) * (&x - 1);
let expr = 1 / &denom;
let anti = expr.integrate(&x);
let s = format!("{anti}");
assert!(
!s.contains("Integral"),
"∫ 1/((x+1)²(x-1)) dx should evaluate: {s}"
);
let f3 = eval_at_int(&anti, &x, 3);
let f2 = eval_at_int(&anti, &x, 2);
if let (Some(f3), Some(f2)) = (f3, f2) {
let val = f3 - f2;
assert!(
val.abs() < 1.0,
"∫₂³ 1/((x+1)²(x-1)) dx should be small: got {val}"
);
}
}
#[test]
fn apart_does_not_regress_simple_inverse() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = 1 / &x;
let anti = expr.integrate(&x);
let s = format!("{anti}");
assert!(s.contains("ln"), "∫ 1/x dx should be ln: {s}");
}
#[test]
fn apart_does_not_regress_polynomial() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.powi(2) + &x;
let anti = expr.integrate(&x);
let f1 = eval_at_int(&anti, &x, 1);
let f0 = eval_at_int(&anti, &x, 0);
if let (Some(f1), Some(f0)) = (f1, f0) {
let val = f1 - f0;
assert!(
(val - 5.0 / 6.0).abs() < 1e-10,
"∫₀¹ (x²+x) dx = 5/6: got {val}"
);
}
}
#[test]
fn apart_with_polynomial_quotient() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.powi(3) / (&x.powi(2) - 1);
let decomposed = expr.partial_fractions(&x);
let orig_val = eval_at_int(&expr, &x, 2).unwrap(); let dec_val = eval_at_int(&decomposed, &x, 2).unwrap();
assert!(
(orig_val - dec_val).abs() < 1e-10,
"x³/(x²-1) apart must match at x=2: {orig_val} vs {dec_val}"
);
}
#[test]
fn apart_1_over_x5_plus_1_not_0_139() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = 1 / (&x.powi(5) + 1);
let decomposed = expr.partial_fractions(&x);
let s = format!("{decomposed}");
assert_ne!(
s,
format!("{expr}"),
"apart(1/(x⁵+1)) should decompose: {s}"
);
let orig = eval_at_int(&expr, &x, 1).unwrap(); let dec = eval_at_int(&decomposed, &x, 1).unwrap();
assert!(
(orig - dec).abs() < 1e-10,
"apart decomposition of 1/(x⁵+1) must equal original at x=1: {orig} vs {dec}"
);
}
#[test]
fn apart_x4_minus_1_four_factors() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = 1 / (&x.powi(4) - 1);
let decomposed = expr.partial_fractions(&x);
let s = format!("{decomposed}");
assert_ne!(s, format!("{expr}"), "1/(x⁴-1) should decompose: {s}");
let orig_val = eval_at_int(&expr, &x, 2).unwrap();
let dec_val = eval_at_int(&decomposed, &x, 2).unwrap();
assert!(
(orig_val - dec_val).abs() < 1e-10,
"1/(x⁴-1) must match at x=2: {orig_val} vs {dec_val}"
);
let orig_val = eval_at_int(&expr, &x, 3).unwrap();
let dec_val = eval_at_int(&decomposed, &x, 3).unwrap();
assert!(
(orig_val - dec_val).abs() < 1e-10,
"1/(x⁴-1) must match at x=3: {orig_val} vs {dec_val}"
);
}
#[test]
fn integrate_1_over_x4_minus_1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = 1 / (&x.powi(4) - 1);
let anti = expr.integrate(&x);
let s = format!("{anti}");
let has_ln = s.contains("ln");
let has_atan = s.contains("atan");
let f3 = eval_at_int(&anti, &x, 3);
let f2 = eval_at_int(&anti, &x, 2);
if let (Some(f3), Some(f2)) = (f3, f2) {
let val = f3 - f2;
assert!(
val.abs() < 0.5,
"∫₂³ 1/(x⁴-1) dx should be small: got {val}"
);
} else {
assert!(
has_ln || has_atan,
"∫ 1/(x⁴-1) dx should have ln or atan: {s}"
);
}
}
#[test]
fn apart_1_over_x2_plus_1_squared_trivial() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = 1 / (&x.powi(2) + 1).powi(2);
let decomposed = expr.partial_fractions(&x);
let orig_val = eval_at_int(&expr, &x, 1).unwrap(); let dec_val = eval_at_int(&decomposed, &x, 1).unwrap();
assert!(
(orig_val - dec_val).abs() < 1e-10,
"1/(x²+1)² apart must match at x=1: {orig_val} vs {dec_val}"
);
}
#[test]
fn integrate_1_over_x3_minus_1_has_both_ln_and_atan() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = 1 / (&x.powi(3) - 1);
let anti = expr.integrate(&x);
let s = format!("{anti}");
assert!(s.contains("ln"), "∫ 1/(x³-1) dx should contain ln: {s}");
assert!(s.contains("atan"), "∫ 1/(x³-1) dx should contain atan: {s}");
}
#[test]
fn integrate_1_over_x3_minus_1_definite_2_to_5() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = 1 / (&x.powi(3) - 1);
let anti = expr.integrate(&x);
let f5 = eval_at_int(&anti, &x, 5);
let f2 = eval_at_int(&anti, &x, 2);
if let (Some(f5), Some(f2)) = (f5, f2) {
let val = f5 - f2;
assert!(
val > 0.0 && val < 0.5,
"∫₂⁵ 1/(x³-1) dx should be positive and small: {val}"
);
}
}
#[test]
fn apart_constant_denom_unchanged() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = ctx.rational(1, 3);
let decomposed = expr.partial_fractions(&x);
let s = format!("{decomposed}");
assert_eq!(s, "1/3", "constant should be unchanged: {s}");
}
#[test]
fn apart_non_rational_unchanged() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.sin() / &x;
let decomposed = expr.partial_fractions(&x);
let orig_val = eval_at_int(&expr, &x, 1);
let dec_val = eval_at_int(&decomposed, &x, 1);
if let (Some(o), Some(d)) = (orig_val, dec_val) {
assert!(
(o - d).abs() < 1e-10,
"sin(x)/x apart should not change value: {o} vs {d}"
);
}
}
#[test]
fn apart_high_degree_multiple_factors() {
let ctx = Context::new();
let x = ctx.symbol("x");
let denom = &x.powi(4) + &x.powi(2) * 5 + 6;
let expr = 1 / &denom;
let decomposed = expr.partial_fractions(&x);
let s = format!("{decomposed}");
assert_ne!(s, format!("{expr}"), "1/(x⁴+5x²+6) should decompose: {s}");
let orig_val = eval_at_int(&expr, &x, 1).unwrap();
let dec_val = eval_at_int(&decomposed, &x, 1).unwrap();
assert!(
(orig_val - dec_val).abs() < 1e-10,
"1/(x⁴+5x²+6) must match at x=1: {orig_val} vs {dec_val}"
);
}
#[test]
fn integrate_1_over_x4_plus_5x2_plus_6() {
let ctx = Context::new();
let x = ctx.symbol("x");
let denom = &x.powi(4) + &x.powi(2) * 5 + 6;
let expr = 1 / &denom;
let anti = expr.integrate(&x);
let s = format!("{anti}");
assert!(
s.contains("atan"),
"∫ 1/(x⁴+5x²+6) dx should contain atan: {s}"
);
let f1 = eval_at_int(&anti, &x, 1);
let f0 = eval_at_int(&anti, &x, 0);
if let (Some(f1), Some(f0)) = (f1, f0) {
let val = f1 - f0;
assert!(
(val - 0.1329).abs() < 0.01,
"∫₀¹ 1/(x⁴+5x²+6) dx ≈ 0.133: got {val}"
);
}
}