Ordinary Differential Equations Exercise 1

Imports


In [2]:
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
from scipy.integrate import odeint
from IPython.html.widgets import interact, fixed


:0: FutureWarning: IPython widgets are experimental and may change in the future.

Euler's method

Euler's method is the simplest numerical approach for solving a first order ODE numerically. Given the differential equation

$$ \frac{dy}{dx} = f(y(x), x) $$

with the initial condition:

$$ y(x_0)=y_0 $$

Euler's method performs updates using the equations:

$$ y_{n+1} = y_n + h f(y_n,x_n) $$$$ h = x_{n+1} - x_n $$

Write a function solve_euler that implements the Euler method for a 1d ODE and follows the specification described in the docstring:


In [3]:
def solve_euler(derivs, y0, x):
    """Solve a 1d ODE using Euler's method.
    
    Parameters
    ----------
    derivs : function
        The derivative of the diff-eq with the signature deriv(y,x) where
        y and x are floats.
    y0 : float
        The initial condition y[0] = y(x[0]).
    x : np.ndarray, list, tuple
        The array of times at which of solve the diff-eq.
    
    Returns
    -------
    y : np.ndarray
        Array of solutions y[i] = y(x[i])
    """
    y = [y0]
    for n in range(1, len(x)):
        y.append(y[n-1] + (x[n] - x[n-1])*derivs(y[n-1], x[n-1]))
        
    return np.asarray(y)

In [4]:
assert np.allclose(solve_euler(lambda y, x: 1, 0, [0,1,2]), [0,1,2])

The midpoint method is another numerical method for solving the above differential equation. In general it is more accurate than the Euler method. It uses the update equation:

$$ y_{n+1} = y_n + h f\left(y_n+\frac{h}{2}f(y_n,x_n),x_n+\frac{h}{2}\right) $$

Write a function solve_midpoint that implements the midpoint method for a 1d ODE and follows the specification described in the docstring:


In [5]:
def solve_midpoint(derivs, y0, x):
    """Solve a 1d ODE using the Midpoint method.
    
    Parameters
    ----------
    derivs : function
        The derivative of the diff-eq with the signature deriv(y,x) where y
        and x are floats.
    y0 : float
        The initial condition y[0] = y(x[0]).
    x : np.ndarray, list, tuple
        The array of times at which of solve the diff-eq.
    
    Returns
    -------
    y : np.ndarray
        Array of solutions y[i] = y(x[i])
    """
    # YOUR CODE HERE
    y = [y0]
    for n in range(1, len(x)):
        h = x[n] - x[n-1]
        y.append(y[n-1] + h*derivs(y[n-1] + h/2*derivs(y[n-1], x[n-1]), x[n-1] + h/2))
        
    return np.asarray(y)

In [6]:
assert np.allclose(solve_midpoint(lambda y, x: 1, 0, [0,1,2]), [0,1,2])

You are now going to solve the following differential equation:

$$ \frac{dy}{dx} = x + 2y $$

which has the analytical solution:

$$ y(x) = 0.25 e^{2x} - 0.5 x - 0.25 $$

First, write a solve_exact function that compute the exact solution and follows the specification described in the docstring:


In [7]:
def solve_exact(x):
    """compute the exact solution to dy/dx = x + 2y.
    
    Parameters
    ----------
    x : np.ndarray
        Array of x values to compute the solution at.
    
    Returns
    -------
    y : np.ndarray
        Array of solutions at y[i] = y(x[i]).
    """
    # YOUR CODE HERE
    return 0.25*np.exp(2*x) - 0.5*x - 0.25

In [8]:
assert np.allclose(solve_exact(np.array([0,1,2])),np.array([0., 1.09726402, 12.39953751]))

In the following cell you are going to solve the above ODE using four different algorithms:

  1. Euler's method
  2. Midpoint method
  3. odeint
  4. Exact

Here are the details:

  • Generate an array of x values with $N=11$ points over the interval $[0,1]$ ($h=0.1$).
  • Define the derivs function for the above differential equation.
  • Using the solve_euler, solve_midpoint, odeint and solve_exact functions to compute the solutions using the 4 approaches.

Visualize the solutions on a sigle figure with two subplots:

  1. Plot the $y(x)$ versus $x$ for each of the 4 approaches.
  2. Plot $\left|y(x)-y_{exact}(x)\right|$ versus $x$ for each of the 3 numerical approaches.

Your visualization should have legends, labeled axes, titles and be customized for beauty and effectiveness.

While your final plot will use $N=10$ points, first try making $N$ larger and smaller to see how that affects the errors of the different approaches.


In [9]:
# YOUR CODE HERE
x = np.linspace(0, 1, 11)

def derivs(yvec, x):
    y = yvec
    dy = x + 2*y
    return np.array([dy])

y0 = np.array([1.0])
print(y0.shape)

   
print(solve_euler(derivs, 1.0, x))
print(solve_midpoint(derivs, 1.0, x))
#gives error "object too deep for desired array". thinks y0 is a 2d array?
print(odeint(derivs, y0, x))


(1,)
[ 1.          1.2         1.45        1.76        2.142       2.6104
  3.18248     3.878976    4.7247712   5.74972544  6.98967053]
[ 1.          1.225       1.5105      1.86981     2.3191682   2.8783852
  3.57162995  4.42838854  5.48463402  6.7842535   8.38078927]
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-9-3bcd7c35e8fb> in <module>()
     13 print(solve_euler(derivs, 1.0, x))
     14 print(solve_midpoint(derivs, 1.0, x))
---> 15 print(odeint(derivs, y0, x))

/usr/local/lib/python3.4/dist-packages/scipy/integrate/odepack.py in odeint(func, y0, t, args, Dfun, col_deriv, full_output, ml, mu, rtol, atol, tcrit, h0, hmax, hmin, ixpr, mxstep, mxhnil, mxordn, mxords, printmessg)
    146     output = _odepack.odeint(func, y0, t, args, Dfun, col_deriv, ml, mu,
    147                              full_output, rtol, atol, tcrit, h0, hmax, hmin,
--> 148                              ixpr, mxstep, mxhnil, mxordn, mxords)
    149     if output[-1] < 0:
    150         print(_msgs[output[-1]])

ValueError: object too deep for desired array

In [10]:
assert True # leave this for grading the plots

In [ ]: