Assumptions#
This page outlines the core assumptions system in SymPy. It explains what the core assumptions system is, how the assumptions system is used and what the different assumptions predicates mean.
Note
This page describes the core assumptions system also often referred to as the “old assumptions” system. There is also a “new assumptions” system which is described elsewhere. Note that the system described here is actually the system that is widely used in SymPy. The “new assumptions” system is not really used anywhere in SymPy yet and the “old assumptions” system will not be removed. At the time of writing (SymPy 1.7) it is still recommended for users to use the old assumption system.
Firstly we consider what happens when taking the square root of the square of a concrete integer such as \(2\) or \(-2\):
>>> from sympy import sqrt
>>> sqrt(2**2)
2
>>> sqrt((-2)**2)
2
>>> x = 2
>>> sqrt(x**2)
2
>>> sqrt(x**2) == x
True
>>> y = -2
>>> sqrt(y**2) == y
False
>>> sqrt(y**2) == -y
True
What these examples demonstrate is that for a positive number \(x\) we have \(\sqrt{x^2} = x\) whereas for a negative number we would instead have \(\sqrt{x^2} = -x\). That may seem obvious but the situation can be more surprising when working with a symbol rather then an explicit number. For example
>>> from sympy import Symbol, simplify
>>> x = Symbol('x')
>>> sqrt(x**2)
sqrt(x**2)
It might look as if that should simplify to x
but it does not even if
simplify()
is used:
>>> simplify(sqrt(x**2))
sqrt(x**2)
This is because SymPy will refuse to simplify this expression if the
simplification is not valid for every possible value of x
. By default
the symbol x
is considered only to represent something roughly like an
arbitrary complex number and the obvious simplification here is only valid for
positive real numbers. Since x
is not known to be positive or even real no
simplification of this expression is possible.
We can tell SymPy that a symbol represents a positive real number when creating the symbol and then the simplification will happen automatically:
>>> y = Symbol('y', positive=True)
>>> sqrt(y**2)
y
This is what is meant by “assumptions” in SymPy. If the symbol y
is created with positive=True
then SymPy will assume that it represents
a positive real number rather than an arbitrary complex or possibly infinite
number. That assumption can make it possible to simplify expressions or
might allow other manipulations to work. It is usually a good idea to be as
precise as possible about the assumptions on a symbol when creating it.
The (old) assumptions system#
There are two sides to the assumptions system. The first side is that we can
declare assumptions on a symbol when creating the symbol. The other side is
that we can query the assumptions on any expression using the corresponding
is_*
attribute. For example:
>>> x = Symbol('x', positive=True)
>>> x.is_positive
True
We can query assumptions on any expression not just a symbol:
>>> x = Symbol('x', positive=True)
>>> expr = 1 + x**2
>>> expr
x**2 + 1
>>> expr.is_positive
True
>>> expr.is_negative
False
The values given in an assumptions query use three-valued “fuzzy” logic. Any
query can return True
, False
, or None
where None
should be
interpreted as meaning that the result is unknown.
>>> x = Symbol('x')
>>> y = Symbol('y', positive=True)
>>> z = Symbol('z', negative=True)
>>> print(x.is_positive)
None
>>> print(y.is_positive)
True
>>> print(z.is_positive)
False
Note
We need to use print
in the above examples because the special
value None
does not display by default in the Python
interpretter.
There are several reasons why an assumptions query might give None
. It is
possible that the query is unknowable as in the case of x
above. Since
x
does not have any assumptions declared it roughly represents an
arbitrary complex number. An arbitrary complex number might be a positive
real number but it also might not be. Without further information there is
no way to resolve the query x.is_positive
.
Another reason why an assumptions query might give None
is that there does
in many cases the problem of determining whether an expression is e.g.
positive is undecidable. That means that there does not exist an algorithm
for answering the query in general. For some cases an algorithm or at least a
simple check would be possible but has not yet been implemented although it
could be added to SymPy.
The final reason that an assumptions query might give None
is just that
the assumptions system does not try very hard to answer complicated queries.
The system is intended to be fast and uses simple heuristic methods to
conclude a True
or False
answer in common cases. For example any sum
of positive terms is positive so:
>>> from sympy import symbols
>>> x, y = symbols('x, y', positive=True)
>>> expr = x + y
>>> expr
x + y
>>> expr.is_positive
True
The last example is particularly simple so the assumptions system is able to give a definite answer. If the sum involved a mix of positive or negative terms it would be a harder query:
>>> x = Symbol('x', real=True)
>>> expr = 1 + (x - 2)**2
>>> expr
(x - 2)**2 + 1
>>> expr.is_positive
True
>>> expr2 = expr.expand()
>>> expr2
x**2 - 4*x + 5
>>> print(expr2.is_positive)
None
Ideally that last example would give True
rather than None
because the
expression is always positive for any real value of x
(and x
has been
assumed real). The assumptions system is intended to be efficient though: it
is expected many more complex queries will not be fully resolved. This is
because assumptions queries are primarily used internally by SymPy as part of
low-level calculations. Making the system more comprehensive would slow SymPy
down.
Note that in fuzzy logic giving an indeterminate result None
is never a
contradiction. If it is possible to infer a definite True
or False
result when resolving a query then that is better than returning None
.
However a result of None
is not a bug. Any code that uses the
assumptions system needs to be prepared to handle all three cases for any
query and should not presume that a definite answer will always be given.
The assumptions system is not just for symbols or for complex expressions. It
can also be used for plain SymPy integers and other objects. The assumptions
predicates are available on any instance of Basic
which is the superclass
for most classes of SymPy objects. A plain Python int
is not a
Basic
instance and can not be used to query assumptions
predicates. We can “sympify” regular Python objects to become SymPy objects
with sympify()
or S
(SingletonRegistry
) and then the
assumptions system can be used:
>>> from sympy import S
>>> x = 2
>>> x.is_positive
Traceback (most recent call last):
...
AttributeError: 'int' object has no attribute 'is_positive'
>>> x = S(2)
>>> type(x)
<class 'sympy.core.numbers.Integer'>
>>> x.is_positive
True
Gotcha: symbols with different assumptions#
In SymPy it is possible to declare two symbols with different names and they will implicitly be considered equal under structural equality:
>>> x1 = Symbol('x')
>>> x2 = Symbol('x')
>>> x1
x
>>> x2
x
>>> x1 == x2
True
However if the symbols have different assumptions then they will be considered to represent distinct symbols:
>>> x1 = Symbol('x', positive=True)
>>> x2 = Symbol('x')
>>> x1
x
>>> x2
x
>>> x1 == x2
False
One way to simplify an expression is to use the posify()
function
which will replace all symbols in an expression with symbols that have the
assumption positive=True
(unless that contradicts any existing assumptions
for the symbol):
>>> from sympy import posify, exp
>>> x = Symbol('x')
>>> expr = exp(sqrt(x**2))
>>> expr
exp(sqrt(x**2))
>>> posify(expr)
(exp(_x), {_x: x})
>>> expr2, rep = posify(expr)
>>> expr2
exp(_x)
The posify()
function returns the expression with all symbols replaced
(which might lead to simplifications) and also a dict which maps the new
symbols to the old that can be used with subs()
. This is
useful because otherwise the new expression with the new symbols having the
positive=True
assumption will not compare equal to the old:
>>> expr2
exp(_x)
>>> expr2 == exp(x)
False
>>> expr2.subs(rep)
exp(x)
>>> expr2.subs(rep) == exp(x)
True
Applying assumptions to string inputs#
We have seen how to set assumptions when Symbol
or
symbols()
explicitly. A natural question to ask is in what other
situations can we assign assumptions to an object?
It is common for users to use strings as input to SymPy functions (although the general feeling among SymPy developers is that this should be discouraged) e.g.:
>>> from sympy import solve
>>> solve('x**2 - 1')
[-1, 1]
When creating symbols explicitly it would be possible to assign assumptions
that would affect the behaviour of solve()
:
>>> x = Symbol('x', positive=True)
>>> solve(x**2 - 1)
[1]
When using string input SymPy will create the expression and create all of the
symbolc implicitly so the question arises how can the assumptions be
specified? The answer is that rather than depending on implicit string
conversion it is better to use the parse_expr()
function explicitly
and then it is possible to provide assumptions for the symbols e.g.:
>>> from sympy import parse_expr
>>> parse_expr('x**2 - 1')
x**2 - 1
>>> eq = parse_expr('x**2 - 1', {'x':Symbol('x', positive=True)})
>>> solve(eq)
[1]
Note
The solve()
function is unusual as a high level API in that it
actually checks the assumptions on any input symbols (the unknowns)
and uses that to tailor its output. The assumptions system otherwise
affects low-level evaluation but is not necessarily handled
explicitly by high-level APIs.
Predicates#
There are many different predicates that can be assumed for a symbol or can be
queried for an expression. It is possible to combine multiple predicates when
creating a symbol. Predicates are logically combined using and so if a
symbol is declared with positive=True
and also with integer=True
then
it is both positive and integer:
>>> x = Symbol('x', positive=True, integer=True)
>>> x.is_positive
True
>>> x.is_integer
True
The full set of known predicates for a symbol can be accessed using the
assumptions0
attribute:
>>> x.assumptions0
{'algebraic': True,
'commutative': True,
'complex': True,
'extended_negative': False,
'extended_nonnegative': True,
'extended_nonpositive': False,
'extended_nonzero': True,
'extended_positive': True,
'extended_real': True,
'finite': True,
'hermitian': True,
'imaginary': False,
'infinite': False,
'integer': True,
'irrational': False,
'negative': False,
'noninteger': False,
'nonnegative': True,
'nonpositive': False,
'nonzero': True,
'positive': True,
'rational': True,
'real': True,
'transcendental': False,
'zero': False}
We can see that there are many more predicates listed than the two that were
used to create x
. This is because the assumptions system can infer some
predicates from combinations of other predicates. For example if a symbol is
declared with positive=True
then it is possible to infer that it should
have negative=False
because a positive number can never be negative.
Similarly if a symbol is created with integer=True
then it is possible to
infer that is should have rational=True
because every integer is a
rational number.
A full table of the possible predicates and their definitions is given below.
Predicate |
Definition |
Implications |
---|---|---|
|
A commutative expression. A |
|
|
An infinite expression such as |
== !finite |
|
A finite expression. Any expression that is not |
== !infinite |
|
An element of the field of Hermitian operators. [antihermitian] |
|
|
An element of the field of antihermitian operators. [antihermitian] |
|
|
A complex number, \(z\in\mathbb{C}\). Any number of the form \(x + iy\)
where \(x\) and \(y\) are |
-> commutative -> finite |
|
An algebraic number, \(z\in\overline{\mathbb{Q}}\). Any number that is a root
of a non-zero polynomial \(p(z)\in\mathbb{Q}[z]\) having rational
coefficients. All |
-> complex |
|
A complex number that is not algebraic,
\(z\in\mathbb{C}-\overline{\mathbb{Q}}\). All |
== (complex & !algebraic) |
|
An element of the extended real number line,
\(x\in\overline{\mathbb{R}}\) where
\(\overline{\mathbb{R}}=\mathbb{R}\cup\{-\infty,+\infty\}\). An
|
-> commutative |
|
A real number, \(x\in\mathbb{R}\). All |
-> complex == (extended_real & finite) == (negative | zero | positive) -> hermitian |
|
An imaginary number, \(z\in\mathbb{I}-\{0\}\). A number of the form \(z=yi\)
where \(y\) is real, \(y\ne 0\) and \(i=\sqrt{-1}\). All |
-> complex -> antihermitian -> !extended_real |
|
A rational number, \(q\in\mathbb{Q}\). Any number of the form
\(\frac{a}{b}\) where \(a\) and \(b\) are integers and \(b \ne 0\). All
|
-> real -> algebraic |
|
A real number that is not rational, \(x\in\mathbb{R}-\mathbb{Q}\). [irrational] |
== (real & !rational) |
|
An integer, \(a\in\mathbb{Z}\). All integers are |
-> rational |
|
An extended real number that is not an integer, \(x\in\overline{\mathbb{R}}-\mathbb{Z}\). |
== (extended_real & !integer) |
|
An even number, \(e\in\{2k: k\in\mathbb{Z}\}\). All |
-> integer -> !odd |
|
An odd number, \(o\in\{2k + 1: k\in\mathbb{Z}\}\). All |
-> integer -> !even |
|
A prime number, \(p\in\mathbb{P}\). All |
-> integer -> positive |
|
A composite number, \(c\in\mathbb{N}-(\mathbb{P}\cup\{1\})\). A positive
integer that is the product of two or more primes. A |
-> (integer & positive & !prime) !composite -> (!positive | !even | prime) |
|
The number \(0\). An expression with |
-> even & finite == (extended_nonnegative & extended_nonpositive) == (nonnegative & nonpositive) |
|
A nonzero real number, \(x\in\mathbb{R}-\{0\}\). A |
-> real == (extended_nonzero & finite) |
|
A member of the extended reals that is not zero, \(x\in\overline{\mathbb{R}}-\{0\}\). |
== (extended_real & !zero) |
|
A positive real number, \(x\in\mathbb{R}, x>0\). All |
== (nonnegative & nonzero) == (extended_positive & finite) |
|
A nonnegative real number, \(x\in\mathbb{R}, x\ge 0\). All |
== (real & !negative) == (extended_nonnegative & finite) |
|
A negative real number, \(x\in\mathbb{R}, x<0\). All |
== (nonpositive & nonzero) == (extended_negative & finite) |
|
A nonpositive real number, \(x\in\mathbb{R}, x\le 0\). All |
== (real & !positive) == (extended_nonpositive & finite) |
|
A positive extended real number, \(x\in\overline{\mathbb{R}}, x>0\).
An |
== (extended_nonnegative & extended_nonzero) |
|
A nonnegative extended real number, \(x\in\overline{\mathbb{R}}, x\ge 0\).
An |
== (extended_real & !extended_negative) |
|
A negative extended real number, \(x\in\overline{\mathbb{R}}, x<0\).
An |
== (extended_nonpositive & extended_nonzero) |
|
A nonpositive extended real number, \(x\in\overline{\mathbb{R}}, x\le 0\).
An |
== (extended_real & !extended_positive) |
References for the above definitions#
Implications#
The assumptions system uses the inference rules to infer new predicates beyond those immediately specified when creating a symbol:
>>> x = Symbol('x', real=True, negative=False, zero=False)
>>> x.is_positive
True
Although x
was not explicitly declared positive
it can be inferred
from the predicates that were given explicitly. Specifically one of the
inference rules is real == negative | zero | positive
so if real
is
True
and both negative
and zero
are False
then positive
must be True
.
In practice the assumption inference rules mean that it is not necessary to include redundant predicates for example a positive real number can be simply be declared as positive:
>>> x1 = Symbol('x1', positive=True, real=True)
>>> x2 = Symbol('x2', positive=True)
>>> x1.is_real
True
>>> x2.is_real
True
>>> x1.assumptions0 == x2.assumptions0
True
Combining predicates that are inconsistent will give an error:
>>> x = Symbol('x', commutative=False, real=True)
Traceback (most recent call last):
...
InconsistentAssumptions: {
algebraic: False,
commutative: False,
complex: False,
composite: False,
even: False,
extended_negative: False,
extended_nonnegative: False,
extended_nonpositive: False,
extended_nonzero: False,
extended_positive: False,
extended_real: False,
imaginary: False,
integer: False,
irrational: False,
negative: False,
noninteger: False,
nonnegative: False,
nonpositive: False,
nonzero: False,
odd: False,
positive: False,
prime: False,
rational: False,
real: False,
transcendental: False,
zero: False}, real=True
Interpretation of the predicates#
Although the predicates are defined in the table above it is worth taking some
time to think about how to interpret them. Firstly many of the concepts
referred to by the predicate names like “zero”, “prime”, “rational” etc have
a basic meaning in mathematics but can also have more general meanings. For
example when dealing with matrices a matrix of all zeros might be referred to
as “zero”. The predicates in the assumptions system do not allow any
generalizations such as this. The predicate zero
is strictly reserved for
the plain number \(0\). Instead matrices have an
is_zero_matrix()
property for this purpose (although
that property is not strictly part of the assumptions system):
>>> from sympy import Matrix
>>> M = Matrix([[0, 0], [0, 0]])
>>> M.is_zero
False
>>> M.is_zero_matrix
True
Similarly there are generalisations of the integers such as the Gaussian
integers which have a different notion of prime number. The prime
predicate in the assumptions system does not include those and strictly refers
only to the standard prime numbers \(\mathbb{P} = \{2, 3, 5, 7, 11, \cdots\}\).
Likewise integer
only means the standard concept of the integers
\(\mathbb{Z} = \{0, \pm 1, \pm 2, \cdots\}\), rational
only means the
standard concept of the rational numbers \(\mathbb{Q}\) and so on.
The predicates set up schemes of subsets such as the chain beginning with the complex numbers which are considered as a superset of the reals which are in turn a superset of the rationals and so on. The chain of subsets
corresponds to the chain of implications in the assumptions system
integer -> rational -> real -> complex
A “vanilla” symbol with no assumptions explicitly attached is not known to belong to any of these sets and is not even known to be finite:
>>> x = Symbol('x')
>>> x.assumptions0
{'commutative': True}
>>> print(x.is_commutative)
True
>>> print(x.is_rational)
None
>>> print(x.is_complex)
None
>>> print(x.is_real)
None
>>> print(x.is_integer)
None
>>> print(x.is_finite)
None
It is hard for SymPy to know what it can do with such a symbol that is not
even known to be finite or complex so it is generally better to give some
assumptions to the symbol explicitly. Many parts of SymPy will implicitly
treat such a symbol as complex and in some cases SymPy will permit
manipulations that would not strictly be valid given that x
is not known
to be finite. In a formal sense though very little is known about a vanilla
symbol which makes manipulations involving it difficult.
Defining something about a symbol can make a big difference. For example if we declare the symbol to be an integer then this implies a suite of other predicates that will help in further manipulations:
>>> n = Symbol('n', integer=True)
>>> n.assumptions0
{'algebraic': True,
'commutative': True,
'complex': True,
'extended_real': True,
'finite': True,
'hermitian': True,
'imaginary': False,
'infinite': False,
'integer': True,
'irrational': False,
'noninteger': False,
'rational': True,
'real': True,
'transcendental': False}
These assumptions can lead to very significant simplifications e.g.
integer=True
gives:
>>> from sympy import sin, pi
>>> n1 = Symbol('n1')
>>> n2 = Symbol('n2', integer=True)
>>> sin(n1 * pi)
sin(pi*n1)
>>> sin(n2 * pi)
0
Replacing a whole expression with \(0\) is about as good as simplification can get!
It is normally advisable to set as many assumptions as possible on any symbols
so that expressions can be simplified as much as possible. A common
misunderstanding leads to defining a symbol with a False
predicate e.g.:
>>> x = Symbol('x', negative=False)
>>> print(x.is_negative)
False
>>> print(x.is_nonnegative)
None
>>> print(x.is_real)
None
>>> print(x.is_complex)
None
>>> print(x.is_finite)
None
If the intention is to say that x
is a real number that is not positive
then that needs to be explicitly stated. In the context that the symbol is
known to be real, the predicate positive=False
becomes much more
meaningful:
>>> x = Symbol('x', real=True, negative=False)
>>> print(x.is_negative)
False
>>> print(x.is_nonnegative)
True
>>> print(x.is_real)
True
>>> print(x.is_complex)
True
>>> print(x.is_finite)
True
A symbol declared as Symbol('x', real=True, negative=False)
is equivalent
to a symbol declared as Symbol('x', nonnegative=True)
. Simply declaring a
symbol as Symbol('x', positive=False)
does not allow the assumptions
system to conclude much about it because a vanilla symbol is not known to be
finite or even complex.
A related confusion arises with Symbol('x', complex=True)
and
Symbol('x', real=False)
. Often when either of these is used neither is
what is actually wanted. The first thing to understand is that all real
numbers are complex so a symbol created with real=True
will also have
complex=True
and a symbol created with complex=True
will not have
real=False
. If the intention was to create a complex number that is not
a real number then it should be Symbol('x', complex=True, real=False)
. On
the other hand declaring real=False
alone is not sufficient to conclude
that complex=True
because knowing that it is not a real number does not
tell us whether it is finite or whether or not it is some completely different
kind of object from a complex number.
A vanilla symbol is defined by not knowing whether it is finite
etc but
there is no clear definition of what it should actually represent. It is
tempting to think of it as an “arbitrary complex number or possibly one of the
infinities” but there is no way to query an arbitrary (non-symbol) expression
in order to determine if it meets those criteria. It is important to bear in
mind that within the SymPy codebase and potentially in downstream libraries
many other kinds of mathematical objects can be found that might also have
commutative=True
while being something very different from an ordinary
number (in this context even SymPy’s standard infinities are considered
“ordinary”).
The only predicate that is applied by default for a symbol is commutative
.
We can also declare a symbol to be noncommutative e.g.:
>>> x, y = symbols('x, y', commutative=False)
>>> z = Symbol('z') # defaults to commutative=True
>>> x*y + y*x
x*y + y*x
>>> x*z + z*x
2*z*x
Note here that since x
and y
are both noncommutative x
and y
do not commute so x*y != y*x
. On the other hand since z
is commutative
x
and z
commute and x*z == z*x
even though x
is
noncommutative.
The interpretation of what a vanilla symbol represents is unclear but the
interpretation of an expression with commutative=False
is entirely
obscure. Such an expression is necessarily not a complex number or an
extended real or any of the standard infinities (even zoo
is commutative).
We are left with very little that we can say about what such an expression
does represent.
Other is_* properties#
There are many properties and attributes in SymPy that that have names
beginning with is_
that look similar to the properties used in the
(old) assumptions system but are not in fact part of the assumptions system.
Some of these have a similar meaning and usage as those of the assumptions
system such as the is_zero_matrix()
property shown
above. Another example is the is_empty
property of sets:
>>> from sympy import FiniteSet, Intersection
>>> S1 = FiniteSet(1, 2)
>>> S1
{1, 2}
>>> print(S1.is_empty)
False
>>> S2 = Intersection(FiniteSet(1), FiniteSet(Symbol('x')))
>>> S2
Intersection({1}, {x})
>>> print(S2.is_empty)
None
The is_empty
property gives a fuzzy-bool indicating whether or not a
Set
is the empty set. In the example of S2
it is not possible to know
whether or not the set is empty without knowing whether or not x
is equal
to 1
so S2.is_empty
gives None
. The is_empty
property for sets
plays a similar role to the is_zero
property for numbers in the
assumptions system: is_empty
is normally only True
for the
EmptySet
object but it is still useful to be able to distinguish between
the cases where is_empty=False
and is_empty=None
.
Although is_zero_matrix
and is_empty
are used for similar purposes to
the assumptions properties such as is_zero
they are not part of the
(old) assumptions system. There are no associated inference rules connecting
e.g. Set.is_empty
and Set.is_finite_set
because the inference rules
are part of the (old) assumptions system which only deals with the predicates
listed in the table above. It is not possible to declare a
MatrixSymbol
with e.g. zero_matrix=False
and there is no
SetSymbol
class but if there was it would not have a system for
understanding predicates like empty=False
.
The properties is_zero_matrix()
and is_empty
are
similar to those of the assumptions system because they concern semantic
aspects of an expression. There are a large number of other properties that
focus on structural aspects such as is_Number
,
is_number()
, is_comparable()
. Since these
properties refer to structural aspects of an expression they will always give
True
or False
rather than a fuzzy bool that also has the possibility
of being None
. Capitalised properties such as is_Number
are usually
shorthand for isinstance
checks e.g.:
>>> from sympy import Number, Rational
>>> x = Rational(1, 2)
>>> isinstance(x, Number)
True
>>> x.is_Number
True
>>> y = Symbol('y', rational=True)
>>> isinstance(y, Number)
False
>>> y.is_Number
False
The Number
class is the superclass for Integer
,
Rational
and Float
so any instance of Number
represents a concrete number with a known value. A symbol such as y
that
is declared with rational=True
might represent the same value as x
but
it is not a concrete number with a known value so this is a structural rather
than a semantic distinction. Properties like is_Number
are sometimes used
in SymPy in place of e.g. isinstance(obj, Number)
because they do not have
problems with circular imports and checking x.is_Number
can be faster than
a call to isinstance
.
The is_number
(lower-case) property is very different from
is_Number
. The is_number
property is True
for any
expression that can be numerically evaluated to a floating point complex
number with evalf()
:
>>> from sympy import I
>>> expr1 = I + sqrt(2)
>>> expr1
sqrt(2) + I
>>> expr1.is_number
True
>>> expr1.evalf()
1.4142135623731 + 1.0*I
>>> x = Symbol('x')
>>> expr2 = 1 + x
>>> expr2
x + 1
>>> expr2.is_number
False
>>> expr2.evalf()
x + 1.0
The primary reason for checking expr.is_number
is to predict whether a
call to evalf()
will fully evaluate. The
is_comparable()
property is similar to
is_number()
except that if is_comparable
gives True
then the expression is guaranteed to numerically evaluate to a real
Float
. When a.is_comparable
and b.is_comparable
the
inequality a < b
should be resolvable as something like a.evalf() <
b.evalf()
.
The full set of is_*
properties, attributes and methods in SymPy is
large. It is important to be clear though that only those that are listed in
the table of predicates above are actually part of the assumptions system. It
is only those properties that are involved in the mechanism that implements
the assumptions system which is explained below.
Implementing assumptions handlers#
We will now work through an example of how to implement a SymPy symbolic
function so that we can see how the old assumptions are used internally. SymPy
already has an exp
function which is defined for all complex numbers but
we will define an expreal
function which is restricted to real arguments.
>>> from sympy import Function
>>> from sympy.core.logic import fuzzy_and, fuzzy_or
>>>
>>> class expreal(Function):
... """exponential function E**x restricted to the extended reals"""
...
... is_extended_nonnegative = True
...
... @classmethod
... def eval(cls, x):
... # Validate the argument
... if x.is_extended_real is False:
... raise ValueError("non-real argument to expreal")
... # Evaluate for special values
... if x.is_zero:
... return S.One
... elif x.is_infinite:
... if x.is_extended_negative:
... return S.Zero
... elif x.is_extended_positive:
... return S.Infinity
...
... @property
... def x(self):
... return self.args[0]
...
... def _eval_is_finite(self):
... return fuzzy_or([self.x.is_real, self.x.is_extended_nonpositive])
...
... def _eval_is_algebraic(self):
... if fuzzy_and([self.x.is_rational, self.x.is_nonzero]):
... return False
...
... def _eval_is_integer(self):
... if self.x.is_zero:
... return True
...
... def _eval_is_zero(self):
... return fuzzy_and([self.x.is_infinite, self.x.is_extended_negative])
The Function.eval
method is used to pick up on special values of the function so
that we can return a different object if it would be a simplification. When
expreal(x)
is called the expreal.__new__
class method (defined in the
superclass Function
) will call expreal.eval(x)
. If expreal.eval
returns something other than None
then that will be returned instead of an
unevaluated expreal(x)
:
>>> from sympy import oo
>>> expreal(1)
expreal(1)
>>> expreal(0)
1
>>> expreal(-oo)
0
>>> expreal(oo)
oo
Note that the expreal.eval
method does not compare the argument using
==
. The special values are verified using the assumptions system to query
the properties of the argument. That means that the expreal
method can
also evaluate for different forms of expression that have matching properties
e.g.
>>> x = Symbol('x', extended_negative=True, infinite=True)
>>> x
x
>>> expreal(x)
0
Of course the assumptions system can only resolve a limited number of special
values so most eval
methods will also check against some special values
with ==
but it is preferable to check e.g. x.is_zero
rather than
x==0
.
Note also that the expreal.eval
method validates that the argument is
real. We want to allow \(\pm\infty\) as arguments to expreal
so we check for
extended_real
rather than real
. If the argument is not extended real
then we raise an error:
>>> expreal(I)
Traceback (most recent call last):
...
ValueError: non-real argument to expreal
Importantly we check x.is_extended_real is False
rather than not
x.is_extended_real
which means that we only reject the argument if it is
definitely not extended real: if x.is_extended_real
gives None
then
the argument will not be rejected. The first reason for allowing
x.is_extended_real=None
is so that a vanilla symbol can be used with
expreal
. The second reason is that an assumptions query can always give
None
even in cases where an argument is definitely real e.g.:
>>> x = Symbol('x')
>>> print(x.is_extended_real)
None
>>> expreal(x)
expreal(x)
>>> expr = (1 + I)/sqrt(2) + (1 - I)/sqrt(2)
>>> print(expr.is_extended_real)
None
>>> expr.expand()
sqrt(2)
>>> expr.expand().is_extended_real
True
>>> expreal(expr)
expreal(sqrt(2)*(1 - I)/2 + sqrt(2)*(1 + I)/2)
Validating the argument in expreal.eval
does mean that it will not be
validated when evaluate=False
is passed but there is not really a better
place to perform the validation:
>>> expreal(I, evaluate=False)
expreal(I)
The extended_nonnegative
class attribute and the _eval_is_*
methods on
the expreal
class implement queries in the assumptions system for
instances of expreal
:
>>> expreal(2)
expreal(2)
>>> expreal(2).is_finite
True
>>> expreal(2).is_integer
False
>>> expreal(2).is_rational
False
>>> expreal(2).is_algebraic
False
>>> z = expreal(-oo, evaluate=False)
>>> z
expreal(-oo)
>>> z.is_integer
True
>>> x = Symbol('x', real=True)
>>> expreal(x)
expreal(x)
>>> expreal(x).is_nonnegative
True
The assumptions system resolves queries like expreal(2).is_finite
using
the corresponding handler expreal._eval_is_finite
and also the
implication rules. For example it is known that expreal(2).is_rational
is
False
because expreal(2)._eval_is_algebraic
returns False
and
there is an implication rule rational -> algebraic
. This means that an
is_rational
query can be resolved in this case by the
_eval_is_algebraic
handler. It is actually better not to implement
assumptions handlers for every possible predicate but rather to try and
identify a minimal set of handlers that can resolve as many queries as
possible with as few checks as possible.
Another point to note is that the _eval_is_*
methods only make assumptions
queries on the argument x
and do not make any assumptions queries on
self
. Recursive assumptions queries on the same object will interfere with
the assumptions implications resolver potentially leading to non-deterministic
behaviour so they should not be used (there are examples of this in the SymPy
codebase but they should be removed).
Many of the expreal
methods implicitly return None
. This is a common
pattern in the assumptions system. The eval
method and the _eval_is_*
methods can all return None
and often will. A Python function that ends
without reaching a return
statement will implicitly return None
. We
take advantage of this by leaving out many of the else
clauses from the
if
statements and allowing None
to be returned implicitly. When
following the control flow of these methods it is important to bear in mind
firstly that any queried property can give True
, False
or None
and
also that any function will implicitly return None
if all of the
conditionals fail.
Mechanism of the assumptions system#
Note
This section describes internal details that could change in a future SymPy version.
This section will explain the inner workings of the assumptions system. It is important to understand that these inner workings are implementation details and could change from one SymPy version to another. This explanation is written as of SymPy 1.7. Although the (old) assumptions system has many limitations (discussed in the next section) it is a mature system that is used extensively in SymPy and has been well optimised for its current usage. The assumptions system is used implicitly in most SymPy operations to control evaluation of elementary expressions.
There are several stages in the implementation of the assumptions system within a SymPy process that lead up to the evaluation of a single query in the assumptions system. Briefly these are:
At import time the assumptions rules defined in
sympy/core/assumptions.py
are processed into a canonical form ready for efficiently applying the implication rules. This happens once when SymPy is imported before even theBasic
class is defined.The
ManagedProperties
metaclass is defined which is the metaclass for allBasic
subclasses. This class will post-process everyBasic
subclass to add the relevant properties needed for assumptions queries. This also adds thedefault_assumptions
attribute to the class. This happens each time aBasic
subclass is defined.Every
Basic
instance initially uses thedefault_assumptions
class attribute. When an assumptions query is made on aBasic
instance in the first instance the query will be answered from thedefault_assumptions
for the class.If there is no cached value for the assumptions query in the
default_assumptions
for the class then the default assumptions will be copied to make an assumptions cache for the instance. Then the_ask()
function is called to resolve the query which will firstly call the relevant instance handler_eval_is
method. If the handler returns non-None then the result will be cached and returned.If the handler does not exist or gives None then the implications resolver is tried. This will enumerate (in a randomised order) all possible combinations of predicates that could potentially be used to resolve the query under the implication rules. In each case the handler
_eval_is
method will be called to see if it gives non-None. If any combination of handlers and implication rules leads to a definitive result for the query then that result is cached in the instance cache and returned.Finally if the implications resolver failed to resolve the query then the query is considered unresolvable. The value of
None
for the query is cached in the instance cache and returned.
The assumptions rules defined in sympy/core/assumptions.py
are given in
forms like real == negative | zero | positive
. When this module is
imported these are converted into a FactRules
instance called
_assume_rules
. This preprocesses the implication rules into the form of
“A” and “B” rules that can be used for the implications resolver. This is
explained in the code in sympy/core/facts.py
. We can access this internal
object directly like (full output omitted):
>>> from sympy.core.assumptions import _assume_rules
>>> _assume_rules.defined_facts
{'algebraic',
'antihermitian',
'commutative',
'complex',
'composite',
'even',
...
>>> _assume_rules.full_implications
defaultdict(set,
{('extended_positive', False): {('composite', False),
('positive', False),
('prime', False)},
('finite', False): {('algebraic', False),
('complex', False),
('composite', False),
...
The ManagedProperties
metaclass will inspect the attributes of each
Basic
class to see if any assumptions related attributes are defined. An
example of these is the is_extended_nonnegative = True
attribute defined
in the expreal
class. The implications of any such attributes will be
used to precompute any statically knowable assumptions. For example
is_extended_nonnegative=True
implies real=True
etc. A StdFactKB
instance is created for the class which stores those assumptions whose values
are known at this stage. The StdFactKB
instance is assigned as the class
attribute default_assumptions
. We can see this with
>>> from sympy import Expr
...
>>> class A(Expr):
... is_positive = True
...
... def _eval_is_rational(self):
... # Let's print something to see when this method is called...
... print('!!! calling _eval_is_rational')
... return True
...
>>> A.is_positive
True
>>> A.is_real # inferred from is_positive
True
Although only is_positive
was defined in the class A
it also has
attributes such as is_real
which are inferred from is_positive
. The
set of all such assumptions for class A
can be seen in
default_assumptions
which looks like a dict
but is in fact a
StdFactKB
instance:
>>> type(A.default_assumptions)
<class 'sympy.core.assumptions.StdFactKB'>
>>> A.default_assumptions
{'commutative': True,
'complex': True,
'extended_negative': False,
'extended_nonnegative': True,
'extended_nonpositive': False,
'extended_nonzero': True,
'extended_positive': True,
'extended_real': True,
'finite': True,
'hermitian': True,
'imaginary': False,
'infinite': False,
'negative': False,
'nonnegative': True,
'nonpositive': False,
'nonzero': True,
'positive': True,
'real': True,
'zero': False}
When an instance of any Basic
subclass is created Basic.__new__
will
assign its _assumptions
attribute which will initially be a reference to
cls.default_assumptions
shared amongst all instances of the same class.
The instance will use this to resolve any assumptions queries until that fails
to give a definitive result at which point a copy of
cls.default_assumptions
will be created and assigned to the instance’s
_assumptions
attribute. The copy will be used as a cache to store any
results computed for the instance by its _eval_is
handlers.
When the _assumptions
attribute fails to give the relevant result it is
time to call the _eval_is
handlers. At this point the _ask()
function
is called. The _ask()
function will initially try to resolve a query such
as is_rational
by calling the corresponding method i.e.
_eval_is_rational
. If that gives non-None then the result is stored in
_assumptions
and any implications of that result are computed and stored
as well. At that point the query is resolved and the value returned.
>>> a = A()
>>> a._assumptions is A.default_assumptions
True
>>> a.is_rational
!!! calling _eval_is_rational
True
>>> a._assumptions is A.default_assumptions
False
>>> a._assumptions # rational now shows as True
{'algebraic': True,
'commutative': True,
'complex': True,
'extended_negative': False,
'extended_nonnegative': True,
'extended_nonpositive': False,
'extended_nonzero': True,
'extended_positive': True,
'extended_real': True,
'finite': True,
'hermitian': True,
'imaginary': False,
'infinite': False,
'irrational': False,
'negative': False,
'nonnegative': True,
'nonpositive': False,
'nonzero': True,
'positive': True,
'rational': True,
'real': True,
'transcendental': False,
'zero': False}
If e.g. _eval_is_rational
does not exist or gives None
then _ask()
will try all possibilities to use the implication rules and any other handler
methods such as _eval_is_integer
, _eval_is_algebraic
etc that might
possibly be able to give an answer to the original query. If any method leads
to a definite result being known for the original query then that is returned.
Otherwise once all possibilities for using a handler and the implication rules
to resolve the query are exhausted None
will be cached and returned.
>>> b = A()
>>> b.is_algebraic # called _eval_is_rational indirectly
!!! calling _eval_is_rational
True
>>> c = A()
>>> print(c.is_prime) # called _eval_is_rational indirectly
!!! calling _eval_is_rational
None
>>> c._assumptions # prime now shows as None
{'algebraic': True,
'commutative': True,
'complex': True,
'extended_negative': False,
'extended_nonnegative': True,
'extended_nonpositive': False,
'extended_nonzero': True,
'extended_positive': True,
'extended_real': True,
'finite': True,
'hermitian': True,
'imaginary': False,
'infinite': False,
'irrational': False,
'negative': False,
'nonnegative': True,
'nonpositive': False,
'nonzero': True,
'positive': True,
'prime': None,
'rational': True,
'real': True,
'transcendental': False,
'zero': False}
Note
In the _ask()
function the handlers are called in a randomised
order which can mean that execution at this point is
non-deterministic. Provided all of the different handler methods
are consistent (i.e. there are no bugs) then the end result will
still be deterministic. However a bug where two handlers are
inconsistent can manifest in non-deterministic behaviour because
this randomisation might lead to the handlers being called in
different orders when the same program is run multiple times.
Limitations#
Combining predicates with or#
In the old assumptions we can easily combine predicates with and when creating a Symbol e.g.:
>>> x = Symbol('x', integer=True, positive=True)
>>> x.is_positive
True
>>> x.is_integer
True
We can also easily query whether two conditions are jointly satisfied with
>>> fuzzy_and([x.is_positive, x.is_integer])
True
>>> x.is_positive and x.is_integer
True
However there is no way in the old assumptions to create a Symbol
with assumptions predicates combined with or. For example if we wanted to
say that “x is positive or x is an integer” then it is not possible to create
a Symbol
with those assumptions.
It is also not possible to ask an assumptions query based on or e.g. “is expr an expression that is positive or an integer”. We can use e.g.
>>> fuzzy_or([x.is_positive, x.is_integer])
True
However if all that is known about x
is that it is possibly positive or
otherwise a negative integer then both queries x.is_positive
and
x.is_integer
will resolve to None
. That means that the query becomes
>>> fuzzy_or([None, None])
which then also gives None
.
Relations between different symbols#
A fundamental limitation of the old assumptions system is that all explicit
assumptions are properties of an individual symbol. There is no way in this
system to make an assumption about the relationship between two symbols. One
of the most common requests is the ability to assume something like x < y
but there is no way to even specify that in the old assumptions.
The new assumptions have the theoretical capability that relational assumptions can be specified. However the algorithms to make use of that information are not yet implemented and the exact API for specifying relational assumptions has not been decided upon.
Dynamic changing assumptions#
Selectively controlling evaluation#
Extensibility#
New assumptions#
ZZZ: Talk about the new assumptions here…