SymPy¶
Note
You can download this example as a Python script:
sympy.py
or Jupyter notebook:
sympy.ipynb
.
Learning Objectives¶
After completing this chapter readers will be able to:
Write mathematical expressions with symbols and functions using SymPy.
Print different forms of expressions and equations with SymPy.
Differentiate mathematical expressions using SymPy.
Evaluate mathematical expressions using SymPy.
Create matrices and do linear algebra using SymPy.
Solve a linear system of equations with SymPy.
Simplify mathematical expressions with SymPy.
Introduction¶
SymPy is an open source, collaboratively developed computer algebra system (CAS) written in Python. It will be used extensively for manipulating symbolic expressions and equations. All of the mathematics needed to formulate the equations of motion of multibody systems can be done with pencil and paper, but the bookkeeping becomes extremely tedious and error prone for systems with even a small number of bodies. SymPy lets a computer handle the tedious aspects (e.g. differentiation or solving linear systems of equations) and reduces the errors one would encounter with pencil and paper. This chapter introduces SymPy and the primary SymPy features we will be using.
Import and Setup¶
I will import SymPy as follows throughout this book:
import sympy as sm
Since SymPy works with mathematical symbols it’s nice to view SymPy objects in
a format that is similar to the math in a textbook. Executing
init_printing()
at the beginning
of your Jupyter Notebook will ensure that SymPy objects render as typeset
mathematics. I use the use_latex='mathjax'
argument here to disable math
png image generation, but that keyword argument is not necessary.
sm.init_printing(use_latex='mathjax')
Symbols¶
Mathematical symbols are created with the
symbols()
function. A symbol \(a\) is
created like so:
a = sm.symbols('a')
a
This symbol object is of the Symbol
type:
type(a)
sympy.core.symbol.Symbol
Multiple symbols can be created with one call to symbols()
and SymPy
recognizes common Greek symbols by their spelled-out name.
b, t, omega, Omega = sm.symbols('b, t, omega, Omega')
b, t, omega, Omega
Note that the argument provided to symbols()
does not need to match the
Python variable name it is assigned to. Using more verbose Python variable
names may make code easier to read and understand, especially if there are many
mathematical variables that you need to keep track of. Note that the subscripts
are recognized too.
pivot_angle, w2 = sm.symbols('alpha1, omega2')
pivot_angle, w2
Exercise
Review the SymPy documentation and create symbols \(q_1, q_2, \ldots,
q_{10}\) with a very succint call to
symbols()
.
Solution
sm.symbols('q1:11')
Undefined Functions¶
You will also work with undefined mathematical functions in addition to symbols. These will play an important role in setting up differential equations, where you typically don’t know the function, but only its derivative(s). You can create arbitrary functions of variables. In this case, you make a function of \(t\). First create the function name:
f = sm.Function('f')
f
f
This is of a type sympy.core.function.UndefinedFunction
.
type(f)
sympy.core.function.UndefinedFunction
Now you can create functions of one or more variables like so:
f(t)
Warning
Due to SymPy’s internal implementations, the type of a function with its argument is not defined as expected:
type(f(t))
f
This can be confusing if you are checking types.
The same UndefinedFunction
can be used to create multivariate functions:
f(a, b, omega, t)
Exercise
Create a function \(H(x, y, z)\).
Solution
x, y, z = sm.symbols('x, y, z')
sm.Function('H')(x, y, z)
Symbolic Expressions¶
Now that you have mathematical variables and functions available, they can be
used to construct mathematical expressions. The most basic way to construct
expressions is with the standard Python operators +
, -
, *
, /
,
and **
. For example:
expr1 = a + b/omega**2
expr1
An expression will have the type Add, Mul, or Pow
:
type(expr1)
sympy.core.add.Add
This is because SymPy stores expressions behind the scenes as a tree. You can
inspect this internal representation by using the
srepr()
function:
sm.srepr(expr1)
"Add(Symbol('a'), Mul(Symbol('b'), Pow(Symbol('omega'), Integer(-2))))"
This is a visual representation of the tree:
This representation is SymPy’s “true” representation of the symbolic
expression. SymPy can display this expression in many other representations,
for example the typeset mathematical expression you have already seen is one of
those representations. This is important to know, because sometimes the
expressions are displayed to you in a way that may be confusing and checking
the srepr()
version can help clear up misunderstandings. See the
manipulation section of the SymPy tutorial for more information on this.
Undefined functions can also be used in expressions just like symbols:
expr2 = f(t) + a*omega
expr2
SymPy has a large number of elementary and special functions. See the SymPy
documentation on functions for more information. For example, here is an
expression that uses
sin
,
Abs
, and
sqrt()
:
expr3 = a*sm.sin(omega) + sm.Abs(f(t))/sm.sqrt(b)
expr3
Note that Python integers and floats can also be used when constructing expressions:
expr4 = 5*sm.sin(12) + sm.Abs(-1001)/sm.sqrt(89.2)
expr4
Warning
Be careful with numbers, as SymPy may not intepret them as expected. For example:
1/2*a
Python does the division before it is multiplied by a
, thus a floating
point value is created. To fix this you can use the S()
function to
“sympify” numbers:
sm.S(1)/2*a
Or you can ensure the symbol comes first in the division operation:
a/2
Lastly, an expression of t
:
expr5 = t*sm.sin(omega*f(t)) + f(t)/sm.sqrt(t)
expr5
Exercise
Create an expression for the normal distribution function:
Solution
x, s, m = sm.symbols('x, sigma, mu')
sm.exp((x-m)**2/2/s**2)/sm.sqrt(2*sm.pi*s)
Notice that SymPy does some minor manipulation of the expression, but it is equivalent to the form shown in the prompt.
Printing¶
I introduced the srepr()
form of SymPy expressions above and mentioned that
expressions can have different representations. For the following srepr()
form:
sm.srepr(expr3)
"Add(Mul(Symbol('a'), sin(Symbol('omega'))), Mul(Pow(Symbol('b'), Rational(-1, 2)), Abs(Function('f')(Symbol('t')))))"
There is also a standard representation accessed with the repr()
function:
repr(expr3)
'a*sin(omega) + Abs(f(t))/sqrt(b)'
This form matches what you typically would type to create the expression and it
returns a string. The print()
function will display that string:
print(expr3)
a*sin(omega) + Abs(f(t))/sqrt(b)
SymPy also has a “pretty printer” (pprint()
) that makes use of unicode symbols
to provide a form that more closely resembles typeset math:
sm.pprint(expr3)
│f(t)│
a⋅sin(ω) + ──────
√b
Lastly, the following lines show how SymPy expressions can be represented as
LaTeX code using sympy.printing.latex.latex()
. The double
backslashes are present because double backslashes represent the escape
character in Python strings.
sm.latex(expr3)
'a \\sin{\\left(\\omega \\right)} + \\frac{\\left|{f{\\left(t \\right)}}\\right|}{\\sqrt{b}}'
print(sm.latex(expr3))
a \sin{\left(\omega \right)} + \frac{\left|{f{\left(t \right)}}\right|}{\sqrt{b}}
Warning
When you are working with long expressions, which will be the case in this course, there is no need to print them to the screen. In fact, printing them to the screen make take a long time and fill your entire notebook with an unreadable mess.
Exercise
Print the normal distribution expression
as a LaTeX string inside an equation environment.
Solution
x, s, m = sm.symbols('x, sigma, mu')
print(sm.latex(sm.exp((x-m)**2/2/s**2)/sm.sqrt(2*sm.pi*s),
mode='equation'))
\begin{equation}\frac{\sqrt{2} e^{\frac{\left(- \mu + x\right)^{2}}{2 \sigma^{2}}}}{2 \sqrt{\pi} \sqrt{\sigma}}\end{equation}
Differentiating¶
One of the most tedious tasks in formulating equations of motion is
differentiating complex trigonometric expressions. SymPy can calculate
derivatives effortlessly. The diff()
SymPy function takes an undefined function or an expression and differentiates
it with respect to the symbol provided as the second argument:
sm.diff(f(t), t)
All functions and expressions also have a .diff()
method which can be used
like so (many SymPy functions exist as standalone functions and methods):
f(t).diff(t)
expr3
is a more complicated expression:
expr3
It can be differentiated, for example, with respect to \(b\):
expr3.diff(b)
You can also calculate partial derivatives with respect to successive variables. If you want to first differentiate with respect to \(b\) and then with respect to \(t\) as in the following operation:
where:
then you can use successive arguments to .diff()
:
expr3.diff(b, t)
Note that the answer includes real and imaginary components and the signum function.
Warning
SymPy assumes all symbols are complex-valued unless told otherwise. You can attach assumptions to symbols to force them to be real, positive, negative, etc. For example, compare these three outputs:
h = sm.Function('h')
sm.Abs(h(t)).diff(t)
h = sm.Function('h', real=True)
sm.Abs(h(t)).diff(t)
h = sm.Function('h', real=True, positive=True)
sm.Abs(h(t)).diff(t)
Sometimes you may need to add assumptions to variables, but in general it will not be necessary. Read more about assumptions in SymPy’s guide.
Exercise
Differentiate expr5
above using this operator:
Solution
First show expr5
:
expr5
The twice partial derivative is:
expr5.diff(t, omega)
or you can chain .diff()
calls:
expr5.diff(t).diff(omega)
Evaluating Symbolic Expressions¶
SymPy expressions can be evaluated numerically in several ways. The
xreplace()
method allows substitution
of exact symbols or sub-expressions. First create a dictionary that maps
symbols, functions or sub-expressions to the replacements:
repl = {omega: sm.pi/4, a: 2, f(t): -12, b: 25}
This dictionary can then be passed to .xreplace()
:
expr3.xreplace(repl)
Notice how the square root and fraction do not automatically reduce to their
decimal equivalents. To do so, you must use the
evalf()
method. This method will
evaluate an expression to an arbitrary number of decimal points. You provide
the number of decimal places and the substitution dictionary to evaluate:
expr3.evalf(n=10, subs=repl)
type(expr3.evalf(n=10, subs=repl))
sympy.core.numbers.Float
Note that this is a SymPy Float
object, which is a special object that can have an arbitrary number of decimal
places, for example here is the expression evaluated to 80 decimal places:
expr3.evalf(n=80, subs=repl)
To convert this to Python floating point number, use float()
:
float(expr3.evalf(n=300, subs=repl))
type(float(expr3.evalf(n=300, subs=repl)))
float
This value is a machine precision floating point value and can be used with standard Python functions that operate on floating point numbers.
To obtain machine precision floating point numbers directly and with more
flexibility, it is better to use the
lambdify()
function to convert the
expression to a Python function. When using lambdify()
, all symbols and
functions should be converted to numbers so first identify what symbols and
functions make up the expression.
expr3
\(\omega, a, f(t)\), and \(b\) are all present in the expression. The
first argument of lambdify()
should be a sequence of all these symbols and
functions and the second argument should be the expression.
eval_expr3 = sm.lambdify((omega, a, f(t), b), expr3)
lambdify()
generates a Python function and, in this case, we store that
function in the variable eval_expr3
. You can see what the inputs and
outputs of the function are with help()
:
help(eval_expr3)
Help on function _lambdifygenerated:
_lambdifygenerated(omega, a, _Dummy_38, b)
Created with lambdify. Signature:
func(omega, a, f, b)
Expression:
a*sin(omega) + Abs(f(t))/sqrt(b)
Source code:
def _lambdifygenerated(omega, a, _Dummy_38, b):
return a*sin(omega) + abs(_Dummy_38)/sqrt(b)
Imported modules:
This function operates on and returns floating point values, for example:
eval_expr3(3.14/4, 2, -12, 25)
The type of lambdify’s return values will be NumPy floats.
type(eval_expr3(3.14/4, 2, -12, 25))
numpy.float64
These floats are interoperable with Python floats for single values (unlike SymPy Floats) but also support arrays of floats. For example:
eval_expr3(3.14/4, 2, -12, [25, 26, 27])
array([3.81365036, 3.76704398, 3.72305144])
type(eval_expr3(3.14/4, 2, -12, [25, 26, 27]))
numpy.ndarray
More on NumPy arrays of floats will be introduced in a later chapter.
Warning
Python and NumPy floats can be mixed, but avoid mixing SymPy Floats with either.
Note
This distinction between SymPy Float
objects and regular Python and
NumPy float
objects is important. In this case, the Python float and the
NumPy float are equivalent. The later will compute much faster because
arbitrary precision is not required. In this book, you will almost always
want to convert SymPy expressions to machine precision floating point
numbers, so use lambdify()
.
Exercise
Create a symbolic expression representing Newton’s Law of Universal
Gravitation. Use
lambdify()
to evaluate the expression for two mass of 5.972E24 kg and 80
kg at a distance of 6371 km apart to find the gravitational force in
Newtons.
Solution
G, m1, m2, r = sm.symbols('G, m1, m2, r')
F = G*m1*m2/r**2
eval_F = sm.lambdify((G, m1, m2, r), F)
eval_F(6.67430E-11, 5.972E24, 80, 6371E3)
Matrices¶
SymPy supports matrices of expressions and linear algebra. Many of the
operations needed in multibody dynamics are more succinctly formulated with
matrices and linear algebra. Matrices can be created by passing nested lists to
the Matrix()
object. For example:
mat1 = sm.Matrix([[a, 2*a], [b/omega, f(t)]])
mat1
mat2 = sm.Matrix([[1, 2], [3, 4]])
mat2
All matrices are two dimensional and the number of rows and columns, in that
order, are stored in the .shape
attribute.
mat1.shape
Individual elements of the matrix can be extracted with the bracket notation taking the row and column indices (remember Python indexes from 0):
mat1[0, 1]
The slice notation can extract rows or columns:
mat1[0, 0:2]
mat1[0:2, 1]
Matrix algebra can be performed. Matrices can be added:
mat1 + mat2
Both the *
and the @
operator perform matrix multiplication:
mat1*mat2
mat1@mat2
Element-by-element multiplication requires the sympy.hadamard_product()
function:
sm.hadamard_product(mat1, mat2)
Note that NumPy uses *
for element-by-element multiplication and @
for matrix multiplication,
so to avoid possible confusion, use @
for SymPy matrix multiplication.
Differentiation operates on each element of the matrix:
mat3 = sm.Matrix([expr1, expr2, expr3, expr4, expr5])
mat3
mat3.diff(a)
mat3.diff(t)
If you have column vectors \(\bar{v}\) and \(\bar{u}\), the
\((i,j)\) entries of the Jacobian of \(\bar{v}\) with respect to the
entries in vector \(\bar{u}\) are found with \(\mathbf{J}_{ij} =
\frac{\partial v_i}{\partial u_j}\). The Jacobian matrix of vector (column
matrix) can be formed with the
jacobian()
method.
This calculates the partial derivatives of each element in the vector with
respect to a vector (or sequence) of variables.
mat4 = sm.Matrix([a, b, omega, t])
mat4
mat3.jacobian(mat4)
Exercise
Write your own function that produces a Jacobian given a column matrix of expressions. It should look like:
def jacobian(v, x):
"""Returns the Jacobian of the vector function v with respect to the
vector of variables x."""
# fill in your code here
return J_v_x
Show that it gives the same solution as the above .jacobian()
method. Do
not use the .jacobian()
method in your function.
Solution
def jacobian(v, x):
"""Returns the Jacobian of the vector function v with respect to the
vector of variables x."""
diffs = []
for expr in v:
for var in x:
diffs.append(expr.diff(var))
J_v_x = sm.Matrix(diffs).reshape(len(v), len(x))
return J_v_x
jacobian(mat3, mat4)
Solving Linear Systems¶
You’ll need to solve linear systems of equations often in this book. SymPy offers a number of ways to do this, but the best way to do so if you know a set of equations are linear in specific variables is the method described below. First, you should confirm you have equations of this form:
These equations can be put into matrix form:
where:
\(\bar{x}\), the solution, is found with matrix inversion (if the matrix is invertible):
Taking the inverse is not computationally efficient and potentially numerically inaccurate, so some form of Gaussian elmination should be used to solve the system.
To solve with SymPy, start with a column matrix of linear expressions:
a1, a2 = sm.symbols('a1, a2')
exprs = sm.Matrix([
[a1*sm.sin(f(t))*sm.cos(2*f(t)) + a2 + omega/sm.log(f(t), t) + 100],
[a1*omega**2 + f(t)*a2 + omega + f(t)**3],
])
exprs
Since we know these two expressions are linear in the \(a_1\) and
\(a_2\) variables, the partial derivatives with respect to those two
variables will return the linear coefficients. The \(\mathbf{A}\) matrix
can be formed in one step with the .jacobian()
method:
A = exprs.jacobian([a1, a2])
A
The \(\bar{b}\) vector can be formed by setting \(a_1=a_2=0\), leaving the terms that are not linear in \(a_1\) and \(a_2\).
b = -exprs.xreplace({a1: 0, a2: 0})
b
The inv()
method can
compute the inverse of A to find the solution:
A.inv() @ b
But it is best to use the
LUsolve()
method to
perform an LU decomposition Gaussian-Elimination to solve the system,
especially as the dimension of \(\mathbf{A}\) grows:
A.LUsolve(b)
Warning
This method of solving symbolic linear systems is fast, but it can give incorrect answers for:
expressions that are not acutally linear in the variables the Jacobian is taken with respect to
\(\mathbf{A}\) matrix entries that would evaluate to zero if simplified or specific numerical values are provided
So only use this method if you are sure your equations are linear and if
your \(\mathbf{A}\) matrix is made up of complex expressions, watch out
for nan
results after lambdifying.
solve()
and
linsolve()
can also solve linear
systems and they check for linearity and properties of the A matrix. The
cost is that they can be extremely slow for large expressions (which we will
have in this book).
Exercise
Solve the following equations for all of the \(L\)’s and then use
lambdify()
to evaluate the solution for \(F_1=13\) and
\(F_2=32\).
Solution
L1, L2, L3, L4, L5, L6, F1, F2 = sm.symbols('L1, L2, L3, L4, L5, L6, F1, F2')
exprs = sm.Matrix([
-L1 + L2 - L3/sm.sqrt(2),
L3/sm.sqrt(2) + L4 - F1,
-L2 - L5/sm.sqrt(2),
L5/sm.sqrt(2) - F2,
L5/sm.sqrt(2) + L6,
-L4 -L5/sm.sqrt(2),
])
exprs
unknowns = sm.Matrix([L1, L2, L3, L4, L5, L6])
coef_mat = exprs.jacobian(unknowns)
rhs = -exprs.xreplace(dict(zip(unknowns, [0]*6)))
sol = coef_mat.LUsolve(rhs)
sm.Eq(unknowns, sol)
eval_sol = sm.lambdify((F1, F2), sol)
eval_sol(13, 32)
array([[-77. ],
[-32. ],
[ 63.63961031],
[-32. ],
[ 45.254834 ],
[-32. ]])
Simplification¶
The above result from
LUsolve()
is a bit
complicated. Reproduced here:
a1, a2 = sm.symbols('a1, a2')
exprs = sm.Matrix([
[a1*sm.sin(f(t))*sm.cos(2*f(t)) + a2 + omega/sm.log(f(t), t) + 100],
[a1*omega**2 + f(t)*a2 + omega + f(t)**3],
])
A = exprs.jacobian([a1, a2])
b = -exprs.xreplace({a1: 0, a2: 0})
sol = A.LUsolve(b)
SymPy has some functionality for automatically simplifying symbolic
expressions. The function simplify()
will attempt to find a simpler version:
sm.simplify(sol)
But you’ll have the best luck at simplifying if you use simplification functions that
target the type of expression you have. The
trigsimp()
function only attempts
trigonometric simplifications, for example:
trig_expr = sm.cos(omega)**2 + sm.sin(omega)**2
trig_expr
sm.trigsimp(trig_expr)
Warning
Only attempt simplification on expressions that are several lines of text. Larger expressions become increasingly computationally intensive to simplify and there is generally no need to do so.
As mentioned earlier, SymPy represents expressions as trees. Symbolic
expressions can also be represented as directed acyclic graphs that contain
only one node for each unique expression (unlike SymPy’s trees which may have
the same expression in more than one node). These unique expressions, or
“common subexpressions”, can be found with the
cse()
function. This function will
provide a simpler form of the equations that minimizes the number of operations
to compute the answer. We can count the number of basic operations (additions,
multiplies, etc.) using count_ops()
:
sm.count_ops(sol)
We can simplify with cse()
:
substitutions, simplified = sm.cse(sol)
The substitutions
variable contains a list of tuples, where each tuple has
a new intermediate variable and the sub-expression it is equal to.
substitutions[0]
The Eq()
class with tuple
unpacking (*
) can be used to display these tuples as equations:
sm.Eq(*substitutions[0])
sm.Eq(*substitutions[1])
sm.Eq(*substitutions[2])
sm.Eq(*substitutions[4])
The simplified
variable contains the simplified expression, made up of the
intermediate variables.
simplified[0]
We can count the number of operations of the simplified version:
num_ops = sm.count_ops(simplified[0])
for sub in substitutions:
num_ops += sm.count_ops(sub[1])
num_ops
Exercise
lambdify()
has an optional
argument cse=True|False
that applies common subexpression elimination
internally to simplify the number of operations. Differentiate the
base_expr
with respect to x
10 times to generate a very long
expression. Create two functions using lambdify()
, one with cse=True
and one with cse=False
. Compare how long it takes to numerically
evaluate the resulting functions using the %timeit
magic.
a, b, c, x, y, z = sm.symbols('a, b, c, x, y, z')
base_expr = a*sm.sin(x*x + b*sm.cos(x*y) + c*sm.sin(x*z))
Solution
Differentiate 10 times:
long_expr = base_expr.diff(x, 10)
Create the numerical functions:
eval_long_expr = sm.lambdify((a, b, c, x, y, z), long_expr)
eval_long_expr_cse = sm.lambdify((a, b, c, x, y, z), long_expr, cse=True)
Now time each function:
%%timeit
eval_long_expr(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)
261 μs ± 1.13 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
%%timeit
eval_long_expr_cse(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)
16.5 μs ± 31.8 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
Learn more¶
This section only scratches the surface of what SymPy can do. The presented concepts are the basic ones needed for this book, but getting more familiar with SymPy and what it can do will help. I recommend doing the SymPy Tutorial. The “Gotchas” section is particularly helpful for common mistakes when using SymPy. The tutorial is part of the SymPy documentation https://docs.sympy.org, where you will find general information on SymPy.
The tutorial is also available on video:
If you want to ask a question about using SymPy (or search to see if someone else has asked your question), you can do so at the following places:
SymPy mailing list: Ask questions via email.
SymPy Github Discussions: Ask questions via Github.
Stackoverflow: Ask and search questions on the most popular coding Q&A website.