pub mod classify;
pub mod first_order;
mod laplace;
pub mod second_order;
pub mod series;
mod systems;
mod util;
use ocas_atom::normalize::normalize;
use ocas_atom::{Atom, AtomArena, Symbol};
use ocas_rewrite::rules::default_rules;
use ocas_rewrite::simplify::simplify;
use crate::rules::calculus_rules;
pub use classify::{ODEType, classify_ode};
#[derive(Debug, Clone, Copy)]
pub struct ODE<'a> {
pub equation: Atom<'a>,
pub func: Atom<'a>,
pub var: Symbol,
}
#[derive(Debug, Clone, Copy)]
pub enum ODESolution<'a> {
Explicit(Atom<'a>),
Implicit(Atom<'a>),
Parametric(Atom<'a>, Atom<'a>),
Series(Atom<'a>, usize),
System(&'a [Atom<'a>]),
Unsolved(ODE<'a>),
}
pub fn dsolve<'a>(ctx: &'a AtomArena<'a>, ode: ODE<'a>, hint: Option<ODEType>) -> ODESolution<'a> {
let ode = normalize_ode(ctx, ode);
let types = match hint {
Some(t) => vec![t],
None => classify_ode(ctx, ode),
};
for ode_type in &types {
let result = match ode_type {
ODEType::Separable => first_order::solve_separable(ctx, ode),
ODEType::LinearFirst => first_order::solve_linear_first(ctx, ode),
ODEType::Bernoulli => first_order::solve_bernoulli(ctx, ode),
ODEType::Exact => first_order::solve_exact(ctx, ode),
ODEType::Homogeneous => first_order::solve_homogeneous(ctx, ode),
ODEType::LinearConstantCoeff => second_order::solve_constant_coeff(ctx, ode),
ODEType::CauchyEuler => second_order::solve_cauchy_euler(ctx, ode),
ODEType::ReductionOfOrder => second_order::solve_reduction_of_order(ctx, ode),
ODEType::PowerSeries => series::solve_power_series(ctx, ode, ctx.num(0), 8)
.or_else(|| series::solve_frobenius(ctx, ode, ctx.num(0), 8)),
};
if let Some(sol) = result {
if !matches!(sol, ODESolution::Unsolved(_)) {
return sol;
}
}
}
ODESolution::Unsolved(ode)
}
pub fn dsolve_ivp<'a>(
ctx: &'a AtomArena<'a>,
ode: ODE<'a>,
y0: Atom<'a>,
y1: Option<Atom<'a>>,
) -> ODESolution<'a> {
let ode = normalize_ode(ctx, ode);
laplace::solve_laplace(ctx, ode, y0, y1).unwrap_or(ODESolution::Unsolved(ode))
}
pub fn dsolve_system<'a>(
ctx: &'a AtomArena<'a>,
equations: &[Atom<'a>],
funcs: &[Atom<'a>],
var: Symbol,
) -> ODESolution<'a> {
match systems::solve_linear_system(ctx, equations, funcs, var) {
Some(sol) => sol,
None => ODESolution::Unsolved(ODE {
equation: equations.first().copied().unwrap_or_else(|| ctx.num(0)),
func: funcs.first().copied().unwrap_or_else(|| ctx.num(0)),
var,
}),
}
}
fn normalize_ode<'a>(ctx: &'a AtomArena<'a>, ode: ODE<'a>) -> ODE<'a> {
let rules = default_rules(ctx, &crate::pattern_alloc::VecAlloc);
let calc_rules = calculus_rules(ctx, &crate::pattern_alloc::VecAlloc);
let simplified = simplify(ctx, ode.equation, &rules, 20);
let simplified = simplify(ctx, simplified, &calc_rules, 10);
let equation = normalize(ctx, simplified);
ODE {
equation,
func: ode.func,
var: ode.var,
}
}
pub fn substitute_solution_collected<'a>(
ctx: &'a AtomArena<'a>,
equation: Atom<'a>,
func: Atom<'a>,
sol: Atom<'a>,
var: Symbol,
) -> Atom<'a> {
let substituted = util::substitute_solution(ctx, equation, func, sol, var);
util::collect_terms(ctx, substituted)
}
#[cfg(test)]
#[allow(dead_code)]
pub(crate) fn verify_solution<'a>(ctx: &'a AtomArena<'a>, ode: ODE<'a>, sol: Atom<'a>) -> bool {
use crate::ode::util::substitute_solution;
use ocas_atom::AtomNode;
let substituted = substitute_solution(ctx, ode.equation, ode.func, sol, ode.var);
let result = util::collect_terms(ctx, substituted);
matches!(result.node(), AtomNode::Num(0))
}
#[cfg(test)]
#[allow(dead_code)]
pub(crate) fn verify_system<'a>(
ctx: &'a AtomArena<'a>,
equations: &[Atom<'a>],
funcs: &[Atom<'a>],
sols: &[Atom<'a>],
var: Symbol,
) -> bool {
use ocas_atom::AtomNode;
equations.iter().all(|eq| {
let mut substituted = *eq;
for (func, sol) in funcs.iter().zip(sols.iter()) {
substituted = util::substitute_solution(ctx, substituted, *func, *sol, var);
}
let residual = util::collect_terms(ctx, substituted);
matches!(residual.node(), AtomNode::Num(0))
})
}
#[cfg(test)]
mod tests {
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;
use super::*;
#[test]
fn classify_first_order_linear() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);
let ode = ODE {
equation: ctx.add(&[dy, y]),
func: y,
var: Symbol::new("x"),
};
let types = classify_ode(&ctx, ode);
assert!(types.contains(&ODEType::LinearFirst));
assert!(types.contains(&ODEType::Separable));
}
#[test]
fn classify_second_order_constant_coeff() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let _dy = ctx.fun("Derivative", &[y, x]);
let d2y = ctx.fun("Derivative", &[y, x, x]);
let ode = ODE {
equation: ctx.add(&[d2y, ctx.mul(&[ctx.num(-1), y])]),
func: y,
var: Symbol::new("x"),
};
let types = classify_ode(&ctx, ode);
assert!(types.contains(&ODEType::LinearConstantCoeff));
}
#[test]
fn dsolve_first_order_linear() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);
let ode = ODE {
equation: ctx.add(&[dy, ctx.mul(&[ctx.num(-1), y])]),
func: y,
var: Symbol::new("x"),
};
let sol = dsolve(&ctx, ode, None);
match sol {
ODESolution::Explicit(expr) => {
let s = expr.to_string();
assert!(s.contains("C1"), "Solution should contain C1: {s}");
assert!(s.contains("exp"), "Solution should contain exp: {s}");
}
_ => panic!("Expected explicit solution, got {:?}", sol),
}
}
#[test]
fn dsolve_first_order_linear_forcing() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);
let ode = ODE {
equation: ctx.add(&[dy, y, ctx.mul(&[ctx.num(-1), x])]),
func: y,
var: Symbol::new("x"),
};
let sol = dsolve(&ctx, ode, None);
match sol {
ODESolution::Explicit(expr) => {
let s = expr.to_string();
assert!(s.contains("C1"), "Solution should contain C1: {s}");
assert!(s.contains("exp"), "Solution should contain exp: {s}");
}
_ => panic!("Expected explicit solution, got {:?}", sol),
}
}
#[test]
fn dsolve_second_order_constant_coeff_real_roots() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);
let d2y = ctx.fun("Derivative", &[y, x, x]);
let ode = ODE {
equation: ctx.add(&[d2y, ctx.mul(&[ctx.num(-3), dy]), ctx.mul(&[ctx.num(2), y])]),
func: y,
var: Symbol::new("x"),
};
let sol = dsolve(&ctx, ode, None);
match sol {
ODESolution::Explicit(expr) => {
let s = expr.to_string();
assert!(s.contains("C1"), "Solution should contain C1: {s}");
assert!(s.contains("C2"), "Solution should contain C2: {s}");
}
_ => panic!("Expected explicit solution, got {:?}", sol),
}
}
#[test]
fn dsolve_second_order_repeated_root() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);
let d2y = ctx.fun("Derivative", &[y, x, x]);
let ode = ODE {
equation: ctx.add(&[d2y, ctx.mul(&[ctx.num(-2), dy]), y]),
func: y,
var: Symbol::new("x"),
};
let sol = dsolve(&ctx, ode, None);
match sol {
ODESolution::Explicit(expr) => {
let s = expr.to_string();
assert!(s.contains("C1"), "Solution should contain C1: {s}");
assert!(s.contains("C2"), "Solution should contain C2: {s}");
assert!(s.contains("exp"), "Solution should contain exp: {s}");
}
_ => panic!("Expected explicit solution, got {:?}", sol),
}
}
#[test]
fn dsolve_unsolvable_returns_unsolved() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let ode = ODE {
equation: ctx.add(&[y, ctx.mul(&[ctx.num(-1), x])]),
func: y,
var: Symbol::new("x"),
};
let sol = dsolve(&ctx, ode, None);
assert!(matches!(sol, ODESolution::Unsolved(_)));
}
#[test]
fn ode_order_detection() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let expr = ctx.add(&[y, x]);
assert_eq!(super::util::ode_order(expr, y, Symbol::new("x")), 0);
let dy = ctx.fun("Derivative", &[y, x]);
assert_eq!(super::util::ode_order(dy, y, Symbol::new("x")), 1);
let d2y = ctx.fun("Derivative", &[y, x, x]);
assert_eq!(super::util::ode_order(d2y, y, Symbol::new("x")), 2);
}
#[test]
fn classify_exact_ode() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);
let m = ctx.mul(&[ctx.num(2), x, y]);
let n_term = ctx.mul(&[ctx.pow(x, ctx.num(2)), dy]);
let ode = ODE {
equation: ctx.add(&[m, n_term]),
func: y,
var: Symbol::new("x"),
};
let types = classify_ode(&ctx, ode);
assert!(
types.contains(&ODEType::Exact),
"Expected Exact classification, got {types:?}"
);
}
#[test]
fn classify_non_exact_ode() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);
let m = ctx.mul(&[ctx.num(2), y]);
let n_term = ctx.mul(&[x, dy]);
let ode = ODE {
equation: ctx.add(&[m, n_term]),
func: y,
var: Symbol::new("x"),
};
let types = classify_ode(&ctx, ode);
assert!(
!types.contains(&ODEType::Exact),
"Did not expect Exact classification, got {types:?}"
);
}
#[test]
fn dsolve_second_order_irrational_roots() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let d2y = ctx.fun("Derivative", &[y, x, x]);
let ode = ODE {
equation: ctx.add(&[d2y, ctx.mul(&[ctx.num(-2), y])]),
func: y,
var: Symbol::new("x"),
};
let sol = dsolve(&ctx, ode, None);
match sol {
ODESolution::Explicit(expr) => {
let s = expr.to_string();
assert!(
s.contains("8^(1/2)") || s.contains("sqrt") || !s.contains("8^-1"),
"Solution should keep sqrt symbolically: {s}"
);
assert!(
!s.contains("exp(x)") && !s.contains("exp(-1*x)"),
"Roots were truncated to integers: {s}"
);
}
_ => panic!("Expected explicit solution, got {:?}", sol),
}
}
#[test]
fn dsolve_exact_ode() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);
let m = ctx.mul(&[ctx.num(2), x, y]);
let n_term = ctx.mul(&[ctx.pow(x, ctx.num(2)), dy]);
let ode = ODE {
equation: ctx.add(&[m, n_term]),
func: y,
var: Symbol::new("x"),
};
let sol = dsolve(&ctx, ode, Some(ODEType::Exact));
match sol {
ODESolution::Implicit(expr) => {
let s = expr.to_string();
assert!(s.contains('y'), "Implicit solution should involve y: {s}");
}
other => panic!("Expected implicit solution, got {other:?}"),
}
}
#[test]
fn dsolve_integrating_factor() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);
let m = y;
let n_term = ctx.mul(&[ctx.num(2), x, dy]);
let ode = ODE {
equation: ctx.add(&[m, n_term]),
func: y,
var: Symbol::new("x"),
};
let sol = dsolve(&ctx, ode, Some(ODEType::Exact));
assert!(
!matches!(sol, ODESolution::Unsolved(_)),
"Integrating-factor ODE should be solvable, got {sol:?}"
);
}
#[test]
fn dsolve_second_order_vop() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let d2y = ctx.fun("Derivative", &[y, x, x]);
let sec = ctx.pow(ctx.fun("cos", &[x]), ctx.num(-1));
let ode = ODE {
equation: ctx.add(&[d2y, y, ctx.mul(&[ctx.num(-1), sec])]),
func: y,
var: Symbol::new("x"),
};
let sol = dsolve(&ctx, ode, Some(ODEType::LinearConstantCoeff));
match sol {
ODESolution::Explicit(expr) => {
let s = expr.to_string();
assert!(
s.contains("sin") || s.contains("log"),
"VOP particular solution missing: {s}"
);
}
other => panic!("Expected explicit solution, got {other:?}"),
}
}
#[test]
fn dsolve_cauchy_euler_forcing() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);
let d2y = ctx.fun("Derivative", &[y, x, x]);
let x2_d2y = ctx.mul(&[ctx.pow(x, ctx.num(2)), d2y]);
let neg_2x_dy = ctx.mul(&[ctx.num(-2), x, dy]);
let two_y = ctx.mul(&[ctx.num(2), y]);
let neg_x3 = ctx.mul(&[ctx.num(-1), ctx.pow(x, ctx.num(3))]);
let ode = ODE {
equation: ctx.add(&[x2_d2y, neg_2x_dy, two_y, neg_x3]),
func: y,
var: Symbol::new("x"),
};
let sol = dsolve(&ctx, ode, Some(ODEType::CauchyEuler));
match sol {
ODESolution::Explicit(expr) => {
let s = expr.to_string();
assert!(
s.contains("x^3") || s.contains("x^(3"),
"Particular solution x^3/2 missing: {s}"
);
}
other => panic!("Expected explicit solution, got {other:?}"),
}
}
#[test]
fn dsolve_undetermined_quadratic_forcing() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let d2y = ctx.fun("Derivative", &[y, x, x]);
let x2 = ctx.pow(x, ctx.num(2));
let ode = ODE {
equation: ctx.add(&[d2y, y, ctx.mul(&[ctx.num(-1), x2])]),
func: y,
var: Symbol::new("x"),
};
let sol = dsolve(&ctx, ode, Some(ODEType::LinearConstantCoeff));
match sol {
ODESolution::Explicit(expr) => {
assert!(
verify_solution(&ctx, ode, expr),
"Solution does not satisfy ODE: {expr}"
);
}
other => panic!("Expected explicit solution, got {other:?}"),
}
}
#[test]
fn dsolve_undetermined_exp_resonance() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);
let d2y = ctx.fun("Derivative", &[y, x, x]);
let exp_x = ctx.fun("exp", &[x]);
let ode = ODE {
equation: ctx.add(&[
d2y,
ctx.mul(&[ctx.num(-3), dy]),
ctx.mul(&[ctx.num(2), y]),
ctx.mul(&[ctx.num(-1), exp_x]),
]),
func: y,
var: Symbol::new("x"),
};
let sol = dsolve(&ctx, ode, Some(ODEType::LinearConstantCoeff));
match sol {
ODESolution::Explicit(expr) => {
let s = expr.to_string();
assert!(
s.contains("x") && s.contains("exp"),
"Resonance particular solution missing x*exp(x): {s}"
);
assert!(
verify_solution(&ctx, ode, expr),
"Solution does not satisfy ODE: {expr}"
);
}
other => panic!("Expected explicit solution, got {other:?}"),
}
}
#[test]
fn dsolve_undetermined_trig_forcing() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let d2y = ctx.fun("Derivative", &[y, x, x]);
let cos_x = ctx.fun("cos", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);
let ode = ODE {
equation: ctx.add(&[d2y, dy, y, ctx.mul(&[ctx.num(-1), cos_x])]),
func: y,
var: Symbol::new("x"),
};
let sol = dsolve(&ctx, ode, Some(ODEType::LinearConstantCoeff));
match sol {
ODESolution::Explicit(expr) => {
let s = expr.to_string();
assert!(
s.contains("sin"),
"Trig particular solution missing sin: {s}"
);
assert!(
verify_solution(&ctx, ode, expr),
"Solution does not satisfy ODE: {expr}"
);
}
other => panic!("Expected explicit solution, got {other:?}"),
}
}
#[test]
fn dsolve_reduction_of_order() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);
let d2y = ctx.fun("Derivative", &[y, x, x]);
let x_d2y = ctx.mul(&[x, d2y]);
let neg_dy = ctx.mul(&[ctx.num(-1), dy]);
let ode = ODE {
equation: ctx.add(&[x_d2y, neg_dy]),
func: y,
var: Symbol::new("x"),
};
let sol = dsolve(&ctx, ode, Some(ODEType::ReductionOfOrder));
match sol {
ODESolution::Explicit(expr) => {
let s = expr.to_string();
assert!(s.contains("C1"), "Solution should contain C1: {s}");
assert!(
s.contains("x^2") || s.contains("x^(2"),
"Second solution x^2 missing: {s}"
);
assert!(
verify_solution(&ctx, ode, expr),
"Solution does not satisfy ODE: {expr}"
);
}
other => panic!("Expected explicit solution, got {other:?}"),
}
}
#[test]
fn dsolve_power_series_exp() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);
let ode = ODE {
equation: ctx.add(&[dy, ctx.mul(&[ctx.num(-1), y])]),
func: y,
var: Symbol::new("x"),
};
let sol = dsolve(&ctx, ode, Some(ODEType::PowerSeries));
match sol {
ODESolution::Series(expr, n) => {
let s = expr.to_string();
assert!(n >= 6, "Expected at least 6 terms, got {n}");
assert!(s.contains("x^2"), "Series should contain x^2 term: {s}");
}
other => panic!("Expected series solution, got {other:?}"),
}
}
#[test]
fn dsolve_power_series_second_order() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let d2y = ctx.fun("Derivative", &[y, x, x]);
let ode = ODE {
equation: ctx.add(&[d2y, y]),
func: y,
var: Symbol::new("x"),
};
let sol = dsolve(&ctx, ode, Some(ODEType::PowerSeries));
match sol {
ODESolution::Series(expr, n) => {
let s = expr.to_string();
assert!(n >= 6, "Expected at least 6 terms, got {n}");
assert!(s.contains("x^2"), "Series should contain x^2 term: {s}");
assert!(
s.contains("-1"),
"Series should contain the negative x^2 coefficient: {s}"
);
}
other => panic!("Expected series solution, got {other:?}"),
}
}
#[test]
fn dsolve_frobenius_euler_point() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);
let d2y = ctx.fun("Derivative", &[y, x, x]);
let two_x_d2y = ctx.mul(&[ctx.num(2), x, d2y]);
let ode = ODE {
equation: ctx.add(&[two_x_d2y, dy]),
func: y,
var: Symbol::new("x"),
};
let sol = dsolve(&ctx, ode, None);
assert!(
!matches!(sol, ODESolution::Unsolved(_)),
"2x*y'' + y' = 0 should be solvable, got {sol:?}"
);
}
#[test]
fn dsolve_frobenius_half_integer_root() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);
let d2y = ctx.fun("Derivative", &[y, x, x]);
let ode = ODE {
equation: ctx.add(&[
ctx.mul(&[ctx.num(2), x, d2y]),
dy,
ctx.mul(&[ctx.num(2), y]),
]),
func: y,
var: Symbol::new("x"),
};
let sol = dsolve(&ctx, ode, Some(ODEType::PowerSeries));
match sol {
ODESolution::Series(expr, _n) => {
let s = expr.to_string();
assert!(
s.contains("x^(2^-1)") || s.contains("x^(1*(2^-1))"),
"Frobenius series missing x^(1/2) factor: {s}"
);
assert!(
s.contains("3^-1"),
"Frobenius series missing 1/3 coefficient: {s}"
);
}
other => panic!("Expected Frobenius series, got {other:?}"),
}
}
#[test]
fn dsolve_ivp_first_order() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);
let ode = ODE {
equation: ctx.add(&[dy, ctx.mul(&[ctx.num(-1), y])]),
func: y,
var: Symbol::new("x"),
};
let sol = dsolve_ivp(&ctx, ode, ctx.num(2), None);
match sol {
ODESolution::Explicit(expr) => {
let s = expr.to_string();
assert!(s.contains("exp"), "Expected exp in IVP solution: {s}");
assert!(!s.contains("C1"), "IVP solution must not contain C1: {s}");
assert!(
verify_solution(&ctx, ode, expr),
"IVP solution does not satisfy ODE: {expr}"
);
}
other => panic!("Expected explicit IVP solution, got {other:?}"),
}
}
#[test]
fn dsolve_ivp_second_order_distinct_roots() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let dy = ctx.fun("Derivative", &[y, x]);
let d2y = ctx.fun("Derivative", &[y, x, x]);
let ode = ODE {
equation: ctx.add(&[d2y, ctx.mul(&[ctx.num(-3), dy]), ctx.mul(&[ctx.num(2), y])]),
func: y,
var: Symbol::new("x"),
};
let sol = dsolve_ivp(&ctx, ode, ctx.num(1), Some(ctx.num(0)));
match sol {
ODESolution::Explicit(expr) => {
let s = expr.to_string();
assert!(s.contains("exp"), "Expected exp in IVP solution: {s}");
assert!(!s.contains("C1"), "IVP solution must not contain C1: {s}");
assert!(
verify_solution(&ctx, ode, expr),
"IVP solution does not satisfy ODE: {expr}"
);
}
other => panic!("Expected explicit IVP solution, got {other:?}"),
}
}
#[test]
fn dsolve_ivp_second_order_trig() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.fun("y", &[x]);
let d2y = ctx.fun("Derivative", &[y, x, x]);
let ode = ODE {
equation: ctx.add(&[d2y, y]),
func: y,
var: Symbol::new("x"),
};
let sol = dsolve_ivp(&ctx, ode, ctx.num(0), Some(ctx.num(1)));
match sol {
ODESolution::Explicit(expr) => {
let s = expr.to_string();
assert!(s.contains("sin"), "Expected sin in IVP solution: {s}");
assert!(
verify_solution(&ctx, ode, expr),
"IVP solution does not satisfy ODE: {expr}"
);
}
other => panic!("Expected explicit IVP solution, got {other:?}"),
}
}
#[test]
fn dsolve_system_distinct_eigenvalues() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y1 = ctx.fun("y1", &[x]);
let y2 = ctx.fun("y2", &[x]);
let dy1 = ctx.fun("Derivative", &[y1, x]);
let dy2 = ctx.fun("Derivative", &[y2, x]);
let eq1 = ctx.add(&[dy1, ctx.mul(&[ctx.num(-1), y2])]);
let eq2 = ctx.add(&[dy2, ctx.mul(&[ctx.num(-1), y1])]);
let sol = dsolve_system(&ctx, &[eq1, eq2], &[y1, y2], Symbol::new("x"));
match sol {
ODESolution::System(comps) => {
assert_eq!(comps.len(), 2);
let s1 = comps[0].to_string();
let s2 = comps[1].to_string();
assert!(s1.contains("exp"), "y1 should contain exp: {s1}");
assert!(s2.contains("exp"), "y2 should contain exp: {s2}");
}
other => panic!("Expected system solution, got {other:?}"),
}
}
#[test]
fn dsolve_system_complex_eigenvalues() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y1 = ctx.fun("y1", &[x]);
let y2 = ctx.fun("y2", &[x]);
let dy1 = ctx.fun("Derivative", &[y1, x]);
let dy2 = ctx.fun("Derivative", &[y2, x]);
let eq1 = ctx.add(&[dy1, ctx.mul(&[ctx.num(-1), y2])]);
let eq2 = ctx.add(&[dy2, y1]);
let sol = dsolve_system(&ctx, &[eq1, eq2], &[y1, y2], Symbol::new("x"));
match sol {
ODESolution::System(comps) => {
assert_eq!(comps.len(), 2);
let s1 = comps[0].to_string();
let s2 = comps[1].to_string();
assert!(
s1.contains("sin") || s1.contains("cos"),
"y1 should contain trig: {s1}"
);
assert!(
s2.contains("sin") || s2.contains("cos"),
"y2 should contain trig: {s2}"
);
assert!(
verify_system(&ctx, &[eq1, eq2], &[y1, y2], comps, Symbol::new("x")),
"System solution does not satisfy equations"
);
}
other => panic!("Expected system solution, got {other:?}"),
}
}
#[test]
fn dsolve_system_repeated_eigenvalue() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y1 = ctx.fun("y1", &[x]);
let y2 = ctx.fun("y2", &[x]);
let dy1 = ctx.fun("Derivative", &[y1, x]);
let dy2 = ctx.fun("Derivative", &[y2, x]);
let eq1 = ctx.add(&[
dy1,
ctx.mul(&[ctx.num(-1), y1]),
ctx.mul(&[ctx.num(-1), y2]),
]);
let eq2 = ctx.add(&[dy2, ctx.mul(&[ctx.num(-1), y2])]);
let sol = dsolve_system(&ctx, &[eq1, eq2], &[y1, y2], Symbol::new("x"));
match sol {
ODESolution::System(comps) => {
assert_eq!(comps.len(), 2);
assert!(
verify_system(&ctx, &[eq1, eq2], &[y1, y2], comps, Symbol::new("x")),
"System solution does not satisfy equations: {comps:?}"
);
}
other => panic!("Expected system solution, got {other:?}"),
}
}
}