use super::*;
use crate::kernel::{Domain, ExprPool};
fn eval_at(expr: ExprId, var: ExprId, val: f64, pool: &ExprPool) -> Option<f64> {
match pool.get(expr) {
ExprData::Integer(n) => Some(n.0.to_f64()),
ExprData::Rational(r) => {
let (n, d) = r.0.clone().into_numer_denom();
Some(n.to_f64() / d.to_f64())
}
ExprData::Float(f) => Some(f.inner.to_f64()),
ExprData::Symbol { .. } => {
if expr == var {
Some(val)
} else {
None
}
}
ExprData::Add(args) => args
.iter()
.try_fold(0.0, |acc, &a| Some(acc + eval_at(a, var, val, pool)?)),
ExprData::Mul(args) => args
.iter()
.try_fold(1.0, |acc, &a| Some(acc * eval_at(a, var, val, pool)?)),
ExprData::Pow { base, exp } => {
Some(eval_at(base, var, val, pool)?.powf(eval_at(exp, var, val, pool)?))
}
ExprData::Func { name, args } if args.len() == 1 => {
let x = eval_at(args[0], var, val, pool)?;
Some(match name.as_str() {
"sin" => x.sin(),
"cos" => x.cos(),
"tan" => x.tan(),
"sinh" => x.sinh(),
"cosh" => x.cosh(),
"exp" => x.exp(),
"log" => x.ln(),
"sqrt" => x.sqrt(),
"heaviside" => {
if x > 0.0 {
1.0
} else if x < 0.0 {
0.0
} else {
0.5
}
}
_ => return None,
})
}
_ => None,
}
}
fn assert_numeric_eq(a: ExprId, b: ExprId, var: ExprId, samples: &[f64], pool: &ExprPool) {
for &x in samples {
let va = eval_at(a, var, x, pool);
let vb = eval_at(b, var, x, pool);
match (va, vb) {
(Some(va), Some(vb)) => assert!(
(va - vb).abs() < 1e-6 * (1.0 + va.abs() + vb.abs()),
"mismatch at {x}: {} = {va} vs {} = {vb}",
pool.display(a),
pool.display(b),
),
_ => panic!(
"could not numerically evaluate at {x}: {} / {}",
pool.display(a),
pool.display(b)
),
}
}
}
fn setup() -> (ExprPool, ExprId, ExprId) {
let pool = ExprPool::new();
let t = pool.symbol("t", Domain::Real);
let s = pool.symbol("s", Domain::Real);
(pool, t, s)
}
#[test]
fn forward_constant() {
let (pool, t, s) = setup();
let f = pool.integer(5_i32);
let got = laplace_transform(f, t, s, &pool).unwrap();
let want = pool.mul(vec![pool.integer(5_i32), pool.pow(s, pool.integer(-1_i32))]);
assert_numeric_eq(got, want, s, &[2.0, 3.0, 5.0], &pool);
}
#[test]
fn forward_t_power() {
let (pool, t, s) = setup();
let f = pool.pow(t, pool.integer(3_i32));
let got = laplace_transform(f, t, s, &pool).unwrap();
let want = pool.mul(vec![pool.integer(6_i32), pool.pow(s, pool.integer(-4_i32))]);
assert_numeric_eq(got, want, s, &[2.0, 3.0, 5.0], &pool);
}
#[test]
fn forward_exp() {
let (pool, t, s) = setup();
let f = pool.func("exp", vec![pool.mul(vec![pool.integer(2_i32), t])]);
let got = laplace_transform(f, t, s, &pool).unwrap();
let want = pool.pow(
pool.add(vec![s, pool.integer(-2_i32)]),
pool.integer(-1_i32),
);
assert_numeric_eq(got, want, s, &[3.0, 4.0, 5.0], &pool);
}
#[test]
fn forward_sin_cos() {
let (pool, t, s) = setup();
let s2 = pool.pow(s, pool.integer(2_i32));
let sin3t = pool.func("sin", vec![pool.mul(vec![pool.integer(3_i32), t])]);
let got = laplace_transform(sin3t, t, s, &pool).unwrap();
let want = pool.mul(vec![
pool.integer(3_i32),
pool.pow(
pool.add(vec![s2, pool.integer(9_i32)]),
pool.integer(-1_i32),
),
]);
assert_numeric_eq(got, want, s, &[2.0, 3.0, 5.0], &pool);
let cos3t = pool.func("cos", vec![pool.mul(vec![pool.integer(3_i32), t])]);
let got = laplace_transform(cos3t, t, s, &pool).unwrap();
let want = pool.mul(vec![
s,
pool.pow(
pool.add(vec![s2, pool.integer(9_i32)]),
pool.integer(-1_i32),
),
]);
assert_numeric_eq(got, want, s, &[2.0, 3.0, 5.0], &pool);
}
#[test]
fn forward_sinh_cosh() {
let (pool, t, s) = setup();
let s2 = pool.pow(s, pool.integer(2_i32));
let f = pool.func("sinh", vec![pool.mul(vec![pool.integer(2_i32), t])]);
let got = laplace_transform(f, t, s, &pool).unwrap();
let want = pool.mul(vec![
pool.integer(2_i32),
pool.pow(
pool.add(vec![s2, pool.integer(-4_i32)]),
pool.integer(-1_i32),
),
]);
assert_numeric_eq(got, want, s, &[3.0, 4.0, 5.0], &pool);
let f = pool.func("cosh", vec![pool.mul(vec![pool.integer(2_i32), t])]);
let got = laplace_transform(f, t, s, &pool).unwrap();
let want = pool.mul(vec![
s,
pool.pow(
pool.add(vec![s2, pool.integer(-4_i32)]),
pool.integer(-1_i32),
),
]);
assert_numeric_eq(got, want, s, &[3.0, 4.0, 5.0], &pool);
}
#[test]
fn forward_linearity() {
let (pool, t, s) = setup();
let f = pool.add(vec![
pool.mul(vec![pool.integer(2_i32), t]),
pool.integer(3_i32),
]);
let got = laplace_transform(f, t, s, &pool).unwrap();
let want = pool.add(vec![
pool.mul(vec![pool.integer(2_i32), pool.pow(s, pool.integer(-2_i32))]),
pool.mul(vec![pool.integer(3_i32), pool.pow(s, pool.integer(-1_i32))]),
]);
assert_numeric_eq(got, want, s, &[2.0, 3.0, 5.0], &pool);
}
#[test]
fn forward_s_shift() {
let (pool, t, s) = setup();
let cos3t = pool.func("cos", vec![pool.mul(vec![pool.integer(3_i32), t])]);
let exp2t = pool.func("exp", vec![pool.mul(vec![pool.integer(2_i32), t])]);
let f = pool.mul(vec![exp2t, cos3t]);
let got = laplace_transform(f, t, s, &pool).unwrap();
let s_minus_2 = pool.add(vec![s, pool.integer(-2_i32)]);
let sm2_sq = pool.pow(s_minus_2, pool.integer(2_i32));
let want = pool.mul(vec![
s_minus_2,
pool.pow(
pool.add(vec![sm2_sq, pool.integer(9_i32)]),
pool.integer(-1_i32),
),
]);
assert_numeric_eq(got, want, s, &[3.0, 4.0, 6.0], &pool);
}
#[test]
fn forward_t_times_exp_sin() {
let (pool, t, s) = setup();
let sin3t = pool.func("sin", vec![pool.mul(vec![pool.integer(3_i32), t])]);
let exp2t = pool.func("exp", vec![pool.mul(vec![pool.integer(2_i32), t])]);
let f = pool.mul(vec![t, exp2t, sin3t]);
let got = laplace_transform(f, t, s, &pool).unwrap();
let s_minus_2 = pool.add(vec![s, pool.integer(-2_i32)]);
let sm2_sq = pool.pow(s_minus_2, pool.integer(2_i32));
let denom = pool.add(vec![sm2_sq, pool.integer(9_i32)]);
let want = pool.mul(vec![
pool.integer(6_i32),
s_minus_2,
pool.pow(denom, pool.integer(-2_i32)),
]);
assert_numeric_eq(got, want, s, &[3.0, 4.0, 5.5], &pool);
}
#[test]
fn forward_heaviside_step() {
let (pool, t, s) = setup();
let arg = pool.add(vec![t, pool.integer(-2_i32)]);
let f = pool.func("heaviside", vec![arg]);
let got = laplace_transform(f, t, s, &pool).unwrap();
let want = pool.mul(vec![
pool.func("exp", vec![pool.mul(vec![pool.integer(-2_i32), s])]),
pool.pow(s, pool.integer(-1_i32)),
]);
assert_numeric_eq(got, want, s, &[1.0, 2.0, 3.0], &pool);
}
#[test]
fn forward_heaviside_shifted_function() {
let (pool, t, s) = setup();
let tm1 = pool.add(vec![t, pool.integer(-1_i32)]);
let f = pool.mul(vec![pool.func("heaviside", vec![tm1]), tm1]);
let got = laplace_transform(f, t, s, &pool).unwrap();
let want = pool.mul(vec![
pool.func("exp", vec![pool.mul(vec![pool.integer(-1_i32), s])]),
pool.pow(s, pool.integer(-2_i32)),
]);
assert_numeric_eq(got, want, s, &[1.5, 2.0, 3.0], &pool);
}
#[test]
fn forward_dirac() {
let (pool, t, s) = setup();
let f = pool.func("diracdelta", vec![t]);
let got = laplace_transform(f, t, s, &pool).unwrap();
assert_numeric_eq(got, pool.integer(1_i32), s, &[1.0, 2.0, 3.0], &pool);
let arg = pool.add(vec![t, pool.integer(-3_i32)]);
let f = pool.func("diracdelta", vec![arg]);
let got = laplace_transform(f, t, s, &pool).unwrap();
let want = pool.func("exp", vec![pool.mul(vec![pool.integer(-3_i32), s])]);
assert_numeric_eq(got, want, s, &[0.5, 1.0, 2.0], &pool);
}
#[test]
fn forward_same_variable_errors() {
let (pool, t, _s) = setup();
let f = pool.integer(1_i32);
assert_eq!(
laplace_transform(f, t, t, &pool),
Err(LaplaceError::SameVariable)
);
}
#[test]
fn forward_declines_unknown() {
let (pool, t, s) = setup();
let f = pool.func("log", vec![t]);
assert!(matches!(
laplace_transform(f, t, s, &pool),
Err(LaplaceError::NoRule(_))
));
}
#[test]
fn derivative_rule_second_order() {
let (pool, _t, s) = setup();
let big_f = pool.symbol("F", Domain::Real);
let y0 = pool.symbol("y0", Domain::Real);
let y1 = pool.symbol("y1", Domain::Real);
let l_ypp = laplace_derivative_rule(big_f, s, 2, &[y0, y1], &pool);
let want = pool.add(vec![
pool.mul(vec![pool.pow(s, pool.integer(2_i32)), big_f]),
pool.mul(vec![pool.integer(-1_i32), s, y0]),
pool.mul(vec![pool.integer(-1_i32), y1]),
]);
let diff =
crate::simplify::simplify_expanded(pool.add(vec![l_ypp, neg(want, &pool)]), &pool).value;
assert_eq!(
diff,
pool.integer(0_i32),
"L{{y''}} mismatch: {}",
pool.display(l_ypp)
);
}
#[test]
fn derivative_rule_first_order_zero_ic() {
let (pool, _t, s) = setup();
let big_f = pool.symbol("F", Domain::Real);
let got = laplace_derivative_rule(big_f, s, 1, &[pool.integer(0_i32)], &pool);
let want = pool.mul(vec![s, big_f]);
let diff =
crate::simplify::simplify_expanded(pool.add(vec![got, neg(want, &pool)]), &pool).value;
assert_eq!(diff, pool.integer(0_i32), "got {}", pool.display(got));
}
#[test]
fn inverse_simple_pole() {
let (pool, t, s) = setup();
let big_f = pool.pow(
pool.add(vec![s, pool.integer(-2_i32)]),
pool.integer(-1_i32),
);
let got = inverse_laplace_transform(big_f, s, t, &pool).unwrap();
let want = pool.func("exp", vec![pool.mul(vec![pool.integer(2_i32), t])]);
assert_numeric_eq(got, want, t, &[0.0, 0.5, 1.0], &pool);
}
#[test]
fn inverse_repeated_pole() {
let (pool, t, s) = setup();
let base = pool.add(vec![s, pool.integer(-1_i32)]);
let big_f = pool.pow(base, pool.integer(-3_i32));
let got = inverse_laplace_transform(big_f, s, t, &pool).unwrap();
let want = pool.mul(vec![
pool.rational(1_i32, 2_i32),
pool.pow(t, pool.integer(2_i32)),
pool.func("exp", vec![t]),
]);
assert_numeric_eq(got, want, t, &[0.0, 0.5, 1.0, 2.0], &pool);
}
#[test]
fn inverse_complex_poles() {
let (pool, t, s) = setup();
let s2 = pool.pow(s, pool.integer(2_i32));
let big_f = pool.pow(
pool.add(vec![s2, pool.integer(4_i32)]),
pool.integer(-1_i32),
);
let got = inverse_laplace_transform(big_f, s, t, &pool).unwrap();
let want = pool.mul(vec![
pool.rational(1_i32, 2_i32),
pool.func("sin", vec![pool.mul(vec![pool.integer(2_i32), t])]),
]);
assert_numeric_eq(got, want, t, &[0.1, 0.5, 1.0, 2.0], &pool);
}
#[test]
fn inverse_damped_oscillation() {
let (pool, t, s) = setup();
let s2 = pool.pow(s, pool.integer(2_i32));
let denom = pool.add(vec![
s2,
pool.mul(vec![pool.integer(2_i32), s]),
pool.integer(5_i32),
]);
let big_f = pool.mul(vec![s, pool.pow(denom, pool.integer(-1_i32))]);
let got = inverse_laplace_transform(big_f, s, t, &pool).unwrap();
let exp_neg_t = pool.func("exp", vec![pool.mul(vec![pool.integer(-1_i32), t])]);
let two_t = pool.mul(vec![pool.integer(2_i32), t]);
let want = pool.mul(vec![
exp_neg_t,
pool.add(vec![
pool.func("cos", vec![two_t]),
pool.mul(vec![
pool.rational(-1_i32, 2_i32),
pool.func("sin", vec![two_t]),
]),
]),
]);
assert_numeric_eq(got, want, t, &[0.0, 0.3, 0.8, 1.5], &pool);
}
#[test]
fn inverse_proper_rational_repeated_and_complex() {
let (pool, t, s) = setup();
let s2 = pool.pow(s, pool.integer(2_i32));
let sm1 = pool.add(vec![s, pool.integer(-1_i32)]);
let sm1_sq = pool.pow(sm1, pool.integer(2_i32));
let quad = pool.add(vec![s2, pool.integer(1_i32)]);
let denom = pool.mul(vec![sm1_sq, quad]);
let big_f = pool.pow(denom, pool.integer(-1_i32));
let f_of_t = inverse_laplace_transform(big_f, s, t, &pool).unwrap();
let back = laplace_transform(f_of_t, t, s, &pool).unwrap();
assert_numeric_eq(back, big_f, s, &[2.0, 3.0, 4.0, 5.0], &pool);
}
#[test]
fn inverse_delay_heaviside() {
let (pool, t, s) = setup();
let big_f = pool.mul(vec![
pool.func("exp", vec![pool.mul(vec![pool.integer(-2_i32), s])]),
pool.pow(s, pool.integer(-1_i32)),
]);
let got = inverse_laplace_transform(big_f, s, t, &pool).unwrap();
let want = pool.func("heaviside", vec![pool.add(vec![t, pool.integer(-2_i32)])]);
assert_numeric_eq(got, want, t, &[0.5, 1.0, 3.0, 4.0], &pool);
}
#[test]
fn inverse_declines_improper() {
let (pool, t, s) = setup();
let big_f = pool.mul(vec![
s,
pool.pow(
pool.add(vec![s, pool.integer(-1_i32)]),
pool.integer(-1_i32),
),
]);
assert!(matches!(
inverse_laplace_transform(big_f, s, t, &pool),
Err(LaplaceError::NotInvertible(_))
));
}
#[test]
fn round_trip_exp() {
let (pool, t, s) = setup();
let f = pool.func("exp", vec![pool.mul(vec![pool.integer(3_i32), t])]);
let big_f = laplace_transform(f, t, s, &pool).unwrap();
let back = inverse_laplace_transform(big_f, s, t, &pool).unwrap();
assert_numeric_eq(back, f, t, &[0.0, 0.2, 0.5, 1.0], &pool);
}
#[test]
fn round_trip_sin() {
let (pool, t, s) = setup();
let f = pool.func("sin", vec![pool.mul(vec![pool.integer(2_i32), t])]);
let big_f = laplace_transform(f, t, s, &pool).unwrap();
let back = inverse_laplace_transform(big_f, s, t, &pool).unwrap();
assert_numeric_eq(back, f, t, &[0.1, 0.5, 1.0, 2.5], &pool);
}
#[test]
fn round_trip_t_squared() {
let (pool, t, s) = setup();
let f = pool.pow(t, pool.integer(2_i32));
let big_f = laplace_transform(f, t, s, &pool).unwrap();
let back = inverse_laplace_transform(big_f, s, t, &pool).unwrap();
assert_numeric_eq(back, f, t, &[0.0, 0.5, 1.0, 2.0], &pool);
}
#[test]
fn round_trip_t_sin() {
let (pool, t, s) = setup();
let f = pool.mul(vec![
t,
pool.func("sin", vec![pool.mul(vec![pool.integer(2_i32), t])]),
]);
let big_f = laplace_transform(f, t, s, &pool).unwrap();
let back = inverse_laplace_transform(big_f, s, t, &pool).unwrap();
assert_numeric_eq(back, f, t, &[0.25, 0.5, 1.0, 1.5, 2.0], &pool);
}
#[test]
fn round_trip_t_cos() {
let (pool, t, s) = setup();
let f = pool.mul(vec![
t,
pool.func("cos", vec![pool.mul(vec![pool.integer(3_i32), t])]),
]);
let big_f = laplace_transform(f, t, s, &pool).unwrap();
let back = inverse_laplace_transform(big_f, s, t, &pool).unwrap();
assert_numeric_eq(back, f, t, &[0.25, 0.5, 1.0, 1.5, 2.0], &pool);
}
#[test]
fn round_trip_table_smoke() {
let (pool, t, s) = setup();
let cases: Vec<ExprId> = vec![
pool.integer(1_i32),
t,
pool.pow(t, pool.integer(2_i32)),
pool.func("exp", vec![pool.mul(vec![pool.integer(-2_i32), t])]),
pool.func("sin", vec![pool.mul(vec![pool.integer(5_i32), t])]),
pool.func("cos", vec![t]),
pool.mul(vec![
pool.func("exp", vec![pool.mul(vec![pool.integer(2_i32), t])]),
pool.func("sin", vec![pool.mul(vec![pool.integer(3_i32), t])]),
]),
pool.mul(vec![t, pool.func("exp", vec![t])]),
];
let samples = [0.3_f64, 0.7, 1.1, 1.9];
for f in cases {
let big_f = laplace_transform(f, t, s, &pool).unwrap();
let back = inverse_laplace_transform(big_f, s, t, &pool).unwrap();
assert_numeric_eq(back, f, t, &samples, &pool);
}
}
#[test]
fn inverse_hyperbolic_poles() {
let (pool, t, s) = setup();
let den = pool.add(vec![pool.pow(s, pool.integer(2_i32)), pool.integer(-2_i32)]);
let big_f = recip(den, &pool);
let got = inverse_laplace_transform(big_f, s, t, &pool).unwrap();
assert!(
pool.display(got).to_string().contains("sinh"),
"expected sinh, got {}",
pool.display(got)
);
let kappa = (2.0_f64).sqrt();
for &x in &[0.5_f64, 1.0, 1.5] {
let va = eval_at(got, t, x, &pool).expect("finite");
let vb = (kappa * x).sinh() / kappa;
assert!(
(va - vb).abs() < 1e-8,
"1/(s²−2) at t={x}: {va} vs sinh/√2 = {vb}"
);
}
let big_f = pool.mul(vec![s, recip(den, &pool)]);
let got = inverse_laplace_transform(big_f, s, t, &pool).unwrap();
assert!(
pool.display(got).to_string().contains("cosh"),
"expected cosh, got {}",
pool.display(got)
);
for &x in &[0.5_f64, 1.0, 1.5] {
let va = eval_at(got, t, x, &pool).expect("finite");
let vb = (kappa * x).cosh();
assert!((va - vb).abs() < 1e-8, "s/(s²−2) at t={x}: {va} vs {vb}");
}
}
#[test]
fn round_trip_sinh_irreducible() {
let (pool, t, s) = setup();
let sqrt2 = pool.pow(pool.integer(2_i32), pool.rational(1_i32, 2_i32));
let f = pool.func("sinh", vec![pool.mul(vec![sqrt2, t])]);
let big_f = laplace_transform(f, t, s, &pool).unwrap();
let back = inverse_laplace_transform(big_f, s, t, &pool).unwrap();
assert_numeric_eq(back, f, t, &[0.25, 0.5, 1.0, 1.5], &pool);
}
#[test]
fn decline_negative_heaviside_shift() {
let (pool, t, s) = setup();
let arg = pool.add(vec![t, pool.integer(1_i32)]);
let f = pool.func("heaviside", vec![arg]);
let err = laplace_transform(f, t, s, &pool).unwrap_err();
assert!(matches!(err, LaplaceError::NoRule(_)), "{err}");
}
#[test]
fn decline_negative_dirac_shift() {
let (pool, t, s) = setup();
let arg = pool.add(vec![t, pool.integer(2_i32)]);
let f = pool.func("diracdelta", vec![arg]);
let err = laplace_transform(f, t, s, &pool).unwrap_err();
assert!(matches!(err, LaplaceError::NoRule(_)), "{err}");
}
mod symbolic_params {
use super::*;
use crate::deriv::SideCondition;
use crate::simplify::assumptions::AssumptionContext;
use crate::transform::inverse_laplace_transform_with_assumptions;
use crate::transform::inverse_laplace_transform_with_conditions;
use std::collections::HashMap;
fn eval_env(expr: ExprId, env: &HashMap<ExprId, f64>, pool: &ExprPool) -> Option<f64> {
match pool.get(expr) {
ExprData::Integer(n) => Some(n.0.to_f64()),
ExprData::Rational(r) => {
let (n, d) = r.0.clone().into_numer_denom();
Some(n.to_f64() / d.to_f64())
}
ExprData::Float(f) => Some(f.inner.to_f64()),
ExprData::Symbol { .. } => env.get(&expr).copied(),
ExprData::Add(args) => args
.iter()
.try_fold(0.0, |acc, &a| Some(acc + eval_env(a, env, pool)?)),
ExprData::Mul(args) => args
.iter()
.try_fold(1.0, |acc, &a| Some(acc * eval_env(a, env, pool)?)),
ExprData::Pow { base, exp } => {
Some(eval_env(base, env, pool)?.powf(eval_env(exp, env, pool)?))
}
ExprData::Func { name, args } if args.len() == 1 => {
let x = eval_env(args[0], env, pool)?;
Some(match name.as_str() {
"sin" => x.sin(),
"cos" => x.cos(),
"sinh" => x.sinh(),
"cosh" => x.cosh(),
"exp" => x.exp(),
"heaviside" => {
if x > 0.0 {
1.0
} else if x < 0.0 {
0.0
} else {
0.5
}
}
_ => return None,
})
}
_ => None,
}
}
fn conds(cs: &[SideCondition], pool: &ExprPool) -> Vec<String> {
cs.iter()
.map(|c| c.display_with(pool).to_string())
.collect()
}
fn sym(pool: &ExprPool, n: &str) -> ExprId {
pool.symbol(n, Domain::Real)
}
#[test]
fn undamped_oscillator_with_a_symbolic_frequency() {
let (pool, t, s) = setup();
let w = sym(&pool, "w");
let den = pool.add(vec![
pool.pow(s, pool.integer(2_i32)),
pool.pow(w, pool.integer(2_i32)),
]);
let (got, cs) =
inverse_laplace_transform_with_conditions(recip(den, &pool), s, t, &pool).unwrap();
for (wv, tv) in [(2.0_f64, 0.7_f64), (0.4, 3.1), (-1.5, 1.2)] {
let env = HashMap::from([(w, wv), (t, tv)]);
let got_v = eval_env(got, &env, &pool).expect("finite");
let want = (wv * tv).sin() / wv;
assert!(
(got_v - want).abs() < 1e-9,
"ω={wv}, t={tv}: {got_v} vs sin(ωt)/ω = {want} — {}",
pool.display(got)
);
}
assert_eq!(conds(&cs, &pool), vec!["w ≠ 0".to_string()]);
}
#[test]
fn second_order_underdamped_response() {
let (pool, t, s) = setup();
let (k, w, zeta) = (sym(&pool, "K"), sym(&pool, "w"), sym(&pool, "zeta"));
let den = pool.add(vec![
pool.pow(s, pool.integer(2_i32)),
pool.mul(vec![pool.integer(2_i32), zeta, w, s]),
pool.pow(w, pool.integer(2_i32)),
]);
let big_f = pool.mul(vec![k, recip(den, &pool)]);
let (got, cs) = inverse_laplace_transform_with_conditions(big_f, s, t, &pool).unwrap();
for (kv, wv, zv, tv) in [
(1.0_f64, 2.0_f64, 0.3_f64, 0.8_f64),
(2.5, 5.0, 0.1, 1.7),
(0.75, 1.25, 0.7, 3.0),
] {
let env = HashMap::from([(k, kv), (w, wv), (zeta, zv), (t, tv)]);
let got_v = eval_env(got, &env, &pool).expect("finite");
let wd = wv * (1.0 - zv * zv).sqrt();
let want = kv * (-zv * wv * tv).exp() * (wd * tv).sin() / wd;
assert!(
(got_v - want).abs() < 1e-9 * (1.0 + want.abs()),
"K={kv} ω={wv} ζ={zv} t={tv}: {got_v} vs {want} — {}",
pool.display(got)
);
}
let strings = conds(&cs, &pool);
assert!(strings.iter().any(|c| c == "w ≠ 0"), "{strings:?}");
assert_eq!(
strings.iter().filter(|c| c.ends_with("≠ 0")).count(),
3,
"expected ω ≠ 0 and ζ ≠ ±1: {strings:?}"
);
assert_eq!(
strings.iter().filter(|c| c.ends_with("> 0")).count(),
1,
"expected the under-damped positivity condition: {strings:?}"
);
}
#[test]
fn bateman_two_compartment() {
let (pool, t, s) = setup();
let (d, ka, ke) = (sym(&pool, "D"), sym(&pool, "ka"), sym(&pool, "ke"));
let den = pool.mul(vec![pool.add(vec![s, ka]), pool.add(vec![s, ke])]);
let big_f = pool.mul(vec![d, ka, recip(den, &pool)]);
let (got, cs) = inverse_laplace_transform_with_conditions(big_f, s, t, &pool).unwrap();
for (dv, kav, kev, tv) in [
(100.0_f64, 1.5_f64, 0.25_f64, 2.0_f64),
(50.0, 0.4, 2.2, 0.7),
(1.0, 3.0, 0.1, 5.5),
] {
let env = HashMap::from([(d, dv), (ka, kav), (ke, kev), (t, tv)]);
let got_v = eval_env(got, &env, &pool).expect("finite");
let want = dv * kav * ((-kav * tv).exp() - (-kev * tv).exp()) / (kev - kav);
assert!(
(got_v - want).abs() < 1e-9 * (1.0 + want.abs()),
"D={dv} ka={kav} ke={kev} t={tv}: {got_v} vs Bateman {want} — {}",
pool.display(got)
);
}
let strings = conds(&cs, &pool);
assert_eq!(strings.len(), 1, "{strings:?}");
assert!(
strings[0].contains("ka") && strings[0].contains("ke") && strings[0].ends_with("≠ 0"),
"{strings:?}"
);
}
#[test]
fn repeated_symbolic_pole_is_the_other_branch() {
let (pool, t, s) = setup();
let ka = sym(&pool, "ka");
let big_f = pool.pow(pool.add(vec![s, ka]), pool.integer(-2_i32));
let (got, cs) = inverse_laplace_transform_with_conditions(big_f, s, t, &pool).unwrap();
assert!(cs.is_empty(), "{:?}", conds(&cs, &pool));
for (kav, tv) in [(1.5_f64, 2.0_f64), (0.25, 4.0)] {
let env = HashMap::from([(ka, kav), (t, tv)]);
let got_v = eval_env(got, &env, &pool).expect("finite");
let want = tv * (-kav * tv).exp();
assert!(
(got_v - want).abs() < 1e-9,
"{got_v} vs t·e^{{−ka t}} {want}"
);
}
}
#[test]
fn non_monic_pole_reports_its_leading_coefficient() {
let (pool, t, s) = setup();
let (a, b) = (sym(&pool, "a"), sym(&pool, "b"));
let big_f = recip(pool.add(vec![pool.mul(vec![a, s]), b]), &pool);
let (got, cs) = inverse_laplace_transform_with_conditions(big_f, s, t, &pool).unwrap();
assert_eq!(conds(&cs, &pool), vec!["a ≠ 0".to_string()]);
for (av, bv, tv) in [(2.0_f64, 3.0_f64, 1.0_f64), (0.5, -1.0, 2.5)] {
let env = HashMap::from([(a, av), (b, bv), (t, tv)]);
let got_v = eval_env(got, &env, &pool).expect("finite");
let want = (-bv / av * tv).exp() / av;
assert!((got_v - want).abs() < 1e-9, "{got_v} vs {want}");
}
}
#[test]
fn symbolic_delay_reports_its_sign_hypothesis() {
let (pool, t, s) = setup();
let a = sym(&pool, "a");
let big_f = pool.mul(vec![
pool.func("exp", vec![pool.mul(vec![pool.integer(-1_i32), a, s])]),
recip(s, &pool),
]);
let (_got, cs) = inverse_laplace_transform_with_conditions(big_f, s, t, &pool).unwrap();
assert!(
cs.iter()
.any(|c| matches!(c, SideCondition::InDomain(id, Domain::NonNegative) if *id == a)),
"{:?}",
conds(&cs, &pool)
);
}
#[test]
fn a_positive_delay_symbol_discharges_the_shift_hypothesis() {
let pool = ExprPool::new();
let t = pool.symbol("t", Domain::Real);
let s = pool.symbol("s", Domain::Real);
let a = pool.symbol("a", Domain::Positive);
let big_f = pool.mul(vec![
pool.func("exp", vec![pool.mul(vec![pool.integer(-1_i32), a, s])]),
recip(s, &pool),
]);
let (_got, cs) = inverse_laplace_transform_with_conditions(big_f, s, t, &pool).unwrap();
assert!(cs.is_empty(), "{:?}", conds(&cs, &pool));
}
#[test]
fn an_assumption_discharges_the_bateman_hypothesis() {
let (pool, t, s) = setup();
let (ka, ke) = (sym(&pool, "ka"), sym(&pool, "ke"));
let den = pool.mul(vec![pool.add(vec![s, ka]), pool.add(vec![s, ke])]);
let big_f = recip(den, &pool);
let (plain, plain_cs) =
inverse_laplace_transform_with_conditions(big_f, s, t, &pool).unwrap();
assert_eq!(plain_cs.len(), 1);
let target = match &plain_cs[0] {
SideCondition::NonZero(id) => *id,
other => panic!("unexpected condition {other:?}"),
};
let mut ctx = AssumptionContext::new();
ctx.refine(
pool.predicate(
crate::kernel::expr::PredicateKind::Ne,
vec![target, pool.integer(0_i32)],
),
&pool,
)
.unwrap();
let (with, with_cs) =
inverse_laplace_transform_with_assumptions(big_f, s, t, &pool, &ctx).unwrap();
assert!(with_cs.is_empty(), "{:?}", conds(&with_cs, &pool));
assert_eq!(with, plain, "the answer must not depend on the context");
}
#[test]
fn the_legacy_signature_reports_out_of_band() {
let (pool, t, s) = setup();
let (ka, ke) = (sym(&pool, "ka"), sym(&pool, "ke"));
let den = pool.mul(vec![pool.add(vec![s, ka]), pool.add(vec![s, ke])]);
let _ = inverse_laplace_transform(recip(den, &pool), s, t, &pool).unwrap();
let first = crate::transform::take_transform_side_conditions();
assert_eq!(first.len(), 1, "{:?}", conds(&first, &pool));
assert!(crate::transform::take_transform_side_conditions().is_empty());
let den2 = pool.mul(vec![
pool.add(vec![s, pool.integer(1_i32)]),
pool.add(vec![s, pool.integer(2_i32)]),
]);
let _ = inverse_laplace_transform(recip(den2, &pool), s, t, &pool).unwrap();
assert!(crate::transform::take_transform_side_conditions().is_empty());
}
#[test]
fn a_failed_call_does_not_leave_stale_hypotheses() {
let (pool, t, s) = setup();
let (ka, ke) = (sym(&pool, "ka"), sym(&pool, "ke"));
let den = pool.mul(vec![pool.add(vec![s, ka]), pool.add(vec![s, ke])]);
let _ = inverse_laplace_transform(recip(den, &pool), s, t, &pool).unwrap();
let bad = pool.mul(vec![
pool.pow(s, pool.integer(3_i32)),
recip(pool.add(vec![s, ka]), &pool),
]);
assert!(inverse_laplace_transform(bad, s, t, &pool).is_err());
assert!(
crate::transform::take_transform_side_conditions().is_empty(),
"a failed call must not report the previous call's hypotheses"
);
}
#[test]
fn round_trip_damped_sinusoid_with_symbolic_parameters() {
let (pool, t, s) = setup();
let (a, b) = (sym(&pool, "a"), sym(&pool, "b"));
let f = pool.mul(vec![
pool.func("exp", vec![pool.mul(vec![pool.integer(-1_i32), a, t])]),
pool.func("sin", vec![pool.mul(vec![b, t])]),
]);
let big_f = laplace_transform(f, t, s, &pool).unwrap();
let (back, _cs) = inverse_laplace_transform_with_conditions(big_f, s, t, &pool).unwrap();
for (av, bv, tv) in [
(0.5_f64, 3.0_f64, 1.1_f64),
(2.0, 0.75, 0.4),
(0.1, 1.0, 4.0),
] {
let env = HashMap::from([(a, av), (b, bv), (t, tv)]);
let lhs = eval_env(f, &env, &pool).expect("finite");
let rhs = eval_env(back, &env, &pool).expect("finite");
assert!(
(lhs - rhs).abs() < 1e-9 * (1.0 + lhs.abs()),
"a={av} b={bv} t={tv}: {lhs} vs {rhs} — {}",
pool.display(back)
);
}
}
#[test]
fn assumption_discharge_stops_at_nonlinear_consequences() {
let (pool, t, s) = setup();
let (w, zeta) = (sym(&pool, "w"), sym(&pool, "zeta"));
let den = pool.add(vec![
pool.pow(s, pool.integer(2_i32)),
pool.mul(vec![pool.integer(2_i32), zeta, w, s]),
pool.pow(w, pool.integer(2_i32)),
]);
let big_f = recip(den, &pool);
let zero = pool.integer(0_i32);
let one = pool.integer(1_i32);
let mut ctx = AssumptionContext::new();
for p in [
pool.predicate(crate::kernel::expr::PredicateKind::Gt, vec![zeta, zero]),
pool.predicate(crate::kernel::expr::PredicateKind::Lt, vec![zeta, one]),
pool.predicate(crate::kernel::expr::PredicateKind::Gt, vec![w, zero]),
] {
ctx.refine(p, &pool).unwrap();
}
let (_out, cs) =
inverse_laplace_transform_with_assumptions(big_f, s, t, &pool, &ctx).unwrap();
let rendered = conds(&cs, &pool);
assert!(
!rendered.iter().any(|c| c == "w ≠ 0"),
"ω > 0 should discharge ω ≠ 0: {rendered:?}"
);
assert_eq!(
rendered.len(),
3,
"ζ ≠ ±1 and the positivity are nonlinear and stay: {rendered:?}"
);
let p2 = ExprPool::new();
let s2 = p2.symbol("s", Domain::Real);
let t2 = p2.symbol("t", Domain::Real);
let w2 = p2.symbol("w", Domain::Positive);
let den2 = p2.add(vec![
p2.pow(s2, p2.integer(2_i32)),
p2.pow(w2, p2.integer(2_i32)),
]);
let (_o, cs2) =
inverse_laplace_transform_with_conditions(recip(den2, &p2), s2, t2, &p2).unwrap();
assert!(cs2.is_empty(), "{:?}", conds(&cs2, &p2));
}
#[test]
fn an_assumption_that_refutes_a_hypothesis_does_not_discharge_it() {
let (pool, t, s) = setup();
let (w, zeta) = (sym(&pool, "w"), sym(&pool, "zeta"));
let den = pool.add(vec![
pool.pow(s, pool.integer(2_i32)),
pool.mul(vec![pool.integer(2_i32), zeta, w, s]),
pool.pow(w, pool.integer(2_i32)),
]);
let big_f = recip(den, &pool);
let zero = pool.integer(0_i32);
let one = pool.integer(1_i32);
let mut ctx = AssumptionContext::new();
for p in [
pool.predicate(crate::kernel::expr::PredicateKind::Gt, vec![zeta, one]),
pool.predicate(crate::kernel::expr::PredicateKind::Gt, vec![w, zero]),
] {
ctx.refine(p, &pool).unwrap();
}
let (out, cs) =
inverse_laplace_transform_with_assumptions(big_f, s, t, &pool, &ctx).unwrap();
let rendered = conds(&cs, &pool);
assert!(
rendered.iter().any(|c| c.contains('>')),
"the positivity that ζ > 1 refutes must still be reported: {rendered:?}"
);
assert!(
pool.display(out).to_string().contains("sin"),
"{}",
pool.display(out)
);
}
#[test]
fn pi_is_not_a_hypothesis() {
let (pool, t, s) = setup();
let pi = pool.symbol("pi", Domain::Real);
let den = pool.add(vec![
pool.pow(s, pool.integer(2_i32)),
pool.pow(pi, pool.integer(2_i32)),
]);
let big_f = pool.mul(vec![pi, recip(den, &pool)]);
let (_out, cs) = inverse_laplace_transform_with_conditions(big_f, s, t, &pool).unwrap();
assert!(cs.is_empty(), "{:?}", conds(&cs, &pool));
}
}
mod shift_genericity {
use super::*;
use crate::deriv::SideCondition;
fn nonneg_conditions(conds: &[SideCondition]) -> Vec<ExprId> {
conds
.iter()
.filter_map(|c| match c {
SideCondition::InDomain(e, Domain::NonNegative) => Some(*e),
_ => None,
})
.collect()
}
#[test]
fn symbolic_delay_reports_its_nonnegativity() {
let (pool, t, s) = setup();
let a = pool.symbol("a", Domain::Real);
let arg = pool.add(vec![t, pool.mul(vec![pool.integer(-1_i32), a])]);
let f = pool.func("heaviside", vec![arg]);
let (out, conds) = laplace_transform_with_conditions(f, t, s, &pool).unwrap();
assert!(
pool.display(out).to_string().contains("exp"),
"expected the shifted-step form, got {}",
pool.display(out)
);
assert_eq!(nonneg_conditions(&conds), vec![a], "conds = {conds:?}");
}
#[test]
fn symbolic_advance_reports_the_hypothesis_it_actually_needs() {
let (pool, t, s) = setup();
let a = pool.symbol("a", Domain::Real);
let f = pool.func("heaviside", vec![pool.add(vec![t, a])]);
let (_out, conds) = laplace_transform_with_conditions(f, t, s, &pool).unwrap();
let want = simp(pool.mul(vec![pool.integer(-1_i32), a]), &pool);
assert_eq!(nonneg_conditions(&conds), vec![want], "conds = {conds:?}");
}
#[test]
fn symbolic_dirac_shift_reports_it_too() {
let (pool, t, s) = setup();
let a = pool.symbol("a", Domain::Real);
let arg = pool.add(vec![t, pool.mul(vec![pool.integer(-1_i32), a])]);
let f = pool.func("diracdelta", vec![arg]);
let (_out, conds) = laplace_transform_with_conditions(f, t, s, &pool).unwrap();
assert_eq!(nonneg_conditions(&conds), vec![a], "conds = {conds:?}");
}
#[test]
fn a_positive_domain_symbol_discharges_the_shift() {
let (pool, t, s) = setup();
let a = pool.symbol("a", Domain::Positive);
let arg = pool.add(vec![t, pool.mul(vec![pool.integer(-1_i32), a])]);
let f = pool.func("heaviside", vec![arg]);
let (_out, conds) = laplace_transform_with_conditions(f, t, s, &pool).unwrap();
assert!(conds.is_empty(), "conds = {conds:?}");
}
#[test]
fn a_literal_shift_is_decided_not_reported() {
let (pool, t, s) = setup();
let arg = pool.add(vec![t, pool.integer(-2_i32)]);
let f = pool.func("heaviside", vec![arg]);
let (_out, conds) = laplace_transform_with_conditions(f, t, s, &pool).unwrap();
assert!(conds.is_empty(), "conds = {conds:?}");
}
#[test]
fn the_shifted_product_rule_reports_it_as_well() {
let (pool, t, s) = setup();
let a = pool.symbol("a", Domain::Real);
let shifted = pool.add(vec![t, pool.mul(vec![pool.integer(-1_i32), a])]);
let f = pool.mul(vec![pool.func("heaviside", vec![shifted]), shifted]);
let (_out, conds) = laplace_transform_with_conditions(f, t, s, &pool).unwrap();
assert_eq!(nonneg_conditions(&conds), vec![a], "conds = {conds:?}");
}
#[test]
fn the_out_of_band_channel_carries_the_forward_hypotheses() {
let (pool, t, s) = setup();
let a = pool.symbol("a", Domain::Real);
let arg = pool.add(vec![t, pool.mul(vec![pool.integer(-1_i32), a])]);
let f = pool.func("heaviside", vec![arg]);
let _ = laplace_transform(f, t, s, &pool).unwrap();
let conds = crate::transform::take_transform_side_conditions();
assert_eq!(nonneg_conditions(&conds), vec![a], "conds = {conds:?}");
assert!(crate::transform::take_transform_side_conditions().is_empty());
}
#[test]
fn a_positive_domain_symbol_refutes_an_advance_rather_than_conditioning_it() {
let (pool, t, s) = setup();
let a = pool.symbol("apos", Domain::Positive);
let f = pool.func("heaviside", vec![pool.add(vec![t, a])]);
let err = laplace_transform(f, t, s, &pool).unwrap_err();
assert!(matches!(err, LaplaceError::NoRule(_)), "{err}");
let arg = pool.add(vec![t, pool.mul(vec![pool.integer(-1_i32), a])]);
let g = pool.func("heaviside", vec![arg]);
let (_out, conds) = laplace_transform_with_conditions(g, t, s, &pool).unwrap();
assert!(conds.is_empty(), "conds = {conds:?}");
}
#[test]
fn inverse_declines_an_advance_rather_than_stating_the_impossible() {
let (pool, t, s) = setup();
let big_f = pool.mul(vec![
pool.func("exp", vec![pool.mul(vec![pool.integer(2_i32), s])]),
pool.pow(pool.add(vec![s, pool.integer(1_i32)]), pool.integer(-1_i32)),
]);
let err = inverse_laplace_transform(big_f, s, t, &pool).unwrap_err();
assert!(matches!(err, LaplaceError::NotInvertible(_)), "{err}");
assert!(
crate::transform::take_transform_side_conditions().is_empty(),
"a refused call must not leave hypotheses behind"
);
}
#[test]
fn inverse_symbolic_delay_is_still_a_hypothesis() {
let (pool, t, s) = setup();
let a = pool.symbol("a", Domain::Real);
let big_f = pool.mul(vec![
pool.func(
"exp",
vec![simp(pool.mul(vec![pool.integer(-1_i32), a, s]), &pool)],
),
pool.pow(s, pool.integer(-1_i32)),
]);
let (_out, conds) = inverse_laplace_transform_with_conditions(big_f, s, t, &pool).unwrap();
assert_eq!(nonneg_conditions(&conds), vec![a], "conds = {conds:?}");
}
}