use super::*;
use crate::kernel::{Domain, ExprPool};
use rug::Rational;
fn ri(n: i64) -> Rational {
Rational::from(n)
}
fn rr(n: i64, d: i64) -> Rational {
Rational::from((n, d))
}
fn solve(
pool: &ExprPool,
x: ExprId,
p: ExprId,
q: ExprId,
r: ExprId,
x0: ExprId,
order: usize,
) -> SeriesResult {
let ode = SeriesOde::new(x, p, q, r);
series_solve(&ode, x0, order, pool).expect("series_solve should succeed")
}
#[test]
fn ordinary_y_pp_plus_y() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let one = pool.integer(1);
let zero = pool.integer(0);
let res = solve(&pool, x, one, zero, one, zero, 9);
assert_eq!(res.kind, PointKind::Ordinary);
assert_eq!(res.solutions.len(), 2);
let c = &res.solutions[0].coeffs;
assert_eq!(c[0], ri(1));
assert_eq!(c[1], ri(0));
assert_eq!(c[2], rr(-1, 2));
assert_eq!(c[3], ri(0));
assert_eq!(c[4], rr(1, 24));
assert_eq!(c[6], rr(-1, 720));
let s = &res.solutions[1].coeffs;
assert_eq!(s[0], ri(0));
assert_eq!(s[1], ri(1));
assert_eq!(s[3], rr(-1, 6));
assert_eq!(s[5], rr(1, 120));
}
#[test]
fn airy_ordinary() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let one = pool.integer(1);
let zero = pool.integer(0);
let neg_x = pool.mul(vec![pool.integer(-1), x]);
let res = solve(&pool, x, one, zero, neg_x, zero, 12);
assert_eq!(res.kind, PointKind::Ordinary);
let c = &res.solutions[0].coeffs; assert_eq!(c[0], ri(1));
assert_eq!(c[1], ri(0));
assert_eq!(c[2], ri(0));
assert_eq!(c[3], rr(1, 6)); assert_eq!(c[6], rr(1, 180)); let s = &res.solutions[1].coeffs;
assert_eq!(s[1], ri(1));
assert_eq!(s[4], rr(1, 12)); }
#[test]
fn legendre_l2_polynomial() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let one = pool.integer(1);
let x2 = pool.pow(x, pool.integer(2));
let p = pool.add(vec![one, pool.mul(vec![pool.integer(-1), x2])]);
let q = pool.mul(vec![pool.integer(-2), x]);
let r = pool.integer(6);
let zero = pool.integer(0);
let res = solve(&pool, x, p, q, r, zero, 10);
assert_eq!(res.kind, PointKind::Ordinary);
let c = &res.solutions[0].coeffs;
assert_eq!(c[0], ri(1));
assert_eq!(c[2], ri(-3));
for (k, ck) in c.iter().enumerate().skip(3) {
assert_eq!(
*ck,
ri(0),
"coeff {k} should vanish (polynomial terminates)"
);
}
}
#[test]
fn bessel_j0_frobenius() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let x2 = pool.pow(x, pool.integer(2));
let zero = pool.integer(0);
let res = solve(&pool, x, x2, x, x2, zero, 9);
assert_eq!(res.kind, PointKind::RegularSingular);
let s1 = &res.solutions[0];
assert_eq!(s1.exponent, ri(0));
let c = &s1.coeffs;
assert_eq!(c[0], ri(1));
assert_eq!(c[2], rr(-1, 4));
assert_eq!(c[4], rr(1, 64));
assert_eq!(c[6], rr(-1, 2304));
}
#[test]
fn bessel_j0_has_log_second_solution() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let x2 = pool.pow(x, pool.integer(2));
let res = solve(&pool, x, x2, x, x2, pool.integer(0), 8);
assert_eq!(
res.solutions.len(),
2,
"log second solution should be produced"
);
let s2 = &res.solutions[1];
assert!(
s2.log_coeff.is_some(),
"second solution should carry a log term"
);
assert_eq!(s2.log_coeff.clone().unwrap(), ri(1));
}
#[test]
fn frobenius_noninteger_gap_two_series() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let x2 = pool.pow(x, pool.integer(2));
let r = pool.rational(-1, 9);
let res = solve(&pool, x, x2, x, r, pool.integer(0), 6);
assert_eq!(res.kind, PointKind::RegularSingular);
assert_eq!(res.solutions.len(), 2);
assert_eq!(res.solutions[0].exponent, rr(1, 3));
assert_eq!(res.solutions[1].exponent, rr(-1, 3));
assert!(res.solutions[0].log_coeff.is_none());
assert!(res.solutions[1].log_coeff.is_none());
}
#[test]
fn euler_equal_root_log() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let x2 = pool.pow(x, pool.integer(2));
let neg_x = pool.mul(vec![pool.integer(-1), x]);
let one = pool.integer(1);
let res = solve(&pool, x, x2, neg_x, one, pool.integer(0), 6);
assert_eq!(res.kind, PointKind::RegularSingular);
assert_eq!(res.solutions.len(), 2);
assert_eq!(res.solutions[0].exponent, ri(1));
assert_eq!(res.solutions[0].coeffs[0], ri(1));
for k in 1..res.solutions[0].coeffs.len() {
assert_eq!(res.solutions[0].coeffs[k], ri(0));
}
let s2 = &res.solutions[1];
assert_eq!(s2.exponent, ri(1));
assert_eq!(s2.log_coeff.clone().unwrap(), ri(1));
for k in 0..s2.coeffs.len() {
assert_eq!(
s2.coeffs[k],
ri(0),
"Euler log bracket should be zero at {k}"
);
}
}
#[test]
fn to_expr_renders() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let one = pool.integer(1);
let zero = pool.integer(0);
let res = solve(&pool, x, one, zero, one, zero, 7);
let e = res.solutions[0].to_expr(x, zero, 7, &pool);
let s = pool.display(e).to_string();
assert!(s.contains('x'), "rendered expr should mention x: {s}");
}
#[test]
fn irregular_singular_declines() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let x3 = pool.pow(x, pool.integer(3));
let one = pool.integer(1);
let zero = pool.integer(0);
let ode = SeriesOde::new(x, x3, zero, one);
let res = series_solve(&ode, pool.integer(0), 6, &pool);
assert!(
matches!(
res,
Err(SeriesError::IrregularSingular) | Err(SeriesError::NotAnalytic(_))
),
"expected irregular/non-analytic decline, got {res:?}"
);
}
#[test]
fn bessel_j1_integer_gap_log() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let x2 = pool.pow(x, pool.integer(2));
let r = pool.add(vec![x2, pool.integer(-1)]);
let res = solve(&pool, x, x2, x, r, pool.integer(0), 8);
assert_eq!(res.kind, PointKind::RegularSingular);
assert_eq!(res.solutions[0].exponent, ri(1));
assert!(res.solutions[0].log_coeff.is_none());
let c = &res.solutions[0].coeffs;
assert_eq!(c[0], ri(1));
assert_eq!(c[2], rr(-1, 8));
assert_eq!(c[4], rr(1, 192));
if res.solutions.len() == 2 {
let s2 = &res.solutions[1];
assert_eq!(s2.exponent, ri(-1));
assert!(
s2.log_coeff.is_some(),
"integer-gap Bessel J₁ needs a log term"
);
} else {
assert!(res.second_solution_declined());
}
}