use crate::deriv::SideCondition;
use crate::diff::diff;
use crate::integrate::engine::integrate;
use crate::kernel::eval_const::try_expr_f64;
use crate::kernel::{Domain, ExprData, ExprId, ExprPool};
use crate::simplify::assumptions::AssumptionContext;
use crate::simplify::engine::{distribute_recip, simplify, simplify_expanded};
use std::collections::HashMap;
use std::fmt;
mod constant_coeff;
#[cfg(test)]
mod corpus;
mod first_order;
pub mod system;
mod variation;
mod verify;
#[cfg(test)]
pub(crate) use verify::solution_is_verified;
pub(crate) use verify::{implicit_relation_is_zero, residual_is_zero};
#[derive(Clone, Debug)]
pub struct OdeInput {
pub x: ExprId,
pub y: ExprId,
pub derivs: Vec<ExprId>,
pub equation: ExprId,
}
impl OdeInput {
fn deriv_symbol(y: ExprId, k: usize, pool: &ExprPool) -> ExprId {
let base = pool.with(y, |d| match d {
ExprData::Symbol { name, .. } => name.clone(),
_ => "y".to_string(),
});
let primes = "'".repeat(k);
pool.symbol(format!("{base}{primes}"), Domain::Real)
}
pub fn first_order(x: ExprId, y: ExprId, pool: &ExprPool) -> (Self, ExprId) {
let yp = Self::deriv_symbol(y, 1, pool);
(
OdeInput {
x,
y,
derivs: vec![yp],
equation: pool.integer(0_i32),
},
yp,
)
}
pub fn second_order(x: ExprId, y: ExprId, pool: &ExprPool) -> (Self, ExprId, ExprId) {
let yp = Self::deriv_symbol(y, 1, pool);
let ypp = Self::deriv_symbol(y, 2, pool);
(
OdeInput {
x,
y,
derivs: vec![yp, ypp],
equation: pool.integer(0_i32),
},
yp,
ypp,
)
}
pub fn higher_order(
x: ExprId,
y: ExprId,
order: usize,
pool: &ExprPool,
) -> (Self, Vec<ExprId>) {
assert!(order >= 1, "ODE order must be ≥ 1");
let derivs: Vec<ExprId> = (1..=order)
.map(|k| Self::deriv_symbol(y, k, pool))
.collect();
(
OdeInput {
x,
y,
derivs: derivs.clone(),
equation: pool.integer(0_i32),
},
derivs,
)
}
pub fn with_equation(mut self, equation: ExprId) -> Self {
self.equation = equation;
self
}
pub fn order(&self) -> usize {
self.derivs.len()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SolutionForm {
Explicit(ExprId),
Implicit(ExprId),
}
#[derive(Clone, Debug)]
pub struct DsolveSolution {
pub y_of_x: ExprId,
pub constants: Vec<ExprId>,
pub method: &'static str,
}
#[derive(Clone, Debug)]
pub struct DsolveBranch {
pub form: SolutionForm,
pub constants: Vec<ExprId>,
pub method: &'static str,
}
impl DsolveBranch {
pub fn explicit(y_of_x: ExprId, constants: Vec<ExprId>, method: &'static str) -> Self {
DsolveBranch {
form: SolutionForm::Explicit(y_of_x),
constants,
method,
}
}
pub fn implicit(relation: ExprId, constants: Vec<ExprId>, method: &'static str) -> Self {
DsolveBranch {
form: SolutionForm::Implicit(relation),
constants,
method,
}
}
pub fn into_solution(self) -> Option<DsolveSolution> {
match self.form {
SolutionForm::Explicit(y_of_x) => Some(DsolveSolution {
y_of_x,
constants: self.constants,
method: self.method,
}),
SolutionForm::Implicit(_) => None,
}
}
pub fn y_of_x(&self) -> Option<ExprId> {
match self.form {
SolutionForm::Explicit(e) => Some(e),
SolutionForm::Implicit(_) => None,
}
}
pub fn implicit_relation(&self) -> Option<ExprId> {
match self.form {
SolutionForm::Implicit(e) => Some(e),
SolutionForm::Explicit(_) => None,
}
}
pub fn is_explicit(&self) -> bool {
matches!(self.form, SolutionForm::Explicit(_))
}
pub fn render(&self, pool: &ExprPool) -> String {
match self.form {
SolutionForm::Explicit(e) => format!("y = {}", pool.display(e)),
SolutionForm::Implicit(e) => format!("0 = {}", pool.display(e)),
}
}
}
#[derive(Clone, Debug)]
pub struct DsolveResult {
pub solutions: Vec<DsolveSolution>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DsolveError {
Unsupported(String),
VerificationFailed(String),
DiffError(String),
}
impl DsolveError {
const QUADRATURE_TAG: &'static str = "required quadrature did not close: ";
const NO_PARTICULAR_TAG: &'static str = "no particular solution available: ";
pub fn quadrature_failed(detail: impl fmt::Display) -> Self {
DsolveError::Unsupported(format!("{}{detail}", Self::QUADRATURE_TAG))
}
pub fn no_particular_solution(detail: impl fmt::Display) -> Self {
DsolveError::Unsupported(format!("{}{detail}", Self::NO_PARTICULAR_TAG))
}
pub fn is_quadrature_failure(&self) -> bool {
matches!(self, DsolveError::Unsupported(m) if m.starts_with(Self::QUADRATURE_TAG))
}
pub fn is_missing_particular_solution(&self) -> bool {
matches!(self, DsolveError::Unsupported(m) if m.starts_with(Self::NO_PARTICULAR_TAG))
}
}
impl fmt::Display for DsolveError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DsolveError::Unsupported(m)
if self.is_quadrature_failure() || self.is_missing_particular_solution() =>
{
write!(f, "dsolve: {m}")
}
DsolveError::Unsupported(m) => write!(f, "dsolve: unsupported ODE: {m}"),
DsolveError::VerificationFailed(m) => {
write!(f, "dsolve: candidate failed verification: {m}")
}
DsolveError::DiffError(m) => write!(f, "dsolve: differentiation error: {m}"),
}
}
}
impl std::error::Error for DsolveError {}
impl crate::errors::AlkahestError for DsolveError {
fn code(&self) -> &'static str {
match self {
_ if self.is_quadrature_failure() => "E-ODE-013",
_ if self.is_missing_particular_solution() => "E-ODE-014",
DsolveError::Unsupported(_) => "E-ODE-010",
DsolveError::VerificationFailed(_) => "E-ODE-011",
DsolveError::DiffError(_) => "E-ODE-012",
}
}
fn remediation(&self) -> Option<&'static str> {
match self {
_ if self.is_quadrature_failure() => Some(
"the ODE was classified, but the integral the method needs is not \
elementary for this integrator; the message names the class and the \
integrand that failed",
),
_ if self.is_missing_particular_solution() => Some(
"supply a particular solution, or expect a closed form outside the \
elementary/special-function vocabulary this solver emits",
),
DsolveError::Unsupported(_) => Some(
"the ODE is outside the implemented classical classes, or a required \
integral is non-elementary; check the equation form",
),
DsolveError::VerificationFailed(_) => Some(
"the solver found a candidate that did not verify by substitution; \
this is reported rather than returned as a (possibly wrong) answer",
),
DsolveError::DiffError(_) => {
Some("ensure the equation only contains differentiable functions")
}
}
}
}
pub fn dsolve(input: &OdeInput, pool: &ExprPool) -> Result<DsolveResult, DsolveError> {
let report = dsolve_with(input, &AssumptionContext::new(), pool)?;
let implicit: Vec<String> = report
.branches
.iter()
.filter(|b| !b.is_explicit())
.map(|b| b.render(pool))
.collect();
let solutions: Vec<DsolveSolution> = report
.branches
.into_iter()
.filter_map(DsolveBranch::into_solution)
.collect();
if solutions.is_empty() {
return Err(DsolveError::Unsupported(format!(
"the general solution is an implicit relation, not an explicit y(x): {}; \
use dsolve_with to obtain it",
implicit.join("; ")
)));
}
Ok(DsolveResult { solutions })
}
pub fn dsolve_with(
input: &OdeInput,
assumptions: &AssumptionContext,
pool: &ExprPool,
) -> Result<DsolveReport, DsolveError> {
let mut gen = ConstGen::new(input, pool);
let mut ctx = SolveCtx {
assumptions,
conds: Conditions::default(),
};
let branches = match input.order() {
1 => first_order::solve(input, &mut gen, pool),
2 => constant_coeff::solve_second_order(input, &mut gen, &mut ctx, pool),
n if n >= 3 => constant_coeff::solve_higher_order(input, n, &mut gen, &mut ctx, pool),
_ => Err(DsolveError::Unsupported("order 0 ODE".to_string())),
}?;
Ok(DsolveReport {
branches,
side_conditions: ctx.conds.side,
notes: ctx.conds.notes,
})
}
#[derive(Clone, Debug)]
pub struct DsolveReport {
pub branches: Vec<DsolveBranch>,
pub side_conditions: Vec<SideCondition>,
pub notes: Vec<String>,
}
pub(crate) struct SolveCtx<'a> {
pub(crate) assumptions: &'a AssumptionContext,
pub(crate) conds: Conditions,
}
#[derive(Default, Debug, Clone)]
pub(crate) struct Conditions {
pub(crate) side: Vec<SideCondition>,
pub(crate) notes: Vec<String>,
}
impl Conditions {
pub(crate) fn require_nonzero(&mut self, e: ExprId) {
let c = SideCondition::NonZero(e);
if !self.side.contains(&c) {
self.side.push(c);
}
}
pub(crate) fn note(&mut self, s: String) {
if !self.notes.contains(&s) {
self.notes.push(s);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AssumedSign {
Positive,
Negative,
NonZero,
Unknown,
}
impl SolveCtx<'_> {
pub(crate) fn assumed_sign(&self, e: ExprId, pool: &ExprPool) -> AssumedSign {
let target = expand_powers(e, pool);
let mut verdict = AssumedSign::Unknown;
for fact in self.assumptions.facts() {
let (u, strict_sign) = match fact {
SideCondition::Positive(id) => (*id, true),
SideCondition::NonZero(id) => (*id, false),
SideCondition::InDomain(..) => continue,
};
let Some(ratio) = constant_ratio(target, expand_powers(u, pool), pool) else {
continue;
};
if !strict_sign {
if verdict == AssumedSign::Unknown {
verdict = AssumedSign::NonZero;
}
continue;
}
return if ratio > 0.0 {
AssumedSign::Positive
} else {
AssumedSign::Negative
};
}
verdict
}
pub(crate) fn asserts_nonzero(&self, e: ExprId, pool: &ExprPool) -> bool {
!matches!(self.assumed_sign(e, pool), AssumedSign::Unknown)
}
}
pub(crate) fn is_settled_nonzero(e: ExprId, pool: &ExprPool) -> bool {
crate::matrix::zero_test::settled_nonzero(e, pool)
}
fn constant_ratio(a: ExprId, b: ExprId, pool: &ExprPool) -> Option<f64> {
if let Some(v) = try_expr_f64(div(a, b, pool), pool) {
if v != 0.0 && v.is_finite() {
return Some(v);
}
}
const NUMS: [i64; 8] = [1, -1, 2, -2, 3, -3, 4, -4];
const DENS: [i64; 4] = [1, 2, 3, 4];
for num in NUMS {
for den in DENS {
let q = if den == 1 {
pool.integer(num)
} else {
pool.rational(num, den)
};
let diff = expand_powers(
pool.add(vec![a, pool.mul(vec![pool.integer(-1_i32), q, b])]),
pool,
);
if crate::matrix::zero_test::zero_status(pool, diff).is_proven_zero() {
return Some(num as f64 / den as f64);
}
}
}
None
}
pub(crate) struct ConstGen {
next: usize,
used: std::collections::HashSet<String>,
issued: Vec<String>,
}
impl ConstGen {
fn new(input: &OdeInput, pool: &ExprPool) -> Self {
let mut used = std::collections::HashSet::new();
collect_symbol_names(input.equation, pool, &mut used);
ConstGen {
next: 1,
used,
issued: Vec::new(),
}
}
pub(crate) fn with_used(used: std::collections::HashSet<String>) -> Self {
ConstGen {
next: 1,
used,
issued: Vec::new(),
}
}
pub(crate) fn fresh(&mut self, pool: &ExprPool) -> ExprId {
loop {
let name = format!("C{}", self.next);
self.next += 1;
if !self.used.contains(&name) {
self.used.insert(name.clone());
self.issued.push(name.clone());
return pool.symbol(name, Domain::Real);
}
}
}
pub(crate) fn checkpoint(&self) -> ConstMark {
ConstMark {
next: self.next,
issued: self.issued.len(),
}
}
pub(crate) fn rollback(&mut self, mark: ConstMark, _pool: &ExprPool) {
for name in self.issued.drain(mark.issued..) {
self.used.remove(&name);
}
self.next = mark.next;
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct ConstMark {
next: usize,
issued: usize,
}
fn collect_symbol_names(
expr: ExprId,
pool: &ExprPool,
out: &mut std::collections::HashSet<String>,
) {
pool.with(expr, |d| match d {
ExprData::Symbol { name, .. } => {
out.insert(name.clone());
}
ExprData::Add(args) | ExprData::Mul(args) | ExprData::Func { args, .. } => {
for &a in args {
collect_symbol_names(a, pool, out);
}
}
ExprData::Pow { base, exp } => {
collect_symbol_names(*base, pool, out);
collect_symbol_names(*exp, pool, out);
}
_ => {}
});
}
pub(crate) fn simp(expr: ExprId, pool: &ExprPool) -> ExprId {
simplify_expanded(expr, pool).value
}
pub(crate) fn simp_plain(expr: ExprId, pool: &ExprPool) -> ExprId {
simplify(expr, pool).value
}
pub(crate) fn ddx(expr: ExprId, var: ExprId, pool: &ExprPool) -> Result<ExprId, DsolveError> {
diff(expr, var, pool)
.map(|d| d.value)
.map_err(|e| DsolveError::DiffError(e.to_string()))
}
pub(crate) fn integrate_or_decline(
expr: ExprId,
var: ExprId,
pool: &ExprPool,
) -> Result<ExprId, DsolveError> {
integrate_first_of(&[expr], var, pool)
}
pub(crate) fn integrate_first_of(
exprs: &[ExprId],
var: ExprId,
pool: &ExprPool,
) -> Result<ExprId, DsolveError> {
let mut last: Option<String> = None;
let mut tried: Vec<ExprId> = Vec::new();
for &expr in exprs {
if tried.contains(&expr) {
continue;
}
tried.push(expr);
match integrate(expr, var, pool) {
Ok(d) => return Ok(simp(d.value, pool)),
Err(e) => last = last.or(Some(e.to_string())),
}
}
for &expr in exprs {
for cand in integrand_spellings(expr, pool) {
if tried.contains(&cand) {
continue;
}
tried.push(cand);
match integrate(cand, var, pool) {
Ok(d) => return Ok(simp(d.value, pool)),
Err(e) => last = last.or(Some(e.to_string())),
}
}
}
#[cfg(test)]
if std::env::var_os("ALKAHEST_DSOLVE_TRACE").is_some() {
for cand in tried {
eprintln!(
"INT_DECLINE\td/d{}\t{}",
pool.display(var),
pool.display(cand)
);
}
}
Err(DsolveError::Unsupported(format!(
"required integral did not close: {}",
last.unwrap_or_else(|| "no candidate form".to_string())
)))
}
pub(crate) fn normalized(expr: ExprId, pool: &ExprPool) -> ExprId {
let cands = integrand_spellings(expr, pool);
*cands.last().unwrap_or(&expr)
}
fn integrand_spellings(expr: ExprId, pool: &ExprPool) -> Vec<ExprId> {
let mut out = vec![expr];
let push = |e: ExprId, out: &mut Vec<ExprId>| {
if !out.contains(&e) {
out.push(e);
}
};
let dr = simp(distribute_recip(expr, pool), pool);
push(dr, &mut out);
for base in [expr, dr] {
let le = simp(
crate::simplify::engine::simplify_log_exp(base, pool, &[]).value,
pool,
);
push(le, &mut out);
if mentions_sin_cos(le, pool) {
let tn = simp(
crate::simplify::engine::simplify_trig_normal_form(le, pool).value,
pool,
);
push(tn, &mut out);
}
}
out
}
pub(crate) fn expand_powers(expr: ExprId, pool: &ExprPool) -> ExprId {
crate::simplify::engine::expand_powers(expr, pool)
}
fn mentions_sin_cos(expr: ExprId, pool: &ExprPool) -> bool {
pool.with(expr, |d| match d {
ExprData::Func { name, args } => {
name == "sin" || name == "cos" || args.iter().any(|&a| mentions_sin_cos(a, pool))
}
ExprData::Add(args) | ExprData::Mul(args) => {
args.iter().any(|&a| mentions_sin_cos(a, pool))
}
ExprData::Pow { base, exp } => {
mentions_sin_cos(*base, pool) || mentions_sin_cos(*exp, pool)
}
_ => false,
})
}
pub(crate) fn exp_of(arg: ExprId, pool: &ExprPool) -> ExprId {
let terms: Vec<ExprId> = match pool.get(simp(arg, pool)) {
ExprData::Add(args) => args,
_ => vec![simp(arg, pool)],
};
let mut factors: Vec<ExprId> = Vec::new();
let mut rest: Vec<ExprId> = Vec::new();
for t in terms {
match log_summand(t, pool) {
Some((u, c)) => factors.push(pool.pow(u, c)),
None => rest.push(t),
}
}
if factors.is_empty() {
return simp(pool.func("exp", vec![arg]), pool);
}
if !rest.is_empty() {
let r = pool.add(rest);
factors.push(pool.func("exp", vec![r]));
}
if factors.len() == 1 {
return simp(factors[0], pool);
}
simp(pool.mul(factors), pool)
}
fn log_summand(term: ExprId, pool: &ExprPool) -> Option<(ExprId, ExprId)> {
if let ExprData::Func { name, args } = pool.get(term) {
if name == "log" && args.len() == 1 {
return Some((args[0], pool.integer(1_i32)));
}
}
let ExprData::Mul(args) = pool.get(term) else {
return None;
};
let mut inner = None;
let mut coeff: Vec<ExprId> = Vec::new();
for a in args {
match pool.get(a) {
ExprData::Func { name, args: fargs } if name == "log" && fargs.len() == 1 => {
if inner.is_some() {
return None; }
inner = Some(fargs[0]);
}
ExprData::Integer(_) | ExprData::Rational(_) => coeff.push(a),
_ => return None, }
}
let u = inner?;
let c = match coeff.len() {
0 => pool.integer(1_i32),
1 => simp(coeff[0], pool),
_ => simp(pool.mul(coeff), pool),
};
Some((u, c))
}
pub(crate) fn contains(expr: ExprId, needle: ExprId, pool: &ExprPool) -> bool {
if expr == needle {
return true;
}
pool.with(expr, |d| match d {
ExprData::Add(args) | ExprData::Mul(args) | ExprData::Func { args, .. } => {
args.iter().any(|&a| contains(a, needle, pool))
}
ExprData::Pow { base, exp } => {
contains(*base, needle, pool) || contains(*exp, needle, pool)
}
_ => false,
})
}
pub(crate) fn sub(a: ExprId, b: ExprId, pool: &ExprPool) -> ExprId {
let neg_b = pool.mul(vec![pool.integer(-1_i32), b]);
simp(pool.add(vec![a, neg_b]), pool)
}
pub(crate) fn div(a: ExprId, b: ExprId, pool: &ExprPool) -> ExprId {
let inv_b = pool.pow(b, pool.integer(-1_i32));
simp(pool.mul(vec![a, inv_b]), pool)
}
pub(crate) fn subs1(expr: ExprId, from: ExprId, to: ExprId, pool: &ExprPool) -> ExprId {
let mut m = HashMap::new();
m.insert(from, to);
simp(crate::kernel::subs::subs(expr, &m, pool), pool)
}
pub(crate) fn is_zero(expr: ExprId, pool: &ExprPool) -> bool {
let s = simp(expr, pool);
matches!(pool.get(s), ExprData::Integer(n) if n.0 == 0)
|| matches!(try_expr_f64(s, pool), Some(v) if v == 0.0)
}
#[cfg(test)]
mod tests;