use symplex::matrix::Matrix;
use symplex::prelude::*;
fn verify_system_numerically(a_matrix: &Matrix, solution: &[Ex], t_var: &Ex, t0: f64) -> bool {
let ctx = t_var.context();
let n = solution.len();
let t_val = ctx.rational((t0 * 1000.0) as i64, 1000);
let mut x_vals = Vec::with_capacity(n);
let mut xp_vals = Vec::with_capacity(n);
for xi in solution {
let xi_at_t = xi.subs(t_var, &t_val).eval();
let xi_prime = xi.diff(t_var).eval();
let xip_at_t = xi_prime.subs(t_var, &t_val).eval();
if let (Ok(xv), Ok(xpv)) = (xi_at_t.eval_f64(), xip_at_t.eval_f64()) {
x_vals.push(xv);
xp_vals.push(xpv);
} else {
return true; }
}
for (i, &xp_i) in xp_vals.iter().enumerate().take(n) {
let mut ax_i = 0.0;
for (j, &x_j) in x_vals.iter().enumerate().take(n) {
let a_ij = a_matrix.get(i, j).eval_f64().unwrap_or(0.0);
ax_i += a_ij * x_j;
}
let residual = (xp_i - ax_i).abs();
let scale = xp_i.abs().max(ax_i.abs()).max(1.0);
if residual / scale > 1e-4 {
return false;
}
}
true
}
#[test]
fn ode_system_2x2_real_eigenvalues() {
let ctx = Context::new();
let t = ctx.symbol("t");
let a = Matrix::new(vec![
vec![ctx.int(0), ctx.int(1)],
vec![ctx.int(-2), ctx.int(-3)],
])
.unwrap();
let sol =
symplex::ode::solve_ode_system(&a, &t).expect("should solve 2x2 real-eigenvalue system");
assert_eq!(sol.len(), 2, "should return 2 solution components");
let s0 = format!("{}", sol[0]);
let s1 = format!("{}", sol[1]);
assert!(
s0.contains("C1") || s0.contains("C2"),
"x1(t) should have arbitrary constants: {s0}"
);
assert!(
s1.contains("C1") || s1.contains("C2"),
"x2(t) should have arbitrary constants: {s1}"
);
assert!(
s0.contains("exp") || s0.contains("e^"),
"x1(t) should contain exponential: {s0}"
);
assert!(
!s0.contains("cos") && !s0.contains("sin"),
"real eigenvalues should not produce trig: {s0}"
);
}
#[test]
fn ode_system_2x2_complex_eigenvalues() {
let ctx = Context::new();
let t = ctx.symbol("t");
let a = Matrix::new(vec![
vec![ctx.int(0), ctx.int(1)],
vec![ctx.int(-1), ctx.int(0)],
])
.unwrap();
let sol = symplex::ode::solve_ode_system(&a, &t).expect("should solve oscillator system");
assert_eq!(sol.len(), 2, "should return 2 solution components");
let s0 = format!("{}", sol[0]);
let s1 = format!("{}", sol[1]);
let has_trig_0 = s0.contains("cos") || s0.contains("sin");
let has_trig_1 = s1.contains("cos") || s1.contains("sin");
assert!(
has_trig_0 || s0.contains("exp") || s0.contains("C"),
"oscillator x1(t) should have trig or exp form: {s0}"
);
assert!(
has_trig_1 || s1.contains("exp") || s1.contains("C"),
"oscillator x2(t) should have trig or exp form: {s1}"
);
assert!(
s0.contains("C1") || s0.contains("C2"),
"x1(t) should have constants: {s0}"
);
}
#[test]
fn ode_system_diagonal() {
let ctx = Context::new();
let t = ctx.symbol("t");
let a_sym = ctx.symbol("a");
let b_sym = ctx.symbol("b");
let a = Matrix::new(vec![
vec![a_sym.clone(), ctx.int(0)],
vec![ctx.int(0), b_sym.clone()],
])
.unwrap();
let sol = symplex::ode::solve_ode_system(&a, &t).expect("should solve diagonal system");
assert_eq!(sol.len(), 2, "should return 2 solution components");
let s0 = format!("{}", sol[0]);
let s1 = format!("{}", sol[1]);
assert!(s0.contains("C1"), "x1(t) should contain C1: {s0}");
assert!(s0.contains("exp"), "x1(t) should contain exp: {s0}");
assert!(s0.contains("a"), "x1(t) should involve symbol a: {s0}");
assert!(s1.contains("C2"), "x2(t) should contain C2: {s1}");
assert!(s1.contains("exp"), "x2(t) should contain exp: {s1}");
assert!(s1.contains("b"), "x2(t) should involve symbol b: {s1}");
assert!(
!s0.contains("C2") && !s0.contains("b"),
"x1(t) should be decoupled: {s0}"
);
assert!(
!s1.contains("C1") && !s1.contains("a"),
"x2(t) should be decoupled: {s1}"
);
}
#[test]
fn ode_system_3x3() {
let ctx = Context::new();
let t = ctx.symbol("t");
let a = Matrix::new(vec![
vec![ctx.int(1), ctx.int(0), ctx.int(0)],
vec![ctx.int(0), ctx.int(2), ctx.int(0)],
vec![ctx.int(0), ctx.int(0), ctx.int(3)],
])
.unwrap();
let sol = symplex::ode::solve_ode_system(&a, &t).expect("should solve 3x3 diagonal system");
assert_eq!(sol.len(), 3, "should return 3 solution components");
for (i, xi) in sol.iter().enumerate() {
let s = format!("{xi}");
let ci = format!("C{}", i + 1);
assert!(
s.contains(&ci),
"x{}(t) should contain {}: {}",
i + 1,
ci,
s,
);
assert!(s.contains("exp"), "x{}(t) should contain exp: {}", i + 1, s,);
}
}
#[test]
fn ode_system_3x3_coupled() {
let ctx = Context::new();
let t = ctx.symbol("t");
let a = Matrix::new(vec![
vec![ctx.int(-1), ctx.int(1), ctx.int(0)],
vec![ctx.int(0), ctx.int(-2), ctx.int(1)],
vec![ctx.int(0), ctx.int(0), ctx.int(-3)],
])
.unwrap();
let sol = symplex::ode::solve_ode_system(&a, &t).expect("should solve 3x3 coupled system");
assert_eq!(sol.len(), 3, "should return 3 solution components");
for (i, xi) in sol.iter().enumerate() {
let s = format!("{xi}");
assert!(
s.contains("exp") || s.contains("C"),
"x{}(t) should contain exp or constants: {}",
i + 1,
s,
);
}
}
#[test]
fn ode_system_identity_matrix() {
let ctx = Context::new();
let t = ctx.symbol("t");
let a = Matrix::identity(&ctx, 2);
let sol = symplex::ode::solve_ode_system(&a, &t).expect("should solve identity system");
assert_eq!(sol.len(), 2, "should return 2 components");
for (i, xi) in sol.iter().enumerate() {
let s = format!("{xi}");
let ci = format!("C{}", i + 1);
assert!(
s.contains(&ci),
"x{}(t) should contain {}: {}",
i + 1,
ci,
s,
);
assert!(
s.contains("exp"),
"x{}(t) should contain exp(t): {}",
i + 1,
s,
);
}
}
#[test]
fn ode_system_numerical_verification_diagonal() {
let ctx = Context::new();
let t = ctx.symbol("t");
let a = Matrix::new(vec![
vec![ctx.int(-1), ctx.int(0)],
vec![ctx.int(0), ctx.int(-2)],
])
.unwrap();
let sol = symplex::ode::solve_ode_system(&a, &t).expect("should solve");
let c1 = ctx.symbol("C1");
let c2 = ctx.symbol("C2");
let sol_concrete: Vec<Ex> = sol
.iter()
.map(|xi| xi.subs(&c1, &ctx.int(1)).subs(&c2, &ctx.int(1)).eval())
.collect();
for &t0 in &[0.0, 0.5, 1.0] {
let ok = verify_system_numerically(&a, &sol_concrete, &t, t0);
assert!(ok, "numerical verification failed at t = {t0}");
}
}
#[test]
fn ode_system_numerical_verification_coupled() {
let ctx = Context::new();
let t = ctx.symbol("t");
let a = Matrix::new(vec![
vec![ctx.int(0), ctx.int(1)],
vec![ctx.int(-2), ctx.int(-3)],
])
.unwrap();
let sol = symplex::ode::solve_ode_system(&a, &t).expect("should solve");
let c1 = ctx.symbol("C1");
let c2 = ctx.symbol("C2");
let sol_concrete: Vec<Ex> = sol
.iter()
.map(|xi| xi.subs(&c1, &ctx.int(1)).subs(&c2, &ctx.int(1)).eval())
.collect();
for &t0 in &[0.0, 0.5, 1.0] {
let ok = verify_system_numerically(&a, &sol_concrete, &t, t0);
assert!(ok, "numerical verification failed at t = {t0}");
}
}
#[test]
fn ode_system_rejects_non_square() {
let ctx = Context::new();
let t = ctx.symbol("t");
let a = Matrix::new(vec![
vec![ctx.int(1), ctx.int(2), ctx.int(3)],
vec![ctx.int(4), ctx.int(5), ctx.int(6)],
])
.unwrap();
assert!(
symplex::ode::solve_ode_system(&a, &t).is_none(),
"non-square matrix should return None"
);
}
#[test]
fn ode_system_rejects_time_dependent() {
let ctx = Context::new();
let t = ctx.symbol("t");
let a = Matrix::new(vec![
vec![t.clone(), ctx.int(0)],
vec![ctx.int(0), ctx.int(1)],
])
.unwrap();
assert!(
symplex::ode::solve_ode_system(&a, &t).is_none(),
"time-dependent matrix should return None"
);
}
#[test]
fn ode_system_zero_matrix() {
let ctx = Context::new();
let t = ctx.symbol("t");
let a = Matrix::zeros(&ctx, 2, 2);
let sol = symplex::ode::solve_ode_system(&a, &t).expect("zero matrix should be solvable");
assert_eq!(sol.len(), 2);
let s0 = format!("{}", sol[0]);
let s1 = format!("{}", sol[1]);
assert!(s0.contains("C1"), "should have C1: {s0}");
assert!(s1.contains("C2"), "should have C2: {s1}");
}
#[test]
fn ode_system_1x1() {
let ctx = Context::new();
let t = ctx.symbol("t");
let a = Matrix::new(vec![vec![ctx.int(-3)]]).unwrap();
let sol = symplex::ode::solve_ode_system(&a, &t).expect("1x1 system should be solvable");
assert_eq!(sol.len(), 1);
let s = format!("{}", sol[0]);
assert!(s.contains("C1"), "should have C1: {s}");
assert!(s.contains("exp"), "should have exp: {s}");
}
#[test]
fn classify_constant_coefficient_system() {
let ctx = Context::new();
let t = ctx.symbol("t");
let a_const = Matrix::new(vec![
vec![ctx.int(1), ctx.int(2)],
vec![ctx.int(3), ctx.int(4)],
])
.unwrap();
assert!(
symplex::ode::classify_ode_system_is_constant(&a_const, &t),
"pure numeric matrix should be constant-coefficient"
);
let x = ctx.symbol("x");
let a_sym = Matrix::new(vec![
vec![x.clone(), ctx.int(0)],
vec![ctx.int(0), ctx.int(1)],
])
.unwrap();
assert!(
symplex::ode::classify_ode_system_is_constant(&a_sym, &t),
"matrix with symbols other than t should be constant-coefficient"
);
let a_time = Matrix::new(vec![
vec![t.clone(), ctx.int(0)],
vec![ctx.int(0), ctx.int(1)],
])
.unwrap();
assert!(
!symplex::ode::classify_ode_system_is_constant(&a_time, &t),
"matrix containing t should NOT be constant-coefficient"
);
}
#[test]
fn ode_system_nonhomogeneous_basic() {
let ctx = Context::new();
let t = ctx.symbol("t");
let a = Matrix::new(vec![
vec![ctx.int(-1), ctx.int(0)],
vec![ctx.int(0), ctx.int(-1)],
])
.unwrap();
let b = vec![ctx.int(1), ctx.int(0)];
let sol = symplex::ode::solve_ode_system_nonhomogeneous(&a, &b, &t);
if let Some(sol) = sol {
assert_eq!(sol.len(), 2, "should return 2 components");
let s0 = format!("{}", sol[0]);
let s1 = format!("{}", sol[1]);
assert!(
s0.contains("C1") || s0.contains("exp"),
"x1(t) should have homogeneous part: {s0}"
);
assert!(
s1.contains("C2") || s1.contains("exp"),
"x2(t) should have homogeneous part: {s1}"
);
}
}
#[test]
fn ode_system_nonhomogeneous_rejects_mismatched_dims() {
let ctx = Context::new();
let t = ctx.symbol("t");
let a = Matrix::new(vec![
vec![ctx.int(1), ctx.int(0)],
vec![ctx.int(0), ctx.int(1)],
])
.unwrap();
let b = vec![ctx.int(1)];
assert!(
symplex::ode::solve_ode_system_nonhomogeneous(&a, &b, &t).is_none(),
"dimension mismatch should return None"
);
}
#[test]
fn ode_system_solution_dimension_matches_matrix() {
let ctx = Context::new();
let t = ctx.symbol("t");
for n in 1..=4 {
let a = Matrix::identity(&ctx, n);
let sol = symplex::ode::solve_ode_system(&a, &t)
.unwrap_or_else(|| panic!("should solve {n}x{n} identity system"));
assert_eq!(
sol.len(),
n,
"solution vector length should equal matrix dimension {n}"
);
}
}
#[test]
fn ode_system_nilpotent_2x2() {
let ctx = Context::new();
let t = ctx.symbol("t");
let a = Matrix::new(vec![
vec![ctx.int(0), ctx.int(1)],
vec![ctx.int(0), ctx.int(0)],
])
.unwrap();
let sol = symplex::ode::solve_ode_system(&a, &t).expect("should solve nilpotent system");
assert_eq!(sol.len(), 2);
let s0 = format!("{}", sol[0]);
let s1 = format!("{}", sol[1]);
assert!(
s0.contains("C1") || s0.contains("C2"),
"x1(t) should have constants: {s0}"
);
assert!(
s1.contains("C2") || s1.contains("C1"),
"x2(t) should have a constant: {s1}"
);
}
#[test]
fn ode_system_negative_diagonal() {
let ctx = Context::new();
let t = ctx.symbol("t");
let a = Matrix::new(vec![
vec![ctx.int(-1), ctx.int(0)],
vec![ctx.int(0), ctx.int(-2)],
])
.unwrap();
let sol = symplex::ode::solve_ode_system(&a, &t).expect("should solve negative diagonal");
assert_eq!(sol.len(), 2);
let s0 = format!("{}", sol[0]);
let s1 = format!("{}", sol[1]);
assert!(s0.contains("exp"), "x1 should have exp: {s0}");
assert!(s1.contains("exp"), "x2 should have exp: {s1}");
}