 INF285 - Computación Científica

 Newton's divided differences, Sinc interpolation and piecewise-linear interpolations.

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

 Version: 1.01

## Table of Contents
* [Introduction](#intro)
* [Newton's divided differences](#NewtonDD)
* [Sinc interpolator](#SincInterpolator)
* [Piecewise linear interpolator](#PiecewiseLinearInterpolator)
* [Convergence analysis](#convergence)
* [Analysis of time vs error for interpolation algorithms](#AnalysisTimeVsError)
* [Acknowledgements](#acknowledgements)

```python
import numpy as np
import matplotlib.pyplot as plt
import sympy as sym
from functools import reduce
import matplotlib as mpl
mpl.rcParams['font.size'] = 14
mpl.rcParams['axes.labelsize'] = 20
mpl.rcParams['xtick.labelsize'] = 14
mpl.rcParams['ytick.labelsize'] = 14
%matplotlib inline
from ipywidgets import interact, fixed, IntSlider, interact_manual, Checkbox, RadioButtons
sym.init_printing()
import time
```

##  Introduction
[Back to TOC](#toc)

In this Bonus jupyter notebook we add the Newton divided differences algorithm, Sinc interpolation and piecewise linear interpolation for comparison purposes.

```python
# Function that evaluates the xi's points in the polynomial
def EvaluateInterpolant(D, xx):
    if D['Method']=='Vandermonde':
        p = lambda x: np.dot(D['PrecomputedData'],np.power(x,np.arange(D['n'])))
    elif D['Method']=='Lagrange':
        p = lambda x: np.dot(D['PrecomputedData'],[np.prod(x - np.delete(D['x'],j)) for j in range(D['n'])])
    elif D['Method']=='Barycentric':
        wi = D['PrecomputedData']
        xi = D['x']
        yi = D['y']
        p = lambda x: np.sum(yi*wi/(x-xi))/np.sum(wi/(x-xi)) if len(xi[xi == x])==0 else np.ndarray.item(yi[xi == x])
    elif D['Method']=='Newton':
        p = lambda x: np.dot(D['PrecomputedData'],np.append([1],[np.prod(x-D['x'][:j]) for j in range(1,D['n'])]))
    elif D['Method']=='SincInterpolator':
        p = lambda x: np.dot([np.sinc((x-xi)/D['h']) for xi in D['x']],D['y'])
    elif D['Method']=='PiecewiseLinear':
        m = D['m']
        n = D['n']
        xi = D['x']
        yi = D['y']
        def p(x):
            for i in range(n-1):
                if xi[i]<= x and x<xi[i+1]:
                    return yi[i]+m[i]*(x-xi[i])
            # This is the last case, if x==x[-1]
            return yi[-1]
    return np.array([p(x) for x in xx])

# Function that shows the data points and the function that interpolates them.
def PlotInterpolant(D,ylim=None):
    xi = np.linspace(min(D['x']),max(D['x']),1000)
    yi = EvaluateInterpolant(D,xi)
    fig = plt.figure(figsize=(8,8))
    plt.plot(D['x'],D['y'],'ro',label='Interpolation points')
    plt.plot(xi,yi,'b-',label='$p(x)$')
    plt.xlim(min(xi)-0.5, max(xi)+0.5)
    if ylim:
        plt.ylim(ylim[0], ylim[1])
    else:
        plt.ylim(min(yi)-0.5, max(yi)+0.5)
    plt.grid(True)
    plt.legend(loc='best')
    plt.xlabel('$x$')
    plt.show()
```

## We repeat the already presented algorithms.
[Back to TOC](#toc)

```python
def Vandermonde(x, y, show=False):
    # We construct the matrix and solve the system of linear equations
    # A = np.array([xi**np.arange(len(x)) for xi in x]) # OLDER VERSION
    A = np.vander(x, increasing=True)
    b = y
    coefficients = np.linalg.solve(A,b)
    n = len(x)

    # The function shows the data if the flag is true
    if show:
        print('Data Points: '); print([(x[i],y[i]) for i in range(n)])
        print('A = '); print(np.array_str(A, precision=2, suppress_small=True))
        print("cond(A) = "+str(np.linalg.cond(A)))
        print('b = '); print(np.array_str(b, precision=2, suppress_small=True))
        print('x = '); print(np.array_str(coefficients, precision=2, suppress_small=True))
        xS = sym.Symbol('x')
        F = np.dot(xS**np.arange(len(x)),coefficients)
        print('Interpolation Function: ')
        print('F(x) = ')
        print(F)

    # Finally, we return a data structure with our interpolating polynomial
    D = {'Method': 'Vandermonde',
         'PrecomputedData': coefficients,
         'x': x,
         'y': y,
         'n': n}
    return D
```

```python
def Lagrange(x, y, show=False):
    # We calculate the li's
    n = len(x)
    p = np.array([y[i]/np.prod(x[i] - np.delete(x,i)) for i in range(n)])

    # The function shows the data if the flag is true
    if show:
        print('Data Points: '); print([(x[i],y[i]) for i in range(n)])
        xS = sym.Symbol('x')
        L = np.dot(np.array([np.prod(xS - np.delete(x,i))/np.prod(x[i] - np.delete(x,i)) for i in range(n)]),y)
        print('Interpolation Function: ');
        print(L)

    # Finally, we return a data structure with our interpolating polynomial
    D = {'Method': 'Lagrange',
         'PrecomputedData': p,
         'x': x,
         'y': y,
         'n': n}
    return D
```

```python
def Barycentric(x, y, show=False):
    W  = np.subtract.outer(x, x)
    wi  = 1/np.prod(W, axis=1, where = W!=0)
    n = len(x)

    # The function shows the data if the flag is true
    if show:
        print('Data Points: '); print([(x[i],y[i]) for i in range(n)])
        xS = sym.Symbol('x')
        N = np.sum(y*wi/(xS-x))/np.sum(wi/(xS-x))
        print('Interpolation Function: ');
        print(N)

    # Finally, we return a data structure with our interpolating polynomial
    D = {'Method':'Barycentric',
         'PrecomputedData': wi,
         'x': x,
         'y': y,
         'n': n}
    return D
```

## Newton's Divided Difference
[Back to TOC](#toc)

In this interpolation method we will use divided differences to calculate the coefficients of our interpolation polynomial. Given a set of $n$ data points $(x_1,y_1),...,(x_n,y_n)$, the Newton polynomial is:
$$ p(x) = \sum^n_{i=1} (f[x_1 ... x_i] \cdot \prod^{i-1}_{j=1} (x-x_j)) ,$$
where $ \prod^{0}_{j=1} (x-x_j) = 0 $, and:
$$ f[x_i] = y_i $$
$$ f[x_j...x_i] = \frac{f[x_{j+1}...x_i]-f[x_j...x_{i-1}]}{x_i-x_j}$$

```python
def Divided_Differences(x, y):
    dd = np.array([y])
    n = len(x)
    for i in range(n-1):
        ddi = []
        for a in range(n-i-1):
            ddi.append((dd[i][a+1]-dd[i][a])/(x[a+i+1]-x[a]))
        ddi = np.append(ddi,np.full((n-len(ddi),),0.0))
        dd = np.append(dd,[ddi],axis=0)
    return np.array(dd)

def NewtonDD(x, y, show=False):
    # We calculate the divided differences and store them in a data structure
    dd = Divided_Differences(x,y)
    n = len(x)

    # The function shows the data if the flag is true
    if show:
        print('Data Points: '); print([(x[i],y[i]) for i in range(n)])
        xS = sym.Symbol('x')
        N = np.dot(dd[:,0],np.append([1],[np.prod(xS-x[:i]) for i in range(1,n)]))
        print('Interpolation Function: ');
        print(N)

    # Finally, we return a data structure with our interpolating polynomial
    D = {'Method':'Newton',
         'PrecomputedData': dd[:,0],
         'x': x,
         'y': y,
         'n': n}
    return D
```

```python
x = np.linspace(0,1,7)
y = np.sin(7*x)
D = NewtonDD(x, y, show=True)
PlotInterpolant(D,ylim=None)
```

Questions about Newton's DD:
- What is the main problem using this method (and Lagrange)? How can you fix it? **A: A problem with polynomial interpolation with equispaced date is the Runge phenomenon and can be handle with Chebyshev points**
- What to do when a new point is added? **A: Pro, is not necessary re-calculate the whole polynomial only a small piece**

## Sinc interpolator
[Back to TOC](#toc)

The Sinc interpolator is not a polynomial interpolator but it is an interesting one to know.
It is useful for equalspaced data $x_i$ and if has the following form:
$$
g(x)=\sum_{i=1}^n y_i\,\text{sinc}\left(\frac{x-x_i}{h}\right),
$$
where $h=x_{i+1}-x_{i}$ and $\text{sinc}(x)=\dfrac{\sin(\pi\,x)}{\pi\,x}$.

```python
def SincInterpolator(x, y, show=False):
    # We first check the data in equalspaced
    dx = np.diff(x)
    if np.abs(np.max(dx)-np.min(dx))>1e-10:
        raise ValueError("SincInterpolator: Data not equalspaced")

    # Computing h = x[i+1]-x[i]
    h = dx[0]
    n = len(x)

    # The function shows the data if the flag is true
    if show:
        print('Data Points: ');
        print([(x[i],y[i]) for i in range(n)])
        xS = sym.Symbol('x')
        SI = np.dot([sym.sinc((xS-xi)/h) for xi in x],y)
        print('Interpolation Function: ');
        print(SI)

    # Finally, we return a data structure with our interpolating polynomial
    D = {'Method': 'SincInterpolator',
         'h': h,
         'x': x,
         'y': y}
    return D
```

```python
D = SincInterpolator(x, y, show=True)
PlotInterpolant(D,ylim=None)
```

## Piecewise linear interpolator
[Back to TOC](#toc)

This interpolator interpolates the data between datapoints $(x_i,y_i)$ and $(x_{i+1},y_{i+1})$ with a linear function of the form $y_i+\frac{y_{i+1}-y_i}{x_{i+1}-x_i}(x-x_i)$.
The general form is the following:
$$
    g(x)=
        \begin{cases}
            y_1+\frac{y_2-y_1}{x_2-x_1}(x-x_1), \quad x \in [x_1,x_2]\\
            y_2+\frac{y_3-y_2}{x_3-x_2}(x-x_2), \quad x \in ]x_2,x_3]\\
            \quad \vdots\\
            y_i+\frac{y_{i+1}-y_i}{x_{i+1}-x_i}(x-x_i), \quad x \in ]x_i,x_{i+1}]\\
            \quad \vdots\\
            y_{n-1}+\frac{y_n-y_{n-1}}{x_n-x_{n-1}}(x-x_{n-1}), \quad x \in ]x_{n-1}, x_n]
        \end{cases}.
$$
In this case we don't requiere that the date is equalspaced.

```python
def PiecewiseLinear(x, y, show=False):
    # Precomputing slopes for linear interpolation
    ii = np.argsort(x)
    x = x[ii]
    y = y[ii]
    n = len(x)
    m = np.diff(y)/np.diff(x)

    # The function shows the data if the flag is true
    if show:
        print('Data Points: ');
        print([(x[i],y[i]) for i in range(n)])
        xS = sym.Symbol('x')
        PL = []
        for i in np.arange(len(m)):
            PL.append([[x[i],x[i+1]], y[i]+ m[i]*(xS-x[i])])
        print('Interpolation Functions: ');
        print(PL)

    # Finally, we return a data structure with our interpolating polynomial
    D = {'Method': 'PiecewiseLinear',
         'm': m,
         'n': n,
         'x': x,
         'y': y}
    return D
```

```python
D = PiecewiseLinear(x, y, show=True)
PlotInterpolant(D,ylim=None)
```

```python
def show_time_new_algorithms(i=0, N=7, method='NewtonDD'):
    x = np.arange(N+1)
    y = np.zeros(N+1)
    y[i]=1
    D = method(x,y,True)
    PlotInterpolant(D,[-1,2])

i_widget = IntSlider(min=0, max=7, step=1, value=0)
N_widget = IntSlider(min=1, max=20, step=1, value=7)
def update_i_range(*args):
    i_widget.max = N_widget.value
N_widget.observe(update_i_range, 'value')

radio_button_interpolation_algorithm=RadioButtons(
    options=[('NewtonDD',NewtonDD),('SincInterpolator',SincInterpolator),('PiecewiseLinear',PiecewiseLinear)],
    value=SincInterpolator,
    description='New Alg.:',
    disabled=False
)

interact(show_time_new_algorithms,i=i_widget,N=N_widget,method=radio_button_interpolation_algorithm)
```

    interactive(children=(IntSlider(value=0, description='i', max=7), IntSlider(value=7, description='N', max=20, …

    <function __main__.show_time_new_algorithms(i=0, N=7, method='NewtonDD')>

## Another set of previous definitions
[Back to TOC](#toc)

```python
def Chebyshev(xmin,xmax,n=5):
    # This function calculates the n Chebyshev points and plots or returns them depending on ax
    ns = np.arange(1,n+1)
    x = np.cos((2*ns-1)*np.pi/(2*n))
    y = np.sin((2*ns-1)*np.pi/(2*n))
    plt.figure(figsize=(10,5))
    plt.ylim(-0.1,1.1)
    plt.xlim(-1.1,1.1)
    plt.plot(np.cos(np.linspace(0,np.pi)),np.sin(np.linspace(0,np.pi)),'k-')
    plt.plot([-2,2],[0,0],'k-')
    plt.plot([0,0],[-1,2],'k-')
    for i in range(len(y)):
        plt.plot([x[i],x[i]],[0,y[i]],'r-')
        plt.plot([0,x[i]],[0,y[i]],'r-')
    plt.plot(x,[0]*len(x),'bo',label='Chebyshev points')
    plt.plot(x,y,'ro')
    plt.xlabel('$x$')
    plt.title('n = '+str(n))
    plt.grid(True)
    plt.legend(loc='best')
    plt.show()
def Chebyshev_points(xmin,xmax,n):
    ns = np.arange(1,n+1)
    x = np.cos((2*ns-1)*np.pi/(2*n))
    #y = np.sin((2*ns-1)*np.pi/(2*n))
    return (xmin+xmax)/2 + (xmax-xmin)*x/2
```

```python
def Error(f, n, xmin, xmax, method=Lagrange, points=np.linspace, plot_flag=True):
    # This function plots f(x), the interpolating polynomial, and the associated error
    # points can be np.linspace to equidistant points or Chebyshev to get Chebyshev points

    # These two lines generate the data that will be used in the interpolation
    x = points(xmin,xmax,n)
    y = f(x)

    # These two lines compute the exact value of the function being interpolated on a finer grid
    xe = np.linspace(xmin,xmax,200)
    ye = f(xe)

    # This two lines build the interpolant chosen, where the name of the method is
    # passed as parameter usign the variable 'method'.
    D = method(x,y)
    yi = EvaluateInterpolant(D, xe)

    if plot_flag:
        plt.figure(figsize=(5,10))
        f, (ax1, ax2) = plt.subplots(1, 2, figsize=(15,5), sharey = False)

        ax1.plot(xe, ye,'k-', label='f(x)')
        ax1.plot(x, y,'ro', label='Interpolation points',markersize=10)
        ax1.plot(xe, yi,'b-', label='Interpolation')
        ax1.set_xlim(xmin-0.5,xmax+0.5)
        ax1.set_ylim(min(yi)-0.5,max(yi)+0.5)
        ax1.set_title('Interpolation')
        ax1.grid(True)
        ax1.set_xlabel('$x$')
        ax1.legend(loc='best')

        ax2.semilogy(xe, abs(ye-yi),'c-', label='Absolute Error')
        ax2.set_xlim(xmin-0.5,xmax+0.5)
        ax2.set_title('Absolute Error')
        ax2.set_xlabel('$x$')
        ax2.grid(True)
        plt.show()

    return max(abs(ye-yi))
```

## Convergence analysis
[Back to TOC](#toc)

The next set of experiments show a convergence analysis comparing equalspaced points to Chebychev points.
The $x$-axis is the number of points used and the $y$-axis is the maxmimum error obtain on the interval in logarithmic scale.
The experiment are the followings:
1. $f(x)=x^{10}$. In this case both sets of points perform similarly, however there is a significant drop in the error when $n$ is equal to $11$. This means, the error reach a minimum of $10^{-14}$ app at that number points. Why?
2. $f(x)=|x^3|$. This looks like a naive function but it is not. The equalspace points start to decrease from an error of $10^{0}$ until an error app $10^{-3}$ with approximately 10 points, it then start to increase the error. On the other hand, the Chebychev points show a decresing behavior upto $n=50$ but it only reaches an error of app $10^{-5}$, which is too high. What is the issue with this funcion?
3. $f(x)=\exp(-(x^{-2}))$. This is also a tricky function bevcause involves a division by $0$ when we evaluate it at $x=0$, but it is well defined at that points. Similarly to the first example, the equalspaced points reduce the error upto $n=10$, and then start to increase. For the Chebychev points, they decrease upto 40 points and the also show a slight increment in the error. What is the value of $f(x)$ at $x=0$?
4. $f(x)=\dfrac{1}{1+x^2}$. In this case both cases decrease the error but faster for the Chebyshev points. The main difference is that the minimum error reached for equalspaced points is about $10^{-8}$ and $10^{-13}$ for the Chebyshev points, in both cases about $n=37$.
5. $f(x)=\sin^3(x)$. In this case both cases reached an error $10^{-14}$ with $n=20$, then for equalspaced points start to increase the error right away but for Chebyshev points it starts to increase at $n=40$.

Answers to the previous questions:
1. The reason is that when $n=11$ we will be interpolation an polynomial of degree $10$ with a polynomial of degree $10$, so we can reproduce it exactly! This is why the error is $0$. When $n$ was lower, we only had an approximation. It is interesting that the error increases as we increase the number of points after $n=10$. Would it be useful to change the algorihtm used?
2. The issue is that the function is not analytic, this is because it involves the absolute value. Recall that expression for the error requires the computation of the $n$-th derivative of $f(x)$ at a $c$ point, so if the function is not differentiable, in this case the issues is at $x=0$, we could expect a bad behavior. The probles can be easily solve if we interpolate the function with a polynomial defined on $[-1,0]$ and another on $[0,1]$, ensurring the are equal at $x=0$. In this case we only need two polynomials of degree $3$ and we will be able to reproduce the function exactly! The issue is to notice this when we only have access to the computational implementation of $f(x)$ and not to the function itself.
3. 0
4. -
5. -

**We strongly suggest you to try different functions and play around!**

```python
my_functions=[lambda x: x**10,
              lambda x: np.abs(x**3),
              lambda x: np.exp(-(x**-2)),
              lambda x: 1/(1+x**2),
              lambda x: np.sin(x)**3]
labels = ["x^{10}",
          "|x^3|",
          "\exp(-x^{-2})",
          "1/(1+x^2)",
          "\sin^3(x)"]
data=zip(labels,my_functions)

radio_button_function=RadioButtons(
    options=list(data),
    description='Function:',
    disabled=False
)

radio_button_interpolation_algorithm=RadioButtons(
    options=[('Vandermonde',Vandermonde),('Lagrange',Lagrange),('Barycentric',Barycentric),('NewtonDD',NewtonDD),('SincInterpolator',SincInterpolator),('PiecewiseLinear',PiecewiseLinear)],
    value=Barycentric,
    description='Algorithm:',
    disabled=False
)

def convergence_study(my_function, method):
    n=100
    shift=2
    n_points=np.arange(shift,n)
    max_error=np.zeros(n-shift)
    max_error_es=np.zeros(n-shift)
    for i in n_points:
        if method!=SincInterpolator:
            max_error[i-shift] = Error(my_function, i, -1, 1, method, Chebyshev_points, plot_flag=False)
        else:
            max_error[i-shift] = 1
        max_error_es[i-shift] = Error(my_function, i, -1, 1, method, points=np.linspace, plot_flag=False)
    axis=plt.figure(figsize=(8,8))
    if method!=SincInterpolator:
        plt.semilogy(n_points,max_error,'bd',label='Chebyshev points',markersize=10)
    plt.semilogy(n_points,max_error_es,'r.',label='Equalspaced poins',markersize=16)
    plt.ylim(10**-16,10**4)
    plt.grid(True)
    plt.title('Interpolation Error')
    plt.xlabel('Number of points used in the interpolation')
    plt.ylabel('Max error on domain')
    plt.legend(loc='best')
    plt.show()
interact(convergence_study,my_function=radio_button_function, method=radio_button_interpolation_algorithm)
```

    interactive(children=(RadioButtons(description='Function:', options=(('x^{10}', <function <lambda> at 0x7fe9b1…

    <function __main__.convergence_study(my_function, method)>

## Analysis of time vs error for interpolation algorithms
[Back to TOC](#toc)

The idea here is to understand the relationship between the error we get for each algorithm and the computation time associated.
This analysis highlights the advantage of Barycentric interpolation with Chebyshev points when the function being interpolated is analytic.
Notice that the first plot shows the computation time required for the construction of the interpolant, the second the cost of evaluation of the interpolant in a finer grid and the third one shows the sum of both times.

```python
def get_error_and_time(f, xmin=-1, xmax=1, method=Lagrange, points=np.linspace):

    construction_times   = []
    evaluation_times     = []
    interpolation_errors = []

    #n_range = np.logspace(1, 2, num=5, endpoint=True, base=10, dtype=int)
    n_range = np.array([2,4,8,16,30,50])

    for n in n_range:

        # This function plots f(x), the interpolating polynomial, and the associated error
        # points can be np.linspace to equidistant points or Chebyshev to get Chebyshev points

        # These two lines generate the data that will be used in the interpolation
        x = points(xmin,xmax,n)
        y = f(x)

        # These two lines compute the exact value of the function being interpolated on a finer grid
        xe = np.linspace(xmin,xmax,100)
        ye = f(xe)

        # This two lines build the interpolant chosen, where the name of the method is
        # passed as parameter usign the variable 'method'.
        time_construction = %timeit -o -q -n 10 -r 5 method(x,y)
        construction_times.append(time_construction.average)
        D = method(x,y)

        time_evaluation = %timeit -o -q -n 10 -r 5 EvaluateInterpolant(D, xe)
        evaluation_times.append(time_evaluation.average)
        yi = EvaluateInterpolant(D, xe)

        if max(abs(ye-yi))<1e-16:
            interpolation_errors.append(1e-16)
        else:
            interpolation_errors.append(max(abs(ye-yi)))

    return construction_times, evaluation_times, interpolation_errors

def analysis_error_vs_time_interpolation_algorihtms(f):
    # List of methods
    list_of_methods = [('Vandermonde',Vandermonde),('Lagrange',Lagrange),('Barycentric',Barycentric),('NewtonDD',NewtonDD),('SincInterpolator',SincInterpolator),('PiecewiseLinear',PiecewiseLinear)]
    # List of type of points
    list_of_points = [('equalspaced',np.linspace), ('Chebyshev',Chebyshev_points)]
    list_output_data = []
    k = 1
    for name_method, method in list_of_methods:
        for points_name, points in list_of_points:
            # We exclude SincInterpolator for Chebyshev points because it need equalspaced data
            if (name_method=='SincInterpolator' and points_name=='Chebyshev'):
                continue
            tic = time.time()
            c_time, e_time, errors = get_error_and_time(f, xmin=-1, xmax=1, method=method, points=points)
            print(k,'/ 11: ',name_method, ',' , points_name, ', time:',time.time()-tic,'[s]')
            k=k+1
            list_output_data.append([name_method, points_name, c_time, e_time, errors])

    markers = ['8','>','<','^','v','o','d','D','H','h','*','p','s']

    fig = plt.figure(figsize=(16,16))
    plt.rcParams.update({'font.size': 12})

    k = 0
    for data in list_output_data:
        name_method, points_name, c_time, e_time, errors = data

        ax = plt.subplot(2,2,1)
        plt.title('Construction time')
        plt.loglog(errors, c_time, markers[k], lw=2.0, ms=10, label=name_method+', '+points_name)
        ax = plt.subplot(2,2,2)
        plt.title('Evaluation time')
        plt.loglog(errors, e_time, markers[k], lw=2.0, ms=10, label=name_method+', '+points_name)
        ax = plt.subplot(2,2,3)
        plt.title('Construction + evaluation times')
        plt.loglog(errors, np.array(c_time)+np.array(e_time), markers[k], lw=2.0, ms=10, label=name_method+', '+points_name)
        k=k+1

    plt.legend(loc='lower left', ncol=1, fancybox=True, shadow=True, numpoints=1, bbox_to_anchor=(1,0))

    for i in range(3):
        ax = plt.subplot(2,2,i+1)
        plt.xlabel("Absolute Error")
        plt.ylabel("Time in seconds")
        plt.grid('on')
    plt.show()

interact(analysis_error_vs_time_interpolation_algorihtms,f=radio_button_function)
```

    interactive(children=(RadioButtons(description='Function:', options=(('x^{10}', <function <lambda> at 0x7fe9b1…

    <function __main__.analysis_error_vs_time_interpolation_algorihtms(f)>

**Warning**: Notice that this experiment shows a _competitive_ behavior of the Vandermonde matrix but don't forget that the Vandermonde matrix is very ill-conditioned!

## Python Modules and Functions
[Back to TOC](#toc)

Interpolation:

https://numpy.org/doc/stable/reference/generated/numpy.polyfit.html

Vandermonde Matrix:

https://numpy.org/doc/stable/reference/generated/numpy.vander.html

Lagrange Interpolator:

https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.lagrange.html

Barycentric Interpolator:

https://people.maths.ox.ac.uk/trefethen/barycentric.pdf

https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.BarycentricInterpolator.html

Chebyshev Points for the First Kind:

https://numpy.org/doc/stable/reference/generated/numpy.polynomial.chebyshev.chebroots.html

# Acknowledgements
[Back to TOC](#toc)

* _Material created by professor Claudio Torres_ (`ctorres@inf.utfsm.cl`) _. DI UTFSM. October 2021._
* _Update October 2021 - v1.01 - C.Torres_ : Re-organizing jupyter notebook.

```python

```

SC1v2/Bonus - 05 - Newton's divided differences, Sinc and piecewiselinear interpolations.ipynb


# Equations
Here we are solving a set of coupled differential equations gives (from Chapman supplement and Arne notebook)

$$\begin{align}
    i \dot{\zeta}_1&= (p_1 B + q_1 B^2 + q\mu_1(t))\zeta_1+c\left[(\zeta_1^*\zeta_1+\zeta_0^*\zeta_0-\zeta_{-1}^*\zeta_{-1})\zeta_1+\zeta_0\zeta_0\zeta_{-1}^*)\right]\\
    i\dot{\zeta}_0&=(p_0 B + q_0 B^2 + q\mu_0(t))\zeta_0+c\left[(\zeta_1^*\zeta_1 + \zeta_{-1}^*\zeta_{-1})\zeta_0 + 2\zeta_1\zeta_{-1}\zeta_0^*\right]\\
    i \dot{\zeta}_{-1}&= (p_{-1} B + q_{-1} B^2 + q\mu_{-1}(t))\zeta_{-1}+c\left[(\zeta_{-1}^*\zeta_{-1}+\zeta_0^*\zeta_0-\zeta_{1}^*\zeta_{1})\zeta_{-1}+\zeta_0\zeta_0\zeta_{1}^*)\right]\\
\end{align}
$$

# Expectation Values
First we note that
$$ S_x = \frac{1}{\sqrt{2}}\begin{pmatrix}
0&1&0\\
1&0&1\\
0&1&0\\
\end{pmatrix}
$$
So to calculate its expectation value with a given wavefunction, we have
$$\psi^*S_x \psi =  (\psi_1^*,\psi_0^*,\psi_{-1}^*)\frac{1}{\sqrt{2}}\begin{pmatrix}
0&1&0\\
1&0&1\\
0&1&0\\
\end{pmatrix}
\begin{pmatrix}
\psi_1\\
\psi_0\\
\psi_{-1}
\end{pmatrix} = \frac{(\psi_1^*+\psi_{-1}^*)\psi_0 + \psi_0^*(\psi_{1}+\psi_{-1})}{\sqrt{2}}$$

```python
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import ode
from numpy.lib import scimath
%matplotlib inline
```

```python
#first define the system dy/dt = f(y,t)
def msqr(x):
    return np.conj(x) * x

def f(t,y,*args):
    """system of ode we want to solve"""
    z1i = y[0]
    z0i = y[1]
    zmi = y[2]
    #now define equations
    f0 = ((p1*B+q1*B**2+qu1)*z1i + c*((msqr(z1i) + msqr(z0i) - msqr(zmi))*z1i + z0i*z0i*np.conj(zmi)))*np.complex(0,-1)
    f1 = ((p0*B+q0*B**2+qu0)*z0i + c*((msqr(z1i) + msqr(zmi))*z0i + 2*z1i*zmi*np.conj(z0i)))*np.complex(0,-1)
    f2 = ((pm1*B+qm1*B**2+qum1)*zmi + c*((msqr(zmi) + msqr(z0i) - msqr(z1i))*zmi + z0i*z0i*np.conj(z1i)))*np.complex(0,-1)
    return [f0,f1,f2]
```

```python
class Operator(object):
    """class of operator for 3x3 matrices in this problem"""
    def __init__(self, mat,rep):
        self.rep = rep
        self.mat = mat

    def apply(self,ele):
        return np.dot(np.conj(ele), np.dot(self.mat,ele.T)).real

S_x = Operator(np.array([[0,1,0],[1,0,1],[0,1,0]])*1/np.sqrt(2),r'$S_x$')
N_yz = Operator(1j/np.sqrt(2)* np.array([[0,-1,0],[1,0,1],[0,-1,0]]),r'$N_{yz}#')
rho_0 = Operator(np.array([[0,0,0],[0,1,0],[0,0,0]]),r'$\rho_0$')
```

```python
#define problem parameters
dt = .1e-4
tfinal = 2
#allot arrays
t = np.linspace(0,tfinal,int(tfinal/dt))

def validate(par,t):
    """function to validate arrays"""
    lt = len(t)
    if isinstance(par,float) or isinstance(par,int) or isinstance(par,np.complex):
        return np.asarray([par for i in t])
    elif len(par)==lt:
        return par
    else:
        print('check array dimensions')

#define parameters
B =  260/700
p1 = 0
p0 = 0
pm1 = 0
qu1 =0
qu0 = 0
qum1= 0
q1 = 0
q0 = -5
qm1= 0
c = 30

B_arr = validate(B,t)
p1_arr = validate(p1,t)
p0_arr = validate(p0,t)
pm1_arr = validate(pm1,t)
qu1_arr = validate(qu1,t)
qu0_arr = validate(qu0,t)
qum1_arr = validate(qum1,t)
q1_arr = validate(q1,t)
q0_arr = validate(q0,t)
qm1_arr = validate(qm1,t)
c_arr = validate(c,t)
```

```python
#solve the sucker and plot
def solve_system(y0):
    r = ode(f).set_integrator('zvode')
    b = ode(f).set_integrator('zvode')
    r.set_initial_value(y0,0)
    b.set_initial_value(y0,0)
    ans = np.zeros((len(t),3),dtype = complex)
    step  = 0
    while r.successful() and r.t < tfinal-dt:
        #update the parameters
        B = B_arr[step]
        p1 = p1_arr[step]
        p0 = p0_arr[step]
        pm1 = pm1_arr[step]
        qu1 = qu1_arr[step]
        qu0 = qu0_arr[step]
        qum1 = qum1_arr[step]
        q1 = q1_arr[step]
        q0 = q0_arr[step]
        qm1 = qm1_arr[step]
        c = c_arr[step]
        ans[step] = np.asarray(r.integrate(r.t + dt))
        step += 1

    sol = []
    while b.successful() and b.t < tfinal:
        b.integrate(tfinal,step = True)
        sol.append([b.t,(np.conj(b.y[1])*b.y[1]).real])
    return [ans, np.array(sol)]

def get_exp_values(soll,step_size):
    sol = soll[0]
    ans = soll[1]
    """function to compute expectation values"""
    r_0 = np.asarray([rho_0.apply(i) for i in sol[::step_size]])
    sx_calc = np.asarray([S_x.apply(i) for i in sol[::step_size]])
    nyz_calc = np.asarray([N_yz.apply(i) for i in sol[::step_size]])
    return np.asarray([r_0, sx_calc, nyz_calc]),ans
```

```python
step_size = 1
#do calculation
N = 10000
m = 0
theta = 0
rho = .99
state = [scimath.sqrt((1-rho+m)/2)*np.exp(theta/2*1j),scimath.sqrt(rho),scimath.sqrt((1-rho-m)/2) * np.exp(theta/2*1j)]
ans, ans1 = get_exp_values(solve_system(state),step_size)
```

```python
plt.plot(t[::step_size],ans[0],'b.')
plt.plot(ans1[:,0],ans1[:,1],'g.-')
```

```python
from lmfit import Model
def sinfunc(x,A,f,off,phi):
    return A * np.sin(2*np.pi*f*x+phi)+off
sinmod = Model(sinfunc)
```

```python
pars = sinmod.make_params(A =1.5e3,f = 3, off = 39.5e3,phi=0)
pars['phi'].min = 0
pars['phi'].max = 2*np.pi
pars['A'].min = 0
fit = sinmod.fit(x = t[::step_size],data = ans[0]*N,params = pars)
fit.plot(fitfmt='-o');
print(fit.fit_report())
#help(fit)
```

```python
fig, ax = plt.subplots(3,1)
for i in range(3):
    m = ans[i]
    s = ans[i]
    ax[i].plot(t[::step_size],m)
    #ax[i].fill_between(t[::step_size,],m-s,m+s,facecolor='green',alpha=0.3)
plt.tight_layout()
```

# Blackman pulse shape
This is a pulse shape which is sort of like a square pulse, will make implementation much simplier in code.

```python
def black_left(t,ts,dur,fv):
    """calc blackman pulse won't set proper elements to zero here"""
    ans = fv*(0.42 +0.5*np.cos(np.pi*(t-ts-dur)/dur)+0.08*np.cos(2*np.pi*(1-ts-dur)))
    ans[t<ts]=0
    ans[t>(ts+dur)]=0
    return ans
def black_right(t,ts,dur,iv):
    """calc blackman pulse
    first we will calculate whole pulse and then set
    proper elements to zero"""
    ans = iv*(0.42 +0.5*np.cos(np.pi*(t-ts)/dur)+0.08*np.cos(2*np.pi*(1-ts)))
    ans[t<ts]=0
    ans[t>(ts+dur)]=0
    return ans

def const_pulse(t,ts,dur,val):
    ans = np.asarray([val for i in t])
    ans[t<ts]=0
    ans[t>(ts+dur)]=0
    return ans

def micro_pulse(t,ts,dur,ew,qm):
    ans = black_left(t,ts,ew,qm)+black_right(t,ts+ew+dur,ew,qm)+const_pulse(t,ts+ew,dur,qm)
    return ans

t = np.linspace(0,3e-3,1000)
ans = micro_pulse(t,0,2e-3,25e-6,800)
plt.plot(t,ans)

```

```python
%timeit micro_pulse(t,0,2e-3,25e-6,800)
```

    10000 loops, best of 3: 187 µs per loop

```python

```

SpinorBECSimulation/MeanField/SMA Spinor Oscillations - Single.ipynb


# Wave Equation
#### John S Butler john.s.butler@tudublin.ie   [Course Notes](https://johnsbutler.netlify.com/files/Teaching/Numerical_Analysis_for_Differential_Equations.pdf)    [Github](https://github.com/john-s-butler-dit/Numerical-Analysis-Python)

## Overview
This notebook will implement the Forward Euler in time and Centered in space method to appoximate the solution of the wave equation.

## The Differential Equation
Condsider the one-dimensional hyperbolic Wave Equation:
\begin{equation}  \frac{\partial u}{\partial t} +a\frac{\partial u}{\partial x}=0,\end{equation}
where $a=1$, with the initial conditions
\begin{equation} u(x,0)=1-\cos(x), \ \ 0 \leq x \leq 2\pi. \end{equation}
with wrap around boundary conditions.

```python
# LIBRARY
# vector manipulation
import numpy as np
# math functions
import math

# THIS IS FOR PLOTTING
%matplotlib inline
import matplotlib.pyplot as plt # side-stepping mpl backend
import warnings
warnings.filterwarnings("ignore")

```

## Discete Grid
The region $\Omega$ is discretised into a uniform mesh $\Omega_h$. In the space $x$ direction into $N$ steps giving a stepsize of
\begin{equation} \Delta_x=\frac{2\pi-0}{N},\end{equation}
resulting in
\begin{equation}x[j]=0+j\Delta_x, \ \ \  j=0,1,...,N,\end{equation}
and into $N_t$ steps in the time $t$ direction giving a stepsize of
\begin{equation} \Delta_t=\frac{1-0}{N_t}\end{equation}
resulting in
\begin{equation}t[n]=0+n\Delta_t, \ \ \ n=0,...,K.\end{equation}
The Figure below shows the discrete grid points for $N=10$ and $Nt=100$,  the known boundary conditions (green), initial conditions (blue) and the unknown values (red) of the Heat Equation.

```python
N=20
Nt=100
h=2*np.pi/N
k=1/Nt
time_steps=100
time=np.arange(0,(time_steps+.5)*k,k)
x=np.arange(0,2*np.pi+h/2,h)

X, Y = np.meshgrid(x, time)

fig = plt.figure()
plt.plot(X,Y,'ro');
plt.plot(x,0*x,'bo',label='Initial Condition');
plt.xlim((-h,2*np.pi+h))
plt.ylim((-k,max(time)+k))
plt.xlabel('x')
plt.ylabel('time (ms)')
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
plt.title(r'Discrete Grid $\Omega_h$ ',fontsize=24,y=1.08)
plt.show();
```

## Initial Conditions

The discrete initial conditions is,
\begin{equation} w[0,j]=1-\cos(x[j]), \ \ 0 \leq x[j] \leq \pi, \end{equation}
The figure below plots values of $w[j,0]$ for the inital (blue) conditions for $t[0]=0.$

```python
w=np.zeros((time_steps+1,N+1))
b=np.zeros(N-1)
# Initial Condition
for j in range (0,N+1):
    w[0,j]=1-np.cos(x[j])

fig = plt.figure(figsize=(12,4))
plt.plot(x,w[0,:],'o:',label='Initial Condition')
plt.xlim([-0.1,max(x)+h])
plt.title('Intitial Condition',fontsize=24)
plt.xlabel('x')
plt.ylabel('w')
plt.legend(loc='best')
plt.show()

```

## Boundary Conditions
To account for the wrap-around boundary conditions
\begin{equation}w_{-1,n}=w_{N,n},\end{equation}
and
\begin{equation}w_{N+1,n}=w_{0,n}.\end{equation}

```python
xpos = np.zeros(N+1)
xneg = np.zeros(N+1)

for j in range(0,N+1):
   xpos[j] = j+1
   xneg[j] = j-1

xpos[N] = 0
xneg[0] = N

```

## The Explicit Forward Time Centered Space  Difference Equation
The explicit Forward Time Centered Space difference equation of the Wave Equation is,
\begin{equation}
\frac{w^{n+1}_{j}-w^{n}_{j}}{\Delta_t}+\big(\frac{w^n_{j+1}-w^n_{j-1}}{2\Delta_x}\big)=0.
\end{equation}
Rearranging the equation we get,
\begin{equation}
w_{j}^{n+1}=w^{n}_{j}-\lambda a(w_{j+1}^{n}-w_{j-1}^{n}),
\end{equation}
for $j=0,...10$ where $\lambda=\frac{\Delta_t}{\Delta_x}$.

This gives the formula for the unknown term $w^{n+1}_{j}$ at the $(j,n+1)$ mesh points
in terms of $x[j]$ along the nth time row.

```python
lamba=k/h
for n in range(0,time_steps):
    for j in range (0,N+1):
         w[n+1,j]=w[n,j]-lamba/2*(w[n,int(xpos[j])]-w[n,int(xneg[j])])

```

## Results

```python
fig = plt.figure(figsize=(12,6))

plt.subplot(121)
for n in range (1,time_steps+1):
    plt.plot(x,w[n,:],'o:')
plt.xlabel('x[j]')
plt.ylabel('w[j,n]')

plt.subplot(122)
X, T = np.meshgrid(x, time)
z_min, z_max = np.abs(w).min(), np.abs(w).max()

plt.pcolormesh( X,T, w, vmin=z_min, vmax=z_max)

#plt.xticks(np.arange(len(x[0:N:2])), x[0:N:2])
#plt.yticks(np.arange(len(time)), time)
plt.xlabel('x[j]')
plt.ylabel('time, t[n]')
clb=plt.colorbar()
clb.set_label('Temperature (w)')
#plt.colorbar()
plt.suptitle('Numerical Solution of the  Wave Equation',fontsize=24,y=1.08)
fig.tight_layout()
plt.show()

```

```python

```

```python

```

Chapter 10 - Hyperbolic Equations/1001_Wave Equation.ipynb


# Tensors

_Colin Leach, May 2019_

Contents

1  Introduction

2  Vectors

2.1  Scalar product

3  One-forms

3.1  Coordinate transformations

3.2  Visualizing one-forms

3.3  Derivatives

4  Higher-rank tensors

4.1  Rank 2 tensors

4.1.1  Symmetries

4.2  Index raising and lowering

4.3  Contraction

4.4  Differentiation of tensors

5  Tensors in Python

5.1  Low-rank tensors as liner algebra

5.1.1  NumPy

5.1.2  SymPy

5.2  Tensor functions

5.2.1  NumPy

5.2.1.1  tensordot

5.2.1.2  einsum

5.2.2  SymPy

5.2.2.1  tensorproduct

5.2.2.2  tensorcontraction

## Introduction

What is a tensor? The answer seems to depend on who you ask and in what context. Some options include the concept of mapping:

- A linear machine that produces numbers when fed with vectors and one-forms. Each lower index represents a slot that can be fed a vector; each upper index represents a slot that can be fed a one-form. When all the slots are filled, out pops a number. (Collier, based on Misner, Thorne & Wheeler)
- An $M \choose N$ tensor is a linear function of $M$ one-forms and $N$ vectors into the real numbers. (Schutz)
- A linear map of the vectors into the reals. (Hobson, Efstathiou & Lasenby)
- A multilinear map from a collection of dual vectors and vectors to $\mathbb{R}$. (Carroll)
- A box with $n$ slots in its top, into which are inserted $n$ vectors, and one slot in its end, which prints out a single real number: the value that the tensor has when evaluated as a function of the $n$ inserted vectors. (Thorne & Blandford)

or of component transformation under rotations:
- A mathematical object represented by a collection of components that transform in a certain way. (Collier)
- A tensor is something that transforms like a tensor. (Zee)
- A tensor of rank $n$ is an array of $3^n$ values (in 3-D space) called "tensor components" that combine with multiple directional indicators (basis vectors) to form a quantity that does not vary as the coordinate system is changed. (Fleisch)
- An array of mathematical objects (usually numbers or functions) which transforms according to certain rules under coordinate change. In a $d$-dimensional space, a tensor of rank-$n$ has $d^n$ components which may be specified with reference to a given coordinate system. (Sochi)

These are mathematically equivalent at some deeper level, but the geometrical / mapping-machine version is usefully independent of coordinate system and the transformation version is usefully specific about how to do calculations.

## Vectors

These were discussed in the SR notebook, but without stressing their tensor nature: a 4-vector $\vec{A}$ is a $1 \choose 0$ tensor, its components have a single upper index $A^\alpha$ and the basis vectors have a single lower index $\vec{e}_\alpha$.

### Scalar product

This is defined by $\vec{A} \cdot \vec{B} = -A^0B^0 + A^1B^1 + A^2B^2 + A^1B^2 = A^\alpha B^\beta (\vec{e}_\alpha \cdot \vec{e}_\beta)$.

But in any reference frame, $\vec{e}_\alpha \cdot \vec{e}_\beta = \eta_{\alpha \beta}$, the metric tensor. Hence:
$$\vec{A} \cdot \vec{B} = A^\alpha B^\beta \eta_{\alpha \beta}$$

The dimension of a vector is defined by $\vec{A} \cdot \vec{A} = \vec{A}^2$. Note that, like the interval $ds^2$, this can be positive, negative or zero. A null vector has $\vec{A}^2 = 0$ but this does _not_ require the components to be all zero. Photons always move on null lines and their 4-velocity always has direction but vanishing magnitude.

## One-forms

These have a variety of names. Some authors prefer to call them dual vectors (e.g. Carroll). They have often been called covariant vectors or covectors (to distinguish them from contravariant vectors), but most physicists now avoid these (Schutz: "The reason that 'co' and 'contra' have been abandoned is that they mix up two very different things").

Naming aside, these are $0 \choose 1$ tensors which take a vector argument to give a real number. The set of all 1-forms satisfies the axioms for a vector space, called the 'dual vector space'.

Duality between 1-forms and vectors is deeply significant. Vectors, as defined in a previous section, are $1 \choose 0$ tensors which take a 1-form argument to give a real number. For other examples of duality in physics think of row vectors vs column vectors, $\psi^*$ vs $\psi$ in QM, or bras vs kets in Dirac notation.

1-forms have the opposite index notation to vectors: components are $p_\beta$ and basis 1-forms are $\tilde{\omega}^\beta$. The arrow and tilde notation is less standard, but is used here copying Schutz.

A vector $\vec{V}$ acts on a 1-form $\tilde{p}$ to give a real number and equivalently $\tilde{p}$ acts on $\vec{V}$ to give a real number. There are various notations for the same result:
$$ \vec{V}(\tilde{p})\ \equiv\ \tilde{p}(\vec{V})\ \equiv\ p_\alpha V^\alpha\ \equiv\ \langle \tilde{p},\vec{V} \rangle $$

### Coordinate transformations
We saw previously that basis vectors transform as $\vec{e}_\alpha = \Lambda_\alpha\!^\bar{\beta}\ \vec{e}_\bar{\beta}$.

1-form components transform in the same way: $\tilde{p}_\bar{\beta} = \Lambda^\alpha\!_\bar{\beta}\ p_\alpha$. The basis 1-forms transform inversely: $\tilde{\omega}^\bar{\alpha} = \Lambda^\bar{\alpha}\!_\beta\ \omega^\beta$.

In any frame, we have the relationship
$$ \tilde{\omega}^\alpha(\vec{e}_\beta) = \delta^\alpha\!_\beta $$
The result is an identity matrix with all-positive entries, in contrast to the Minkowski metric.

### Visualizing one-forms

We tend to think of a vector as an arrow in space or spacetime. A 1-form is a different beast, often represented as a set of parallel surfaces whose spacing is _inversely_ related to the magnitude of the 1-form. This is like a higher-dimensional version of contour lines on a topographic map: the closer the lines, the steeper the slope; the more lines you walk cross, the bigger the height difference.

The analogy is even closer: the gradient of a function is a 1-form.

### Derivatives

Consider a scalar field at $\phi(\vec{x})$ every event $\vec{x}$ and a curve which can be parameterized by $\tau$: $\phi(\tau) = \phi[t(\tau), x(\tau), y(\tau), z(\tau)]$ with tangent 4-velocity $U$. Then
$$ \frac{d\phi}{d\tau} = \frac{\partial{\phi}}{\partial{t}} U^t + \frac{\partial{\phi}}{\partial{x}} U^x + \frac{\partial{\phi}}{\partial{y}} U^y + \frac{\partial{\phi}}{\partial{z}} U^z $$

The _gradient_ of $\phi$ is the 1-form:
$$ \tilde{d}\phi\ \xrightarrow[\mathcal{O}]{}\ \left( \frac{\partial{\phi}}{\partial{t}}, \frac{\partial{\phi}}{\partial{x}}, \frac{\partial{\phi}}{\partial{y}}, \frac{\partial{\phi}}{\partial{z}} \right) $$

The gradient transforms like any 1-form:
$$ (\tilde{d}\phi)_\bar{\alpha} = \frac{\partial x^\beta}{\partial x^\bar{\alpha}} (\tilde{d}\phi)_\beta \qquad \text{where} \qquad \frac{\partial x^\beta}{\partial x^\bar{\alpha}}= \Lambda^\beta\!_\bar{\alpha} $$

The shorthand notation for partial derivatives includes a comma:
$$ \frac{\partial{\phi}}{\partial{x^\alpha}} \equiv \phi_{,\alpha} \qquad \frac{\partial{x^\alpha}}{\partial{x^\beta}} \equiv x^\alpha\!_{,\beta} \equiv \delta^\alpha\!_\beta $$

## Higher-rank tensors

The _rank_ of a tensor is the total number of indices. A tensor of _type_ $M \choose N$ has a rank of $M+N$. A tensor of rank $k$ can (after choosing a reference frame) be represented by a $k$-dimensional array of coordinates, 4 per dimension.

### Rank 2 tensors

Familiar examples of $0 \choose 2$ tensors from SR include the metric tensor $\eta_{\mu \nu}$ and the Lorentz tranformation tensor $\Lambda$.

The outer product (tensor product) of two 1-forms gives a $0 \choose 2$ tensor: $\tilde{\omega}^{\alpha \beta} = \tilde{\omega}^\alpha \otimes \tilde{\omega}^\beta$.

Such tensors are a basis for all $0 \choose 2$ tensors and $ \mathbf{f} = f_{\alpha \beta} \tilde{\omega}^\alpha \otimes \tilde{\omega}^\beta $.

Similarly, $\vec{e}_\alpha \otimes \vec{e}_\beta$ is a basis for $2 \choose 0$ tensors.

#### Symmetries

A tensor $\mathbf{f}$ is _symmetric_ if $\  \mathbf{f}(\vec{A} \vec{B}) = \mathbf{f}(\vec{B} \vec{A}) \quad \forall \vec{A}, \vec{B}\ $ and _antisymmetric_ if $\ \mathbf{f}(\vec{A} \vec{B}) = -\mathbf{f}(\vec{B} \vec{A}) \quad \forall \vec{A}, \vec{B} $.

An arbitrary $0 \choose 2$ tensor can be uniquely split into its symmetric and antisymmetic parts, with notation $f_{(\alpha \beta)}$ and $f_{[\alpha \beta]}$ respectively:
$$ f_{(\alpha \beta)} \equiv \frac{1}{2} (f_{\alpha \beta} + f_{\beta \alpha}) \qquad f_{[\alpha \beta]} \equiv \frac{1}{2} (f_{\alpha \beta} - f_{\beta \alpha}) \qquad f_{\alpha \beta} =  f_{(\alpha \beta)} + f_{[\alpha \beta]} $$

### Index raising and lowering

The metric tensor maps a vector $\vec{V}$ of type $1 \choose 0$ into a 1-form $\tilde{V}$ of type $0 \choose 1$: $A_\alpha = \eta_{\alpha \beta} A^\beta$.

The inverse metric maps a 1-form into a vector: $A^\alpha = \eta^{\alpha \beta} A_\beta$.

More generally, the metric maps a $M \choose N$ tensor into an $M-1 \choose N+1$ tensor and the inverse metric maps it to an $M+1 \choose N-1$ tensor. For example, an arbitrary $2 \choose 1$ tensor:
$$ T^\alpha\!_{\beta \gamma} = \eta_{\beta \mu} T^{\alpha \mu}\!_\gamma $$

Note that in SR the Minkowski metric is its own inverse, so all these operations do is flip the sign of the time component. GR is less simple.

### Contraction

A $M \choose N$ tensor can be converted to a $M-1 \choose N-1$ tensor by summing over an upper/lower pair of indices: $T^\alpha\!_{\mu \nu \alpha} = T_{\mu \nu}$. The order of indices matters and in general $T^\alpha\!_{\mu \nu \alpha} \ne T^\alpha\!_{\mu \alpha \nu}$.

### Differentiation of tensors

Differentiation of a $M \choose N$ tensor gives a $M \choose N+1$ tensor with one higher rank. In the simplest case, a function $f$ is a $0 \choose 0$ tensor and its derivative $\tilde{d}f$ is a $0 \choose 1$ tensor.

A $1 \choose 1$ tensor can be written $\mathbf{T} = T^\alpha\!_\beta\, \tilde{\omega}^\beta \otimes \vec{e}_\alpha$. Then the gradient of $\mathbf{T}$ is a $1 \choose 2$ tensor: $ \nabla \mathbf{T} = T^\alpha\!_{\beta, \gamma}\, \tilde{\omega}^\beta \otimes \tilde{\omega}^\gamma \otimes \vec{e}_\alpha $

## Tensors in Python

### Low-rank tensors as liner algebra

By the time you get to implementing things in code, you have probably already chosen a coordinate system. Then tensors with rank 1 or 2 can be represented by vectors and matrices. Then the `@` operator handles matrix multiplication (since Python 3.5) and packages like `numpy.linalg` and `sympy.matrices` will do the rest.

Conventionally, 4-vectors are represented by column vectors and one-forms by row vectors. This is largely irrelevant in NumPy, which doesn't distinguish between a 1-D array and its transpose (unlike, for example, Matlab). The difference tends to be more important in SymPy.

```python
import numpy as np
import sympy as sp
```

#### NumPy

Set up some numpy arrays to play with:

```python
eta = np.eye(4)
eta[0,0] *= -1
eta
```

    array([[-1.,  0.,  0.,  0.],
           [ 0.,  1.,  0.,  0.],
           [ 0.,  0.,  1.,  0.],
           [ 0.,  0.,  0.,  1.]])

```python
A = np.array([1,2,3,4])
B = np.array([4,3,2,1])
A, B
```

    (array([1, 2, 3, 4]), array([4, 3, 2, 1]))

$A_\mu = \eta_{\mu \nu} A^\nu$ :

```python
A_tilde = eta @ A
A_tilde
```

    array([-1.,  2.,  3.,  4.])

$\left = \eta_{\mu \nu} A^\mu b^\nu$ :

```python
eta @ A @ B
```

    12.0

#### SymPy

I'm conflicted on this package: it's very impressive in some respects, but for doing symbolic math I'm not sure it's the best tool for the job (and I recently paid for a Maple license).

```python
from sympy.interactive.printing import init_printing
init_printing()
from sympy.abc import phi, theta
```

```python
coords = (theta, phi)
c = sp.Matrix(coords)
```

`c` is a colum vector:

```python
c, c.shape
```

Inner product:

```python
c.transpose() @ c
```

Outer product:

```python
c @ c.transpose()
```

Simple example for the surface of a sphere in polar coordinates. See the Curvature notebook for much more of this.

```python
g_ij = sp.Matrix([[1, 0], [0, sp.sin(theta)**2]])
inv_gij = g_ij.inv()
display(g_ij, inv_gij)
```

```python
g_ij @ c
```

### Tensor functions

Python has a number of ways to handle higher-rank tensors as such, without falling back on linear algebra routines. This is a fairly niche area of interest so the documentation isn't always the easiest to understand.

#### NumPy

The main functions are `tensordot()` for multiplications and `einsum()` for Einstein summation.

##### tensordot

[__tensordot()__](https://docs.scipy.org/doc/numpy-1.14.0/reference/generated/numpy.tensordot.html) provides a variety of ways to multiply two n-dimensional arrays together, controlled by the (optional) third parameter `axes`.

`axes=0` gives the tensor product $A \otimes B$:

```python
display(A, B)
np.tensordot(A, B, 0)
```

    array([1, 2, 3, 4])

    array([4, 3, 2, 1])

    array([[ 4,  3,  2,  1],
           [ 8,  6,  4,  2],
           [12,  9,  6,  3],
           [16, 12,  8,  4]])

`axes=1` gives the tensor dot product $A \cdot B$, similar to the previous examples using `@`:

```python
eta_contracted = np.tensordot(eta, A, 1)
display(eta_contracted)
display(np.tensordot(eta_contracted, A, 1))
```

    array([-1.,  2.,  3.,  4.])

    array(28.)

`axes=2` is the default and gives a double contraction $C^{\mu \alpha \beta} D_{\alpha \beta \nu} = T^\mu\!_\nu$. Similarly, higher integers will give an N-fold contraction.

```python
C = np.array(range(1,9))
C = C.reshape(2,2,2)
display(C)
```

    array([[[1, 2],
            [3, 4]],

           [[5, 6],
            [7, 8]]])

```python
np.tensordot(C, C, 2)
```

    array([[ 50,  60],
           [114, 140]])

`axes=int` takes the last N indices of C and the first N indices of D in order. For more control, give a (2,N) array of indices to contract on. So $C^{\alpha \mu \beta} D_{\alpha \beta \nu} = T^\mu\!_\nu$ needs ([0,2],[0,1]):

```python
np.tensordot(C, C, axes=([0,2],[0,1]))
```

    array([[ 74,  88],
           [106, 128]])

##### einsum

[__`einsum()`__](https://docs.scipy.org/doc/numpy-1.14.0/reference/generated/numpy.einsum.html) is a powerful and flexible function that has many uses, including everything tensordot can do. And just reading that, you know it's also going to be confusing. This has also [been addressed on stackoverflow](https://stackoverflow.com/questions/26089893/understanding-numpys-einsum) in helpful detail. See also the [related blog post](http://ajcr.net/Basic-guide-to-einsum/) - really, _do read it_ if you want to use this successfully. As the author says, "on the downside, it can take a little while to understand the notation and sometimes a few attempts to apply it correctly to a tricky problem." Also see [this longer blog](https://obilaniu6266h16.wordpress.com/2016/02/04/einstein-summation-in-numpy/).

```python
np.einsum('ikj, ijl', C, C)
```

    array([[ 74,  88],
           [106, 128]])

#### SymPy

For symbolic work we have the `tensorproduct()` and `tensorcontraction()` functions.

There is also a tensor module, but that seems to be a rather abstract base class and only useful to someone creating a library.

##### tensorproduct

[__`tensorproduct()`__](https://docs.sympy.org/latest/modules/physics/quantum/tensorproduct.html): "The tensor product is a non-commutative multiplication that is used primarily with operators and states in quantum mechanics."

```python
T1 = sp.tensorproduct(c, c.transpose())
display(T1)
display(T1.shape)
```

```python
T2 = sp.tensorproduct(c.transpose(), c)
display(T2)
display(T2.shape)
```

```python
T3 = sp.tensorproduct(c, c)
display(T3)
display(T3.shape)
```

##### tensorcontraction

__`tensorcontraction()`__ does broadly what the name suggests, though I couldn't find much documentation. The second parameter is a list of indices to contract on.

```python
from sympy.abc import x
C_sym = sp.Array(C*x)
C_sym, C_sym.shape
```

```python
sp.tensorcontraction(C_sym, (0,2))
```

```python

```


```python
%matplotlib inline
```

```python
"""MB Metodo da bissecao para funcoes unidimensionais
entrada:
    fx: uma string dependendo de x, i.e., a funcao
           (e.g., 'x^2 + 1', 'x^2*cos(x)', etc.)
    a: limite inferior do dominio
    b: limite superior do dominio
    tol: tolerancia
    N: numero maximo de iteracoes do metodo

saida:
    xm: raiz da funcao
"""

import inspect, re
import numpy as np

def bisecao(f,a,b,tol,N):

    # TODO identificar a variável usada na função
    #      Aqui, tentei assumir que apenas uma era usada (e.g. 'x'),
    #      mas foi complicado generalizar quando há objeto numpy
    #var = re.search('[a-zA-Z]+',f)
    #var = var.group()

    # adiciona as chamadas das funções de numpy
    f = re.sub('(cos|sin|tan|exp)', r'np.\1', f)

    # identifica a variavel
    var = re.search(r'([a-zA-Z][\w]*) ?([\+\-\/*]|$|\))', f).group(1)

    print('f('+ var +') = ' + f + '\n')

    # cria função anônima
    f = eval('lambda ' + var + ' :' + f)

    varPlot = np.arange(a,b+1,1)

    # Se função não for de uma variável, lança erro.
    # Mais aplicável se o caso geral fosse implementado.
    if len(inspect.getfullargspec(f).args) - 1 > 0:
        raise ValueError('O código é válido apenas para uma variável.')

    # calcula valor da função nos extremos
    fa = f(a)
    fb = f(b)

    # verifica sinal da função para o intervalo passado
    if fa*fb >= 0:
        raise ValueError('A função deve ter sinais opostos em a e b!')

    # flag usada para prevenir a obtenção da raiz
    # antes de o intervalo ter sido
    # suficientemente reduzido
    done = 0;

    # loop principal

    # bisecta o intervalo
    xm = (a+b)/2

    i = 1 # contador
    xms = [xm]
    fxms = [f(xm)]

    while abs(a-b) > tol and ( not done or N != 0 ):
    # avalia a função no ponto médio
        fxm = f(xm)
        print("(i = {0:d}) f(xm)={1:f} | f(a)={2:f} | f(b)={3:f}".format(i,fxm,fa,fb))

        if fa*fxm < 0:       # Raiz esta à esquerda de xm
            b = xm
            fb = fxm
            xm = (a+b)/2
        elif fxm*fb < 0:     # Raiz esta à direita de xm
            a = xm
            fa = fxm
            xm = (a+b)/2
        else:               # Achamos a raiz
            done = 1

        xms.append(xm)
        fxms.append(f(xm))

        N -= 1              # Atualiza passo
        i += 1              # Atualiza contador

    colors = []

    for i in range(len(xms)):
        colors.append(int((256/len(xms)) * i))

    plt.plot(varPlot, f(varPlot))
    plt.scatter(xms, fxms, c=colors, cmap='Wistia')
    plt.xlabel('X')
    plt.ylabel('Y')
    plt.title("Xm's testados ao longo da função")
    plt.colorbar()
    plt.show()
    print("Solução encontrada: {0}".format(xm))

    return xm
```

```python
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import bisect, newton

# Dados de entrada

t = np.arange(0,520,1)   # tempo [s]
c = 1.46   # coeficiente de arrasto [kg/s]
m = 90     # massa [kg]
g = 9.81   # constante de gravidade [m/s2]

# Dados de saída

## velocidade terminal [m/s]
v_ms1 = (g*m/c)*(1 - np.exp((-c/m)*t))

# velocidade terminal [km/h]
v_kh1 = (1/3.6)*v_ms1;

# gráfico tempo x velocidade
#plt.figure
#plt.plot(t,v_ms1)
#plt.xlabel('t [s]')
#plt.ylabel('v [m/s]')
#plt.title('Velocidade terminal - paraquedista')

#############################################
# Dados de entrada

x = np.arange(0,21,1)   # tempo [s]
#x = 20
ca = 42   # coeficiente de arrasto [kg/s]
cb = 28     # massa [kg]
cc = 4   # constante de gravidade [m/s2]
K = 0.016

# Dados de saída

f = ((cc + x)/((ca - 2 * x)**2 * (cb - x))) - K

#print(f)
# velocidade terminal [km/h]
#v_kh1 = (1/3.6)*v_ms1;

# gráfico tempo x velocidade
plt.figure
plt.plot(x,f)
plt.xlabel('X')
plt.ylabel('Y')
```

```python
import sympy as sp

time = 12     # tempo [s]
mass = 70     # massa [kg]
vel = 42     # velocidade [m/s]
grav = 9.81   # constante de gravidade [m/s2]

# defino variáveis simbólicas
g,m,t,v,c = sp.symbols('g,m,t,v,c')

# expressão geral
f_g = (g*m/c)*(1 - sp.exp((-c/m)*t)) - v

# expressão particular com valores substituídos
# convertida para string
f_s = str(f_g.subs({'g':grav,'m':mass,'v':vel,'t':time}))

# TODO
# para esta função, teremos que substituir 'exp' por 'np.exp')
#print('f(c) = ' + f_s + '\n')
#f_s = '-42 + 686.7*(1 - np.exp(-6*c/35))/c'

# resolve bisseção
xm = bisecao(f_s,12,16,1e-5,100)
```

## Tarefa: SUBMÓDULO BISSEÇÃO

* Melhore o código Python tratando os TODOs:

Tente generalizar o código da bisseção para que identifique automaticamente a variável de entrada utilizada pelo usuário (use expressões regulares e remova o argumento `var` da definição da função).

Note que o trecho simbólico abaixo foi necessário para substituir a função da chamada `exp`, não interpretada por `eval` por uma nova string que usasse `np.exp`.
```python
# TODO
# para esta função, teremos que substituir 'exp' por 'np.exp')
print('f(c) = ' + f_s + '\n')
f_s = '-42 + 686.7*(1 - np.exp(-6*c/35))/c'
```
Tente fazer as correções necessárias no código. **Sugestão:** verifique a função `sympy.core.evalf` do módulo `sympy`)

* Adicione mecanismos de plotagem no código Python

* Crie um código em Javascript para adicionarmos na página do projeto Numbiosis com o máximo possível de GUI (labels + input data).

* Teste a implementação com um problema realista.

**Problema sugerido:**
Uma reação química reversível

$$2A+B \iff C$$

pode ser caracterizada pela relação de equilíbrio

$$K = \dfrac{c_c}{c_a^2c_b},$$

onde a nomenclatura $c_i$ representa a concentração do constituinte $i$. Suponha que definamos uma variável $x$ como o número de moles de $C$ que são produzidos. A conservação da massa pode ser usada para reformular a relação de equilíbrio como

$$K = \dfrac{(c_{c,0} + x)}{(c_{a,0} - 2x)^2 (c_{b,0} - x),}$$

onde o subscrito $0$ designa a concentração inicial de cada constituinte. Se $K = 0,016$, $c_{a,0} = 42$, $c_{b,0} = 28$ e $c_{c,0} = 4$, determine o valor de $x$.

(a) Obtenha a solução graficamente.

(b) Com base em (a), resolva a raiz com suposições iniciais de $x_l = 0$ e $x_u = 20$, com critério de erro de $\epsilon_s = 0,5\%$. (Vide clipping _Definições de erro_ para entender $\epsilon_s$.)

(c) Use o método da bisseção.

```python
K0 = 0.016
ca0 = 42
cb0 = 28
cc0 = 4

K, ca, cb, cc, x = sp.symbols('K,ca,cb,cc,x')

f = ((cc + x)/((ca - 2 * x)**2 * (cb - x))) - K

f = str(f.subs({K:K0, ca:ca0, cb:cb0, cc:cc0}))

#f = '((4 + x)/((42 - 2 * x)**2 * (28 - x))) - 0.016'

xm = bisecao(f,0,20,5e-3,100)
```