use super::{
contains, ddx, div, implicit_relation_is_zero, integrate_or_decline, is_zero, residual_is_zero,
simp, sub, subs1, ConstGen, DsolveBranch, DsolveError, OdeInput,
};
use crate::kernel::eval_const::try_expr_f64;
use crate::kernel::{Domain, ExprData, ExprId, ExprPool};
enum Outcome {
Solved(DsolveBranch),
NoMatch,
Declined(DsolveError),
}
pub(crate) fn solve(
input: &OdeInput,
gen: &mut ConstGen,
pool: &ExprPool,
) -> Result<Vec<DsolveBranch>, DsolveError> {
let yp = input.derivs[0];
let mut cascade = Cascade::default();
let mark = gen.checkpoint();
let clairaut = try_clairaut(input, gen, pool);
if let Some(res) = cascade.offer("clairaut", clairaut, pool) {
return Ok(res);
}
gen.rollback(mark, pool);
let a = ddx(input.equation, yp, pool)?; if contains(a, yp, pool) || is_zero(a, pool) {
return Err(DsolveError::Unsupported(
"equation is not linear in y' (or independent of y')".to_string(),
));
}
let ayp = simp(pool.mul(vec![a, yp]), pool);
let b = sub(input.equation, ayp, pool); if contains(b, yp, pool) {
return Err(DsolveError::Unsupported(
"equation is not affine in y'".to_string(),
));
}
let rhs = simp(div(pool.mul(vec![pool.integer(-1_i32), b]), a, pool), pool);
type Attempt = fn(&OdeInput, ExprId, &mut ConstGen, &ExprPool) -> Outcome;
let attempts: [(&'static str, Attempt); 6] = [
("separable", try_separable),
("linear", try_linear),
("bernoulli", try_bernoulli),
("exact", try_exact),
("homogeneous", try_homogeneous),
("riccati", try_riccati),
];
for (class, attempt_fn) in attempts {
let mark = gen.checkpoint();
let outcome = attempt_fn(input, rhs, gen, pool);
if let Some(res) = cascade.offer(class, outcome, pool) {
return Ok(res);
}
gen.rollback(mark, pool);
}
cascade.finish()
}
#[derive(Default)]
struct Cascade {
implicit: Option<DsolveBranch>,
notes: Vec<(String, DsolveError)>,
}
#[cfg(test)]
fn trace_class(class: &str, outcome: &Outcome, pool: &ExprPool) {
if std::env::var_os("ALKAHEST_DSOLVE_TRACE").is_none() {
return;
}
let what = match outcome {
Outcome::Solved(s) if s.is_explicit() => format!("SOLVED {}", s.render(pool)),
Outcome::Solved(s) => format!("SOLVED(implicit) {}", s.render(pool)),
Outcome::NoMatch => "NO_MATCH".to_string(),
Outcome::Declined(e) => format!("DECLINED {e}"),
};
eprintln!("CLASS\t{class}\t{what}");
}
#[cfg(not(test))]
fn trace_class(_class: &str, _outcome: &Outcome, _pool: &ExprPool) {}
impl Cascade {
fn offer(
&mut self,
class: &str,
outcome: Outcome,
pool: &ExprPool,
) -> Option<Vec<DsolveBranch>> {
trace_class(class, &outcome, pool);
match outcome {
Outcome::Solved(sol) if sol.is_explicit() => Some(vec![sol]),
Outcome::Solved(sol) => {
if self.implicit.is_none() {
self.implicit = Some(sol);
}
None
}
Outcome::NoMatch => None,
Outcome::Declined(e) => {
self.notes.push((class.to_string(), e));
None
}
}
}
fn finish(self) -> Result<Vec<DsolveBranch>, DsolveError> {
if let Some(sol) = self.implicit {
return Ok(vec![sol]);
}
if self.notes.is_empty() {
return Err(DsolveError::Unsupported(
"no implemented first-order class matched".to_string(),
));
}
let detail = self
.notes
.iter()
.map(|(class, e)| format!("{class}: {e}"))
.collect::<Vec<_>>()
.join("; ");
let kind = |e: &DsolveError| {
if e.is_quadrature_failure() {
3
} else if e.is_missing_particular_solution() {
2
} else if matches!(e, DsolveError::VerificationFailed(_)) {
1
} else {
0
}
};
let best = self.notes.iter().map(|(_, e)| kind(e)).max().unwrap_or(0);
let msg = format!("no first-order class produced a verified solution ({detail})");
Err(match best {
3 => DsolveError::quadrature_failed(msg),
2 => DsolveError::no_particular_solution(msg),
_ => DsolveError::Unsupported(msg),
})
}
}
fn finalize_explicit(
input: &OdeInput,
y_of_x: ExprId,
constants: Vec<ExprId>,
method: &'static str,
pool: &ExprPool,
) -> Outcome {
let y_of_x = simp(y_of_x, pool);
match residual_is_zero(input, y_of_x, &constants, pool) {
Ok(()) => Outcome::Solved(DsolveBranch::explicit(y_of_x, constants, method)),
Err(e) => Outcome::Declined(e),
}
}
fn finalize_implicit(
input: &OdeInput,
relation: ExprId,
constants: Vec<ExprId>,
method: &'static str,
pool: &ExprPool,
) -> Outcome {
let relation = simp(relation, pool);
match implicit_relation_is_zero(input, relation, &constants, pool) {
Ok(()) => Outcome::Solved(DsolveBranch::implicit(relation, constants, method)),
Err(e) => Outcome::Declined(e),
}
}
fn explicit_or_implicit(
input: &OdeInput,
relation: ExprId,
constants: Vec<ExprId>,
method: &'static str,
pool: &ExprPool,
) -> Outcome {
let c = *constants
.first()
.expect("a general solution carries a constant");
if let Some(y_expr) = solve_relation_for_y(relation, input.x, input.y, c, pool) {
if !contains(y_expr, input.y, pool) {
match finalize_explicit(input, y_expr, constants.clone(), method, pool) {
Outcome::Solved(sol) => return Outcome::Solved(sol),
#[cfg(test)]
Outcome::Declined(e) if std::env::var_os("ALKAHEST_DSOLVE_TRACE").is_some() => {
eprintln!(
"INVERSION_REFUSED\t{method}\ty = {}\t{e}",
pool.display(y_expr)
);
}
_ => {}
}
}
}
finalize_implicit(input, relation, constants, method, pool)
}
fn quadrature_decline(
class: &str,
what: &str,
integrand: ExprId,
e: DsolveError,
pool: &ExprPool,
) -> Outcome {
Outcome::Declined(DsolveError::quadrature_failed(format!(
"{class} needs {what} = ∫ `{}`: {e}",
pool.display(integrand)
)))
}
fn unwrap1(e: ExprId, pool: &ExprPool) -> ExprId {
match pool.get(e) {
ExprData::Mul(args) | ExprData::Add(args) if args.len() == 1 => unwrap1(args[0], pool),
_ => e,
}
}
fn separable_split(
expr: ExprId,
x: ExprId,
y: ExprId,
pool: &ExprPool,
) -> Option<(ExprId, ExprId)> {
let factors: Vec<ExprId> = match pool.get(expr) {
ExprData::Mul(args) => args,
_ => vec![expr],
};
let mut gx: Vec<ExprId> = Vec::new();
let mut hy: Vec<ExprId> = Vec::new();
for f in factors {
let has_x = contains(f, x, pool);
let has_y = contains(f, y, pool);
match (has_x, has_y) {
(true, false) => gx.push(f),
(false, true) => hy.push(f),
(false, false) => gx.push(f), (true, true) => return None, }
}
let g = if gx.is_empty() {
pool.integer(1_i32)
} else {
pool.mul(gx)
};
let h = if hy.is_empty() {
pool.integer(1_i32)
} else {
pool.mul(hy)
};
Some((unwrap1(simp(g, pool), pool), unwrap1(simp(h, pool), pool)))
}
fn separable_split_quotient(
expr: ExprId,
x: ExprId,
y: ExprId,
pool: &ExprPool,
) -> Option<(ExprId, ExprId)> {
let (numer, denom) = crate::poly::cancel::together_parts(expr, vec![x, y], pool).ok()?;
let (gn, hn) = separable_split(numer, x, y, pool)?;
let (gd, hd) = separable_split(denom, x, y, pool)?;
if is_zero(gd, pool) || is_zero(hd, pool) {
return None;
}
Some((div(gn, gd, pool), div(hn, hd, pool)))
}
fn try_separable(input: &OdeInput, rhs: ExprId, gen: &mut ConstGen, pool: &ExprPool) -> Outcome {
let (x, y) = (input.x, input.y);
let Some((g, h)) = separable_split(rhs, x, y, pool)
.or_else(|| separable_split(super::simp_plain(rhs, pool), x, y, pool))
.or_else(|| separable_split_quotient(rhs, x, y, pool))
else {
return Outcome::NoMatch;
};
if !contains(h, y, pool) {
return Outcome::NoMatch; }
let inv_h = simp(pool.pow(h, pool.integer(-1_i32)), pool);
let lhs_int = match integrate_or_decline(inv_h, y, pool) {
Ok(v) => v, Err(e) => return quadrature_decline("separable", "∫ dy/h(y)", inv_h, e, pool),
};
let rhs_int = match integrate_or_decline(g, x, pool) {
Ok(v) => v, Err(e) => return quadrature_decline("separable", "∫ g(x) dx", g, e, pool),
};
let c = gen.fresh(pool);
let relation = sub(sub(lhs_int, rhs_int, pool), c, pool);
explicit_or_implicit(input, relation, vec![c], "separable", pool)
}
fn solve_relation_for_y(
relation: ExprId,
x: ExprId,
y: ExprId,
c: ExprId,
pool: &ExprPool,
) -> Option<ExprId> {
let zero = pool.integer(0_i32);
if let Some(e) = solve_implicit_for_y(relation, x, y, zero, pool) {
return Some(e);
}
let (h_of_y, target) = split_y_part(relation, x, y, pool)?;
invert_simple(h_of_y, y, target, c, pool)
}
fn split_y_part(
relation: ExprId,
x: ExprId,
y: ExprId,
pool: &ExprPool,
) -> Option<(ExprId, ExprId)> {
let terms: Vec<ExprId> = match pool.get(relation) {
ExprData::Add(args) => args,
_ => vec![relation],
};
let mut h: Vec<ExprId> = Vec::new();
let mut rest: Vec<ExprId> = Vec::new();
for t in terms {
match (contains(t, y, pool), contains(t, x, pool)) {
(true, false) => h.push(t),
(true, true) => return None,
(false, _) => rest.push(t),
}
}
if h.is_empty() {
return None;
}
let h = unwrap1(simp(pool.add(h), pool), pool);
let neg_rest = if rest.is_empty() {
pool.integer(0_i32)
} else {
unwrap1(
simp(pool.mul(vec![pool.integer(-1_i32), pool.add(rest)]), pool),
pool,
)
};
Some((h, neg_rest))
}
fn split_const(target: ExprId, c: ExprId, pool: &ExprPool) -> Option<ExprId> {
let g = sub(target, c, pool);
(!contains(g, c, pool)).then_some(g)
}
fn exp_with_constant(target: ExprId, k: ExprId, c: ExprId, pool: &ExprPool) -> ExprId {
match split_const(target, c, pool) {
Some(g) => simp(
pool.mul(vec![c, pool.func("exp", vec![div(g, k, pool)])]),
pool,
),
None => simp(pool.func("exp", vec![div(target, k, pool)]), pool),
}
}
fn unary_of_y(lhs: ExprId, y: ExprId, pool: &ExprPool) -> Option<(ExprId, String)> {
let is_f_of_y = |e: ExprId| match pool.get(e) {
ExprData::Func { name, args } if args.len() == 1 && unwrap1(args[0], pool) == y => {
Some(name)
}
_ => None,
};
if let Some(name) = is_f_of_y(lhs) {
return Some((pool.integer(1_i32), name));
}
let ExprData::Mul(args) = pool.get(lhs) else {
return None;
};
let mut found: Option<String> = None;
let mut k_factors = Vec::new();
for a in &args {
match is_f_of_y(*a) {
Some(name) if found.is_none() => found = Some(name),
Some(_) => return None, None => k_factors.push(*a),
}
}
let name = found?;
let k = if k_factors.is_empty() {
pool.integer(1_i32)
} else {
unwrap1(simp(pool.mul(k_factors), pool), pool)
};
Some((k, name))
}
fn unary_inverse(name: &str) -> Option<&'static str> {
Some(match name {
"atan" => "tan",
"atanh" => "tanh",
"asin" => "sin",
"acos" => "cos",
_ => return None,
})
}
fn invert_simple(
lhs: ExprId,
y: ExprId,
target: ExprId,
c: ExprId,
pool: &ExprPool,
) -> Option<ExprId> {
let lhs = unwrap1(simp(lhs, pool), pool);
if lhs == y {
return Some(target);
}
let dl = super::ddx(lhs, y, pool).ok()?;
if !contains(dl, y, pool) && !is_zero(dl, pool) {
let lin = simp(pool.mul(vec![dl, y]), pool);
let cst = sub(lhs, lin, pool);
if !contains(cst, y, pool) {
return Some(div(sub(target, cst, pool), dl, pool));
}
}
if let Some((k, name)) = unary_of_y(lhs, y, pool) {
if !contains(k, y, pool) && !is_zero(k, pool) {
if name == "log" {
return Some(exp_with_constant(target, k, c, pool));
}
if let Some(inverse) = unary_inverse(&name) {
let arg = div(target, k, pool);
return Some(simp(pool.func(inverse, vec![arg]), pool));
}
}
}
if let Some(y_expr) = invert_log_sum(lhs, y, target, c, pool) {
return Some(y_expr);
}
if let Some(y_expr) = invert_log_plus_linear(lhs, y, target, pool) {
return Some(y_expr);
}
None
}
fn log_term(term: ExprId, pool: &ExprPool) -> Option<(ExprId, ExprId)> {
match pool.get(term) {
ExprData::Func { name, args } if name == "log" && args.len() == 1 => {
Some((pool.integer(1_i32), args[0]))
}
ExprData::Mul(factors) => {
let mut logarg = None;
let mut coeff: Vec<ExprId> = Vec::new();
for f in &factors {
match pool.get(*f) {
ExprData::Func { name, args } if name == "log" && args.len() == 1 => {
if logarg.is_some() {
return None; }
logarg = Some(args[0]);
}
_ => coeff.push(*f),
}
}
let arg = logarg?;
let k = if coeff.is_empty() {
pool.integer(1_i32)
} else {
unwrap1(simp(pool.mul(coeff), pool), pool)
};
Some((k, arg))
}
_ => None,
}
}
fn invert_log_sum(
lhs: ExprId,
y: ExprId,
target: ExprId,
c: ExprId,
pool: &ExprPool,
) -> Option<ExprId> {
let terms: Vec<ExprId> = match pool.get(lhs) {
ExprData::Add(args) => args,
_ => vec![lhs],
};
let parsed: Vec<(ExprId, ExprId)> = terms
.iter()
.map(|&t| log_term(t, pool))
.collect::<Option<_>>()?;
let (k0, _) = *parsed.first()?;
if contains(k0, y, pool) || is_zero(k0, pool) {
return None;
}
let mut numer_args: Vec<ExprId> = Vec::new(); let mut denom_args: Vec<ExprId> = Vec::new(); for (k, arg) in parsed {
if contains(k, y, pool) {
return None;
}
let darg = super::ddx(arg, y, pool).ok()?;
if contains(darg, y, pool) || is_zero(darg, pool) {
return None;
}
match try_expr_f64(unwrap1(div(k, k0, pool), pool), pool) {
Some(r) if (r - 1.0).abs() < 1e-12 => numer_args.push(arg),
Some(r) if (r + 1.0).abs() < 1e-12 => denom_args.push(arg),
_ => return None,
}
}
let et = exp_with_constant(target, k0, c, pool);
let num = if numer_args.is_empty() {
pool.integer(1_i32)
} else {
unwrap1(simp(pool.mul(numer_args), pool), pool)
};
let den = if denom_args.is_empty() {
pool.integer(1_i32)
} else {
unwrap1(simp(pool.mul(denom_args), pool), pool)
};
let e_y = sub(num, simp(pool.mul(vec![et, den]), pool), pool);
solve_linear_in_y(e_y, y, pool)
}
fn solve_linear_in_y(expr: ExprId, y: ExprId, pool: &ExprPool) -> Option<ExprId> {
let b = super::ddx(expr, y, pool).ok()?;
if contains(b, y, pool) || is_zero(b, pool) {
return None;
}
let by = simp(pool.mul(vec![b, y]), pool);
let a = sub(expr, by, pool);
if contains(a, y, pool) {
return None;
}
Some(div(
simp(pool.mul(vec![pool.integer(-1_i32), a]), pool),
b,
pool,
))
}
fn invert_log_plus_linear(
lhs: ExprId,
y: ExprId,
target: ExprId,
pool: &ExprPool,
) -> Option<ExprId> {
let terms: Vec<ExprId> = match pool.get(lhs) {
ExprData::Add(args) => args,
_ => return None,
};
let mut k: Option<ExprId> = None; let mut m: Option<ExprId> = None; for t in terms {
if let Some((coeff, arg)) = log_term(t, pool) {
if unwrap1(arg, pool) != y || k.is_some() || contains(coeff, y, pool) {
return None;
}
k = Some(coeff);
continue;
}
let d = super::ddx(t, y, pool).ok()?;
if contains(d, y, pool) || is_zero(d, pool) || m.is_some() {
return None;
}
if !is_zero(sub(t, simp(pool.mul(vec![d, y]), pool), pool), pool) {
return None; }
m = Some(d);
}
let (k, m) = (k?, m?);
if is_zero(k, pool) || is_zero(m, pool) {
return None;
}
let m_over_k = div(m, k, pool);
let arg = simp(
pool.mul(vec![
m_over_k,
simp(pool.func("exp", vec![div(target, k, pool)]), pool),
]),
pool,
);
let w = pool.func("lambert_w", vec![arg]);
Some(simp(pool.mul(vec![div(k, m, pool), w]), pool))
}
fn linear_split(rhs: ExprId, _x: ExprId, y: ExprId, pool: &ExprPool) -> Option<(ExprId, ExprId)> {
let dr = super::ddx(rhs, y, pool).ok()?; if contains(dr, y, pool) {
return None; }
let ry = simp(pool.mul(vec![dr, y]), pool);
let q = sub(rhs, ry, pool); if contains(q, y, pool) {
return None;
}
let p = simp(pool.mul(vec![pool.integer(-1_i32), dr]), pool);
Some((p, q))
}
fn try_linear(input: &OdeInput, rhs: ExprId, gen: &mut ConstGen, pool: &ExprPool) -> Outcome {
let (x, y) = (input.x, input.y);
let Some((p, q)) = linear_split(rhs, x, y, pool) else {
return Outcome::NoMatch;
};
let int_p = match integrate_or_decline(p, x, pool) {
Ok(v) => v,
Err(e) => return quadrature_decline("linear", "∫ p(x) dx", p, e, pool),
};
let mu = super::exp_of(int_p, pool);
let muq = simp(pool.mul(vec![mu, q]), pool);
let int_muq = match integrate_or_decline(muq, x, pool) {
Ok(v) => v,
Err(e) => return quadrature_decline("linear", "∫ μ(x)·q(x) dx", muq, e, pool),
};
let c = gen.fresh(pool);
let numer = simp(pool.add(vec![int_muq, c]), pool);
let y_expr = div(numer, mu, pool);
finalize_explicit(input, y_expr, vec![c], "linear", pool)
}
fn try_bernoulli(input: &OdeInput, rhs: ExprId, gen: &mut ConstGen, pool: &ExprPool) -> Outcome {
let (x, y) = (input.x, input.y);
let terms: Vec<ExprId> = match pool.get(rhs) {
ExprData::Add(args) => args,
_ => vec![rhs],
};
let mut linear_coeff: Vec<ExprId> = Vec::new(); let mut bern_coeff: Vec<ExprId> = Vec::new(); let mut n_exp: Option<i64> = None;
for t in terms {
let Some((coeff, pw)) = monomial_in_y(t, y, pool) else {
return Outcome::NoMatch;
};
if contains(coeff, y, pool) {
return Outcome::NoMatch;
}
match pw {
1 => linear_coeff.push(coeff),
other => {
if other == 0 {
return Outcome::NoMatch;
}
match n_exp {
None => n_exp = Some(other),
Some(e) if e == other => {}
Some(_) => return Outcome::NoMatch, }
bern_coeff.push(coeff);
}
}
}
let Some(n) = n_exp else {
return Outcome::NoMatch;
};
if n == 1 {
return Outcome::NoMatch;
}
let p = simp(
pool.mul(vec![pool.integer(-1_i32), pool.add(linear_coeff)]),
pool,
); let q = simp(pool.add(bern_coeff), pool); if contains(q, y, pool) || contains(p, y, pool) {
return Outcome::NoMatch;
}
let one_minus_n = pool.integer((1 - n) as i32);
let pv = simp(pool.mul(vec![one_minus_n, p]), pool);
let qv = simp(pool.mul(vec![one_minus_n, q]), pool);
let int_pv = match integrate_or_decline(pv, x, pool) {
Ok(v) => v,
Err(e) => return quadrature_decline("bernoulli", "∫ (1−n)·p(x) dx", pv, e, pool),
};
let mu = super::exp_of(int_pv, pool);
let muq = simp(pool.mul(vec![mu, qv]), pool);
let int_muq = match integrate_or_decline(muq, x, pool) {
Ok(v) => v,
Err(e) => return quadrature_decline("bernoulli", "∫ μ(x)·(1−n)·q(x) dx", muq, e, pool),
};
let c = gen.fresh(pool);
let v = div(simp(pool.add(vec![int_muq, c]), pool), mu, pool);
let exp = pool.rational(1_i32, (1 - n) as i32);
let y_expr = simp(pool.pow(v, exp), pool);
finalize_explicit(input, y_expr, vec![c], "bernoulli", pool)
}
fn monomial_in_y(term: ExprId, y: ExprId, pool: &ExprPool) -> Option<(ExprId, i64)> {
let factors: Vec<ExprId> = match pool.get(term) {
ExprData::Mul(args) => args,
_ => vec![term],
};
let mut coeff: Vec<ExprId> = Vec::new();
let mut power: i64 = 0;
let mut found_y = false;
for f in factors {
if f == y {
power += 1;
found_y = true;
continue;
}
if let ExprData::Pow { base, exp } = pool.get(f) {
if base == y {
if let ExprData::Integer(k) = pool.get(exp) {
power += k.0.to_i64()?;
found_y = true;
continue;
}
return None;
}
}
if contains(f, y, pool) {
return None;
}
coeff.push(f);
}
let c = if coeff.is_empty() {
pool.integer(1_i32)
} else {
pool.mul(coeff)
};
Some((simp(c, pool), if found_y { power } else { 0 }))
}
fn samples_degree_zero(rhs: ExprId, x: ExprId, y: ExprId, pool: &ExprPool) -> bool {
use std::collections::HashMap;
const POINTS: [(f64, f64); 4] = [(1.3, 0.7), (0.9, 2.1), (2.4, -1.1), (0.6, 1.7)];
const SCALES: [f64; 2] = [2.0, 0.45];
let mut agreed = 0usize;
for (xv, yv) in POINTS {
let mut env: HashMap<ExprId, f64> = HashMap::new();
env.insert(x, xv);
env.insert(y, yv);
let Some(base) = super::verify::eval(rhs, &env, pool).filter(|v| v.is_finite()) else {
continue;
};
for t in SCALES {
let mut scaled: HashMap<ExprId, f64> = HashMap::new();
scaled.insert(x, t * xv);
scaled.insert(y, t * yv);
match super::verify::eval(rhs, &scaled, pool) {
Some(v) if v.is_finite() => {
if (v - base).abs() > 1e-9 * (1.0 + base.abs()) {
return false;
}
agreed += 1;
}
_ => continue,
}
}
}
agreed >= 4
}
fn try_homogeneous(input: &OdeInput, rhs: ExprId, gen: &mut ConstGen, pool: &ExprPool) -> Outcome {
let (x, y) = (input.x, input.y);
if !contains(rhs, x, pool) || !contains(rhs, y, pool) {
return Outcome::NoMatch;
}
let v = pool.symbol("__v_hom", Domain::Real);
let vx = simp(pool.mul(vec![v, x]), pool);
let mut g_vx = subs1(rhs, y, vx, pool);
if contains(g_vx, x, pool) {
g_vx = super::normalized(g_vx, pool);
}
if contains(g_vx, x, pool) {
if !samples_degree_zero(rhs, x, y, pool) {
return Outcome::NoMatch;
}
g_vx = simp(subs1(g_vx, x, pool.integer(1_i32), pool), pool);
if contains(g_vx, x, pool) {
return Outcome::NoMatch;
}
}
let gmv = sub(g_vx, v, pool);
if is_zero(gmv, pool) {
return Outcome::NoMatch; }
let inv = simp(pool.pow(gmv, pool.integer(-1_i32)), pool);
let lhs_int = match integrate_or_decline(inv, v, pool) {
Ok(val) => val, Err(e) => return quadrature_decline("homogeneous", "∫ dv/(G(v)−v)", inv, e, pool),
};
let c = gen.fresh(pool);
let logx = pool.func("log", vec![x]);
let target = simp(pool.add(vec![logx, c]), pool);
let relation_v = sub(lhs_int, target, pool);
if let Some(v_expr) = solve_relation_for_y(relation_v, x, v, c, pool) {
let y_expr = simp(pool.mul(vec![v_expr, x]), pool);
if !contains(y_expr, v, pool) {
if let Outcome::Solved(sol) =
finalize_explicit(input, y_expr, vec![c], "homogeneous", pool)
{
return Outcome::Solved(sol);
}
}
}
let relation = sub(subs1(lhs_int, v, div(y, x, pool), pool), target, pool);
if contains(relation, v, pool) {
return Outcome::NoMatch;
}
finalize_implicit(input, relation, vec![c], "homogeneous", pool)
}
fn try_exact(input: &OdeInput, _rhs: ExprId, gen: &mut ConstGen, pool: &ExprPool) -> Outcome {
let (x, y, yp) = (input.x, input.y, input.derivs[0]);
let n = match ddx(input.equation, yp, pool) {
Ok(v) => v,
Err(e) => return Outcome::Declined(e),
};
if contains(n, yp, pool) || is_zero(n, pool) {
return Outcome::NoMatch;
}
let m = sub(input.equation, simp(pool.mul(vec![n, yp]), pool), pool);
if contains(m, yp, pool) {
return Outcome::NoMatch;
}
let mut declines: Vec<DsolveError> = Vec::new();
for (m, n, method) in integrating_factor_candidates(m, n, x, y, pool) {
match exact_potential(m, n, x, y, pool) {
Ok(Some(f)) => {
let mark = gen.checkpoint();
let c = gen.fresh(pool);
let relation = sub(f, c, pool);
match explicit_or_implicit(input, relation, vec![c], method, pool) {
Outcome::Solved(sol) => return Outcome::Solved(sol),
Outcome::Declined(e) => {
gen.rollback(mark, pool);
declines.push(e);
}
Outcome::NoMatch => gen.rollback(mark, pool),
}
}
Ok(None) => {}
Err(e) => declines.push(e),
}
}
match declines.into_iter().next() {
Some(e) => Outcome::Declined(e),
None => Outcome::NoMatch,
}
}
fn ratio_normal(e: ExprId, x: ExprId, y: ExprId, pool: &ExprPool) -> ExprId {
let normal = super::normalized(e, pool);
match crate::poly::cancel::cancel(normal, vec![x, y], pool) {
Ok(v) => simp(v, pool),
Err(_) => normal,
}
}
fn integrating_factor_candidates(
m: ExprId,
n: ExprId,
x: ExprId,
y: ExprId,
pool: &ExprPool,
) -> Vec<(ExprId, ExprId, &'static str)> {
let (Ok(dmy), Ok(dnx)) = (ddx(m, y, pool), ddx(n, x, pool)) else {
return Vec::new();
};
let diff = sub(dmy, dnx, pool);
if is_zero(diff, pool) {
return vec![(m, n, "exact")];
}
let mut out = Vec::new();
let fx = ratio_normal(div(diff, n, pool), x, y, pool);
if !contains(fx, y, pool) {
if let Ok(i) = integrate_or_decline(fx, x, pool) {
let mu = super::exp_of(i, pool);
out.push((
simp(pool.mul(vec![mu, m]), pool),
simp(pool.mul(vec![mu, n]), pool),
"exact_integrating_factor_x",
));
}
}
let gy = ratio_normal(
div(
simp(pool.mul(vec![pool.integer(-1_i32), diff]), pool),
m,
pool,
),
x,
y,
pool,
);
if !contains(gy, x, pool) {
if let Ok(i) = integrate_or_decline(gy, y, pool) {
let mu = super::exp_of(i, pool);
out.push((
simp(pool.mul(vec![mu, m]), pool),
simp(pool.mul(vec![mu, n]), pool),
"exact_integrating_factor_y",
));
}
}
out
}
fn exact_potential(
m: ExprId,
n: ExprId,
x: ExprId,
y: ExprId,
pool: &ExprPool,
) -> Result<Option<ExprId>, DsolveError> {
let dmy = ddx(m, y, pool)?;
let dnx = ddx(n, x, pool)?;
if !is_zero(sub(dmy, dnx, pool), pool) {
return Ok(None);
}
let int_m = integrate_or_decline(m, x, pool).map_err(|e| {
DsolveError::quadrature_failed(format!("exact needs ∫ M dx of `{}`: {e}", pool.display(m)))
})?;
let dint_m_dy = ddx(int_m, y, pool)?;
let gy_prime = sub(n, dint_m_dy, pool); if contains(gy_prime, x, pool) {
return Ok(None);
}
let g_of_y = integrate_or_decline(gy_prime, y, pool).map_err(|e| {
DsolveError::quadrature_failed(format!(
"exact needs ∫ (N − ∂ₓ⁻¹) dy of `{}`: {e}",
pool.display(gy_prime)
))
})?;
Ok(Some(simp(pool.add(vec![int_m, g_of_y]), pool)))
}
fn solve_implicit_for_y(
f: ExprId,
_x: ExprId,
y: ExprId,
c: ExprId,
pool: &ExprPool,
) -> Option<ExprId> {
let b = super::ddx(f, y, pool).ok()?; if !contains(b, y, pool) {
if is_zero(b, pool) {
return None;
}
let by = simp(pool.mul(vec![b, y]), pool);
let a = sub(f, by, pool);
if contains(a, y, pool) {
return None;
}
return Some(div(sub(c, a, pool), b, pool));
}
let d2 = super::ddx(b, y, pool).ok()?; if contains(d2, y, pool) || is_zero(d2, pool) {
return None; }
let a_coeff = simp(pool.mul(vec![pool.rational(1_i32, 2_i32), d2]), pool); let b_coeff = sub(b, simp(pool.mul(vec![d2, y]), pool), pool);
if contains(b_coeff, y, pool) {
return None;
}
let ay2 = simp(
pool.mul(vec![a_coeff, pool.pow(y, pool.integer(2_i32))]),
pool,
);
let by = simp(pool.mul(vec![b_coeff, y]), pool);
let d_coeff = sub(sub(f, ay2, pool), by, pool); if contains(d_coeff, y, pool) {
return None;
}
let dc = sub(d_coeff, c, pool);
let disc = sub(
simp(pool.pow(b_coeff, pool.integer(2_i32)), pool),
simp(pool.mul(vec![pool.integer(4_i32), a_coeff, dc]), pool),
pool,
);
let sqrt_disc = simp(pool.pow(disc, pool.rational(1_i32, 2_i32)), pool);
let numer = simp(
pool.add(vec![
pool.mul(vec![pool.integer(-1_i32), b_coeff]),
sqrt_disc,
]),
pool,
);
let denom = simp(pool.mul(vec![pool.integer(2_i32), a_coeff]), pool);
Some(div(numer, denom, pool))
}
fn try_riccati(input: &OdeInput, rhs: ExprId, gen: &mut ConstGen, pool: &ExprPool) -> Outcome {
let (x, y) = (input.x, input.y);
let Some((_q0, q1, q2)) = quadratic_in_y(rhs, y, pool) else {
return Outcome::NoMatch;
};
if is_zero(q2, pool) {
return Outcome::NoMatch; }
let Some(yp_part) = find_poly_particular(rhs, x, y, pool) else {
return Outcome::Declined(DsolveError::no_particular_solution(format!(
"riccati `y' = {}` has no polynomial particular solution of degree ≤ 2; \
the general solution is in terms of solutions of the associated \
second-order linear equation, which dsolve does not emit",
pool.display(rhs)
)));
};
let two_q2_yp = simp(pool.mul(vec![pool.integer(2_i32), q2, yp_part]), pool);
let p = simp(pool.add(vec![q1, two_q2_yp]), pool); let qrhs = simp(pool.mul(vec![pool.integer(-1_i32), q2]), pool);
let int_p = match integrate_or_decline(p, x, pool) {
Ok(v) => v,
Err(e) => return quadrature_decline("riccati", "∫ (q₁ + 2q₂y_p) dx", p, e, pool),
};
let mu = super::exp_of(int_p, pool);
let muq = simp(pool.mul(vec![mu, qrhs]), pool);
let int_muq = match integrate_or_decline(muq, x, pool) {
Ok(v) => v,
Err(e) => return quadrature_decline("riccati", "∫ μ(x)·q₂ dx", muq, e, pool),
};
let c = gen.fresh(pool);
let v = div(simp(pool.add(vec![int_muq, c]), pool), mu, pool);
let inv_v = simp(pool.pow(v, pool.integer(-1_i32)), pool);
let y_expr = simp(pool.add(vec![yp_part, inv_v]), pool);
finalize_explicit(input, y_expr, vec![c], "riccati", pool)
}
fn quadratic_in_y(expr: ExprId, y: ExprId, pool: &ExprPool) -> Option<(ExprId, ExprId, ExprId)> {
let terms: Vec<ExprId> = match pool.get(expr) {
ExprData::Add(args) => args,
_ => vec![expr],
};
let mut q = [Vec::new(), Vec::new(), Vec::new()];
for t in terms {
let (coeff, pw) = monomial_in_y(t, y, pool)?;
if contains(coeff, y, pool) || !(0..=2).contains(&pw) {
return None;
}
q[pw as usize].push(coeff);
}
let mk = |v: Vec<ExprId>| {
if v.is_empty() {
pool.integer(0_i32)
} else {
simp(pool.add(v), pool)
}
};
Some((mk(q[0].clone()), mk(q[1].clone()), mk(q[2].clone())))
}
fn find_poly_particular(rhs: ExprId, x: ExprId, y: ExprId, pool: &ExprPool) -> Option<ExprId> {
let candidates = candidate_constants();
for degree in 0..=2usize {
for combo in coefficient_combinations(degree + 1, &candidates) {
let yp = build_poly(&combo, x, pool);
let ypp = super::ddx(yp, x, pool).ok()?;
let r = subs1(rhs, y, yp, pool);
let resid = sub(ypp, r, pool);
if is_zero(resid, pool) {
return Some(yp);
}
}
}
None
}
fn candidate_constants() -> Vec<(i64, i64)> {
vec![
(0, 1),
(1, 1),
(-1, 1),
(2, 1),
(-2, 1),
(1, 2),
(-1, 2),
(3, 1),
(-3, 1),
]
}
fn coefficient_combinations(n: usize, candidates: &[(i64, i64)]) -> Vec<Vec<(i64, i64)>> {
if n == 0 {
return vec![vec![]];
}
let rest = coefficient_combinations(n - 1, candidates);
let mut out = Vec::new();
for &c in candidates {
for r in &rest {
let mut v = vec![c];
v.extend_from_slice(r);
out.push(v);
}
}
out
}
fn build_poly(coeffs: &[(i64, i64)], x: ExprId, pool: &ExprPool) -> ExprId {
let mut terms = Vec::new();
for (k, &(num, den)) in coeffs.iter().enumerate() {
if num == 0 {
continue;
}
let c = pool.rational(num, den);
let term = if k == 0 {
c
} else {
let xk = pool.pow(x, pool.integer(k as i32));
pool.mul(vec![c, xk])
};
terms.push(term);
}
if terms.is_empty() {
pool.integer(0_i32)
} else {
simp(pool.add(terms), pool)
}
}
fn try_clairaut(input: &OdeInput, gen: &mut ConstGen, pool: &ExprPool) -> Outcome {
let (x, y, yp) = (input.x, input.y, input.derivs[0]);
let coeff_y = match ddx(input.equation, y, pool) {
Ok(v) => v,
Err(e) => return Outcome::Declined(e),
};
let cy = try_expr_f64(simp(coeff_y, pool), pool);
if cy != Some(1.0) && cy != Some(-1.0) {
return Outcome::NoMatch;
}
let sign = cy.unwrap();
let rest = sub(input.equation, simp(pool.mul(vec![coeff_y, y]), pool), pool);
if contains(rest, y, pool) {
return Outcome::NoMatch;
}
let y_solved = div(
simp(pool.mul(vec![pool.integer(-1_i32), rest]), pool),
simp(pool.integer(sign as i32), pool),
pool,
); let f_of_yp = sub(y_solved, simp(pool.mul(vec![x, yp]), pool), pool);
if contains(f_of_yp, x, pool) || contains(f_of_yp, y, pool) {
return Outcome::NoMatch;
}
if !contains(y_solved, yp, pool) {
return Outcome::NoMatch;
}
let c = gen.fresh(pool);
let f_of_c = subs1(f_of_yp, yp, c, pool);
let y_expr = simp(pool.add(vec![pool.mul(vec![c, x]), f_of_c]), pool);
finalize_explicit(input, y_expr, vec![c], "clairaut", pool)
}