Symbolic and fuzzy booleans

This page describes what a symbolic Boolean in SymPy is and also how that relates to three-valued fuzzy-bools that are used in many parts of SymPy. It also discusses some common problems that arise when writing code that uses three-valued logic and how to handle them correctly.

Symbolic Boolean vs three valued bool

Assumptions queries like x.ispositive give fuzzy-bool True, False or None results [1]. These are low-level Python objects rather than SymPy’s symbolic Boolean expressions.

>>> from sympy import Symbol, symbols
>>> xpos = Symbol('xpos', positive=True)
>>> xneg = Symbol('xneg', negative=True)
>>> x = Symbol('x')
>>> print(xpos.is_positive)
True
>>> print(xneg.is_positive)
False
>>> print(x.is_positive)
None

A None result as a fuzzy-bool should be interpreted as meaning “maybe” or “unknown”.

An example of a symbolic Boolean class in SymPy can be found when using inequalities. When an inequality is not known to be true or false a Boolean can represent indeterminate results symbolically:

>>> xpos > 0
True
>>> xneg > 0
False
>>> x > 0
x > 0
>>> type(x > 0)
<class 'sympy.core.relational.StrictGreaterThan'>

The last example shows what happens when an inequality is indeterminate: we get an instance of StrictGreaterThan which represents the inequality as a symbolic expression. Internally when attempting to evaluate an inequality like a > b SymPy will compute (a - b).is_extended_positive. If the result is True or False then SymPy’s symbolic S.true or S.false will be returned. If the result is None then an unevaluated StrictGreaterThan is returned as shown for x > 0 above.

It is not obvious that queries like xpos > 0 return S.true rather than True because both objects display in the same way but we can check this using the Python is operator:

>>> from sympy import S
>>> xpos.is_positive is True
True
>>> xpos.is_positive is S.true
False
>>> (xpos > 0) is True
False
>>> (xpos > 0) is S.true
True

There is no general symbolic analogue of None in SymPy. In the cases where a low-level assumptions query gives None the symbolic query will result in an unevaluated symbolic Boolean (e.g, x > 0). We can use a symbolic Boolean as part of a symbolic expression such as a Piecewise:

>>> from sympy import Piecewise
>>> p = Piecewise((1, x > 0), (2, True))
>>> p
Piecewise((1, x > 0), (2, True))
>>> p.subs(x, 3)
1

Here p represents an expression that will be equal to 1 if x > 0 or otherwise it will be equal to 2. The unevaluated Boolean inequality x > 0 represents the condition for deciding the value of the expression symbolically. When we substitute a value for x the inequality will resolve to S.true and then the Piecewise can evaluate to 1 or 2.

The same will not work when using a fuzzy-bool instead of a symbolic Boolean:

>>> p2 = Piecewise((1, x.is_positive), (2, True))
Traceback (most recent call last):
...
TypeError: Second argument must be a Boolean, not `NoneType`

The Piecewise can not use None as the condition because unlike the inequality x > 0 it gives no information. With the inequality it is possible to decide in future if the condition might True or False once a value for x is known. A value of None can not be used in that way so it is rejected.

Note

We can use True in the Piecewise because True sympifies to S.true. Sympifying None just gives None again which is not a valid symbolic SymPy object.

There are many other symbolic Boolean types in SymPy. The same considerations about the differences between fuzzy bool and symbolic Boolean apply to all other SymPy Boolean types. To give a different example there is Contains which represents the statement that an object is contained in a set:

>>> from sympy import Reals, Contains
>>> x = Symbol('x', real=True)
>>> y = Symbol('y')
>>> Contains(x, Reals)
True
>>> Contains(y, Reals)
Contains(y, Reals)
>>> Contains(y, Reals).subs(y, 1)
True

The Python operator corresponding to Contains is in. A quirk of in is that it can only evaluate to a bool (True or False) so if the result is indeterminate then an exception will be raised:

>>> from sympy import I
>>> 2 in Reals
True
>>> I in Reals
False
>>> x in Reals
True
>>> y in Reals
Traceback (most recent call last):
...
TypeError: did not evaluate to a bool: (-oo < y) & (y < oo)

The exception can be avoided by using Contains(x, Reals) or Reals.contains(x) rather than x in Reals.

Three-valued logic with fuzzy bools

Whether we use the fuzzy-bool or symbolic Boolean we always need to be aware of the possibility that a query might be indeterminate. How to write code that handles this is different in the two cases though. We will look at fuzzy-bools first.

Consider the following function:

>>> def both_positive(a, b):
...     """ask whether a and b are both positive"""
...     if a.is_positive and b.is_positive:
...         return True
...     else:
...         return False

The both_positive function is supposed to tell us whether or not a and b are both positive. However the both_positive function will fail if either of the is_positive queries gives None:

>>> print(both_positive(S(1), S(1)))
True
>>> print(both_positive(S(1), S(-1)))
False
>>> print(both_positive(S(-1), S(-1)))
False
>>> x = Symbol('x') # may or may not be positive
>>> print(both_positive(S(1), x))
False

Note

We need to sympify the arguments to this function using S because the assumptions are only defined on SymPy objects and not regular Python int objects.

Here False is incorrect because it is possible that x is positive in which case both arguments would be positive. We get False here because x.is_positive gives None and Python will treat None as “falsey”.

In order to handle all possible cases correctly we need to separate the logic for identifying the True and False cases. An improved function might be:

>>> def both_positive_better(a, b):
...     """ask whether a and b are both positive"""
...     if a.is_positive is False or b.is_positive is False:
...         return False
...     elif a.is_positive is True and b.is_positive is True:
...         return True
...     else:
...         return None

This function now can handle all cases of True, False or None for both a and b and will always return a fuzzy bool representing whether the statement “a and b are both positive” is true, false or unknown:

>>> print(both_positive_better(S(1), S(1)))
True
>>> print(both_positive_better(S(1), S(-1)))
False
>>> x = Symbol('x')
>>> y = Symbol('y', positive=True)
>>> print(both_positive_better(S(1), x))
None
>>> print(both_positive_better(S(-1), x))
False
>>> print(both_positive_better(S(1), y))
True

Another case that we need to be careful of when using fuzzy-bools is negation with Python’s not operator e.g.:

>>> x = Symbol('x')
>>> print(x.is_positive)
None
>>> not x.is_positive
True

The correct negation of a fuzzy bool None is None again. If we do not know whether the statement “x is positive” is True or False then we also do not know whether its negation “x is not positive” is True or False. The reason we get True instead is again because None is considered “falsey”. When None is used with a logical operator such as not it will first be converted to a bool and then negated:

>>> bool(None)
False
>>> not bool(None)
True
>>> not None
True

The fact that None is treated as falsey can be useful if used correctly. For example we may want to do something only if x is known to positive in which case we can do

>>> x = Symbol('x', positive=True)
>>> if x.is_positive:
...     print("x is definitely positive")
... else:
...     print("x may or may not be positive")
x is definitely positive

Provided we understand that an alternate condition branch refers to two cases (False and None) then this can be a useful way of writing conditionals. When we really do need to distinguish all cases then we need to use things like x.is_positive is False. What we need to be careful of though is using Python’s binary logic operators like not or and with fuzzy bools as they will not handle the indeterminate case correctly.

In fact SymPy has internal functions that are designed to handle fuzzy-bools correctly:

>>> from sympy.core.logic import fuzzy_not, fuzzy_and
>>> print(fuzzy_not(True))
False
>>> print(fuzzy_not(False))
True
>>> print(fuzzy_not(None))
None
>>> print(fuzzy_and([True, True]))
True
>>> print(fuzzy_and([True, None]))
None
>>> print(fuzzy_and([False, None]))
False

Using the fuzzy_and function we can write the both_positive function more simply:

>>> def both_positive_best(a, b):
...     """ask whether a and b are both positive"""
...     return fuzzy_and([a.is_positive, b.is_positive])

Making use of fuzzy_and, fuzzy_or and fuzzy_not leads to simpler code and can also reduce the chance of introducing a logic error because the code can look more like it would in the case of ordinary binary logic.

Three-valued logic with symbolic Booleans

When working with symbolic Boolean rather than fuzzy-bool the issue of None silently being treated as falsey does not arise so it is easier not to end up with a logic error. However instead the indeterminate case will often lead to an exception being raised if not handled carefully.

We will try to implement the both_positive function this time using symbolic Boolean:

>>> def both_positive(a, b):
...     """ask whether a and b are both positive"""
...     if a > 0 and b > 0:
...         return S.true
...     else:
...         return S.false

The first difference is that we return the symbolic Boolean objects S.true and S.false rather than True and False. The second difference is that we test e.g. a > 0 rather than a.is_positive. Trying this out we get

>>> both_positive(1, 2)
True
>>> both_positive(-1, 1)
False
>>> x = Symbol('x')  # may or may not be positive
>>> both_positive(x, 1)
Traceback (most recent call last):
...
TypeError: cannot determine truth value of Relational

What happens now is that testing x > 0 gives an exception when x is not known to be positive or not positive. More precisely x > 0 does not give an exception but if x > 0 does and that is because the if statement implicitly calls bool(x > 0) which raises.

>>> x > 0
x > 0
>>> bool(x > 0)
Traceback (most recent call last):
...
TypeError: cannot determine truth value of Relational
>>> if x > 0:
...     print("x is positive")
Traceback (most recent call last):
...
TypeError: cannot determine truth value of Relational

The Python expression x > 0 creates a SymPy Boolean. Since in this case the Boolean can not evaluate to True or False we get an unevaluated StrictGreaterThan. Attempting to force that into a bool with bool(x > 0) raises an exception. That is because a regular Python bool must be either True or False and neither of those are known to be correct in this case.

The same kind of issue arises when using and, or or not with symbolic Boolean. The solution is to use SymPy’s symbolic And, Or and Not or equivalently Python’s bitwise logical operators &, | and ~:

>>> from sympy import And, Or, Not
>>> x > 0
x > 0
>>> x > 0 and x < 1
Traceback (most recent call last):
...
TypeError: cannot determine truth value of Relational
>>> And(x > 0, x < 1)
(x > 0) & (x < 1)
>>> (x > 0) & (x < 1)
(x > 0) & (x < 1)
>>> Or(x < 0, x > 1)
(x > 1) | (x < 0)
>>> Not(x < 0)
x >= 0
>>> ~(x < 0)
x >= 0

As before we can make a better version of both_positive if we avoid directly using a SymPy Boolean in an if, and, or, or not. Instead we can test whether or not the Boolean has evaluated to S.true or S.false:

>>> def both_positive_better(a, b):
...     """ask whether a and b are both positive"""
...     if (a > 0) is S.false or (b > 0) is S.false:
...         return S.false
...     elif (a > 0) is S.true and (b > 0) is S.true:
...         return S.true
...     else:
...         return And(a > 0, b > 0)

Now with this version we don’t get any exceptions and if the result is indeterminate we will get a symbolic Boolean representing the conditions under which the statement “a and b are both positive” would be true:

>>> both_positive_better(S(1), S(2))
True
>>> both_positive_better(S(1), S(-1))
False
>>> x, y = symbols("x, y")
>>> both_positive_better(x, y + 1)
(x > 0) & (y + 1 > 0)
>>> both_positive_better(x, S(3))
x > 0

The last case shows that actually using the And with a condition that is known to be true simplifies the And. In fact we have

>>> And(x > 0, 3 > 0)
x > 0
>>> And(4 > 0, 3 > 0)
True
>>> And(-1 > 0, 3 > 0)
False

What this means is that we can improve both_positive_better. The different cases are not needed at all. Instead we can simply return the And and let it simplify if possible:

>>> def both_positive_best(a, b):
...     """ask whether a and b are both positive"""
...     return And(a > 0, b > 0)

Now this will work with any symbolic real objects and produce a symbolic result. We can also substitute into the result to see how it would work for particular values:

>>> both_positive_best(2, 1)
True
>>> both_positive_best(-1, 2)
False
>>> both_positive_best(x, 3)
x > 0
>>> condition = both_positive_best(x/y, x + y)
>>> condition
(x + y > 0) & (x/y > 0)
>>> condition.subs(x, 1)
(1/y > 0) & (y + 1 > 0)
>>> condition.subs(x, 1).subs(y, 2)
True

The idea when working with symbolic Boolean objects is as much as possible to avoid trying to branch on them with if/else and other logical operators like and etc. Instead think of computing a condition and passing it around as a variable. The elementary symbolic operations like And, Or and Not can then take care of the logic for you.

Footnotes