Repository: sympy/sympy
Pull request: Small errors in _laplace_rule_exp and _laplace_rule_trig corrected
#### References to other Issues or PRs

Fixes  #24294

#### Brief description of what is fixed or changed

The functions _laplace_rule_exp and _laplace_rule_trig did not return the
constant factors of their expressions.

#### Other comments


#### Release Notes

<!-- BEGIN RELEASE NOTES -->
* integrals
  * corrected small errors in Laplace transform rules
<!-- END RELEASE NOTES -->


Problem statement:
laplace_transform drops coefficient when Heaviside used
when using Heaviside with another function, I notice that adding a scalar coefficient (e.g., `2`) gets ignored in the output. I would expect `L(2 * sin(t) * u(t))` to be `2/(s**2 + 1)`, but `laplace_transform` outputs `1/(s**2 + 1)` (see **Out[5]** below).

Note that when I explicitly state `t > 0`, I get the answer I expected (**Out[9]**). But by setting `t > 0`, the Laplace transform of time-domain functions having a delta function get messed up (since 0 is excluded).

Is this a bug?

```python
In [1]: import sympy as sympy 
   ...: from sympy import laplace_transform, Heaviside, Symbol 
   ...: from sympy.abc import t, s

In [2]: laplace_transform(Heaviside(t), t, s)
Out[2]: (1/s, 0, True)

In [3]: laplace_transform(2 * Heaviside(t), t, s)
Out[3]: (2/s, 0, True)

In [4]: laplace_transform(sympy.sin(t) * Heaviside(t), t, s)
Out[4]: (1/(s**2 + 1), 0, True)

In [5]: laplace_transform(2 * sympy.sin(t) * Heaviside(t), t, s)
Out[5]: (1/(s**2 + 1), 0, True)

In [6]: laplace_transform(sympy.sin(t), t, s)
Out[6]: (1/(s**2 + 1), 0, True)

In [7]: laplace_transform(2 * sympy.sin(t), t, s)
Out[7]: (2/(s**2 + 1), 0, True)

In [8]: t = Symbol('t', positive=True)

In [9]: laplace_transform(2 * sympy.sin(t) * Heaviside(t), t, s)
Out[9]: (2/(s**2 + 1), 0, True)
```
---
Sympy version 1.11.1

Hints from issue discussion:
CC @hanspi42 

This seems to fix it but the code around seems a bit fishy so maybe more is needed:
```python
diff --git a/sympy/integrals/transforms.py b/sympy/integrals/transforms.py
index 071a6f1..4160acd 100644
--- a/sympy/integrals/transforms.py
+++ b/sympy/integrals/transforms.py
@@ -1694,6 +1694,7 @@ def _laplace_rule_trig(f, t, s, doit=True, **hints):
                 L = _laplace_apply_rules(ma1[z], t, s, doit=doit, **hints)
                 try:
                     r, p, c = L
+                    r = k * r
                     # The convergence plane changes only if the shift has been
                     # done along the real axis:
                     if sd==1:
```
With that:
```python
In [1]: from sympy.abc import t, s

In [2]: import sympy

In [3]: laplace_transform(2 * sympy.sin(t) * Heaviside(t), t, s)
Out[3]: 
⎛  2            ⎞
⎜──────, 0, True⎟
⎜ 2             ⎟
⎝s  + 1         ⎠
```
Yes, this looks like the correct fix. I will write test cases and implement it right away.

Commit message: Moved all constant-factor calculations to `_laplace_apply_rules`

Patch to review:
diff --git a/sympy/integrals/tests/test_transforms.py b/sympy/integrals/tests/test_transforms.py
index fa0a69085bef..884ad0a6d5b6 100644
--- a/sympy/integrals/tests/test_transforms.py
+++ b/sympy/integrals/tests/test_transforms.py
@@ -707,6 +707,10 @@ def test_laplace_transform():
     assert inverse_laplace_transform(
         f(w), w, t, plane=0) == InverseLaplaceTransform(f(w), w, t, 0)
     assert LT(f(t)*g(t), t, s) == LaplaceTransform(f(t)*g(t), t, s)
+    # Issue #24294
+    assert LT(b*f(a*t), t, s) == b*LaplaceTransform(f(t), t, s/a)/a
+    assert LT(3*exp(t)*Heaviside(t), t, s) == (3/(s - 1), 1, True)
+    assert LT(2*sin(t)*Heaviside(t), t, s) == (2/(s**2 + 1), 0, True)
 
     # additional basic tests from wikipedia
     assert LT((t - a)**b*exp(-c*(t - a))*Heaviside(t - a), t, s) == \
diff --git a/sympy/integrals/transforms.py b/sympy/integrals/transforms.py
index 071a6f111894..8318dc5c1175 100644
--- a/sympy/integrals/transforms.py
+++ b/sympy/integrals/transforms.py
@@ -1579,8 +1579,7 @@ def _laplace_rule_timescale(f, t, s, doit=True, **hints):
     _simplify = hints.pop('simplify', True)
     b = Wild('b', exclude=[t])
     g = WildFunction('g', nargs=1)
-    k, func = f.as_independent(t, as_Add=False)
-    ma1 = func.match(g)
+    ma1 = f.match(g)
     if ma1:
         arg = ma1[g].args[0].collect(t)
         ma2 = arg.match(b*t)
@@ -1591,18 +1590,18 @@ def _laplace_rule_timescale(f, t, s, doit=True, **hints):
             if ma2[b]==1:
                 if doit==True and not any(func.has(t) for func
                                           in ma1[g].atoms(AppliedUndef)):
-                    return k*_laplace_transform(ma1[g].func(t), t, s,
+                    return _laplace_transform(ma1[g].func(t), t, s,
                                                 simplify=_simplify)
                 else:
-                    return k*LaplaceTransform(ma1[g].func(t), t, s, **hints)
+                    return LaplaceTransform(ma1[g].func(t), t, s, **hints)
             else:
                 L = _laplace_apply_rules(ma1[g].func(t), t, s/ma2[b],
                                          doit=doit, **hints)
                 try:
                     r, p, c = L
-                    return (k/ma2[b]*r, p, c)
+                    return (1/ma2[b]*r, p, c)
                 except TypeError:
-                    return k/ma2[b]*L
+                    return 1/ma2[b]*L
     return None
 
 def _laplace_rule_heaviside(f, t, s, doit=True, **hints):
@@ -1615,8 +1614,7 @@ def _laplace_rule_heaviside(f, t, s, doit=True, **hints):
     b = Wild('b', exclude=[t])
     y = Wild('y')
     g = WildFunction('g', nargs=1)
-    k, func = f.as_independent(t, as_Add=False)
-    ma1 = func.match(Heaviside(y)*g)
+    ma1 = f.match(Heaviside(y)*g)
     if ma1:
         ma2 = ma1[y].match(t-a)
         ma3 = ma1[g].args[0].collect(t).match(t-b)
@@ -1627,9 +1625,9 @@ def _laplace_rule_heaviside(f, t, s, doit=True, **hints):
             L = _laplace_apply_rules(ma1[g].func(t), t, s, doit=doit, **hints)
             try:
                 r, p, c = L
-                return (k*exp(-ma2[a]*s)*r, p, c)
+                return (exp(-ma2[a]*s)*r, p, c)
             except TypeError:
-                return k*exp(-ma2[a]*s)*L
+                return exp(-ma2[a]*s)*L
     return None
 
 
@@ -1643,8 +1641,7 @@ def _laplace_rule_exp(f, t, s, doit=True, **hints):
 
     y = Wild('y')
     z = Wild('z')
-    k, func = f.as_independent(t, as_Add=False)
-    ma1 = func.match(exp(y)*z)
+    ma1 = f.match(exp(y)*z)
     if ma1:
         ma2 = ma1[y].collect(t).match(a*t)
         if ma2:
@@ -1669,7 +1666,6 @@ def _laplace_rule_trig(f, t, s, doit=True, **hints):
     a = Wild('a', exclude=[t])
     y = Wild('y')
     z = Wild('z')
-    k, func = f.as_independent(t, as_Add=False)
     # All of the rules have a very similar form: trig(y)*z is matched, and then
     # two copies of the Laplace transform of z are shifted in the s Domain
     # and added with a weight; see rules 1.6 to 1.9 in
@@ -1684,7 +1680,7 @@ def _laplace_rule_trig(f, t, s, doit=True, **hints):
                  (sin(y),  '1.8', -I, -1, I), (cos(y),  '1.9', 1, 1, I)]
     for trigrule in trigrules:
         fm, nu, s1, s2, sd = trigrule
-        ma1 = func.match(fm*z)
+        ma1 = f.match(fm*z)
         if ma1:
             ma2 = ma1[y].collect(t).match(a*t)
             if ma2:
@@ -1777,8 +1773,13 @@ def _laplace_apply_rules(f, t, s, doit=True, **hints):
     prog_rules = [_laplace_rule_timescale, _laplace_rule_heaviside,
                   _laplace_rule_exp, _laplace_rule_trig, _laplace_rule_diff]
     for p_rule in prog_rules:
-        LT = p_rule(f, t, s, doit=doit, **hints)
+        LT = p_rule(func, t, s, doit=doit, **hints)
         if LT is not None:
+            try:
+                r, p, c = LT
+                return (k*r, p, c)
+            except TypeError:
+                return k*LT
             return LT
     return None
 


Code review comments:
diff_hunk: @@ -1777,8 +1773,13 @@ def _laplace_apply_rules(f, t, s, doit=True, **hints):
     prog_rules = [_laplace_rule_timescale, _laplace_rule_heaviside,
                   _laplace_rule_exp, _laplace_rule_trig, _laplace_rule_diff]
     for p_rule in prog_rules:
-        LT = p_rule(f, t, s, doit=doit, **hints)
+        LT = p_rule(func, t, s, doit=doit, **hints)
         if LT is not None:
+            try:
+                r, p, c = LT
+                return (k*r, p, c)
+            except TypeError:
+                return k*LT
path: sympy/integrals/transforms.py
text: @user1:
Rather than `try/except` it would be better to test the condition explicitly e.g.:
```python
conds = hints.get('noconds', False):
if conds:
    r, p, c = LT
    return (k*r, p, c)
else:
    return k*LT
```
This is better because:
- It is more explicit
- We should generally avoid catching TypeError which is usually indicative of bugs
- Some expressions are iterable so there is a possibility of confusion here:
   ```python
   In [1]: r, p, c = Matrix([1, 2, 3])

   In [2]: r, p, c
   Out[2]: (1, 2, 3)
   ```
- This also ensures that when `noconds` is set we will see an error if the wrong type of object was returned here.

In general catching exceptions is something that should be used for exceptional rather than expected cases.

@author:
That was how we handled it a year ago, but I agree it is not good. I have rewritten all instances. However, since it is not always certain inside the recursive rule engine that conditions are returned even if `nocond` is not there, I am now also checking whether the return value's type is tuple.

Push will follow soon ...

@user1:
> it is not always certain inside the recursive rule engine that conditions are returned even if `nocond` is not there

This is precisely why I would check `nocond`: if `nocond` is passed and a tuple is being returned then that's a bug is it not? Surely in that case we should *want* to see a `TypeError` rather than silently passing the wrong type through.

@author:
It is the other way around: if noconds is not passed, it can still be that no tuple is returned inside the recursive tree. 

@author:
To explain a bit better: if, in the recursion tree, the Laplace transform cannot be evaluated, an unevaluated `LaplaceTransform()` is returned. So even if `noconds` is not set, and conditions should be derived, the result may not be a tuple, but a `LaplaceTransform()` object with `**hints` in the argument that do not contain `noconds`.

Without the type tuple check, e.g., `LT(b*f(a*t), t, s)` would fail.

@user1:
Okay that makes sense. I think it's not a good API to return either a tuple or a LaplaceTransform but I guess that can't be changed now.