use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{One, Zero};
use crate::poly::dense::Poly;
#[derive(Clone, Debug)]
pub enum RdeResult {
Solution { numer: Poly, denom: Poly },
NoSolution,
NotImplemented(String),
}
#[allow(dead_code)]
pub fn solve_risch_de_rational(
f_numer: &Poly,
f_denom: &Poly,
g_numer: &Poly,
g_denom: &Poly,
) -> RdeResult {
assert!(!f_denom.is_zero(), "f denominator must be nonzero");
assert!(!g_denom.is_zero(), "g denominator must be nonzero");
if g_numer.is_zero() {
return RdeResult::Solution {
numer: Poly::zero(),
denom: Poly::from_int(1),
};
}
if f_numer.is_zero() {
return solve_y_prime_equals_g(g_numer, g_denom);
}
let d_y = compute_denominator_bound(f_numer, f_denom, g_numer, g_denom);
let n_bound = compute_degree_bound(f_numer, f_denom, g_numer, g_denom, &d_y);
if n_bound < 0 {
return RdeResult::NoSolution;
}
let n_bound = n_bound as usize;
solve_with_ansatz(f_numer, f_denom, g_numer, g_denom, &d_y, n_bound)
}
#[allow(dead_code)]
pub fn solve_risch_de(
f_numer: &Poly,
f_denom: &Poly,
g_numer: &Poly,
g_denom: &Poly,
de: &super::tower::DifferentialExtension,
) -> RdeResult {
if de.is_base_level() {
return solve_risch_de_rational(f_numer, f_denom, g_numer, g_denom);
}
RdeResult::NotImplemented("Risch DE in monomial extensions not yet implemented".into())
}
fn solve_y_prime_equals_g(g_numer: &Poly, g_denom: &Poly) -> RdeResult {
let hr = super::hermite::hermite_reduce(g_numer, g_denom);
if hr.h_numer.is_zero() {
return RdeResult::Solution {
numer: hr.g_numer,
denom: hr.g_denom,
};
}
RdeResult::NoSolution
}
fn compute_denominator_bound(
_f_numer: &Poly,
f_denom: &Poly,
_g_numer: &Poly,
g_denom: &Poly,
) -> Poly {
let f_sqfree = f_denom.square_free_part();
let g_sqfree = g_denom.square_free_part();
let gcd_val = Poly::gcd(&f_sqfree, &g_sqfree);
if gcd_val.degree().unwrap_or(0) == 0 {
let product = &f_sqfree * &g_sqfree;
if product.is_zero() || product.is_constant() {
return Poly::from_int(1);
}
product.make_monic()
} else {
let lcm = &(&f_sqfree * &g_sqfree).div(&gcd_val);
if lcm.is_zero() || lcm.is_constant() {
return Poly::from_int(1);
}
lcm.make_monic()
}
}
fn compute_degree_bound(
f_numer: &Poly,
f_denom: &Poly,
g_numer: &Poly,
g_denom: &Poly,
d_y: &Poly,
) -> i64 {
let deg_f = f_numer.degree().unwrap_or(0) as i64 - f_denom.degree().unwrap_or(0) as i64;
let deg_g = g_numer.degree().unwrap_or(0) as i64 - g_denom.degree().unwrap_or(0) as i64;
let deg_dy = d_y.degree().unwrap_or(0) as i64;
let bound1 = deg_g + deg_dy; let bound2 = deg_dy + 1; let bound3 = deg_g - deg_f + deg_dy;
let bound = bound1.max(bound2).max(bound3).max(0);
bound.min(50)
}
#[allow(clippy::needless_range_loop)]
fn solve_with_ansatz(
f_numer: &Poly,
f_denom: &Poly,
g_numer: &Poly,
g_denom: &Poly,
d_y: &Poly,
n_bound: usize,
) -> RdeResult {
let d_y_prime = d_y.derivative();
let d_y_sq = &(d_y * d_y);
let rhs_poly = &(&(g_numer * d_y_sq) * f_denom);
let rhs_degree = rhs_poly.degree().unwrap_or(0);
let num_unknowns = n_bound + 1;
let mut lhs_polys: Vec<Poly> = Vec::with_capacity(num_unknowns);
for i in 0..num_unknowns {
let n_i = Poly::monomial(Ratio::one(), i);
let n_i_prime = n_i.derivative();
let np_dy = &n_i_prime * d_y;
let n_dyp = &n_i * &d_y_prime;
let term1_inner = &np_dy - &n_dyp;
let t1_fd = &term1_inner * f_denom;
let term1 = &t1_fd * g_denom;
let fn_ni = f_numer * &n_i;
let fn_ni_dy = &fn_ni * d_y;
let term2 = &fn_ni_dy * g_denom;
lhs_polys.push(&term1 + &term2);
}
let max_lhs_degree = lhs_polys
.iter()
.filter_map(|p| p.degree())
.max()
.unwrap_or(0);
let num_equations = rhs_degree.max(max_lhs_degree) + 1;
let mut matrix: Vec<Vec<Ratio<BigInt>>> =
vec![vec![Ratio::zero(); num_unknowns + 1]; num_equations];
for j in 0..num_equations {
matrix[j][num_unknowns] = rhs_poly.coeff(j);
}
for (i, lhs_i) in lhs_polys.iter().enumerate() {
for j in 0..num_equations {
matrix[j][i] = lhs_i.coeff(j);
}
}
match solve_linear_system(&mut matrix, num_unknowns) {
Some(solution) => {
let n_poly = Poly::from_coeffs(solution);
if n_poly.is_zero() && !g_numer.is_zero() {
return RdeResult::NoSolution;
}
let gcd_nd = Poly::gcd(&n_poly, d_y);
let result_numer = n_poly.div(&gcd_nd);
let result_denom = d_y.div(&gcd_nd);
RdeResult::Solution {
numer: result_numer,
denom: result_denom,
}
}
None => RdeResult::NoSolution,
}
}
#[allow(clippy::needless_range_loop)]
fn solve_linear_system(
matrix: &mut [Vec<Ratio<BigInt>>],
num_unknowns: usize,
) -> Option<Vec<Ratio<BigInt>>> {
let m = matrix.len();
let n = num_unknowns;
let mut pivot_row = 0;
for col in 0..n {
let mut found = None;
for row in pivot_row..m {
if !matrix[row][col].is_zero() {
found = Some(row);
break;
}
}
let pr = match found {
Some(r) => r,
None => continue, };
matrix.swap(pivot_row, pr);
let pivot_val = matrix[pivot_row][col].clone();
for row in (pivot_row + 1)..m {
if !matrix[row][col].is_zero() {
let factor = matrix[row][col].clone() / &pivot_val;
for j in col..=n {
let sub = &factor * &matrix[pivot_row][j];
matrix[row][j] = &matrix[row][j] - ⊂
}
}
}
pivot_row += 1;
}
for row in pivot_row..m {
if !matrix[row][n].is_zero() {
return None; }
}
let mut solution = vec![Ratio::zero(); n];
for row in (0..pivot_row).rev() {
let mut pivot_col = None;
for col in 0..n {
if !matrix[row][col].is_zero() {
pivot_col = Some(col);
break;
}
}
let col = match pivot_col {
Some(c) => c,
None => continue,
};
let mut rhs = matrix[row][n].clone();
for j in (col + 1)..n {
rhs -= &matrix[row][j] * &solution[j];
}
solution[col] = rhs / &matrix[row][col];
}
Some(solution)
}
#[cfg(test)]
mod tests {
use super::*;
fn rat(n: i64, d: i64) -> Ratio<BigInt> {
Ratio::new(BigInt::from(n), BigInt::from(d))
}
fn verify_rde_solution(
f_numer: &Poly,
f_denom: &Poly,
g_numer: &Poly,
g_denom: &Poly,
y_numer: &Poly,
y_denom: &Poly,
) {
let yn_prime = y_numer.derivative();
let yd_prime = y_denom.derivative();
let yn_yd = &yn_prime * y_denom;
let yn_ydp = y_numer * &yd_prime;
let y_prime_numer = &yn_yd - &yn_ydp;
let y_prime_denom = y_denom * y_denom;
let fd_yd = &(f_denom * y_denom);
let lhs_term1 = &y_prime_numer * fd_yd;
let fn_yn = &(f_numer * y_numer);
let lhs_term2 = fn_yn * &y_prime_denom;
let lhs_numer = &lhs_term1 + &lhs_term2;
let ypd_fd = &y_prime_denom * f_denom;
let lhs_denom = &ypd_fd * y_denom;
let lhs_cross = &lhs_numer * g_denom;
let rhs_cross = g_numer * &lhs_denom;
let check = &lhs_cross - &rhs_cross;
assert!(
check.is_zero(),
"RDE solution verification failed: y' + f·y ≠ g\n residual = {check}"
);
}
#[test]
fn rde_trivial_g_zero() {
let f_n = Poly::from_int(2);
let f_d = Poly::from_int(1);
let g_n = Poly::zero();
let g_d = Poly::from_int(1);
match solve_risch_de_rational(&f_n, &f_d, &g_n, &g_d) {
RdeResult::Solution { numer, denom } => {
assert!(numer.is_zero(), "y should be 0 for g=0");
verify_rde_solution(&f_n, &f_d, &g_n, &g_d, &numer, &denom);
}
other => panic!("expected Solution, got {:?}", other),
}
}
#[test]
fn rde_f_zero_poly_g() {
let f_n = Poly::zero();
let f_d = Poly::from_int(1);
let g_n = Poly::from_coeffs(vec![rat(0, 1), rat(2, 1)]); let g_d = Poly::from_int(1);
match solve_risch_de_rational(&f_n, &f_d, &g_n, &g_d) {
RdeResult::Solution { numer, denom } => {
verify_rde_solution(&f_n, &f_d, &g_n, &g_d, &numer, &denom);
assert_eq!(numer.degree(), Some(2), "y should be degree 2");
assert!(denom.is_constant(), "denom should be 1");
}
other => panic!("expected Solution, got {:?}", other),
}
}
#[test]
fn rde_constant_f_constant_g() {
let f_n = Poly::from_int(1);
let f_d = Poly::from_int(1);
let g_n = Poly::from_int(1);
let g_d = Poly::from_int(1);
match solve_risch_de_rational(&f_n, &f_d, &g_n, &g_d) {
RdeResult::Solution { numer, denom } => {
verify_rde_solution(&f_n, &f_d, &g_n, &g_d, &numer, &denom);
}
other => panic!("expected Solution, got {:?}", other),
}
}
#[test]
fn rde_exp_minus_x_squared_nonelementary() {
let f_n = Poly::from_coeffs(vec![rat(0, 1), rat(-2, 1)]); let f_d = Poly::from_int(1);
let g_n = Poly::from_int(1);
let g_d = Poly::from_int(1);
let result = solve_risch_de_rational(&f_n, &f_d, &g_n, &g_d);
assert!(
matches!(result, RdeResult::NoSolution),
"y' - 2x·y = 1 should have no rational solution, got {:?}",
result
);
}
#[test]
fn rde_f_zero_rational_g() {
let f_n = Poly::zero();
let f_d = Poly::from_int(1);
let g_n = Poly::from_int(1);
let g_d = Poly::from_coeffs(vec![rat(0, 1), rat(0, 1), rat(1, 1)]);
match solve_risch_de_rational(&f_n, &f_d, &g_n, &g_d) {
RdeResult::Solution { numer, denom } => {
verify_rde_solution(&f_n, &f_d, &g_n, &g_d, &numer, &denom);
}
other => panic!("expected Solution for y'=1/x², got {:?}", other),
}
}
#[test]
fn rde_f_zero_log_g_has_no_rational_solution() {
let f_n = Poly::zero();
let f_d = Poly::from_int(1);
let g_n = Poly::from_int(1);
let g_d = Poly::x();
let result = solve_risch_de_rational(&f_n, &f_d, &g_n, &g_d);
assert!(
matches!(result, RdeResult::NoSolution),
"y' = 1/x should have no rational solution, got {:?}",
result
);
}
#[test]
fn rde_linear_ode_with_poly_solution() {
let f_n = Poly::zero();
let f_d = Poly::from_int(1);
let g_n = Poly::from_coeffs(vec![rat(0, 1), rat(0, 1), rat(3, 1)]); let g_d = Poly::from_int(1);
match solve_risch_de_rational(&f_n, &f_d, &g_n, &g_d) {
RdeResult::Solution { numer, denom } => {
verify_rde_solution(&f_n, &f_d, &g_n, &g_d, &numer, &denom);
assert_eq!(numer.degree(), Some(3), "y should be x³");
}
other => panic!("expected Solution, got {:?}", other),
}
}
#[test]
fn solve_linear_2x2() {
let mut matrix = vec![
vec![rat(1, 1), rat(1, 1), rat(3, 1)],
vec![rat(2, 1), rat(3, 1), rat(8, 1)],
];
let sol = solve_linear_system(&mut matrix, 2).unwrap();
assert_eq!(sol[0], rat(1, 1));
assert_eq!(sol[1], rat(2, 1));
}
#[test]
fn solve_linear_inconsistent() {
let mut matrix = vec![
vec![rat(1, 1), rat(1, 1), rat(1, 1)],
vec![rat(1, 1), rat(1, 1), rat(2, 1)],
];
assert!(solve_linear_system(&mut matrix, 2).is_none());
}
#[test]
fn solve_linear_overdetermined_consistent() {
let mut matrix = vec![
vec![rat(1, 1), rat(3, 1)],
vec![rat(1, 1), rat(3, 1)],
vec![rat(2, 1), rat(6, 1)],
];
let sol = solve_linear_system(&mut matrix, 1).unwrap();
assert_eq!(sol[0], rat(3, 1));
}
#[test]
fn rde_large_degree_polynomial_rhs() {
let f_n = Poly::from_int(1);
let f_d = Poly::from_int(1);
let g_n = Poly::from_coeffs(vec![
rat(0, 1),
rat(1, 1),
rat(0, 1),
rat(1, 1),
rat(0, 1),
rat(1, 1),
]); let g_d = Poly::from_int(1);
match solve_risch_de_rational(&f_n, &f_d, &g_n, &g_d) {
RdeResult::Solution { numer, denom } => {
verify_rde_solution(&f_n, &f_d, &g_n, &g_d, &numer, &denom);
}
other => panic!("expected Solution for y'+y=x^5+x^3+x, got {:?}", other),
}
}
#[test]
fn rde_zero_f_zero_g() {
let f_n = Poly::zero();
let f_d = Poly::from_int(1);
let g_n = Poly::zero();
let g_d = Poly::from_int(1);
match solve_risch_de_rational(&f_n, &f_d, &g_n, &g_d) {
RdeResult::Solution { numer, denom } => {
assert!(numer.is_zero(), "y should be 0");
verify_rde_solution(&f_n, &f_d, &g_n, &g_d, &numer, &denom);
}
other => panic!("expected Solution(0), got {:?}", other),
}
}
#[test]
fn rde_negative_constant_f() {
let f_n = Poly::from_int(-3);
let f_d = Poly::from_int(1);
let g_n = Poly::zero();
let g_d = Poly::from_int(1);
match solve_risch_de_rational(&f_n, &f_d, &g_n, &g_d) {
RdeResult::Solution { numer, .. } => {
assert!(
numer.is_zero(),
"y' - 3y = 0: only rational solution is y=0"
);
}
other => panic!("expected Solution(0), got {:?}", other),
}
}
#[test]
fn rde_negative_f_with_rhs() {
let f_n = Poly::from_int(-1);
let f_d = Poly::from_int(1);
let g_n = Poly::x(); let g_d = Poly::from_int(1);
match solve_risch_de_rational(&f_n, &f_d, &g_n, &g_d) {
RdeResult::Solution { numer, denom } => {
verify_rde_solution(&f_n, &f_d, &g_n, &g_d, &numer, &denom);
}
other => panic!("expected Solution for y'-y=x, got {:?}", other),
}
}
#[test]
fn rde_quadratic_f_no_solution() {
let f_n = Poly::from_coeffs(vec![rat(0, 1), rat(0, 1), rat(1, 1)]); let f_d = Poly::from_int(1);
let g_n = Poly::from_int(1);
let g_d = Poly::from_int(1);
let result = solve_risch_de_rational(&f_n, &f_d, &g_n, &g_d);
assert!(
matches!(result, RdeResult::NoSolution),
"y' + x²·y = 1 should have no rational solution, got {:?}",
result
);
}
#[test]
fn rde_f_zero_cubic_g() {
let f_n = Poly::zero();
let f_d = Poly::from_int(1);
let g_n = Poly::from_coeffs(vec![rat(1, 1), rat(2, 1), rat(6, 1)]); let g_d = Poly::from_int(1);
match solve_risch_de_rational(&f_n, &f_d, &g_n, &g_d) {
RdeResult::Solution { numer, denom } => {
verify_rde_solution(&f_n, &f_d, &g_n, &g_d, &numer, &denom);
assert_eq!(numer.degree(), Some(3), "y should be degree 3");
}
other => panic!("expected Solution, got {:?}", other),
}
}
}