use super::{contains, ddx, simp, subs1, DsolveError, OdeInput};
use crate::eval::symbols::{collect_free_symbols, is_pi, walk_symbols};
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 params = free_parameters(input, &[residual], constants, pool);
if !params.is_empty() || mentions_imaginary_unit(residual, pool) {
let report =
parametric_report(input, residual, &candidate_derivs, constants, ¶ms, pool);
if report.certifies() {
return Ok(());
}
if report.has_counterevidence() {
return Err(DsolveError::VerificationFailed(format!(
"residual did not reduce to zero over the parameters {} ({report}): {}",
param_names(¶ms, pool),
pool.display(residual)
)));
}
let real = numeric_report(input, residual, &candidate_derivs, constants, ¶ms, pool);
if real.certifies() {
return Ok(());
}
return Err(DsolveError::VerificationFailed(format!(
"residual did not reduce to zero over the parameters {} (over ℂ: {report}; \
over ℝ: {real}): {}",
param_names(¶ms, pool),
pool.display(residual)
)));
}
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 free_parameters(
input: &OdeInput,
exprs: &[ExprId],
constants: &[ExprId],
pool: &ExprPool,
) -> Vec<ExprId> {
let mut bound: Vec<ExprId> = vec![input.x, input.y];
bound.extend_from_slice(&input.derivs);
bound.extend_from_slice(constants);
let mut out: Vec<ExprId> = Vec::new();
for &e in exprs {
collect_free_symbols(e, pool, &mut out);
}
out.retain(|s| !bound.contains(s));
out.sort_by_key(|&s| pool.display(s).to_string());
out.dedup();
out
}
fn param_names(params: &[ExprId], pool: &ExprPool) -> String {
if params.is_empty() {
return "(none: a complex-valued residual)".to_string();
}
params
.iter()
.map(|&p| pool.display(p).to_string())
.collect::<Vec<_>>()
.join(", ")
}
fn mentions_imaginary_unit(expr: ExprId, pool: &ExprPool) -> bool {
let mut found = false;
walk_symbols(expr, pool, &mut |s, pool| {
found |= pool.is_imaginary_unit(s);
});
found
}
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
)
}
}
pub(crate) const 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],
];
pub(crate) const X_SAMPLES: [f64; 5] = [0.11, 0.27, 0.43, 0.61, 0.79];
fn numeric_report(
input: &OdeInput,
residual: ExprId,
candidate_derivs: &[ExprId],
constants: &[ExprId],
params: &[ExprId],
pool: &ExprPool,
) -> NumericReport {
let mut report = NumericReport::default();
let rows = if params.is_empty() {
1
} else {
PARAM_SETS.len()
};
for (set, cs) in (0..rows).flat_map(|s| CONST_SETS.iter().map(move |cs| (s, cs))) {
let param_env = parameter_env(params, set);
let mut env: HashMap<ExprId, f64> = param_env.clone();
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,
¶m_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;
}
}
#[allow(clippy::too_many_arguments)]
fn classify_nonfinite(
input: &OdeInput,
candidate_derivs: &[ExprId],
env: &HashMap<ExprId, f64>,
params: &HashMap<ExprId, f64>,
xv: f64,
pool: &ExprPool,
report: &mut NumericReport,
) {
if !ode_is_regular_at(input, xv, params, 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> = params.clone();
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,
params: &HashMap<ExprId, 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> = params.clone();
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()
.or_else(|| is_pi(expr, pool).then_some(std::f64::consts::PI)),
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(),
"lambert_w" => return crate::special::lambert_w0(x),
_ => return None,
})
}
pub(crate) const PARAM_SETS: [&[f64]; 6] = [
&[1.7, 0.6, 2.3, 1.1, 0.4],
&[0.37, 1.9, 0.83, 2.7, 1.3],
&[2.9, 0.45, 1.15, 0.71, 3.3],
&[-1.3, 0.9, -2.1, 1.6, -0.55],
&[0.62, -1.45, 2.05, -0.78, 1.1],
&[-2.4, -0.83, -1.35, -3.1, -0.6],
];
fn parameter_env(params: &[ExprId], set: usize) -> HashMap<ExprId, f64> {
let values = PARAM_SETS[set % PARAM_SETS.len()];
params
.iter()
.enumerate()
.map(|(i, &p)| (p, values[i % values.len()]))
.collect()
}
const PARAM_MIN_AGREEING: usize = 12;
const PARAM_REL_TOL: f64 = 1e-7;
#[derive(Default, Debug, Clone, Copy)]
struct ParametricReport {
inner: NumericReport,
sets_agreeing: usize,
sets_unresolved: usize,
}
impl ParametricReport {
fn certifies(&self) -> bool {
!self.inner.unevaluable
&& self.inner.disagree == 0
&& self.inner.blowup_at_regular_point == 0
&& self.inner.agree >= PARAM_MIN_AGREEING
&& self.sets_agreeing >= 2
}
fn has_counterevidence(&self) -> bool {
self.inner.disagree > 0 || self.inner.blowup_at_regular_point > 0
}
}
impl fmt::Display for ParametricReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}; {} parameter sets agreeing, {} with no resolved sample",
self.inner, self.sets_agreeing, self.sets_unresolved
)
}
}
fn parametric_report(
input: &OdeInput,
residual: ExprId,
candidate_derivs: &[ExprId],
constants: &[ExprId],
params: &[ExprId],
pool: &ExprPool,
) -> ParametricReport {
parametric_report_over(
&PARAM_SETS,
input,
residual,
candidate_derivs,
constants,
params,
pool,
)
}
fn parametric_report_over(
rows: &[&[f64]],
input: &OdeInput,
residual: ExprId,
candidate_derivs: &[ExprId],
constants: &[ExprId],
params: &[ExprId],
pool: &ExprPool,
) -> ParametricReport {
let mut report = ParametricReport::default();
for &ps in rows {
let mut env: HashMap<ExprId, C64> = HashMap::new();
for (i, &p) in params.iter().enumerate() {
env.insert(p, C64::real(ps[i % ps.len()]));
}
let before = report.inner.agree;
for cs in CONST_SETS {
for (i, &c) in constants.iter().enumerate() {
env.insert(c, C64::real(cs[i % cs.len()]));
}
for &xv in &X_SAMPLES {
env.insert(input.x, C64::real(xv));
match eval_complex(residual, &env, pool) {
Some(v) if v.is_finite() => {
record_parametric(&mut report.inner, v, candidate_derivs, &env, pool)
}
Some(_) => classify_nonfinite_complex(
input,
candidate_derivs,
&env,
xv,
pool,
&mut report.inner,
),
None => {
report.inner.unevaluable = true;
return report;
}
}
}
}
if report.inner.agree > before {
report.sets_agreeing += 1;
} else if report.inner.disagree == 0 && report.inner.blowup_at_regular_point == 0 {
report.sets_unresolved += 1;
}
}
report
}
fn record_parametric(
report: &mut NumericReport,
v: C64,
candidate_derivs: &[ExprId],
env: &HashMap<ExprId, C64>,
pool: &ExprPool,
) {
let mut scale = 1.0_f64;
for &d in candidate_derivs {
if let Some(dv) = eval_complex(d, env, pool) {
if dv.is_finite() {
scale = scale.max(dv.abs());
}
}
}
if v.abs() <= PARAM_REL_TOL * scale {
report.agree += 1;
} else {
report.disagree += 1;
}
}
fn classify_nonfinite_complex(
input: &OdeInput,
candidate_derivs: &[ExprId],
env: &HashMap<ExprId, C64>,
xv: f64,
pool: &ExprPool,
report: &mut NumericReport,
) {
if !ode_is_regular_at_complex(input, env, xv, pool) {
report.skipped_singular_ode += 1;
return;
}
let mut vals = Vec::with_capacity(candidate_derivs.len());
for &d in candidate_derivs {
match eval_complex(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 = env.clone();
eq_env.insert(input.x, C64::real(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_complex(input.equation, &eq_env, pool) {
Some(v) if v.is_finite() => record_parametric(report, v, candidate_derivs, env, pool),
_ => report.skipped_singular_ode += 1,
}
}
fn ode_is_regular_at_complex(
input: &OdeInput,
env: &HashMap<ExprId, C64>,
xv: f64,
pool: &ExprPool,
) -> bool {
const PROBES: [f64; 4] = [1.0, 2.5, 0.5, -1.5];
PROBES.iter().any(|&p| {
let mut e = env.clone();
e.insert(input.x, C64::real(xv));
e.insert(input.y, C64::real(p));
for (k, &dsym) in input.derivs.iter().enumerate() {
e.insert(dsym, C64::real(p + 0.25 * (k as f64 + 1.0)));
}
matches!(eval_complex(input.equation, &e, pool), Some(v) if v.is_finite())
})
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct C64 {
re: f64,
im: f64,
}
impl C64 {
pub(crate) fn real(re: f64) -> Self {
C64 { re, im: 0.0 }
}
pub(crate) fn is_finite(self) -> bool {
self.re.is_finite() && self.im.is_finite()
}
pub(crate) fn abs(self) -> f64 {
self.re.hypot(self.im)
}
pub(crate) fn add(self, o: C64) -> C64 {
C64 {
re: self.re + o.re,
im: self.im + o.im,
}
}
pub(crate) fn sub(self, o: C64) -> C64 {
C64 {
re: self.re - o.re,
im: self.im - o.im,
}
}
pub(crate) fn mul(self, o: C64) -> C64 {
C64 {
re: self.re * o.re - self.im * o.im,
im: self.re * o.im + self.im * o.re,
}
}
pub(crate) fn div(self, o: C64) -> C64 {
let d = o.re * o.re + o.im * o.im;
C64 {
re: (self.re * o.re + self.im * o.im) / d,
im: (self.im * o.re - self.re * o.im) / d,
}
}
fn neg(self) -> C64 {
C64 {
re: -self.re,
im: -self.im,
}
}
fn exp(self) -> C64 {
let m = self.re.exp();
C64 {
re: m * self.im.cos(),
im: m * self.im.sin(),
}
}
fn ln(self) -> C64 {
C64 {
re: self.abs().ln(),
im: self.im.atan2(self.re),
}
}
fn powi(self, n: i64) -> C64 {
if n < 0 {
return C64::real(1.0).div(self.powi(-n));
}
let mut acc = C64::real(1.0);
for _ in 0..n {
acc = acc.mul(self);
}
acc
}
fn pow(self, w: C64) -> C64 {
if w.im == 0.0 && w.re.fract() == 0.0 && w.re.abs() <= 64.0 {
return self.powi(w.re as i64);
}
if self.re == 0.0 && self.im == 0.0 {
return if w.re > 0.0 {
C64::real(0.0)
} else {
C64::real(f64::INFINITY)
};
}
w.mul(self.ln()).exp()
}
fn sin(self) -> C64 {
C64 {
re: self.re.sin() * self.im.cosh(),
im: self.re.cos() * self.im.sinh(),
}
}
fn cos(self) -> C64 {
C64 {
re: self.re.cos() * self.im.cosh(),
im: -self.re.sin() * self.im.sinh(),
}
}
fn sinh(self) -> C64 {
C64 {
re: self.re.sinh() * self.im.cos(),
im: self.re.cosh() * self.im.sin(),
}
}
fn cosh(self) -> C64 {
C64 {
re: self.re.cosh() * self.im.cos(),
im: self.re.sinh() * self.im.sin(),
}
}
}
const I: C64 = C64 { re: 0.0, im: 1.0 };
pub(crate) fn eval_complex(
expr: ExprId,
env: &HashMap<ExprId, C64>,
pool: &ExprPool,
) -> Option<C64> {
match pool.get(expr) {
ExprData::Integer(n) => Some(C64::real(n.0.to_f64())),
ExprData::Rational(r) => {
let (num, den) = r.0.clone().into_numer_denom();
Some(C64::real(num.to_f64() / den.to_f64()))
}
ExprData::Float(f) => Some(C64::real(f.inner.to_f64())),
ExprData::Symbol { .. } => env.get(&expr).copied().or_else(|| {
if is_pi(expr, pool) {
Some(C64::real(std::f64::consts::PI))
} else if pool.is_imaginary_unit(expr) {
Some(I)
} else {
None
}
}),
ExprData::Add(args) => {
let mut s = C64::real(0.0);
for a in args {
s = s.add(eval_complex(a, env, pool)?);
}
Some(s)
}
ExprData::Mul(args) => {
let mut p = C64::real(1.0);
for a in args {
p = p.mul(eval_complex(a, env, pool)?);
}
Some(p)
}
ExprData::Pow { base, exp } => {
let b = eval_complex(base, env, pool)?;
let e = eval_complex(exp, env, pool)?;
Some(b.pow(e))
}
ExprData::Func { name, args } => {
let v: Vec<C64> = args
.iter()
.map(|&a| eval_complex(a, env, pool))
.collect::<Option<_>>()?;
eval_func_complex(&name, &v)
}
_ => None,
}
}
fn eval_func_complex(name: &str, a: &[C64]) -> Option<C64> {
let z = *a.first()?;
let one = C64::real(1.0);
let half = C64::real(0.5);
Some(match name {
"sin" => z.sin(),
"cos" => z.cos(),
"tan" => z.sin().div(z.cos()),
"exp" => z.exp(),
"log" | "ln" => z.ln(),
"sqrt" => z.pow(half),
"sinh" => z.sinh(),
"cosh" => z.cosh(),
"tanh" => z.sinh().div(z.cosh()),
"atan" => I.div(C64::real(2.0)).mul(
one.add(I.mul(z).neg())
.ln()
.add(one.add(I.mul(z)).ln().neg()),
),
"asin" => I
.neg()
.mul(I.mul(z).add(one.add(z.mul(z).neg()).pow(half)).ln()),
"acos" => C64::real(std::f64::consts::FRAC_PI_2)
.add(I.mul(I.mul(z).add(one.add(z.mul(z).neg()).pow(half)).ln())),
"abs" => C64::real(z.abs()),
_ => return None,
})
}
#[cfg(test)]
pub(crate) fn solution_is_verified(
input: &OdeInput,
sol: &super::DsolveBranch,
pool: &ExprPool,
) -> Result<(), DsolveError> {
match sol.form {
super::SolutionForm::Explicit(y) => residual_is_zero(input, y, &sol.constants, pool),
super::SolutionForm::Implicit(g) => {
implicit_relation_is_zero(input, g, &sol.constants, pool)
}
}
}
pub(crate) fn implicit_relation_is_zero(
input: &OdeInput,
relation: ExprId,
constants: &[ExprId],
pool: &ExprPool,
) -> Result<(), DsolveError> {
let outcome = implicit_inner(input, relation, 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 implicit_inner(
input: &OdeInput,
relation: ExprId,
constants: &[ExprId],
pool: &ExprPool,
) -> Result<(), DsolveError> {
if input.derivs.len() != 1 {
return Err(DsolveError::VerificationFailed(
"implicit solutions are only verified for first-order equations".to_string(),
));
}
let yp = input.derivs[0];
if !contains(relation, input.y, pool) {
return Err(DsolveError::VerificationFailed(
"implicit relation does not depend on y".to_string(),
));
}
if !constants.iter().any(|&c| contains(relation, c, pool)) {
return Err(DsolveError::VerificationFailed(
"implicit relation carries no integration constant, so it is not a \
general solution"
.to_string(),
));
}
let gx = ddx(relation, input.x, pool)?;
let gy = ddx(relation, input.y, pool)?;
if super::is_zero(gy, pool) {
return Err(DsolveError::VerificationFailed(
"∂G/∂y is identically zero: the relation defines no slope field".to_string(),
));
}
for &c in constants {
if contains(gx, c, pool) || contains(gy, c, pool) {
return Err(DsolveError::VerificationFailed(
"the slope field −Gx/Gy still mentions an integration constant, so \
the relation cannot be checked off its own level sets"
.to_string(),
));
}
}
let slope = super::div(
simp(pool.mul(vec![pool.integer(-1_i32), gx]), pool),
gy,
pool,
);
let residual = simp(subs1(input.equation, yp, slope, pool), pool);
if is_symbolic_zero(residual, pool) || is_symbolic_zero(super::simp_plain(residual, pool), pool)
{
return Ok(());
}
let report = implicit_numeric_report(input, residual, slope, gy, constants, pool);
if report.certifies() {
return Ok(());
}
Err(DsolveError::VerificationFailed(format!(
"implicit relation did not reduce to zero ({report}): {}",
pool.display(residual)
)))
}
const Y_SAMPLES: [f64; 5] = [0.37, 0.83, 1.4, 2.1, 3.3];
fn implicit_numeric_report(
input: &OdeInput,
residual: ExprId,
slope: ExprId,
gy: ExprId,
constants: &[ExprId],
pool: &ExprPool,
) -> NumericReport {
let sources = [input.equation, residual, slope];
let free = free_parameters(input, &sources, constants, pool);
let mut report = NumericReport::default();
for set in 0..PARAM_SETS.len() {
let params = parameter_env(&free, set);
let mut env = params.clone();
for &xv in &X_SAMPLES {
env.insert(input.x, xv);
for &yv in &Y_SAMPLES {
env.insert(input.y, yv);
match eval(residual, &env, pool) {
Some(v) if v.is_finite() => {
record_scaled(&mut report, v, input, &env, slope, pool)
}
Some(_) => classify_implicit_nonfinite(
input,
slope,
gy,
&env,
¶ms,
xv,
yv,
pool,
&mut report,
),
None => {
report.unevaluable = true;
return report;
}
}
}
}
}
report
}
fn record_scaled(
report: &mut NumericReport,
v: f64,
input: &OdeInput,
env: &HashMap<ExprId, f64>,
slope: ExprId,
pool: &ExprPool,
) {
let scale = equation_term_scale(input, env, slope, pool).unwrap_or(0.0);
if v.abs() <= ZERO_TOL * (1.0 + scale) {
report.agree += 1;
} else {
report.disagree += 1;
}
}
fn equation_term_scale(
input: &OdeInput,
env: &HashMap<ExprId, f64>,
slope: ExprId,
pool: &ExprPool,
) -> Option<f64> {
let eq = subs1(input.equation, input.derivs[0], slope, pool);
let terms: Vec<ExprId> = match pool.get(eq) {
ExprData::Add(args) => args,
_ => vec![eq],
};
let mut scale: f64 = 0.0;
for t in terms {
let v = eval(t, env, pool)?;
if !v.is_finite() {
return None;
}
scale = scale.max(v.abs());
}
Some(scale)
}
#[allow(clippy::too_many_arguments)]
fn classify_implicit_nonfinite(
input: &OdeInput,
slope: ExprId,
gy: ExprId,
env: &HashMap<ExprId, f64>,
params: &HashMap<ExprId, f64>,
xv: f64,
yv: f64,
pool: &ExprPool,
report: &mut NumericReport,
) {
if !ode_is_regular_at(input, xv, params, pool) {
report.skipped_singular_ode += 1;
return;
}
match (eval(gy, env, pool), eval(slope, env, pool)) {
(Some(g), Some(s)) if g.is_finite() && g.abs() > 1e-12 && s.is_finite() => {
let mut eq_env: HashMap<ExprId, f64> = params.clone();
eq_env.insert(input.x, xv);
eq_env.insert(input.y, yv);
eq_env.insert(input.derivs[0], s);
match eval(input.equation, &eq_env, pool) {
Some(v) if v.is_finite() => record_scaled(report, v, input, env, slope, pool),
_ => report.skipped_singular_ode += 1,
}
}
(None, _) | (_, None) => report.skipped_unknown_construct += 1,
_ => report.skipped_singular_ode += 1,
}
}
#[cfg(test)]
mod tests;