use super::*;
use crate::kernel::Domain;
struct Fixture {
pool: ExprPool,
x: ExprId,
y: ExprId,
c1: ExprId,
}
impl Fixture {
fn new() -> Self {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let y = pool.symbol("y", Domain::Real);
let c1 = pool.symbol("C1", Domain::Real);
Fixture { pool, x, y, c1 }
}
fn neg(&self, e: ExprId) -> ExprId {
self.pool.mul(vec![self.pool.integer(-1_i32), e])
}
fn x_minus(&self, n: i32, d: i32) -> ExprId {
let r = self.pool.rational(n, d);
self.pool.add(vec![self.x, self.neg(r)])
}
fn sqrt_half_minus_x(&self) -> ExprId {
let half = self.pool.rational(1_i32, 2_i32);
let arg = self.pool.add(vec![half, self.neg(self.x)]);
self.pool.func("sqrt", vec![arg])
}
fn one_that_does_not_simplify(&self) -> ExprId {
let ex = self.pool.func("exp", vec![self.x]);
let emx = self.pool.func("exp", vec![self.neg(self.x)]);
self.pool.mul(vec![ex, emx])
}
}
#[test]
fn wrong_candidate_blowing_up_at_a_regular_point_is_rejected() {
let f = Fixture::new();
let (input, yp) = OdeInput::first_order(f.x, f.y, &f.pool);
let input = input.with_equation(yp);
let root = f.pool.func("sqrt", vec![f.x_minus(-1, -2)]);
let sq = |n: i32, d: i32| f.pool.pow(f.x_minus(n, d), f.pool.integer(2_i32));
let wrong = simp(
f.pool
.add(vec![f.c1, f.pool.mul(vec![root, sq(61, 100), sq(79, 100)])]),
&f.pool,
);
let (residual, derivs) = build_residual(&input, wrong, &f.pool).expect("residual builds");
let report = numeric_report(&input, residual, &derivs, &[f.c1], &f.pool);
assert_eq!(report.agree, 6, "report: {report}");
assert_eq!(report.disagree, 0, "report: {report}");
assert_eq!(report.blowup_at_regular_point, 9, "report: {report}");
assert_eq!(report.skipped_singular_ode, 0, "report: {report}");
assert!(!report.certifies(), "report: {report}");
residual_is_zero(&input, wrong, &[f.c1], &f.pool)
.expect_err("a candidate that blows up where the ODE is regular must not certify");
}
#[test]
fn plain_finite_disagreement_is_still_rejected() {
let f = Fixture::new();
let (input, yp) = OdeInput::first_order(f.x, f.y, &f.pool);
let two_y = f.pool.mul(vec![
f.pool.integer(-2_i32),
f.y,
f.one_that_does_not_simplify(),
]);
let input = input.with_equation(f.pool.add(vec![yp, two_y]));
let cand = simp(
f.pool.mul(vec![f.c1, f.pool.func("exp", vec![f.x])]),
&f.pool,
);
let (residual, derivs) = build_residual(&input, cand, &f.pool).expect("residual builds");
let report = numeric_report(&input, residual, &derivs, &[f.c1], &f.pool);
assert_eq!(report.agree, 0, "report: {report}");
assert_eq!(report.disagree, 15, "report: {report}");
assert!(!report.certifies(), "report: {report}");
}
#[test]
fn correct_solution_of_a_singular_ode_still_verifies() {
let f = Fixture::new();
let (input, yp) = OdeInput::first_order(f.x, f.y, &f.pool);
let root = f.sqrt_half_minus_x();
let eq = f.pool.add(vec![
yp,
f.neg(f.y),
f.neg(f.pool.mul(vec![root, f.one_that_does_not_simplify()])),
root,
]);
let input = input.with_equation(eq);
let cand = simp(
f.pool.mul(vec![f.c1, f.pool.func("exp", vec![f.x])]),
&f.pool,
);
assert!(ode_is_regular_at(&input, 0.43, &f.pool));
assert!(!ode_is_regular_at(&input, 0.61, &f.pool));
let (residual, derivs) = build_residual(&input, cand, &f.pool).expect("residual builds");
let report = numeric_report(&input, residual, &derivs, &[f.c1], &f.pool);
assert_eq!(report.agree, 9, "report: {report}");
assert_eq!(report.disagree, 0, "report: {report}");
assert_eq!(report.blowup_at_regular_point, 0, "report: {report}");
assert_eq!(report.skipped_singular_ode, 6, "report: {report}");
assert!(report.certifies(), "report: {report}");
residual_is_zero(&input, cand, &[f.c1], &f.pool)
.expect("a correct solution of a singular ODE must still verify");
}
#[test]
fn candidate_blowup_where_the_ode_is_also_singular_stays_a_skip() {
let f = Fixture::new();
let (input, yp) = OdeInput::first_order(f.x, f.y, &f.pool);
let root = f.sqrt_half_minus_x();
let inv_root = f.pool.pow(root, f.pool.integer(-1_i32));
let rhs = f
.pool
.mul(vec![f.y, f.one_that_does_not_simplify(), inv_root]);
let input = input.with_equation(f.pool.add(vec![yp, f.neg(rhs)]));
let expo = f.pool.mul(vec![f.pool.integer(-2_i32), root]);
let cand = simp(
f.pool.mul(vec![f.c1, f.pool.func("exp", vec![expo])]),
&f.pool,
);
assert!(!ode_is_regular_at(&input, 0.61, &f.pool));
let (residual, derivs) = build_residual(&input, cand, &f.pool).expect("residual builds");
let report = numeric_report(&input, residual, &derivs, &[f.c1], &f.pool);
assert_eq!(report.disagree, 0, "report: {report}");
assert_eq!(report.blowup_at_regular_point, 0, "report: {report}");
assert_eq!(report.skipped_singular_ode, 6, "report: {report}");
assert_eq!(report.agree, 9, "report: {report}");
residual_is_zero(&input, cand, &[f.c1], &f.pool)
.expect("correct solution, ODE singular past the branch point");
}
#[test]
fn regularity_probe_ignores_an_unlucky_state() {
let f = Fixture::new();
let (input, yp) = OdeInput::first_order(f.x, f.y, &f.pool);
let eq = f.pool.add(vec![yp, f.neg(f.pool.func("log", vec![f.y]))]);
let input = input.with_equation(eq);
assert!(ode_is_regular_at(&input, 0.43, &f.pool));
let (input2, yp2) = OdeInput::first_order(f.x, f.y, &f.pool);
let half = f.pool.rational(1_i32, 2_i32);
let arg = f.pool.add(vec![half, f.neg(f.x)]);
let eq2 = f.pool.add(vec![yp2, f.neg(f.pool.func("log", vec![arg]))]);
let input2 = input2.with_equation(eq2);
assert!(ode_is_regular_at(&input2, 0.43, &f.pool));
assert!(!ode_is_regular_at(&input2, 0.61, &f.pool));
}
#[test]
fn regularity_probe_declines_on_an_unknown_construct() {
let f = Fixture::new();
let (input, yp) = OdeInput::first_order(f.x, f.y, &f.pool);
let ei = f.pool.func("Ei", vec![f.x]);
let input = input.with_equation(f.pool.add(vec![yp, f.neg(ei)]));
assert!(!ode_is_regular_at(&input, 0.43, &f.pool));
}