mod common;
use symplex::prelude::*;
fn numerical_eq_1var(a: &Ex, b: &Ex, var: &Ex, points: &[i64], tol: f64) -> bool {
for &p in points {
let va = a.subs_i64(var, p).eval().eval_f64();
let vb = b.subs_i64(var, p).eval().eval_f64();
match (va, vb) {
(Ok(fa), Ok(fb)) => {
if (fa - fb).abs() > tol * fa.abs().max(fb.abs()).max(1.0) {
return false;
}
}
(Err(_), Err(_)) => {} _ => return false,
}
}
true
}
fn fmt(e: &Ex) -> String {
format!("{e}")
}
#[test]
fn trig_double_angle_sin_2x_from_2sincos() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.sin() * &x.cos() * 2;
let simplified = expr.simplify();
let s = fmt(&simplified);
assert!(
numerical_eq_1var(&expr, &simplified, &x, &[1, 2, 3, 5], 1e-10),
"2*sin(x)*cos(x) simplify broke numerical equivalence: {s}"
);
}
#[test]
fn trig_double_angle_cos2x_from_cos2_minus_sin2() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.cos().powi(2) - &x.sin().powi(2);
let simplified = expr.simplify();
assert!(
numerical_eq_1var(&expr, &simplified, &x, &[1, 2, 3], 1e-10),
"cos²(x)-sin²(x) simplify broke numerical equivalence: {}",
fmt(&simplified)
);
}
#[test]
fn trig_pythagorean_identity_basic() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.sin().powi(2) + &x.cos().powi(2);
let simplified = expr.simplify();
assert_eq!(fmt(&simplified), "1", "sin²+cos² should be 1");
}
#[test]
fn trig_pythagorean_scaled() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &(&x.sin().powi(2) * 3) + &(&x.cos().powi(2) * 3);
let simplified = expr.simplify();
assert!(
numerical_eq_1var(&expr, &simplified, &x, &[1, 2, 3], 1e-10),
"3*(sin²+cos²) simplify broke numerical equivalence: {}",
fmt(&simplified)
);
}
#[test]
fn trig_nested_sin_of_sin() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.sin().sin();
let simplified = expr.simplify();
let s = fmt(&simplified);
assert!(
s.contains("sin"),
"sin(sin(x)) should still contain sin: {s}"
);
assert!(
numerical_eq_1var(&expr, &simplified, &x, &[1, 2, 3], 1e-10),
"sin(sin(x)) simplify broke numerical equivalence: {s}"
);
}
#[test]
fn trig_nested_cos_of_sin() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.sin().cos();
let simplified = expr.simplify();
assert!(
numerical_eq_1var(&expr, &simplified, &x, &[1, 2, 3], 1e-10),
"cos(sin(x)) simplify broke numerical equivalence: {}",
fmt(&simplified)
);
}
#[test]
fn trig_expand_sin_3x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (&x * 3).sin();
let expanded = expr.expand_trig();
let s = fmt(&expanded);
assert!(
!s.contains("3*x") && !s.contains("2*x"),
"sin(3x) should fully expand, got: {s}"
);
assert!(
numerical_eq_1var(&expr, &expanded, &x, &[1, 2, 3], 1e-10),
"sin(3x) trig_expand broke numerical equivalence: {s}"
);
}
#[test]
fn trig_expand_cos_4x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (&x * 4).cos();
let expanded = expr.expand_trig();
let s = fmt(&expanded);
assert!(!s.contains("4*x"), "cos(4x) should be expanded, got: {s}");
assert!(
numerical_eq_1var(&expr, &expanded, &x, &[1, 2, 3], 1e-10),
"cos(4x) trig_expand broke numerical equivalence: {s}"
);
}
#[test]
fn trig_product_to_sum_sinx_cosy() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let expr = &x.sin() * &y.cos();
let combined = expr.trig_combine();
let original_val = expr.subs_i64(&x, 1).subs_i64(&y, 2).eval().eval_f64();
let combined_val = combined.subs_i64(&x, 1).subs_i64(&y, 2).eval().eval_f64();
match (original_val, combined_val) {
(Ok(a), Ok(b)) => {
assert!(
(a - b).abs() < 1e-10,
"sin(x)*cos(y) trig_combine broke equivalence: {a} vs {b}, result={}",
fmt(&combined)
);
}
_ => panic!("evaluation failed"),
}
}
#[test]
fn trig_product_to_sum_sinx_siny() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let expr = &x.sin() * &y.sin();
let combined = expr.trig_combine();
let original_val = expr.subs_i64(&x, 1).subs_i64(&y, 2).eval().eval_f64();
let combined_val = combined.subs_i64(&x, 1).subs_i64(&y, 2).eval().eval_f64();
match (original_val, combined_val) {
(Ok(a), Ok(b)) => {
assert!(
(a - b).abs() < 1e-10,
"sin(x)*sin(y) trig_combine broke equivalence: {a} vs {b}"
);
}
_ => panic!("evaluation failed"),
}
}
#[test]
fn trig_product_to_sum_cosx_cosy() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let expr = &x.cos() * &y.cos();
let combined = expr.trig_combine();
let original_val = expr.subs_i64(&x, 2).subs_i64(&y, 3).eval().eval_f64();
let combined_val = combined.subs_i64(&x, 2).subs_i64(&y, 3).eval().eval_f64();
match (original_val, combined_val) {
(Ok(a), Ok(b)) => {
assert!(
(a - b).abs() < 1e-10,
"cos(x)*cos(y) trig_combine broke equivalence: {a} vs {b}"
);
}
_ => panic!("evaluation failed"),
}
}
#[test]
fn trig_hyperbolic_identity_cosh2_minus_sinh2() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.cosh().powi(2) - &x.sinh().powi(2);
for p in [1, 2, 3] {
let val = expr.subs_i64(&x, p).eval().eval_f64();
match val {
Ok(v) => assert!(
(v - 1.0).abs() < 1e-9,
"cosh²(x)-sinh²(x) should be 1 at x={p}, got {v}"
),
Err(e) => panic!("evaluation failed at x={p}: {e}"),
}
}
let simplified = expr.simplify();
let full = expr.simplify();
let s_simp = fmt(&simplified);
let s_full = fmt(&full);
eprintln!("cosh²-sinh²: simplify -> {s_simp}");
eprintln!("cosh²-sinh²: full_simplify -> {s_full}");
for p in [1, 2, 3] {
let v1 = simplified.subs_i64(&x, p).eval().eval_f64();
let v2 = full.subs_i64(&x, p).eval().eval_f64();
match v1 {
Ok(v) => assert!(
(v - 1.0).abs() < 1e-9,
"simplified cosh²-sinh² at x={p}: {v}"
),
Err(e) => panic!("simplified eval failed at x={p}: {e}"),
}
match v2 {
Ok(v) => assert!(
(v - 1.0).abs() < 1e-9,
"full_simplified cosh²-sinh² at x={p}: {v}"
),
Err(e) => panic!("full_simplified eval failed at x={p}: {e}"),
}
}
}
#[test]
fn trig_hyperbolic_tanh_equals_sinh_over_cosh() {
let ctx = Context::new();
let x = ctx.symbol("x");
let tanh_x = x.tanh();
let ratio = &x.sinh() / &x.cosh();
for p in [1, 2, 3] {
let v1 = tanh_x.subs_i64(&x, p).eval().eval_f64();
let v2 = ratio.subs_i64(&x, p).eval().eval_f64();
match (v1, v2) {
(Ok(a), Ok(b)) => assert!(
(a - b).abs() < 1e-10,
"tanh(x) != sinh(x)/cosh(x) at x={p}: {a} vs {b}"
),
_ => panic!("evaluation failed at x={p}"),
}
}
}
#[test]
fn log_expand_product() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let expr = (&a * &b).ln();
let expanded = expr.expand_log();
let s = fmt(&expanded);
assert!(
s.contains("ln(a)") && s.contains("ln(b)"),
"ln(a*b) should expand to ln(a)+ln(b), got: {s}"
);
}
#[test]
fn log_expand_power() {
let ctx = Context::new();
let a = ctx.symbol("a");
let expr = a.powi(3).ln();
let expanded = expr.expand_log();
let s = fmt(&expanded);
assert!(
s.contains("ln(a)") && s.contains("3"),
"ln(a^3) should expand to 3*ln(a), got: {s}"
);
}
#[test]
fn log_combine_sum() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let expr = &a.ln() + &b.ln();
let combined = expr.log_combine();
let s = fmt(&combined);
let ln_count = s.matches("ln(").count();
assert_eq!(
ln_count, 1,
"ln(a)+ln(b) should combine to single ln, got: {s}"
);
}
#[test]
fn log_combine_coeff() {
let ctx = Context::new();
let a = ctx.symbol("a");
let expr = &a.ln() * 2;
let combined = expr.log_combine();
let s = fmt(&combined);
assert!(
s.contains("ln("),
"2*ln(a) should combine to ln(a^2), got: {s}"
);
}
#[test]
fn log_roundtrip_expand_combine() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let original = (&a * &b).ln();
let expanded = original.expand_log();
let recombined = expanded.log_combine();
for (pa, pb) in [(2, 3), (5, 7)] {
let v_orig = original.subs_i64(&a, pa).subs_i64(&b, pb).eval().eval_f64();
let v_recom = recombined
.subs_i64(&a, pa)
.subs_i64(&b, pb)
.eval()
.eval_f64();
match (v_orig, v_recom) {
(Ok(o), Ok(r)) => assert!(
(o - r).abs() < 1e-10,
"ln(a*b) roundtrip failed at a={pa},b={pb}: {o} vs {r}"
),
_ => panic!("evaluation failed"),
}
}
}
#[test]
fn log_exp_simplify() {
let ctx = Context::new();
let x = ctx.symbol_with("x", &[Assumption::Real]);
let expr = x.exp().ln();
let simplified = expr.simplify();
assert_eq!(
fmt(&simplified),
"x",
"ln(exp(x)) should simplify to x, got: {}",
fmt(&simplified)
);
}
#[test]
fn exp_log_simplify() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.ln().exp();
let simplified = expr.simplify();
assert_eq!(
fmt(&simplified),
"x",
"exp(ln(x)) should simplify to x, got: {}",
fmt(&simplified)
);
}
#[test]
fn log_of_one_is_zero() {
let ctx = Context::new();
let expr = ctx.int(1).ln();
let evaled = expr.eval();
assert_eq!(
fmt(&evaled),
"0",
"ln(1) should eval to 0, got: {}",
fmt(&evaled)
);
}
#[test]
fn log_of_e_is_one() {
let ctx = Context::new();
let expr = ctx.e().ln();
let evaled = expr.eval();
assert_eq!(
fmt(&evaled),
"1",
"ln(e) should eval to 1, got: {}",
fmt(&evaled)
);
}
#[test]
fn log_of_negative_number_is_complex() {
let ctx = Context::new();
let expr = ctx.int(-1).ln();
let evaled = expr.eval();
let s = fmt(&evaled);
eprintln!("ln(-1) = {s}");
let eval_result = evaled.eval_complex64();
match eval_result {
Ok((re, im)) => {
eprintln!("ln(-1) complex: ({re}, {im})");
assert!(re.abs() < 1e-9, "ln(-1) real part should be 0, got: {re}");
assert!(
(im.abs() - std::f64::consts::PI).abs() < 1e-9,
"ln(-1) imaginary part should be ±π, got: {im}"
);
}
Err(e) => {
eprintln!("ln(-1) eval failed (may be expected): {e}");
}
}
}
#[test]
fn log_expand_three_factors() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let c = ctx.symbol("c");
let expr = (&a * &b * &c).ln();
let expanded = expr.expand_log();
let s = fmt(&expanded);
let ln_count = s.matches("ln(").count();
assert_eq!(
ln_count, 3,
"ln(a*b*c) should expand to 3 ln terms, got {ln_count}: {s}"
);
}
#[test]
fn pow_x_squared_sqrt_no_assumption() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.powi(2).sqrt();
let simplified = expr.simplify();
let s = fmt(&simplified);
eprintln!("sqrt(x^2) with no assumptions = {s}");
let val_neg = simplified.subs_i64(&x, -3).eval().eval_f64();
match val_neg {
Ok(v) => {
assert!(
(v - 3.0).abs() < 1e-10,
"sqrt((-3)^2) should be 3, got {v} (simplified form: {s}) — BUG if it's -3"
);
}
Err(e) => eprintln!("sqrt(x^2) eval at x=-3 failed: {e}"),
}
}
#[test]
fn pow_x_squared_sqrt_positive_assumption() {
let ctx = Context::new();
let x = ctx.symbol_with("x", &[Assumption::Positive]);
let expr = x.powi(2).sqrt();
let refined = expr.refine();
let s = fmt(&refined);
assert_eq!(
s, "x",
"sqrt(x²) with x positive should refine to x, got: {s}"
);
}
#[test]
fn pow_x_squared_sqrt_real_assumption() {
let ctx = Context::new();
let x = ctx.symbol_with("x", &[Assumption::Real]);
let expr = x.powi(2).sqrt();
let refined = expr.refine();
let s = fmt(&refined);
assert_eq!(
s, "abs(x)",
"sqrt(x²) with x real should refine to abs(x), got: {s}"
);
}
#[test]
fn pow_combine_exponents_xa_times_xb() {
let ctx = Context::new();
let x = ctx.symbol("x");
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let expr = &x.pow(&a) * &x.pow(&b);
let simplified = expr.simplify();
let s = fmt(&simplified);
eprintln!("x^a * x^b = {s}");
let val_orig = expr
.subs_i64(&x, 2)
.subs_i64(&a, 3)
.subs_i64(&b, 4)
.eval()
.eval_f64();
let val_simp = simplified
.subs_i64(&x, 2)
.subs_i64(&a, 3)
.subs_i64(&b, 4)
.eval()
.eval_f64();
match (val_orig, val_simp) {
(Ok(o), Ok(s)) => assert!((o - s).abs() < 1e-6, "x^a*x^b simplify broke: {o} vs {s}"),
_ => eprintln!("evaluation failed for x^a*x^b test"),
}
}
#[test]
fn pow_of_pow_integer_exponents() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.powi(2).powi(3);
let simplified = expr.simplify();
let s = fmt(&simplified);
eprintln!("(x^2)^3 = {s}");
let val = simplified.subs_i64(&x, 3).eval().eval_f64();
match val {
Ok(v) => assert!(
(v - 729.0).abs() < 1e-6,
"(x^2)^3 at x=3 should be 729, got {v}"
),
Err(e) => panic!("eval failed: {e}"),
}
}
#[test]
fn pow_zero_exponent() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.powi(0);
let s = fmt(&expr);
let evaled = expr.eval();
let s2 = fmt(&evaled);
assert!(
s == "1" || s2 == "1",
"x^0 should be 1, got raw={s}, eval={s2}"
);
}
#[test]
fn pow_one_exponent() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.powi(1);
assert_eq!(fmt(&expr), "x", "x^1 should be x, got: {}", fmt(&expr));
}
#[test]
fn pow_negative_exponent() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.powi(-1);
let val = expr.subs_i64(&x, 5).eval().eval_f64();
match val {
Ok(v) => assert!(
(v - 0.2).abs() < 1e-10,
"x^(-1) at x=5 should be 0.2, got {v}"
),
Err(e) => panic!("eval failed: {e}"),
}
}
#[test]
fn expand_binomial_small() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let expr = (&a + &b).powi(2);
let expanded = expr.expand();
let s = fmt(&expanded);
eprintln!("(a+b)^2 expanded = {s}");
let val = expanded.subs_i64(&a, 3).subs_i64(&b, 5).eval().eval_f64();
match val {
Ok(v) => assert!(
(v - 64.0).abs() < 1e-10,
"(a+b)^2 at (3,5) should be 64, got {v}"
),
Err(e) => panic!("eval failed: {e}"),
}
}
#[test]
fn expand_binomial_large_exponent() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let expr = (&a + &b).powi(10);
let expanded = expr.expand();
let s = fmt(&expanded);
eprintln!("(a+b)^10 expanded length: {} chars", s.len());
let val_expanded = expanded.subs_i64(&a, 1).subs_i64(&b, 1).eval().eval_f64();
let val_original = expr.subs_i64(&a, 1).subs_i64(&b, 1).eval().eval_f64();
match (val_original, val_expanded) {
(Ok(o), Ok(e)) => assert!((o - e).abs() < 1e-6, "(a+b)^10 expand broke: {o} vs {e}"),
_ => panic!("eval failed for (a+b)^10"),
}
let v2 = expanded.subs_i64(&a, 2).subs_i64(&b, 3).eval().eval_f64();
match v2 {
Ok(v) => assert!(
(v - 9765625.0).abs() < 1.0,
"(a+b)^10 at (2,3) should be 9765625, got {v}"
),
Err(e) => panic!("eval failed: {e}"),
}
}
#[test]
fn expand_trinomial() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let c = ctx.symbol("c");
let expr = (&a + &b + &c).powi(3);
let expanded = expr.expand();
let val = expanded
.subs_i64(&a, 1)
.subs_i64(&b, 2)
.subs_i64(&c, 3)
.eval()
.eval_f64();
match val {
Ok(v) => assert!(
(v - 216.0).abs() < 1e-6,
"(a+b+c)^3 at (1,2,3) should be 216, got {v}"
),
Err(e) => panic!("eval failed: {e}"),
}
}
#[test]
fn expand_nested_product() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let c = ctx.symbol("c");
let d = ctx.symbol("d");
let expr = &(&a + &b) * &(&c + &d);
let expanded = expr.expand();
let val = expanded
.subs_i64(&a, 1)
.subs_i64(&b, 2)
.subs_i64(&c, 3)
.subs_i64(&d, 4)
.eval()
.eval_f64();
match val {
Ok(v) => assert!((v - 21.0).abs() < 1e-10, "expected 21, got {v}"),
Err(e) => panic!("eval failed: {e}"),
}
}
#[test]
fn expand_trig_sin_a_plus_b() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let expr = (&a + &b).sin();
let expanded = expr.expand_trig();
let s = fmt(&expanded);
assert!(
s.contains("sin") && s.contains("cos"),
"sin(a+b) expand_trig should produce sin and cos terms, got: {s}"
);
let v_orig = expr.subs_i64(&a, 1).subs_i64(&b, 2).eval().eval_f64();
let v_exp = expanded.subs_i64(&a, 1).subs_i64(&b, 2).eval().eval_f64();
match (v_orig, v_exp) {
(Ok(o), Ok(e)) => assert!(
(o - e).abs() < 1e-10,
"sin(a+b) expand_trig broke: {o} vs {e}"
),
_ => panic!("eval failed"),
}
}
#[test]
fn expand_does_not_change_atoms() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expanded = x.expand();
assert_eq!(fmt(&expanded), "x");
}
#[test]
fn rewrite_sin_as_exp() {
let ctx = Context::new();
let x = ctx.symbol("x");
let sin_x = x.sin();
let rewritten = sin_x.rewrite_as_exp();
let s = fmt(&rewritten);
eprintln!("sin(x) rewritten as exp = {s}");
assert!(
s.contains("exp") || s.contains("E"),
"sin(x) rewrite_as_exp should contain exp, got: {s}"
);
let v_orig = sin_x.subs_i64(&x, 2).eval().eval_f64();
let v_rew = rewritten.subs_i64(&x, 2).eval().eval_complex64();
match (v_orig, v_rew) {
(Ok(o), Ok((re, im))) => {
assert!(
(o - re).abs() < 1e-9 && im.abs() < 1e-9,
"sin(x) rewrite_as_exp broke: orig={o}, rewritten=({re}+{im}i)"
);
}
(Ok(o), Err(e)) => {
eprintln!("rewritten eval failed (may be structural): orig={o}, err={e}");
}
_ => eprintln!("evaluation issues"),
}
}
#[test]
fn rewrite_cos_as_exp() {
let ctx = Context::new();
let x = ctx.symbol("x");
let cos_x = x.cos();
let rewritten = cos_x.rewrite_as_exp();
let s = fmt(&rewritten);
eprintln!("cos(x) rewritten as exp = {s}");
assert!(
s.contains("exp") || s.contains("E"),
"cos(x) rewrite_as_exp should contain exp, got: {s}"
);
}
#[test]
fn rewrite_exp_ix_as_trig() {
let ctx = Context::new();
let x = ctx.symbol("x");
let i = ctx.i_unit();
let exp_ix = (&i * &x).exp();
let rewritten = exp_ix.rewrite_as_trig();
let s = fmt(&rewritten);
eprintln!("exp(ix) rewritten as trig = {s}");
assert!(
s.contains("cos") && s.contains("sin"),
"exp(ix) rewrite_as_trig should produce cos and sin, got: {s}"
);
let orig_val = exp_ix.subs_i64(&x, 1).eval().eval_complex64();
let rew_val = rewritten.subs_i64(&x, 1).eval().eval_complex64();
match (orig_val, rew_val) {
(Ok((r1, i1)), Ok((r2, i2))) => {
assert!(
(r1 - r2).abs() < 1e-9 && (i1 - i2).abs() < 1e-9,
"exp(ix) rewrite_as_trig broke: ({r1}+{i1}i) vs ({r2}+{i2}i)"
);
}
_ => eprintln!("complex evaluation issue, may be ok"),
}
}
#[test]
fn rewrite_roundtrip_sin_to_exp_and_back() {
let ctx = Context::new();
let x = ctx.symbol("x");
let sin_x = x.sin();
let as_exp = sin_x.rewrite_as_exp();
let back_to_trig = as_exp.rewrite_as_trig();
let s = fmt(&back_to_trig);
eprintln!("sin(x) -> exp -> trig = {s}");
let v_orig = sin_x.subs_i64(&x, 2).eval().eval_f64();
let v_round = back_to_trig.subs_i64(&x, 2).eval().eval_complex64();
match (v_orig, v_round) {
(Ok(o), Ok((re, im))) => {
assert!(
(o - re).abs() < 1e-8 && im.abs() < 1e-8,
"sin(x) roundtrip rewrite broke: orig={o}, roundtrip=({re}+{im}i)"
);
}
_ => eprintln!("roundtrip eval issue"),
}
}
#[test]
fn rewrite_atom_unchanged() {
let ctx = Context::new();
let x = ctx.symbol("x");
let as_exp = x.rewrite_as_exp();
let as_trig = x.rewrite_as_trig();
assert_eq!(fmt(&as_exp), "x", "symbol rewrite_as_exp should be x");
assert_eq!(fmt(&as_trig), "x", "symbol rewrite_as_trig should be x");
}
#[test]
fn eval_pi_is_recognizable() {
let ctx = Context::new();
let pi = ctx.pi();
let s = fmt(&pi);
assert!(
s.contains("pi") || s.contains("π"),
"pi should display as pi, got: {s}"
);
let val = pi.eval_f64();
match val {
Ok(v) => assert!(
(v - std::f64::consts::PI).abs() < 1e-10,
"pi eval_f64 should be ~3.14159, got {v}"
),
Err(e) => panic!("pi eval failed: {e}"),
}
}
#[test]
fn eval_e_is_recognizable() {
let ctx = Context::new();
let e = ctx.e();
let s = fmt(&e);
assert!(
s.contains("E") || s.contains("e"),
"e should display properly, got: {s}"
);
let val = e.eval_f64();
match val {
Ok(v) => assert!(
(v - std::f64::consts::E).abs() < 1e-10,
"e eval_f64 should be ~2.71828, got {v}"
),
Err(e) => panic!("e eval failed: {e}"),
}
}
#[test]
fn eval_sqrt2_decimal() {
let ctx = Context::new();
let sqrt2 = ctx.int(2).sqrt();
let val = sqrt2.eval_f64();
match val {
Ok(v) => assert!(
(v - std::f64::consts::SQRT_2).abs() < 1e-10,
"sqrt(2) should be ~1.41421, got {v}"
),
Err(e) => panic!("sqrt(2) eval failed: {e}"),
}
}
#[test]
fn eval_sqrt3_decimal() {
let ctx = Context::new();
let sqrt3 = ctx.int(3).sqrt();
let val = sqrt3.eval_f64();
match val {
Ok(v) => assert!(
(v - 3.0_f64.sqrt()).abs() < 1e-10,
"sqrt(3) should be ~1.73205, got {v}"
),
Err(e) => panic!("sqrt(3) eval failed: {e}"),
}
}
#[test]
fn eval_decimal_precision() {
let ctx = Context::new();
let pi = ctx.pi();
let result = pi.eval_decimal(30);
match result {
Ok(s) => {
eprintln!("pi to 30 digits: {s}");
assert!(
s.starts_with("3.14159265358979"),
"pi should start with 3.14159265358979, got: {s}"
);
}
Err(e) => panic!("eval_decimal failed: {e}"),
}
}
#[test]
fn equals_structural_identity() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_eq!(x.equals(&x), Some(true), "x should equal itself");
}
#[test]
fn equals_simple_algebraic() {
let ctx = Context::new();
let x = ctx.symbol("x");
let two_x = &x + &x;
let also_two_x = &x * 2;
assert_eq!(
two_x.equals(&also_two_x),
Some(true),
"x+x should equal 2*x"
);
}
#[test]
fn equals_after_expand() {
let ctx = Context::new();
let x = ctx.symbol("x");
let lhs = (&x + 1).powi(2);
let rhs = &(&x.powi(2) + &(&x * 2)) + 1;
let result = lhs.equals(&rhs);
assert_eq!(
result,
Some(true),
"(x+1)^2 should equal x^2+2x+1, got {:?}",
result
);
}
#[test]
fn equals_different_expressions() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let result = x.equals(&y);
assert_ne!(result, Some(true), "x should not equal y");
}
#[test]
fn equals_pythagorean_identity() {
let ctx = Context::new();
let x = ctx.symbol("x");
let lhs = &x.sin().powi(2) + &x.cos().powi(2);
let one = ctx.int(1);
let result = lhs.equals(&one);
eprintln!("sin²+cos² equals 1? {:?}", result);
let simplified = lhs.simplify();
assert_eq!(
fmt(&simplified),
"1",
"sin²+cos² should simplify to 1 even if equals doesn't detect it"
);
}
#[test]
fn equals_commutative_addition() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let lhs = &a + &b;
let rhs = &b + &a;
assert_eq!(
lhs.equals(&rhs),
Some(true),
"a+b should equal b+a (commutative)"
);
}
#[test]
fn equals_commutative_multiplication() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let lhs = &a * &b;
let rhs = &b * &a;
assert_eq!(
lhs.equals(&rhs),
Some(true),
"a*b should equal b*a (commutative)"
);
}
#[test]
fn equals_zero_difference() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &(&x + 1).powi(2) - &x.powi(2) - &(&x * 2) - 1;
let simplified = expr.simplify();
let s = fmt(&simplified);
eprintln!("(x+1)^2 - x^2 - 2x - 1 fully simplified = {s}");
let zero = ctx.int(0);
let is_zero = simplified.equals(&zero);
assert_eq!(
is_zero,
Some(true),
"(x+1)^2 - x^2 - 2x - 1 should be 0, got: {s}"
);
}
#[test]
fn assume_positive_implies_real() {
let ctx = Context::new();
let x = ctx.symbol_with("x", &[Assumption::Positive]);
assert_eq!(x.is_positive(), Some(true));
assert_eq!(x.is_real(), Some(true));
assert_eq!(x.is_negative(), Some(false));
}
#[test]
fn assume_negative_implies_real() {
let ctx = Context::new();
let x = ctx.symbol_with("x", &[Assumption::Negative]);
assert_eq!(x.is_negative(), Some(true));
assert_eq!(x.is_real(), Some(true));
assert_eq!(x.is_positive(), Some(false));
}
#[test]
fn assume_integer_implies_rational_real() {
let ctx = Context::new();
let n = ctx.symbol_with("n", &[Assumption::Integer]);
assert_eq!(ctx.query(&n, Props::INTEGER), Some(true));
assert_eq!(ctx.query(&n, Props::RATIONAL), Some(true));
assert_eq!(ctx.query(&n, Props::REAL), Some(true));
}
#[test]
fn refine_abs_positive() {
let ctx = Context::new();
let x = ctx.symbol_with("x", &[Assumption::Positive]);
let expr = x.abs();
let refined = expr.refine();
assert_eq!(
fmt(&refined),
"x",
"abs(x) with positive x should refine to x, got: {}",
fmt(&refined)
);
}
#[test]
fn refine_abs_negative() {
let ctx = Context::new();
let x = ctx.symbol_with("x", &[Assumption::Negative]);
let expr = x.abs();
let refined = expr.refine();
let s = fmt(&refined);
assert_eq!(
s, "-x",
"abs(x) with negative x should refine to -x, got: {s}"
);
}
#[test]
fn refine_with_temporary_assumptions() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert!(x.is_positive().is_none());
let expr = x.abs();
let refined = expr.refine_with(&[(&x, Assumption::Positive)]);
assert_eq!(
fmt(&refined),
"x",
"abs(x) with temp positive assumption should be x, got: {}",
fmt(&refined)
);
assert!(x.is_positive().is_none());
}
#[test]
fn refine_sqrt_x_squared_negative_x() {
let ctx = Context::new();
let x = ctx.symbol_with("x", &[Assumption::Negative]);
let expr = x.powi(2).sqrt();
let refined = expr.refine();
let s = fmt(&refined);
eprintln!("sqrt(x²) with x<0 refined to: {s}");
let val = refined.subs_i64(&x, -5).eval().eval_f64();
match val {
Ok(v) => assert!((v - 5.0).abs() < 1e-10, "sqrt((-5)²) should be 5, got {v}"),
Err(e) => panic!("eval failed: {e}"),
}
}
#[test]
fn assume_nonneg_and_nonpositive_implies_zero() {
let ctx = Context::new();
let x = ctx.symbol_with("x", &[Assumption::NonNegative, Assumption::NonPositive]);
assert_eq!(ctx.query(&x, Props::ZERO), Some(true));
}
#[test]
fn simplify_preserves_value_trig_heavy() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.sin().powi(4) + &x.cos().powi(4);
let simplified = expr.simplify();
assert!(
numerical_eq_1var(&expr, &simplified, &x, &[1, 2, 3, 4, 5], 1e-10),
"sin⁴+cos⁴ simplify broke numerical equivalence, simplified to: {}",
fmt(&simplified)
);
}
#[test]
fn full_simplify_polynomial_identity() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expanded = (&x + 1).powi(3);
let manual = &(&(&x.powi(3) + &(&x.powi(2) * 3)) + &(&x * 3)) + 1;
let diff = &expanded - &manual;
let simplified = diff.simplify();
let s = fmt(&simplified);
assert_eq!(
s, "0",
"(x+1)^3 minus expansion should full_simplify to 0, got: {s}"
);
}
#[test]
fn expand_then_simplify_restores() {
let ctx = Context::new();
let x = ctx.symbol("x");
let original = (&x + 1).powi(2);
let expanded = original.expand();
let s = fmt(&expanded);
assert!(
s.contains("x^2"),
"expanded (x+1)^2 should contain x^2, got: {s}"
);
for p in [0, 1, 2, -1, -2] {
let v1 = original.subs_i64(&x, p).eval().eval_f64();
let v2 = expanded.subs_i64(&x, p).eval().eval_f64();
match (v1, v2) {
(Ok(a), Ok(b)) => assert!(
(a - b).abs() < 1e-10,
"(x+1)^2 expand broke at x={p}: {a} vs {b}"
),
_ => panic!("eval failed at x={p}"),
}
}
}
#[test]
#[allow(clippy::erasing_op)] fn simplify_zero_times_anything() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x * 0;
let s = fmt(&expr);
assert_eq!(s, "0", "x*0 should be 0, got: {s}");
}
#[test]
fn simplify_one_times_anything() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x * 1;
let s = fmt(&expr);
assert_eq!(s, "x", "x*1 should be x, got: {s}");
}
#[test]
fn simplify_add_zero() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x + 0;
let s = fmt(&expr);
assert_eq!(s, "x", "x+0 should be x, got: {s}");
}
#[test]
fn trig_expand_cos_2x_identity() {
let ctx = Context::new();
let x = ctx.symbol("x");
let cos_2x = (&x * 2).cos();
let expanded = cos_2x.expand_trig();
assert!(
numerical_eq_1var(&cos_2x, &expanded, &x, &[1, 2, 3, 4], 1e-10),
"cos(2x) expand_trig broke, result: {}",
fmt(&expanded)
);
}
#[test]
fn trig_combine_then_expand_roundtrip() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.sin() * &x.cos();
let combined = expr.trig_combine();
let re_expanded = combined.expand_trig();
for p in [1, 2, 3] {
let v_orig = expr.subs_i64(&x, p).eval().eval_f64();
let v_comb = combined.subs_i64(&x, p).eval().eval_f64();
let v_reex = re_expanded.subs_i64(&x, p).eval().eval_f64();
match (v_orig, v_comb, v_reex) {
(Ok(a), Ok(b), Ok(c)) => {
assert!(
(a - b).abs() < 1e-10,
"trig_combine broke at x={p}: {a} vs {b}"
);
assert!(
(a - c).abs() < 1e-10,
"expand_trig(trig_combine(...)) broke at x={p}: {a} vs {c}"
);
}
_ => eprintln!("eval issue at x={p}"),
}
}
}
#[test]
fn smart_simplify_pythagorean() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.sin().powi(2) + &x.cos().powi(2);
let result = expr.simplify();
assert_eq!(
fmt(&result),
"1",
"smart_simplify should get sin²+cos²=1, got: {}",
fmt(&result)
);
}
#[test]
fn log_expand_nested_deep() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let expr = (&x.powi(2) * &y).ln();
let expanded = expr.expand_log();
let s = fmt(&expanded);
eprintln!("ln(x²*y) expand_log = {s}");
let expanded2 = expanded.expand_log();
let s2 = fmt(&expanded2);
eprintln!("ln(x²*y) expand_log x2 = {s2}");
assert!(
s2.contains("ln(x)") && s2.contains("ln(y)"),
"ln(x²*y) should fully expand, got: {s2}"
);
}
#[test]
fn eval_trig_special_values() {
let ctx = Context::new();
let zero = ctx.int(0);
let pi = ctx.pi();
let s0 = zero.sin().eval();
assert_eq!(fmt(&s0), "0", "sin(0) should be 0, got: {}", fmt(&s0));
let c0 = zero.cos().eval();
assert_eq!(fmt(&c0), "1", "cos(0) should be 1, got: {}", fmt(&c0));
let sp = pi.sin().eval();
assert_eq!(fmt(&sp), "0", "sin(π) should be 0, got: {}", fmt(&sp));
let cp = pi.cos().eval();
assert_eq!(fmt(&cp), "-1", "cos(π) should be -1, got: {}", fmt(&cp));
}
#[test]
fn eval_exp_special_values() {
let ctx = Context::new();
let zero = ctx.int(0);
let one = ctx.int(1);
let e0 = zero.exp().eval();
assert_eq!(fmt(&e0), "1", "exp(0) should be 1, got: {}", fmt(&e0));
let l1 = one.ln().eval();
assert_eq!(fmt(&l1), "0", "ln(1) should be 0, got: {}", fmt(&l1));
}
#[test]
fn simplify_double_negative() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = -(&(-&x));
let s = fmt(&expr);
eprintln!("-(-x) = {s}");
assert_eq!(s, "x", "-(-x) should be x, got: {s}");
}
#[test]
fn expand_large_power_does_not_panic() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (&x + 1).powi(15);
let expanded = expr.expand();
let val = expanded.subs_i64(&x, 1).eval().eval_f64();
match val {
Ok(v) => assert!(
(v - 32768.0).abs() < 1.0,
"(x+1)^15 at x=1 should be 2^15=32768, got {v}"
),
Err(e) => panic!("eval failed: {e}"),
}
}
#[test]
fn simplify_x_minus_x_is_zero() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x - &x;
let s = fmt(&expr);
assert_eq!(s, "0", "x-x should be 0, got: {s}");
}
#[test]
fn simplify_x_div_x_is_one() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x / &x;
let simplified = expr.simplify();
let s = fmt(&simplified);
assert_eq!(s, "1", "x/x should simplify to 1, got: {s}");
}
#[test]
fn fu_simplification() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.sin().powi(2) + &x.cos().powi(2);
let result = expr.fu();
let s = fmt(&result);
eprintln!("fu(sin²+cos²) = {s}");
assert!(
numerical_eq_1var(&expr, &result, &x, &[1, 2, 3], 1e-10),
"fu() broke numerical equivalence: {s}"
);
}
#[test]
fn trig_expand_handles_negative_argument() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (-&x).sin();
let simplified = expr.simplify();
let s = fmt(&simplified);
eprintln!("sin(-x) simplified = {s}");
assert!(
numerical_eq_1var(&expr, &simplified, &x, &[1, 2, 3], 1e-10),
"sin(-x) simplify broke numerical equivalence: {s}"
);
}
#[test]
fn cos_negative_argument() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (-&x).cos();
let simplified = expr.simplify();
let s = fmt(&simplified);
eprintln!("cos(-x) simplified = {s}");
assert!(
numerical_eq_1var(&expr, &simplified, &x, &[1, 2, 3], 1e-10),
"cos(-x) simplify broke numerical equivalence: {s}"
);
}
#[test]
fn log_combine_handles_subtraction() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let expr = &a.ln() - &b.ln();
let combined = expr.log_combine();
let s = fmt(&combined);
eprintln!("ln(a)-ln(b) log_combine = {s}");
let v_orig = expr.subs_i64(&a, 6).subs_i64(&b, 3).eval().eval_f64();
let v_comb = combined.subs_i64(&a, 6).subs_i64(&b, 3).eval().eval_f64();
match (v_orig, v_comb) {
(Ok(o), Ok(c)) => assert!(
(o - c).abs() < 1e-10,
"ln(a)-ln(b) log_combine broke: {o} vs {c}"
),
_ => eprintln!("eval issue, may be structural"),
}
}
#[test]
fn power_simplify_numeric_bases() {
let ctx = Context::new();
let expr = ctx.int(2).powi(3);
let evaled = expr.eval();
assert_eq!(fmt(&evaled), "8", "2^3 should be 8, got: {}", fmt(&evaled));
}
#[test]
fn power_simplify_rational_exponent() {
let ctx = Context::new();
let four = ctx.int(4);
let result = four.sqrt().eval();
assert_eq!(
fmt(&result),
"2",
"sqrt(4) should be 2, got: {}",
fmt(&result)
);
}
#[test]
fn power_simplify_27_cbrt() {
let ctx = Context::new();
let expr = ctx.int(27).pow(&ctx.rational(1, 3));
let evaled = expr.eval();
let s = fmt(&evaled);
eprintln!("27^(1/3) = {s}");
let val = evaled.eval_f64();
match val {
Ok(v) => assert!((v - 3.0).abs() < 1e-10, "27^(1/3) should be 3, got {v}"),
Err(e) => panic!("eval failed: {e}"),
}
}
#[test]
fn refine_floor_of_integer() {
let ctx = Context::new();
let n = ctx.symbol_with("n", &[Assumption::Integer]);
let expr = n.floor();
let refined = expr.refine();
assert_eq!(
fmt(&refined),
"n",
"floor(integer) should refine to n, got: {}",
fmt(&refined)
);
}
#[test]
fn constants_are_real() {
let ctx = Context::new();
let pi = ctx.pi();
let e = ctx.e();
assert_eq!(pi.is_real(), Some(true), "pi should be real");
assert_eq!(e.is_real(), Some(true), "e should be real");
}
#[test]
fn imaginary_unit_properties() {
let ctx = Context::new();
let i = ctx.i_unit();
assert_eq!(i.is_real(), Some(false), "i should not be real");
}
#[test]
fn pow_symbolic_double_pow_xa_b_simplify() {
let ctx = Context::new();
let x = ctx.symbol("x");
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let expr = x.pow(&a).pow(&b);
let simplified = expr.simplify();
let v_orig = expr
.subs_i64(&x, 2)
.subs_i64(&a, 3)
.subs_i64(&b, 2)
.eval()
.eval_f64();
let v_simp = simplified
.subs_i64(&x, 2)
.subs_i64(&a, 3)
.subs_i64(&b, 2)
.eval()
.eval_f64();
match (v_orig, v_simp) {
(Ok(o), Ok(s)) => assert!(
(o - s).abs() < 1e-6,
"(x^a)^b simplify broke numerical equiv: {o} vs {s}, form={}",
fmt(&simplified)
),
_ => eprintln!("(x^a)^b eval issue"),
}
}
#[test]
fn pow_fractional_of_negative_number() {
let ctx = Context::new();
let expr = ctx.int(-8).pow(&ctx.rational(1, 3));
let evaled = expr.eval();
let s = fmt(&evaled);
eprintln!("(-8)^(1/3) = {s}");
let val = evaled.eval_complex64();
match val {
Ok((re, im)) => {
eprintln!("(-8)^(1/3) complex = ({re}, {im})");
let mag = (re * re + im * im).sqrt();
assert!(
(mag - 2.0).abs() < 1e-9,
"(-8)^(1/3) magnitude should be 2, got {mag}"
);
}
Err(e) => eprintln!("(-8)^(1/3) eval failed: {e}"),
}
}
#[test]
fn pow_zero_base_positive_exponent() {
let ctx = Context::new();
let expr = ctx.int(0).powi(5);
let evaled = expr.eval();
assert_eq!(fmt(&evaled), "0", "0^5 should be 0, got: {}", fmt(&evaled));
}
#[test]
fn pow_zero_base_zero_exponent() {
let ctx = Context::new();
let expr = ctx.int(0).powi(0);
let evaled = expr.eval();
let s = fmt(&evaled);
eprintln!("0^0 = {s}");
}
#[test]
fn pow_one_base_any_exponent() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = ctx.int(1).pow(&x);
let simplified = expr.simplify();
let s = fmt(&simplified);
assert_eq!(s, "1", "1^x should be 1, got: {s}");
}
#[test]
fn log_of_zero_is_neg_infinity_or_error() {
let ctx = Context::new();
let expr = ctx.int(0).ln();
let evaled = expr.eval();
let s = fmt(&evaled);
eprintln!("ln(0) = {s}");
let val = evaled.eval_f64();
match val {
Ok(v) => {
assert!(
v.is_infinite() && v < 0.0 || v < -1e10,
"ln(0) should be -∞, got {v}"
);
}
Err(_) => {
eprintln!("ln(0) eval returned error (acceptable)");
}
}
}
#[test]
fn log_exp_without_real_assumption() {
let ctx = Context::new();
let x = ctx.symbol("x"); let expr = x.exp().ln();
let simplified = expr.simplify();
let s = fmt(&simplified);
eprintln!("ln(exp(x)) no assumption = {s}");
for p in [1, 2, -1, -3] {
let v_orig = expr.subs_i64(&x, p).eval().eval_f64();
let v_simp = simplified.subs_i64(&x, p).eval().eval_f64();
if let (Ok(o), Ok(s)) = (v_orig, v_simp) {
assert!(
(o - s).abs() < 1e-9,
"ln(exp(x)) simplify wrong at x={p}: {o} vs {s}"
)
}
}
}
#[test]
fn log_expand_respects_negative_exponents() {
let ctx = Context::new();
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let expr = (&x / &y).ln();
let expanded = expr.expand_log();
let s = fmt(&expanded);
eprintln!("ln(x/y) expand_log = {s}");
assert!(
s.contains("ln(x)") && s.contains("ln(y)"),
"ln(x/y) should expand to ln(x)-ln(y), got: {s}"
);
let v_orig = expr.subs_i64(&x, 10).subs_i64(&y, 2).eval().eval_f64();
let v_exp = expanded.subs_i64(&x, 10).subs_i64(&y, 2).eval().eval_f64();
match (v_orig, v_exp) {
(Ok(o), Ok(e)) => assert!((o - e).abs() < 1e-10, "ln(x/y) expand broke: {o} vs {e}"),
_ => eprintln!("ln(x/y) eval issue"),
}
}
#[test]
fn trig_half_angle_sin_squared() {
let ctx = Context::new();
let x = ctx.symbol("x");
let half = ctx.rational(1, 2);
let sin_half = (&x * &half).sin();
let sin_half_sq = sin_half.powi(2);
let identity_rhs = &(&ctx.int(1) - &x.cos()) * ½
for p in [1, 2, 3, 4] {
let v_lhs = sin_half_sq.subs_i64(&x, p).eval().eval_f64();
let v_rhs = identity_rhs.subs_i64(&x, p).eval().eval_f64();
match (v_lhs, v_rhs) {
(Ok(a), Ok(b)) => assert!(
(a - b).abs() < 1e-10,
"sin²(x/2) != (1-cos(x))/2 at x={p}: {a} vs {b}"
),
_ => panic!("eval failed at x={p}"),
}
}
}
#[test]
fn trig_tan_squared_plus_one() {
let ctx = Context::new();
let x = ctx.symbol("x");
let lhs = &ctx.int(1) + &x.tan().powi(2);
let rhs = &ctx.int(1) / &x.cos().powi(2);
for p in [1, 2, 4] {
let v_lhs = lhs.subs_i64(&x, p).eval().eval_f64();
let v_rhs = rhs.subs_i64(&x, p).eval().eval_f64();
match (v_lhs, v_rhs) {
(Ok(a), Ok(b)) => assert!(
(a - b).abs() < 1e-9,
"1+tan²(x) != 1/cos²(x) at x={p}: {a} vs {b}"
),
_ => panic!("eval failed at x={p}"),
}
}
}
#[test]
fn trig_sin_pi_over_4_eval() {
let ctx = Context::new();
let expr = (&ctx.pi() / 4).sin();
let evaled = expr.eval();
let val = evaled.eval_f64();
match val {
Ok(v) => assert!(
(v - std::f64::consts::FRAC_1_SQRT_2).abs() < 1e-10,
"sin(π/4) should be √2/2 ≈ 0.7071, got {v}"
),
Err(e) => panic!("sin(π/4) eval failed: {e}"),
}
}
#[test]
fn trig_simplify_does_not_increase_complexity() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = x.sin();
let simplified = expr.simplify();
let s = fmt(&simplified);
assert_eq!(s, "sin(x)", "sin(x) simplify should stay sin(x), got: {s}");
}
#[test]
fn trig_simplify_1_minus_sin_squared() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &ctx.int(1) - &x.sin().powi(2);
let simplified = expr.simplify();
assert!(
numerical_eq_1var(&expr, &simplified, &x, &[1, 2, 3, 4], 1e-10),
"1-sin²(x) simplify broke: {}",
fmt(&simplified)
);
}
#[test]
fn trig_expand_sin_5x_fully() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (&x * 5).sin();
let expanded = expr.expand_trig();
let s = fmt(&expanded);
assert!(
!s.contains("5*x") && !s.contains("4*x") && !s.contains("3*x") && !s.contains("2*x"),
"sin(5x) not fully expanded: {s}"
);
assert!(
numerical_eq_1var(&expr, &expanded, &x, &[1, 2, 3], 1e-9),
"sin(5x) expand broke numerical equiv: {s}"
);
}
#[test]
fn trig_sinh_of_zero() {
let ctx = Context::new();
let expr = ctx.int(0).sinh();
let evaled = expr.eval();
let val = evaled.eval_f64();
match val {
Ok(v) => assert!(v.abs() < 1e-15, "sinh(0) should be 0, got {v}"),
Err(e) => panic!("sinh(0) eval failed: {e}"),
}
}
#[test]
fn trig_cosh_of_zero() {
let ctx = Context::new();
let expr = ctx.int(0).cosh();
let evaled = expr.eval();
let val = evaled.eval_f64();
match val {
Ok(v) => assert!((v - 1.0).abs() < 1e-15, "cosh(0) should be 1, got {v}"),
Err(e) => panic!("cosh(0) eval failed: {e}"),
}
}
#[test]
fn expand_binomial_zero_exponent() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let expr = (&a + &b).powi(0);
let s = fmt(&expr);
let expanded = expr.expand();
let se = fmt(&expanded);
assert!(
s == "1" || se == "1",
"(a+b)^0 should be 1, raw={s}, expanded={se}"
);
}
#[test]
fn expand_binomial_one_exponent() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let expr = (&a + &b).powi(1);
let expanded = expr.expand();
let s = fmt(&expanded);
assert!(
s.contains('a') && s.contains('b'),
"(a+b)^1 should be a+b, got: {s}"
);
let val = expanded.subs_i64(&a, 3).subs_i64(&b, 7).eval().eval_f64();
match val {
Ok(v) => assert!((v - 10.0).abs() < 1e-10, "expected 10, got {v}"),
Err(e) => panic!("eval failed: {e}"),
}
}
#[test]
fn expand_product_of_three_sums() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let c = ctx.symbol("c");
let d = ctx.symbol("d");
let e = ctx.symbol("e");
let f = ctx.symbol("f");
let expr = &(&(&a + &b) * &(&c + &d)) * &(&e + &f);
let expanded = expr.expand();
let val = expanded
.subs_i64(&a, 1)
.subs_i64(&b, 1)
.subs_i64(&c, 1)
.subs_i64(&d, 1)
.subs_i64(&e, 1)
.subs_i64(&f, 1)
.eval()
.eval_f64();
match val {
Ok(v) => assert!((v - 8.0).abs() < 1e-10, "expected 8, got {v}"),
Err(e) => panic!("eval failed: {e}"),
}
}
#[test]
fn expand_a_plus_b_pow20_does_not_panic() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let expr = (&a + &b).powi(20);
let expanded = expr.expand();
let val = expanded.subs_i64(&a, 1).subs_i64(&b, 1).eval().eval_f64();
match val {
Ok(v) => assert!(
(v - 1048576.0).abs() < 1.0,
"(a+b)^20 at (1,1) should be 1048576, got {v}"
),
Err(e) => panic!("eval failed: {e}"),
}
}
#[test]
fn rewrite_tan_as_exp() {
let ctx = Context::new();
let x = ctx.symbol("x");
let tan_x = x.tan();
let rewritten = tan_x.rewrite_as_exp();
let s = fmt(&rewritten);
eprintln!("tan(x) rewrite_as_exp = {s}");
assert!(
s.contains("exp"),
"tan(x) rewrite_as_exp should contain exp, got: {s}"
);
let v_orig = tan_x.subs_i64(&x, 1).eval().eval_f64();
let v_rew = rewritten.subs_i64(&x, 1).eval().eval_complex64();
match (v_orig, v_rew) {
(Ok(o), Ok((re, im))) => {
assert!(
(o - re).abs() < 1e-8 && im.abs() < 1e-8,
"tan(x) rewrite broke: {o} vs ({re}+{im}i)"
);
}
_ => eprintln!("tan rewrite eval issue"),
}
}
#[test]
fn rewrite_nested_sin_cos_as_exp() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.sin() + &x.cos();
let rewritten = expr.rewrite_as_exp();
let s = fmt(&rewritten);
eprintln!("sin(x)+cos(x) as exp = {s}");
assert!(
s.contains("exp"),
"sin(x)+cos(x) rewrite should contain exp, got: {s}"
);
assert!(
!s.contains("sin(") && !s.contains("cos("),
"rewrite_as_exp should eliminate trig, got: {s}"
);
}
#[test]
fn rewrite_exp_a_plus_ib_as_trig() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let i = ctx.i_unit();
let expr = (&a + &(&i * &b)).exp();
let rewritten = expr.rewrite_as_trig();
let s = fmt(&rewritten);
eprintln!("exp(a+ib) as trig = {s}");
assert!(
s.contains("cos") || s.contains("sin"),
"exp(a+ib) rewrite should produce trig, got: {s}"
);
}
#[test]
fn equals_distributive() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let c = ctx.symbol("c");
let lhs = &a * &(&b + &c);
let rhs = &(&a * &b) + &(&a * &c);
let result = lhs.equals(&rhs);
assert_eq!(
result,
Some(true),
"a*(b+c) should equal a*b+a*c, got {:?}",
result
);
}
#[test]
fn equals_nested_expand() {
let ctx = Context::new();
let x = ctx.symbol("x");
let lhs = (&x + 1).powi(3);
let rhs = &(&(&x.powi(3) + &(&x.powi(2) * 3)) + &(&x * 3)) + 1;
let result = lhs.equals(&rhs);
assert_eq!(
result,
Some(true),
"(x+1)^3 should equal x^3+3x^2+3x+1, got {:?}",
result
);
}
#[test]
fn equals_subtraction_order() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let lhs = &a - &b;
let rhs = &b - &a;
let result = lhs.equals(&rhs);
assert_ne!(
result,
Some(true),
"(a-b) should not be proven equal to (b-a)"
);
}
#[test]
fn assume_positive_sum_is_positive() {
let ctx = Context::new();
let x = ctx.symbol_with("x", &[Assumption::Positive]);
let y = ctx.symbol_with("y", &[Assumption::Positive]);
let sum = &x + &y;
assert_eq!(
sum.is_positive(),
Some(true),
"positive + positive should be positive"
);
}
#[test]
fn assume_positive_times_negative_is_negative() {
let ctx = Context::new();
let x = ctx.symbol_with("x", &[Assumption::Positive]);
let y = ctx.symbol_with("y", &[Assumption::Negative]);
let prod = &x * &y;
assert_eq!(
prod.is_negative(),
Some(true),
"positive * negative should be negative"
);
}
#[test]
fn assume_integer_squared_is_integer() {
let ctx = Context::new();
let n = ctx.symbol_with("n", &[Assumption::Integer]);
let n2 = n.powi(2);
assert_eq!(
ctx.query(&n2, Props::INTEGER),
Some(true),
"integer² should be integer"
);
}
#[test]
fn assume_real_x_squared_is_nonneg() {
let ctx = Context::new();
let x = ctx.symbol_with("x", &[Assumption::Real]);
let x2 = x.powi(2);
let result = ctx.query(&x2, Props::NONNEGATIVE);
eprintln!("x² nonneg for real x? {:?}", result);
if result != Some(true) {
eprintln!("NOTE: assumption system does not infer x² ≥ 0 for real x");
}
}
#[test]
fn refine_neg_one_to_even_power() {
let ctx = Context::new();
let n = ctx.symbol_with("n", &[Assumption::Integer]);
let two_n = &n * 2;
let expr = ctx.int(-1).pow(&two_n);
let refined = expr.refine();
let s = fmt(&refined);
eprintln!("(-1)^(2n) refined = {s}");
for p in [0, 1, 2, 3, -1] {
let val = expr.subs_i64(&n, p).eval().eval_f64();
match val {
Ok(v) => assert!((v - 1.0).abs() < 1e-10, "(-1)^(2*{p}) should be 1, got {v}"),
Err(e) => eprintln!("(-1)^(2*{p}) eval failed: {e}"),
}
}
}
#[test]
fn simplify_is_idempotent() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.sin().powi(2) + &x.cos().powi(2) + &x;
let s1 = expr.simplify();
let s2 = s1.simplify();
assert_eq!(
fmt(&s1),
fmt(&s2),
"simplify should be idempotent: first={}, second={}",
fmt(&s1),
fmt(&s2)
);
}
#[test]
fn full_simplify_is_idempotent() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &(&x + 1).powi(2) - &x.powi(2) - &(&x * 2) - 1;
let s1 = expr.simplify();
let s2 = s1.simplify();
assert_eq!(
fmt(&s1),
fmt(&s2),
"full_simplify should be idempotent: first={}, second={}",
fmt(&s1),
fmt(&s2)
);
}
#[test]
fn expand_is_idempotent() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (&x + 1).powi(3);
let e1 = expr.expand();
let e2 = e1.expand();
assert_eq!(
fmt(&e1),
fmt(&e2),
"expand should be idempotent: first={}, second={}",
fmt(&e1),
fmt(&e2)
);
}
#[test]
fn simplify_mixed_hyp_trig_no_panic() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.sinh() + &x.sin();
let simplified = expr.simplify();
assert!(
numerical_eq_1var(&expr, &simplified, &x, &[1, 2, 3], 1e-10),
"mixed sinh+sin simplify broke: {}",
fmt(&simplified)
);
}
#[test]
fn simplify_exp_sum_rule() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let expr = &a.exp() * &b.exp();
let simplified = expr.simplify();
let s = fmt(&simplified);
eprintln!("exp(a)*exp(b) simplified = {s}");
let v_orig = expr.subs_i64(&a, 1).subs_i64(&b, 2).eval().eval_f64();
let v_simp = simplified.subs_i64(&a, 1).subs_i64(&b, 2).eval().eval_f64();
match (v_orig, v_simp) {
(Ok(o), Ok(s)) => assert!(
(o - s).abs() / o.abs().max(1.0) < 1e-10,
"exp(a)*exp(b) simplify broke: {o} vs {s}"
),
_ => eprintln!("exp product eval issue"),
}
}
#[test]
fn simplify_large_expression_no_stack_overflow() {
let ctx = Context::new();
let mut expr = ctx.symbol("x");
for _ in 0..100 {
expr = &expr + 1;
}
let simplified = expr.simplify();
let val = simplified.subs_i64(&ctx.symbol("x"), 0).eval().eval_f64();
match val {
Ok(v) => assert!(
(v - 100.0).abs() < 1e-10,
"x + 100 at x=0 should be 100, got {v}"
),
Err(e) => panic!("eval failed: {e}"),
}
}
#[test]
fn trig_expand_cos_negative_2x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let cos_neg2x = (&x * -2).cos();
let cos_2x = (&x * 2).cos();
for p in [1, 2, 3] {
let v1 = cos_neg2x.subs_i64(&x, p).eval().eval_f64();
let v2 = cos_2x.subs_i64(&x, p).eval().eval_f64();
match (v1, v2) {
(Ok(a), Ok(b)) => assert!(
(a - b).abs() < 1e-10,
"cos(-2x) != cos(2x) at x={p}: {a} vs {b}"
),
_ => panic!("eval failed"),
}
}
}
#[test]
fn log_combine_three_logs() {
let ctx = Context::new();
let a = ctx.symbol("a");
let b = ctx.symbol("b");
let c = ctx.symbol("c");
let expr = &(&a.ln() + &b.ln()) + &c.ln();
let combined = expr.log_combine();
let s = fmt(&combined);
let ln_count = s.matches("ln(").count();
assert_eq!(
ln_count, 1,
"3 ln terms should combine to 1, got {ln_count}: {s}"
);
let v_orig = expr
.subs_i64(&a, 2)
.subs_i64(&b, 3)
.subs_i64(&c, 5)
.eval()
.eval_f64();
let v_comb = combined
.subs_i64(&a, 2)
.subs_i64(&b, 3)
.subs_i64(&c, 5)
.eval()
.eval_f64();
match (v_orig, v_comb) {
(Ok(o), Ok(c)) => assert!((o - c).abs() < 1e-10, "3-log combine broke: {o} vs {c}"),
_ => eprintln!("3-log combine eval issue"),
}
}
#[test]
fn eval_decimal_sqrt2_many_digits() {
let ctx = Context::new();
let sqrt2 = ctx.int(2).sqrt();
let result = sqrt2.eval_decimal(20);
match result {
Ok(s) => {
eprintln!("sqrt(2) to 20 digits: {s}");
assert!(
s.starts_with("1.41421356237"),
"sqrt(2) should start with 1.41421356237, got: {s}"
);
}
Err(e) => panic!("eval_decimal failed: {e}"),
}
}
#[test]
fn bug_sin_negative_arg_not_simplified() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (-&x).sin();
let simplified = expr.simplify();
let s = fmt(&simplified);
assert_eq!(
s, "-sin(x)",
"BUG: sin(-x) should simplify to -sin(x), got: {s}"
);
}
#[test]
fn bug_neg1_to_even_power_not_refined() {
let ctx = Context::new();
let n = ctx.symbol_with("n", &[Assumption::Integer]);
let two_n = &n * 2;
let expr = ctx.int(-1).pow(&two_n);
let refined = expr.refine();
let s = fmt(&refined);
assert_eq!(
s, "1",
"BUG: (-1)^(2n) with integer n should refine to 1, got: {s}"
);
}
#[test]
fn bug_cosh2_minus_sinh2_not_simplified_symbolically() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.cosh().powi(2) - &x.sinh().powi(2);
let simplified = expr.simplify();
let s = fmt(&simplified);
assert_eq!(s, "1", "cosh²(x) - sinh²(x) should simplify to 1, got: {s}");
}
#[test]
fn bug_equals_misses_trig_identity() {
let ctx = Context::new();
let x = ctx.symbol("x");
let lhs = &x.sin().powi(2) + &x.cos().powi(2);
let one = ctx.int(1);
let result = lhs.equals(&one);
assert_eq!(
result,
Some(true),
"BUG: equals() should detect sin²+cos²=1, got {:?}",
result
);
}
#[test]
fn bug_rewrite_roundtrip_not_clean() {
let ctx = Context::new();
let x = ctx.symbol("x");
let sin_x = x.sin();
let as_exp = sin_x.rewrite_as_exp();
let back = as_exp.rewrite_as_trig();
let simplified_back = back.simplify();
let s = fmt(&simplified_back);
eprintln!("[INFO] sin(x)->exp->trig->simplify = {s}");
assert!(
numerical_eq_1var(&sin_x, &simplified_back, &x, &[1, 2, 3, 4], 1e-9),
"rewrite roundtrip broke numerical equivalence: sin(x) vs {s}"
);
}
#[test]
fn bug_ln_exp_no_assumption_correctness() {
let ctx = Context::new();
let x = ctx.symbol("x"); let expr = x.exp().ln();
let simplified = expr.simplify();
let s = fmt(&simplified);
eprintln!("[BUG?] ln(exp(x)) no assumptions = {s}");
if s == "x" {
eprintln!(
"NOTE: ln(exp(x)) simplified to x without Real assumption — \
this is only correct on the principal branch"
);
}
for p in [1, 2, -1, -3] {
let v_orig = expr.subs_i64(&x, p).eval().eval_f64();
let v_simp = simplified.subs_i64(&x, p).eval().eval_f64();
if let (Ok(o), Ok(s)) = (v_orig, v_simp) {
assert!(
(o - s).abs() < 1e-9,
"ln(exp(x)) wrong at x={p}: {o} vs {s}"
)
}
}
}
#[test]
fn bug_rewrite_sin_cos_sum_as_exp_messy() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &x.sin() + &x.cos();
let rewritten = expr.rewrite_as_exp();
let s = fmt(&rewritten);
eprintln!("[BUG?] sin(x)+cos(x) as exp = {s}");
assert!(
!s.contains("sin(") && !s.contains("cos("),
"rewrite_as_exp should eliminate all trig, got: {s}"
);
assert!(
s.contains("exp"),
"rewrite_as_exp result should contain exp, got: {s}"
);
let has_double_neg = s.contains("*-") || s.contains("--");
if has_double_neg {
eprintln!("NOTE: rewrite_as_exp produces messy double-negative structure: {s}");
}
for p in [1, 2, 3] {
let v_orig = expr.subs_i64(&x, p).eval().eval_f64();
let v_rew = rewritten.subs_i64(&x, p).eval().eval_complex64();
match (v_orig, v_rew) {
(Ok(o), Ok((re, im))) => {
assert!(
(o - re).abs() < 1e-9 && im.abs() < 1e-9,
"rewrite broke numerical equiv at x={p}: {o} vs ({re}+{im}i)"
);
}
_ => eprintln!("eval issue at x={p}"),
}
}
}
#[test]
fn bug_3sin2_plus_3cos2_not_simplified_to_3() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &(&x.sin().powi(2) * 3) + &(&x.cos().powi(2) * 3);
let simplified = expr.simplify();
let s = fmt(&simplified);
eprintln!("[INFO] 3*sin²+3*cos² simplified = {s}");
assert!(
numerical_eq_1var(&expr, &simplified, &x, &[1, 2, 3, 4], 1e-10),
"3*sin²+3*cos² simplify broke numerical equivalence: {s}"
);
}
#[test]
fn bug_x_squared_nonneg_not_inferred_for_real() {
let ctx = Context::new();
let x = ctx.symbol_with("x", &[Assumption::Real]);
let x2 = x.powi(2);
let result = ctx.query(&x2, Props::NONNEGATIVE);
assert_eq!(
result,
Some(true),
"x² should be nonnegative when x is real, got {:?}",
result
);
}
#[test]
fn bug_trig_simplify_1_minus_cos2_to_sin2() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &ctx.int(1) - &x.cos().powi(2);
let simplified = expr.simplify();
let s = fmt(&simplified);
eprintln!("[INFO] 1-cos²(x) simplified = {s}");
assert!(
numerical_eq_1var(&expr, &simplified, &x, &[1, 2, 3, 4], 1e-10),
"1-cos²(x) simplify broke numerical equivalence: {s}"
);
}
#[test]
fn bug_cos_neg_x_simplify_to_cos_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (-&x).cos();
let simplified = expr.simplify();
let s = fmt(&simplified);
assert_eq!(s, "cos(x)", "cos(-x) should simplify to cos(x), got: {s}");
}
#[test]
fn bug_tan_neg_x_should_be_neg_tan_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (-&x).tan();
let simplified = expr.simplify();
let s = fmt(&simplified);
assert_eq!(
s, "-tan(x)",
"BUG: tan(-x) should simplify to -tan(x), got: {s}"
);
}
#[test]
fn bug_sinh_neg_x_should_be_neg_sinh_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (-&x).sinh();
let simplified = expr.simplify();
let s = fmt(&simplified);
assert_eq!(
s, "-sinh(x)",
"BUG: sinh(-x) should simplify to -sinh(x), got: {s}"
);
}
#[test]
fn bug_cosh_neg_x_should_be_cosh_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = (-&x).cosh();
let simplified = expr.simplify();
let s = fmt(&simplified);
assert_eq!(
s, "cosh(x)",
"cosh(-x) should simplify to cosh(x), got: {s}"
);
}
#[test]
fn bug_exp_ln_should_always_simplify() {
let ctx = Context::new();
let x = ctx.symbol("x"); let expr = x.ln().exp();
let simplified = expr.simplify();
let s = fmt(&simplified);
assert_eq!(s, "x", "exp(ln(x)) should always simplify to x, got: {s}");
}
#[test]
fn bug_full_simplify_catches_scaled_pythagorean() {
let ctx = Context::new();
let x = ctx.symbol("x");
let expr = &(&x.sin().powi(2) * 3) + &(&x.cos().powi(2) * 3);
let result = expr.simplify();
let s = fmt(&result);
assert_eq!(
s, "3",
"full_simplify should reduce 3*sin²+3*cos² to 3, got: {s}"
);
}
#[test]
fn bug_simplify_sin4_plus_cos4() {
let ctx = Context::new();
let x = ctx.symbol("x");
let original = &x.sin().powi(4) + &x.cos().powi(4);
let simplified = original.simplify();
let full = original.simplify();
let s_orig = fmt(&original);
let s_simp = fmt(&simplified);
let s_full = fmt(&full);
eprintln!("[INFO] sin⁴+cos⁴ original = {s_orig}");
eprintln!("[INFO] sin⁴+cos⁴ simplify = {s_simp}");
eprintln!("[INFO] sin⁴+cos⁴ full_simp = {s_full}");
assert!(
s_full.len() <= s_orig.len(),
"full_simplify should not make sin⁴+cos⁴ larger: orig={s_orig}, full={s_full}"
);
assert!(
numerical_eq_1var(&original, &full, &x, &[1, 2, 3, 4], 1e-10),
"full_simplify broke sin⁴+cos⁴ numerical equivalence"
);
}
#[test]
fn bug_sqrt_x_squared_no_assumption_returns_abs() {
let ctx = Context::new();
let x = ctx.symbol("x"); let expr = x.powi(2).sqrt();
let simplified = expr.simplify();
let s = fmt(&simplified);
assert_ne!(
s, "x",
"BUG: sqrt(x²) without assumptions must NOT simplify to bare x \
(would be wrong for x<0), got: {s}"
);
}