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