use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{One, Signed, Zero};
use crate::base::arena::Arena;
use crate::base::combinatorics::factorial;
use crate::base::errors::SymplexError;
use crate::base::node::{ExprId, ExprNode, SymbolId};
pub(crate) fn laplace_transform(
arena: &mut Arena,
expr: ExprId,
t: ExprId,
s: ExprId,
) -> Result<ExprId, SymplexError> {
let t_sym = match arena.node(t) {
ExprNode::Symbol(sid) => *sid,
_ => {
return Err(SymplexError::ComputationFailed {
operation: "laplace_transform",
reason: "t must be a symbol".to_string(),
});
}
};
match arena.node(s) {
ExprNode::Symbol(_) => {}
_ => {
return Err(SymplexError::ComputationFailed {
operation: "laplace_transform",
reason: "s must be a symbol".to_string(),
});
}
};
do_forward(arena, expr, t, t_sym, s)
}
fn do_forward(
arena: &mut Arena,
expr: ExprId,
t: ExprId,
t_sym: SymbolId,
s: ExprId,
) -> Result<ExprId, SymplexError> {
let node = arena.node(expr).clone();
if let ExprNode::Add(ref children) = node {
let kids = children.clone();
let mut terms = Vec::with_capacity(kids.len());
for child in kids {
terms.push(do_forward(arena, child, t, t_sym, s)?);
}
return Ok(arena.add(&terms));
}
if let ExprNode::Neg(inner) = node {
let transformed = do_forward(arena, inner, t, t_sym, s)?;
return Ok(arena.neg(transformed));
}
tracing::debug!("laplace: trying derivative rule");
if let ExprNode::Derivative(body, deriv_var) = node
&& deriv_var == t
{
let f_transform = do_forward(arena, body, t, t_sym, s)?;
let s_times_f = arena.mul(&[s, f_transform]);
let zero = arena.zero;
let f_at_0 = crate::transforms::subs::subs(arena, body, t, zero);
let f_at_0_eval = crate::transforms::eval::eval(arena, f_at_0);
return Ok(arena.sub(s_times_f, f_at_0_eval));
}
let (coeff, body) = split_independent(arena, expr, t);
if coeff != arena.one {
let transformed = do_forward(arena, body, t, t_sym, s)?;
return Ok(arena.mul(&[coeff, transformed]));
}
tracing::debug!("laplace: trying table forward");
if let Some(result) = try_table_forward(arena, expr, t, t_sym, s) {
return Ok(result);
}
if let Some(result) = try_special_forward(arena, expr, t, s)? {
return Ok(result);
}
if let Some(result) = try_divide_by_t(arena, expr, t, t_sym, s)? {
return Ok(result);
}
if let Some(result) = try_time_shift(arena, expr, t, t_sym, s)? {
return Ok(result);
}
if let Some(result) = try_freq_shift(arena, expr, t, t_sym, s)? {
return Ok(result);
}
if let Some(result) = try_tn_exp(arena, expr, t, t_sym, s) {
return Ok(result);
}
if let Some(result) = try_freq_diff(arena, expr, t, t_sym, s)? {
return Ok(result);
}
Err(SymplexError::ComputationFailed {
operation: "laplace_transform",
reason: "cannot transform expression".to_string(),
})
}
fn contains_var(arena: &Arena, expr: ExprId, t: ExprId) -> bool {
crate::base::walk::contains(arena, expr, t)
}
fn split_independent(arena: &mut Arena, expr: ExprId, t: ExprId) -> (ExprId, ExprId) {
let node = arena.node(expr).clone();
if let ExprNode::Mul(ref children) = node {
let mut indep = Vec::new();
let mut dep = Vec::new();
for &child in children {
if contains_var(arena, child, t) {
dep.push(child);
} else {
indep.push(child);
}
}
if !indep.is_empty() && !dep.is_empty() {
let coeff = if indep.len() == 1 {
indep[0]
} else {
arena.mul(&indep)
};
let body = if dep.len() == 1 {
dep[0]
} else {
arena.mul(&dep)
};
return (coeff, body);
}
}
(arena.one, expr)
}
fn extract_linear_coeff(arena: &mut Arena, expr: ExprId, t: ExprId) -> Option<ExprId> {
if expr == t {
return Some(arena.one);
}
let node = arena.node(expr).clone();
if let ExprNode::Mul(ref children) = node {
let mut t_count = 0usize;
let mut others = Vec::new();
for &child in children {
if child == t {
t_count += 1;
} else if contains_var(arena, child, t) {
return None;
} else {
others.push(child);
}
}
if t_count == 1 {
if others.is_empty() {
return Some(arena.one);
} else if others.len() == 1 {
return Some(others[0]);
} else {
return Some(arena.mul(&others));
}
}
}
let poly = crate::poly::polybridge::expr_to_poly(arena, expr, t)?;
if poly.degree()? != 1 {
return None;
}
let c0 = poly.coeff(0);
if !c0.is_zero() {
return None;
}
let a = poly.coeff(1);
if a.is_zero() {
return None;
}
let nid = arena.intern_num(a);
Some(arena.intern(ExprNode::Num(nid)))
}
fn try_table_forward(
arena: &mut Arena,
expr: ExprId,
t: ExprId,
t_sym: SymbolId,
s: ExprId,
) -> Option<ExprId> {
let node = arena.node(expr).clone();
match node {
_ if !contains_var(arena, expr, t) => Some(arena.div(expr, s)),
ExprNode::Symbol(sid) if sid == t_sym => {
let two = arena.int(2);
let s2 = arena.pow(s, two);
Some(arena.div(arena.one, s2))
}
ExprNode::Pow(base, exp) if base == t => {
if let Some(r) = arena.as_num(exp).cloned()
&& r.is_integer()
&& r.is_positive()
{
let n_val = r.to_integer().try_into().ok()?;
let fact = factorial(n_val);
let fact_rat = Ratio::from_integer(fact);
let fact_id = arena.num_ratio(fact_rat.clone());
let n_plus_1_rat = r + Ratio::one();
let n_plus_1_id = arena.num_ratio(n_plus_1_rat.clone());
let s_pow = arena.pow(s, n_plus_1_id);
return Some(arena.div(fact_id, s_pow));
}
None
}
ExprNode::Exp(arg) => {
if let Some(a) = extract_linear_coeff(arena, arg, t) {
let s_minus_a = arena.sub(s, a);
return Some(arena.div(arena.one, s_minus_a));
}
if let Some(poly) = crate::poly::polybridge::expr_to_poly(arena, arg, t)
&& poly.degree() == Some(1)
{
let a_coeff = poly.coeff(1);
let b_coeff = poly.coeff(0);
if !a_coeff.is_zero() && !b_coeff.is_zero() {
let a_id = arena.num_ratio(a_coeff.clone());
let b_id = arena.num_ratio(b_coeff.clone());
let s_minus_a = arena.sub(s, a_id);
let exp_b = arena.exp(b_id);
return Some(arena.div(exp_b, s_minus_a));
}
}
None
}
ExprNode::Sin(arg) => {
if let Some(omega) = extract_linear_coeff(arena, arg, t) {
let omega_sq = arena.mul(&[omega, omega]);
let s_sq = arena.mul(&[s, s]);
let denom = arena.add(&[s_sq, omega_sq]);
return Some(arena.div(omega, denom));
}
None
}
ExprNode::Cos(arg) => {
if let Some(omega) = extract_linear_coeff(arena, arg, t) {
let omega_sq = arena.mul(&[omega, omega]);
let s_sq = arena.mul(&[s, s]);
let denom = arena.add(&[s_sq, omega_sq]);
return Some(arena.div(s, denom));
}
None
}
ExprNode::Sinh(arg) => {
if let Some(a) = extract_linear_coeff(arena, arg, t) {
let a_sq = arena.mul(&[a, a]);
let s_sq = arena.mul(&[s, s]);
let denom = arena.sub(s_sq, a_sq);
return Some(arena.div(a, denom));
}
None
}
ExprNode::Cosh(arg) => {
if let Some(a) = extract_linear_coeff(arena, arg, t) {
let a_sq = arena.mul(&[a, a]);
let s_sq = arena.mul(&[s, s]);
let denom = arena.sub(s_sq, a_sq);
return Some(arena.div(s, denom));
}
None
}
_ => None,
}
}
fn fail(reason: impl Into<String>) -> SymplexError {
SymplexError::ComputationFailed {
operation: "laplace_transform",
reason: reason.into(),
}
}
fn param_sign(arena: &mut Arena, e: ExprId) -> Option<i32> {
crate::calculus::limit::const_sign(arena, e)
}
fn need_sign(arena: &Arena, e: ExprId, cond: &str) -> SymplexError {
fail(format!(
"requires {cond} (declare the sign of {} with Assumption::Positive / Assumption::Negative)",
arena.display(e)
))
}
fn linear_in(arena: &mut Arena, expr: ExprId, t: ExprId) -> Option<(ExprId, ExprId)> {
if expr == t {
return Some((arena.one, arena.zero));
}
if !contains_var(arena, expr, t) {
return Some((arena.zero, expr));
}
match arena.node(expr).clone() {
ExprNode::Neg(inner) => {
let (a, b) = linear_in(arena, inner, t)?;
Some((arena.neg(a), arena.neg(b)))
}
ExprNode::Mul(children) => {
let mut coeff = Vec::new();
let mut seen = false;
for &c in &children {
if c == t {
if seen {
return None;
}
seen = true;
} else if contains_var(arena, c, t) {
return None;
} else {
coeff.push(c);
}
}
if !seen {
return None;
}
let a = if coeff.is_empty() {
arena.one
} else {
arena.mul(&coeff)
};
Some((a, arena.zero))
}
ExprNode::Add(children) => {
let mut a_terms = Vec::new();
let mut b_terms = Vec::new();
for &c in &children {
let (a, b) = linear_in(arena, c, t)?;
if !arena.is_zero_structural(a) {
a_terms.push(a);
}
if !arena.is_zero_structural(b) {
b_terms.push(b);
}
}
let a = arena.add(&a_terms);
let b = arena.add(&b_terms);
Some((a, b))
}
_ => None,
}
}
fn try_special_forward(
arena: &mut Arena,
expr: ExprId,
t: ExprId,
s: ExprId,
) -> Result<Option<ExprId>, SymplexError> {
let one = arena.one;
let two = arena.int(2);
let expr = flatten_nested_power(arena, expr);
match arena.node(expr).clone() {
ExprNode::Pow(base, nu) if base == t && !contains_var(arena, nu, t) => {
let nu_p1 = arena.add(&[nu, one]);
let nu_p1 = crate::transforms::eval::eval(arena, nu_p1);
match param_sign(arena, nu_p1) {
Some(sg) if sg > 0 => {}
Some(_) => {
return Err(fail("t^ν with ν ≤ −1 is not Laplace transformable"));
}
None => return Err(need_sign(arena, nu_p1, "ν > −1 in t^ν")),
}
let g = arena.gamma(nu_p1);
let g = crate::transforms::eval::eval(arena, g);
let neg = arena.neg(nu_p1);
let s_pow = arena.pow(s, neg);
Ok(Some(arena.mul(&[g, s_pow])))
}
ExprNode::Ln(arg) if arg == t => {
let gamma = arena.euler_gamma();
let ln_s = arena.ln(s);
let sum = arena.add(&[gamma, ln_s]);
let r = arena.div(sum, s);
Ok(Some(arena.neg(r)))
}
ExprNode::DiracDelta(arg) => {
let Some((k, b)) = linear_in(arena, arg, t) else {
return Ok(None);
};
if k != one {
return Ok(None);
}
let a = arena.neg(b);
let a = crate::transforms::eval::eval(arena, a);
match param_sign(arena, a) {
Some(0) => return Ok(Some(one)),
Some(sg) if sg > 0 => {}
Some(_) => return Ok(Some(arena.zero)),
None => return Err(need_sign(arena, a, "a ≥ 0 in δ(t − a)")),
}
let as_ = arena.mul(&[a, s]);
let neg = arena.neg(as_);
Ok(Some(arena.exp(neg)))
}
ExprNode::Heaviside(arg) => {
let Some((k, b)) = linear_in(arena, arg, t) else {
return Ok(None);
};
if k != one {
return Ok(None);
}
let a = arena.neg(b);
let a = crate::transforms::eval::eval(arena, a);
match param_sign(arena, a) {
Some(sg) if sg > 0 => {}
Some(_) => return Ok(Some(arena.div(one, s))),
None => return Err(need_sign(arena, a, "a ≥ 0 in H(t − a)")),
}
let as_ = arena.mul(&[a, s]);
let neg = arena.neg(as_);
let e = arena.exp(neg);
Ok(Some(arena.div(e, s)))
}
ExprNode::Apply(name, args)
if arena.symbol_name(name) == crate::base::arena::FN_BESSELJ && args.len() == 2 =>
{
let order = args[0];
let arg = args[1];
let Some(n) = arena.as_num(order).cloned() else {
return Ok(None);
};
if !n.is_integer() || n.is_negative() {
return Ok(None);
}
let n: u32 = n
.to_integer()
.try_into()
.map_err(|_| fail("Bessel order too large"))?;
let Some((a, b)) = linear_in(arena, arg, t) else {
return Ok(None);
};
if !arena.is_zero_structural(b) {
return Ok(None);
}
let s2 = arena.pow(s, two);
let a2 = arena.pow(a, two);
let sum = arena.add(&[s2, a2]);
let root = arena.sqrt(sum);
if n == 0 {
return Ok(Some(arena.div(one, root)));
}
let n_id = arena.int(i64::from(n));
let diff = arena.sub(root, s);
let num = arena.pow(diff, n_id);
let an = arena.pow(a, n_id);
let den = arena.mul(&[an, root]);
Ok(Some(arena.div(num, den)))
}
ExprNode::Erf(arg) => {
let half = arena.rational(1, 2);
let sqrt_t = arena.pow(t, half);
let (a, inner) = match arena.node(arg).clone() {
ExprNode::Pow(..) if arg == sqrt_t => (one, sqrt_t),
ExprNode::Mul(ch) => {
let rest: Vec<ExprId> = ch.iter().copied().filter(|&c| c != sqrt_t).collect();
if rest.len() + 1 != ch.len() || rest.iter().any(|&c| contains_var(arena, c, t))
{
return Ok(None);
}
(arena.mul(&rest), sqrt_t)
}
_ => return Ok(None),
};
let _ = inner;
match param_sign(arena, a) {
Some(sg) if sg > 0 => {}
Some(_) => return Ok(None),
None => return Err(need_sign(arena, a, "a > 0 in erf(a√t)")),
}
let a2 = arena.pow(a, two);
let sum = arena.add(&[s, a2]);
let root = arena.sqrt(sum);
let den = arena.mul(&[s, root]);
Ok(Some(arena.div(a, den)))
}
_ => Ok(None),
}
}
fn flatten_nested_power(arena: &mut Arena, expr: ExprId) -> ExprId {
if let ExprNode::Pow(inner, q) = arena.node(expr).clone()
&& let ExprNode::Pow(base, p) = arena.node(inner).clone()
&& arena.as_num(p).is_some()
&& arena.as_num(q).is_some()
{
let pq = arena.mul(&[p, q]);
let pq = crate::transforms::eval::eval(arena, pq);
return arena.pow(base, pq);
}
expr
}
fn try_divide_by_t(
arena: &mut Arena,
expr: ExprId,
t: ExprId,
t_sym: SymbolId,
s: ExprId,
) -> Result<Option<ExprId>, SymplexError> {
let ExprNode::Mul(children) = arena.node(expr).clone() else {
return Ok(None);
};
let inv_t = arena.pow(t, arena.neg_one);
if !children.contains(&inv_t) {
return Ok(None);
}
let rest: Vec<ExprId> = children.iter().copied().filter(|&c| c != inv_t).collect();
if rest.is_empty() {
return Err(fail(
"1/t is not Laplace transformable (not integrable at 0)",
));
}
let g = arena.mul(&rest);
let one = arena.one;
let two = arena.int(2);
match arena.node(g).clone() {
ExprNode::Sin(arg) => {
if let Some((a, b)) = linear_in(arena, arg, t)
&& arena.is_zero_structural(b)
{
let ratio = arena.div(a, s);
return Ok(Some(arena.atan(ratio)));
}
}
ExprNode::Add(terms) if terms.len() == 2 && terms.contains(&one) => {
let other = if terms[0] == one { terms[1] } else { terms[0] };
let neg_cos = match arena.node(other).clone() {
ExprNode::Neg(c) => Some(c),
ExprNode::Mul(ch) if ch.len() == 2 && ch.contains(&arena.neg_one) => {
Some(if ch[0] == arena.neg_one { ch[1] } else { ch[0] })
}
_ => None,
};
if let Some(c) = neg_cos
&& let ExprNode::Cos(arg) = arena.node(c).clone()
&& let Some((a, b)) = linear_in(arena, arg, t)
&& arena.is_zero_structural(b)
{
let a2 = arena.pow(a, two);
let s2 = arena.pow(s, two);
let ratio = arena.div(a2, s2);
let arg = arena.add(&[one, ratio]);
let ln = arena.ln(arg);
return Ok(Some(arena.div(ln, two)));
}
}
_ => {}
}
let zero = arena.zero;
let g0 = crate::calculus::limit::safe_substitute(arena, g, t, zero);
match g0 {
Some(v) if arena.is_zero_structural(v) => {}
_ => {
return Err(fail(format!(
"{}/t is not integrable at t = 0 (numerator does not vanish there)",
arena.display(g)
)));
}
}
let big_g = do_forward(arena, g, t, t_sym, s)?;
let u = arena.symbol("__lap_u");
let big_g_u = crate::transforms::subs::subs(arena, big_g, s, u);
let inf = arena.infinity();
let ok = |arena: &Arena, r: ExprId| {
!contains_var(arena, r, u) && !crate::base::walk::has_unevaluated(arena, r)
};
if let Ok(integral) = crate::calculus::definite::integrate_definite(arena, big_g_u, u, s, inf)
&& ok(arena, integral)
{
return Ok(Some(integral));
}
let anti = crate::transforms::integrate::integrate(arena, big_g_u, u);
if crate::base::walk::has_unevaluated(arena, anti) {
return Err(fail("∫_s^∞ F(u) du has no closed form"));
}
let at_inf = crate::calculus::limit::limit(arena, anti, u, inf)
.map_err(|e| fail(format!("∫_s^∞ F(u) du: {e}")))?;
if at_inf == inf || at_inf == arena.neg_infinity() {
return Err(fail("∫_s^∞ F(u) du diverges"));
}
let at_s = crate::transforms::subs::subs(arena, anti, u, s);
let r = arena.sub(at_inf, at_s);
let r = crate::transforms::eval::eval(arena, r);
if ok(arena, r) {
Ok(Some(r))
} else {
Err(fail("∫_s^∞ F(u) du has no closed form"))
}
}
fn try_time_shift(
arena: &mut Arena,
expr: ExprId,
t: ExprId,
t_sym: SymbolId,
s: ExprId,
) -> Result<Option<ExprId>, SymplexError> {
let node = arena.node(expr).clone();
if let ExprNode::Mul(ref children) = node {
let kids = children.clone();
for (i, &child) in kids.iter().enumerate() {
if let ExprNode::Heaviside(h_arg) = arena.node(child).clone() {
if let Some(a) = extract_shift(arena, h_arg, t, t_sym) {
tracing::debug!("laplace: time-shift detected with a={:?}", a);
let remaining: Vec<ExprId> = kids
.iter()
.enumerate()
.filter(|&(j, _)| j != i)
.map(|(_, &c)| c)
.collect();
let g = if remaining.len() == 1 {
remaining[0]
} else if remaining.is_empty() {
arena.one
} else {
arena.mul(&remaining)
};
let t_plus_a = arena.add(&[t, a]);
let g_shifted = crate::transforms::subs::subs(arena, g, t, t_plus_a);
let g_transform = do_forward(arena, g_shifted, t, t_sym, s)?;
let a_s = arena.mul(&[a, s]);
let neg_a_s = arena.neg(a_s);
let exp_factor = arena.exp(neg_a_s);
return Ok(Some(arena.mul(&[exp_factor, g_transform])));
}
}
}
}
Ok(None)
}
fn extract_shift(arena: &mut Arena, expr: ExprId, t: ExprId, _t_sym: SymbolId) -> Option<ExprId> {
if expr == t {
return Some(arena.zero);
}
let (k, b) = linear_in(arena, expr, t)?;
if k != arena.one {
return None;
}
let a = arena.neg(b);
let a = crate::transforms::eval::eval(arena, a);
match param_sign(arena, a) {
Some(sg) if sg >= 0 => Some(a),
_ => None,
}
}
fn try_freq_diff(
arena: &mut Arena,
expr: ExprId,
t: ExprId,
t_sym: SymbolId,
s: ExprId,
) -> Result<Option<ExprId>, SymplexError> {
let node = arena.node(expr).clone();
if let ExprNode::Mul(ref children) = node {
let kids = children.clone();
for (i, &child) in kids.iter().enumerate() {
let n = check_t_power(arena, child, t, t_sym);
if n > 0 {
tracing::debug!("laplace: frequency differentiation detected, t^{}", n);
let remaining: Vec<ExprId> = kids
.iter()
.enumerate()
.filter(|&(j, _)| j != i)
.map(|(_, &c)| c)
.collect();
let g = if remaining.len() == 1 {
remaining[0]
} else if remaining.is_empty() {
arena.one
} else {
arena.mul(&remaining)
};
let g_transform = do_forward(arena, g, t, t_sym, s)?;
let mut result = g_transform;
for _ in 0..n {
result = crate::transforms::diff::diff(arena, result, s);
result = arena.neg(result);
}
return Ok(Some(result));
}
}
}
Ok(None)
}
fn check_t_power(arena: &Arena, expr: ExprId, t: ExprId, t_sym: SymbolId) -> u64 {
if expr == t {
return 1;
}
if let ExprNode::Symbol(sid) = arena.node(expr)
&& *sid == t_sym
{
return 1;
}
if let ExprNode::Pow(base, exp) = arena.node(expr)
&& *base == t
&& let Some(r) = arena.as_num(*exp)
&& r.is_integer()
&& r.is_positive()
{
return r.to_integer().try_into().unwrap_or(0);
}
0
}
fn try_freq_shift(
arena: &mut Arena,
expr: ExprId,
t: ExprId,
t_sym: SymbolId,
s: ExprId,
) -> Result<Option<ExprId>, SymplexError> {
let node = arena.node(expr).clone();
if let ExprNode::Mul(ref children) = node {
let kids = children.clone();
for (i, &child) in kids.iter().enumerate() {
let child_node = arena.node(child).clone();
if let ExprNode::Exp(arg) = child_node
&& let Some(a) = extract_linear_coeff(arena, arg, t)
{
let remaining: Vec<ExprId> = kids
.iter()
.enumerate()
.filter(|&(j, _)| j != i)
.map(|(_, &c)| c)
.collect();
let g = if remaining.len() == 1 {
remaining[0]
} else {
arena.mul(&remaining)
};
let g_of_s = do_forward(arena, g, t, t_sym, s)?;
let s_shifted = arena.sub(s, a);
let result = crate::transforms::subs::subs(arena, g_of_s, s, s_shifted);
return Ok(Some(result));
}
}
}
Ok(None)
}
fn try_tn_exp(
arena: &mut Arena,
expr: ExprId,
t: ExprId,
t_sym: SymbolId,
s: ExprId,
) -> Option<ExprId> {
let node = arena.node(expr).clone();
if let ExprNode::Mul(ref children) = node {
let kids = children.clone();
let mut exp_idx = None;
let mut pow_idx = None;
let mut t_idx = None;
for (i, &child) in kids.iter().enumerate() {
let cn = arena.node(child).clone();
match cn {
ExprNode::Exp(_) => {
if exp_idx.is_some() {
return None;
}
exp_idx = Some(i);
}
ExprNode::Pow(base, _) if base == t => {
if pow_idx.is_some() {
return None;
}
pow_idx = Some(i);
}
ExprNode::Symbol(sid) if sid == t_sym => {
if t_idx.is_some() {
return None;
}
t_idx = Some(i);
}
_ => {}
}
}
let exp_i = exp_idx?;
let exp_node = arena.node(kids[exp_i]).clone();
let ExprNode::Exp(exp_arg) = exp_node else {
return None;
};
let a = extract_linear_coeff(arena, exp_arg, t)?;
let n_val: u64;
if let Some(pi) = pow_idx {
let pow_node = arena.node(kids[pi]).clone();
if let ExprNode::Pow(_, exp) = pow_node {
let r = arena.as_num(exp).cloned()?;
if !r.is_integer() || !r.is_positive() {
return None;
}
n_val = r.to_integer().try_into().ok()?;
} else {
return None;
}
} else {
let _ti = t_idx?;
n_val = 1;
}
let skip: Vec<usize> = {
let mut v = vec![exp_i];
if let Some(pi) = pow_idx {
v.push(pi);
}
if let Some(ti) = t_idx {
v.push(ti);
}
v
};
for (i, &child) in kids.iter().enumerate() {
if skip.contains(&i) {
continue;
}
if contains_var(arena, child, t) {
return None;
}
}
let const_factors: Vec<ExprId> = kids
.iter()
.enumerate()
.filter(|(i, _)| !skip.contains(i))
.map(|(_, &c)| c)
.collect();
let fact = factorial(n_val);
let fact_rat = Ratio::from_integer(fact);
let fact_id = arena.num_ratio(fact_rat.clone());
let s_minus_a = arena.sub(s, a);
let n_plus_1 = arena.int(n_val as i64 + 1);
let denom = arena.pow(s_minus_a, n_plus_1);
let mut result = arena.div(fact_id, denom);
if !const_factors.is_empty() {
let mut all = const_factors;
all.push(result);
result = arena.mul(&all);
}
return Some(result);
}
None
}
pub(crate) fn inverse_laplace_transform(
arena: &mut Arena,
expr: ExprId,
s: ExprId,
t: ExprId,
) -> Result<ExprId, SymplexError> {
match arena.node(s) {
ExprNode::Symbol(_) => {}
_ => {
return Err(SymplexError::ComputationFailed {
operation: "inverse_laplace_transform",
reason: "s must be a symbol".to_string(),
});
}
};
match arena.node(t) {
ExprNode::Symbol(_) => {}
_ => {
return Err(SymplexError::ComputationFailed {
operation: "inverse_laplace_transform",
reason: "t must be a symbol".to_string(),
});
}
};
do_inverse(arena, expr, s, t, 0)
}
fn do_inverse(
arena: &mut Arena,
expr: ExprId,
s: ExprId,
t: ExprId,
depth: u32,
) -> Result<ExprId, SymplexError> {
if depth > 20 {
return Err(SymplexError::ComputationFailed {
operation: "inverse_laplace_transform",
reason: "recursion limit reached".to_string(),
});
}
let node = arena.node(expr).clone();
if let ExprNode::Add(ref children) = node {
let kids = children.clone();
let mut terms = Vec::with_capacity(kids.len());
for child in kids {
terms.push(do_inverse(arena, child, s, t, depth + 1)?);
}
return Ok(arena.add(&terms));
}
if let ExprNode::Neg(inner) = node {
let result = do_inverse(arena, inner, s, t, depth + 1)?;
return Ok(arena.neg(result));
}
if !contains_var(arena, expr, s) {
let d = arena.dirac_delta(t);
return Ok(arena.mul(&[expr, d]));
}
let (coeff, body) = split_independent(arena, expr, s);
if coeff != arena.one {
let result = do_inverse(arena, body, s, t, depth + 1)?;
return Ok(arena.mul(&[coeff, result]));
}
if let Some(result) = try_inverse_delay(arena, expr, s, t, depth)? {
return Ok(result);
}
if let Some(result) = try_special_inverse(arena, expr, s, t)? {
return Ok(result);
}
if let Some(result) = try_table_inverse(arena, expr, s, t) {
return Ok(result);
}
let decomposed = crate::transforms::apart::apart(arena, expr, s);
if decomposed != expr {
return do_inverse(arena, decomposed, s, t, depth + 1);
}
Err(SymplexError::ComputationFailed {
operation: "inverse_laplace_transform",
reason: "cannot invert expression".to_string(),
})
}
fn ifail(reason: impl Into<String>) -> SymplexError {
SymplexError::ComputationFailed {
operation: "inverse_laplace_transform",
reason: reason.into(),
}
}
fn try_inverse_delay(
arena: &mut Arena,
expr: ExprId,
s: ExprId,
t: ExprId,
depth: u32,
) -> Result<Option<ExprId>, SymplexError> {
let children: Vec<ExprId> = match arena.node(expr).clone() {
ExprNode::Mul(ch) => ch.iter().copied().collect(),
ExprNode::Exp(_) => vec![expr],
_ => return Ok(None),
};
let mut delay: Option<ExprId> = None;
let mut rest: Vec<ExprId> = Vec::new();
for &c in &children {
if delay.is_none()
&& let ExprNode::Exp(arg) = arena.node(c).clone()
&& let Some((k, b)) = linear_in(arena, arg, s)
&& arena.is_zero_structural(b)
{
let a = arena.neg(k);
let a = crate::transforms::eval::eval(arena, a);
match param_sign(arena, a) {
Some(sg) if sg > 0 => {
delay = Some(a);
continue;
}
_ => {}
}
}
rest.push(c);
}
let Some(a) = delay else {
return Ok(None);
};
if rest.is_empty() {
let t_minus_a = arena.sub(t, a);
return Ok(Some(arena.dirac_delta(t_minus_a)));
}
let g = arena.mul(&rest);
let g_t = do_inverse(arena, g, s, t, depth + 1)?;
let t_minus_a = arena.sub(t, a);
let shifted = crate::transforms::subs::subs(arena, g_t, t, t_minus_a);
let h = arena.heaviside(t_minus_a);
Ok(Some(arena.mul(&[shifted, h])))
}
fn try_special_inverse(
arena: &mut Arena,
expr: ExprId,
s: ExprId,
t: ExprId,
) -> Result<Option<ExprId>, SymplexError> {
let one = arena.one;
let two = arena.int(2);
let neg_one = arena.neg_one;
let neg_half = arena.rational(-1, 2);
let expr = flatten_nested_power(arena, expr);
match arena.node(expr).clone() {
ExprNode::Pow(base, e) if base == s && !contains_var(arena, e, s) => {
let nu = arena.neg(e);
let nu = crate::transforms::eval::eval(arena, nu);
if arena.as_num(nu).is_some_and(|r| r.is_integer()) {
return Ok(None); }
match param_sign(arena, nu) {
Some(sg) if sg > 0 => {}
Some(_) => {
return Err(ifail(
"s^k with k ≥ 0 is not an inverse-transformable function",
));
}
None => {
return Err(ifail(format!(
"requires ν > 0 in s^(-ν) (declare the sign of {} with Assumption::Positive)",
arena.display(nu)
)));
}
}
let nu_m1 = arena.sub(nu, one);
let tp = arena.pow(t, nu_m1);
let g = arena.gamma(nu);
let g = crate::transforms::eval::eval(arena, g);
Ok(Some(arena.div(tp, g)))
}
ExprNode::Pow(base, e) if !contains_var(arena, e, s) => {
let s2 = arena.pow(s, two);
if let ExprNode::Add(terms) = arena.node(base).clone()
&& terms.len() == 2
&& terms.contains(&s2)
{
let a2 = if terms[0] == s2 { terms[1] } else { terms[0] };
if contains_var(arena, a2, s) {
return Ok(None);
}
let a = match arena.node(a2).clone() {
ExprNode::Pow(b, k) if k == two && param_sign(arena, b) == Some(1) => b,
_ => {
if arena.as_num(a2).is_some() && e == neg_one {
return Ok(None); }
match param_sign(arena, a2) {
Some(1) => {
let r = arena.sqrt(a2);
crate::transforms::eval::eval(arena, r)
}
_ => return Ok(None),
}
}
};
let at = arena.mul(&[a, t]);
if e == neg_half {
let zero = arena.zero;
return Ok(Some(arena.besselj(zero, at)));
}
if e == neg_one {
let sn = arena.sin(at);
return Ok(Some(arena.div(sn, a)));
}
return Ok(None);
}
if let Some((k, b)) = linear_in(arena, base, s)
&& !arena.is_zero_structural(k)
&& let Some(r) = arena.as_num(e).cloned()
&& r.is_integer()
&& r.is_negative()
{
if arena.as_num(k).is_some() && arena.as_num(b).is_some() {
return Ok(None); }
let n: u64 = (-r.to_integer())
.try_into()
.map_err(|_| ifail("power too large"))?;
let a = arena.div(b, k);
let a = arena.neg(a);
let at = arena.mul(&[a, t]);
let ex = arena.exp(at);
let n_id = arena.int(n as i64);
let kn = arena.pow(k, n_id);
let nm1 = n - 1;
let tp = if nm1 == 0 {
one
} else {
let m = arena.int(nm1 as i64);
arena.pow(t, m)
};
let f = factorial(nm1);
let f_id = arena.num_ratio(Ratio::from_integer(f).clone());
let den = arena.mul(&[kn, f_id]);
let num = arena.mul(&[tp, ex]);
return Ok(Some(arena.div(num, den)));
}
Ok(None)
}
ExprNode::Atan(arg) => {
let inv_s = arena.pow(s, neg_one);
let a = match arena.node(arg).clone() {
ExprNode::Pow(..) if arg == inv_s => one,
ExprNode::Mul(ch) if ch.contains(&inv_s) => {
let rest: Vec<ExprId> = ch.iter().copied().filter(|&c| c != inv_s).collect();
if rest.iter().any(|&c| contains_var(arena, c, s)) {
return Ok(None);
}
arena.mul(&rest)
}
_ => return Ok(None),
};
let at = arena.mul(&[a, t]);
let sn = arena.sin(at);
Ok(Some(arena.div(sn, t)))
}
ExprNode::Mul(children) => {
let kids: Vec<ExprId> = children
.iter()
.map(|&k| flatten_nested_power(arena, k))
.collect();
let inv_s = arena.pow(s, neg_one);
if kids
.iter()
.any(|&k| matches!(arena.node(k), ExprNode::Add(_)))
{
let expanded = crate::transforms::expand::expand(arena, expr);
if expanded != expr && matches!(arena.node(expanded), ExprNode::Add(_)) {
return do_inverse(arena, expanded, s, t, 1).map(Some);
}
}
if kids.len() == 2 && kids.contains(&inv_s) {
let other = if kids[0] == inv_s { kids[1] } else { kids[0] };
if let ExprNode::Ln(arg) = arena.node(other).clone()
&& arg == s
{
let ln_t = arena.ln(t);
let g = arena.euler_gamma();
let sum = arena.add(&[ln_t, g]);
return Ok(Some(arena.neg(sum)));
}
}
if kids.len() == 2 && kids.contains(&s) {
let other = if kids[0] == s { kids[1] } else { kids[0] };
let s2 = arena.pow(s, two);
if let ExprNode::Pow(base, e) = arena.node(other).clone()
&& e == neg_one
&& let ExprNode::Add(terms) = arena.node(base).clone()
&& terms.len() == 2
&& terms.contains(&s2)
{
let a2 = if terms[0] == s2 { terms[1] } else { terms[0] };
if arena.as_num(a2).is_some() || contains_var(arena, a2, s) {
return Ok(None);
}
let a = match arena.node(a2).clone() {
ExprNode::Pow(b, k) if k == two && param_sign(arena, b) == Some(1) => b,
_ if param_sign(arena, a2) == Some(1) => {
let r = arena.sqrt(a2);
crate::transforms::eval::eval(arena, r)
}
_ => return Ok(None),
};
let at = arena.mul(&[a, t]);
return Ok(Some(arena.cos(at)));
}
}
if kids.len() == 2 && kids.contains(&inv_s) {
let other = if kids[0] == inv_s { kids[1] } else { kids[0] };
if let ExprNode::Pow(base, e) = arena.node(other).clone()
&& e == neg_half
&& let Some((k, a2)) = linear_in(arena, base, s)
&& k == one
&& param_sign(arena, a2) == Some(1)
{
let a = match arena.node(a2).clone() {
ExprNode::Pow(b, kk) if kk == two && param_sign(arena, b) == Some(1) => b,
_ => {
let r = arena.sqrt(a2);
crate::transforms::eval::eval(arena, r)
}
};
let rt = arena.sqrt(t);
let arg = arena.mul(&[a, rt]);
let er = arena.erf(arg);
return Ok(Some(arena.div(er, a)));
}
}
Ok(None)
}
_ => Ok(None),
}
}
fn try_table_inverse(arena: &mut Arena, expr: ExprId, s: ExprId, t: ExprId) -> Option<ExprId> {
let (numer, denom) = crate::poly::polybridge::as_numer_denom(arena, expr);
if denom == arena.one {
return None;
}
let denom_poly = crate::poly::polybridge::expr_to_poly(arena, denom, s)?;
let deg = denom_poly.degree()?;
if deg == 1 {
return inverse_degree1(arena, numer, &denom_poly, s, t);
}
if deg == 2 {
return inverse_degree2(arena, numer, &denom_poly, s, t);
}
if deg >= 2
&& let Some(result) = inverse_power_form(arena, numer, denom, &denom_poly, s, t)
{
return Some(result);
}
None
}
fn inverse_degree1(
arena: &mut Arena,
numer: ExprId,
denom_poly: &crate::poly::Poly,
s: ExprId,
t: ExprId,
) -> Option<ExprId> {
let c0 = denom_poly.coeff(0);
let c1 = denom_poly.coeff(1);
if c1.is_zero() {
return None;
}
let a_rat = -&c0 / &c1;
if !contains_var(arena, numer, s) {
let a_id = arena.num_ratio(a_rat.clone());
let at = arena.mul(&[a_id, t]);
let exp_at = arena.exp(at);
if a_rat.is_zero() {
let c1_id = arena.num_ratio(c1.clone());
return Some(arena.div(numer, c1_id));
}
let c1_id = arena.num_ratio(c1.clone());
let coeff = arena.div(numer, c1_id);
return Some(arena.mul(&[coeff, exp_at]));
}
let numer_poly = crate::poly::polybridge::expr_to_poly(arena, numer, s)?;
let numer_deg = numer_poly.degree()?;
if numer_deg == 0 {
let n0 = numer_poly.coeff(0);
let a_id = arena.num_ratio(a_rat.clone());
let at = arena.mul(&[a_id, t]);
let exp_at = arena.exp(at);
if a_rat.is_zero() {
let result_rat = n0 / c1;
return Some(arena.num_ratio(result_rat.clone()));
}
let scale = n0 / c1;
let scale_id = arena.num_ratio(scale.clone());
return Some(arena.mul(&[scale_id, exp_at]));
}
None
}
fn inverse_degree2(
arena: &mut Arena,
numer: ExprId,
denom_poly: &crate::poly::Poly,
s: ExprId,
t: ExprId,
) -> Option<ExprId> {
let c0 = denom_poly.coeff(0);
let c1 = denom_poly.coeff(1);
let c2 = denom_poly.coeff(2);
if c2.is_zero() {
return None;
}
let b = &c1 / &c2; let c = &c0 / &c2;
let numer_poly = crate::poly::polybridge::expr_to_poly(arena, numer, s);
if b.is_zero() {
if c.is_zero() {
if let Some(np) = &numer_poly {
let nd = np.degree().unwrap_or(0);
if nd == 0 {
let k = np.coeff(0);
let k_id = arena.num_ratio(k.clone());
let c2_id = arena.num_ratio(c2.clone());
let scale = arena.div(k_id, c2_id);
return Some(arena.mul(&[scale, t]));
}
}
return None;
}
if !c.is_positive() {
return None;
}
let omega_sq = c.clone();
let omega_id = {
let omega_sq_id = arena.num_ratio(omega_sq.clone());
let raw_sqrt = arena.sqrt(omega_sq_id);
crate::transforms::eval::eval(arena, raw_sqrt)
};
if let Some(np) = &numer_poly {
let nd = np.degree().unwrap_or(0);
if nd == 0 {
let k = np.coeff(0);
let k_id = arena.num_ratio(k.clone());
let c2_id = arena.num_ratio(c2.clone());
let omega_t = arena.mul(&[omega_id, t]);
let sin_omega_t = arena.sin(omega_t);
let c2_omega = arena.mul(&[c2_id, omega_id]);
let scale = arena.div(k_id, c2_omega);
return Some(arena.mul(&[scale, sin_omega_t]));
}
if nd == 1 {
let a0 = np.coeff(0);
let a1 = np.coeff(1);
let c2_id = arena.num_ratio(c2.clone());
let omega_t = arena.mul(&[omega_id, t]);
let sin_omega_t = arena.sin(omega_t);
let cos_omega_t = arena.cos(omega_t);
let mut terms = Vec::new();
if !a1.is_zero() {
let a1_id = arena.num_ratio(a1.clone());
let cos_coeff = arena.div(a1_id, c2_id);
terms.push(arena.mul(&[cos_coeff, cos_omega_t]));
}
if !a0.is_zero() {
let a0_id = arena.num_ratio(a0.clone());
let c2_omega = arena.mul(&[c2_id, omega_id]);
let sin_coeff = arena.div(a0_id, c2_omega);
terms.push(arena.mul(&[sin_coeff, sin_omega_t]));
}
if terms.is_empty() {
return Some(arena.zero);
} else if terms.len() == 1 {
return Some(terms[0]);
} else {
return Some(arena.add(&terms));
}
}
}
return None;
}
let alpha = -&b / Ratio::from_integer(BigInt::from(2));
let beta_sq = &c - &(&b * &b) / Ratio::from_integer(BigInt::from(4));
if beta_sq.is_zero() {
let alpha_id = arena.num_ratio(alpha.clone());
if let Some(np) = &numer_poly {
let nd = np.degree().unwrap_or(0);
let c2_id = arena.num_ratio(c2.clone());
if nd == 0 {
let k = np.coeff(0);
let k_id = arena.num_ratio(k.clone());
let scale = arena.div(k_id, c2_id);
if alpha.is_zero() {
return Some(arena.mul(&[scale, t]));
}
let alpha_t = arena.mul(&[alpha_id, t]);
let exp_alpha_t = arena.exp(alpha_t);
return Some(arena.mul(&[scale, t, exp_alpha_t]));
}
if nd == 1 {
let a0 = np.coeff(0);
let a1 = np.coeff(1);
let d_const = &a1 * &alpha + &a0;
let alpha_t = arena.mul(&[alpha_id, t]);
let exp_alpha_t = arena.exp(alpha_t);
let mut terms = Vec::new();
if !a1.is_zero() {
let a1_id = arena.num_ratio(a1.clone());
let exp_coeff = arena.div(a1_id, c2_id);
terms.push(arena.mul(&[exp_coeff, exp_alpha_t]));
}
if !d_const.is_zero() {
let d_id = arena.num_ratio(d_const.clone());
let t_exp_coeff = arena.div(d_id, c2_id);
terms.push(arena.mul(&[t_exp_coeff, t, exp_alpha_t]));
}
if terms.is_empty() {
return Some(arena.zero);
} else if terms.len() == 1 {
return Some(terms[0]);
} else {
return Some(arena.add(&terms));
}
}
}
return None;
}
if beta_sq.is_negative() {
let gamma_sq = -&beta_sq;
let gamma_sq_id = arena.num_ratio(gamma_sq.clone());
let gamma_id = {
let raw = arena.sqrt(gamma_sq_id);
crate::transforms::eval::eval(arena, raw)
};
let alpha_id = arena.num_ratio(alpha.clone());
if let Some(np) = &numer_poly {
let nd = np.degree().unwrap_or(0);
let c2_id = arena.num_ratio(c2.clone());
if nd == 0 {
let k = np.coeff(0);
let k_id = arena.num_ratio(k.clone());
let alpha_t = arena.mul(&[alpha_id, t]);
let exp_alpha_t = arena.exp(alpha_t);
let gamma_t = arena.mul(&[gamma_id, t]);
let sinh_gamma_t = arena.sinh(gamma_t);
let c2_gamma = arena.mul(&[c2_id, gamma_id]);
let scale = arena.div(k_id, c2_gamma);
return Some(arena.mul(&[scale, exp_alpha_t, sinh_gamma_t]));
}
if nd == 1 {
let a0 = np.coeff(0);
let a1 = np.coeff(1);
let d_const = &a1 * &alpha + &a0;
let alpha_t = arena.mul(&[alpha_id, t]);
let exp_alpha_t = arena.exp(alpha_t);
let gamma_t = arena.mul(&[gamma_id, t]);
let sinh_gamma_t = arena.sinh(gamma_t);
let cosh_gamma_t = arena.cosh(gamma_t);
let mut terms = Vec::new();
if !a1.is_zero() {
let a1_id = arena.num_ratio(a1.clone());
let cosh_coeff = arena.div(a1_id, c2_id);
terms.push(arena.mul(&[cosh_coeff, exp_alpha_t, cosh_gamma_t]));
}
if !d_const.is_zero() {
let d_id = arena.num_ratio(d_const.clone());
let c2_gamma = arena.mul(&[c2_id, gamma_id]);
let sinh_coeff = arena.div(d_id, c2_gamma);
terms.push(arena.mul(&[sinh_coeff, exp_alpha_t, sinh_gamma_t]));
}
if terms.is_empty() {
return Some(arena.zero);
} else if terms.len() == 1 {
return Some(terms[0]);
} else {
return Some(arena.add(&terms));
}
}
}
return None;
}
let beta_sq_id = arena.num_ratio(beta_sq.clone());
let beta_id = {
let raw = arena.sqrt(beta_sq_id);
crate::transforms::eval::eval(arena, raw)
};
let alpha_id = arena.num_ratio(alpha.clone());
if let Some(np) = &numer_poly {
let nd = np.degree().unwrap_or(0);
let c2_id = arena.num_ratio(c2.clone());
if nd == 0 {
let k = np.coeff(0);
let k_id = arena.num_ratio(k.clone());
let alpha_t = arena.mul(&[alpha_id, t]);
let exp_alpha_t = arena.exp(alpha_t);
let beta_t = arena.mul(&[beta_id, t]);
let sin_beta_t = arena.sin(beta_t);
let c2_beta = arena.mul(&[c2_id, beta_id]);
let scale = arena.div(k_id, c2_beta);
return Some(arena.mul(&[scale, exp_alpha_t, sin_beta_t]));
}
if nd == 1 {
let a0 = np.coeff(0);
let a1 = np.coeff(1);
let d_const = &a1 * &alpha + &a0;
let alpha_t = arena.mul(&[alpha_id, t]);
let exp_alpha_t = arena.exp(alpha_t);
let beta_t = arena.mul(&[beta_id, t]);
let sin_beta_t = arena.sin(beta_t);
let cos_beta_t = arena.cos(beta_t);
let mut terms = Vec::new();
if !a1.is_zero() {
let a1_id = arena.num_ratio(a1.clone());
let cos_coeff = arena.div(a1_id, c2_id);
terms.push(arena.mul(&[cos_coeff, exp_alpha_t, cos_beta_t]));
}
if !d_const.is_zero() {
let d_id = arena.num_ratio(d_const.clone());
let c2_beta = arena.mul(&[c2_id, beta_id]);
let sin_coeff = arena.div(d_id, c2_beta);
terms.push(arena.mul(&[sin_coeff, exp_alpha_t, sin_beta_t]));
}
if terms.is_empty() {
return Some(arena.zero);
} else if terms.len() == 1 {
return Some(terms[0]);
} else {
return Some(arena.add(&terms));
}
}
}
None
}
fn inverse_power_form(
arena: &mut Arena,
numer: ExprId,
denom: ExprId,
_denom_poly: &crate::poly::Poly,
s: ExprId,
t: ExprId,
) -> Option<ExprId> {
let denom_node = arena.node(denom).clone();
if let ExprNode::Pow(base, exp) = denom_node
&& let Some(r) = arena.as_num(exp).cloned()
&& r.is_integer()
&& r.is_positive()
{
let n_val: u64 = r.to_integer().try_into().ok()?;
let base_poly = crate::poly::polybridge::expr_to_poly(arena, base, s)?;
if base_poly.degree()? != 1 {
return None;
}
let bc0 = base_poly.coeff(0);
let bc1 = base_poly.coeff(1);
if !bc1.is_one() {
return None; }
let a_rat = -bc0;
if contains_var(arena, numer, s) {
return None;
}
let a_id = arena.num_ratio(a_rat.clone());
let at = arena.mul(&[a_id, t]);
let exp_at = arena.exp(at);
if n_val == 1 {
if a_rat.is_zero() {
return Some(numer);
}
return Some(arena.mul(&[numer, exp_at]));
}
let n_minus_1 = arena.int(n_val as i64 - 1);
let t_pow = arena.pow(t, n_minus_1);
let fact = factorial(n_val - 1);
let fact_rat = Ratio::from_integer(fact);
let fact_id = arena.num_ratio(fact_rat.clone());
let numer_t = arena.mul(&[numer, t_pow]);
let scaled = arena.div(numer_t, fact_id);
if a_rat.is_zero() {
return Some(scaled);
}
return Some(arena.mul(&[scaled, exp_at]));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::arena::Arena;
fn sym(a: &mut Arena, name: &str) -> ExprId {
a.symbol(name)
}
fn display(a: &Arena, id: ExprId) -> String {
a.display(id).to_string()
}
#[test]
fn forward_constant() {
let mut a = Arena::new();
let t = sym(&mut a, "t");
let s = sym(&mut a, "s");
let five = a.int(5);
let result = laplace_transform(&mut a, five, t, s).unwrap();
let d = display(&a, result);
assert!(
d.contains("5") && d.contains("s"),
"L{{5}} should be 5/s, got: {d}"
);
}
#[test]
fn forward_t() {
let mut a = Arena::new();
let t = sym(&mut a, "t");
let s = sym(&mut a, "s");
let result = laplace_transform(&mut a, t, t, s).unwrap();
let d = display(&a, result);
assert!(d.contains("s"), "L{{t}} should be 1/s², got: {d}");
}
#[test]
fn forward_exp() {
let mut a = Arena::new();
let t = sym(&mut a, "t");
let s = sym(&mut a, "s");
let two = a.int(2);
let two_t = a.mul(&[two, t]);
let exp_2t = a.exp(two_t);
let result = laplace_transform(&mut a, exp_2t, t, s).unwrap();
let d = display(&a, result);
assert!(
d.contains("s") && d.contains("2"),
"L{{exp(2t)}} should involve s and 2, got: {d}"
);
}
#[test]
fn forward_sin() {
let mut a = Arena::new();
let t = sym(&mut a, "t");
let s = sym(&mut a, "s");
let three = a.int(3);
let three_t = a.mul(&[three, t]);
let sin_3t = a.sin(three_t);
let result = laplace_transform(&mut a, sin_3t, t, s).unwrap();
let d = display(&a, result);
assert!(
d.contains("3") && d.contains("s"),
"L{{sin(3t)}} should involve 3 and s, got: {d}"
);
}
#[test]
fn forward_cos() {
let mut a = Arena::new();
let t = sym(&mut a, "t");
let s = sym(&mut a, "s");
let cos_t = a.cos(t);
let result = laplace_transform(&mut a, cos_t, t, s).unwrap();
let d = display(&a, result);
assert!(d.contains("s"), "L{{cos(t)}} should involve s, got: {d}");
}
#[test]
fn forward_linearity() {
let mut a = Arena::new();
let t = sym(&mut a, "t");
let s = sym(&mut a, "s");
let exp_t = a.exp(t);
let sin_t = a.sin(t);
let sum = a.add(&[exp_t, sin_t]);
let result = laplace_transform(&mut a, sum, t, s);
assert!(result.is_ok(), "linearity should work");
}
#[test]
fn forward_t_must_be_symbol() {
let mut a = Arena::new();
let t = a.int(5); let s = a.symbol("s");
let one = a.one;
let result = laplace_transform(&mut a, one, t, s);
assert!(result.is_err());
}
}