use symplex::prelude::*;
fn verify_first_order_numerically(
ode_expr: &Ex,
solution: &Ex,
constants: &[Ex],
y: &Ex,
x: &Ex,
sample_x_num: i64,
sample_x_den: i64,
) {
let ctx = ode_expr.context();
let one = ctx.int(1);
let mut concrete_sol = solution.clone();
for c in constants {
concrete_sol = concrete_sol.subs(c, &one);
}
let sol_prime = concrete_sol.diff(x);
let dy_formal = y.formal_diff(x);
let residual = ode_expr.subs(&dy_formal, &sol_prime).subs(y, &concrete_sol);
let sample_val = ctx.rational(sample_x_num, sample_x_den);
let residual_at = residual.subs(x, &sample_val);
let val = residual_at
.eval_f64()
.expect("evalf_f64 should succeed for first-order ODE residual evaluation");
assert!(
val.abs() < 1e-6,
"First-order ODE residual should be ~0, got {val} at x={sample_x_num}/{sample_x_den}\n \
solution (C1=1): {concrete_sol}\n residual: {residual_at}"
);
}
fn verify_second_order_numerically(
ode_expr: &Ex,
solution: &Ex,
constants: &[Ex],
y: &Ex,
x: &Ex,
sample_x_num: i64,
sample_x_den: i64,
) {
let ctx = ode_expr.context();
let one = ctx.int(1);
let mut concrete_sol = solution.clone();
for c in constants {
concrete_sol = concrete_sol.subs(c, &one);
}
let sol_prime = concrete_sol.diff(x);
let sol_double_prime = sol_prime.diff(x);
let dy_formal = y.formal_diff(x);
let d2y_formal = dy_formal.formal_diff(x);
let residual = ode_expr
.subs(&d2y_formal, &sol_double_prime)
.subs(&dy_formal, &sol_prime)
.subs(y, &concrete_sol);
let sample_val = ctx.rational(sample_x_num, sample_x_den);
let residual_at = residual.subs(x, &sample_val);
let val = residual_at
.eval_f64()
.expect("evalf_f64 should succeed for second-order ODE residual evaluation");
assert!(
val.abs() < 1e-4,
"Second-order ODE residual should be ~0, got {val} at x={sample_x_num}/{sample_x_den}\n \
solution (C1=C2=1): {concrete_sol}\n residual: {residual_at}"
);
}
#[test]
fn separable_dy_dx_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.try_solve_ode(&y, &x).expect("should solve y' = x");
let s = format!("{sol}");
assert!(s.contains("C1"), "solution should have C1: {s}");
let c1 = ctx.symbol("C1");
verify_first_order_numerically(&ode, &sol, &[c1], &y, &x, 3, 2);
}
#[test]
fn separable_dy_dx_eq_zero() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let ode = y.formal_diff(&x);
let sol = ode.try_solve_ode(&y, &x).expect("should solve y' = 0");
let s = format!("{sol}");
assert!(s.contains("C1"), "solution should be C1: {s}");
}
#[test]
fn separable_dy_dx_eq_sin_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.sin();
let sol = ode.try_solve_ode(&y, &x).expect("should solve y' = sin(x)");
let s = format!("{sol}");
assert!(s.contains("C1"), "solution should have C1: {s}");
let c1 = ctx.symbol("C1");
verify_first_order_numerically(&ode, &sol, &[c1], &y, &x, 7, 10);
}
#[test]
fn separable_dy_dx_eq_constant() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let three = ctx.int(3);
let ode = &dy - &three;
let sol = ode.try_solve_ode(&y, &x).expect("should solve y' = 3");
let s = format!("{sol}");
assert!(s.contains("C1"), "solution should have C1: {s}");
let c1 = ctx.symbol("C1");
verify_first_order_numerically(&ode, &sol, &[c1], &y, &x, 2, 1);
}
#[test]
fn first_order_linear_exponential_decay() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let ode = &dy + &(&y * 2);
let sol = ode.try_solve_ode(&y, &x).expect("should solve y' + 2y = 0");
let s = format!("{sol}");
assert!(s.contains("exp"), "solution should contain exp: {s}");
assert!(s.contains("C1"), "solution should have C1: {s}");
let c1 = ctx.symbol("C1");
verify_first_order_numerically(&ode, &sol, &[c1], &y, &x, 1, 2);
}
#[test]
fn first_order_linear_exponential_growth() {
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.try_solve_ode(&y, &x).expect("should solve y' - y = 0");
let s = format!("{sol}");
assert!(s.contains("exp"), "solution should contain exp: {s}");
assert!(s.contains("C1"), "solution should have C1: {s}");
let c1 = ctx.symbol("C1");
verify_first_order_numerically(&ode, &sol, &[c1], &y, &x, 1, 1);
}
#[test]
fn second_order_distinct_real_roots() {
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
.try_solve_ode(&y, &x)
.expect("should solve y'' - 3y' + 2y = 0");
let s = format!("{sol}");
assert!(s.contains("C1"), "solution should have C1: {s}");
assert!(s.contains("C2"), "solution should have C2: {s}");
assert!(s.contains("exp"), "solution should contain exp: {s}");
let c1 = ctx.symbol("C1");
let c2 = ctx.symbol("C2");
verify_second_order_numerically(&ode, &sol, &[c1, c2], &y, &x, 3, 10);
}
#[test]
fn second_order_repeated_root() {
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
.try_solve_ode(&y, &x)
.expect("should solve y'' - 2y' + y = 0");
let s = format!("{sol}");
assert!(s.contains("C1"), "solution should have C1: {s}");
assert!(s.contains("C2"), "solution should have C2: {s}");
assert!(s.contains("exp"), "solution should contain exp: {s}");
assert!(
s.contains("x"),
"repeated root solution should contain x: {s}"
);
let c1 = ctx.symbol("C1");
let c2 = ctx.symbol("C2");
verify_second_order_numerically(&ode, &sol, &[c1, c2], &y, &x, 2, 5);
}
#[test]
fn second_order_complex_roots() {
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);
if !sol.has_unevaluated() {
let s = format!("{sol}");
assert!(
s.contains("C1") && s.contains("C2"),
"should have two constants: {s}"
);
assert!(
s.contains("exp") || s.contains("sin") || s.contains("cos"),
"should contain exp or trig: {s}"
);
}
}
#[test]
fn second_order_distinct_real_negative_roots() {
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 * 5) + &(&y * 6);
let sol = ode
.try_solve_ode(&y, &x)
.expect("should solve y'' + 5y' + 6y = 0");
let s = format!("{sol}");
assert!(s.contains("C1"), "solution should have C1: {s}");
assert!(s.contains("C2"), "solution should have C2: {s}");
assert!(s.contains("exp"), "solution should contain exp: {s}");
let c1 = ctx.symbol("C1");
let c2 = ctx.symbol("C2");
verify_second_order_numerically(&ode, &sol, &[c1, c2], &y, &x, 1, 4);
}
#[test]
fn no_derivative_returns_none() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let expr = &x + &y;
let result = expr.solve_ode(&y, &x);
assert!(
result.has_unevaluated(),
"expression without derivative should return None"
);
}
#[test]
fn pure_number_returns_none() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let expr = ctx.int(42);
let result = expr.solve_ode(&y, &x);
assert!(result.has_unevaluated(), "pure number should return None");
}
#[test]
fn expr_macro_separable_ode() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let ode = expr!(ctx, diff(y, x) - x);
let sol = ode
.try_solve_ode(&y, &x)
.expect("expr! separable ODE should solve");
let s = format!("{sol}");
assert!(s.contains("C1"), "should have C1: {s}");
}
#[test]
fn expr_macro_first_order_linear_ode() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let ode = expr!(ctx, diff(y, x) + 2 * y);
let sol = ode
.try_solve_ode(&y, &x)
.expect("expr! first-order linear ODE should solve");
let s = format!("{sol}");
assert!(
s.contains("exp") && s.contains("C1"),
"solution should be C1*exp(-2x): {s}"
);
}