mod common;
use symplex::prelude::*;
fn ftc_check(integrand: &Ex, var: &Ex, points: &[f64], tol: f64, label: &str) -> (String, bool) {
let antideriv = integrand.integrate(var);
let s = format!("{antideriv}");
let is_uneval = s.contains("Integral") || antideriv.has_unevaluated();
if is_uneval {
return (s, true);
}
let deriv = antideriv.diff(var);
let ctx = integrand.context();
let mut checked = 0usize;
for &pt_f in points {
let numer = (pt_f * 10000.0).round() as i64;
let pt = ctx.rational(numer, 10000);
let orig_val = integrand.subs(var, &pt).eval().eval_f64();
let deriv_val = deriv.subs(var, &pt).eval().eval_f64();
if let (Ok(o), Ok(d)) = (orig_val, deriv_val)
&& o.is_finite()
&& d.is_finite()
{
checked += 1;
let scale = o.abs().max(d.abs()).max(1.0);
assert!(
(o - d).abs() < tol * scale,
"FTC FAILED for {label} at {var}={pt_f}: \
integrand={o}, d/dx(antideriv)={d}, diff={}, \
antideriv='{antideriv}', deriv='{deriv}'",
(o - d).abs(),
);
}
}
if checked == 0 {
eprintln!("WARNING: FTC {label}: no evaluation points succeeded");
}
(s, false)
}
#[test]
fn integrate_tan_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let integrand = &x.sin() / &x.cos();
let (s, is_uneval) = ftc_check(&integrand, &x, &[0.3, 0.7, 1.0], 1e-6, "∫tan(x)dx");
if is_uneval {
panic!(
"BUG: ∫tan(x)dx returned unevaluated form: {s}\n\
Expected: -ln(cos(x)) or equivalent"
);
}
}
#[test]
fn integrate_sec_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let integrand = &ctx.int(1) / &x.cos();
let (s, is_uneval) = ftc_check(&integrand, &x, &[0.2, 0.5, 0.8], 1e-6, "∫sec(x)dx");
if is_uneval {
panic!(
"BUG: ∫sec(x)dx returned unevaluated form: {s}\n\
Expected: ln|sec(x)+tan(x)| or equivalent"
);
}
}
#[test]
fn integrate_x_squared_sin_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let integrand = &x.powi(2) * &x.sin();
let (s, is_uneval) = ftc_check(&integrand, &x, &[0.5, 1.0, 1.5, 2.0], 1e-6, "∫x²sin(x)dx");
if is_uneval {
panic!(
"BUG: ∫x²·sin(x)dx returned unevaluated form: {s}\n\
Expected: -x²·cos(x) + 2x·sin(x) + 2·cos(x) or equivalent"
);
}
}
#[test]
fn integrate_x_cos_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let integrand = &x * &x.cos();
let (s, is_uneval) = ftc_check(&integrand, &x, &[0.5, 1.0, 1.5, 2.0], 1e-6, "∫x·cos(x)dx");
if is_uneval {
panic!(
"BUG: ∫x·cos(x)dx returned unevaluated form: {s}\n\
Expected: x·sin(x) + cos(x) or equivalent"
);
}
}
#[test]
fn integrate_x_exp_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let integrand = &x * &x.exp();
let (s, is_uneval) = ftc_check(&integrand, &x, &[0.3, 0.7, 1.0, 1.5], 1e-6, "∫x·exp(x)dx");
if is_uneval {
panic!(
"BUG: ∫x·exp(x)dx returned unevaluated form: {s}\n\
Expected: (x-1)·exp(x) or equivalent"
);
}
}
#[test]
fn integrate_x_squared_exp_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let integrand = &x.powi(2) * &x.exp();
let (s, is_uneval) = ftc_check(&integrand, &x, &[0.3, 0.7, 1.0, 1.5], 1e-6, "∫x²·exp(x)dx");
if is_uneval {
panic!(
"BUG: ∫x²·exp(x)dx returned unevaluated form: {s}\n\
Expected: (x²-2x+2)·exp(x) or equivalent"
);
}
}
#[test]
fn integrate_one_over_one_plus_x_squared() {
let ctx = Context::new();
let x = ctx.symbol("x");
let integrand = &ctx.int(1) / &(&x.powi(2) + &ctx.int(1));
let (s, is_uneval) = ftc_check(&integrand, &x, &[0.0, 0.5, 1.0, 2.0], 1e-6, "∫1/(1+x²)dx");
if is_uneval {
panic!(
"BUG: ∫1/(1+x²)dx returned unevaluated form: {s}\n\
Expected: arctan(x) or equivalent"
);
}
}
#[test]
fn integrate_completing_the_square() {
let ctx = Context::new();
let x = ctx.symbol("x");
let denom = &(&x.powi(2) + &(&x * 2)) + &ctx.int(2);
let integrand = &ctx.int(1) / &denom;
let (s, is_uneval) = ftc_check(
&integrand,
&x,
&[0.0, 0.5, 1.0, 2.0],
1e-6,
"∫1/(x²+2x+2)dx",
);
if is_uneval {
panic!(
"BUG: ∫1/(x²+2x+2)dx returned unevaluated form: {s}\n\
Expected: arctan(x+1) or equivalent"
);
}
}
#[test]
fn integrate_sin_squared_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let integrand = x.sin().powi(2);
let (s, is_uneval) = ftc_check(&integrand, &x, &[0.3, 0.7, 1.0, 1.5], 1e-6, "∫sin²(x)dx");
if is_uneval {
panic!(
"BUG: ∫sin²(x)dx returned unevaluated form: {s}\n\
Expected: x/2 - sin(2x)/4 or equivalent"
);
}
}
#[test]
fn integrate_cos_squared_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let integrand = x.cos().powi(2);
let (s, is_uneval) = ftc_check(&integrand, &x, &[0.3, 0.7, 1.0, 1.5], 1e-6, "∫cos²(x)dx");
if is_uneval {
panic!(
"BUG: ∫cos²(x)dx returned unevaluated form: {s}\n\
Expected: x/2 + sin(2x)/4 or equivalent"
);
}
}
#[test]
fn integrate_exp_neg_x_squared_no_elementary() {
let ctx = Context::new();
let x = ctx.symbol("x");
let integrand = (-&x.powi(2)).exp();
let antideriv = integrand.integrate(&x);
let s = format!("{antideriv}");
if !antideriv.has_unevaluated() && !s.contains("Integral") {
let deriv = antideriv.diff(&x);
let pt = ctx.rational(7, 10); let orig_val = integrand.subs(&x, &pt).eval().eval_f64();
let deriv_val = deriv.subs(&x, &pt).eval().eval_f64();
if let (Ok(o), Ok(d)) = (orig_val, deriv_val) {
let scale = o.abs().max(d.abs()).max(1.0);
assert!(
(o - d).abs() < 1e-5 * scale,
"BUG: ∫exp(-x²)dx claims closed form '{s}' but FTC fails: \
integrand={o}, d/dx(result)={d}"
);
}
}
}
#[test]
fn definite_sin_0_to_pi_equals_2() {
let ctx = Context::new();
let x = ctx.symbol("x");
let result = x.sin().integrate_definite(&x, &ctx.int(0), &ctx.pi());
let evaled = result.eval();
let s = format!("{evaled}");
assert_eq!(s, "2", "∫₀^π sin(x)dx should be exactly 2, got: {s}");
}
#[test]
fn definite_x_squared_0_to_1_equals_one_third() {
let ctx = Context::new();
let x = ctx.symbol("x");
let result = x.powi(2).integrate_definite(&x, &ctx.int(0), &ctx.int(1));
let s = format!("{result}");
assert_eq!(s, "1/3", "∫₀¹ x²dx should be 1/3, got: {s}");
}
#[test]
fn definite_x_cubed_0_to_1_equals_one_quarter() {
let ctx = Context::new();
let x = ctx.symbol("x");
let result = x.powi(3).integrate_definite(&x, &ctx.int(0), &ctx.int(1));
let s = format!("{result}");
assert_eq!(s, "1/4", "∫₀¹ x³dx should be 1/4, got: {s}");
}
#[test]
fn definite_cos_0_to_pi_half_equals_1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pi_half = &ctx.pi() / &ctx.int(2);
let result = x.cos().integrate_definite(&x, &ctx.int(0), &pi_half);
let evaled = result.eval();
let s = format!("{evaled}");
assert_eq!(s, "1", "∫₀^{{π/2}} cos(x)dx should be 1, got: {s}");
}
#[test]
fn definite_exp_0_to_1_is_e_minus_1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let result = x.exp().integrate_definite(&x, &ctx.int(0), &ctx.int(1));
let evaled = result.eval();
if let Ok(v) = evaled.eval_f64() {
let expected = std::f64::consts::E - 1.0;
assert!(
(v - expected).abs() < 1e-8,
"∫₀¹ exp(x)dx should be e-1 ≈ {expected}, got: {v}"
);
} else {
let s = format!("{evaled}");
assert!(
s.contains("e") || s.contains("E") || s.contains("exp"),
"∫₀¹ exp(x)dx should be e-1, got: {s}"
);
}
}
#[test]
fn definite_sin_0_to_2pi_equals_0() {
let ctx = Context::new();
let x = ctx.symbol("x");
let two_pi = &ctx.int(2) * &ctx.pi();
let result = x.sin().integrate_definite(&x, &ctx.int(0), &two_pi);
let evaled = result.eval();
let s = format!("{evaled}");
assert_eq!(s, "0", "∫₀^{{2π}} sin(x)dx should be 0, got: {s}");
}
#[test]
fn definite_integral_additivity() {
let ctx = Context::new();
let x = ctx.symbol("x");
let full = x.integrate_definite(&x, &ctx.int(0), &ctx.int(2));
let part1 = x.integrate_definite(&x, &ctx.int(0), &ctx.int(1));
let part2 = x.integrate_definite(&x, &ctx.int(1), &ctx.int(2));
let full_v = full.eval_f64().expect("full integral evals");
let sum_v = {
let p1 = part1.eval_f64().expect("part1 evals");
let p2 = part2.eval_f64().expect("part2 evals");
p1 + p2
};
assert!(
(full_v - sum_v).abs() < 1e-10,
"∫₀² x dx = {full_v}, ∫₀¹ + ∫₁² = {sum_v} — additivity failed"
);
}
#[test]
fn definite_polynomial_high_degree() {
let ctx = Context::new();
let x = ctx.symbol("x");
let poly = &(&(&(&x.powi(4) + &x.powi(3)) + &x.powi(2)) + &x) + &ctx.int(1);
let result = poly.integrate_definite(&x, &ctx.int(0), &ctx.int(1));
let v = result.eval_f64().expect("should evaluate to f64");
let expected = 137.0 / 60.0;
assert!(
(v - expected).abs() < 1e-10,
"∫₀¹ (x⁴+x³+x²+x+1)dx should be 137/60 ≈ {expected}, got: {v}"
);
}
#[test]
fn definite_integral_reversed_bounds_negates() {
let ctx = Context::new();
let x = ctx.symbol("x");
let forward = x.powi(2).integrate_definite(&x, &ctx.int(0), &ctx.int(3));
let reversed = x.powi(2).integrate_definite(&x, &ctx.int(3), &ctx.int(0));
let fv = forward.eval_f64().expect("forward evals");
let rv = reversed.eval_f64().expect("reversed evals");
assert!(
(fv + rv).abs() < 1e-10,
"Reversing bounds should negate: forward={fv}, reversed={rv}, sum={}",
fv + rv
);
}
#[test]
fn ode_y_prime_equals_y() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let ode = &dy - &y;
let sol = ode.solve_ode(&y, &x);
let s = format!("{sol}");
assert!(
!sol.has_unevaluated(),
"BUG: y' - y = 0 should be solvable, got unevaluated: {s}"
);
assert!(
s.contains("exp"),
"Solution of y' = y should contain exp, got: {s}"
);
let particular = x.exp();
assert!(
ode.check_ode_solution(&particular, &y, &x),
"y = exp(x) should satisfy y' - y = 0"
);
}
#[test]
fn ode_y_prime_plus_y_equals_0() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let ode = &dy + &y;
let sol = ode.solve_ode(&y, &x);
let s = format!("{sol}");
assert!(
!sol.has_unevaluated(),
"BUG: y' + y = 0 should be solvable, got unevaluated: {s}"
);
assert!(
s.contains("exp"),
"Solution of y' + y = 0 should contain exp, got: {s}"
);
let particular = (-&x).exp();
assert!(
ode.check_ode_solution(&particular, &y, &x),
"y = exp(-x) should satisfy y' + y = 0"
);
}
#[test]
fn ode_y_prime_plus_y_wrong_solution_rejected() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let ode = &dy + &y;
let wrong = x.exp(); assert!(
!ode.check_ode_solution(&wrong, &y, &x),
"BUG: y = exp(x) should NOT satisfy y' + y = 0 but check_ode_solution says it does"
);
}
#[test]
fn ode_second_order_y_pp_plus_y_eq_0() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let d2y = dy.formal_diff(&x);
let ode = &d2y + &y;
let sol = ode.solve_ode(&y, &x);
let s = format!("{sol}");
assert!(
!sol.has_unevaluated(),
"BUG: y'' + y = 0 should be solvable, got unevaluated: {s}"
);
assert!(
s.contains("sin") && s.contains("cos"),
"Solution of y'' + y = 0 should have sin and cos, got: {s}"
);
assert!(
ode.check_ode_solution(&x.cos(), &y, &x),
"y = cos(x) should satisfy y'' + y = 0"
);
assert!(
ode.check_ode_solution(&x.sin(), &y, &x),
"y = sin(x) should satisfy y'' + y = 0"
);
assert!(
!ode.check_ode_solution(&x.exp(), &y, &x),
"BUG: y = exp(x) should NOT satisfy y'' + y = 0"
);
}
#[test]
fn ode_second_order_y_pp_minus_y_eq_0() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let d2y = dy.formal_diff(&x);
let ode = &d2y - &y;
let sol = ode.solve_ode(&y, &x);
let s = format!("{sol}");
assert!(
!sol.has_unevaluated(),
"BUG: y'' - y = 0 should be solvable, got unevaluated: {s}"
);
assert!(
ode.check_ode_solution(&x.exp(), &y, &x),
"y = exp(x) should satisfy y'' - y = 0"
);
assert!(
ode.check_ode_solution(&(-&x).exp(), &y, &x),
"y = exp(-x) should satisfy y'' - y = 0"
);
}
#[test]
fn ode_overdamped_y_pp_plus_3yp_plus_2y() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let d2y = dy.formal_diff(&x);
let ode = &(&d2y + &(&dy * 3)) + &(&y * 2);
let sol = ode.solve_ode(&y, &x);
let s = format!("{sol}");
assert!(
!sol.has_unevaluated(),
"BUG: y'' + 3y' + 2y = 0 should be solvable, got unevaluated: {s}"
);
let exp_neg_x = (-&x).exp();
assert!(
ode.check_ode_solution(&exp_neg_x, &y, &x),
"y = exp(-x) should satisfy y'' + 3y' + 2y = 0"
);
let exp_neg_2x = (-&x * 2).exp();
assert!(
ode.check_ode_solution(&exp_neg_2x, &y, &x),
"y = exp(-2x) should satisfy y'' + 3y' + 2y = 0"
);
}
#[test]
fn ode_classification_first_order_linear() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let ode = &dy + &y; let cls = ode.classify_ode(&y, &x);
let cls_s = format!("{cls:?}");
assert!(
cls_s != "Unknown",
"y' + y = 0 should not classify as Unknown, got: {cls_s}"
);
}
#[test]
fn ode_classification_second_order() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let d2y = dy.formal_diff(&x);
let ode = &d2y + &y; let cls = ode.classify_ode(&y, &x);
let cls_s = format!("{cls:?}");
assert!(
cls_s != "Unknown",
"y'' + y = 0 should not classify as Unknown, got: {cls_s}"
);
assert!(
cls_s.contains("SecondOrder"),
"y'' + y = 0 should classify as SecondOrder*, got: {cls_s}"
);
}
#[test]
fn ode_simple_separable_y_prime_eq_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let ode = &dy - &x;
let sol = ode.solve_ode(&y, &x);
let s = format!("{sol}");
assert!(
!sol.has_unevaluated(),
"y' - x = 0 should be solvable, got unevaluated: {s}"
);
let particular = &x.powi(2) / 2;
assert!(
ode.check_ode_solution(&particular, &y, &x),
"y = x²/2 should satisfy y' = x"
);
}
#[test]
fn ode_full_separable_y_prime_eq_xy() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let ode = &dy - &(&x * &y);
let sol = ode.solve_ode(&y, &x);
let s = format!("{sol}");
assert!(
!sol.has_unevaluated(),
"BUG: y' = x·y should be solvable, got unevaluated: {s}"
);
let particular = (&x.powi(2) / 2).exp();
assert!(
ode.check_ode_solution(&particular, &y, &x),
"y = exp(x²/2) should satisfy y' = x·y, solution was: {s}"
);
}
#[test]
fn ode_general_solution_has_right_number_of_constants() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let ode1 = &dy + &y;
let sol1 = ode1.solve_ode(&y, &x);
let s1 = format!("{sol1}");
if !sol1.has_unevaluated() {
assert!(
s1.contains("C1"),
"First-order solution should have C1: {s1}"
);
}
let d2y = dy.formal_diff(&x);
let ode2 = &d2y + &y;
let sol2 = ode2.solve_ode(&y, &x);
let s2 = format!("{sol2}");
if !sol2.has_unevaluated() {
assert!(
s2.contains("C1") && s2.contains("C2"),
"Second-order solution should have C1 and C2: {s2}"
);
}
}
#[test]
fn series_exp_maclaurin_order_5() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let s = x.exp().series(&x, &zero, 5);
let expanded = s.expand().eval();
let val = expanded.subs(&x, &ctx.rational(1, 2)).eval();
let v = val.eval_f64().expect("val.eval_f64() must evaluate");
let expected_trunc = 1.0 + 0.5 + 0.125 + 1.0 / 48.0 + 1.0 / 384.0;
assert!(
(v - expected_trunc).abs() < 0.001,
"exp(x) series at x=0.5 should be ≈{expected_trunc}, got: {v}"
);
}
#[test]
fn series_sin_maclaurin_order_7() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let s = x.sin().series(&x, &zero, 8);
let expanded = s.expand().eval();
let val = expanded.subs(&x, &ctx.int(1)).eval();
let v = val.eval_f64().expect("val.eval_f64() must evaluate");
let expected = 1.0 - 1.0 / 6.0 + 1.0 / 120.0 - 1.0 / 5040.0;
assert!(
(v - expected).abs() < 0.001,
"sin(x) series at x=1 should be ≈{expected}, got: {v}"
);
}
#[test]
fn series_cos_maclaurin_order_6() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let s = x.cos().series(&x, &zero, 7);
let expanded = s.expand().eval();
let val = expanded.subs(&x, &ctx.int(1)).eval();
let v = val.eval_f64().expect("val.eval_f64() must evaluate");
let expected = 1.0 - 0.5 + 1.0 / 24.0 - 1.0 / 720.0;
assert!(
(v - expected).abs() < 0.001,
"cos(x) series at x=1 should be ≈{expected}, got: {v}"
);
}
#[test]
fn series_one_over_one_minus_x_geometric() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let expr = &ctx.int(1) / &(&ctx.int(1) - &x);
let s = expr.series(&x, &zero, 5);
let expanded = s.expand().eval();
let result = format!("{expanded}");
let val = expanded.subs(&x, &ctx.rational(1, 2)).eval();
if let Ok(v) = val.eval_f64() {
let expected = 1.0 + 0.5 + 0.25 + 0.125 + 0.0625;
assert!(
(v - expected).abs() < 1e-8,
"1/(1-x) series at x=1/2 should be {expected}, got: {v} (series: {result})"
);
} else {
panic!("1/(1-x) series should evaluate numerically at x=1/2, series: {result}");
}
}
#[test]
fn series_sqrt_1_plus_x_binomial() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let expr = (&ctx.int(1) + &x).pow(&ctx.rational(1, 2));
let s = expr.series(&x, &zero, 4);
let expanded = s.expand().eval();
let result = format!("{expanded}");
let val = expanded.subs(&x, &ctx.rational(1, 2)).eval();
if let Ok(v) = val.eval_f64() {
let expected = (1.5_f64).sqrt();
assert!(
(v - expected).abs() < 0.02,
"sqrt(1+x) series at x=0.5 should be ≈{expected}, got: {v} (series: {result})"
);
} else {
assert!(
result.contains("x"),
"sqrt(1+x) series should contain x: {result}"
);
}
}
#[test]
fn series_at_nonzero_point_exp() {
let ctx = Context::new();
let x = ctx.symbol("x");
let one = ctx.int(1);
let s = x.exp().series(&x, &one, 3);
let expanded = s.expand().eval();
let val_at_1 = expanded.subs(&x, &ctx.int(1)).eval();
let v = val_at_1
.eval_f64()
.expect("val_at_1.eval_f64() must evaluate");
assert!(
(v - std::f64::consts::E).abs() < 1e-6,
"exp(x) series around 1 at x=1 should be e ≈ 2.71828, got: {v}"
);
let val_near = expanded.subs(&x, &ctx.rational(11, 10)).eval();
let v = val_near
.eval_f64()
.expect("val_near.eval_f64() must evaluate");
let expected = 1.1_f64.exp();
assert!(
(v - expected).abs() < 0.01,
"exp(x) series around 1 at x=1.1 should be ≈{expected}, got: {v}"
);
}
#[test]
fn series_order_zero_gives_zero() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let s = x.exp().series(&x, &zero, 0);
let result = format!("{s}");
assert_eq!(result, "0", "Series of order 0 should be 0, got: {result}");
}
#[test]
fn series_constant_is_itself() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let s = ctx.int(7).series(&x, &zero, 4);
let result = format!("{s}");
assert_eq!(
result, "7",
"Series of constant 7 should be 7, got: {result}"
);
}
#[test]
fn series_polynomial_is_exact() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let poly = &(&x.powi(3) + &(&x * 2)) + &ctx.int(5);
let s = poly.series(&x, &zero, 6);
let expanded = s.expand().eval();
let orig_v = poly.subs(&x, &ctx.int(2)).eval().eval_f64().unwrap();
let series_v = expanded.subs(&x, &ctx.int(2)).eval().eval_f64().unwrap();
assert!(
(orig_v - series_v).abs() < 1e-8,
"Series of polynomial should be exact: original={orig_v}, series={series_v}"
);
}
#[test]
fn laurent_series_1_over_x() {
let ctx = Context::new();
ctx.with_arena_mut(|arena| {
let x = arena.symbol("x");
let zero = arena.zero();
let one = arena.one();
let expr = arena.div(one, x);
match arena.laurent_series_expr(expr, x, zero, 3) {
Ok(result) => {
let d = arena.display(result).to_string();
assert!(
d.contains("x^-1")
|| d.contains("x^(-1)")
|| d.contains("1/x")
|| d.contains("x^{-1}"),
"Laurent of 1/x should have x^(-1) term: {d}"
);
}
Err(e) => {
eprintln!("NOTE: Laurent series of 1/x not supported: {e}");
}
}
});
}
#[test]
fn laurent_series_sin_x_over_x() {
let ctx = Context::new();
ctx.with_arena_mut(|arena| {
let x = arena.symbol("x");
let zero = arena.zero();
let sin_x = arena.sin(x);
let expr = arena.div(sin_x, x);
match arena.laurent_series_expr(expr, x, zero, 4) {
Ok(result) => {
let d = arena.display(result).to_string();
assert!(
d.contains('1'),
"Laurent of sin(x)/x should start with 1: {d}"
);
}
Err(e) => {
eprintln!("NOTE: Laurent series of sin(x)/x not supported: {e}");
}
}
});
}
#[test]
fn laplace_of_1() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let result = ctx.int(1).laplace(&t, &s);
let val = result.subs(&s, &ctx.int(2)).eval();
let v = val.eval_f64().expect("val.eval_f64() must evaluate");
assert!(
(v - 0.5).abs() < 1e-8,
"L{{1}} at s=2 should be 0.5, got: {v}"
);
}
#[test]
fn laplace_of_t() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let result = t.laplace(&t, &s);
let val = result.subs(&s, &ctx.int(2)).eval();
let v = val.eval_f64().expect("val.eval_f64() must evaluate");
assert!(
(v - 0.25).abs() < 1e-8,
"L{{t}} at s=2 should be 0.25, got: {v}"
);
}
#[test]
fn laplace_of_t_squared() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let result = t.powi(2).laplace(&t, &s);
assert!(
!result.has_unevaluated(),
"L{{t²}} should not be unevaluated: {}",
result
);
let val = result.subs(&s, &ctx.int(2)).eval();
let v = val.eval_f64().expect("val.eval_f64() must evaluate");
assert!(
(v - 0.25).abs() < 1e-8,
"L{{t²}} at s=2 should be 0.25, got: {v}"
);
}
#[test]
fn laplace_of_t_cubed() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let result = t.powi(3).laplace(&t, &s);
assert!(
!result.has_unevaluated(),
"L{{t³}} should not be unevaluated: {}",
result
);
let val = result.subs(&s, &ctx.int(1)).eval();
let v = val.eval_f64().expect("val.eval_f64() must evaluate");
assert!(
(v - 6.0).abs() < 1e-8,
"L{{t³}} at s=1 should be 6, got: {v}"
);
}
#[test]
fn laplace_of_exp() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let result = (&t * 3).exp().laplace(&t, &s);
let val = result.subs(&s, &ctx.int(5)).eval();
let v = val.eval_f64().expect("val.eval_f64() must evaluate");
assert!(
(v - 0.5).abs() < 1e-8,
"L{{exp(3t)}} at s=5 should be 0.5, got: {v}"
);
}
#[test]
fn laplace_of_sin() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let result = (&t * 2).sin().laplace(&t, &s);
let val = result.subs(&s, &ctx.int(0)).eval();
let v = val.eval_f64().expect("val.eval_f64() must evaluate");
assert!(
(v - 0.5).abs() < 1e-8,
"L{{sin(2t)}} at s=0 should be 0.5, got: {v}"
);
}
#[test]
fn laplace_of_cos() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let result = (&t * 3).cos().laplace(&t, &s);
let val = result.subs(&s, &ctx.int(3)).eval();
let v = val.eval_f64().expect("val.eval_f64() must evaluate");
assert!(
(v - 1.0 / 6.0).abs() < 1e-8,
"L{{cos(3t)}} at s=3 should be 1/6, got: {v}"
);
let val0 = result.subs(&s, &ctx.int(0)).eval();
let v = val0.eval_f64().expect("val0.eval_f64() must evaluate");
assert!(v.abs() < 1e-8, "L{{cos(3t)}} at s=0 should be 0, got: {v}");
}
#[test]
fn laplace_of_sinh() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let result = (&t * 2).sinh().laplace(&t, &s);
let val = result.subs(&s, &ctx.int(3)).eval();
let v = val.eval_f64().expect("val.eval_f64() must evaluate");
assert!(
(v - 0.4).abs() < 1e-8,
"L{{sinh(2t)}} at s=3 should be 0.4, got: {v}"
);
}
#[test]
fn laplace_of_cosh() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let result = (&t * 2).cosh().laplace(&t, &s);
let val = result.subs(&s, &ctx.int(3)).eval();
let v = val.eval_f64().expect("val.eval_f64() must evaluate");
assert!(
(v - 0.6).abs() < 1e-8,
"L{{cosh(2t)}} at s=3 should be 0.6, got: {v}"
);
}
#[test]
fn laplace_linearity() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let combined = &(&t.exp() * 3) + &(&t * 2);
let result = combined.laplace(&t, &s);
let val = result.subs(&s, &ctx.int(2)).eval();
let v = val.eval_f64().expect("val.eval_f64() must evaluate");
assert!(
(v - 3.5).abs() < 1e-6,
"L{{3·exp(t) + 2·t}} at s=2 should be 3.5, got: {v}"
);
}
#[test]
fn inverse_laplace_1_over_s() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let expr = &ctx.int(1) / &s;
let result = expr.inverse_laplace(&s, &t);
let d = format!("{result}");
assert!(
!result.has_unevaluated(),
"L⁻¹{{1/s}} should not be unevaluated: {d}"
);
let v = result
.subs(&t, &ctx.int(1))
.eval()
.eval_f64()
.expect("result.subs(&t, &ctx.int(1)).eval().eval_f64() must evaluate");
assert!(
(v - 1.0).abs() < 1e-8,
"L⁻¹{{1/s}} should be 1, got: {v} (display: {d})"
);
}
#[test]
fn inverse_laplace_1_over_s_squared() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let expr = &ctx.int(1) / &s.powi(2);
let result = expr.inverse_laplace(&s, &t);
let d = format!("{result}");
assert!(
!result.has_unevaluated(),
"L⁻¹{{1/s²}} should not be unevaluated: {d}"
);
let v = result
.subs(&t, &ctx.int(5))
.eval()
.eval_f64()
.expect("result.subs(&t, &ctx.int(5)).eval().eval_f64() must evaluate");
assert!(
(v - 5.0).abs() < 1e-8,
"L⁻¹{{1/s²}} at t=5 should be 5, got: {v} (display: {d})"
);
}
#[test]
fn inverse_laplace_1_over_s_minus_a() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let expr = &ctx.int(1) / &(&s - 2);
let result = expr.inverse_laplace(&s, &t);
let d = format!("{result}");
assert!(
!result.has_unevaluated(),
"L⁻¹{{1/(s-2)}} should not be unevaluated: {d}"
);
let v = result
.subs(&t, &ctx.int(1))
.eval()
.eval_f64()
.expect("result.subs(&t, &ctx.int(1)).eval().eval_f64() must evaluate");
let expected = 2.0_f64.exp();
assert!(
(v - expected).abs() < 1e-4,
"L⁻¹{{1/(s-2)}} at t=1 should be exp(2) ≈ {expected}, got: {v}"
);
}
#[test]
fn inverse_laplace_omega_over_s2_plus_omega2() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let expr = &ctx.int(2) / &(&s.powi(2) + &ctx.int(4));
let result = expr.inverse_laplace(&s, &t);
let d = format!("{result}");
assert!(
!result.has_unevaluated(),
"L⁻¹{{2/(s²+4)}} should not be unevaluated: {d}"
);
let (t_num, t_den) = (7854_i64, 10000_i64);
let t_val = t_num as f64 / t_den as f64;
if let Ok(v) = result
.subs(&t, &ctx.rational(t_num, t_den))
.eval()
.eval_f64()
{
let expected = (2.0 * t_val).sin();
assert!(
(v - expected).abs() < 0.01,
"L⁻¹{{2/(s²+4)}} at t≈π/4 should be ≈{expected}, got: {v}"
);
}
}
#[test]
fn inverse_laplace_s_over_s2_plus_omega2() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let expr = &s / &(&s.powi(2) + &ctx.int(4));
let result = expr.inverse_laplace(&s, &t);
let d = format!("{result}");
assert!(
!result.has_unevaluated(),
"L⁻¹{{s/(s²+4)}} should not be unevaluated: {d}"
);
let v = result
.subs(&t, &ctx.int(0))
.eval()
.eval_f64()
.expect("result.subs(&t, &ctx.int(0)).eval().eval_f64() must evaluate");
assert!(
(v - 1.0).abs() < 1e-6,
"L⁻¹{{s/(s²+4)}} at t=0 should be 1, got: {v}"
);
}
#[test]
fn laplace_roundtrip_exp() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let original = (&t * 2).exp();
let transformed = original.laplace(&t, &s);
let recovered = transformed.inverse_laplace(&s, &t);
if !recovered.has_unevaluated() {
let ov = original.subs(&t, &ctx.int(1)).eval().eval_f64();
let rv = recovered.subs(&t, &ctx.int(1)).eval().eval_f64();
if let (Ok(o), Ok(r)) = (ov, rv) {
assert!(
(o - r).abs() < 1e-6,
"Laplace roundtrip exp(2t): orig={o}, recovered={r} (display: {recovered})"
);
}
} else {
panic!("Laplace roundtrip of exp(2t) gave unevaluated form: {recovered}");
}
}
#[test]
fn laplace_roundtrip_sin() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let original = (&t * 3).sin();
let transformed = original.laplace(&t, &s);
let recovered = transformed.inverse_laplace(&s, &t);
if !recovered.has_unevaluated() {
let ov = original.subs(&t, &ctx.int(1)).eval().eval_f64();
let rv = recovered.subs(&t, &ctx.int(1)).eval().eval_f64();
if let (Ok(o), Ok(r)) = (ov, rv) {
assert!(
(o - r).abs() < 1e-6,
"Laplace roundtrip sin(3t): orig={o}, recovered={r} (display: {recovered})"
);
}
} else {
panic!("Laplace roundtrip of sin(3t) gave unevaluated form: {recovered}");
}
}
#[test]
fn laplace_roundtrip_cos() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let original = (&t * 5).cos();
let transformed = original.laplace(&t, &s);
let recovered = transformed.inverse_laplace(&s, &t);
if !recovered.has_unevaluated() {
let ov = original.subs(&t, &ctx.int(1)).eval().eval_f64();
let rv = recovered.subs(&t, &ctx.int(1)).eval().eval_f64();
if let (Ok(o), Ok(r)) = (ov, rv) {
assert!(
(o - r).abs() < 1e-6,
"Laplace roundtrip cos(5t): orig={o}, recovered={r} (display: {recovered})"
);
}
} else {
panic!("Laplace roundtrip of cos(5t) gave unevaluated form: {recovered}");
}
}
#[test]
fn z_transform_constant() {
let ctx = Context::new();
let n = ctx.symbol("n");
let z = ctx.symbol("z");
let result = ctx.int(1).z_transform(&n, &z).expect("Z{1} should succeed");
let val = result.subs(&z, &ctx.int(2)).eval();
let v = val.eval_f64().expect("val.eval_f64() must evaluate");
assert!(
(v - 2.0).abs() < 1e-8,
"Z{{1}} at z=2 should be 2, got: {v}"
);
}
#[test]
fn z_transform_a_to_n() {
let ctx = Context::new();
let n = ctx.symbol("n");
let z = ctx.symbol("z");
let half = ctx.rational(1, 2);
let result = half
.pow(&n)
.z_transform(&n, &z)
.expect("Z{(1/2)^n} should succeed");
let val = result.subs(&z, &ctx.int(2)).eval();
let v = val.eval_f64().expect("val.eval_f64() must evaluate");
assert!(
(v - 4.0 / 3.0).abs() < 1e-6,
"Z{{(1/2)^n}} at z=2 should be 4/3, got: {v}"
);
}
#[test]
fn z_transform_n() {
let ctx = Context::new();
let n = ctx.symbol("n");
let z = ctx.symbol("z");
let result = n.z_transform(&n, &z).expect("Z{n} should succeed");
let val = result.subs(&z, &ctx.int(2)).eval();
let v = val.eval_f64().expect("val.eval_f64() must evaluate");
assert!(
(v - 2.0).abs() < 1e-8,
"Z{{n}} at z=2 should be 2, got: {v}"
);
let val3 = result.subs(&z, &ctx.int(3)).eval();
let v = val3.eval_f64().expect("val3.eval_f64() must evaluate");
assert!(
(v - 0.75).abs() < 1e-8,
"Z{{n}} at z=3 should be 0.75, got: {v}"
);
}
#[test]
fn z_transform_sin() {
let ctx = Context::new();
let n = ctx.symbol("n");
let z = ctx.symbol("z");
let expr = n.sin(); let result = expr.z_transform(&n, &z);
match result {
Ok(r) => {
let z_val = 3.0_f64;
let mut direct_sum = 0.0;
for k in 0..50 {
direct_sum += (k as f64).sin() * z_val.powi(-k);
}
let v = r
.subs(&z, &ctx.int(3))
.eval()
.eval_f64()
.expect("r.subs(&z, &ctx.int(3)).eval().eval_f64() must evaluate");
assert!(
(v - direct_sum).abs() < 1e-4,
"Z{{sin(n)}} at z=3: transform={v}, partial sum={direct_sum}"
);
}
Err(e) => {
panic!("BUG: Z{{sin(n)}} should be computable: {e}");
}
}
}
#[test]
fn z_transform_cos() {
let ctx = Context::new();
let n = ctx.symbol("n");
let z = ctx.symbol("z");
let expr = n.cos();
let result = expr.z_transform(&n, &z);
match result {
Ok(r) => {
let z_val = 3.0_f64;
let mut direct_sum = 0.0;
for k in 0..50 {
direct_sum += (k as f64).cos() * z_val.powi(-k);
}
let v = r
.subs(&z, &ctx.int(3))
.eval()
.eval_f64()
.expect("r.subs(&z, &ctx.int(3)).eval().eval_f64() must evaluate");
assert!(
(v - direct_sum).abs() < 1e-4,
"Z{{cos(n)}} at z=3: transform={v}, partial sum={direct_sum}"
);
}
Err(e) => {
panic!("BUG: Z{{cos(n)}} should be computable: {e}");
}
}
}
#[test]
fn z_transform_roundtrip_exponential() {
let ctx = Context::new();
let n = ctx.symbol("n");
let z = ctx.symbol("z");
let two_n = ctx.int(2).pow(&n);
let transformed = two_n.z_transform(&n, &z).expect("Z{2^n} should succeed");
let recovered = transformed
.inverse_z_transform(&z, &n)
.expect("Z⁻¹{z/(z-2)} should succeed");
let orig_val = two_n.subs(&n, &ctx.int(3)).eval().eval_f64();
let recov_val = recovered.subs(&n, &ctx.int(3)).eval().eval_f64();
if let (Ok(o), Ok(r)) = (orig_val, recov_val) {
assert!(
(o - r).abs() < 1e-6,
"Z roundtrip of 2^n at n=3: original={o}, recovered={r}"
);
}
}
#[test]
fn z_transform_roundtrip_half_n() {
let ctx = Context::new();
let n = ctx.symbol("n");
let z = ctx.symbol("z");
let third_n = ctx.rational(1, 3).pow(&n);
let transformed = third_n
.z_transform(&n, &z)
.expect("Z{(1/3)^n} should succeed");
let recovered = transformed
.inverse_z_transform(&z, &n)
.expect("Z⁻¹ should succeed");
let orig_val = third_n.subs(&n, &ctx.int(4)).eval().eval_f64();
let recov_val = recovered.subs(&n, &ctx.int(4)).eval().eval_f64();
if let (Ok(o), Ok(r)) = (orig_val, recov_val) {
assert!(
(o - r).abs() < 1e-6,
"Z roundtrip of (1/3)^n at n=4: original={o}, recovered={r}"
);
}
}
#[test]
fn inverse_z_transform_z_over_z_minus_a() {
let ctx = Context::new();
let n = ctx.symbol("n");
let z = ctx.symbol("z");
let expr = &z / &(&z - 3);
let result = expr
.inverse_z_transform(&z, &n)
.expect("Z⁻¹{z/(z-3)} should succeed");
let val = result.subs(&n, &ctx.int(4)).eval();
let v = val.eval_f64().expect("val.eval_f64() must evaluate");
assert!(
(v - 81.0).abs() < 1e-6,
"Z⁻¹{{z/(z-3)}} at n=4 should be 81, got: {v}"
);
}
#[test]
fn z_transform_linearity() {
let ctx = Context::new();
let n = ctx.symbol("n");
let z = ctx.symbol("z");
let half_n = ctx.rational(1, 2).pow(&n);
let expr = &(&half_n * 3) + &ctx.int(2);
let result = expr.z_transform(&n, &z);
match result {
Ok(r) => {
let val = r.subs(&z, &ctx.int(4)).eval();
let v = val.eval_f64().expect("val.eval_f64() must evaluate");
let expected = 3.0 * (4.0 / 3.5) + 2.0 * (4.0 / 3.0);
assert!(
(v - expected).abs() < 1e-4,
"Z{{3·(1/2)^n + 2}} at z=4 should be ≈{expected}, got: {v}"
);
}
Err(e) => {
panic!("Z-transform of 3·(1/2)^n + 2 should succeed: {e}");
}
}
}
#[test]
fn fourier_delta_gives_1() {
let ctx = Context::new();
ctx.with_arena_mut(|arena| {
let t = arena.symbol("t");
let omega = arena.symbol("omega");
let delta_t = arena.dirac_delta(t);
let result = arena
.fourier_transform_expr(delta_t, t, omega)
.expect("F{δ(t)} should succeed");
assert_eq!(
result,
arena.one(),
"F{{δ(t)}} should be 1, got: {}",
arena.display(result)
);
});
}
#[test]
fn fourier_constant_gives_2pi_delta() {
let ctx = Context::new();
ctx.with_arena_mut(|arena| {
let t = arena.symbol("t");
let omega = arena.symbol("omega");
let five = arena.int(5);
let result = arena
.fourier_transform_expr(five, t, omega)
.expect("F{5} should succeed");
let d = arena.display(result).to_string();
assert!(
d.contains("DiracDelta") || d.contains("delta") || d.contains("pi"),
"F{{5}} should contain delta or pi: {d}"
);
});
}
#[test]
fn fourier_sin_gives_delta_pair() {
let ctx = Context::new();
ctx.with_arena_mut(|arena| {
let t = arena.symbol("t");
let omega = arena.symbol("omega");
let three = arena.int(3);
let three_t = arena.mul(&[three, t]);
let sin_3t = arena.sin(three_t);
let result = arena
.fourier_transform_expr(sin_3t, t, omega)
.expect("F{sin(3t)} should succeed");
let d = arena.display(result).to_string();
assert!(
d.contains("DiracDelta") || d.contains("delta"),
"F{{sin(3t)}} should contain delta: {d}"
);
});
}
#[test]
fn fourier_cos_gives_delta_pair() {
let ctx = Context::new();
ctx.with_arena_mut(|arena| {
let t = arena.symbol("t");
let omega = arena.symbol("omega");
let two = arena.int(2);
let two_t = arena.mul(&[two, t]);
let cos_2t = arena.cos(two_t);
let result = arena
.fourier_transform_expr(cos_2t, t, omega)
.expect("F{cos(2t)} should succeed");
let d = arena.display(result).to_string();
assert!(
d.contains("DiracDelta") || d.contains("delta"),
"F{{cos(2t)}} should contain delta: {d}"
);
});
}
#[test]
fn fourier_exp_heaviside() {
let ctx = Context::new();
ctx.with_arena_mut(|arena| {
let t = arena.symbol("t");
let omega = arena.symbol("omega");
let neg3 = arena.int(-3);
let neg3_t = arena.mul(&[neg3, t]);
let exp_neg3t = arena.exp(neg3_t);
let h_t = arena.heaviside(t);
let expr = arena.mul(&[exp_neg3t, h_t]);
let result = arena
.fourier_transform_expr(expr, t, omega)
.expect("F{exp(-3t)·H(t)} should succeed");
let d = arena.display(result).to_string();
assert!(
d.contains("i") || d.contains("omega") || d.contains("3"),
"F{{exp(-3t)·H(t)}} should be 1/(iω+3), got: {d}"
);
});
}
#[test]
fn fourier_heaviside_alone() {
let ctx = Context::new();
ctx.with_arena_mut(|arena| {
let t = arena.symbol("t");
let omega = arena.symbol("omega");
let h_t = arena.heaviside(t);
let result = arena
.fourier_transform_expr(h_t, t, omega)
.expect("F{H(t)} should succeed");
let d = arena.display(result).to_string();
assert!(
(d.contains("DiracDelta") || d.contains("delta")) && d.contains("pi"),
"F{{H(t)}} should contain πδ(ω): {d}"
);
});
}
#[test]
fn fourier_linearity_scaled_delta() {
let ctx = Context::new();
ctx.with_arena_mut(|arena| {
let t = arena.symbol("t");
let omega = arena.symbol("omega");
let seven = arena.int(7);
let delta_t = arena.dirac_delta(t);
let expr = arena.mul(&[seven, delta_t]);
let result = arena
.fourier_transform_expr(expr, t, omega)
.expect("F{7·δ(t)} should succeed");
let d = arena.display(result).to_string();
assert_eq!(d, "7", "F{{7·δ(t)}} should be 7, got: {d}");
});
}
#[test]
fn fourier_neg_delta() {
let ctx = Context::new();
ctx.with_arena_mut(|arena| {
let t = arena.symbol("t");
let omega = arena.symbol("omega");
let delta_t = arena.dirac_delta(t);
let neg_delta = arena.neg(delta_t);
let result = arena
.fourier_transform_expr(neg_delta, t, omega)
.expect("F{-δ(t)} should succeed");
let d = arena.display(result).to_string();
assert!(
d.contains("-1") || d.contains("−1"),
"F{{-δ(t)}} should be -1, got: {d}"
);
});
}
#[test]
fn fourier_roundtrip_delta() {
let ctx = Context::new();
ctx.with_arena_mut(|arena| {
let t = arena.symbol("t");
let omega = arena.symbol("omega");
let delta_t = arena.dirac_delta(t);
let forward = arena
.fourier_transform_expr(delta_t, t, omega)
.expect("F{δ(t)} forward");
assert_eq!(forward, arena.one());
let inverse = arena
.inverse_fourier_transform_expr(forward, omega, t)
.expect("F⁻¹{1} inverse");
let d = arena.display(inverse).to_string();
assert!(
d.contains("DiracDelta") || d.contains("delta"),
"F⁻¹{{1}} should contain δ(t), got: {d}"
);
});
}
#[test]
fn inverse_fourier_of_delta_omega() {
let ctx = Context::new();
ctx.with_arena_mut(|arena| {
let t = arena.symbol("t");
let omega = arena.symbol("omega");
let delta_omega = arena.dirac_delta(omega);
let result = arena
.inverse_fourier_transform_expr(delta_omega, omega, t)
.expect("F⁻¹{δ(ω)} should succeed");
let d = arena.display(result).to_string();
assert!(
d.contains("pi") || d.contains("π"),
"F⁻¹{{δ(ω)}} should contain π (result = 1/(2π)): {d}"
);
});
}
#[test]
fn fourier_sum_of_terms() {
let ctx = Context::new();
ctx.with_arena_mut(|arena| {
let t = arena.symbol("t");
let omega = arena.symbol("omega");
let delta_t = arena.dirac_delta(t);
let h_t = arena.heaviside(t);
let expr = arena.add(&[delta_t, h_t]);
let result = arena.fourier_transform_expr(expr, t, omega);
assert!(result.is_ok(), "F{{δ(t) + H(t)}} should succeed");
});
}
#[test]
fn convergence_p_series_p2_converges() {
let ctx = Context::new();
let k = ctx.symbol("k");
let body = k.powi(-2); assert_eq!(
body.is_convergent(&k),
Some(true),
"Σ 1/k² should converge (p=2 > 1)"
);
}
#[test]
fn convergence_p_series_p3_converges() {
let ctx = Context::new();
let k = ctx.symbol("k");
let body = k.powi(-3);
assert_eq!(body.is_convergent(&k), Some(true), "Σ 1/k³ should converge");
}
#[test]
fn convergence_harmonic_diverges() {
let ctx = Context::new();
let k = ctx.symbol("k");
let body = k.powi(-1); assert_eq!(
body.is_convergent(&k),
Some(false),
"Σ 1/k should diverge (harmonic series)"
);
}
#[test]
fn convergence_p_half_diverges() {
let ctx = Context::new();
let k = ctx.symbol("k");
let body = k.pow(&ctx.rational(-1, 2)); assert_eq!(
body.is_convergent(&k),
Some(false),
"Σ 1/√k should diverge (p=0.5 < 1)"
);
}
#[test]
fn convergence_geometric_third_converges() {
let ctx = Context::new();
let k = ctx.symbol("k");
let body = ctx.rational(1, 3).pow(&k); assert_eq!(
body.is_convergent(&k),
Some(true),
"Σ (1/3)^k should converge (|r| < 1)"
);
}
#[test]
fn convergence_geometric_half_converges() {
let ctx = Context::new();
let k = ctx.symbol("k");
let body = ctx.rational(1, 2).pow(&k);
assert_eq!(
body.is_convergent(&k),
Some(true),
"Σ (1/2)^k should converge"
);
}
#[test]
fn convergence_geometric_2_diverges() {
let ctx = Context::new();
let k = ctx.symbol("k");
let body = ctx.int(2).pow(&k); assert_eq!(
body.is_convergent(&k),
Some(false),
"Σ 2^k should diverge (|r| ≥ 1)"
);
}
#[test]
fn convergence_geometric_neg_half_converges() {
let ctx = Context::new();
let k = ctx.symbol("k");
let body = ctx.rational(-1, 2).pow(&k); assert_eq!(
body.is_convergent(&k),
Some(true),
"Σ (-1/2)^k should converge (|r| = 1/2 < 1)"
);
}
#[test]
fn convergence_constant_zero_converges() {
let ctx = Context::new();
let k = ctx.symbol("k");
let body = ctx.int(0);
assert_eq!(body.is_convergent(&k), Some(true), "Σ 0 should converge");
}
#[test]
fn convergence_constant_nonzero_diverges() {
let ctx = Context::new();
let k = ctx.symbol("k");
let body = ctx.int(7);
assert_eq!(body.is_convergent(&k), Some(false), "Σ 7 should diverge");
}
#[test]
fn convergence_growing_k_squared_diverges() {
let ctx = Context::new();
let k = ctx.symbol("k");
let body = k.powi(2); assert_eq!(body.is_convergent(&k), Some(false), "Σ k² should diverge");
}
#[test]
fn convergence_geometric_base_1_diverges() {
let ctx = Context::new();
let k = ctx.symbol("k");
let body = ctx.int(1).pow(&k);
let result = body.is_convergent(&k);
assert!(
result == Some(false) || result.is_none(),
"Σ 1^k should diverge or be inconclusive, got: {result:?}"
);
}
#[test]
fn diff_of_integral_is_identity() {
let ctx = Context::new();
let x = ctx.symbol("x");
let functions: Vec<(&str, Ex)> = vec![
("sin(x)", x.sin()),
("cos(x)", x.cos()),
("exp(x)", x.exp()),
("x^2", x.powi(2)),
("x^3", x.powi(3)),
];
for (name, f) in &functions {
let antideriv = f.integrate(&x);
let roundtrip = antideriv.diff(&x);
let orig_val = f.subs(&x, &ctx.int(1)).eval().eval_f64();
let rt_val = roundtrip.subs(&x, &ctx.int(1)).eval().eval_f64();
if let (Ok(o), Ok(r)) = (orig_val, rt_val) {
assert!(
(o - r).abs() < 1e-8,
"d/dx(∫{name}dx) ≠ {name} at x=1: got {r}, expected {o}"
);
}
}
}
#[test]
fn integral_of_diff_recovers_up_to_constant() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = &x.powi(3) + &(&x.powi(2) * 2) + &x; let df = f.diff(&x);
let integral_df = df.integrate(&x);
let v1_f = f.subs(&x, &ctx.int(1)).eval().eval_f64().unwrap();
let v1_i = integral_df.subs(&x, &ctx.int(1)).eval().eval_f64().unwrap();
let diff1 = v1_f - v1_i;
let v2_f = f.subs(&x, &ctx.int(2)).eval().eval_f64().unwrap();
let v2_i = integral_df.subs(&x, &ctx.int(2)).eval().eval_f64().unwrap();
let diff2 = v2_f - v2_i;
assert!(
(diff1 - diff2).abs() < 1e-8,
"∫(df/dx)dx and f should differ by a constant: diff@1={diff1}, diff@2={diff2}"
);
}
#[test]
fn series_of_polynomial_is_exact() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let poly = &(&x.powi(3) + &(&x.powi(2) * 2)) + &(&x * 3);
let s = poly.series(&x, &zero, 6);
let expanded = s.expand().eval();
let vo = poly.subs(&x, &ctx.int(2)).eval().eval_f64().unwrap();
let vs = expanded.subs(&x, &ctx.int(2)).eval().eval_f64().unwrap();
assert!(
(vo - vs).abs() < 1e-8,
"Series of polynomial should be exact: orig={vo}, series={vs}"
);
}
#[test]
fn laplace_derivative_property() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let l_2t = (&t * 2).laplace(&t, &s);
let l_t2 = t.powi(2).laplace(&t, &s);
let rhs = &s * &l_t2;
let lv = l_2t.subs(&s, &ctx.int(3)).eval().eval_f64();
let rv = rhs.subs(&s, &ctx.int(3)).eval().eval_f64();
if let (Ok(l), Ok(r)) = (lv, rv) {
assert!(
(l - r).abs() < 1e-6,
"Laplace derivative property: L{{2t}}={l}, s·L{{t²}}={r}"
);
}
}
#[test]
fn definite_integral_reversed_bounds_negate() {
let ctx = Context::new();
let x = ctx.symbol("x");
let fwd = x.powi(2).integrate_definite(&x, &ctx.int(0), &ctx.int(3));
let rev = x.powi(2).integrate_definite(&x, &ctx.int(3), &ctx.int(0));
let fv = fwd.eval_f64().expect("forward evals");
let rv = rev.eval_f64().expect("reversed evals");
assert!(
(fv + rv).abs() < 1e-10,
"Reversing bounds should negate: fwd={fv}, rev={rv}"
);
}
#[test]
fn definite_integral_same_bounds_is_zero() {
let ctx = Context::new();
let x = ctx.symbol("x");
let result = x.sin().integrate_definite(&x, &ctx.int(5), &ctx.int(5));
let v = result.eval_f64().expect("same-bounds evals");
assert!(v.abs() < 1e-10, "∫_a^a f dx should be 0, got: {v}");
}
#[test]
fn inverse_laplace_1_over_s_cubed() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let expr = &ctx.int(1) / &s.powi(3);
let result = expr.inverse_laplace(&s, &t);
let d = format!("{result}");
assert!(
!result.has_unevaluated(),
"BUG: L⁻¹{{1/s³}} should not be unevaluated: {d}\n\
Expected: t²/2. The inverse_power_form handler may not recognise s³ \
when the expression is represented as s^(-3)."
);
let v = result
.subs(&t, &ctx.int(3))
.eval()
.eval_f64()
.expect("result.subs(&t, &ctx.int(3)).eval().eval_f64() must evaluate");
assert!(
(v - 4.5).abs() < 1e-6,
"L⁻¹{{1/s³}} at t=3 should be 4.5, got: {v}"
);
}
#[test]
fn inverse_laplace_1_over_s_fourth() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let expr = &ctx.int(1) / &s.powi(4);
let result = expr.inverse_laplace(&s, &t);
let d = format!("{result}");
assert!(
!result.has_unevaluated(),
"BUG: L⁻¹{{1/s⁴}} should not be unevaluated: {d}\n\
Expected: t³/6."
);
let v = result
.subs(&t, &ctx.int(2))
.eval()
.eval_f64()
.expect("result.subs(&t, &ctx.int(2)).eval().eval_f64() must evaluate");
let expected = 8.0 / 6.0;
assert!(
(v - expected).abs() < 1e-6,
"L⁻¹{{1/s⁴}} at t=2 should be {expected}, got: {v}"
);
}
#[test]
fn inverse_laplace_2_over_s_cubed() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let expr = &ctx.int(2) / &s.powi(3);
let result = expr.inverse_laplace(&s, &t);
let d = format!("{result}");
assert!(
!result.has_unevaluated(),
"BUG: L⁻¹{{2/s³}} should not be unevaluated: {d}\n\
Expected: t²."
);
let v = result
.subs(&t, &ctx.int(3))
.eval()
.eval_f64()
.expect("result.subs(&t, &ctx.int(3)).eval().eval_f64() must evaluate");
assert!(
(v - 9.0).abs() < 1e-6,
"L⁻¹{{2/s³}} at t=3 should be 9, got: {v}"
);
}
#[test]
fn inverse_laplace_s_over_s2_minus_a2() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let expr = &s / &(&s.powi(2) - &ctx.int(9));
let result = expr.inverse_laplace(&s, &t);
let d = format!("{result}");
assert!(
!result.has_unevaluated(),
"BUG: L⁻¹{{s/(s²-9)}} should not be unevaluated: {d}\n\
Expected: cosh(3t). The inverse_degree2 handler rejects denominators \
s²-a² (negative constant term) but this should give cosh."
);
let v = result
.subs(&t, &ctx.int(0))
.eval()
.eval_f64()
.expect("result.subs(&t, &ctx.int(0)).eval().eval_f64() must evaluate");
assert!(
(v - 1.0).abs() < 1e-6,
"L⁻¹{{s/(s²-9)}} at t=0 should be 1 (cosh(0)), got: {v}"
);
}
#[test]
fn inverse_laplace_a_over_s2_minus_a2() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let expr = &ctx.int(3) / &(&s.powi(2) - &ctx.int(9));
let result = expr.inverse_laplace(&s, &t);
let d = format!("{result}");
assert!(
!result.has_unevaluated(),
"BUG: L⁻¹{{3/(s²-9)}} should not be unevaluated: {d}\n\
Expected: sinh(3t). Same root cause as s/(s²-9) — the handler \
rejects negative constant term in denominator."
);
let v = result
.subs(&t, &ctx.int(0))
.eval()
.eval_f64()
.expect("result.subs(&t, &ctx.int(0)).eval().eval_f64() must evaluate");
assert!(
v.abs() < 1e-6,
"L⁻¹{{3/(s²-9)}} at t=0 should be 0 (sinh(0)), got: {v}"
);
}
#[test]
fn inverse_laplace_repeated_root_s_minus_2_squared() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let expr = &ctx.int(1) / &(&s - 2).powi(2);
let result = expr.inverse_laplace(&s, &t);
let d = format!("{result}");
assert!(
!result.has_unevaluated(),
"BUG: L⁻¹{{1/(s-2)²}} should not be unevaluated: {d}\n\
Expected: t·exp(2t)."
);
let v = result
.subs(&t, &ctx.int(1))
.eval()
.eval_f64()
.expect("result.subs(&t, &ctx.int(1)).eval().eval_f64() must evaluate");
let expected = 2.0_f64.exp();
assert!(
(v - expected).abs() < 1e-3,
"L⁻¹{{1/(s-2)²}} at t=1 should be e² ≈ {expected}, got: {v}"
);
}
#[test]
fn laplace_roundtrip_t() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let forward = t.laplace(&t, &s);
let d_fwd = format!("{forward}");
assert!(!forward.has_unevaluated(), "L{{t}} should succeed: {d_fwd}");
let roundtrip = forward.inverse_laplace(&s, &t);
let d_rt = format!("{roundtrip}");
assert!(
!roundtrip.has_unevaluated(),
"BUG: Laplace roundtrip of t failed.\n\
L{{t}} = {d_fwd}\n\
L⁻¹{{L{{t}}}} = {d_rt} (unevaluated)\n\
Expected: t"
);
let v = roundtrip
.subs(&t, &ctx.int(5))
.eval()
.eval_f64()
.expect("roundtrip.subs(&t, &ctx.int(5)).eval().eval_f64() must evaluate");
assert!(
(v - 5.0).abs() < 1e-6,
"Laplace roundtrip of t at t=5 should be 5, got: {v}"
);
}
#[test]
fn laplace_roundtrip_t_squared() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let forward = t.powi(2).laplace(&t, &s);
assert!(!forward.has_unevaluated(), "L{{t²}} should succeed");
let roundtrip = forward.inverse_laplace(&s, &t);
let d_rt = format!("{roundtrip}");
assert!(
!roundtrip.has_unevaluated(),
"BUG: Laplace roundtrip of t² failed.\n\
L{{t²}} = {forward}\n\
L⁻¹ = {d_rt} (unevaluated)\n\
Expected: t²"
);
let v = roundtrip
.subs(&t, &ctx.int(3))
.eval()
.eval_f64()
.expect("roundtrip.subs(&t, &ctx.int(3)).eval().eval_f64() must evaluate");
assert!(
(v - 9.0).abs() < 1e-6,
"Laplace roundtrip of t² at t=3 should be 9, got: {v}"
);
}
#[test]
fn laplace_roundtrip_t_cubed() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let forward = t.powi(3).laplace(&t, &s);
assert!(!forward.has_unevaluated(), "L{{t³}} should succeed");
let roundtrip = forward.inverse_laplace(&s, &t);
let d_rt = format!("{roundtrip}");
assert!(
!roundtrip.has_unevaluated(),
"BUG: Laplace roundtrip of t³ failed.\n\
L{{t³}} = {forward}\n\
L⁻¹ = {d_rt} (unevaluated)\n\
Expected: t³"
);
let v = roundtrip
.subs(&t, &ctx.int(2))
.eval()
.eval_f64()
.expect("roundtrip.subs(&t, &ctx.int(2)).eval().eval_f64() must evaluate");
assert!(
(v - 8.0).abs() < 1e-6,
"Laplace roundtrip of t³ at t=2 should be 8, got: {v}"
);
}
#[test]
fn ode_check_rejects_constant_for_nontrivial_ode() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let ode = &dy + &y;
assert!(
!ode.check_ode_solution(&ctx.int(5), &y, &x),
"BUG: y = 5 should NOT satisfy y' + y = 0"
);
}
#[test]
fn ode_check_accepts_zero_for_homogeneous() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let ode = &dy + &y;
assert!(
ode.check_ode_solution(&ctx.int(0), &y, &x),
"BUG: y = 0 should satisfy y' + y = 0 (trivial solution)"
);
}
#[test]
fn ode_check_second_order_sum_of_solutions() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let d2y = dy.formal_diff(&x);
let ode = &d2y + &y;
let combo = &(&x.sin() * 3) + &(&x.cos() * 2);
assert!(
ode.check_ode_solution(&combo, &y, &x),
"BUG: 3sin(x) + 2cos(x) should satisfy y'' + y = 0"
);
}
#[test]
fn ode_repeated_root_y_pp_plus_2yp_plus_y() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let d2y = dy.formal_diff(&x);
let ode = &(&d2y + &(&dy * 2)) + &y;
let sol = ode.solve_ode(&y, &x);
let s = format!("{sol}");
assert!(
!sol.has_unevaluated(),
"BUG: y'' + 2y' + y = 0 (repeated root) should be solvable: {s}"
);
let exp_neg_x = (-&x).exp();
assert!(
ode.check_ode_solution(&exp_neg_x, &y, &x),
"y = e^(-x) should satisfy y'' + 2y' + y = 0"
);
let x_exp_neg_x = &x * &(-&x).exp();
assert!(
ode.check_ode_solution(&x_exp_neg_x, &y, &x),
"y = x·e^(-x) should satisfy y'' + 2y' + y = 0"
);
}
#[test]
fn series_exp_convergence_increases_with_order() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let target = std::f64::consts::E;
let mut prev_err = f64::MAX;
for order in [3, 5, 8, 12] {
let s = x.exp().series(&x, &zero, order);
let expanded = s.expand().eval();
let v = expanded
.subs(&x, &ctx.int(1))
.eval()
.eval_f64()
.expect("expanded.subs(&x, &ctx.int(1)).eval().eval_f64() must evaluate");
let err = (v - target).abs();
assert!(
err < prev_err,
"Series order {order}: error {err} should be less than previous {prev_err}"
);
prev_err = err;
}
}
#[test]
fn series_sin_at_nonzero_point() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pi = ctx.pi();
let quarter_pi = &pi / &ctx.int(4);
let s = x.sin().series(&x, &quarter_pi, 4);
let expanded = s.expand().eval();
let val = expanded.subs(&x, &quarter_pi).eval();
let v = val.eval_f64().expect("val.eval_f64() must evaluate");
let expected = std::f64::consts::FRAC_PI_4.sin();
assert!(
(v - expected).abs() < 1e-4,
"sin(x) series around π/4, at x=π/4: expected {expected}, got {v}"
);
}
#[test]
fn integrate_ln_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let integrand = x.ln();
let (s, is_uneval) = ftc_check(&integrand, &x, &[0.5, 1.0, 2.0, 3.0], 1e-6, "∫ln(x)dx");
if is_uneval {
panic!(
"BUG: ∫ln(x)dx returned unevaluated form: {s}\n\
Expected: x·ln(x) - x or equivalent"
);
}
}
#[test]
fn integrate_1_over_x_squared_plus_a() {
let ctx = Context::new();
let x = ctx.symbol("x");
let integrand = &ctx.int(1) / &(&x.powi(2) + &ctx.int(4));
let (s, is_uneval) = ftc_check(&integrand, &x, &[0.0, 0.5, 1.0, 2.0], 1e-6, "∫1/(x²+4)dx");
if is_uneval {
panic!(
"BUG: ∫1/(x²+4)dx returned unevaluated form: {s}\n\
Expected: (1/2)·arctan(x/2) or equivalent"
);
}
}
#[test]
fn integrate_exp_2x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let integrand = (&x * 2).exp();
let (s, is_uneval) = ftc_check(&integrand, &x, &[0.0, 0.5, 1.0], 1e-6, "∫exp(2x)dx");
if is_uneval {
panic!(
"BUG: ∫exp(2x)dx returned unevaluated form: {s}\n\
Expected: exp(2x)/2 or equivalent"
);
}
}
#[test]
fn integrate_sin_2x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let integrand = (&x * 2).sin();
let (s, is_uneval) = ftc_check(&integrand, &x, &[0.3, 0.7, 1.0, 1.5], 1e-6, "∫sin(2x)dx");
if is_uneval {
panic!(
"BUG: ∫sin(2x)dx returned unevaluated form: {s}\n\
Expected: -cos(2x)/2 or equivalent"
);
}
}
#[test]
fn inverse_z_transform_z_over_z_minus_1_squared() {
let ctx = Context::new();
let n = ctx.symbol("n");
let z = ctx.symbol("z");
let expr = &z / &(&z - 1).powi(2);
let result = expr.inverse_z_transform(&z, &n);
match result {
Ok(r) => {
let v = r
.subs(&n, &ctx.int(5))
.eval()
.eval_f64()
.expect("r.subs(&n, &ctx.int(5)).eval().eval_f64() must evaluate");
assert!(
(v - 5.0).abs() < 1e-6,
"Z⁻¹{{z/(z-1)²}} at n=5 should be 5 (= n), got: {v}"
);
}
Err(e) => {
panic!("BUG: Z⁻¹{{z/(z-1)²}} should succeed (expected: n): {e}");
}
}
}
#[test]
fn inverse_z_transform_z_over_z_minus_1() {
let ctx = Context::new();
let n = ctx.symbol("n");
let z = ctx.symbol("z");
let expr = &z / &(&z - 1);
let result = expr
.inverse_z_transform(&z, &n)
.expect("Z⁻¹{z/(z-1)} should succeed");
for nv in [0, 1, 5, 10] {
let v = result
.subs(&n, &ctx.int(nv))
.eval()
.eval_f64()
.expect("result.subs(&n, &ctx.int(nv)).eval().eval_f64() must evaluate");
assert!(
(v - 1.0).abs() < 1e-6,
"Z⁻¹{{z/(z-1)}} at n={nv} should be 1, got: {v}"
);
}
}
#[test]
fn convergence_scaled_p_series_converges() {
let ctx = Context::new();
let k = ctx.symbol("k");
let body = &ctx.int(5) * &k.powi(-2);
let result = body.is_convergent(&k);
assert_eq!(
result,
Some(true),
"Σ 5/k² should converge, got: {result:?}"
);
}
#[test]
fn convergence_scaled_harmonic_diverges() {
let ctx = Context::new();
let k = ctx.symbol("k");
let body = &ctx.int(3) * &k.powi(-1);
let result = body.is_convergent(&k);
assert_eq!(result, Some(false), "Σ 3/k should diverge, got: {result:?}");
}
#[test]
fn laplace_t_times_exp() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let expr = &t * &(&t * 2).exp();
let result = expr.laplace(&t, &s);
let d = format!("{result}");
assert!(
!result.has_unevaluated(),
"BUG: L{{t·exp(2t)}} should succeed, got unevaluated: {d}"
);
let v = result
.subs(&s, &ctx.int(4))
.eval()
.eval_f64()
.expect("result.subs(&s, &ctx.int(4)).eval().eval_f64() must evaluate");
assert!(
(v - 0.25).abs() < 1e-6,
"L{{t·exp(2t)}} at s=4 should be 0.25, got: {v}"
);
}
#[test]
fn laplace_exp_sin() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let expr = &t.exp() * &(&t * 2).sin();
let result = expr.laplace(&t, &s);
let d = format!("{result}");
assert!(
!result.has_unevaluated(),
"BUG: L{{exp(t)·sin(2t)}} should succeed: {d}"
);
let v = result
.subs(&s, &ctx.int(3))
.eval()
.eval_f64()
.expect("result.subs(&s, &ctx.int(3)).eval().eval_f64() must evaluate");
assert!(
(v - 0.25).abs() < 1e-6,
"L{{exp(t)·sin(2t)}} at s=3 should be 0.25, got: {v}"
);
}
#[test]
fn laplace_exp_cos() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let expr = &(-&t).exp() * &(&t * 3).cos();
let result = expr.laplace(&t, &s);
let d = format!("{result}");
assert!(
!result.has_unevaluated(),
"BUG: L{{exp(-t)·cos(3t)}} should succeed: {d}"
);
let v = result
.subs(&s, &ctx.int(1))
.eval()
.eval_f64()
.expect("result.subs(&s, &ctx.int(1)).eval().eval_f64() must evaluate");
let expected = 2.0 / 13.0;
assert!(
(v - expected).abs() < 1e-6,
"L{{exp(-t)·cos(3t)}} at s=1 should be {expected}, got: {v}"
);
}
#[test]
fn inverse_laplace_shifted_sin() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let denom = &(&s.powi(2) - &(&s * 2)) + &ctx.int(5);
let expr = &ctx.int(1) / &denom;
let result = expr.inverse_laplace(&s, &t);
let d = format!("{result}");
assert!(
!result.has_unevaluated(),
"BUG: L⁻¹{{1/(s²-2s+5)}} should not be unevaluated: {d}\n\
Expected: (1/2)·exp(t)·sin(2t)"
);
let v = result
.subs(&t, &ctx.int(0))
.eval()
.eval_f64()
.expect("result.subs(&t, &ctx.int(0)).eval().eval_f64() must evaluate");
assert!(
v.abs() < 1e-6,
"L⁻¹{{1/(s²-2s+5)}} at t=0 should be 0, got: {v}"
);
}
#[test]
fn inverse_laplace_shifted_cos() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let denom = &(&s.powi(2) - &(&s * 2)) + &ctx.int(5);
let numer = &s - 1;
let expr = &numer / &denom;
let result = expr.inverse_laplace(&s, &t);
let d = format!("{result}");
assert!(
!result.has_unevaluated(),
"BUG: L⁻¹{{(s-1)/(s²-2s+5)}} should not be unevaluated: {d}\n\
Expected: exp(t)·cos(2t)"
);
let v = result
.subs(&t, &ctx.int(0))
.eval()
.eval_f64()
.expect("result.subs(&t, &ctx.int(0)).eval().eval_f64() must evaluate");
assert!(
(v - 1.0).abs() < 1e-6,
"L⁻¹{{(s-1)/(s²-2s+5)}} at t=0 should be 1, got: {v}"
);
}
#[test]
fn inverse_laplace_n_over_s_power_n_roundtrip() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let expr = &ctx.int(24) / &s.powi(5);
let result = expr.inverse_laplace(&s, &t);
let d = format!("{result}");
assert!(
!result.has_unevaluated(),
"BUG: L⁻¹{{24/s⁵}} should not be unevaluated: {d}\n\
Expected: t⁴"
);
let v = result
.subs(&t, &ctx.int(2))
.eval()
.eval_f64()
.expect("result.subs(&t, &ctx.int(2)).eval().eval_f64() must evaluate");
assert!(
(v - 16.0).abs() < 1e-6,
"L⁻¹{{24/s⁵}} at t=2 should be 16 (=2⁴), got: {v}"
);
}
#[test]
fn ode_nonhomogeneous_y_pp_plus_y_eq_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let d2y = dy.formal_diff(&x);
let ode = &(&d2y + &y) - &x;
let sol = ode.solve_ode(&y, &x);
let s = format!("{sol}");
if !sol.has_unevaluated() {
let particular = x.clone();
assert!(
ode.check_ode_solution(&particular, &y, &x),
"y = x should satisfy y'' + y - x = 0, solution was: {s}"
);
}
}
#[test]
fn ode_y_prime_eq_minus_2xy() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let ode = &dy + &(&(&x * 2) * &y);
let sol = ode.solve_ode(&y, &x);
let s = format!("{sol}");
if !sol.has_unevaluated() {
let particular = (-&x.powi(2)).exp();
assert!(
ode.check_ode_solution(&particular, &y, &x),
"y = exp(-x²) should satisfy y' + 2x·y = 0, solution was: {s}"
);
}
}
#[test]
fn ode_y_prime_eq_y_squared_is_hard() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let ode = &dy - &y.powi(2);
let sol = ode.solve_ode(&y, &x);
let s = format!("{sol}");
if !sol.has_unevaluated() {
let particular = -&(&ctx.int(1) / &x);
assert!(
ode.check_ode_solution(&particular, &y, &x),
"y = -1/x should satisfy y' = y², solution was: {s}"
);
}
}
#[test]
fn series_1_over_1_minus_x_coefficients_exact() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let expr = &ctx.int(1) / &(&ctx.int(1) - &x);
let s = expr.series(&x, &zero, 5);
let expanded = s.expand().eval();
let val = expanded.subs(&x, &ctx.rational(1, 10)).eval();
let v = val.eval_f64().expect("val.eval_f64() must evaluate");
let expected = 1.0 + 0.1 + 0.01 + 0.001 + 0.0001;
assert!(
(v - expected).abs() < 1e-10,
"1/(1-x) 5-term series at x=0.1 should be exactly {expected}, got: {v}"
);
}
#[test]
fn series_exp_x_coefficient_check() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let s = x.exp().series(&x, &zero, 5);
let expanded = s.expand().eval();
let val = expanded.subs(&x, &ctx.int(1)).eval();
let v = val.eval_f64().expect("val.eval_f64() must evaluate");
let expected = 1.0 + 1.0 + 0.5 + 1.0 / 6.0 + 1.0 / 24.0;
assert!(
(v - expected).abs() < 1e-10,
"exp(x) 5-term series at x=1: expected {expected}, got {v}"
);
}
#[test]
fn series_sin_x_odd_terms_only() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let s = x.sin().series(&x, &zero, 6);
let expanded = s.expand().eval();
let result = format!("{expanded}");
assert!(
!result.contains("x^2") || result.contains("x^2") && result.contains("0*x^2"),
"sin(x) series should have no x² term: {result}"
);
}
#[test]
fn series_cos_x_even_terms_only() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let s = x.cos().series(&x, &zero, 6);
let expanded = s.expand().eval();
let result = format!("{expanded}");
assert!(
result.contains("x^2"),
"cos(x) series should contain x^2 term: {result}"
);
assert!(
result.contains("x^4"),
"cos(x) series should contain x^4 term: {result}"
);
}
#[test]
fn definite_cos_0_to_2pi_equals_0() {
let ctx = Context::new();
let x = ctx.symbol("x");
let two_pi = &ctx.int(2) * &ctx.pi();
let result = x.cos().integrate_definite(&x, &ctx.int(0), &two_pi);
let evaled = result.eval();
let s = format!("{evaled}");
assert_eq!(s, "0", "∫₀^{{2π}} cos(x)dx should be 0, got: {s}");
}
#[test]
fn definite_x_exp_neg_x_0_to_inf_is_hard() {
let ctx = Context::new();
let x = ctx.symbol("x");
let integrand = &x * &(-&x).exp();
let antideriv = integrand.integrate(&x);
let s = format!("{antideriv}");
if !antideriv.has_unevaluated() {
let deriv = antideriv.diff(&x);
let pt = ctx.rational(1, 1);
let orig = integrand.subs(&x, &pt).eval().eval_f64();
let dval = deriv.subs(&x, &pt).eval().eval_f64();
if let (Ok(o), Ok(d)) = (orig, dval) {
assert!(
(o - d).abs() < 1e-6,
"FTC for x·exp(-x) at x=1: integrand={o}, deriv={d}, antideriv='{s}'"
);
}
}
}
#[test]
fn integrate_1_over_x_plus_1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let integrand = &ctx.int(1) / &(&x + 1);
let (s, is_uneval) = ftc_check(&integrand, &x, &[0.5, 1.0, 2.0, 3.0], 1e-6, "∫1/(x+1)dx");
if is_uneval {
panic!(
"BUG: ∫1/(x+1)dx returned unevaluated form: {s}\n\
Expected: ln|x+1| or equivalent"
);
}
}
#[test]
fn integrate_x_over_x_squared_plus_1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let integrand = &x / &(&x.powi(2) + &ctx.int(1));
let (s, is_uneval) = ftc_check(&integrand, &x, &[0.5, 1.0, 2.0], 1e-6, "∫x/(x²+1)dx");
if is_uneval {
panic!(
"BUG: ∫x/(x²+1)dx returned unevaluated form: {s}\n\
Expected: (1/2)·ln(x²+1) or equivalent"
);
}
}
#[test]
fn integrate_partial_fractions_1_over_x2_minus_1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let integrand = &ctx.int(1) / &(&x.powi(2) - &ctx.int(1));
let (s, is_uneval) = ftc_check(
&integrand,
&x,
&[2.0, 3.0, 5.0], 1e-6,
"∫1/(x²-1)dx",
);
if is_uneval {
panic!(
"BUG: ∫1/(x²-1)dx returned unevaluated form: {s}\n\
Expected: partial fraction result"
);
}
}
#[test]
fn definite_integral_matches_antideriv_evaluation() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = &x.powi(3) + &x.sin();
let a = ctx.int(1);
let b = ctx.int(2);
let direct = f.integrate_definite(&x, &a, &b);
let antideriv = f.integrate(&x);
let manual = &antideriv.subs(&x, &b) - &antideriv.subs(&x, &a);
let dv = direct.eval().eval_f64();
let mv = manual.eval().eval_f64();
if let (Ok(d), Ok(m)) = (dv, mv) {
assert!(
(d - m).abs() < 1e-8,
"definite_integral vs manual F(b)-F(a) mismatch: direct={d}, manual={m}"
);
}
}
#[test]
fn z_transform_2_to_n_partial_sum_check() {
let ctx = Context::new();
let n = ctx.symbol("n");
let z = ctx.symbol("z");
let expr = ctx.int(2).pow(&n);
let result = expr.z_transform(&n, &z).expect("Z{2^n} should succeed");
let z_val = 5.0_f64;
let mut partial_sum = 0.0;
for k in 0..60 {
partial_sum += 2.0_f64.powi(k) * z_val.powi(-k);
}
let v = result
.subs(&z, &ctx.int(5))
.eval()
.eval_f64()
.expect("result.subs(&z, &ctx.int(5)).eval().eval_f64() must evaluate");
assert!(
(v - partial_sum).abs() < 1e-6,
"Z{{2^n}} at z=5: closed form={v}, partial sum={partial_sum}"
);
}
#[test]
fn ode_y_prime_3y_eq_0_numerical_check() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let ode = &dy + &(&y * 3);
let sol = ode.solve_ode(&y, &x);
if sol.has_unevaluated() {
panic!("y' + 3y = 0 should be solvable");
}
let c1 = ctx.symbol("C1");
let particular = sol.subs(&c1, &ctx.int(1));
let y_val = particular.clone();
let yp_val = particular.diff(&x);
for &xv in &[0.1_f64, 0.5, 1.0, 2.0] {
let numer = (xv * 10000.0).round() as i64;
let pt = ctx.rational(numer, 10000);
let yv = y_val.subs(&x, &pt).eval().eval_f64();
let ypv = yp_val.subs(&x, &pt).eval().eval_f64();
if let (Ok(y_f), Ok(yp_f)) = (yv, ypv) {
let residual = yp_f + 3.0 * y_f;
assert!(
residual.abs() < 1e-6,
"ODE residual y'+3y at x={xv}: y'={yp_f}, y={y_f}, residual={residual}"
);
}
}
}
#[test]
fn integrate_linearity_sum() {
let ctx = Context::new();
let x = ctx.symbol("x");
let combined = (&x.sin() + &x.cos()).integrate(&x);
let separate_sin = x.sin().integrate(&x);
let separate_cos = x.cos().integrate(&x);
let separate_sum = &separate_sin + &separate_cos;
let cv = combined.subs(&x, &ctx.int(1)).eval().eval_f64();
let sv = separate_sum.subs(&x, &ctx.int(1)).eval().eval_f64();
if let (Ok(c), Ok(s)) = (cv, sv) {
assert!(
(c - s).abs() < 1e-8,
"∫(sin+cos)dx vs ∫sin dx + ∫cos dx: combined={c}, separate={s}"
);
}
}
#[test]
fn integrate_linearity_scalar() {
let ctx = Context::new();
let x = ctx.symbol("x");
let combined = (&x.powi(2) * 5).integrate(&x);
let scaled = &x.powi(2).integrate(&x) * 5;
let cv = combined.subs(&x, &ctx.int(2)).eval().eval_f64();
let sv = scaled.subs(&x, &ctx.int(2)).eval().eval_f64();
if let (Ok(c), Ok(s)) = (cv, sv) {
assert!(
(c - s).abs() < 1e-8,
"∫5x²dx vs 5·∫x²dx: combined={c}, scaled={s}"
);
}
}
#[test]
fn convergence_p_series_p_exactly_1_diverges() {
let ctx = Context::new();
let k = ctx.symbol("k");
let body = k.pow(&ctx.int(-1)); assert_eq!(
body.is_convergent(&k),
Some(false),
"Σ k^(-1) (harmonic) should diverge"
);
}
#[test]
fn convergence_p_series_p_just_above_1() {
let ctx = Context::new();
let k = ctx.symbol("k");
let body = k.pow(&ctx.int(-2));
assert_eq!(
body.is_convergent(&k),
Some(true),
"Σ k^(-2) should converge"
);
}
#[test]
fn convergence_geometric_neg_one_diverges() {
let ctx = Context::new();
let k = ctx.symbol("k");
let body = ctx.int(-1).pow(&k);
let result = body.is_convergent(&k);
assert_eq!(
result,
Some(false),
"Σ (-1)^k should diverge (|r|=1), got: {result:?}"
);
}
#[test]
fn series_of_diff_equals_diff_of_series() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
let f = x.exp();
let order = 6u32;
let series_then_diff = f.series(&x, &zero, order).expand().eval().diff(&x);
let diff_then_series = f.diff(&x).series(&x, &zero, order - 1).expand().eval();
let v1 = series_then_diff
.subs(&x, &ctx.rational(1, 2))
.eval()
.eval_f64();
let v2 = diff_then_series
.subs(&x, &ctx.rational(1, 2))
.eval()
.eval_f64();
if let (Ok(a), Ok(b)) = (v1, v2) {
assert!(
(a - b).abs() < 0.01,
"d/dx(series(exp)) vs series(d/dx(exp)) at x=0.5: {a} vs {b}"
);
}
}
#[test]
fn laplace_roundtrip_sinh() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let original = (&t * 2).sinh();
let forward = original.laplace(&t, &s);
assert!(
!forward.has_unevaluated(),
"L{{sinh(2t)}} should succeed: {forward}"
);
let recovered = forward.inverse_laplace(&s, &t);
let d = format!("{recovered}");
if !recovered.has_unevaluated() {
let ov = original.subs(&t, &ctx.int(1)).eval().eval_f64();
let rv = recovered.subs(&t, &ctx.int(1)).eval().eval_f64();
if let (Ok(o), Ok(r)) = (ov, rv) {
assert!(
(o - r).abs() < 1e-4,
"Laplace roundtrip sinh(2t): original={o}, recovered={r}"
);
}
} else {
panic!(
"BUG: Laplace roundtrip of sinh(2t) gave unevaluated: {d}\n\
L{{sinh(2t)}} = {forward}"
);
}
}
#[test]
fn laplace_roundtrip_cosh() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let original = (&t * 3).cosh();
let forward = original.laplace(&t, &s);
assert!(
!forward.has_unevaluated(),
"L{{cosh(3t)}} should succeed: {forward}"
);
let recovered = forward.inverse_laplace(&s, &t);
let d = format!("{recovered}");
if !recovered.has_unevaluated() {
let ov = original.subs(&t, &ctx.int(1)).eval().eval_f64();
let rv = recovered.subs(&t, &ctx.int(1)).eval().eval_f64();
if let (Ok(o), Ok(r)) = (ov, rv) {
assert!(
(o - r).abs() < 1e-4,
"Laplace roundtrip cosh(3t): original={o}, recovered={r}"
);
}
} else {
panic!(
"BUG: Laplace roundtrip of cosh(3t) gave unevaluated: {d}\n\
L{{cosh(3t)}} = {forward}"
);
}
}