Repository: sympy/sympy

Issue:
Expected Value of the Product of Two Uniform RVs Fails
See title. As these variables are independent, I would expect this to give the result `E[XY] = E[X] * E[Y] = 0 * 0 = 0`

```python
from sympy import stats

X = stats.Uniform('X', -1, +1)
Y = stats.Uniform('Y', -1, +1)

stats.E(X * Y)
```

Results in `TypeError: 'NegativeInfinity' object cannot be interpreted as an integer`

Discussion:
sympy-bot: :white_check_mark:

Hi, I am the [SymPy bot](https://github.com/sympy/sympy-bot) (v161). I'm here to help you write a release notes entry. Please read the [guide on how to write release notes](https://github.com/sympy/sympy/wiki/Writing-Release-Notes).



Your release notes are in good order.

Here is what the release notes will look like:
* stats
  - Expectation can now be calculated across multiple Uniform RVs evaluating to zero ([#20771](https://github.com/sympy/sympy/pull/20771) by [@kc611](https://github.com/kc611))

This will be added to https://github.com/sympy/sympy/wiki/Release-Notes-for-1.8.

<details><summary>Click here to see the pull request description that was parsed.</summary>

    <!-- Your title above should be a short description of what
    was changed. Do not include the issue number in the title. -->

    #### References to other Issues or PRs
    <!-- If this pull request fixes an issue, write "Fixes #NNNN" in that exact
    format, e.g. "Fixes #1234" (see
    https://tinyurl.com/auto-closing for more information). Also, please
    write a comment on that issue linking back to this pull request once it is
    open. -->
    Fixes #20756

    #### Brief description of what is fixed or changed

    The issue was arising due to `SingleContinuousDistribution` trying to calculate expectation using Taylor series. 

    Since degree of `Poly(0)` is `-oo` It tries to calculate series of values the mgf over infinite points.

    https://github.com/sympy/sympy/blob/ab39cbb48e23284889ca851c35845ec9b66a37c9/sympy/stats/crv.py#L427-L428

    Rather, It'll be better for it to follow the simple integration path when expression is zero. The case in which it'll simply return zero for integrating over zero.   

    https://github.com/sympy/sympy/blob/ab39cbb48e23284889ca851c35845ec9b66a37c9/sympy/stats/crv.py#L436

    #### Other comments


    #### Release Notes


    <!-- Write the release notes for this release below. See
    https://github.com/sympy/sympy/wiki/Writing-Release-Notes for more information
    on how to write release notes. The bot will check your release notes
    automatically to see if they are formatted correctly. -->

    <!-- BEGIN RELEASE NOTES -->

    - stats
      - Expectation can now be calculated across multiple Uniform RVs evaluating to zero

    <!-- END RELEASE NOTES -->

</details><p>


Patch:
diff --git a/doc/src/gotchas.rst b/doc/src/gotchas.rst
index aaedc01476..f907b79e79 100644
--- a/doc/src/gotchas.rst
+++ b/doc/src/gotchas.rst
@@ -269,40 +269,6 @@ trick for getting tab completion in the regular Python console.
     See also `What is the best way to create symbols?
     <https://github.com/sympy/sympy/wiki/Faq>`_ in the FAQ.
 
-.. _calling-functions:
-
-Functions
----------
-
-A function like ``f(x)`` can be created by defining the Function
-and the variable:
-
-    >>> from sympy import Function
-    >>> f = Function('f')
-    >>> x = Symbol('x')
-    >>> f(x)
-    f(x)
-
-If you assign ``f(x)`` to a Python variable `f` you will lose your
-ability to copy and paste that function or to create a function
-with a different argument: ``Function('f')`` is callable, but
-``Function('f')(x)`` is not:
-
-    >>> f1 = Function('f1')
-    >>> f2 = Function('f2')('x')
-    >>> f1
-    f1
-    >>> f2
-    f2(x)
-    >>> f1(1)
-    f1(1)
-    >>> f2(1)
-    Traceback (most recent call last):
-    ...
-    TypeError: 'f2' object is not callable
-    >>> f2.subs(x, 1)
-    f2(1)
-
 .. _symbolic-expressions:
 
 Symbolic Expressions
diff --git a/sympy/matrices/decompositions.py b/sympy/matrices/decompositions.py
index 964c6a5058..e8af9d2777 100644
--- a/sympy/matrices/decompositions.py
+++ b/sympy/matrices/decompositions.py
@@ -577,8 +577,8 @@ def _LUdecomposition(M, iszerofunc=_iszero, simpfunc=None, rankcheck=False):
     """Returns (L, U, perm) where L is a lower triangular matrix with unit
     diagonal, U is an upper triangular matrix, and perm is a list of row
     swap index pairs. If A is the original matrix, then
-    ``A = (L*U).permuteBkwd(perm)``, and the row permutation matrix P such
-    that $P A = L U$ can be computed by ``P = eye(A.rows).permuteFwd(perm)``.
+    A = (L*U).permuteBkwd(perm), and the row permutation matrix P such
+    that P*A = L*U can be computed by P=eye(A.row).permuteFwd(perm).
 
     See documentation for LUCombined for details about the keyword argument
     rankcheck, iszerofunc, and simpfunc.
diff --git a/sympy/printing/str.py b/sympy/printing/str.py
index 6ec482e6c7..8a20c7c325 100644
--- a/sympy/printing/str.py
+++ b/sympy/printing/str.py
@@ -807,13 +807,13 @@ def _print_WildFunction(self, expr):
         return expr.name + '_'
 
     def _print_WildDot(self, expr):
-        return expr.name
+        return expr.name + '_'
 
     def _print_WildPlus(self, expr):
-        return expr.name
+        return expr.name + '__'
 
     def _print_WildStar(self, expr):
-        return expr.name
+        return expr.name + '___'
 
     def _print_Zero(self, expr):
         if self._settings.get("sympy_integers", False):
diff --git a/sympy/stats/crv.py b/sympy/stats/crv.py
index 632363ad21..830d196342 100644
--- a/sympy/stats/crv.py
+++ b/sympy/stats/crv.py
@@ -420,6 +420,8 @@ def expectation(self, expr, var, evaluate=True, **kwargs):
         if evaluate:
             try:
                 p = poly(expr, var)
+                if p.is_zero:
+                    return S.Zero
                 t = Dummy('t', real=True)
                 mgf = self._moment_generating_function(t)
                 if mgf is None:
diff --git a/sympy/utilities/matchpy_connector.py b/sympy/utilities/matchpy_connector.py
index 66f5f3f768..c2e20fa534 100644
--- a/sympy/utilities/matchpy_connector.py
+++ b/sympy/utilities/matchpy_connector.py
@@ -2,16 +2,13 @@
 The objects in this module allow the usage of the MatchPy pattern matching
 library on SymPy expressions.
 """
-import re
-from typing import List, Callable
-
 from sympy.core.sympify import _sympify
 from sympy.external import import_module
 from sympy.functions import (log, sin, cos, tan, cot, csc, sec, erf, gamma, uppergamma)
 from sympy.functions.elementary.hyperbolic import acosh, asinh, atanh, acoth, acsch, asech, cosh, sinh, tanh, coth, sech, csch
 from sympy.functions.elementary.trigonometric import atan, acsc, asin, acot, acos, asec
 from sympy.functions.special.error_functions import fresnelc, fresnels, erfc, erfi, Ei
-from sympy import (Basic, Mul, Add, Pow, Integral, exp, Symbol, Expr, srepr, Equality, Unequality)
+from sympy import (Basic, Mul, Add, Pow, Integral, exp, Symbol)
 from sympy.utilities.decorator import doctest_depends_on
 
 matchpy = import_module("matchpy")
@@ -34,11 +31,6 @@
     CommutativeOperation.register(Mul)
     AssociativeOperation.register(Mul)
 
-    Operation.register(Equality)
-    CommutativeOperation.register(Equality)
-    Operation.register(Unequality)
-    CommutativeOperation.register(Unequality)
-
     Operation.register(exp)
     Operation.register(log)
     Operation.register(gamma)
@@ -139,9 +131,6 @@ def __copy__(self) -> '_WildAbstract':
     def __repr__(self):
         return str(self)
 
-    def __str__(self):
-        return self.name
-
 
 @doctest_depends_on(modules=('matchpy',))
 class WildDot(_WildAbstract):
@@ -159,110 +148,3 @@ class WildPlus(_WildAbstract):
 class WildStar(_WildAbstract):
     min_length = 0
     fixed_size = False
-
-
-def _get_srepr(expr):
-    s = srepr(expr)
-    s = re.sub(r"WildDot\('(\w+)'\)", r"\1", s)
-    s = re.sub(r"WildPlus\('(\w+)'\)", r"*\1", s)
-    s = re.sub(r"WildStar\('(\w+)'\)", r"*\1", s)
-    return s
-
-
-@doctest_depends_on(modules=('matchpy',))
-class Replacer:
-    """
-    Replacer object to perform multiple pattern matching and subexpression
-    replacements in SymPy expressions.
-
-    Examples
-    ========
-
-    Example to construct a simple first degree equation solver:
-
-    >>> from sympy.utilities.matchpy_connector import WildDot, Replacer
-    >>> from sympy import Equality, Symbol
-    >>> x = Symbol("x")
-    >>> a_ = WildDot("a_", optional=1)
-    >>> b_ = WildDot("b_", optional=0)
-
-    The lines above have defined two wildcards, ``a_`` and ``b_``, the
-    coefficients of the equation `a x + b = 0`. The optional values specified
-    indicate which expression to return in case no match is found, they are
-    necessary in equations like `a x = 0` and `x + b = 0`.
-
-    Create two constraints to make sure that ``a_`` and ``b_`` will not match
-    any expression containing ``x``:
-
-    >>> from matchpy import CustomConstraint
-    >>> free_x_a = CustomConstraint(lambda a_: not a_.has(x))
-    >>> free_x_b = CustomConstraint(lambda b_: not b_.has(x))
-
-    Now create the rule replacer with the constraints:
-
-    >>> replacer = Replacer(common_constraints=[free_x_a, free_x_b])
-
-    Add the matching rule:
-
-    >>> replacer.add(Equality(a_*x + b_, 0), -b_/a_)
-
-    Let's try it:
-
-    >>> replacer.replace(Equality(3*x + 4, 0))
-    -4/3
-
-    Notice that it won't match equations expressed with other patterns:
-
-    >>> eq = Equality(3*x, 4)
-    >>> replacer.replace(eq)
-    Eq(3*x, 4)
-
-    In order to extend the matching patterns, define another one (we also need
-    to clear the cache, because the previous result has already been memorized
-    and the pattern matcher won't iterate again if given the same expression)
-
-    >>> replacer.add(Equality(a_*x, b_), b_/a_)
-    >>> replacer._replacer.matcher.clear()
-    >>> replacer.replace(eq)
-    4/3
-    """
-
-    def __init__(self, common_constraints: list = []):
-        self._replacer = matchpy.ManyToOneReplacer()
-        self._common_constraint = common_constraints
-
-    def _get_lambda(self, lambda_str: str) -> Callable[..., Expr]:
-        exec("from sympy import *")
-        return eval(lambda_str, locals())
-
-    def _get_custom_constraint(self, constraint_expr: Expr, condition_template: str) -> Callable[..., Expr]:
-        wilds = list(map(lambda x: x.name, constraint_expr.atoms(_WildAbstract)))
-        lambdaargs = ', '.join(wilds)
-        fullexpr = _get_srepr(constraint_expr)
-        condition = condition_template.format(fullexpr)
-        return matchpy.CustomConstraint(
-            self._get_lambda(f"lambda {lambdaargs}: ({condition})"))
-
-    def _get_custom_constraint_nonfalse(self, constraint_expr: Expr) -> Callable[..., Expr]:
-        return self._get_custom_constraint(constraint_expr, "({}) != False")
-
-    def _get_custom_constraint_true(self, constraint_expr: Expr) -> Callable[..., Expr]:
-        return self._get_custom_constraint(constraint_expr, "({}) == True")
-
-    def add(self, expr: Expr, result: Expr, conditions_true: List[Expr] = [], conditions_nonfalse: List[Expr] = []) -> None:
-        expr = _sympify(expr)
-        result = _sympify(result)
-        lambda_str = f"lambda {', '.join(map(lambda x: x.name, expr.atoms(_WildAbstract)))}: {_get_srepr(result)}"
-        lambda_expr = self._get_lambda(lambda_str)
-        constraints = self._common_constraint[:]
-        constraint_conditions_true = [
-            self._get_custom_constraint_true(cond) for cond in conditions_true]
-        constraint_conditions_nonfalse = [
-            self._get_custom_constraint_nonfalse(cond) for cond in conditions_nonfalse]
-        constraints.extend(constraint_conditions_true)
-        constraints.extend(constraint_conditions_nonfalse)
-        self._replacer.add(
-            matchpy.ReplacementRule(matchpy.Pattern(expr, *constraints), lambda_expr))
-
-    def replace(self, expr: Expr) -> Expr:
-        return self._replacer.replace(expr)


Test patch:
diff --git a/sympy/printing/tests/test_str.py b/sympy/printing/tests/test_str.py
index 77be2df38f..cb00abd867 100644
--- a/sympy/printing/tests/test_str.py
+++ b/sympy/printing/tests/test_str.py
@@ -726,9 +726,9 @@ def test_wild_matchpy():
     if matchpy is None:
         return
 
-    wd = WildDot('w_')
-    wp = WildPlus('w__')
-    ws = WildStar('w___')
+    wd = WildDot('w')
+    wp = WildPlus('w')
+    ws = WildStar('w')
 
     assert str(wd) == 'w_'
     assert str(wp) == 'w__'
diff --git a/sympy/stats/tests/test_continuous_rv.py b/sympy/stats/tests/test_continuous_rv.py
index 463db11720..0ed812c87b 100644
--- a/sympy/stats/tests/test_continuous_rv.py
+++ b/sympy/stats/tests/test_continuous_rv.py
@@ -1558,6 +1558,13 @@ def test_issue_13324():
     assert E(X, X > S.Half) == Rational(3, 4)
     assert E(X, X > 0) == S.Half
 
+def test_issue_20756():
+    X = Uniform('X', -1, +1)
+    Y = Uniform('Y', -1, +1)
+    assert E(X * Y) == S.Zero
+    assert E(X * ((Y + 1) - 1)) == S.Zero
+    assert E(Y * (X*(X + 1) - X*X)) == S.Zero
+
 def test_FiniteSet_prob():
     E = Exponential('E', 3)
     N = Normal('N', 5, 7)
diff --git a/sympy/utilities/tests/test_matchpy_connector.py b/sympy/utilities/tests/test_matchpy_connector.py
index 8100c00b48..4e74a3c9b1 100644
--- a/sympy/utilities/tests/test_matchpy_connector.py
+++ b/sympy/utilities/tests/test_matchpy_connector.py
@@ -1,7 +1,6 @@
-from sympy import symbols, cos, sin, S, Eq, Equality, sqrt, Ne
+from sympy import symbols, cos, sin
 from sympy.external import import_module
-from sympy.testing.pytest import skip
-from sympy.utilities.matchpy_connector import WildDot, WildPlus, WildStar, Replacer
+from sympy.utilities.matchpy_connector import WildDot, WildPlus, WildStar
 
 matchpy = import_module("matchpy")
 
@@ -18,7 +17,7 @@ def _get_first_match(expr, pattern):
 
 def test_matchpy_connector():
     if matchpy is None:
-        skip("matchpy not installed")
+        return
 
     from multiset import Multiset
     from matchpy import Pattern, Substitution
@@ -48,7 +47,7 @@ def test_matchpy_connector():
 
 def test_matchpy_optional():
     if matchpy is None:
-        skip("matchpy not installed")
+        return
 
     from matchpy import Pattern, Substitution
     from matchpy import ManyToOneReplacer, ReplacementRule
@@ -84,43 +83,3 @@ def test_matchpy_optional():
     assert replacer.replace(expr2) == sin(1)*cos(3)
     assert replacer.replace(expr3) == sin(1)*cos(0)
     assert replacer.replace(expr4) == sin(y)*cos(z)
-
-
-def test_replacer():
-    if matchpy is None:
-        skip("matchpy not installed")
-
-    x1_ = WildDot("x1_")
-    x2_ = WildDot("x2_")
-
-    a_ = WildDot("a_", optional=S.One)
-    b_ = WildDot("b_", optional=S.One)
-    c_ = WildDot("c_", optional=S.Zero)
-
-    replacer = Replacer(common_constraints=[
-        matchpy.CustomConstraint(lambda a_: not a_.has(x)),
-        matchpy.CustomConstraint(lambda b_: not b_.has(x)),
-        matchpy.CustomConstraint(lambda c_: not c_.has(x)),
-    ])
-
-    # Rewrite the equation into implicit form, unless it's already solved:
-    replacer.add(Eq(x1_, x2_), Eq(x1_ - x2_, 0), conditions_nonfalse=[Ne(x2_, 0), Ne(x1_, 0), Ne(x1_, x), Ne(x2_, x)])
-
-    # Simple equation solver for real numbers:
-    replacer.add(Equality(a_*x + b_, 0), Equality(x, -b_/a_))
-    disc = b_**2 - 4*a_*c_
-    replacer.add(
-        Equality(a_*x**2 + b_*x + c_, 0),
-        Eq(x, (-b_ - sqrt(disc))/(2*a_)) | Eq(x, (-b_ + sqrt(disc))/(2*a_)),
-        conditions_nonfalse=[disc >= 0]
-    )
-    replacer.add(
-        Equality(a_*x**2 + c_, 0),
-        Eq(x, sqrt(-c_/a_)) | Eq(x, -sqrt(-c_/a_)),
-        conditions_nonfalse=[-c_*a_ > 0]
-    )
-
-    assert replacer.replace(Eq(3*x, y)) == Eq(x, y/3)
-    assert replacer.replace(Eq(x**2 + 1, 0)) == Eq(x**2 + 1, 0)
-    assert replacer.replace(Eq(x**2, 4)) == (Eq(x, 2) | Eq(x, -2))
-    assert replacer.replace(Eq(x**2 + 4*y*x + 4*y**2, 0)) == Eq(x, -2*y)