use symplex::expr::ExprType;
use symplex::ode::OdeType;
use symplex::prelude::*;
#[allow(dead_code)]
fn verify_first_order(
ode_expr: &Ex,
solution: &Ex,
constants: &[Ex],
y: &Ex,
x: &Ex,
sample_x_num: i64,
sample_x_den: i64,
) {
let ctx = Context::new();
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("residual should evaluate to f64");
assert!(
val.abs() < 1e-4,
"First-order ODE residual should be ~0, got {val} at x={sample_x_num}/{sample_x_den}\n \
solution (C=1): {concrete_sol}\n residual expr: {residual_at}"
);
}
#[test]
fn ode_homogeneous_coeff_basic() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let one = ctx.int(1);
let y_over_x = &y / &x;
let ode = &dy - &one - &y_over_x;
let sol = ode.solve_ode(&y, &x);
let s_check = format!("{sol}");
assert!(
sol.expr_type() != ExprType::Unevaluated,
"should solve y' = (x+y)/x, got: {s_check}"
);
let s = format!("{sol}");
assert!(s.contains("C1"), "solution should contain constant C1: {s}");
assert!(
s.contains("x") || s.contains("ln"),
"solution should reference x or ln: {s}"
);
}
#[test]
fn ode_homogeneous_coeff_quadratic() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let x_sq = x.powi(2);
let y_sq = y.powi(2);
let numer = &x_sq + &y_sq;
let denom = &x * &y;
let rhs = &numer / &denom;
let ode = &dy - &rhs;
let sol = ode.solve_ode(&y, &x);
let s_check = format!("{sol}");
assert!(
sol.expr_type() != ExprType::Unevaluated,
"should solve y' = (x²+y²)/(xy), got: {s_check}"
);
let s = format!("{sol}");
assert!(s.contains("C1"), "solution should contain constant C1: {s}");
}
#[test]
fn ode_classify_homogeneous() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let x_sq = x.powi(2);
let y_sq = y.powi(2);
let rhs = &(&x_sq + &y_sq) / &x_sq; let ode = &dy - &rhs;
let ode_type = ode.classify_ode(&y, &x);
assert_eq!(
ode_type,
OdeType::HomogeneousCoefficient,
"y' = (x²+y²)/x² should be classified as HomogeneousCoefficient, got {ode_type:?}"
);
}
#[test]
fn ode_nth_reducible_basic() {
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;
let sol = ode.solve_ode(&y, &x);
let s_check = format!("{sol}");
assert!(
sol.expr_type() != ExprType::Unevaluated,
"should solve y'' - y' = 0 via nth-order reducible, got: {s_check}"
);
let s = format!("{sol}");
assert!(
s.contains("C1") || s.contains("C2"),
"solution should contain constants: {s}"
);
}
#[test]
fn ode_nth_reducible_nonlinear() {
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 dy_sq = dy.powi(2);
let y_d2y = &y * &d2y;
let ode = &y_d2y - &dy_sq;
let ode_type = ode.classify_ode(&y, &x);
assert_eq!(
ode_type,
OdeType::NthOrderReducible,
"y·y'' = (y')² should classify as NthOrderReducible, got {ode_type:?}"
);
let sol = ode.solve_ode(&y, &x);
if sol.expr_type() != ExprType::Unevaluated {
let s = format!("{sol}");
assert!(
s.contains("C1") || s.contains("C2"),
"solution should contain constants: {s}"
);
}
}
#[test]
fn integrate_ln_ln_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let ln_x = x.ln();
let ln_ln_x = ln_x.ln();
let result = ln_ln_x.integrate(&x);
let s = format!("{result}");
assert!(
!s.contains("Integral("),
"∫ ln(ln(x)) dx should not be unevaluated, got: {s}"
);
assert!(
s.contains("li") || s.contains("ln"),
"result should contain li or ln: {s}"
);
}
#[test]
fn no_regression_existing_odes() {
let ctx = Context::new();
{
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let ode = expr!(ctx, diff(y, x) - x);
let result = ode.solve_ode(&y, &x);
assert!(
result.expr_type() != ExprType::Unevaluated,
"y' = x should still work (simple separable)"
);
}
{
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let ode = expr!(ctx, diff(y, x) + 2 * y);
let result = ode.solve_ode(&y, &x);
assert!(
result.expr_type() != ExprType::Unevaluated,
"y' + 2y = 0 should still work (linear CC)"
);
let sol = result;
let s = format!("{sol}");
assert!(s.contains("C1"), "should have constant: {s}");
assert!(s.contains("exp"), "should contain exp: {s}");
}
{
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 result = ode.solve_ode(&y, &x);
assert!(
result.expr_type() != ExprType::Unevaluated,
"y'' + y = 0 should still work"
);
let sol = result;
let s = format!("{sol}");
assert!(
s.contains("C1") && s.contains("C2"),
"should have two constants: {s}"
);
assert!(
s.contains("cos") && s.contains("sin"),
"should use trig form: {s}"
);
}
{
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let ode = expr!(ctx, diff(y, x) - x * y);
let result = ode.solve_ode(&y, &x);
assert!(
result.expr_type() != ExprType::Unevaluated,
"y' = xy should still work (full separable)"
);
let sol = result;
let s = format!("{sol}");
assert!(s.contains("C1"), "should have constant: {s}");
assert!(s.contains("exp"), "should contain exp: {s}");
}
{
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let ode = expr!(ctx, diff(y, x) + 2 * x * y);
let result = ode.solve_ode(&y, &x);
assert!(
result.expr_type() != ExprType::Unevaluated,
"y' + 2xy = 0 should still work"
);
}
}
#[test]
fn no_regression_classification_simple_separable() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let ode = expr!(ctx, diff(y, x) - x);
let ode_type = ode.classify_ode(&y, &x);
assert_eq!(
ode_type,
OdeType::SimpleSeparable,
"y' = x should classify as SimpleSeparable"
);
}
#[test]
fn no_regression_classification_bernoulli() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let y_sq = y.powi(2);
let ode = &(&dy + &y) - &y_sq;
let ode_type = ode.classify_ode(&y, &x);
assert_eq!(
ode_type,
OdeType::Bernoulli,
"y' + y = y² should classify as Bernoulli, got {ode_type:?}"
);
}