#![cfg(not(target_arch = "wasm32"))]
use proptest::prelude::*;
use ries_rs::eval::{evaluate, EvalError};
use ries_rs::expr::Expression;
fn approx_eq(a: f64, b: f64, tolerance: f64) -> bool {
if a.is_nan() && b.is_nan() {
return true;
}
if a.is_infinite() && b.is_infinite() {
return a.is_sign_positive() == b.is_sign_positive();
}
(a - b).abs() < tolerance
}
fn x_strategy() -> impl Strategy<Value = f64> {
(-10.0..10.0).prop_filter("avoid near-zero", |x: &f64| x.abs() > 0.1)
}
fn h_strategy() -> impl Strategy<Value = f64> {
1e-8..1e-5
}
proptest! {
#[test]
fn derivative_approximates_central_difference(
x in x_strategy(),
h in h_strategy()
) {
let expr = Expression::parse("xs").unwrap();
let result = evaluate(&expr, x).unwrap();
let f_plus = evaluate(&expr, x + h).unwrap().value;
let f_minus = evaluate(&expr, x - h).unwrap().value;
let numerical = (f_plus - f_minus) / (2.0 * h);
prop_assert!(
approx_eq(result.derivative, numerical, 1e-4),
"Derivative {} != numerical {} for x^2 at x={}",
result.derivative, numerical, x
);
}
#[test]
fn derivative_x_cubed(x in x_strategy(), h in h_strategy()) {
let expr = Expression::parse("xsx*").unwrap(); let result = evaluate(&expr, x).unwrap();
let f_plus = evaluate(&expr, x + h).unwrap().value;
let f_minus = evaluate(&expr, x - h).unwrap().value;
let numerical = (f_plus - f_minus) / (2.0 * h);
let expected = 3.0 * x * x;
prop_assert!(
approx_eq(result.derivative, expected, 1e-6),
"d(x^3)/dx = {} but got {} at x={}",
expected, result.derivative, x
);
prop_assert!(
approx_eq(result.derivative, numerical, 1e-3),
"Derivative {} != numerical {} for x^3 at x={}",
result.derivative, numerical, x
);
}
#[test]
fn derivative_sqrt(x in 0.5f64..10.0, h in h_strategy()) {
let expr = Expression::parse("xq").unwrap(); let result = evaluate(&expr, x).unwrap();
let f_plus = evaluate(&expr, x + h).unwrap().value;
let f_minus = evaluate(&expr, x - h).unwrap().value;
let numerical = (f_plus - f_minus) / (2.0 * h);
let expected = 1.0 / (2.0 * x.sqrt());
prop_assert!(
approx_eq(result.derivative, expected, 1e-6),
"d(sqrt)/dx = {} but got {} at x={}",
expected, result.derivative, x
);
prop_assert!(
approx_eq(result.derivative, numerical, 1e-4),
"Derivative {} != numerical {} for sqrt(x) at x={}",
result.derivative, numerical, x
);
}
#[test]
fn derivative_exp(x in -5.0f64..5.0, h in h_strategy()) {
let expr = Expression::parse("xE").unwrap(); let result = evaluate(&expr, x).unwrap();
let f_plus = evaluate(&expr, x + h).unwrap().value;
let f_minus = evaluate(&expr, x - h).unwrap().value;
let numerical = (f_plus - f_minus) / (2.0 * h);
let expected = x.exp();
prop_assert!(
approx_eq(result.derivative, expected, 1e-6),
"d(e^x)/dx = {} but got {} at x={}",
expected, result.derivative, x
);
prop_assert!(
approx_eq(result.derivative, numerical, 1e-4),
"Derivative {} != numerical {} for e^x at x={}",
result.derivative, numerical, x
);
}
#[test]
fn derivative_ln(x in 0.5f64..10.0, h in h_strategy()) {
let expr = Expression::parse("xl").unwrap(); let result = evaluate(&expr, x).unwrap();
let f_plus = evaluate(&expr, x + h).unwrap().value;
let f_minus = evaluate(&expr, x - h).unwrap().value;
let numerical = (f_plus - f_minus) / (2.0 * h);
let expected = 1.0 / x;
prop_assert!(
approx_eq(result.derivative, expected, 1e-6),
"d(ln)/dx = {} but got {} at x={}",
expected, result.derivative, x
);
prop_assert!(
approx_eq(result.derivative, numerical, 1e-4),
"Derivative {} != numerical {} for ln(x) at x={}",
result.derivative, numerical, x
);
}
}
proptest! {
#[test]
fn derivative_linearity(x in x_strategy()) {
let expr = Expression::parse("xsx+").unwrap();
let result = evaluate(&expr, x).unwrap();
let expected = 2.0 * x + 1.0;
prop_assert!(
approx_eq(result.derivative, expected, 1e-6),
"d(x^2+x)/dx = {} but got {} at x={}",
expected, result.derivative, x
);
}
#[test]
fn derivative_product_rule(x in x_strategy()) {
let expr = Expression::parse("xsx*").unwrap();
let result = evaluate(&expr, x).unwrap();
let expected_value = x * x * x;
prop_assert!(
approx_eq(result.value, expected_value, 1e-10),
"x^3 = {} but got {} at x={}",
expected_value, result.value, x
);
let expected_deriv = 3.0 * x * x;
prop_assert!(
approx_eq(result.derivative, expected_deriv, 1e-6),
"d(x^3)/dx = {} but got {} at x={}",
expected_deriv, result.derivative, x
);
}
#[test]
fn derivative_quotient_rule(x in x_strategy()) {
let expr = Expression::parse("xsx/").unwrap();
let result = evaluate(&expr, x).unwrap();
prop_assert!(
approx_eq(result.value, x, 1e-10),
"x = {} but got {} at x={}",
x, result.value, x
);
prop_assert!(
approx_eq(result.derivative, 1.0, 1e-6),
"d(x)/dx = 1 but got {} at x={}",
result.derivative, x
);
}
#[test]
fn derivative_chain_rule_exp_square(x in -2.0f64..2.0) {
let expr = Expression::parse("xsE").unwrap();
let result = evaluate(&expr, x).unwrap();
let expected_deriv = (x * x).exp() * 2.0 * x;
prop_assert!(
approx_eq(result.derivative, expected_deriv, 1e-4),
"d(e^(x^2))/dx = {} but got {} at x={}",
expected_deriv, result.derivative, x
);
}
}
proptest! {
#[test]
fn parse_postfix_roundtrip(expr_str in "[1-9xqsp][1-9xqsp+*-]*") {
if let Some(expr) = Expression::parse(&expr_str) {
let postfix = expr.to_postfix();
if let Some(reparsed) = Expression::parse(&postfix) {
prop_assert_eq!(expr.to_postfix(), reparsed.to_postfix());
}
}
}
}
proptest! {
#[test]
fn sqrt_negative_domain_error(x in -10.0f64..-0.01) {
let expr = Expression::parse("xq").unwrap(); let result = evaluate(&expr, x);
prop_assert!(
matches!(result, Err(EvalError::SqrtDomain)),
"sqrt({}) should return SqrtDomain error, got {:?}",
x, result
);
}
#[test]
fn ln_nonpositive_domain_error(x in -10.0f64..0.0) {
let expr = Expression::parse("xl").unwrap(); let result = evaluate(&expr, x);
prop_assert!(
matches!(result, Err(EvalError::LogDomain)),
"ln({}) should return LogDomain error, got {:?}",
x, result
);
}
#[test]
fn division_by_zero_error(x in x_strategy()) {
let expr = Expression::parse("1xx-/").unwrap();
let result = evaluate(&expr, x);
prop_assert!(
matches!(result, Err(EvalError::DivisionByZero)),
"1/(x-x) should return DivisionByZero error at x={}, got {:?}",
x, result
);
}
}
proptest! {
#[test]
fn arithmetic_correctness(x in x_strategy()) {
let expr = Expression::parse("x1+").unwrap();
let result = evaluate(&expr, x).unwrap();
prop_assert!(approx_eq(result.value, x + 1.0, 1e-10));
let expr = Expression::parse("x1-").unwrap();
let result = evaluate(&expr, x).unwrap();
prop_assert!(approx_eq(result.value, x - 1.0, 1e-10));
let expr = Expression::parse("x2*").unwrap();
let result = evaluate(&expr, x).unwrap();
prop_assert!(approx_eq(result.value, x * 2.0, 1e-10));
let expr = Expression::parse("x2/").unwrap();
let result = evaluate(&expr, x).unwrap();
prop_assert!(approx_eq(result.value, x / 2.0, 1e-10));
}
#[test]
fn constant_values(
_pi_val in proptest::bool::ANY,
e_val in proptest::bool::ANY,
phi_val in proptest::bool::ANY
) {
{
let expr = Expression::parse("p").unwrap();
let result = evaluate(&expr, 0.0).unwrap();
prop_assert!(approx_eq(result.value, std::f64::consts::PI, 1e-10));
prop_assert!(approx_eq(result.derivative, 0.0, 1e-10));
}
if e_val {
let expr = Expression::parse("e").unwrap();
let result = evaluate(&expr, 0.0).unwrap();
prop_assert!(approx_eq(result.value, std::f64::consts::E, 1e-10));
prop_assert!(approx_eq(result.derivative, 0.0, 1e-10));
}
if phi_val {
let expr = Expression::parse("f").unwrap();
let result = evaluate(&expr, 0.0).unwrap();
let phi = 1.618_033_988_749_895;
prop_assert!(approx_eq(result.value, phi, 1e-10));
prop_assert!(approx_eq(result.derivative, 0.0, 1e-10));
}
}
}
proptest! {
#[test]
fn derivative_sinpi(x in -2.0f64..2.0) {
let expr = Expression::parse("xS").unwrap(); let result = evaluate(&expr, x).unwrap();
let pi = std::f64::consts::PI;
let expected_deriv = pi * (pi * x).cos();
prop_assert!(
approx_eq(result.derivative, expected_deriv, 1e-6),
"d(sin(πx))/dx = {} but got {} at x={}",
expected_deriv, result.derivative, x
);
}
#[test]
fn derivative_cospi(x in -2.0f64..2.0) {
let expr = Expression::parse("xC").unwrap(); let result = evaluate(&expr, x).unwrap();
let pi = std::f64::consts::PI;
let expected_deriv = -pi * (pi * x).sin();
prop_assert!(
approx_eq(result.derivative, expected_deriv, 1e-6),
"d(cos(πx))/dx = {} but got {} at x={}",
expected_deriv, result.derivative, x
);
}
#[test]
fn derivative_x_to_x(x in 0.5f64..3.0) {
let expr = Expression::parse("xx^").unwrap(); let result = evaluate(&expr, x).unwrap();
let expected_value = x.powf(x);
let expected_deriv = expected_value * (x.ln() + 1.0);
prop_assert!(
approx_eq(result.value, expected_value, 1e-6),
"x^x = {} but got {} at x={}",
expected_value, result.value, x
);
prop_assert!(
approx_eq(result.derivative, expected_deriv, 1e-4),
"d(x^x)/dx = {} but got {} at x={}",
expected_deriv, result.derivative, x
);
}
}
fn newton_raphson_test(
expr: &Expression,
target: f64,
initial_x: f64,
max_iterations: usize,
) -> Option<(f64, usize)> {
let mut x = initial_x;
let tolerance = 1e-14;
for i in 0..max_iterations {
let result = evaluate(expr, x).ok()?;
let f = result.value - target;
let df = result.derivative;
if df.abs() < 1e-100 {
return None; }
let delta = f / df;
x -= delta;
if delta.abs() < tolerance * (1.0 + x.abs()) {
return Some((x, i + 1));
}
if x.abs() > 1e100 || x.is_nan() {
return None;
}
}
let result = evaluate(expr, x).ok()?;
if (result.value - target).abs() < 1e-10 {
Some((x, max_iterations))
} else {
None
}
}
proptest! {
#[test]
fn newton_quadratic_convergence(target in 1.0f64..100.0) {
let expr = Expression::parse("xs").unwrap(); let sqrt_target = target.sqrt();
let initial_x = sqrt_target + 1.0;
if let Some((solution, iterations)) = newton_raphson_test(&expr, target, initial_x, 20) {
let error = (solution.abs() - sqrt_target).abs();
prop_assert!(
error < 1e-10,
"Newton failed to converge: expected ±{:.6}, got {:.10}",
sqrt_target, solution
);
prop_assert!(
iterations <= 10,
"Quadratic convergence took {} iterations (expected ≤10)",
iterations
);
} else {
}
}
#[test]
fn newton_cubic_polynomial(target in 1.0f64..100.0) {
let expr = Expression::parse("xsx*").unwrap(); let cbrt_target = target.cbrt();
let initial_x = cbrt_target + 1.0;
if let Some((solution, iterations)) = newton_raphson_test(&expr, target, initial_x, 20) {
let error = (solution - cbrt_target).abs();
prop_assert!(
error < 1e-9,
"Newton failed for x^3 = {}: expected {:.6}, got {:.10}",
target, cbrt_target, solution
);
prop_assert!(
iterations <= 15,
"Cubic convergence took {} iterations",
iterations
);
}
}
#[test]
fn newton_exponential(target in 1.0f64..20.0) {
let expr = Expression::parse("xE").unwrap(); let ln_target = target.ln();
let initial_x = ln_target + 0.5;
if let Some((solution, _iterations)) = newton_raphson_test(&expr, target, initial_x, 20) {
let error = (solution - ln_target).abs();
prop_assert!(
error < 1e-10,
"Newton failed for e^x = {}: expected {:.6}, got {:.10}",
target, ln_target, solution
);
}
}
#[test]
fn newton_logarithm(target in -2.0f64..3.0) {
let expr = Expression::parse("xl").unwrap(); let exp_target = target.exp();
if exp_target <= 0.0 || exp_target > 100.0 {
return Ok(()); }
let initial_x = exp_target + 0.5;
if let Some((solution, _iterations)) = newton_raphson_test(&expr, target, initial_x, 20) {
let error = (solution - exp_target).abs();
prop_assert!(
error < 1e-9,
"Newton failed for ln(x) = {}: expected {:.6}, got {:.10}",
target, exp_target, solution
);
}
}
#[test]
fn newton_x_to_x(target in 2.0f64..20.0) {
let expr = Expression::parse("xx^").unwrap(); let approx_x = (target.ln() / std::f64::consts::E).powf(1.0 / std::f64::consts::E);
let initial_x = approx_x.max(1.5);
if let Some((solution, iterations)) = newton_raphson_test(&expr, target, initial_x, 30) {
let verify = evaluate(&expr, solution).unwrap();
let error = (verify.value - target).abs();
prop_assert!(
error < 1e-6,
"Newton failed for x^x = {}: verification error = {:.2e}",
target, error
);
prop_assert!(
iterations <= 25,
"x^x convergence took {} iterations",
iterations
);
}
}
#[test]
fn newton_difficult_start(target in 5.0f64..15.0) {
let expr = Expression::parse("xs").unwrap(); let initial_x = -10.0;
if let Some((solution, _iterations)) = newton_raphson_test(&expr, target, initial_x, 30) {
let verify = evaluate(&expr, solution).unwrap();
let error = (verify.value - target).abs();
prop_assert!(
error < 1e-8,
"Newton from bad start: verification error = {:.2e}",
error
);
}
}
#[test]
fn newton_sinpi(target in -1.0f64..1.0) {
let expr = Expression::parse("xS").unwrap(); let arcsin_t = target.asin();
let solution1 = arcsin_t / std::f64::consts::PI;
let initial_x = solution1 + 0.1;
if let Some((solution, _iterations)) = newton_raphson_test(&expr, target, initial_x, 20) {
let verify = evaluate(&expr, solution).unwrap();
let error = (verify.value - target).abs();
prop_assert!(
error < 1e-9,
"Newton failed for sin(πx) = {}: verification error = {:.2e}",
target, error
);
}
}
}
fn newton_with_history(
expr: &Expression,
target: f64,
initial_x: f64,
max_iterations: usize,
) -> Option<Vec<f64>> {
let mut x = initial_x;
let mut errors = Vec::new();
for _ in 0..max_iterations {
let result = evaluate(expr, x).ok()?;
let f = result.value - target;
let df = result.derivative;
errors.push(f.abs());
if df.abs() < 1e-100 {
return None;
}
let delta = f / df;
x -= delta;
if x.abs() > 1e100 || x.is_nan() {
return None;
}
}
Some(errors)
}
proptest! {
#[test]
fn quadratic_convergence_rate(c in 2.0f64..50.0) {
let expr = Expression::parse("xs").unwrap(); let sqrt_c = c.sqrt();
let initial_x = sqrt_c + 0.5;
if let Some(errors) = newton_with_history(&expr, c, initial_x, 10) {
if errors.len() >= 3 {
for i in 1..errors.len().saturating_sub(1) {
if errors[i] > 1e-14 && errors[i + 1] > 1e-14 {
let ratio = errors[i + 1] / (errors[i] * errors[i]);
let expected_ratio = 1.0 / (2.0 * sqrt_c);
prop_assert!(
(ratio - expected_ratio).abs() < expected_ratio * 2.0,
"Convergence rate ratio {:.3} differs from expected {:.3} at iteration {}",
ratio, expected_ratio, i
);
}
}
}
}
}
}