use super::*;
use crate::integrate::special::basis_functions_used;
use crate::kernel::{Domain, ExprPool};
fn setup() -> (ExprPool, ExprId, ExprId) {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let y = p.symbol("y", Domain::Real);
(p, x, y)
}
fn assert_verifies(input: &OdeInput, sol: &DsolveSolution, pool: &ExprPool) {
residual_is_zero(input, sol.y_of_x, &sol.constants, pool)
.unwrap_or_else(|e| panic!("returned solution failed verification: {e}"));
}
#[test]
fn separable_logistic() {
let (p, x, y) = setup();
let (input, yp) = OdeInput::first_order(x, y, &p);
let one = p.integer(1_i32);
let one_minus_y = p.add(vec![one, p.mul(vec![p.integer(-1_i32), y])]);
let rhs = p.mul(vec![y, one_minus_y]);
let eq = p.add(vec![yp, p.mul(vec![p.integer(-1_i32), rhs])]);
let input = input.with_equation(eq);
let res = dsolve(&input, &p).expect("logistic should solve");
assert!(!res.solutions.is_empty());
assert_verifies(&input, &res.solutions[0], &p);
}
#[test]
fn separable_exponential() {
let (p, x, y) = setup();
let (input, yp) = OdeInput::first_order(x, y, &p);
let eq = p.add(vec![yp, p.mul(vec![p.integer(-1_i32), y])]);
let input = input.with_equation(eq);
let res = dsolve(&input, &p).expect("y'=y should solve");
assert_verifies(&input, &res.solutions[0], &p);
}
#[test]
fn linear_first_order() {
let (p, x, y) = setup();
let (input, yp) = OdeInput::first_order(x, y, &p);
let eq = p.add(vec![
yp,
p.mul(vec![p.integer(-3_i32), y]),
p.mul(vec![p.integer(-1_i32), x]),
]);
let input = input.with_equation(eq);
let res = dsolve(&input, &p).expect("linear first order should solve");
assert_verifies(&input, &res.solutions[0], &p);
}
#[test]
fn bernoulli_first_order() {
let (p, x, y) = setup();
let (input, yp) = OdeInput::first_order(x, y, &p);
let y2 = p.pow(y, p.integer(2_i32));
let eq = p.add(vec![yp, y, p.mul(vec![p.integer(-1_i32), y2])]);
let input = input.with_equation(eq);
let res = dsolve(&input, &p).expect("Bernoulli should solve");
assert_verifies(&input, &res.solutions[0], &p);
}
#[test]
fn exact_first_order() {
let (p, x, y) = setup();
let (input, yp) = OdeInput::first_order(x, y, &p);
let m = p.add(vec![p.mul(vec![p.integer(2_i32), x]), y]); let n = p.add(vec![x, p.mul(vec![p.integer(2_i32), y])]); let eq = p.add(vec![m, p.mul(vec![n, yp])]);
let input = input.with_equation(eq);
let res = dsolve(&input, &p).expect("exact should solve");
assert_verifies(&input, &res.solutions[0], &p);
}
#[test]
fn homogeneous_first_order() {
let (p, x, y) = setup();
let (input, yp) = OdeInput::first_order(x, y, &p);
let rhs = p.add(vec![p.integer(1_i32), div(y, x, &p)]);
let eq = p.add(vec![yp, p.mul(vec![p.integer(-1_i32), rhs])]);
let input = input.with_equation(eq);
let res = dsolve(&input, &p).expect("homogeneous should solve");
assert_verifies(&input, &res.solutions[0], &p);
}
#[test]
fn clairaut_first_order() {
let (p, x, y) = setup();
let (input, yp) = OdeInput::first_order(x, y, &p);
let yp2 = p.pow(yp, p.integer(2_i32));
let eq = p.add(vec![
y,
p.mul(vec![p.integer(-1_i32), x, yp]),
p.mul(vec![p.integer(-1_i32), yp2]),
]);
let input = input.with_equation(eq);
let res = dsolve(&input, &p).expect("Clairaut should solve");
assert_eq!(res.solutions[0].method, "clairaut");
assert_verifies(&input, &res.solutions[0], &p);
}
#[test]
fn riccati_with_polynomial_particular() {
let (p, x, y) = setup();
let (input, yp) = OdeInput::first_order(x, y, &p);
let y2 = p.pow(y, p.integer(2_i32));
let x2 = p.pow(x, p.integer(2_i32));
let rhs = p.add(vec![
y2,
p.mul(vec![p.integer(-1_i32), x2]),
p.integer(1_i32),
]);
let eq = p.add(vec![yp, p.mul(vec![p.integer(-1_i32), rhs])]);
let input = input.with_equation(eq);
match dsolve(&input, &p) {
Ok(res) => assert_verifies(&input, &res.solutions[0], &p),
Err(DsolveError::Unsupported(_)) => {}
Err(e) => panic!("unexpected error: {e}"),
}
}
#[test]
fn riccati_declined_without_particular() {
let (p, x, y) = setup();
let (input, yp) = OdeInput::first_order(x, y, &p);
let y2 = p.pow(y, p.integer(2_i32));
let rhs = p.add(vec![y2, x]);
let eq = p.add(vec![yp, p.mul(vec![p.integer(-1_i32), rhs])]);
let input = input.with_equation(eq);
assert!(
dsolve(&input, &p).is_err(),
"should decline Riccati w/o particular"
);
}
#[test]
fn harmonic_oscillator() {
let (p, x, y) = setup();
let (input, _yp, ypp) = OdeInput::second_order(x, y, &p);
let eq = p.add(vec![ypp, y]);
let input = input.with_equation(eq);
let res = dsolve(&input, &p).expect("harmonic oscillator should solve");
assert_eq!(res.solutions[0].constants.len(), 2);
assert_verifies(&input, &res.solutions[0], &p);
}
#[test]
fn real_distinct_roots() {
let (p, x, y) = setup();
let (input, yp, ypp) = OdeInput::second_order(x, y, &p);
let eq = p.add(vec![
ypp,
p.mul(vec![p.integer(-3_i32), yp]),
p.mul(vec![p.integer(2_i32), y]),
]);
let input = input.with_equation(eq);
let res = dsolve(&input, &p).expect("distinct roots should solve");
assert_verifies(&input, &res.solutions[0], &p);
}
#[test]
fn repeated_root() {
let (p, x, y) = setup();
let (input, yp, ypp) = OdeInput::second_order(x, y, &p);
let eq = p.add(vec![ypp, p.mul(vec![p.integer(-2_i32), yp]), y]);
let input = input.with_equation(eq);
let res = dsolve(&input, &p).expect("repeated root should solve");
assert_verifies(&input, &res.solutions[0], &p);
}
#[test]
fn complex_roots() {
let (p, x, y) = setup();
let (input, yp, ypp) = OdeInput::second_order(x, y, &p);
let eq = p.add(vec![
ypp,
p.mul(vec![p.integer(2_i32), yp]),
p.mul(vec![p.integer(5_i32), y]),
]);
let input = input.with_equation(eq);
let res = dsolve(&input, &p).expect("complex roots should solve");
assert_verifies(&input, &res.solutions[0], &p);
}
#[test]
fn undetermined_coefficients_x_exp_x() {
let (p, x, y) = setup();
let (input, _yp, ypp) = OdeInput::second_order(x, y, &p);
let xex = p.mul(vec![x, p.func("exp", vec![x])]);
let eq = p.add(vec![
ypp,
p.mul(vec![p.integer(-1_i32), y]),
p.mul(vec![p.integer(-1_i32), xex]),
]);
let input = input.with_equation(eq);
let res = dsolve(&input, &p).expect("undetermined coefficients should solve");
assert_verifies(&input, &res.solutions[0], &p);
}
#[test]
fn variation_of_parameters_tan() {
let (p, x, y) = setup();
let (input, _yp, ypp) = OdeInput::second_order(x, y, &p);
let tanx = p.func("tan", vec![x]);
let eq = p.add(vec![ypp, y, p.mul(vec![p.integer(-1_i32), tanx])]);
let input = input.with_equation(eq);
match dsolve(&input, &p) {
Ok(res) => assert_verifies(&input, &res.solutions[0], &p),
Err(DsolveError::Unsupported(_)) => {} Err(e) => panic!("must decline, not error wrongly: {e}"),
}
}
#[test]
fn nonhomogeneous_polynomial_rhs() {
let (p, x, y) = setup();
let (input, _yp, ypp) = OdeInput::second_order(x, y, &p);
let rhs = p.add(vec![p.pow(x, p.integer(2_i32)), p.integer(1_i32)]);
let eq = p.add(vec![
ypp,
p.mul(vec![p.integer(-1_i32), y]),
p.mul(vec![p.integer(-1_i32), rhs]),
]);
let input = input.with_equation(eq);
let res = dsolve(&input, &p).expect("polynomial RHS should solve");
assert_verifies(&input, &res.solutions[0], &p);
}
#[test]
fn nonhomogeneous_nonresonant_exp() {
let (p, x, y) = setup();
let (input, _yp, ypp) = OdeInput::second_order(x, y, &p);
let e2x = p.func("exp", vec![p.mul(vec![p.integer(2_i32), x])]);
let eq = p.add(vec![
ypp,
p.mul(vec![p.integer(-1_i32), y]),
p.mul(vec![p.integer(-1_i32), e2x]),
]);
let input = input.with_equation(eq);
let res = dsolve(&input, &p).expect("non-resonant exp RHS should solve");
assert_verifies(&input, &res.solutions[0], &p);
}
#[test]
fn fourth_order_constant_coeff() {
let (p, x, y) = setup();
let (input, derivs) = OdeInput::higher_order(x, y, 4, &p);
let eq = p.add(vec![derivs[3], p.mul(vec![p.integer(-1_i32), y])]);
let input = input.with_equation(eq);
let res = dsolve(&input, &p).expect("fourth order should solve");
assert_eq!(res.solutions[0].constants.len(), 4);
assert_verifies(&input, &res.solutions[0], &p);
}
#[test]
fn euler_cauchy_distinct() {
let (p, x, y) = setup();
let (input, yp, ypp) = OdeInput::second_order(x, y, &p);
let x2 = p.pow(x, p.integer(2_i32));
let eq = p.add(vec![
p.mul(vec![x2, ypp]),
p.mul(vec![p.integer(2_i32), x, yp]),
p.mul(vec![p.integer(-2_i32), y]),
]);
let input = input.with_equation(eq);
let res = dsolve(&input, &p).expect("Euler-Cauchy should solve");
assert_eq!(res.solutions[0].method, "euler_cauchy");
assert_verifies(&input, &res.solutions[0], &p);
}
#[test]
fn euler_cauchy_repeated() {
let (p, x, y) = setup();
let (input, yp, ypp) = OdeInput::second_order(x, y, &p);
let x2 = p.pow(x, p.integer(2_i32));
let eq = p.add(vec![
p.mul(vec![x2, ypp]),
p.mul(vec![p.integer(-1_i32), x, yp]),
y,
]);
let input = input.with_equation(eq);
let res = dsolve(&input, &p).expect("Euler-Cauchy repeated should solve");
assert_verifies(&input, &res.solutions[0], &p);
}
#[test]
fn third_order_constant_coeff() {
let (p, x, y) = setup();
let (input, derivs) = OdeInput::higher_order(x, y, 3, &p);
let (yp, ypp, yppp) = (derivs[0], derivs[1], derivs[2]);
let eq = p.add(vec![
yppp,
p.mul(vec![p.integer(-6_i32), ypp]),
p.mul(vec![p.integer(11_i32), yp]),
p.mul(vec![p.integer(-6_i32), y]),
]);
let input = input.with_equation(eq);
let res = dsolve(&input, &p).expect("third order should solve");
assert_eq!(res.solutions[0].constants.len(), 3);
assert_verifies(&input, &res.solutions[0], &p);
}
#[test]
fn fresh_constants_avoid_user_symbols() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let y = p.symbol("y", Domain::Real);
let c1 = p.symbol("C1", Domain::Real);
let (input, _yp, ypp) = OdeInput::second_order(x, y, &p);
let zero_term = p.mul(vec![p.add(vec![c1, p.mul(vec![p.integer(-1_i32), c1])]), x]);
let eq = p.add(vec![ypp, y, zero_term]);
let input = input.with_equation(eq);
let res = dsolve(&input, &p).expect("should still solve");
for c in &res.solutions[0].constants {
assert_ne!(*c, c1, "fresh constant collided with user symbol C1");
}
}
fn solve_src(order: usize, src: &str) -> (ExprPool, OdeInput, DsolveSolution) {
let pool = ExprPool::new();
let input = super::corpus::build_ode(order, src, &pool).expect("corpus source parses");
let res = dsolve(&input, &pool).unwrap_or_else(|e| panic!("`{src}` should solve: {e}"));
let sol = res.solutions[0].clone();
residual_is_zero(&input, sol.y_of_x, &sol.constants, &pool)
.unwrap_or_else(|e| panic!("`{src}` returned an unverified solution: {e}"));
(pool, input, sol)
}
fn assert_declines(order: usize, src: &str) {
let pool = ExprPool::new();
let input = super::corpus::build_ode(order, src, &pool).expect("corpus source parses");
match dsolve(&input, &pool) {
Err(DsolveError::Unsupported(_)) => {}
Err(e) => panic!("`{src}` should decline as Unsupported, got {e}"),
Ok(res) => panic!(
"`{src}` should decline, but returned {}",
pool.display(res.solutions[0].y_of_x)
),
}
}
#[test]
fn vop_forcing_no_ansatz_can_express() {
for src in [
"ypp + y - 1/cos(x)", "ypp + y - tan(x)", "ypp + y - 1/sin(x)", "ypp + y - tan(x)/cos(x)", "ypp + y - 1/(1 + sin(x))", "ypp - 2*yp + y - exp(x)/x", "ypp - y - 1/(1 + exp(x))", "ypp - y - exp(x)/(1 + exp(x))",
] {
let (_, _, sol) = solve_src(2, src);
assert_eq!(sol.constants.len(), 2, "`{src}`: wrong constant count");
}
}
#[test]
fn vop_generalises_above_second_order() {
for src in ["yppp + yp - 1/cos(x)", "yppp + yp - tan(x)"] {
let (_, _, sol) = solve_src(3, src);
assert_eq!(sol.constants.len(), 3, "`{src}`: wrong constant count");
}
}
#[test]
fn euler_cauchy_nonhomogeneous() {
for src in [
"x^2*ypp + x*yp - y - x^2",
"x^2*ypp - 2*x*yp + 2*y - x^3",
"x^2*ypp + x*yp - y - log(x)",
"x^2*ypp - x*yp + y - x",
] {
let (_, _, sol) = solve_src(2, src);
assert_eq!(sol.method, "euler_cauchy");
assert_eq!(sol.constants.len(), 2, "`{src}`: wrong constant count");
}
}
#[test]
fn reduction_of_order_variable_coefficients() {
for src in [
"ypp - (2/x)*yp + (2/x^2)*y", "(1 - x^2)*ypp - 2*x*yp + 2*y", "x*ypp - (x + 1)*yp + y", "ypp - yp/x - x", ] {
let (_, _, sol) = solve_src(2, src);
assert_eq!(sol.method, "reduction_of_order");
assert_eq!(sol.constants.len(), 2, "`{src}`: wrong constant count");
}
}
#[test]
fn integrating_factor_folds_logarithms() {
for src in [
"yp - y/x - x*log(x)",
"yp + y*tan(x) - sin(x)",
"yp + y/(x*log(x)) - 1",
"yp + y/x - cos(x)/x",
"yp - 2*x*y - exp(x^2)*sin(x)",
"yp + y - x*exp(-x)",
] {
let (_, _, sol) = solve_src(1, src);
assert_eq!(sol.constants.len(), 1, "`{src}`: wrong constant count");
}
}
#[test]
fn quadrature_over_the_special_function_basis_closes() {
for (src, basis) in [
("ypp - y - 1/x", &["Ei"] as &[&str]),
("ypp + y - 1/x", &["Ci", "Si"]),
] {
let (pool, input, sol) = solve_src(2, src);
assert_eq!(sol.constants.len(), 2, "`{src}`: wrong constant count");
assert!(
!super::verify::certifies_symbolically(&input, sol.y_of_x, &pool),
"`{src}`: now certified symbolically — the comment above is stale",
);
assert_eq!(
basis_functions_used(sol.y_of_x, &pool),
basis,
"`{src}`: wrong special-function vocabulary in {}",
pool.display(sol.y_of_x)
);
for c in &sol.constants {
assert!(
super::contains(sol.y_of_x, *c, &pool),
"`{src}`: constant {} does not appear in the solution",
pool.display(*c)
);
}
}
}
#[test]
fn declines_when_the_quadrature_leaves_the_special_function_basis() {
assert_declines(2, "ypp + y - x/(1 + x^2)");
}
#[test]
fn general_solution_has_exactly_order_many_constants() {
for (order, src) in [
(2_usize, "ypp + y - 1/cos(x)"),
(2, "x^2*ypp + x*yp - y - log(x)"),
(2, "ypp - yp/x - x"),
(3, "yppp + yp - tan(x)"),
] {
let (pool, _, sol) = solve_src(order, src);
assert_eq!(sol.constants.len(), order, "`{src}`");
for c in &sol.constants {
assert!(
super::contains(sol.y_of_x, *c, &pool),
"`{src}`: constant {} does not appear in the solution",
pool.display(*c)
);
}
}
}