use super::{ddx, simp, subs1, DsolveError, OdeInput};
use crate::kernel::{ExprData, ExprId, ExprPool};
use std::collections::HashMap;
use std::fmt;
const ZERO_TOL: f64 = 1e-6;
const MIN_AGREEING_SAMPLES: usize = 6;
#[cfg(test)]
thread_local! {
pub(crate) static GATE_TALLY: std::cell::Cell<(usize, usize)> =
const { std::cell::Cell::new((0, 0)) };
}
#[cfg(test)]
pub(crate) fn take_gate_tally() -> (usize, usize) {
GATE_TALLY.with(|t| t.replace((0, 0)))
}
#[cfg(test)]
pub(crate) fn certifies_symbolically(input: &OdeInput, y_of_x: ExprId, pool: &ExprPool) -> bool {
match build_residual(input, y_of_x, pool) {
Ok((residual, _)) => {
is_symbolic_zero(residual, pool)
|| is_symbolic_zero(super::simp_plain(residual, pool), pool)
}
Err(_) => false,
}
}
pub(crate) fn residual_is_zero(
input: &OdeInput,
y_of_x: ExprId,
constants: &[ExprId],
pool: &ExprPool,
) -> Result<(), DsolveError> {
let outcome = verify_inner(input, y_of_x, constants, pool);
#[cfg(test)]
GATE_TALLY.with(|t| {
let (offered, refused) = t.get();
t.set((offered + 1, refused + usize::from(outcome.is_err())));
});
outcome
}
fn verify_inner(
input: &OdeInput,
y_of_x: ExprId,
constants: &[ExprId],
pool: &ExprPool,
) -> Result<(), DsolveError> {
let (residual, candidate_derivs) = build_residual(input, y_of_x, pool)?;
if is_symbolic_zero(residual, pool) || is_symbolic_zero(super::simp_plain(residual, pool), pool)
{
return Ok(());
}
let report = numeric_report(input, residual, &candidate_derivs, constants, pool);
if report.certifies() {
return Ok(());
}
Err(DsolveError::VerificationFailed(format!(
"residual did not reduce to zero ({report}): {}",
pool.display(residual)
)))
}
fn build_residual(
input: &OdeInput,
y_of_x: ExprId,
pool: &ExprPool,
) -> Result<(ExprId, Vec<ExprId>), DsolveError> {
let mut candidate_derivs = Vec::with_capacity(input.derivs.len() + 1);
candidate_derivs.push(y_of_x);
let mut cur = y_of_x;
for _ in &input.derivs {
cur = ddx(cur, input.x, pool)?;
candidate_derivs.push(cur);
}
let mut residual = subs1(input.equation, input.y, y_of_x, pool);
for (k, &dsym) in input.derivs.iter().enumerate() {
residual = subs1(residual, dsym, candidate_derivs[k + 1], pool);
}
Ok((simp(residual, pool), candidate_derivs))
}
fn is_symbolic_zero(expr: ExprId, pool: &ExprPool) -> bool {
matches!(pool.get(expr), ExprData::Integer(n) if n.0 == 0)
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
struct NumericReport {
agree: usize,
disagree: usize,
blowup_at_regular_point: usize,
skipped_singular_ode: usize,
skipped_unknown_construct: usize,
unevaluable: bool,
}
impl NumericReport {
fn certifies(&self) -> bool {
!self.unevaluable
&& self.disagree == 0
&& self.blowup_at_regular_point == 0
&& self.agree >= MIN_AGREEING_SAMPLES
}
}
impl fmt::Display for NumericReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.unevaluable {
return write!(
f,
"residual contains a construct the sampler cannot evaluate"
);
}
write!(
f,
"samples: {} agreeing, {} disagreeing, {} candidate blow-ups at a regular point, \
{} skipped (ODE singular), {} skipped (unknown construct)",
self.agree,
self.disagree,
self.blowup_at_regular_point,
self.skipped_singular_ode,
self.skipped_unknown_construct
)
}
}
fn numeric_report(
input: &OdeInput,
residual: ExprId,
candidate_derivs: &[ExprId],
constants: &[ExprId],
pool: &ExprPool,
) -> NumericReport {
let const_sets: [&[f64]; 3] = [
&[5.7, 4.3, 6.4, 5.1, 4.9],
&[8.5, 7.8, 6.6, 9.2, 7.1],
&[12.3, 10.0, 11.7, 10.5, 9.4],
];
let x_samples = [0.11, 0.27, 0.43, 0.61, 0.79];
let mut report = NumericReport::default();
for cs in const_sets {
let mut env: HashMap<ExprId, f64> = HashMap::new();
for (i, &c) in constants.iter().enumerate() {
env.insert(c, cs[i % cs.len()]);
}
for &xv in &x_samples {
env.insert(input.x, xv);
match eval(residual, &env, pool) {
Some(v) if v.is_finite() => record(&mut report, v),
Some(_) => classify_nonfinite(input, candidate_derivs, &env, xv, pool, &mut report),
None => {
report.unevaluable = true;
return report;
}
}
}
}
report
}
fn record(report: &mut NumericReport, v: f64) {
if v.abs() < ZERO_TOL {
report.agree += 1;
} else {
report.disagree += 1;
}
}
fn classify_nonfinite(
input: &OdeInput,
candidate_derivs: &[ExprId],
env: &HashMap<ExprId, f64>,
xv: f64,
pool: &ExprPool,
report: &mut NumericReport,
) {
if !ode_is_regular_at(input, xv, pool) {
report.skipped_singular_ode += 1;
return;
}
let mut vals = Vec::with_capacity(candidate_derivs.len());
for &d in candidate_derivs {
match eval(d, env, pool) {
Some(v) if v.is_finite() => vals.push(v),
Some(_) => {
report.blowup_at_regular_point += 1;
return;
}
None => {
report.skipped_unknown_construct += 1;
return;
}
}
}
let mut eq_env: HashMap<ExprId, f64> = HashMap::with_capacity(vals.len() + 1);
eq_env.insert(input.x, xv);
eq_env.insert(input.y, vals[0]);
for (k, &dsym) in input.derivs.iter().enumerate() {
eq_env.insert(dsym, vals[k + 1]);
}
match eval(input.equation, &eq_env, pool) {
Some(v) if v.is_finite() => record(report, v),
_ => report.skipped_singular_ode += 1,
}
}
fn ode_is_regular_at(input: &OdeInput, xv: f64, pool: &ExprPool) -> bool {
const PROBES: [f64; 4] = [1.0, 2.5, 0.5, -1.5];
PROBES.iter().any(|&p| {
let mut env: HashMap<ExprId, f64> = HashMap::with_capacity(input.derivs.len() + 2);
env.insert(input.x, xv);
env.insert(input.y, p);
for (k, &dsym) in input.derivs.iter().enumerate() {
env.insert(dsym, p + 0.25 * (k as f64 + 1.0));
}
matches!(eval(input.equation, &env, pool), Some(v) if v.is_finite())
})
}
pub(crate) fn eval(expr: ExprId, env: &HashMap<ExprId, f64>, pool: &ExprPool) -> Option<f64> {
match pool.get(expr) {
ExprData::Integer(n) => Some(n.0.to_f64()),
ExprData::Rational(r) => {
let (num, den) = r.0.clone().into_numer_denom();
Some(num.to_f64() / den.to_f64())
}
ExprData::Float(f) => Some(f.inner.to_f64()),
ExprData::Symbol { .. } => env.get(&expr).copied(),
ExprData::Add(args) => {
let mut s = 0.0;
for a in args {
s += eval(a, env, pool)?;
}
Some(s)
}
ExprData::Mul(args) => {
let mut p = 1.0;
for a in args {
p *= eval(a, env, pool)?;
}
Some(p)
}
ExprData::Pow { base, exp } => {
let b = eval(base, env, pool)?;
let e = eval(exp, env, pool)?;
Some(b.powf(e))
}
ExprData::Func { name, args } => {
let v: Vec<f64> = args
.iter()
.map(|&a| eval(a, env, pool))
.collect::<Option<_>>()?;
eval_func(&name, &v)
}
_ => None,
}
}
fn eval_func(name: &str, a: &[f64]) -> Option<f64> {
let x = *a.first()?;
Some(match name {
"sin" => x.sin(),
"cos" => x.cos(),
"tan" => x.tan(),
"exp" => x.exp(),
"log" | "ln" => x.ln(),
"sqrt" => x.sqrt(),
"sinh" => x.sinh(),
"cosh" => x.cosh(),
"tanh" => x.tanh(),
"asin" => x.asin(),
"acos" => x.acos(),
"atan" => x.atan(),
"abs" => x.abs(),
_ => return None,
})
}
#[cfg(test)]
mod tests;