ILI286 - Computación Científica II

Integración Numérica

[S]cientific [C]omputing [T]eam

Version: 1.23

Introducción

En este notebook estudiaremos métodos numéricos para integrar numéricamente funciones de distinto tipo. Las motivaciones para la utilización de estos métodos son varias, algunas de ellas:

  • El cálculo teórico de la antiderivada de una función es un proceso tedioso, incluso para librerías de álgebra simbólica como SymPy.
  • Peor que lo anterior, existen funciones que no tienen integral elemental, esto es, aquellas cuya antiderivada no puede ser expresada como una expresión de funciones elementales. Un ejemplo clásico es: $$ \int e^{-x^2}dx $$
  • Como veremos, existen métodos numéricos sumamente precisos, para los cuales el error que se comete debido a las aproximaciones, converge rápidamente a $0$ a medida que se aumenta la cantidad de puntos en la malla utilizada.

Partiendo de la noción geométrica de la integral definida, como aquella cantidad que expresa el área bajo la curva de una función, nacen los distintos métodos que estudiaremos a continuación.

Configuraciones

Librerías utilizadas en el notebook


In [1]:
import numpy as np
from matplotlib import pyplot as plt
import math
import time
%matplotlib inline
from ipywidgets import interact
import inspect

La siguiente función nos permitirá graficar y visualizar apropiadamente los resultados.


In [2]:
###########################################################################
# General plotting framework
###########################################################################
def plot(f, xbin, ybin, int_val, N, text, figname=''):
    plt.figure(figsize=(12,6))
    n = 201
    # Get a representation of f as a continuous function
    x = np.linspace(xbin.min(), xbin.max(), n)
    y = f(x)
    # Plot the function
    plt.plot(x, y, 'r', lw=2.0)
    # Plot the interpolation
    plt.fill_between(xbin, 0, ybin, alpha=0.25, lw=2.0)
    # Setting the lims
    ymin, ymax = y.min(), y.max()
    if abs(ymax-ymin)<1E-6:
        ymin, ymax = 0.0, 1.0
    dy = .1*(ymax-ymin)
    plt.ylim([ymin-dy,ymax+dy])
    xmin, xmax = x.min(), x.max()
    if abs(b-a)<1E-6:
        xmin, xmax = 0.0, 1.0
    dx = .1*(b-a)
    plt.xlim([xmin-dx,xmax+dx])
    # Do the text
    if N>1:
        text_N = r"$%s \approx %.10f$ (usando %d evaluaciones de $f$)" %(text, int_val, N)
        plt.text(min(x), max(y), text_N, fontsize=18)
        #plt.text(min(x), 0.9*max(y), "Valor exacto $2.35040$", fontsize=18)
    plt.xlabel("x")
    plt.ylabel("y")
    plt.show()
    return

Parámetros

Para los experimentos numéricos, consideraremos la función $f(x)$ con dominio $x \in [a,b]$. El número de puntos a utilizar en las mallas equiespaciadas vendrá definido por $N$. Todas las anteriores se definen en la celda siguiente.


In [3]:
#limits of integration
a = -1; b = 1
#function to integrate
myfun = lambda x : np.exp(x) #x**2 #1 # x #np.exp(-x)
#number the points in the 1D grid
N = 10
#text to show in the graphs
text= r"\int_{%+.2f}^{%+.2f} e^x dx" %(a,b)

Sumas de Riemann

Como primera aproximación hacia la integración numérica, revisaremos la suma que compone la base sobre la cual se define una integral definida: La integral de Riemann. Esta consiste en particionar el dominio de integración $D = [a,b] \rightarrow a = x_0 < x_1 < \cdots < x_{n-1} < x_n = b $, de tal modo que para cada partición $[x_k,x_{k+1}]$ aproximemos el área bajo $f$ por rectángulos, tomando como altura de los rectángulos a $f(c)$ con $c \in [x_k,x_{k+1}]$. Cuando se elige tal $c$ como uno de los extremos de la partición, se da origen a las siguiente dos aproximaciones:

Suma izquierda de Riemann

Eligiendo $c = x_{k}$ (extremo izquierdo) para cada partición, sobre una malla regular de ancho $x_{k+1}-x_{k}=\Delta x$: \begin{align*} A = \int_a^b f(x) dx & \approx \sum_{k=0}^{n-1} f(x_k) \Delta x \end{align*}

Suma derecha de Riemann

Eligiendo $c = x_{k+1}$ (extremo derecho) para cada partición, sobre una malla regular de ancho $x_{k+1}-x_{k}=\Delta x$: \begin{align*} A = \int_a^b f(x) dx \approx \sum_{k=1}^{n} f(x_k)\Delta x \end{align*}

El código que se provee a continuación, implementa la integración numérica por sumas de Riemann.


In [4]:
###########################################################################
# Riemann Rule
###########################################################################
def riemann(myfun, N, a, b, direction='left', verbose=False, text=text, figname=''):
    f = np.vectorize(myfun) # So we can apply it to arrays without trouble
    x = np.linspace(a, b, N+1) # We want N bins, so N+1 points  
    dx = x[1]-x[0]
    if direction in 'left':
        points = x[:-1]
    elif direction in 'right':
        points = x[1:]
    else:
        print("Riemann Sum: choose left or right")
        return
    point_values = f(points)
    int_val = sum(point_values*dx)
    if verbose:
        xbin = np.vstack([x[:-1], x[1:]]).flatten('F')
        ybin = np.vstack([point_values, point_values]).flatten('F')
        plot(f, xbin, ybin, int_val, N, text, figname)
    return int_val

print('Approximated sum: {0}'.format(riemann(myfun, N, a, b, direction="left", 
                                    verbose=True, figname="riemann_left_%d.png"%N)))
print('Approximated sum: {0}'.format(riemann(myfun, N, a, b, direction="right", 
                                    verbose=True, figname="riemann_right_%d.png"%N)))


Approximated sum: 2.123191605036145
Approximated sum: 2.5932720824936655

Métodos de Newton-Cotes

Los siguientes dos métodos que se presentan a continuación, conforman parte de una familia de métodos llamados de Newton-Cotes. Estos generan una malla equiespaciada para particionar el dominio de integración $[a,b]$.

Todos estos métodos se basan en aproximar la función $f$ por un polinomio de grado $n-1: \ p_{n-1}$, que interpole $n$ puntos de tal malla, y de este modo computar el área bajo la curva de este polinomio (En vez de $f$), lo cual es fácil dado que las integrales de polinomios son también polinomios (de un grado superior).

Las derivaciones de las fórmulas a utilizar pueden ser consultadas en el texto guía: Numerical Analysis, Timothy Sauer.

Regla del Trapecio

El primero y más simple de tales métodos, es utilizar polinomios de grado $1$ que interpolen cada $(x_k,f(x_k))$ y $(x_{k+1},f(x_{k+1}))$. Es fácil notar que como resultado, el área aproximada entre cada dos puntos de la malla, será el área de un trapecio, motivo del nombre de tal regla.

Al particionar el intervalo $[a,b]$ en $m$ segmentos y $m+1$ puntos $a = x_0 < \cdots < x_{m} = b \ \ $, se obtiene el siguiente resultado

\begin{align*} \int_{x_0}^{x_m} f(x) dx = \sum_{i=1}^{m} \int_{x_{i-1}}^{x_{i}} f(x) dx = \frac{h}{2}\left[f(a) + f(b) + 2\sum_{i=1}^{m-1} f(x_i) \right] - \underbrace{(b-a) \frac{h^2}{12} f''(c)}_{\text{Error term}} \end{align*}

donde $h=(b-a)/m \ $ es el largo de cada subintervalo, y $\ c \in [a, b]$. Dado que $c$ no es conocido, el Error term en la práctica no se toma en cuenta, y por lo mismo constituye el error del método.


In [5]:
def trapezoid(myfun, N, a, b, verbose=False, text='', figname=''):
    f = np.vectorize(myfun) # So we can apply it to arrays without trouble
    x = np.linspace(a, b, N+1) # We want N bins, so N+1 points  
    h = x[1]-x[0]
    xleft = x[:-1]
    xright = x[1:]
    int_val = 0.5*h*sum(f(xleft)+f(xright))
    if verbose:
        xbin = x
        ybin = f(x) 
        plot(f, xbin, ybin, int_val, N, text, figname)
    return int_val

N = 40
#myfun = lambda x : x**2
print('Approximated sum: {0}'.format(trapezoid(myfun, N, a, b, verbose=True, text=text, figname="trapezoid_%d.png"%N)))


Approximated sum: 2.350892034050039

Regla de Simpson

La extensión lógica a la regla anterior, es utilizar polinomio de grado $2$ (parábolas) para aproximar la función $f$. Para ello, cada tres puntos $(x_k,f(x_k))$, $(x_{k+1},f(x_{k+1}))$ y $(x_{k+2},f(x_{k+2}))$ una parábola es utilizada para aproximar la función.

Al particionar el intervalo $[a,b]$ en $m$ segmentos y $m+1$ puntos $a = x_0 < \cdots < x_{m} = b \ \ $, con $\ m\ $ par, se obtiene el siguiente resultado:

\begin{align*} \int_{a}^{b} f(x) dx = \frac{h}{3} \left( f(x_0) + \sum_{i=1}^{N} 4 f(x_{2i-1}) + \sum_{i=1}^{N-1} 2 f(x_{2i}) + f(x_N) \right) - \underbrace{(b-a)\frac{h^4}{90} f^{(4)}(c)}_{\text{Error term}} \end{align*}

donde $h=(x_{i+1}-x_i) \ \ $ y $c \in [a, b]$


In [6]:
def simpsons(myfun, N, a, b, verbose=False, text="", figname=""):
    f = np.vectorize(myfun) # So we can apply it to arrays without trouble
    x = np.linspace(a, b, N+1) # We want N bins, so N+1 points
    if N%2==1:
        if verbose: print("Simpsons rule only applicable to even number of segments")
        return np.nan
    dx = x[1]-x[0]
    xleft   = x[:-2:2]
    xmiddle = x[1::2]
    xright  = x[2::2]
    int_val = sum((f(xleft)+4*f(xmiddle)+f(xright))*dx/3)
    if verbose:
        xbin, ybin = simpsons_bins(f, xleft, xmiddle, xright)
        plot(f, xbin, ybin, int_val, N, text, figname)
    return int_val

def simpsons_bins(f, xleft, xmiddle, xright):
    xbin, ybin = [], []
    n = 21
    for x0, x1, x2 in zip(xleft, xmiddle, xright):
        x = np.linspace(x0, x2, n)
        y = (f(x0)*(x-x1)*(x-x2)) / ((x0-x1)*(x0-x2))
        y+= (f(x1)*(x-x0)*(x-x2)) / ((x1-x0)*(x1-x2))
        y+= (f(x2)*(x-x0)*(x-x1)) / ((x2-x0)*(x2-x1))
        xbin.extend(list(x))
        ybin.extend(list(y))
    return np.array(xbin), np.array(ybin)
N=4
print('Approximated sum: {0}'.format(simpsons(myfun, N, a, b, verbose=True, text=text, figname="simpsons_%d.png"%N)))


Approximated sum: 2.3511948318802554

Midpoint

Una de las limitaciones de las dos reglas anteriores, es que requieren evaluar $f$ en los extremos del intervalo, y pueden haber casos en donde $f$ no esté bien definida en tales puntos.

La regla del punto medio midpoint para cada dos puntos $(x_k,f(x_k))$ y $(x_{k+1},f(x_{k+1}))$, aproxima la función por una expansión de Taylor de grado $1$, centrada en el punto medio $\displaystyle \left( \frac{x_k+x_{k+1}}{2}, f\left(\frac{x_k+x_{k+1}}{2} \right) \right)$, es decir, aproxima $f$ por un polinomio de grado $1$ (al igual que la regla del trapecio).

Al particionar el intervalo $[a,b]$ en $m$ segmentos y $m+1$ puntos $a = x_0 < \cdots < x_{m} = b \ \ $, se obtiene el siguiente resultado:

\begin{align*} \int_{a}^{b} f(x) dx = \sum_{i=1}^{m} \int_{x_{i-1}}^{x_{i}} f(x) dx \\ = \sum_{i=1}^{m} h f(w_i) + \underbrace{\frac{(b-a)}{24} h^2 f''(c)}_{\text{Error term}} \end{align*}

donde $h=(b-a)/m \ \ \ $, $w_i=\frac{1}{2}(x_{i-1}+x_{i})\ \ \ $ y $\ \ c \in [a, b]$


In [7]:
def midpoint(myfun, N, a, b, verbose=False, text='', figname=''):
    f = np.vectorize(myfun) # So we can apply it to arrays without trouble
    x = np.linspace(a, b, N+1) # We want N bins, so N+1 points  
    dx = x[1]-x[0]
    midpoints = x[:-1] + .5*dx
    midpoint_values = f(midpoints)
    int_val = sum(midpoint_values*dx)
    if verbose:
        xbin = np.vstack([x[:-1], x[1:]]).flatten('F')
        ybin = np.vstack([midpoint_values, midpoint_values]).flatten('F')
        plot(f, xbin, ybin, int_val, N, text, figname)
    return int_val

N = 20
#myfun = lambda x : x + 1
print('Approximated sum: {0}'.format(midpoint(myfun, N, a, b, verbose=True, text=text, figname="midpoint_%d.png"%N)))


Approximated sum: 2.3494233385234686

Cuadratura Gaussiana

Similar a las ideas que propuso Chebyshev para mejorar los método de interpolación, nace la siguiente pregunta:

  • ¿Se podrá mejorar el proceso de integración particionando $[a,b]$ de una forma no equiespaciada?

La respuesta a esta pregunta es . De modo análogo a los puntos de Chebyshev, aca podemos seleccionar las raíces de los polinomios de Legendre $p_n(x)$ siguientes: \begin{align*} p_n(x) = \frac{1}{2^n n!} \frac{d^n}{dx^n} \left[ (x^2 - 1)^n \right] \end{align*} para luego interpolar $f$ sobre estos puntos, generando un polinomio $p_{n-1}$ sobre el cual realizar la integración.

Este método utiliza la siguiente aproximación, para un intervalo $[-1,1]$: \begin{align*} \int_{a}^{b} f(x) dx \approx \sum_{i=1}^n w_i f(x_i) \end{align*} donde los $x_i$ se definen como las raíces del n-ésimo polinomio de Legendre $p_n(x)$, y los $w_i$ se calculan como: $$ w_i = \int_{-1}^{1}L_{i}(x)dx, \ \ \ i = 1,\dots,n $$ siendo $L_i(x)$ los conocidos polinomio de la interpolación de Lagrange.

Nota: Para un intervalo arbitrario $[a,b]$ es necesario realizar la transformación lineal correspondiente.


In [8]:
def gaussianquad(myfun, N, a, b, verbose=False, text="", figname=""):
    f = np.vectorize(myfun) # So we can apply it to arrays without trouble
    x, w = gaussian_nodes_and_weights(N, a, b)
    int_val = sum( w * f(x) )
    if verbose:
        xbin, ybin = gaussian_bins(f, x, w)
        plot(f, xbin, ybin, int_val, N, text, figname)
    return int_val

# Comment: These nodes could be precomputed in advance, here
# they are computed as needed so it will penalize the 
# computation time.
def gaussian_nodes_and_weights(N, a, b):
    if N==1: 
        return np.array([1]), np.array([2])
    beta = .5 / np.sqrt(1.-(2.*np.arange(1.,N))**(-2))
    T = np.diag(beta,1) + np.diag(beta,-1)
    D, V = np.linalg.eigh(T)
    x = D
    x = .5 * ( (b-a)*x + b + a) # Rescaling
    w = 2*V[0,:]**2
    w = .5*(b-a)*w
    return x, w

def gaussian_bins(f, x, w):
    z = [a] + list(a + w.cumsum())
    xbin = np.vstack([z[:-1], z[1:]]).flatten('F')
    z = f(x)
    ybin = np.vstack([z[:], z[:]]).flatten('F')
    return np.array(xbin), np.array(ybin)

print('Approximated sum: {0}'.format(gaussianquad(myfun, N, a, b, verbose=True, 
                                    text=text, figname="gaussianquad_%d.png"%N)))


Approximated sum: 2.3504023872876028

In [9]:
gaussian_nodes_and_weights(2, -1, 1)


Out[9]:
(array([-0.57735027,  0.57735027]), array([1., 1.]))

Preguntas

  • El método de Midpoint y el del Trapecio se basan ambos en aproximaciones por polinomio de grado $1$. ¿Cuál considera usted mejor?. Hint: Considere la cantidad de evaluaciones de funciones, y el termino de error.
  • ¿Cuál es la importancia del término $h$ en los Error term de cada método?
  • ¿Cuál es la importancia del orden de la derivada $f^{(k)}(c)$ en los Error term de cada método?

Análisis de Convergencia

Para entender bien el concepto de convergencia, es fundamental entender correctamente dos conceptos:

  • Grado de Precisión (Degree of Precision). Para un método de integración numérica, corresponde al mayor entero $k$ para el cual todos los polinomios de grado $k$ o menor, son integrados exactamente (sin error). Si recordamos los métodos ya estudiados, el término de error tiene la forma general: $$ \text{Error term} = C \ h^p \ f^{(q)}(c) \ \ \ \ \text{con} \ \ C \ \text{constante} \ \ \text{y} \ \ p,q \in \mathbb{N} $$ Dado que para cualquier polinomio de grado $\leq q-1$ se obtiene que $f^{(q)}=0$, entonces el grado de precisión de un tal método, viene determinado por el orden de la derivada, y es $q-1$.
  • Orden de convergencia. El orden de convergencia está derminada por la otra componente del Error term, esto es, por $h^p$. Recordar que $h$ debe ser pequeño ($h < 1$), y por lo tanto a mayor potencia $p$, menor será el orden del método. El orden de convergencia, nos dice cómo decrece el error a medida que se aumentan la cantidad de puntos de la malla.

A continuación compararemos gráficamente la convergencia de estos métodos:


In [10]:
def get_error(quadrature_rule, myfun, Nrange, a, b, true_value):
    quad_error = []
    for N in Nrange:
        error = np.abs(true_value - quadrature_rule(myfun, N, a, b) )
        if error<1E-16:
            quad_error.append(1E-16)
        else:
            quad_error.append(error)
    return quad_error

def set_ylim(ymin, ymax):
    ymin = min(plt.ylim()[0], ymin)
    ymax = max(plt.ylim()[1], ymax)
    plt.ylim([ymin, ymax])
    return

def convergence(exp_data):
    Nrange     = exp_data[0]
    myfun      = exp_data[1]
    a          = exp_data[2]
    b          = exp_data[3]
    true_value = exp_data[4]
    #######################################################
    print('Printing numerical experiment details:')
    print('Nrange: ', Nrange)
    print(inspect.getsource(myfun).replace('\n', ''))
    print('a: ', a)
    print('b: ', b)
    print('true_value: ', true_value)
    #######################################################
    ms = 10
    f = np.vectorize(myfun) # So we can apply it to arrays without trouble
    e_mp = get_error(midpoint, myfun, Nrange, a, b, true_value)
    e_tr = get_error(trapezoid, myfun, Nrange, a, b, true_value)
    e_sp = get_error(simpsons, myfun, Nrange, a, b, true_value)
    e_gq = get_error(gaussianquad, myfun, Nrange, a, b, true_value)
    #plt.figure(figsize=(12,16))
    fig = plt.figure(figsize=(24,24))
    plt.rcParams.update({'font.size': 22})
    
    # First plot
    ax = plt.subplot(2,2,1)
    dd = 0.1*(b-a)
    x = np.linspace(a-dd, b+dd, 1000)
    plt.plot(x, f(x), 'k', label="f(x)", lw=2.0)
    x = np.linspace(a, b, 1000)
    plt.fill_between(x, f(x), 0, alpha=0.5, label=r"$\int_a^b f(x) dx$")
    plt.xlabel("x")
    plt.ylabel("f(x)")
    ymax = 1.05*plt.ylim()[1]
    plt.ylim([-ymax, ymax])
    plt.grid('on')
    plt.legend(loc="lower left")
    
    # Second plot
    ax = plt.subplot(2,2,2) 
    plt.plot(Nrange, e_mp, 'sb', lw=2.0, ms=ms, label="Midpoint")
    plt.plot(Nrange, e_tr, 'or', lw=2.0, ms=ms, label="Trapezoid")
    plt.plot(Nrange, e_sp, '>y', lw=2.0, ms=ms, label="Simpsons")
    plt.plot(Nrange, e_gq, 'Dg', lw=2.0, ms=ms, label="Gaussian Quad")
    set_ylim(-5E-2, 1E-1)
    plt.xlabel("N")
    plt.ylabel("Absolute Error")
    ax.legend(loc='best', bbox_to_anchor=(0.5, 1.00), ncol=1, fancybox=True, shadow=True, numpoints=1)
    plt.grid('on')
    
    # Third plot
    ax = plt.subplot(2,2,3)
    plt.loglog(Nrange, e_mp, 'sb', ms=ms, lw=2.0, label="Midpoint")
    plt.loglog(Nrange, e_tr, 'or', ms=ms, lw=2.0, label="Trapezoid")
    plt.loglog(Nrange, e_sp, '>y', ms=ms, lw=2.0, label="Simpsons")
    plt.loglog(Nrange, e_gq, 'Dg', ms=ms, lw=2.0, label="Gaussian Quad")
    ax.legend(loc='lower left', ncol=1, fancybox=True, shadow=True, numpoints=1)
    plt.ylim([1E-18, 1E+1])
    N = np.arange(1,101,10)
    #plt.loglog(N, 1./N, '-k', lw=2.0, alpha=0.5)
    plt.loglog(N, 1./N**2, '-k', lw=2.0, alpha=0.5)
    #plt.loglog(N, 1./N**3, '-k', lw=2.0, alpha=0.5)
    plt.loglog(N, 1./N**4, '-k', lw=2.0, alpha=0.5)
    plt.xlabel("N")
    plt.ylabel("Absolute Error")
    plt.xlim([0.9*min(Nrange),1.1*max(Nrange)])
    plt.grid('on')
    #print(Nrange)
    #print(np.log10(e_mp))
    #print(np.log10(e_tr))
    #print(np.log10(e_sp))
    #print(np.log10(e_gq))
    
    # Forth plot
    ax = plt.subplot(2,2,4)
    h = 1./np.arange(1,101,10)
    #plt.loglog(h, h, '-k', lw=2.0, alpha=0.5)
    plt.loglog(h, h**2, '-k', lw=2.0, alpha=0.5)
    #plt.loglog(h, h**3, '-k', lw=2.0, alpha=0.5)
    plt.loglog(h, h**4, '-k', lw=2.0, alpha=0.5)
    # Plotting Gaussian Quadratupre first but using larger markers
    h = 1./np.array(Nrange)
    plt.loglog(h, e_mp, 'sb', lw=2.0, ms=ms, label="Midpoint")
    plt.loglog(h, e_tr, 'or', lw=2.0, ms=ms, label="Trapezoid")
    plt.loglog(h, e_sp, '>y', lw=2.0, ms=ms, label="Simpsons")
    plt.loglog(h, e_gq, 'Dg', lw=2.0, ms=ms, label="Gaussian Quad")
    ax.legend(loc='lower right', ncol=1, fancybox=True, shadow=True, numpoints=1)
    plt.ylim([1E-18, 1E+1])
    plt.xlabel("h")
    plt.ylabel("Absolute Error")
    plt.grid('on')
    plt.xlim([0.9*min(h),1.1*max(h)])
    plt.show()
    #fig.savefig('myfig.eps', format='eps')

Parámetros

A continuación, se defininen los parámetros a utilizar en nuestros experimentos.


In [11]:
list_experiments=[]
###########################
# Function 1: Constant
# All methods are good
Ns = range(4, 11)
x0, x1, x2 = 5., -1., 3.
f = lambda x : 1
a = -1.0
b = +1.0
sol = 2
exp1 = (Ns, f, a, b, sol)
list_experiments.append(('exp1: '+inspect.getsource(f).replace('\n', ''),exp1))

###########################
# Function 2: sin
# All method equal if symmetric interval
# Gaussian quad better if asymmetric interval
Ns = range(4, 100)
f = lambda x : np.sin(x)
a = 0.0
b = +1.0
sol = -np.cos(b)+np.cos(a)
exp2 = (Ns, f, a, b, sol)
list_experiments.append(('exp2: '+inspect.getsource(f).replace('\n', ''),exp2))

###########################
# Function 3: gaussian bell
# gaussian quad outperforms all the other methods
Ns = range(4, 20)
f = lambda x : np.exp(-x**2)
a = -1.0
b = +1.0
sol = 1.4936482656248541 # Specific value for the range [-1,1]
exp3 = (Ns, f, a, b, sol)
list_experiments.append(('exp3: '+inspect.getsource(f).replace('\n', ''),exp3))

###########################
# Function 4: exponential
# gaussian quad outperforms all the other methods
Ns = range(4, 11) #20
f = lambda x : np.exp(x)
a = -1.0
b = +2.0
sol = np.exp(b) - np.exp(a)
exp4 = (Ns, f, a, b, sol)
list_experiments.append(('exp4: '+inspect.getsource(f).replace('\n', ''),exp4))

###########################
# Function 5: logarithm
Ns = range(4, 20)
f = lambda x : np.log(np.abs(x))
a = 1e-10
b = +1.0
sol = (b*np.log(b)-b) - (a*np.log(a)-a)#-2.0000000000000000
exp5 = (Ns, f, a, b, sol)
list_experiments.append(('exp5: '+inspect.getsource(f).replace('\n', ''),exp5))

###########################
# Function 6 and true value
Ns = range(4, 20)
f = lambda x : np.sin(x)/x #if abs(x)>1e-6 else 1.0
a = -1.0
b = +1.0
sol = 1.8921661407343660
exp6 = (Ns, f, a, b, sol)
list_experiments.append(('exp6: '+inspect.getsource(f).replace('\n', ''),exp6))

###########################
# Function 7 : absolute value
# Midpoint wins
# Do (-1,1) and (-2,1)
Ns = range(4, 20)
f = lambda x : abs(x)
a = -1 #-np.pi
b = +1.0
sol = (a**2+b**2)/2.
exp7 = (Ns, f, a, b, sol)
list_experiments.append(('exp7: '+inspect.getsource(f).replace('\n', ''),exp7))

###########################
# Function 8 : Gaussian
# Midpoint/Traps wins over gaussian
Ns = range(4, 100)
f = lambda x : np.exp(-x**2)
a = -10.0
b = +10.0
sol = np.sqrt(math.pi)
exp8 = (Ns, f, a, b, sol)
list_experiments.append(('exp8: '+inspect.getsource(f).replace('\n', ''),exp8))

###########################
# Function 9 : 1/x^2
# Gaussian wins, but they all degrade if a->0
# Here we should try an adaptative method
Ns = range(4, 20)
f = lambda x : 1.0/(x**2)
a = 1e-4
b = +1.
sol = 1.0/a - 1.0/b
exp9 = (Ns, f, a, b, sol)
list_experiments.append(('exp9: '+inspect.getsource(f).replace('\n', ''),exp9))

###########################
# Function 10 : 1/x^0.5
Ns = range(4, 20)
f = lambda x : 1.0/(np.sqrt(np.abs(x)))
a = 0.0
b = 1.0
sol = 2.0
exp10 = (Ns, f, a, b, sol)
list_experiments.append(('exp10: '+inspect.getsource(f).replace('\n', ''),exp10))

Test de Convergencia

Finalmente podemos testear la convergencia de nuestros métodos


In [12]:
interact(convergence, exp_data=list_experiments)


Out[12]:
<function __main__.convergence(exp_data)>

Análisis de Tiempo

Usamos el siguiente decorador para medir los tiempos de ejecución de las funciones


In [13]:
def timeit(f):
    def timed(*args, **kw):
        N = 50
        T = 0.
        for i in range(N):
            ts = time.time()
            result = f(*args, **kw)
            te = time.time()
            T += (te-ts)*1000. # In ms  
        return T/N
    return timed

In [14]:
"""
Decorated functions
"""
@timeit
def t_trapezoid(myfun, N, a, b):
    return trapezoid(myfunc, N, a, b)

@timeit
def t_simpsons(myfun, N, a, b):
    return simpsons(myfunc, N, a, b)

@timeit
def t_midpoint(myfun, N, a, b):
    return midpoint(myfun, N, a, b)

@timeit
def t_gaussianquad(myfun, N, a, b):
    return gaussianquad(myfunc, N, a, b)

In [15]:
def timing(exp_data):
    Nrange     = exp_data[0]
    myfun      = exp_data[1]
    a          = exp_data[2]
    b          = exp_data[3]
    true_value = 0.0
    #######################################################
    print('Printing numerical experiment details:')
    print('Nrange: ', Nrange)
    print(inspect.getsource(myfun).replace('\n', ''))
    print('a: ', a)
    print('b: ', b)
    print('true_value (for time!): ', true_value)
    #######################################################
    ms = 10
    f = np.vectorize(myfun) # So we can apply it to arrays without trouble
    e_mp = get_error(midpoint, myfun, Nrange, a, b, true_value)
    e_tr = get_error(trapezoid, myfun, Nrange, a, b, true_value)
    e_sp = get_error(simpsons, myfun, Nrange, a, b, true_value)
    e_gq = get_error(gaussianquad, myfun, Nrange, a, b, true_value)
    plt.figure(figsize=(12,16))
    # First plot
    ax = plt.subplot(4,1,1)
    dd = 0.1*(b-a)
    x = np.linspace(a-dd, b+dd, 1000)
    plt.plot(x, f(x), 'k', label="f(x)", lw=2.0)
    x = np.linspace(a, b, 1000)
    plt.fill_between(x, myfun(x), 0, alpha=0.5, label=r"$\int_a^b f(x) dx$")
    plt.xlabel("x")
    plt.ylabel("f(x)")
    ymax = 1.05*plt.ylim()[1]
    plt.ylim([-ymax, ymax])
    plt.grid('on')
    plt.legend(loc="lower left")
    # Second plot
    ax = plt.subplot(4,1,2) 
    plt.plot(Nrange, e_mp, 'sb', lw=2.0, ms=ms, label="Midpoint")
    plt.plot(Nrange, e_tr, 'or', lw=2.0, ms=ms, label="Trapezoid")
    plt.plot(Nrange, e_gq, 'Dg', lw=2.0, ms=ms, label="Gaussian Quad")
    plt.plot(Nrange, e_sp, '>y', lw=2.0, ms=ms, label="Simpsons")
    set_ylim(-5E-2, 1E-1)
    plt.xlabel("N")
    plt.ylabel("Tiempo [ms]")
    ax.legend(loc='upper center', bbox_to_anchor=(0.5, 1.00),
          ncol=4, fancybox=True, shadow=True, numpoints=1)
    plt.grid('on')
    # Third plot
    plt.subplot(4,1,3)
    plt.loglog(Nrange, e_mp, 'sb', ms=ms, lw=2.0)
    plt.loglog(Nrange, e_tr, 'or', ms=ms, lw=2.0)
    plt.loglog(Nrange, e_sp, '>y', ms=ms, lw=2.0)
    plt.loglog(Nrange, e_gq, 'Dg', ms=ms, lw=2.0)
    #set_ylim(1E-18, 1E+1)
    plt.xlabel("N")
    plt.ylabel("Tiempo [ms]")
    plt.grid('on')
    # Forth plot
    plt.subplot(4,1,4)
    # Plotting Gaussian Quadratupre first but using larger markers
    h = 1./np.array(Nrange)
    plt.loglog(h, e_gq, 'Dg', lw=2.0, ms=ms)
    plt.loglog(h, e_mp, 'sb', lw=2.0, ms=ms)
    plt.loglog(h, e_tr, 'or', lw=2.0, ms=ms)
    plt.loglog(h, e_sp, '>y', lw=2.0, ms=ms)
    #set_ylim(1E-18, 1E+1)
    plt.xlabel("h")
    plt.ylabel("Tiempo [ms]")
    plt.grid('on')
    plt.show()

Test de tiempos

Finalmente podemos testear los tiempos de nuestros métodos


In [16]:
interact(timing, exp_data=list_experiments);


Acknowledgements

  • Material creado por profesor Claudio Torres (ctorres@inf.utfsm.cl) y ayudantes: Alvaro Salinas y Martín Villanueva. DI UTFSM. Abril 2016.

DISCLAIMER

El presente notebook ha sido creado para el curso ILI286 - Computación Científica 2, del Departamento de Informática, Universidad Técnica Federico Santa María.

El material ha sido creado por Claudio Torres ctorres@inf.utfsm.cl y Sebastian Flores sebastian.flores@usm.cl, y es distribuido sin restricciones. En caso de encontrar un error, por favor no dude en contactarnos.

[Update 2016] (Martín) Notebook creado como una fusión de las tres partes anteriores. Modificadas funciones de integración con parámetro verbose para controlar la salida, y añadido el decorador para medir los tiempos.

[Update 2017] (Cristopher) Corrección de typos, correccion de método del trapecio.

[Update 2017] (C. Torres) Adding interact.

[Update 2019] (C. Torres) changing .flatten(1) to .flatten('F'). Fixing issue with titles of sections.



In [ ]: