.. _booleans-guide:
=============================
Symbolic and fuzzy booleans
=============================
This page describes what a symbolic :class:`~.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 [#fuzzy]_. These are low-level Python objects
rather than SymPy's symbolic :class:`~.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 :class:`~.Boolean` class in SymPy can be found when
using inequalities. When an inequality is not known to be true or false a
:class:`~.Boolean` can represent indeterminate results symbolically:
>>> xpos > 0
True
>>> xneg > 0
False
>>> x > 0
x > 0
>>> type(x > 0)
The last example shows what happens when an inequality is indeterminate: we
get an instance of :class:`~.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
:class:`~.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 :class:`~.Boolean` (e.g, ``x > 0``). We can use a
symbolic :class:`~.Boolean` as part of a symbolic expression such as a
:class:`~.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 :class:`~.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 :class:`~.Piecewise` can evaluate to ``1`` or ``2``.
The same will not work when using a fuzzy-bool instead of a symbolic
:class:`~.Boolean`:
>>> p2 = Piecewise((1, x.is_positive), (2, True))
Traceback (most recent call last):
...
TypeError: Second argument must be a Boolean, not `NoneType`
The :class:`~.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 :class:`~.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 :class:`~.Boolean` types in SymPy. The same
considerations about the differences between fuzzy bool and symbolic
:class:`~.Boolean` apply to all other SymPy :class:`~.Boolean` types. To give
a different example there is :class:`~.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 :class:`~.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 :class:`~.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 :class:`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 :class:`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 :class:`~.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 :class:`~.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 :class:`~.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 :class:`~.Boolean`. Since in this
case the :class:`~.Boolean` can not evaluate to ``True`` or ``False`` we get an
unevaluated :class:`~.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 :class:`~.Boolean`. The solution is to use SymPy's symbolic
:class:`~.And`, :class:`~.Or` and :class:`~.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 :class:`~.Boolean` in an ``if``, ``and``, ``or``, or ``not``.
Instead we can test whether or not the :class:`~.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 :class:`~.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 :class:`~.And` with a condition that is
known to be true simplifies the :class:`~.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
:class:`~.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 :class:`~.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 :class:`~.And`,
:class:`~.Or` and :class:`~.Not` can then take care of the logic for you.
.. rubric:: Footnotes
.. [#fuzzy] Note that what is referred to in SymPy as a "fuzzy bool" is really
about using three-valued logic. In normal usage "fuzzy logic" refers to a
system where logical values are continuous in between zero and one which is
something different from three-valued logic.