Repository: sympy/sympy

Issue:
P(X < oo ) for any Continuous Distribution raises AttributeError
```
>>> from sympy import *
>>> from sympy.stats import P, Exponential
>>> X = Exponential('X', 3)
>>> P(X < oo)

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-5-c320871e22f8> in <module>()
----> 1 P(X < oo)

/home/gxyd/Public/sympy/sympy/stats/rv.py in probability(condition, given_condition, numsamples, evaluate, **kwargs)
    617 
    618     # Otherwise pass work off to the ProbabilitySpace
--> 619     result = pspace(condition).probability(condition, **kwargs)
    620     if evaluate and hasattr(result, 'doit'):
    621         return result.doit()

AttributeError: 'NoneType' object has no attribute 'probability'
```

Discussion:
jksuom: This looks good.

There are other (nonsensical) cases where ``pspace(condition)`` returns ``None`` and ``AttributeError`` results, for example ``P(1)``. Perhaps these should also be caught and something more informative be returned (maybe ``nan``).
```
gxyd: > Perhaps these should also be caught and something more informative be returned (maybe nan).

Yes, i have added `ValueError`, but since `condition` is expected to be a `Relational`, so i think returning `ValueError` looks better to me instead of `nan`.
jksuom: It is a good idea to catch any ``condition`` that is not a ``Relational``.
In addition, I think it would be advisable to make sure that ``pspace(condition)`` does not return ``None``. That can happen even when ``condition`` is a ``Relational`` if it does not contain random variables. For example, ``P(x < 1)`` for an ordinary symbol ``x``.
gxyd: > It is a good idea to catch any condition that is not a Relational.

It can even take non-relationals like: `And(X + Y < 3, Y < 2) `. That's why
the current tests fail. I think i should use some, `.has(Relational)`
property  for that may be.

On Wed, Oct 28, 2015 at 12:41 PM, Kalevi Suominen <notifications@github.com>
wrote:

> It is a good idea to catch any condition that is not a Relational.
> In addition, I think it would be advisable to make sure that
> pspace(condition) does not return None. That can happen even when
> condition is a Relational if it does not contain random variables. For
> example, P(x < 1) for an ordinary symbol x.
>
> —
> Reply to this email directly or view it on GitHub
> <https://github.com/sympy/sympy/pull/10053#issuecomment-151749233>.
>

gxyd: Ex. P(And(X + Y < 2, X > 1)) where `X` and `Y` are both `RandomSymbol`.

On Wed, Oct 28, 2015 at 12:46 PM, Gaurav Dhingra <axyd0000@gmail.com> wrote:

> > It is a good idea to catch any condition that is not a Relational.
>
> It can even take non-relationals like: `And(X + Y < 3, Y < 2) `. That's
> why the current tests fail. I think i should use some, `.has(Relational)`
> property  for that may be.
>
> On Wed, Oct 28, 2015 at 12:41 PM, Kalevi Suominen <
> notifications@github.com> wrote:
>
>> It is a good idea to catch any condition that is not a Relational.
>> In addition, I think it would be advisable to make sure that
>> pspace(condition) does not return None. That can happen even when
>> condition is a Relational if it does not contain random variables. For
>> example, P(x < 1) for an ordinary symbol x.
>>
>> —
>> Reply to this email directly or view it on GitHub
>> <https://github.com/sympy/sympy/pull/10053#issuecomment-151749233>.
>>
>
>

jksuom: Yes, there are ``BooleanFunction``s like ``And`` that should be accepted. Perhaps everything that derives from ``Boolean`` should be allowed. That would simplify the test.
gxyd: I too think it is better for `given(condition)` to raise an Error instead of returning `None`. It is now ready for review.

Patch:
diff --git a/sympy/stats/rv.py b/sympy/stats/rv.py
index ff9060cf6e..b3686a1fab 100644
--- a/sympy/stats/rv.py
+++ b/sympy/stats/rv.py
@@ -15,7 +15,9 @@
 from __future__ import print_function, division
 
 from sympy import (Basic, S, Expr, Symbol, Tuple, And, Add, Eq, lambdify,
-        Equality, Lambda, DiracDelta)
+        Equality, Lambda, DiracDelta, sympify)
+from sympy.core.relational import Relational
+from sympy.logic.boolalg import Boolean
 from sympy.solvers.solveset import solveset
 from sympy.sets.sets import FiniteSet, ProductSet, Intersection
 from sympy.abc import x
@@ -121,7 +123,7 @@ class PSpace(Basic):
     A Probability Space
 
     Probability Spaces encode processes that equal different values
-    probabalistically. These underly Random Symbols which occur in SymPy
+    probabilistically. These underly Random Symbols which occur in SymPy
     expressions and contain the mechanics to evaluate statistical statements.
 
     See Also
@@ -422,9 +424,10 @@ def pspace(expr):
     True
     """
 
+    expr = sympify(expr)
     rvs = random_symbols(expr)
     if not rvs:
-        return None
+        raise ValueError("Expression containing Random Variable expected, not %s" % (expr))
     # If only one space present
     if all(rv.pspace == rvs[0].pspace for rv in rvs):
         return rvs[0].pspace
@@ -469,7 +472,7 @@ def given(expr, condition=None, **kwargs):
 
     >>> from sympy.stats import given, density, Die
     >>> X = Die('X', 6)
-    >>> Y = given(X, X>3)
+    >>> Y = given(X, X > 3)
     >>> density(Y).dict
     {4: 1/3, 5: 1/3, 6: 1/3}
 
@@ -550,7 +553,7 @@ def expectation(expr, condition=None, numsamples=None, evaluate=True, **kwargs):
     >>> E(2*X + 1)
     8
 
-    >>> E(X, X>3) # Expectation of X given that it is above 3
+    >>> E(X, X > 3) # Expectation of X given that it is above 3
     5
     """
 
@@ -585,10 +588,11 @@ def probability(condition, given_condition=None, numsamples=None,
     Parameters
     ==========
 
-    condition : Relational containing RandomSymbols
+    condition : Combination of Relationals containing RandomSymbols
         The condition of which you want to compute the probability
-    given_condition : Relational containing RandomSymbols
-        A conditional expression. P(X>1, X>0) is expectation of X>1 given X>0
+    given_condition : Combination of Relationals containing RandomSymbols
+        A conditional expression. P(X > 1, X > 0) is expectation of X > 1
+        given X > 0
     numsamples : int
         Enables sampling and approximates the probability with this many samples
     evaluate : Bool (defaults to True)
@@ -608,6 +612,23 @@ def probability(condition, given_condition=None, numsamples=None,
     5/12
     """
 
+    condition = sympify(condition)
+    given_condition = sympify(given_condition)
+
+    if given_condition is not None and \
+            not isinstance(given_condition, (Relational, Boolean)):
+        raise ValueError("%s is not a relational or combination of relationals"
+                % (given_condition))
+    if given_condition == False:
+        return S.Zero
+    if not isinstance(condition, (Relational, Boolean)):
+        raise ValueError("%s is not a relational or combination of relationals"
+                % (condition))
+    if condition is S.true:
+        return S.One
+    if condition is S.false:
+        return S.Zero
+
     if numsamples:
         return sampling_P(condition, given_condition, numsamples=numsamples,
                 **kwargs)
@@ -667,7 +688,8 @@ def density(expr, condition=None, evaluate=True, numsamples=None, **kwargs):
     expr : Expr containing RandomSymbols
         The expression of which you want to compute the density value
     condition : Relational containing RandomSymbols
-        A conditional expression. density(X>1, X>0) is density of X>1 given X>0
+        A conditional expression. density(X > 1, X > 0) is density of X > 1
+        given X > 0
     numsamples : int
         Enables sampling and approximates the density with this many samples
 
@@ -720,7 +742,7 @@ def cdf(expr, condition=None, evaluate=True, **kwargs):
     {1: 1/6, 2: 1/6, 3: 1/6, 4: 1/6, 5: 1/6, 6: 1/6}
     >>> cdf(D)
     {1: 1/6, 2: 1/3, 3: 1/2, 4: 2/3, 5: 5/6, 6: 1}
-    >>> cdf(3*D, D>2)
+    >>> cdf(3*D, D > 2)
     {9: 1/4, 12: 1/2, 15: 3/4, 18: 1}
 
     >>> cdf(X)


Test patch:
diff --git a/sympy/stats/tests/test_rv.py b/sympy/stats/tests/test_rv.py
index 69115c2771..52ef81f9f1 100644
--- a/sympy/stats/tests/test_rv.py
+++ b/sympy/stats/tests/test_rv.py
@@ -7,6 +7,7 @@
 from sympy.stats.rv import ProductPSpace, rs_swap, Density, NamedArgsMixin
 from sympy.utilities.pytest import raises, XFAIL
 from sympy.core.compatibility import range
+from sympy.abc import x
 
 
 def test_where():
@@ -45,7 +46,8 @@ def test_random_symbols():
 def test_pspace():
     X, Y = Normal('X', 0, 1), Normal('Y', 0, 1)
 
-    assert not pspace(5 + 3)
+    raises(ValueError, lambda: pspace(5 + 3))
+    raises(ValueError, lambda: pspace(x < 1))
     assert pspace(X) == X.pspace
     assert pspace(2*X + 1) == X.pspace
     assert pspace(2*X + Y) == ProductPSpace(Y.pspace, X.pspace)
@@ -195,3 +197,15 @@ def test_density_constant():
 def test_real():
     x = Normal('x', 0, 1)
     assert x.is_real
+
+
+def test_issue_10052():
+    X = Exponential('X', 3)
+    assert P(X < oo) == 1
+    assert P(X > oo) == 0
+    assert P(X < 2, X > oo) == 0
+    assert P(X < oo, X > oo) == 0
+    assert P(X < oo, X > 2) == 1
+    assert P(X < 3, X == 2) == 0
+    raises(ValueError, lambda: P(1))
+    raises(ValueError, lambda: P(X < 1, 2))