symplex 0.17.0

Exact symbolic mathematics for Rust: calculus, summation, solving, linear algebra, transforms, compile-time dimensional analysis, and Rust/C code generation
Documentation
//! Integration tests for Wave K — simplification depth.
//!
//! Tests for `trigsimp`, `powsimp`, `rewrite_as_exp`, and `rewrite_as_trig`.

// ═══════════════════════════════════════════════════════════════════════════
// trigsimp
// ═══════════════════════════════════════════════════════════════════════════

use symplex::prelude::*;
#[test]
fn trigsimp_pythagorean() {
    let ctx = Context::new();
    symplex::syms!(ctx; x);
    let e = &x.sin().powi(2) + &x.cos().powi(2);
    assert_eq!(format!("{}", e.simplify_trig()), "1");
}

#[test]
fn trigsimp_in_larger_expr() {
    let ctx = Context::new();
    symplex::syms!(ctx; x);
    let e = &x.sin().powi(2) + &x.cos().powi(2) + &x;
    let result = e.simplify_trig();
    let s = format!("{result}");
    assert_eq!(s, "x + 1");
}

#[test]
fn trigsimp_pythagorean_plus_number() {
    let ctx = Context::new();
    symplex::syms!(ctx; x);
    let e = &x.sin().powi(2) + &x.cos().powi(2) + 5;
    assert_eq!(format!("{}", e.simplify_trig()), "6");
}

#[test]
fn trigsimp_leaves_bare_trig_alone() {
    let ctx = Context::new();
    symplex::syms!(ctx; x);
    let e = x.sin();
    let result = e.simplify_trig();
    let s = format!("{result}");
    assert_eq!(s, "sin(x)");
}

#[test]
fn trigsimp_preserves_numeric_value() {
    let ctx = Context::new();
    // After trigsimp the expression should evaluate to the same number.
    symplex::syms!(ctx; x);
    let e = &x.sin().powi(2) + &x.cos().powi(2) + &x;
    let val_before = e.subs(&x, &ctx.rational(7, 10)).eval_f64().unwrap();
    let result = e.simplify_trig();
    let val_after = result.subs(&x, &ctx.rational(7, 10)).eval_f64().unwrap();
    assert!(
        (val_before - val_after).abs() < 1e-10,
        "trigsimp should preserve value: {val_before} vs {val_after}"
    );
}

// ═══════════════════════════════════════════════════════════════════════════
// powsimp
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn powsimp_symbolic_exponents() {
    let ctx = Context::new();
    symplex::syms!(ctx; x, a, b);
    // x^a * x^b should become x^(a+b)
    let e = &x.pow(&a) * &x.pow(&b);
    let result = e.simplify_powers();
    let s = format!("{result}");
    // Should contain a+b in the exponent
    assert!(
        s.contains("a + b") || s.contains("b + a"),
        "powsimp should combine: {s}"
    );
}

#[test]
fn powsimp_three_factors() {
    let ctx = Context::new();
    symplex::syms!(ctx; x, a, b, c);
    let e = &(&x.pow(&a) * &x.pow(&b)) * &x.pow(&c);
    let result = e.simplify_powers();
    let s = format!("{result}");
    // All three exponents should be combined
    assert!(
        s.contains('a') && s.contains('b') && s.contains('c'),
        "powsimp should combine all three: {s}"
    );
    // Should be a single power, not a product of powers
    // Count how many '^' appear — should be exactly one
    let carets = s.matches('^').count();
    assert_eq!(carets, 1, "should be a single power: {s}");
}

#[test]
fn powsimp_different_bases_untouched() {
    let ctx = Context::new();
    symplex::syms!(ctx; x, y, a, b);
    let e = &x.pow(&a) * &y.pow(&b);
    let result = e.simplify_powers();
    let s = format!("{result}");
    // Both bases should still be present
    assert!(
        s.contains("x^a") && s.contains("y^b"),
        "different bases should be untouched: {s}"
    );
}

#[test]
fn powsimp_atom_unchanged() {
    let ctx = Context::new();
    symplex::syms!(ctx; x);
    let result = x.simplify_powers();
    assert_eq!(format!("{result}"), "x");
}

// ═══════════════════════════════════════════════════════════════════════════
// rewrite_as_exp
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn rewrite_sin_as_exp() {
    let ctx = Context::new();
    symplex::syms!(ctx; x);
    let e = x.sin();
    let result = e.rewrite_as_exp();
    let s = format!("{result}");
    // Should contain exp or E (the exponential base)
    assert!(
        s.contains("exp") || s.contains("E"),
        "rewrite should produce exponentials: {s}"
    );
}

#[test]
fn rewrite_cos_as_exp() {
    let ctx = Context::new();
    symplex::syms!(ctx; x);
    let e = x.cos();
    let result = e.rewrite_as_exp();
    let s = format!("{result}");
    assert!(
        s.contains("exp") || s.contains("E"),
        "rewrite should produce exponentials: {s}"
    );
}

#[test]
fn rewrite_as_exp_preserves_value() {
    let ctx = Context::new();
    symplex::syms!(ctx; x);
    let e = x.sin();
    let val = ctx.rational(7, 10);
    let orig_f = e.subs(&x, &val).eval_f64().unwrap();
    let rewritten = e.rewrite_as_exp();
    let rw_f = rewritten.subs(&x, &val).eval_f64();
    // The rewritten form involves complex exponentials, so evalf_f64
    // might fail (complex intermediate). Use evalf_complex64 instead.
    let rw_val: Result<Complex64, _> = rewritten.subs(&x, &val).eval_complex64();
    if let Ok(Complex64 { re, im }) = rw_val {
        assert!(
            im.abs() < 1e-10,
            "sin rewritten as exp should be real for real input: im={im}"
        );
        assert!(
            (orig_f - re).abs() < 1e-10,
            "rewrite should preserve value: {orig_f} vs {re}"
        );
    } else if let Ok(f) = rw_f {
        assert!(
            (orig_f - f).abs() < 1e-10,
            "rewrite should preserve value: {orig_f} vs {f}"
        );
    }
    // If both fail, the rewrite at least produced *something* — don't panic.
}

// ═══════════════════════════════════════════════════════════════════════════
// rewrite_as_trig
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn rewrite_exp_ix_as_trig() {
    let ctx = Context::new();
    symplex::syms!(ctx; x);
    let i = ctx.i_unit();
    let expr = (&i * &x).exp();
    let result = expr.rewrite_as_trig();
    let s = format!("{result}");
    // Should contain cos and sin (Euler's formula)
    assert!(
        s.contains("cos") && s.contains("sin"),
        "rewrite should produce trig: {s}"
    );
}

#[test]
fn rewrite_as_trig_atom_unchanged() {
    let ctx = Context::new();
    symplex::syms!(ctx; x);
    let result = x.rewrite_as_trig();
    assert_eq!(format!("{result}"), "x");
}

#[test]
fn rewrite_as_exp_atom_unchanged() {
    let ctx = Context::new();
    symplex::syms!(ctx; x);
    let result = x.rewrite_as_exp();
    assert_eq!(format!("{result}"), "x");
}

// ═══════════════════════════════════════════════════════════════════════════
// roundtrip: trig → exp → trig
// ═══════════════════════════════════════════════════════════════════════════

#[test]
fn rewrite_roundtrip_numerical() {
    let ctx = Context::new();
    // sin(x) → exp form → trig form → should be numerically equivalent
    symplex::syms!(ctx; x);
    let e = x.sin();
    let as_exp = e.rewrite_as_exp();
    let back = as_exp.rewrite_as_trig().eval().simplify();
    // Compare numerically at x = 0.7
    let val = ctx.rational(7, 10);
    let orig_f = e.subs(&x, &val).eval_f64().unwrap();

    // The roundtrip may produce complex intermediates, so try complex eval
    let back_result: Result<Complex64, _> = back.subs(&x, &val).eval_complex64();
    if let Ok(Complex64 { re, im }) = back_result {
        assert!(
            (orig_f - re).abs() < 1e-10,
            "roundtrip should preserve value: orig={orig_f}, back_re={re}, back_im={im}"
        );
    }
    // If evalf fails (e.g. free symbols in intermediate), just skip — the
    // structural tests above already confirm the rewrite mechanics.
}