use symplex::prelude::*;
fn verify_laplace_numerically(
result: &Ex,
s: &Ex,
s_num: i64,
s_den: i64,
expected: f64,
label: &str,
) {
let ctx = result.context();
let s_val = ctx.rational(s_num, s_den);
let at_s = result.subs(s, &s_val);
let val = at_s
.eval_f64()
.unwrap_or_else(|_| panic!("{label}: should evaluate numerically at s={s_num}/{s_den}"));
assert!(
(val - expected).abs() < 1e-3,
"{label}: at s={s_num}/{s_den}, expected {expected}, got {val}"
);
}
#[test]
fn laplace_constant() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let f = ctx.int(5);
let result = f.laplace(&t, &s);
let d = format!("{result}");
assert!(
d.contains("5") && d.contains("s"),
"L{{5}} should be 5/s, got: {d}"
);
verify_laplace_numerically(&result, &s, 3, 1, 5.0 / 3.0, "L{5}");
}
#[test]
fn laplace_one() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let f = ctx.int(1);
let result = f.laplace(&t, &s);
let d = format!("{result}");
assert!(d.contains("s"), "L{{1}} should be 1/s, got: {d}");
verify_laplace_numerically(&result, &s, 4, 1, 0.25, "L{1}");
}
#[test]
fn laplace_exp() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let f = (&t * 2).exp();
let result = f.laplace(&t, &s);
let d = format!("{result}");
assert!(
d.contains("s") && d.contains("2"),
"L{{exp(2t)}} should involve s and 2, got: {d}"
);
verify_laplace_numerically(&result, &s, 5, 1, 1.0 / 3.0, "L{exp(2t)}");
}
#[test]
fn laplace_exp_negative() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let f = (&t * -3).exp();
let result = f.laplace(&t, &s);
let d = format!("{result}");
assert!(
d.contains("s") && d.contains("3"),
"L{{exp(-3t)}} should involve s and 3, got: {d}"
);
verify_laplace_numerically(&result, &s, 2, 1, 0.2, "L{exp(-3t)}");
}
#[test]
fn laplace_sin() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let f = (&t * 3).sin();
let result = f.laplace(&t, &s);
let d = format!("{result}");
assert!(
d.contains("3") && d.contains("s"),
"L{{sin(3t)}} should involve 3 and s, got: {d}"
);
verify_laplace_numerically(&result, &s, 4, 1, 3.0 / 25.0, "L{sin(3t)}");
}
#[test]
fn laplace_cos() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let f = t.cos();
let result = f.laplace(&t, &s);
let d = format!("{result}");
assert!(d.contains("s"), "L{{cos(t)}} should involve s, got: {d}");
verify_laplace_numerically(&result, &s, 2, 1, 2.0 / 5.0, "L{cos(t)}");
}
#[test]
fn laplace_t_squared() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let f = t.powi(2);
let result = f.laplace(&t, &s);
let d = format!("{result}");
assert!(
d.contains("2") && d.contains("s"),
"L{{t²}} should be 2/s³, got: {d}"
);
verify_laplace_numerically(&result, &s, 3, 1, 2.0 / 27.0, "L{t²}");
}
#[test]
fn laplace_linearity() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let term1 = &t.exp() * 3;
let term2 = &t.sin() * 2;
let f = &term1 + &term2;
let r = f.laplace(&t, &s);
let d = format!("{r}");
assert!(d.contains("s"), "result should contain s, got: {d}");
verify_laplace_numerically(&r, &s, 4, 1, 3.0 / 3.0 + 2.0 / 17.0, "L{3*exp(t)+2*sin(t)}");
}
#[test]
fn laplace_freq_shift() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let f = &((&t * 2).exp()) * &((&t * 3).sin());
let r = f.laplace(&t, &s);
let d = format!("{r}");
assert!(
d.contains("s") && d.contains("3"),
"L{{exp(2t)*sin(3t)}} should involve s and 3, got: {d}"
);
verify_laplace_numerically(&r, &s, 5, 1, 3.0 / 18.0, "L{exp(2t)*sin(3t)}");
}
#[test]
fn laplace_sinh() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let f = (&t * 2).sinh();
let result = f.laplace(&t, &s);
let d = format!("{result}");
assert!(
d.contains("s") && d.contains("2"),
"L{{sinh(2t)}} should involve s and 2, got: {d}"
);
verify_laplace_numerically(&result, &s, 3, 1, 2.0 / 5.0, "L{sinh(2t)}");
}
#[test]
fn laplace_cosh() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let f = t.cosh();
let result = f.laplace(&t, &s);
let d = format!("{result}");
assert!(d.contains("s"), "L{{cosh(t)}} should involve s, got: {d}");
verify_laplace_numerically(&result, &s, 3, 1, 3.0 / 8.0, "L{cosh(t)}");
}
#[test]
fn inverse_laplace_1_over_s() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let f = &ctx.int(1) / &s;
let result = f.inverse_laplace(&s, &t);
let d = format!("{result}");
assert!(
d == "1" || !d.contains("s"),
"L⁻¹{{1/s}} should be 1 (constant), got: {d}"
);
}
#[test]
fn inverse_laplace_1_over_s_minus_a() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let f = &ctx.int(1) / &(&s - 2);
let r = f.inverse_laplace(&s, &t);
let d = format!("{r}");
assert!(d.contains("exp"), "L⁻¹{{1/(s-2)}} should contain exp: {d}");
}
#[test]
fn inverse_laplace_constant_over_s() {
let ctx = Context::new();
let t = ctx.symbol("t");
let s = ctx.symbol("s");
let f = &ctx.int(5) / &s;
let result = f.inverse_laplace(&s, &t);
let d = format!("{result}");
assert!(d.contains("5"), "L⁻¹{{5/s}} should be 5, got: {d}");
}
#[test]
fn laplace_rejects_non_symbol_t() {
let ctx = Context::new();
let s = ctx.symbol("s");
let f = ctx.int(1);
let bad_t = ctx.int(42); let result = f.laplace(&bad_t, &s);
assert!(
result.has_unevaluated(),
"should produce unevaluated node for non-symbol t"
);
}
#[test]
fn laplace_rejects_non_symbol_s() {
let ctx = Context::new();
let t = ctx.symbol("t");
let f = ctx.int(1);
let bad_s = ctx.int(42); let result = f.laplace(&t, &bad_s);
assert!(
result.has_unevaluated(),
"should produce unevaluated node for non-symbol s"
);
}