use super::*;
use crate::deriv::SideCondition;
use crate::errors::AlkahestError;
use crate::integrate::special::basis_functions_used;
use crate::kernel::{Domain, ExprPool};
use crate::simplify::assumptions::AssumptionContext;
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)
}
struct Branches {
solutions: Vec<DsolveBranch>,
}
fn solve_all(input: &OdeInput, pool: &ExprPool) -> Result<Branches, DsolveError> {
dsolve_with(input, &AssumptionContext::new(), pool).map(|r| Branches {
solutions: r.branches,
})
}
fn assert_verifies(input: &OdeInput, sol: &DsolveBranch, pool: &ExprPool) {
solution_is_verified(input, sol, pool)
.unwrap_or_else(|e| panic!("returned solution failed verification: {e}"));
}
fn explicit_of(sol: &DsolveBranch) -> ExprId {
sol.y_of_x()
.expect("expected an explicit y(x), got an implicit relation")
}
#[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 = solve_all(&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 = solve_all(&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 = solve_all(&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 = solve_all(&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 = solve_all(&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 = solve_all(&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 = solve_all(&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 solve_all(&input, &p) {
Ok(res) => assert_verifies(&input, &res.solutions[0], &p),
Err(ref e) if e.is_quadrature_failure() => {
assert_eq!(e.code(), "E-ODE-013");
let m = e.to_string();
assert!(
m.contains("riccati"),
"decline does not name the class: {m}"
);
assert!(
m.contains("exp(x^2)"),
"decline does not name the integral: {m}"
);
}
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!(
solve_all(&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 = solve_all(&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 = solve_all(&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 = solve_all(&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 = solve_all(&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 = solve_all(&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 solve_all(&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 = solve_all(&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 = solve_all(&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 = solve_all(&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 = solve_all(&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 = solve_all(&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 = solve_all(&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 = solve_all(&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, DsolveBranch) {
let pool = ExprPool::new();
let input = super::corpus::build_ode(order, src, &pool).expect("corpus source parses");
let res = solve_all(&input, &pool).unwrap_or_else(|e| panic!("`{src}` should solve: {e}"));
let sol = res.solutions[0].clone();
solution_is_verified(&input, &sol, &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 solve_all(&input, &pool) {
Err(DsolveError::Unsupported(_)) => {}
Err(e) => panic!("`{src}` should decline as Unsupported, got {e}"),
Ok(res) => panic!(
"`{src}` should decline, but returned {}",
res.solutions[0].render(&pool)
),
}
}
#[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, explicit_of(&sol), &pool),
"`{src}`: now certified symbolically — the comment above is stale",
);
assert_eq!(
basis_functions_used(explicit_of(&sol), &pool),
basis,
"`{src}`: wrong special-function vocabulary in {}",
pool.display(explicit_of(&sol))
);
for c in &sol.constants {
assert!(
super::contains(explicit_of(&sol), *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)");
}
fn solve1(src: &str) -> (ExprPool, OdeInput, DsolveBranch) {
solve_src(1, src)
}
#[test]
fn michaelis_menten_elimination() {
for src in ["(Km + y)*yp + Vm*y", "yp + Vm*y/(Km + y)"] {
let (pool, _, sol) = solve1(src);
assert_eq!(sol.method, "separable", "`{src}`");
let y_of_x = explicit_of(&sol);
assert!(
pool.display(y_of_x).to_string().contains("lambert_w"),
"`{src}`: expected a Lambert-W form, got {}",
pool.display(y_of_x)
);
assert_eq!(sol.constants.len(), 1, "`{src}`");
assert!(super::contains(y_of_x, sol.constants[0], &pool), "`{src}`");
}
}
#[test]
fn logistic_growth_with_symbolic_parameters() {
for src in ["yp - r*y*(1 - y/K)", "yp - k*y*(1 - y/K)"] {
let (pool, _, sol) = solve1(src);
assert_eq!(sol.constants.len(), 1, "`{src}`");
let y_of_x = explicit_of(&sol);
for name in ["K", "exp"] {
assert!(
pool.display(y_of_x).to_string().contains(name),
"`{src}`: expected `{name}` in {}",
pool.display(y_of_x)
);
}
}
let (_, _, sol) = solve1("yp - y*(1 - y)");
assert!(sol.is_explicit());
}
#[test]
fn linear_handles_symbolic_and_variable_coefficients() {
for (src, want) in [
("yp + p*y - q", "p"),
("yp + ke*y", "ke"),
("yp + y/x - x^2", "x"),
("yp - y/x - x*log(x)", "log"),
] {
let (pool, _, sol) = solve1(src);
let y_of_x = explicit_of(&sol);
assert!(
pool.display(y_of_x).to_string().contains(want),
"`{src}`: expected `{want}` in {}",
pool.display(y_of_x)
);
assert_eq!(sol.constants.len(), 1, "`{src}`");
}
}
#[test]
fn bernoulli_and_riccati_still_close() {
for src in ["yp + y - y^2", "yp - y - x*y^2", "yp - exp(x)*y^2"] {
let (_, _, sol) = solve1(src);
assert!(sol.is_explicit(), "`{src}`");
}
let (_, _, sol) = solve1("yp - y^2 + 2*x*y - x^2 - 1");
assert_eq!(sol.method, "riccati");
}
#[test]
fn exact_integrating_factor_rescues() {
for (src, method) in [
("(2*x*y) + (y^2 - 3*x^2)*yp", "exact_integrating_factor_y"),
("(x^2 + y^2 + x) + x*y*yp", "exact_integrating_factor_x"),
("y + (2*x - y*exp(y))*yp", "exact_integrating_factor_y"),
] {
let (_, _, sol) = solve1(src);
assert_eq!(sol.method, method, "`{src}`");
assert_eq!(sol.constants.len(), 1, "`{src}`");
}
let (_, _, sol) = solve1("(2*x + y) + (x + 2*y)*yp");
assert_eq!(sol.method, "exact");
}
#[test]
fn homogeneous_quotients_are_recognised() {
for src in [
"yp - (x^2 + y^2)/(x*y)",
"yp - (x + y)/(x - y)",
"yp - (x + 3*y)/(x - y)",
] {
let (_, _, sol) = solve1(src);
assert_eq!(sol.method, "homogeneous", "`{src}`");
}
let (_, _, sol) = solve1("yp*y*x - y^2 - x^2");
assert!(sol.is_explicit());
}
#[test]
fn implicit_solutions_are_labelled_rather_than_disguised() {
for src in [
"yp - sin(x)*sin(y)",
"exp(x)*sin(y) + exp(x)*cos(y)*yp",
"y + (2*x - y*exp(y))*yp",
] {
let (pool, input, sol) = solve1(src);
assert!(!sol.is_explicit(), "`{src}`: expected an implicit answer");
assert!(sol.y_of_x().is_none(), "`{src}`: y_of_x must be None");
let rel = sol
.implicit_relation()
.expect("an implicit solution carries a relation");
assert!(super::contains(rel, input.y, &pool), "`{src}`");
assert!(super::contains(rel, sol.constants[0], &pool), "`{src}`");
implicit_relation_is_zero(&input, rel, &sol.constants, &pool)
.unwrap_or_else(|e| panic!("`{src}`: relation does not verify: {e}"));
}
}
#[test]
fn implicit_only_answer_is_not_an_explicit_solution() {
let pool = ExprPool::new();
let input = super::corpus::build_ode(1, "exp(x)*sin(y) + exp(x)*cos(y)*yp", &pool)
.expect("corpus source parses");
let report = dsolve_with(&input, &AssumptionContext::new(), &pool)
.expect("dsolve_with reports the relation");
assert_eq!(report.branches.len(), 1);
assert!(!report.branches[0].is_explicit());
match dsolve(&input, &pool) {
Ok(res) => panic!(
"dsolve must not report an implicit relation as y(x); got {} branch(es)",
res.solutions.len()
),
Err(e) => {
let m = e.to_string();
assert!(m.contains("implicit"), "decline does not say why: {m}");
assert!(m.contains("dsolve_with"), "decline does not say where: {m}");
}
}
let lin = super::corpus::build_ode(1, "yp - y", &pool).expect("corpus source parses");
let res = dsolve(&lin, &pool).expect("y' = y is explicit");
assert_eq!(res.solutions.len(), 1);
residual_is_zero(
&lin,
res.solutions[0].y_of_x,
&res.solutions[0].constants,
&pool,
)
.expect("dsolve's explicit answer verifies");
}
#[test]
fn an_explicit_answer_wins_over_an_implicit_one() {
let (pool, _, sol) = solve1("yp - exp(x)*y^2");
assert!(sol.is_explicit());
assert_eq!(sol.method, "bernoulli");
assert_eq!(sol.constants.len(), 1);
assert_eq!(pool.display(sol.constants[0]).to_string(), "C1");
}
fn implicit_fixture(relation_src: &str) -> (ExprPool, OdeInput, ExprId, ExprId) {
use crate::parse::parse;
use std::collections::HashMap;
let pool = ExprPool::new();
let input = super::corpus::build_ode(1, "yp - y", &pool).expect("parses");
let c1 = pool.symbol("C1", Domain::Real);
let mut syms: HashMap<String, ExprId> = HashMap::new();
syms.insert("x".to_owned(), input.x);
syms.insert("y".to_owned(), input.y);
syms.insert("C1".to_owned(), c1);
let rel = parse(relation_src, &pool, &mut syms).expect("relation parses");
let rel = simp(rel, &pool);
(pool, input, rel, c1)
}
#[test]
fn the_implicit_gate_certifies_a_relation_and_refuses_a_wrong_one() {
let (pool, input, rel, c1) = implicit_fixture("log(y) - x - C1");
implicit_relation_is_zero(&input, rel, &[c1], &pool).expect("the true relation must certify");
for src in [
"log(y) - 2*x - C1", "y^2 + x^2 - C1", "log(y) - x", "x - C1", ] {
let (pool, input, rel, c1) = implicit_fixture(src);
assert!(
implicit_relation_is_zero(&input, rel, &[c1], &pool).is_err(),
"`{src}` must not certify as a general solution of y' = y"
);
}
}
#[test]
fn the_implicit_gate_refuses_a_slope_field_that_mentions_the_constant() {
let (pool, input, rel, c1) = implicit_fixture("y - C1*exp(x)");
let err = implicit_relation_is_zero(&input, rel, &[c1], &pool)
.expect_err("a constant-dependent slope field must be refused");
assert!(
format!("{err}").contains("integration constant"),
"refusal does not say why: {err}"
);
}
#[test]
fn riccati_without_a_particular_solution_refuses_by_name() {
let pool = ExprPool::new();
let input = super::corpus::build_ode(1, "yp - y^2 - x", &pool).expect("parses");
match solve_all(&input, &pool) {
Err(ref e) if e.is_missing_particular_solution() => {
assert_eq!(e.code(), "E-ODE-014");
let m = e.to_string();
assert!(
m.contains("riccati"),
"decline does not name the class: {m}"
);
assert!(
m.contains("particular solution"),
"decline does not name what is missing: {m}"
);
}
Err(e) => panic!("expected a named Riccati refusal, got {e}"),
Ok(res) => panic!("expected a refusal, got {}", res.solutions[0].render(&pool)),
}
}
#[test]
fn a_failed_inner_integral_is_reported_as_such() {
let pool = ExprPool::new();
let input = super::corpus::build_ode(1, "yp - y^2 + x^2 - 1", &pool).expect("parses");
match solve_all(&input, &pool) {
Err(ref e) if e.is_quadrature_failure() => {
assert_eq!(e.code(), "E-ODE-013");
let m = e.to_string();
assert!(m.contains('∫'), "decline does not quote an integral: {m}");
}
Err(e) => panic!("expected a quadrature decline, got {e}"),
Ok(_) => {}
}
}
#[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(explicit_of(&sol), *c, &pool),
"`{src}`: constant {} does not appear in the solution",
pool.display(*c)
);
}
}
}
fn report_src(order: usize, src: &str) -> (ExprPool, OdeInput, DsolveReport) {
let pool = ExprPool::new();
let input = super::corpus::build_ode(order, src, &pool).expect("source parses");
let rep = dsolve_with(&input, &AssumptionContext::new(), &pool)
.unwrap_or_else(|e| panic!("`{src}` should solve: {e}"));
let sol = &rep.branches[0];
residual_is_zero(
&input,
sol.y_of_x().expect("an explicit solution"),
&sol.constants,
&pool,
)
.unwrap_or_else(|e| panic!("`{src}` returned an unverified solution: {e}"));
(pool, input, rep)
}
fn nonzero_conditions(rep: &DsolveReport, pool: &ExprPool) -> Vec<String> {
rep.side_conditions
.iter()
.map(|c| match c {
SideCondition::NonZero(id) => pool.display(*id).to_string(),
other => format!("{other:?}"),
})
.collect()
}
#[test]
fn damped_oscillator_with_symbolic_zeta_and_omega() {
let (pool, _, rep) = report_src(2, "ypp + 2*z*w*yp + w^2*y");
let sol = &rep.branches[0];
assert_eq!(sol.constants.len(), 2);
assert_eq!(sol.method, "constant_coefficient_symbolic");
let y = pool
.display(sol.y_of_x().expect("an explicit solution"))
.to_string();
assert_eq!(
y.matches("exp(").count(),
2,
"expected two exponentials: {y}"
);
assert_eq!(
rep.side_conditions.len(),
1,
"expected exactly the discriminant condition, got {:?}",
nonzero_conditions(&rep, &pool)
);
assert!(matches!(rep.side_conditions[0], SideCondition::NonZero(_)));
assert!(
rep.notes.iter().any(|n| n.contains("discriminant")),
"the repeated-root case must be named in prose, got {:?}",
rep.notes
);
}
#[test]
fn a_provable_double_root_gets_the_secular_solution() {
let (pool, _, rep) = report_src(2, "ypp + 2*a*yp + a^2*y");
let sol = &rep.branches[0];
assert_eq!(sol.method, "constant_coefficient_symbolic_repeated_root");
assert!(
rep.side_conditions.is_empty(),
"nothing is undecided here: {:?}",
nonzero_conditions(&rep, &pool)
);
let y = pool
.display(sol.y_of_x().expect("an explicit solution"))
.to_string();
assert!(
y.contains("x * exp") || y.contains("exp(") && y.contains("x *"),
"expected an x·e^{{−ax}} term, got {y}"
);
}
#[test]
fn a_symbolic_leading_coefficient_is_a_stated_assumption() {
let (pool, _, rep) = report_src(2, "a*ypp + b*yp + c*y");
let conds = nonzero_conditions(&rep, &pool);
assert!(
conds.iter().any(|c| c == "a"),
"`a ≠ 0` must be stated — at a = 0 the equation is first order, not \
second — got {conds:?}"
);
assert_eq!(
conds.len(),
2,
"leading coefficient and discriminant: {conds:?}"
);
}
#[test]
fn an_asserted_nonzero_discriminant_removes_the_condition() {
let pool = ExprPool::new();
let input = super::corpus::build_ode(2, "ypp + 2*z*w*yp + w^2*y", &pool).expect("parses");
let z = pool.symbol("z", Domain::Real);
let w = pool.symbol("w", Domain::Real);
let w2 = pool.pow(w, pool.integer(2_i32));
let d = pool.add(vec![
pool.mul(vec![pool.pow(z, pool.integer(2_i32)), w2]),
pool.mul(vec![pool.integer(-1_i32), w2]),
]);
let mut assumptions = AssumptionContext::new();
assumptions
.refine(pool.pred_ne(d, pool.integer(0_i32)), &pool)
.expect("satisfiable");
let rep = dsolve_with(&input, &assumptions, &pool).expect("solves");
assert!(
rep.side_conditions.is_empty(),
"the caller settled the branch, got {:?}",
nonzero_conditions(&rep, &pool)
);
assert_eq!(
rep.branches[0].method,
"constant_coefficient_symbolic_distinct_roots"
);
}
#[test]
fn an_asserted_negative_discriminant_gives_the_real_oscillatory_form() {
let pool = ExprPool::new();
let input = super::corpus::build_ode(2, "ypp + 2*z*w*yp + w^2*y", &pool).expect("parses");
let z = pool.symbol("z", Domain::Real);
let w = pool.symbol("w", Domain::Real);
let w2 = pool.pow(w, pool.integer(2_i32));
let neg_d = pool.add(vec![
w2,
pool.mul(vec![
pool.integer(-1_i32),
pool.pow(z, pool.integer(2_i32)),
w2,
]),
]);
let mut assumptions = AssumptionContext::new();
assumptions
.refine(pool.pred_gt(neg_d, pool.integer(0_i32)), &pool)
.expect("satisfiable");
let rep = dsolve_with(&input, &assumptions, &pool).expect("solves");
let sol = &rep.branches[0];
assert_eq!(sol.method, "constant_coefficient_symbolic_oscillatory");
let y = pool
.display(sol.y_of_x().expect("an explicit solution"))
.to_string();
assert!(y.contains("cos(") && y.contains("sin("), "got {y}");
residual_is_zero(
&input,
sol.y_of_x().expect("an explicit solution"),
&sol.constants,
&pool,
)
.expect("the oscillatory form must verify too");
assert!(rep.side_conditions.is_empty());
}
#[test]
fn a_lambda_factor_reduces_the_symbolic_degree() {
let (pool, _, rep) = report_src(3, "yppp + 2*z*w*ypp + w^2*yp");
let sol = &rep.branches[0];
assert_eq!(sol.constants.len(), 3);
let y = pool
.display(sol.y_of_x().expect("an explicit solution"))
.to_string();
assert_eq!(
y.matches("exp(").count(),
2,
"one constant mode + two exponentials: {y}"
);
}
#[test]
fn a_symbolic_cubic_characteristic_polynomial_is_refused() {
let pool = ExprPool::new();
let input = super::corpus::build_ode(3, "yppp + a*ypp + b*yp + c*y", &pool).expect("parses");
match solve_all(&input, &pool) {
Err(DsolveError::Unsupported(m)) => {
assert!(
m.contains("degree"),
"the message must name the degree: {m}"
);
}
Err(e) => panic!("expected an Unsupported decline, got {e}"),
Ok(r) => panic!(
"expected a decline, got {}",
pool.display(r.solutions[0].y_of_x().expect("an explicit solution"))
),
}
}
#[test]
fn numeric_coefficients_still_take_the_rational_route() {
let (pool, _, rep) = report_src(2, "ypp + 2*yp + 5*y");
let sol = &rep.branches[0];
assert_eq!(sol.method, "constant_coefficient");
assert!(rep.side_conditions.is_empty());
let y = pool
.display(sol.y_of_x().expect("an explicit solution"))
.to_string();
assert!(y.contains("cos(") && y.contains("sin("), "got {y}");
}
#[test]
fn symbolic_corpus_entries_all_verify() {
for (class, name, order, src) in super::corpus::CORPUS {
if *class != "cc-sym" {
continue;
}
let pool = ExprPool::new();
let input = super::corpus::build_ode(*order, src, &pool).expect("parses");
let res = solve_all(&input, &pool).unwrap_or_else(|e| panic!("`{name}` should solve: {e}"));
let sol = &res.solutions[0];
assert_eq!(sol.constants.len(), *order, "`{name}` constant count");
residual_is_zero(
&input,
sol.y_of_x().expect("an explicit solution"),
&sol.constants,
&pool,
)
.unwrap_or_else(|e| panic!("`{name}` returned an unverified solution: {e}"));
}
}
#[test]
fn a_candidate_right_at_one_parameter_value_only_is_refused() {
let pool = ExprPool::new();
let input = super::corpus::build_ode(2, "ypp + 2*z*w*yp + w^2*y", &pool).expect("parses");
let w = pool.symbol("w", Domain::Real);
let c1 = pool.symbol("C1", Domain::Real);
let arg = pool.mul(vec![pool.integer(-1_i32), w, input.x]);
let bogus = pool.mul(vec![c1, pool.func("exp", vec![arg])]);
assert!(
residual_is_zero(&input, bogus, &[c1], &pool).is_err(),
"a candidate that only works at ζ = 1 must not be certified"
);
}