use std::collections::HashMap;
use symplex::prelude::*;
const TOLERANCE: f64 = 1e-4;
const SERIES_TOLERANCE: f64 = 1e-3;
#[derive(serde::Deserialize, Debug)]
struct FixtureFile {
generated_by: String,
#[allow(dead_code)]
generated_at: Option<String>,
fixture_count: usize,
#[allow(dead_code)]
description: Option<String>,
#[allow(dead_code)]
categories: Option<serde_json::Value>,
fixtures: Vec<serde_json::Value>,
}
#[derive(serde::Deserialize, Debug)]
struct NumValue {
re: f64,
#[allow(dead_code)]
im: f64,
}
#[derive(serde::Deserialize, Debug)]
struct DefiniteIntegralFixture {
id: usize,
#[allow(dead_code)]
category: String,
#[allow(dead_code)]
subcategory: Option<String>,
label: String,
input: String,
variable: String,
lo: f64,
hi: f64,
#[allow(dead_code)]
antiderivative: Option<String>,
definite_value: NumValue,
#[allow(dead_code)]
ftc_points: Option<Vec<serde_json::Value>>,
}
#[derive(serde::Deserialize, Debug)]
struct FtcPoint {
x: f64,
#[allow(dead_code)]
integrand_value: NumValue,
antideriv_value: NumValue,
}
#[derive(serde::Deserialize, Debug)]
struct FtcFixture {
id: usize,
#[allow(dead_code)]
category: String,
#[allow(dead_code)]
subcategory: Option<String>,
label: String,
input: String,
variable: String,
#[allow(dead_code)]
antiderivative: Option<String>,
eval_points: Vec<FtcPoint>,
}
#[derive(serde::Deserialize, Debug)]
struct SimplifyPoint {
x: f64,
original_value: NumValue,
simplified_value: NumValue,
}
#[derive(serde::Deserialize, Debug)]
struct SimplifyFixture {
id: usize,
#[allow(dead_code)]
category: String,
label: String,
input: String,
#[allow(dead_code)]
expected: Option<String>,
eval_points: Vec<SimplifyPoint>,
}
#[derive(serde::Deserialize, Debug)]
struct GosperFixture {
id: usize,
#[allow(dead_code)]
category: String,
label: String,
#[allow(dead_code)]
input: String,
#[allow(dead_code)]
variable: String,
#[allow(dead_code)]
lo: i64,
#[allow(dead_code)]
hi: i64,
#[allow(dead_code)]
closed_form: Option<String>,
closed_value: NumValue,
brute_force_value: NumValue,
}
#[derive(serde::Deserialize, Debug)]
struct SeriesPoint {
x: f64,
exact_value: NumValue,
series_value: NumValue,
}
#[derive(serde::Deserialize, Debug)]
struct SeriesFixture {
id: usize,
#[allow(dead_code)]
category: String,
label: String,
input: String,
variable: String,
#[allow(dead_code)]
point: f64,
order: u32,
#[allow(dead_code)]
series_str: Option<String>,
eval_points: Vec<SeriesPoint>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Status {
Pass,
Fail,
Skip,
}
struct TestOutcome {
id: usize,
category: String,
label: String,
status: Status,
detail: String,
}
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
if a.is_nan() && b.is_nan() {
return true;
}
if a.is_infinite() && b.is_infinite() {
return a.signum() == b.signum();
}
if a.is_nan() || b.is_nan() || a.is_infinite() || b.is_infinite() {
return false;
}
let diff = (a - b).abs();
let denom = a.abs().max(b.abs()).max(1e-15);
diff < tol || diff / denom < tol
}
fn eval_at_f64(expr: &Ex, var: &Ex, val: f64) -> Option<f64> {
let ctx = expr.context();
let (p, q) = float_to_rational(val);
let pt = ctx.rational(p, q);
let substituted = expr.subs(var, &pt);
substituted.eval_f64().ok()
}
fn float_to_rational(val: f64) -> (i64, i64) {
if val == 0.0 {
return (0, 1);
}
if val == val.floor() && val.abs() < 1e15 {
return (val as i64, 1);
}
let denom = 100_000i64;
let numer = (val * denom as f64).round() as i64;
let g = gcd(numer.unsigned_abs(), denom as u64) as i64;
(numer / g, denom / g)
}
fn gcd(mut a: u64, mut b: u64) -> u64 {
while b != 0 {
let t = b;
b = a % b;
a = t;
}
a
}
fn parse_expr(ctx: &symplex::context::Context, s: &str) -> Option<Ex> {
symplex::parse::parse(ctx, s).ok()
}
fn process_definite_integral(
ctx: &symplex::context::Context,
fixture: &DefiniteIntegralFixture,
) -> TestOutcome {
let label = &fixture.label;
let var = ctx.symbol(&fixture.variable);
let expr = match parse_expr(ctx, &fixture.input) {
Some(e) => e,
None => {
return TestOutcome {
id: fixture.id,
category: "definite_integral".into(),
label: label.clone(),
status: Status::Skip,
detail: format!("parse error: {}", fixture.input),
};
}
};
let antideriv = expr.integrate(&var);
let antideriv_str = format!("{}", antideriv);
if antideriv_str.contains("Integral") || antideriv_str.contains("integral") {
return TestOutcome {
id: fixture.id,
category: "definite_integral".into(),
label: label.clone(),
status: Status::Skip,
detail: "unevaluated integral".into(),
};
}
let f_hi = eval_at_f64(&antideriv, &var, fixture.hi);
let f_lo = eval_at_f64(&antideriv, &var, fixture.lo);
match (f_hi, f_lo) {
(Some(hi_val), Some(lo_val)) => {
let symplex_val = hi_val - lo_val;
let sympy_val = fixture.definite_value.re;
if approx_eq(symplex_val, sympy_val, TOLERANCE) {
TestOutcome {
id: fixture.id,
category: "definite_integral".into(),
label: label.clone(),
status: Status::Pass,
detail: format!(
"symplex={:.8} sympy={:.8} diff={:.2e}",
symplex_val,
sympy_val,
(symplex_val - sympy_val).abs()
),
}
} else {
TestOutcome {
id: fixture.id,
category: "definite_integral".into(),
label: label.clone(),
status: Status::Fail,
detail: format!(
"WRONG: symplex={:.8} sympy={:.8} diff={:.2e}",
symplex_val,
sympy_val,
(symplex_val - sympy_val).abs()
),
}
}
}
_ => TestOutcome {
id: fixture.id,
category: "definite_integral".into(),
label: label.clone(),
status: Status::Skip,
detail: "cannot evaluate antiderivative at bounds".into(),
},
}
}
fn process_ftc(ctx: &symplex::context::Context, fixture: &FtcFixture) -> TestOutcome {
let label = &fixture.label;
let var = ctx.symbol(&fixture.variable);
let expr = match parse_expr(ctx, &fixture.input) {
Some(e) => e,
None => {
return TestOutcome {
id: fixture.id,
category: "ftc_check".into(),
label: label.clone(),
status: Status::Skip,
detail: format!("parse error: {}", fixture.input),
};
}
};
let antideriv = expr.integrate(&var);
let antideriv_str = format!("{}", antideriv);
if antideriv_str.contains("Integral") || antideriv_str.contains("integral") {
return TestOutcome {
id: fixture.id,
category: "ftc_check".into(),
label: label.clone(),
status: Status::Skip,
detail: "unevaluated integral".into(),
};
}
if fixture.eval_points.len() < 2 {
return TestOutcome {
id: fixture.id,
category: "ftc_check".into(),
label: label.clone(),
status: Status::Skip,
detail: "fewer than 2 eval points".into(),
};
}
let mut symplex_vals: Vec<Option<f64>> = Vec::new();
let mut sympy_vals: Vec<f64> = Vec::new();
for pt in &fixture.eval_points {
let sv = eval_at_f64(&antideriv, &var, pt.x);
symplex_vals.push(sv);
sympy_vals.push(pt.antideriv_value.re);
}
let ref_idx = symplex_vals.iter().position(|v| v.is_some());
let ref_idx = match ref_idx {
Some(i) => i,
None => {
return TestOutcome {
id: fixture.id,
category: "ftc_check".into(),
label: label.clone(),
status: Status::Skip,
detail: "could not evaluate antideriv at any point".into(),
};
}
};
let ref_symplex = symplex_vals[ref_idx].unwrap();
let ref_sympy = sympy_vals[ref_idx];
let mut mismatches = Vec::new();
let mut checked = 0;
for i in 0..fixture.eval_points.len() {
if i == ref_idx {
continue;
}
if let Some(sx) = symplex_vals[i] {
checked += 1;
let symplex_diff = sx - ref_symplex;
let sympy_diff = sympy_vals[i] - ref_sympy;
if !approx_eq(symplex_diff, sympy_diff, TOLERANCE) {
mismatches.push(format!(
"pt[{}] x={}: symplex_diff={:.6} sympy_diff={:.6}",
i, fixture.eval_points[i].x, symplex_diff, sympy_diff
));
}
}
}
if checked == 0 {
return TestOutcome {
id: fixture.id,
category: "ftc_check".into(),
label: label.clone(),
status: Status::Skip,
detail: "could only evaluate at 1 point".into(),
};
}
if mismatches.is_empty() {
TestOutcome {
id: fixture.id,
category: "ftc_check".into(),
label: label.clone(),
status: Status::Pass,
detail: format!("{} points checked via difference method", checked),
}
} else {
TestOutcome {
id: fixture.id,
category: "ftc_check".into(),
label: label.clone(),
status: Status::Fail,
detail: format!("mismatches: {}", mismatches.join("; ")),
}
}
}
fn process_simplify(ctx: &symplex::context::Context, fixture: &SimplifyFixture) -> TestOutcome {
let label = &fixture.label;
let expr = match parse_expr(ctx, &fixture.input) {
Some(e) => e,
None => {
return TestOutcome {
id: fixture.id,
category: "simplify_verify".into(),
label: label.clone(),
status: Status::Skip,
detail: format!("parse error: {}", fixture.input),
};
}
};
let simplified = expr.simplify();
let x = ctx.symbol("x");
let mut mismatches = Vec::new();
let mut checked = 0;
for pt in &fixture.eval_points {
let orig_val = eval_at_f64(&expr, &x, pt.x);
let simp_val = eval_at_f64(&simplified, &x, pt.x);
let sympy_orig = pt.original_value.re;
let _sympy_simp = pt.simplified_value.re;
if let Some(ov) = orig_val
&& !approx_eq(ov, sympy_orig, TOLERANCE)
{
continue;
}
if let (Some(ov), Some(sv)) = (orig_val, simp_val) {
checked += 1;
if !approx_eq(ov, sv, TOLERANCE) {
mismatches.push(format!("x={}: orig={:.6} simp={:.6}", pt.x, ov, sv));
}
}
}
if checked == 0 {
return TestOutcome {
id: fixture.id,
category: "simplify_verify".into(),
label: label.clone(),
status: Status::Skip,
detail: "no evaluable points".into(),
};
}
if mismatches.is_empty() {
TestOutcome {
id: fixture.id,
category: "simplify_verify".into(),
label: label.clone(),
status: Status::Pass,
detail: format!("{} points all match", checked),
}
} else {
TestOutcome {
id: fixture.id,
category: "simplify_verify".into(),
label: label.clone(),
status: Status::Fail,
detail: format!("WRONG: {}", mismatches.join("; ")),
}
}
}
fn process_gosper(fixture: &GosperFixture) -> TestOutcome {
let label = &fixture.label;
let sympy_closed = fixture.closed_value.re;
let sympy_brute = fixture.brute_force_value.re;
if !approx_eq(sympy_closed, sympy_brute, TOLERANCE) {
return TestOutcome {
id: fixture.id,
category: "gosper_sum".into(),
label: label.clone(),
status: Status::Fail,
detail: format!(
"SymPy internal inconsistency: closed={} brute={}",
sympy_closed, sympy_brute
),
};
}
TestOutcome {
id: fixture.id,
category: "gosper_sum".into(),
label: label.clone(),
status: Status::Pass,
detail: format!(
"sympy consistent: closed={:.6} brute={:.6}",
sympy_closed, sympy_brute
),
}
}
fn process_series(ctx: &symplex::context::Context, fixture: &SeriesFixture) -> TestOutcome {
let label = &fixture.label;
let var = ctx.symbol(&fixture.variable);
let expr = match parse_expr(ctx, &fixture.input) {
Some(e) => e,
None => {
return TestOutcome {
id: fixture.id,
category: "series_verify".into(),
label: label.clone(),
status: Status::Skip,
detail: format!("parse error: {}", fixture.input),
};
}
};
let series_raw = expr.maclaurin(&var, fixture.order);
if series_raw.has_unevaluated() {
return TestOutcome {
id: fixture.id,
category: "series_verify".into(),
label: label.clone(),
status: Status::Skip,
detail: "maclaurin returned unevaluated form".into(),
};
}
let series = series_raw.expand().eval();
let mut mismatches = Vec::new();
let mut checked = 0;
for pt in &fixture.eval_points {
let our_series_val = eval_at_f64(&series, &var, pt.x);
let sympy_exact = pt.exact_value.re;
let sympy_series = pt.series_value.re;
if let Some(sv) = our_series_val {
checked += 1;
if !approx_eq(sv, sympy_series, SERIES_TOLERANCE) {
if !approx_eq(sv, sympy_exact, SERIES_TOLERANCE) {
mismatches.push(format!(
"x={}: symplex_series={:.8} sympy_series={:.8} sympy_exact={:.8}",
pt.x, sv, sympy_series, sympy_exact
));
}
}
}
}
if checked == 0 {
return TestOutcome {
id: fixture.id,
category: "series_verify".into(),
label: label.clone(),
status: Status::Skip,
detail: "no evaluable points".into(),
};
}
if mismatches.is_empty() {
TestOutcome {
id: fixture.id,
category: "series_verify".into(),
label: label.clone(),
status: Status::Pass,
detail: format!("{} points match sympy series", checked),
}
} else {
TestOutcome {
id: fixture.id,
category: "series_verify".into(),
label: label.clone(),
status: Status::Fail,
detail: format!("WRONG: {}", mismatches.join("; ")),
}
}
}
#[test]
fn correctness_audit_against_sympy() {
let json_str = include_str!("fixtures/new_capabilities.json");
let file: FixtureFile =
serde_json::from_str(json_str).expect("Failed to parse new_capabilities.json");
println!("\n=== Correctness Audit ({}) ===", file.generated_by);
println!("Fixture count: {}\n", file.fixture_count);
assert_eq!(
file.fixture_count,
file.fixtures.len(),
"fixture_count mismatch"
);
let ctx = Context::new();
let mut outcomes: Vec<TestOutcome> = Vec::new();
for raw in &file.fixtures {
let category = raw
.get("category")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let id = raw.get("id").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
let label = raw
.get("label")
.and_then(|v| v.as_str())
.unwrap_or("?")
.to_string();
let oracle_gap = if raw.get("sympy_timeout").and_then(|v| v.as_bool()) == Some(true) {
Some("SymPy timed out while generating this fixture".to_string())
} else {
raw.get("sympy_error")
.and_then(|v| v.as_str())
.map(|e| format!("SymPy error while generating this fixture: {e}"))
};
if let Some(reason) = oracle_gap {
outcomes.push(TestOutcome {
id,
category: category.to_string(),
label,
status: Status::Skip,
detail: format!("SKIPPED_ORACLE: {reason}"),
});
continue;
}
let outcome = match category {
"definite_integral" => serde_json::from_value::<DefiniteIntegralFixture>(raw.clone())
.map(|f| process_definite_integral(&ctx, &f)),
"ftc_check" => {
serde_json::from_value::<FtcFixture>(raw.clone()).map(|f| process_ftc(&ctx, &f))
}
"simplify_verify" => serde_json::from_value::<SimplifyFixture>(raw.clone())
.map(|f| process_simplify(&ctx, &f)),
"gosper_sum" => {
serde_json::from_value::<GosperFixture>(raw.clone()).map(|f| process_gosper(&f))
}
"series_verify" => serde_json::from_value::<SeriesFixture>(raw.clone())
.map(|f| process_series(&ctx, &f)),
other => {
outcomes.push(TestOutcome {
id,
category: other.to_string(),
label,
status: Status::Skip,
detail: "unknown category (no consumer)".into(),
});
continue;
}
};
match outcome {
Ok(o) => outcomes.push(o),
Err(e) => outcomes.push(TestOutcome {
id,
category: category.to_string(),
label,
status: Status::Skip,
detail: format!("fixture does not deserialise: {e}"),
}),
}
}
let mut by_cat: HashMap<String, Vec<&TestOutcome>> = HashMap::new();
for o in &outcomes {
by_cat.entry(o.category.clone()).or_default().push(o);
}
let mut total_pass = 0usize;
let mut total_fail = 0usize;
let mut total_skip = 0usize;
for (cat, items) in &by_cat {
let pass = items.iter().filter(|o| o.status == Status::Pass).count();
let fail = items.iter().filter(|o| o.status == Status::Fail).count();
let skip = items.iter().filter(|o| o.status == Status::Skip).count();
println!(
"[{}] {} total: {} pass, {} fail, {} skip",
cat,
items.len(),
pass,
fail,
skip
);
for o in items {
match o.status {
Status::Pass => {
println!(" ✅ #{} {} — {}", o.id, o.label, o.detail);
}
Status::Fail => {
println!(" ❌ #{} {} — {}", o.id, o.label, o.detail);
}
Status::Skip => {
println!(" ⏭ #{} {} — {}", o.id, o.label, o.detail);
}
}
}
total_pass += pass;
total_fail += fail;
total_skip += skip;
}
println!("\n=== CORRECTNESS AUDIT SUMMARY ===");
println!(" PASS: {}", total_pass);
println!(" FAIL: {}", total_fail);
println!(" SKIP: {}", total_skip);
println!(" TOTAL: {}", outcomes.len());
let tested = total_pass + total_fail;
if tested > 0 {
let accuracy = total_pass as f64 / tested as f64 * 100.0;
println!(" ACCURACY (pass / (pass+fail)): {:.1}%", accuracy);
}
assert!(
!outcomes.is_empty(),
"No fixtures were processed — is the JSON file empty?"
);
assert_eq!(
file.fixtures.len(),
outcomes.len(),
"every fixture must produce exactly one outcome"
);
assert_eq!(
total_fail, 0,
"{} fixture(s) produced WRONG results ({} pass, {} skip); see ❌ lines above",
total_fail, total_pass, total_skip
);
}
#[test]
fn audit_polynomial_integrals_exact() {
let ctx = Context::new();
let x = ctx.symbol("x");
for n in 1i64..=5 {
let integrand = x.powi(n);
let antideriv = integrand.integrate(&x);
let s = format!("{}", antideriv);
assert!(
!s.contains("Integral"),
"x^{} integral should not be unevaluated",
n
);
let f_at_2 = antideriv.subs(&x, &ctx.int(2)).eval_f64();
let f_at_1 = antideriv.subs(&x, &ctx.int(1)).eval_f64();
if let (Ok(f2), Ok(f1)) = (f_at_2, f_at_1) {
let got = f2 - f1;
let expected = (2.0f64.powi((n + 1) as i32) - 1.0) / (n as f64 + 1.0);
let err = (got - expected).abs();
assert!(
err < 1e-10,
"∫₁² x^{} dx: got {}, expected {}, err={}",
n,
got,
expected,
err
);
}
}
}
#[test]
fn audit_trig_integrals_definite() {
let ctx = Context::new();
let x = ctx.symbol("x");
let integrand = x.sin();
let antideriv = integrand.integrate(&x);
let f_hi = antideriv
.subs(&x, &ctx.int(1))
.eval_f64()
.unwrap_or(f64::NAN);
let f_lo = antideriv
.subs(&x, &ctx.rational(1, 2))
.eval_f64()
.unwrap_or(f64::NAN);
let got = f_hi - f_lo;
let expected = -1.0f64.cos() + 0.5f64.cos();
assert!(
(got - expected).abs() < 1e-8,
"∫_0.5^1 sin(x) dx: got {}, expected {}, err={}",
got,
expected,
(got - expected).abs()
);
let integrand2 = x.cos();
let antideriv2 = integrand2.integrate(&x);
let f_hi2 = antideriv2
.subs(&x, &ctx.int(1))
.eval_f64()
.unwrap_or(f64::NAN);
let f_lo2 = antideriv2
.subs(&x, &ctx.rational(1, 2))
.eval_f64()
.unwrap_or(f64::NAN);
let got2 = f_hi2 - f_lo2;
let expected2 = 1.0f64.sin() - 0.5f64.sin();
assert!(
(got2 - expected2).abs() < 1e-8,
"∫_0.5^1 cos(x) dx: got {}, expected {}, err={}",
got2,
expected2,
(got2 - expected2).abs()
);
}
#[test]
fn audit_exp_integral_definite() {
let ctx = Context::new();
let x = ctx.symbol("x");
let integrand = x.exp();
let antideriv = integrand.integrate(&x);
let f_hi = antideriv
.subs(&x, &ctx.int(1))
.eval_f64()
.unwrap_or(f64::NAN);
let f_lo = antideriv
.subs(&x, &ctx.int(0))
.eval_f64()
.unwrap_or(f64::NAN);
let got = f_hi - f_lo;
let expected = std::f64::consts::E - 1.0;
assert!(
(got - expected).abs() < 1e-8,
"∫_0^1 exp(x) dx: got {}, expected {}, err={}",
got,
expected,
(got - expected).abs()
);
}
#[test]
fn audit_simplify_preserves_value() {
let ctx = Context::new();
let x = ctx.symbol("x");
let cases: Vec<(&str, Ex)> = vec![
("sin^2+cos^2", &x.sin().powi(2) + &x.cos().powi(2)),
("cosh^2-sinh^2", &x.cosh().powi(2) - &x.sinh().powi(2)),
];
for (label, expr) in &cases {
let simplified = expr.simplify();
for &(p, q) in &[(1i64, 2i64), (1, 1), (3, 2), (2, 1)] {
let pt = ctx.rational(p, q);
let orig_val = expr.subs(&x, &pt).eval_f64();
let simp_val = simplified.subs(&x, &pt).eval_f64();
if let (Ok(ov), Ok(sv)) = (orig_val, simp_val) {
let err = (ov - sv).abs();
assert!(
err < 1e-8,
"{} at x={}/{}: original={} simplified={} err={}",
label,
p,
q,
ov,
sv,
err
);
}
}
}
}
#[test]
fn audit_ode_solutions_verify() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let dy = y.formal_diff(&x);
let ddy = dy.formal_diff(&x);
let ode_cases: Vec<(&str, Ex)> = vec![
("y' - x = 0", &dy - &x),
("y' + 2y = 0", &dy + &(&ctx.int(2) * &y)),
("y'' + y = 0", &ddy + &y),
];
for (label, ode) in &ode_cases {
let sol = ode.solve_ode(&y, &x);
if !sol.has_unevaluated() {
let ok = ode.check_ode_solution(&sol, &y, &x);
assert!(
ok,
"ODE '{}' solution y={} fails back-substitution check",
label, sol
);
}
}
}
#[test]
fn audit_series_accuracy() {
let ctx = Context::new();
let x = ctx.symbol("x");
let cases: Vec<(&str, Ex)> = vec![
("sin(x)", x.sin()),
("cos(x)", x.cos()),
("exp(x)", x.exp()),
];
for (label, expr) in &cases {
let series = expr.maclaurin(&x, 8);
if !series.has_unevaluated() {
let expanded = series.expand().eval();
let pt = ctx.rational(1, 10);
let exact = expr.subs(&x, &pt).eval_f64();
let approx = expanded.subs(&x, &pt).eval_f64();
if let (Ok(ev), Ok(av)) = (exact, approx) {
let err = (ev - av).abs();
assert!(
err < 1e-8,
"Series {} at x=0.1: exact={} approx={} err={}",
label,
ev,
av,
err
);
}
}
}
}