use crate::calculus::fps::{Fps, FpsError};
use crate::kernel::{ExprData, ExprId, ExprPool};
use rug::{Integer, Rational};
use std::collections::HashMap;
use std::fmt;
#[derive(Clone, Debug)]
pub struct SeriesOde {
pub x: ExprId,
pub p: ExprId,
pub q: ExprId,
pub r: ExprId,
}
impl SeriesOde {
pub fn new(x: ExprId, p: ExprId, q: ExprId, r: ExprId) -> Self {
SeriesOde { x, p, q, r }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PointKind {
Ordinary,
RegularSingular,
}
#[derive(Clone, Debug)]
pub struct SeriesSolution {
pub exponent: Rational,
pub coeffs: Vec<Rational>,
pub log_coeff: Option<Rational>,
pub log_base: Option<(Rational, Vec<Rational>)>,
}
#[derive(Clone, Debug)]
pub struct SeriesResult {
pub kind: PointKind,
pub x0: ExprId,
pub order: usize,
pub solutions: Vec<SeriesSolution>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SeriesError {
NotAnalytic(String),
IrregularSingular,
IrrationalIndicialRoot,
DegenerateLeadingCoefficient,
VerificationFailed(String),
SecondSolutionDeclined(String),
}
impl fmt::Display for SeriesError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SeriesError::NotAnalytic(m) => write!(f, "series_solve: not analytic at x₀: {m}"),
SeriesError::IrregularSingular => {
write!(
f,
"series_solve: irregular singular point (no Frobenius series)"
)
}
SeriesError::IrrationalIndicialRoot => {
write!(f, "series_solve: irrational indicial roots are not handled")
}
SeriesError::DegenerateLeadingCoefficient => {
write!(
f,
"series_solve: leading coefficient p(x) is identically zero"
)
}
SeriesError::VerificationFailed(m) => {
write!(
f,
"series_solve: candidate failed exact-residual check: {m}"
)
}
SeriesError::SecondSolutionDeclined(m) => {
write!(f, "series_solve: second solution declined: {m}")
}
}
}
}
impl std::error::Error for SeriesError {}
impl crate::errors::AlkahestError for SeriesError {
fn code(&self) -> &'static str {
match self {
SeriesError::NotAnalytic(_) => "E-ODE-020",
SeriesError::IrregularSingular => "E-ODE-021",
SeriesError::IrrationalIndicialRoot => "E-ODE-022",
SeriesError::DegenerateLeadingCoefficient => "E-ODE-023",
SeriesError::VerificationFailed(_) => "E-ODE-024",
SeriesError::SecondSolutionDeclined(_) => "E-ODE-025",
}
}
fn remediation(&self) -> Option<&'static str> {
match self {
SeriesError::NotAnalytic(_) => {
Some("ensure p, q, r are analytic at x₀ with rational Taylor coefficients")
}
SeriesError::IrregularSingular => {
Some("series_solve handles ordinary and regular-singular points only")
}
SeriesError::IrrationalIndicialRoot => {
Some("the indicial equation has irrational roots, outside the ℚ-recurrence path")
}
SeriesError::DegenerateLeadingCoefficient => {
Some("supply a non-zero coefficient for y''")
}
SeriesError::VerificationFailed(_) => {
Some("the candidate series did not satisfy the ODE exactly; it is withheld")
}
SeriesError::SecondSolutionDeclined(_) => Some(
"the logarithmic second solution is intractable for this equation; only the \
first solution is returned (SymPy also struggles with this case)",
),
}
}
}
pub fn series_solve(
ode: &SeriesOde,
x0: ExprId,
order: usize,
pool: &ExprPool,
) -> Result<SeriesResult, SeriesError> {
let order = order.max(2);
let t = pool.symbol("·t_series·", crate::kernel::Domain::Real);
let shifted_x = pool.add(vec![x0, t]);
let p = subs_x(ode.p, ode.x, shifted_x, pool);
let q = subs_x(ode.q, ode.x, shifted_x, pool);
let r = subs_x(ode.r, ode.x, shifted_x, pool);
let need = order + 8; let pa = expr_coeffs(p, t, need, pool)?;
let qa = expr_coeffs(q, t, need, pool)?;
let ra = expr_coeffs(r, t, need, pool)?;
if pa.iter().all(|c| *c == 0) {
return Err(SeriesError::DegenerateLeadingCoefficient);
}
let p0 = pa[0].clone();
if p0 != 0 {
ordinary_point(&pa, &qa, &ra, x0, order, pool)
} else {
frobenius(&pa, &qa, &ra, x0, order, pool)
}
}
fn ordinary_point(
pa: &[Rational],
qa: &[Rational],
ra: &[Rational],
x0: ExprId,
order: usize,
pool: &ExprPool,
) -> Result<SeriesResult, SeriesError> {
let pfps = Fps::from_poly(pa);
let qfps = Fps::from_poly(qa);
let rfps = Fps::from_poly(ra);
let big_p = qfps.div(&pfps).map_err(map_fps)?;
let big_q = rfps.div(&pfps).map_err(map_fps)?;
let pc = big_p.coeffs(order + 2);
let qc = big_q.coeffs(order + 2);
let sol1 = power_series_solution(&pc, &qc, [Rational::from(1), Rational::from(0)], order);
let sol2 = power_series_solution(&pc, &qc, [Rational::from(0), Rational::from(1)], order);
let mut solutions = Vec::new();
for coeffs in [sol1, sol2] {
let s = SeriesSolution {
exponent: Rational::from(0),
coeffs,
log_coeff: None,
log_base: None,
};
verify_solution(pa, qa, ra, &s, order, pool)?;
solutions.push(s);
}
Ok(SeriesResult {
kind: PointKind::Ordinary,
x0,
order,
solutions,
})
}
fn power_series_solution(
pc: &[Rational],
qc: &[Rational],
init: [Rational; 2],
order: usize,
) -> Vec<Rational> {
let mut a: Vec<Rational> = Vec::with_capacity(order);
a.push(init[0].clone());
if order >= 2 {
a.push(init[1].clone());
}
for n in 0..order.saturating_sub(2) {
let mut acc = Rational::from(0);
for j in 0..=n {
let pj = pc.get(j).cloned().unwrap_or_else(|| Rational::from(0));
if pj != 0 {
acc += pj * Rational::from(n + 1 - j) * a[n + 1 - j].clone();
}
let qj = qc.get(j).cloned().unwrap_or_else(|| Rational::from(0));
if qj != 0 {
acc += qj * a[n - j].clone();
}
}
let denom = Rational::from((n + 2) * (n + 1));
a.push(-acc / denom);
}
a.truncate(order);
a
}
fn frobenius(
pa: &[Rational],
qa: &[Rational],
ra: &[Rational],
x0: ExprId,
order: usize,
pool: &ExprPool,
) -> Result<SeriesResult, SeriesError> {
let nq = shift_coeffs(qa, 1); let nr = shift_coeffs(ra, 2); let pc = series_quotient_shifted(&nq, pa, order + 2)?; let qc = series_quotient_shifted(&nr, pa, order + 2)?; let p0 = pc[0].clone();
let q0 = qc[0].clone();
let b = p0.clone() - Rational::from(1);
let c = q0.clone();
let (r1, r2) = match rational_quadratic_roots(&b, &c) {
Some(roots) => roots,
None => return Err(SeriesError::IrrationalIndicialRoot),
};
let mut solutions = Vec::new();
let coeffs1 = frobenius_coeffs(&pc, &qc, &r1, order);
let sol1 = SeriesSolution {
exponent: r1.clone(),
coeffs: coeffs1.clone(),
log_coeff: None,
log_base: None,
};
verify_solution(pa, qa, ra, &sol1, order, pool)?;
solutions.push(sol1);
let diff = r1.clone() - r2.clone();
let diff_is_int = diff.denom() == &Integer::from(1);
if !diff_is_int {
let coeffs2 = frobenius_coeffs(&pc, &qc, &r2, order);
let sol2 = SeriesSolution {
exponent: r2.clone(),
coeffs: coeffs2,
log_coeff: None,
log_base: None,
};
verify_solution(pa, qa, ra, &sol2, order, pool)?;
solutions.push(sol2);
} else if r1 == r2 {
match frobenius_log_equal(&pc, &qc, &r1, &coeffs1, order) {
Ok(sol2) => {
verify_solution(pa, qa, ra, &sol2, order, pool)?;
solutions.push(sol2);
}
Err(e) => return finish_with_decline(solutions, x0, order, e),
}
} else {
match frobenius_log_integer(pa, qa, ra, &r1, &r2, &coeffs1, order) {
Ok(sol2) => {
verify_solution(pa, qa, ra, &sol2, order, pool)?;
solutions.push(sol2);
}
Err(e) => return finish_with_decline(solutions, x0, order, e),
}
}
Ok(SeriesResult {
kind: PointKind::RegularSingular,
x0,
order,
solutions,
})
}
fn indicial(rho: &Rational, n: usize, p0: &Rational, q0: &Rational) -> Rational {
let s = rho.clone() + Rational::from(n);
s.clone() * (s.clone() - Rational::from(1)) + p0.clone() * s + q0.clone()
}
fn frobenius_coeffs(
pc: &[Rational],
qc: &[Rational],
rho: &Rational,
order: usize,
) -> Vec<Rational> {
let p0 = pc[0].clone();
let q0 = qc[0].clone();
let mut b: Vec<Rational> = Vec::with_capacity(order);
b.push(Rational::from(1));
for n in 1..order {
let mut acc = Rational::from(0);
for k in 1..=n {
let pk = pc.get(k).cloned().unwrap_or_else(|| Rational::from(0));
let qk = qc.get(k).cloned().unwrap_or_else(|| Rational::from(0));
if pk == 0 && qk == 0 {
continue;
}
let factor = pk * (rho.clone() + Rational::from(n - k)) + qk;
acc += factor * b[n - k].clone();
}
let ind = indicial(rho, n, &p0, &q0);
if ind == 0 {
b.push(Rational::from(0));
} else {
b.push(-acc / ind);
}
}
b
}
fn frobenius_log_equal(
pc: &[Rational],
qc: &[Rational],
rho: &Rational,
coeffs1: &[Rational],
order: usize,
) -> Result<SeriesSolution, SeriesError> {
let dcoeffs = frobenius_dcoeffs(pc, qc, rho, order)?;
Ok(SeriesSolution {
exponent: rho.clone(),
coeffs: dcoeffs,
log_coeff: Some(Rational::from(1)),
log_base: Some((rho.clone(), coeffs1.to_vec())),
})
}
fn frobenius_dcoeffs(
pc: &[Rational],
qc: &[Rational],
rho: &Rational,
order: usize,
) -> Result<Vec<Rational>, SeriesError> {
let p0 = pc[0].clone();
let q0 = qc[0].clone();
let b = frobenius_coeffs(pc, qc, rho, order);
let mut db: Vec<Rational> = Vec::with_capacity(order);
db.push(Rational::from(0)); for n in 1..order {
let ind = indicial(rho, n, &p0, &q0);
if ind == 0 {
return Err(SeriesError::SecondSolutionDeclined(format!(
"indicial obstruction at n={n} (equal-root log path)"
)));
}
let s = rho.clone() + Rational::from(n);
let ind_prime = Rational::from(2) * s.clone() - Rational::from(1) + p0.clone();
let mut acc = ind_prime * b[n].clone();
for k in 1..=n {
let pk = pc.get(k).cloned().unwrap_or_else(|| Rational::from(0));
let qk = qc.get(k).cloned().unwrap_or_else(|| Rational::from(0));
if pk != 0 {
acc += pk.clone() * b[n - k].clone();
}
let factor = pk * (rho.clone() + Rational::from(n - k)) + qk;
if factor != 0 {
acc += factor * db[n - k].clone();
}
}
db.push(-acc / ind);
}
Ok(db)
}
fn frobenius_log_integer(
pa: &[Rational],
qa: &[Rational],
ra: &[Rational],
r1: &Rational,
r2: &Rational,
coeffs1: &[Rational],
order: usize,
) -> Result<SeriesSolution, SeriesError> {
let p = Fps::from_poly(pa);
let q = Fps::from_poly(qa);
let r = Fps::from_poly(ra);
let v = valuation(pa).ok_or(SeriesError::DegenerateLeadingCoefficient)?;
let resid = |bracket: &[Rational], big_c: &Rational, idx: usize| -> Rational {
let log = if *big_c == 0 {
None
} else {
Some((big_c, r1, coeffs1))
};
residual_series(&p, &q, &r, r2, bracket, log).coeff(idx)
};
let zero = Rational::from(0);
let one = Rational::from(1);
let mut c: Vec<Rational> = vec![Rational::from(0); order];
c[0] = one.clone();
let mut big_c = zero.clone();
for idx in v..(order + v) {
let j = idx - v;
if j >= order {
break;
}
c[j] = zero.clone();
let base = resid(&c, &big_c, idx);
c[j] = one.clone();
let with_one = resid(&c, &big_c, idx);
c[j] = zero.clone();
let diag = with_one - base.clone();
if diag != 0 {
c[j] = -base / diag;
continue;
}
let base_c0 = resid(&c, &zero, idx);
let with_c1 = resid(&c, &one, idx);
let dc = with_c1 - base_c0.clone();
if dc == 0 {
return Err(SeriesError::SecondSolutionDeclined(format!(
"indicial obstruction at bracket index {j} cannot be resolved by a log term"
)));
}
big_c = -base_c0 / dc;
}
let (log_coeff, log_base) = if big_c == 0 {
(None, None)
} else {
(Some(big_c), Some((r1.clone(), coeffs1.to_vec())))
};
Ok(SeriesSolution {
exponent: r2.clone(),
coeffs: c,
log_coeff,
log_base,
})
}
fn finish_with_decline(
solutions: Vec<SeriesSolution>,
x0: ExprId,
order: usize,
_e: SeriesError,
) -> Result<SeriesResult, SeriesError> {
Ok(SeriesResult {
kind: PointKind::RegularSingular,
x0,
order,
solutions,
})
}
impl SeriesSolution {
pub fn to_expr(&self, x: ExprId, x0: ExprId, order: usize, pool: &ExprPool) -> ExprId {
let t = shift_expr(x, x0, pool);
let body = series_body_expr(&self.coeffs, t, order, true, pool);
let prefactor = pow_expr(t, &self.exponent, pool);
let main = pool.mul(vec![prefactor, body]);
if let (Some(c), Some((rho1, base))) = (&self.log_coeff, &self.log_base) {
let y1_body = series_body_expr(base, t, order, false, pool);
let y1 = pool.mul(vec![pow_expr(t, rho1, pool), y1_body]);
let ln = pool.func("ln", vec![t]);
let log_term = pool.mul(vec![rat_to_expr(c, pool), y1, ln]);
pool.add(vec![log_term, main])
} else {
main
}
}
}
impl SeriesResult {
pub fn second_solution_declined(&self) -> bool {
self.kind == PointKind::RegularSingular && self.solutions.len() == 1
}
}
fn series_body_expr(
coeffs: &[Rational],
t: ExprId,
order: usize,
with_o: bool,
pool: &ExprPool,
) -> ExprId {
let mut terms = Vec::new();
for (k, c) in coeffs.iter().enumerate().take(order) {
if *c == 0 {
continue;
}
let ce = rat_to_expr(c, pool);
let term = if k == 0 {
ce
} else if k == 1 {
pool.mul(vec![ce, t])
} else {
pool.mul(vec![ce, pool.pow(t, pool.integer(k as i64))])
};
terms.push(term);
}
if terms.is_empty() {
terms.push(pool.integer(0));
}
if with_o {
terms.push(pool.big_o(pool.pow(t, pool.integer(order as i64))));
}
pool.add(terms)
}
fn pow_expr(t: ExprId, rho: &Rational, pool: &ExprPool) -> ExprId {
if *rho == 0 {
return pool.integer(1);
}
if *rho == 1 {
return t;
}
pool.pow(t, rat_to_expr(rho, pool))
}
fn shift_expr(x: ExprId, x0: ExprId, pool: &ExprPool) -> ExprId {
if matches!(pool.get(x0), ExprData::Integer(ref n) if n.0 == 0) {
return x;
}
let neg = pool.mul(vec![pool.integer(-1), x0]);
pool.add(vec![x, neg])
}
fn verify_solution(
pa: &[Rational],
qa: &[Rational],
ra: &[Rational],
sol: &SeriesSolution,
order: usize,
_pool: &ExprPool,
) -> Result<(), SeriesError> {
let rho = &sol.exponent;
let p = Fps::from_poly(pa);
let q = Fps::from_poly(qa);
let r = Fps::from_poly(ra);
let check_to = order.saturating_sub(1);
let log = sol
.log_base
.as_ref()
.zip(sol.log_coeff.as_ref())
.map(|((rho1, base), c)| (c, rho1, base.as_slice()));
let residual = residual_series(&p, &q, &r, rho, &sol.coeffs, log);
for m in 0..=check_to {
let cm = residual.coeff(m);
if cm != 0 {
return Err(SeriesError::VerificationFailed(format!(
"residual coefficient at order {m} is {cm} (exponent {rho})"
)));
}
}
Ok(())
}
fn residual_series<'p>(
p: &Fps<'p>,
q: &Fps<'p>,
r: &Fps<'p>,
rho: &Rational,
bracket: &[Rational],
log: Option<(&Rational, &Rational, &[Rational])>, ) -> Fps<'p> {
let bfps = Fps::from_poly(bracket);
let d1 = {
let bracket = bracket.to_vec();
let rho = rho.clone();
Fps::from_fn(move |n| {
(rho.clone() + Rational::from(n))
* bracket.get(n).cloned().unwrap_or_else(|| Rational::from(0))
})
};
let d2 = {
let bracket = bracket.to_vec();
let rho = rho.clone();
Fps::from_fn(move |n| {
let s = rho.clone() + Rational::from(n);
s.clone()
* (s - Rational::from(1))
* bracket.get(n).cloned().unwrap_or_else(|| Rational::from(0))
})
};
let tq = mul_t(q);
let t2r = mul_t(&mul_t(r));
let mut residual = p.mul(&d2).add(&tq.mul(&d1)).add(&t2r.mul(&bfps));
if let Some((c, rho1, base)) = log {
let m_shift = (rho1.clone() - rho.clone())
.numer()
.to_i64()
.map(|v| v.max(0) as usize)
.unwrap_or(0);
let s1 = Fps::from_poly(base);
let s2 = {
let base = base.to_vec();
let rho1 = rho1.clone();
Fps::from_fn(move |n| {
(Rational::from(2) * (rho1.clone() + Rational::from(n)) - Rational::from(1))
* base.get(n).cloned().unwrap_or_else(|| Rational::from(0))
})
};
let coupling = p
.mul(&s2)
.add(&tq.mul(&s1))
.scale(c.clone())
.shift_up(m_shift);
residual = residual.add(&coupling);
}
residual
}
fn mul_t<'p>(f: &Fps<'p>) -> Fps<'p> {
f.shift_up(1)
}
fn shift_coeffs(c: &[Rational], k: usize) -> Vec<Rational> {
let mut out = vec![Rational::from(0); k];
out.extend(c.iter().cloned());
out
}
fn valuation(c: &[Rational]) -> Option<usize> {
c.iter().position(|x| *x != 0)
}
fn series_quotient_shifted(
num: &[Rational],
den: &[Rational],
n: usize,
) -> Result<Vec<Rational>, SeriesError> {
let vden = match valuation(den) {
Some(v) => v,
None => return Err(SeriesError::DegenerateLeadingCoefficient),
};
let vnum = valuation(num);
let vnum = match vnum {
Some(v) => v,
None => return Ok(vec![Rational::from(0); n]),
};
if vnum < vden {
return Err(SeriesError::IrregularSingular);
}
let num_shifted: Vec<Rational> = num.iter().skip(vden).cloned().collect();
let den_shifted: Vec<Rational> = den.iter().skip(vden).cloned().collect();
let nf = Fps::from_poly(&num_shifted);
let df = Fps::from_poly(&den_shifted);
let quot = nf.div(&df).map_err(map_fps)?;
Ok(quot.coeffs(n))
}
fn rational_quadratic_roots(b: &Rational, c: &Rational) -> Option<(Rational, Rational)> {
let disc = b.clone() * b.clone() - Rational::from(4) * c.clone();
if disc < 0 {
return None; }
let sqrt = rational_sqrt(&disc)?;
let half = Rational::from((1, 2));
let r1 = half.clone() * (-b.clone() + sqrt.clone());
let r2 = half * (-b.clone() - sqrt);
if r1 >= r2 {
Some((r1, r2))
} else {
Some((r2, r1))
}
}
fn rational_sqrt(x: &Rational) -> Option<Rational> {
if *x < 0 {
return None;
}
if *x == 0 {
return Some(Rational::from(0));
}
let num = x.numer().clone();
let den = x.denom().clone();
let ns = num.sqrt_ref().into();
let ds = den.sqrt_ref().into();
let ns: Integer = ns;
let ds: Integer = ds;
if ns.clone() * ns.clone() == num && ds.clone() * ds.clone() == den {
Some(Rational::from((ns, ds)))
} else {
None
}
}
fn subs_x(expr: ExprId, x: ExprId, replacement: ExprId, pool: &ExprPool) -> ExprId {
let mut m = HashMap::new();
m.insert(x, replacement);
crate::kernel::subs::subs(expr, &m, pool)
}
fn expr_coeffs(
expr: ExprId,
var: ExprId,
n: usize,
pool: &ExprPool,
) -> Result<Vec<Rational>, SeriesError> {
let f = Fps::from_expr(expr, var, pool).map_err(map_fps)?;
Ok(f.coeffs(n))
}
fn map_fps(e: FpsError) -> SeriesError {
match e {
FpsError::NotAnalyticAtZero | FpsError::DenominatorVanishesAtZero => {
SeriesError::NotAnalytic(e.to_string())
}
FpsError::NonRationalCoefficient => SeriesError::NotAnalytic(e.to_string()),
other => SeriesError::NotAnalytic(other.to_string()),
}
}
fn rat_to_expr(r: &Rational, pool: &ExprPool) -> ExprId {
let num = r.numer().clone();
let den = r.denom().clone();
if den == 1 {
pool.integer(num)
} else {
pool.rational(num, den)
}
}
#[cfg(test)]
mod tests;