📓 About This Tutorial

This webpage is a static view of an interactive Jupyter Notebook tutorial. To get the full interactive experience where you can run Python code, modify examples and complete exercises, click the "Open in Colab" button. This will access the Google Colab notebook from a GitHub repository github.com/dchappell2/Computational-Physics and open it in Google Colab. You will need a Google account if you want to open it in Colab.

Note: You can download a pdf of the lecture slides for this tutorial here: ODE 4 slides

ODE-4: SciPy Integration Methods

Loading the SciPy library

Because the SciPy library is quite extensive (and takes up a lot of memory), its usually a good idea to just load the particular module or function that you will use. In this tutorial we will be using a numerical integration function solve_ivp() for solving ODEs. The "ivp" initials stand for "initial value problem," which is the type of problem we are usually intersted in.

Here's the command to load the function:

from scipy.integrate import solve_ivp

Solving and ODE with solve_ivp() and the RK45 scheme

The built-in numerical integrator in the SciPy library is called solve_ivp() which stands for "Solve Initial Value Problem". It offers multiple integration methods for ODEs with different properties. In this tutorial we will explore one of the most widely-used integration methods: RK45, an adaptive time-step method that uses 4th and 5th order Runge Kutta schemes.

On each timestep, the RK45 method solves the ODE using both a 4th and a 5th order Runge Kutta method. It then calculates the difference between the solutions, call it $\epsilon = |y_{n+1}(RK4) -y_{n+1}(RK5)|$. If $\epsilon > atol + rtol\cdot |y_n|$, then it reduces the time step and recalculates the error. Once the error is within the acceptable tolerance, the method advances to the next timestep. It also has a method for expanding the time step when the error is too small. The result is a method that can reduce the size of the timestep when the solution is rapidy changing and increase the timestep when the solution is slowly varying. This approach can vastly speed up the integration while preserving accuracy.

Here's an example of calling solve_ivp():

```

sol = solve_ivp(deriv_sho, (0,tmax), y0, method='RK45', args=params,

atol=1e-6,rtol=1e-6)

```

Passed parameters for solve_ivp:

Returned value:

Derivative and Plotting Functions

The following is the Simple Harmonic Oscillator derivative function that we used in the last tutorial.

We also include a plotting function to plot the results and errors obtained from the numerical integration.

import numpy as np
import matplotlib.pyplot as plt

#########  SHO Derivative Function  #########
#
# This function returns the derivatives for the Simple Harmoni Oscillator
#      dx/dt = v     dv/dt = -(k/m) x
#
# Passed parameters:
#   t = time
#   y = 1x2 array containing position (x) and velocity (v)
#   m = mass
#   k = spring constant
#
# Returned values:
#   dydt = 1x2 array containing the derivatives dx/dt and dv/dt

def deriv_sho(t, y, m, k):

    # extract variables from y array
    x = y[0]          # position
    v = y[1]          # velocity

    # calculate derivatives
    dxdt = v
    dvdt = -(k/m)*x

    # return derivatives in a numpy array
    return np.array([dxdt, dvdt])



#########  Plot Results  #########
#
# This function plots the results of the numerical integration along with
# the analytic solution and the absolute error
#
# Passed parameters:
#   x      = 1D array containing the numerical solution
#   x_true = 1D array containing the analytic solution
#   t      = 1D array containing the times
#   title  = text string containing a message to print for the title
#   style  = style for plotting, default valus is to connect data points
#            with a straight line. This parameter is optional
#
# Returned values:
#   none
#
# Action:
#   top plot:    plot of numerical solution vs time overlayed with the analytic solution
#   lower plot:  error between numerical and analytic solutions
#
def plot_solution(x, x_true, t, title, style="-"):

    err = np.abs(x-x_true)     # calculate numerical error

    #########  Plot Solution  #########

    plt.subplot(2,1,1)                           # upper subplot
    plt.plot(t, x, style, label='Numerical')            # plot position
    plt.plot(t, x_true, '--', label='Analytic')  # analytic solution

    plt.xlabel('t')            # label the x and y axes
    plt.ylabel('x')
    plt.title(title)           # give the plot a title
    plt.legend()               # display the legend

    #########  Plot Error  #########

    plt.subplot(2,1,2)         # lower subplot
    plt.plot(t, err, style)    # plot position
    plt.xlabel('t')            # label the x and y axes
    plt.ylabel('error')

    plt.show()                 # display the plot

solve_ivp() solution to the S.H.O.

We'll now use the solve_ivp() command to solve the simple harmonic oscillator ODE.

Run the following Code Cell to see the results.

import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp

#########  Parameters  #########

m    = 1         # mass
k    = 1         # spring constant
tmax = 50        # maximum time
dt   = 0.01      # time step
x0   = 1         # initial position
v0   = 0         # initial velocity

params = np.array([m,k])    # bundle derivative parameters together
y0 = np.array([x0,v0])      # bundle initial conditions together

#########  Perform RK45 Integration  #########

sol = solve_ivp(deriv_sho, (0,tmax), y0, method='RK45', args=params,
    atol=1e-4,rtol=1e-3)
t = sol.t            # extract times
x = sol.y[0,:]       # extract positions
v = sol.y[1,:]       # extract velocities

#########  Analytic Solution  #########

omega = np.sqrt(k/m)
x_true = x0 * np.cos(omega*t)

#########  Plot Solution  #########

plot_solution(x, x_true, t, "SHO - RK45")

Output image

After running the above code, you may notice that the solution looks jagged, with sharp angles. You may also notice that the numerical solution appears to overlay the analytic solution well (i.e. they are on top of each other). So the question is: Did the RK45 integrator do a good job of solving our ODE?

The jagged appearance is due to the fact that the adaptive time step algorithm is able to use pretty large time steps and still produce an error less than 0.015 over 8 periods. Because we chose to connect the data points with straight line segments, we get a jagged graph (normally, the time step is so small we don't notice this effect).

In otherwords, there are two separate effects:

In the graph above, the accuracy is decent (less than the thickness of the line used for the graph), but the sampling is infrequent so we get a jagged graph.

So, back to our question: Did the RK45 integrator do a good job of solving our ODE?

✅ Skill Check 1

We can better visualize the effect of sampling by re-plotting the solution using filled circles to mark the data points instead of line segments (the analytic solution will still use a dashed line). We pass the 'o' optional parameter to our plot_solution() function. Run the following Code Cell:

plot_solution(x, x_true, t, "SHO - RK45",'o')
Output image

What do you notice?

Your answer here

✅ Skill Check 2

Below is a copy of the RK45 code from above. Play around with the relative and absolute tolerances. Can you get the resulting error graphed in the plot to be less than $10^{-9}$? Remember, the atol and rtol arguments only set the tolerance error between the 4th and 5th order Runge Kutta solutions, not with the actual, analytic solution. How does adjusting the tolerances affect the appearance of the solution?

import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp

#########  Parameters  #########

m    = 1         # mass
k    = 1         # spring constant
tmax = 50        # maximum time
dt   = 0.01      # time step
x0   = 1         # initial position
v0   = 0         # initial velocity

params = np.array([m,k])    # bundle derivative parameters together
y0 = np.array([x0,v0])      # bundle initial conditions together

#########  Perform RK45 Integration  #########

sol = solve_ivp(deriv_sho, (0,tmax), y0, method='RK45', args=params,
    atol=1e-10,rtol=1e-10)
t = sol.t            # extract times
x = sol.y[0,:]       # extract positions
v = sol.y[1,:]       # extract velocities

#########  Analytic Solution  #########

omega = np.sqrt(k/m)
x_true = x0 * np.cos(omega*t)

#########  Plot Solution  #########

plot_solution(x, x_true, t, "SHO - RK45")

Output image

Custom Evaluation Times

Adaptive timestep methods produce solutions at nonuniform times. If you want to generate a solution on uniformly spaced times, you have two options:

We'll give exaples of each method.

🔆 Example: Specify an array of times to solve_ivp()

Method:

We'll assume you ran the preceeding code, so will just run the solve_ivp() command:



#########  Perform RK45 Integration  #########

times = np.linspace(0,tmax,500)
sol = solve_ivp(deriv_sho, (0,tmax), y0, method='RK45', t_eval=times,
    args=params, atol=1e-6,rtol=1e-6)
t = sol.t            # extract times
x = sol.y[0,:]       # extract positions
v = sol.y[1,:]       # extract velocities

#########  Analytic Solution  #########

omega = np.sqrt(k/m)
x_true = x0 * np.cos(omega*t)

#########  Plot Solution  #########

plot_solution(x, x_true, t, "SHO - RK45")

Output image

🔆 Example: Use dense output to interpolate solution after integration

Method:



#########  Perform RK45 Integration  #########

sol = solve_ivp(deriv_sho, (0,tmax), y0, method='RK45', dense_output=True,
    args=params, atol=1e-6,rtol=1e-6)

# Extract solution on a regular grid of time values
t = np.linspace(0, tmax, 500)      # define time array
y = sol.sol(t)                     # create a 2D solution array
x = y[0,:]                         # extract position from the solution array

#########  Analytic Solution  #########

omega = np.sqrt(k/m)
x_true = x0 * np.cos(omega*t)

#########  Plot Solution  #########

plot_solution(x, x_true, t, "SHO - RK45")

Output image

Application: Chaos and the Lorentz Attractor

Chaotic dynamical systems are deterministic systems whose behavior is highly sensitive to initial conditions, leading to outcomes that appear random. Often referred to as the "butterfly effect," this sensitivity means that even tiny differences in the starting point of the system will result in vastly different trajectories over time, making long-term, precise prediction impossible. Despite their unpredictable nature, these systems can exhibit hidden patterns, self-similarity and structure, often involving fractals and strange attractors.

The most widely studied chaotic system is the Lorenz system. It was first studied by Edward lorenz, Ellen Fetter and Margaret Hamilton in 1963 to model atmospheric convection in weather systems. The system consists of three coupled ODEs:

$$\frac{dx}{dt}=\sigma (y-x) $$

$$\frac{dy}{dt}=x (r - z) -y$$

$$\frac{dz}{dt}=x y -b z$$

where $x$, $y$ and $z$ are dimennsionless variables proportional to atmospheric quantities.

System parameters showing their standard values for chaotic dynamics:

The dyanamics of the system change dramatically as the parameter $r$ is changed.

In the following Skill Checks, you will explore the dynamics of the Lorenz system through numerical integration.

✅ Skill Check 3

Write some code to numerically integrate the Lorenz system.

For each of the values of $r$ given below, use your code to explore the dynamics of the Lorentz system, assuming $\sigma = 10$ and $b=8/3$.

In each case do the following:

hint: You might consider placing your code in a function that calculates and plots the graphs. If you pass the value $r$ to your function, you can use your same function to produce the plots for each $r$ value (this will save you from making multiple copies of your code.)

import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp

# your code here




✅ Skill Check 4

In this Skill Check, you will explore how sensitive the Lorenz system is to initial conditions. We will see how the evolution of the system changes when the starting location is changed by a small amount.

Specs:

This example illustrates why it is practically impossible to predict chaotic systems (such as the weather) very far into the future.

import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp

# your code here



✅ Skill Check 5

In this Skill Check, you explore how rapidly closely spaced initial conditions diverge from each other in a chaotic system.

Specs:

$$d(t) = \sqrt{(x_1(t)-x_2(t))^2 + (y_1(t)-y_2(t))^2 + (z_1(t)-z_2(t))^2}$$.

import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp

# your code here