SymPy¶
So far we have mostly discussed scientific computing from a numerical perspective. Symbolic computing can also done readily in Python using SymPy. The SymPy website has many great tutorials as well as the documentation for SymPy.
An example: Finding wavespeeds of a hyperbolic system¶
In physics, wavelike phenomena often arise from systems of hyperbolic conservation laws. Generally, these take the form
where \(\textbf{U}\) is a vector of conserved quantities and \(\textbf{F}\) is a flux tensor describing the flow of these quantities. In two space dimensions the above becomes
where \(\textbf{F} = \left(\textbf{F}(\textbf{U}),\textbf{G}(\textbf{U})\right)\) consists of the \(x\) and \(y\) direction fluxes. We can use the chain rule to decompose the flux derivatives, which gives the so called quasi-linear form of the equation:
where \(\frac{\partial\textbf{F}}{\partial\textbf{U}}\) and \(\frac{\partial\textbf{G}}{\partial\textbf{U}}\) are Jacobian matrices of each flux. These matrices are interesting both mathematically and physically. In particular, the eigenvalues of these matrices yield the wavespeeds in each direction.
Let’s consider the shallow water equations (in conservation form)
where the conserved variables are the height of the fluid \(h\), and the momenta \(hu\) and \(hv\). Additionally, \(g\) is the gravitational acceleration. The following SymPy code generates the flux Jacobian in the \(x\) direction, and finds the corresponding wavespeeds.
# File: sweEigs.py
# Author: Ian May
# Purpose: Symbolically compute flux Jacobian for 2D shallow
# water equations and look at eigendecomposition
import sympy as sym
# Shallow water equations depend on:
# h: height, strictly positive
# hu: x-direction momentum
# hv: y-direction momentum
# g: gravitational acc. (constant, positive)
def sweVars():
h = sym.Symbol('h',positive=True)
hu = sym.Symbol('hu')
hv = sym.Symbol('hv')
g = sym.Symbol('g',positive=True)
return h,hu,hv,g
# Flux of conserved quantities in x-direction
def fluxX(h,hu,hv,g):
return sym.Matrix([hu,hu**2/h + g*h**2/2,hu*hv/h])
# Flux of conserved quantities in y-direction
def fluxY(h,hu,hv,g):
return sym.Matrix([hv,hu*hv/h,hv**2/h + g*h**2/2])
# Jacobian of x-flux wrt. conserved variables
def fluxJacX(h,hu,hv,g):
return fluxX(h,hu,hv,g).jacobian([h,hu,hv])
# x-direction wavespeeds are e-vals of flux Jacobian
def wavespeedX(h,hu,hv,g):
return tuple(fluxJacX(h,hu,hv,g).eigenvals().keys())
# Final results can be simplified by using primitive variables
def relations(h,hu,hv,g):
u = sym.Symbol('u')
v = sym.Symbol('v')
rels = {hu/h:u, hv/h:v}
return rels
if __name__ == '__main__':
# Setup SWE variables as symbols
h,hu,hv,g = sweVars()
# Get wavespeeds as e-vals of flux Jacobian
wvX = wavespeedX(h,hu,hv,g)
# Get dictionary of conservative -> primitive vars
rels = relations(h,hu,hv,g)
# Try to simplify wavespeeds
print('Found x-direction wavespeeds as:')
for w in wvX:
print(w.subs(rels))
Exercises:
Generate \(y\) direction wavespeeds as well
Generate eigenvectors (waves) of the flux Jacobian
SciPy¶
SciPy is a Python package that contains numerous scientific toolboxes that are commonly used in the scientific computing community. SciPy is built on top of NumPy, and two work together quite well.
Included in SciPy are implementations of numerical interpolation and quadrature rules, optimization, image processing, statistics, special functions, etc. The routines available in SciPy are highly optimized, robust, and well documented. To see examples of how to use SciPy for scientific computing check out the SciPy users guide
An example: Solving a nonlinear, chaotic, ODE¶
To demonstrate one small piece of SciPy we’ll solve the Lorenz system of equations:
where \(\rho,\sigma,\beta\) are parameters which we’ll fix at \(28\), \(10\), and \(8/3\) respectively. These equations arise as a highly simplified, reduced order model, for convection as applicable to weather prediction. While quite simple to write down, these equations are nonlinear and can not be solved analytically. Furthermore, they give rise to complex and chaotic behavior, and are arguably the most well known example of a (continuous) chaotic system.
As such, these are a perfect candidate for numerical treatment. SciPy makes solving equations like this nearly trivial, as the following code demonstrates.
1# File: lorenz_demo.py
2# Author: Ian May
3# Purpose: Demonstrate SciPy by solving the Lorenz equations
4
5import numpy as np
6import matplotlib.pyplot as plt
7from scipy.integrate import solve_ivp
8
9# Define right hand side of ODE
10# Note calling signature must look like this
11def lorenz(t,y):
12 # Parameters for the system
13 rho = 28
14 sigma = 10
15 beta = 8/3
16 # Return RHS
17 f = np.zeros_like(y)
18 f[0] = sigma*(y[1] - y[0])
19 f[1] = y[0]*(rho - y[2]) - y[1]
20 f[2] = y[0]*y[1] - beta*y[2]
21 return f
22
23if __name__ == '__main__':
24 # Set initial condition and solve
25 t0, tf = 0.0, 100.0
26 dt_max = 0.02
27 y0 = np.ones(3)
28 sol = solve_ivp(lorenz,(t0,tf),y0,max_step=dt_max)
29
30 # Plot time series of each component
31 fig, axs = plt.subplots(3,1)
32
33 axs[0].plot(sol.t,sol.y[0,:])
34 axs[0].set_ylabel('x',fontsize=14)
35 axs[0].set_title('Evolution of Lorenz equations',fontsize=16)
36
37 axs[1].plot(sol.t,sol.y[1,:])
38 axs[1].set_ylabel('y',fontsize=14)
39
40 axs[2].plot(sol.t,sol.y[2,:])
41 axs[2].set_xlabel('t',fontsize=14)
42 axs[2].set_ylabel('z',fontsize=14)
43
44 fig.tight_layout()
45 plt.show()
46
47 # Plot phase space trajectory
48 ax = plt.figure().add_subplot(projection='3d')
49
50 ax.plot(sol.y[0,:],sol.y[1,:],sol.y[2,:])
51
52 ax.set_xlabel('x',fontsize=15)
53 ax.set_ylabel('y',fontsize=15)
54 ax.set_zlabel('z',fontsize=15)
55 ax.set_title('Lorenz phase space trajectory',fontsize=18)
56
57 plt.show()
This should have produced the following two figures.

Time history of each component in the Lorenz system¶

Phase space trajectory of the Lorenz system from the given initial condition.¶