(c) Juan Gomez 2019. Thanks to Universidad EAFIT for support. This material is part of the course Introduction to Finite Element Analysis

# Piecewise Interpolation

## Introduction

In the previous notebook we introduced Lagrange interpolation as a method to approximate functions in terms of discrete known values of the function. Accordingly, if there were $N$ known values of the function it was possible to propose an $N-1$ order interpolating polynomial. Although this approach is theoretically fine, the specific interpolating polynomial would be problem dependent difficulting its automatization in a general computer code. In the finite element method this lack of generality is dealt with through the use of sub-domain based interpolation where the interpolation scheme is implemented for a general sub-domain of a fixed number of discrete points. Accordingly, in this technique the sample of $N$ data points is arranged in sub-domains, say of pairs of two points and the resulting interpolating polynomial is now piece-wise continous. In this notebook we cover the basics of this sub-domain or element based interpolation scheme. **After completing this notebook you should be able to:**

* Recognize the differences between a global and a local based interpolation scheme.

* Recognize the advantages and disadvantages between global and local interpolation scheme.

* Formulate, and implement in Python, locally based interpolation schemes.

* Recognize the fudamental concept of a finite element as a locally based interpolation scheme.

## Global scheme

Leat us consider the same function used for the interpolation problem described in the previous notebook and defined as:

 $$ f(x)=x^3+4x^2-10 $$

with $x$ in the interval $\left[ {{-1},{1}} \right]$. Assume also that we know the exact value of the function at points $[-1, -0.5, 0, 0.5, 1].$ These points will be called in what follows **nodes**. We will compute a unique Lagrange interpolating polynomial using the 5 knwon values of the function at the given nodes. The following block of code shows the steps required in this global scheme.

**(Add comments to clarify the relevant steps ion the code below)**

```
#Try using matplotlib inline instead of matplotlib notebook to see the difference
%matplotlib notebook
import matplotlib.pyplot as plt
import numpy as np
from scipy import interpolate
import sympy as sym
```

```
def LagrangPoly(x, order, i, xi=None):
    if xi == None:
        xi = sym.symbols('x:%d' % (order+1))
    index = list(range(order+1))
    index.pop(i)
    return sym.prod([(x-xi[j])/(xi[i]-xi[j]) for j in index])
```

```
fx = lambda x: x**3 + 4.0*x**2 - 10.0

fdx = lambda x: 3*x**2 + 8.0*x

#
npts = 200
xx = np.linspace(-1, 1, npts)
x_data = np.array([-1, -0.5, 0.0, 0.5, 1])
fd = fx(x_data)

plt.figure(0)
yy = fx(xx)
plt.plot(xx, yy, 'r--')
plt.plot([-1, -0.5, 0.0, 0.5, 1], fd, 'ko')
```

    <IPython.core.display.Javascript object>

    [<matplotlib.lines.Line2D at 0x7fc7c27026a0>]

### Locally based Lagrange polynomials

We now split the problem domain corresponding to $x \in \left[ {{-1},{1}} \right]$ into 4-constant size subdomains. Each resulting sub-domain is comprised of two **nodes** which are used to define linear first-order polynomials.

In the following block of code we compute interpolation polynomials at each one of these $\triangle x=0.5$ sub-domians. Note that although each polynomial exists only at a given sub-domain all of them are exactly the same. This suggests that a more general and powerful implementation should compute only the linear polynomials once and then use them recursively in the construction of the approximating function.

**(Add comments to clarify the relevant steps ion the code below)**

```
x = sym.symbols('x')
pol = []
pol.append(sym.simplify(LagrangPoly(x, 1, 0, [-1.0, -0.5])))
pol.append(sym.simplify(LagrangPoly(x, 1, 1, [-1.0, -0.5])))
pol.append(sym.simplify(LagrangPoly(x, 1, 0, [-0.5,  0.0])))
pol.append(sym.simplify(LagrangPoly(x, 1, 1, [-0.5,  0.0])))
pol.append(sym.simplify(LagrangPoly(x, 1, 0, [0.0,  0.5])))
pol.append(sym.simplify(LagrangPoly(x, 1, 1, [0.0,  0.5])))
pol.append(sym.simplify(LagrangPoly(x, 1, 0, [0.5,  1.0])))
pol.append(sym.simplify(LagrangPoly(x, 1, 1, [0.5,  1.0])))
```

```
plt.figure()
xx = np.linspace(-1, -0.5, npts)
for k in range(2):
    for i in range(npts):
        yy[i] = pol[k].subs([(x, xx[i])])
    plt.plot(xx, yy)
xx = np.linspace(-0.5, 0.0, npts)
for k in range(2):
    for i in range(npts):
        yy[i] = pol[k+2].subs([(x, xx[i])])
    plt.plot(xx, yy)
#
xx = np.linspace(0.0, 0.5, npts)
for k in range(2):
    for i in range(npts):
        yy[i] = pol[k+4].subs([(x, xx[i])])
    plt.plot(xx, yy)
#
xx = np.linspace(0.5, 1.0, npts)
for k in range(2):
    for i in range(npts):
        yy[i] = pol[k+6].subs([(x, xx[i])])
    plt.plot(xx, yy)
```

    <IPython.core.display.Javascript object>

### Interpolating polynomial $p(x)$ to approximate $f(x)$
Now we build the complete approximating polynomial $p(x)$. Since each polynomial is now local at each sub-domain we use:

$$p(x) = {L^L}(x)f({x^L}) + {L^R}(x)f({x^R})$$

where $f({x^L})$ and $f({x^R})$ are the function values at the left and right ends of the sub-domain respectively while ${L^L}(x)$ and ${L^R}(x)$ are the associated first-order polynomials.

Within the context of the finite element method each one of the sub-domains is termed a **finite element**. As such, a finte element is then a prescribed sub-domain and its corresponding local interpolation functions. In this problem the sub-domains are comprised of two points therefore defining linear finite elements. However, elements with higher order variations can also be formulated. This local approach becomes easy to code if the local polynomials are formulated in an auxiliary reference system, as will be described later.

In the following code snippet we approximate the unknown function $f(x)$ in each sub-domain using the two locally based Lagrange polynomials. In the plot the black dots represent the exact or known nodal values of the fuction, while the continous line is the exact function.

**Questions:**

**Suggest an approach to improve the approximation to the function $f(x)$ but using the idea of locally based interpolation polynomials.**

**(Add comments to clarify the relevant steps ion the code below)**

```
plt.figure()
plt.grid()
xx = np.linspace(-1.0, -0.5, npts)
for i in range(npts):
    yy[i] = fd[0]*pol[0].subs([(x, xx[i])]) + fd[1]*pol[1].subs([(x, xx[i])])
plt.plot(xx, yy)
xx = np.linspace(-0.5, 0.0, npts)
for i in range(npts):
    yy[i] = fd[1]*pol[2].subs([(x, xx[i])]) + fd[2]*pol[3].subs([(x, xx[i])])
plt.plot(xx, yy)
xx = np.linspace( 0.0, 0.5, npts)
for i in range(npts):
    yy[i] = fd[2]*pol[4].subs([(x, xx[i])]) + fd[3]*pol[5].subs([(x, xx[i])])
plt.plot(xx, yy)
xx = np.linspace( 0.5, 1.0, npts)
for i in range(npts):
    yy[i] = fd[3]*pol[6].subs([(x, xx[i])]) + fd[4]*pol[7].subs([(x, xx[i])])
plt.plot(xx, yy)
#
xx = np.linspace(-1.0, 1.0, npts)
zz = fx(xx)
plt.plot(xx, zz)
plt.plot([-1, -0.5, 0, 0.5, 1], fd, 'ko')
```

    <IPython.core.display.Javascript object>

    [<matplotlib.lines.Line2D at 0x7fc7c019d6a0>]

### Secondary variables

The term secondary variable is given here to these functions which are obtained using the approximated function $p(x)$. For instance, assume that in the current problem we are also interested in the first order derivative of the function $f(x)$ but we don't have nodal values in order to conduct interpolation. We can however use the found approximation $p(x)$ to $f(x)$ like:

$$p'(x) = \frac{dL^L(x)}{dx}f({x^L}) + \frac{dL^R(x)}{dx}f({x^R}).$$

The following code snippet plots the known values of the first-otder derivatives (black dots), the exact derivative $f'(x)$ (blue line) and the locally computed derivatives. Note that since in each interval the function is approximated by a linear function the derivative is constant leading to jumps at the boundaries of each sub-domain.

**Questions:**

**Propose an alternative to improve the approximation to the first order derivative of the function shown below.**

**How are the discontinuities in the first derivative of the function related to the local function?**

```
dpol = []
for j in range(8):
    dpol.append(sym.diff(pol[j], x))
```

```
plt.figure()
plt.grid()
xx = np.linspace(-1.0, -0.5, npts)
for i in range(npts):
    yy[i] = fd[0]*dpol[0].subs([(x, xx[i])]) + fd[1]*dpol[1].subs([(x, xx[i])])
plt.plot(xx, yy)
xx = np.linspace(-0.5, 0.0, npts)
for i in range(npts):
    yy[i] = fd[1]*dpol[2].subs([(x, xx[i])]) + fd[2]*dpol[3].subs([(x, xx[i])])
plt.plot(xx, yy)
xx = np.linspace(0.0, 0.5, npts)
for i in range(npts):
    yy[i] = fd[2]*dpol[4].subs([(x, xx[i])]) + fd[3]*dpol[5].subs([(x, xx[i])])
plt.plot(xx, yy)
xx = np.linspace(0.5, 1.0, npts)
for i in range(npts):
    yy[i] = fd[3]*dpol[6].subs([(x, xx[i])]) + fd[4]*dpol[7].subs([(x, xx[i])])
plt.plot(xx, yy)
#
fc = fdx(x_data)
plt.plot([-1, -0.5, 0, 0.5, 1], fc, 'ko')

xx = np.linspace(-1.0, 1.0, npts)
for i in range(npts):
    zz[i] = fdx(xx[i])
plt.plot(xx, zz)
```

    <IPython.core.display.Javascript object>

    [<matplotlib.lines.Line2D at 0x7fc7c0060128>]

As a result of the local interpolation scheme the first derivative becomes discontinous. Although these disconitnuities introduce error in the solution these can be reduced using a larger number of sub-domains. In finite element analysis this corresponds to conducting a mesh refinement.

### Glossary of terms

**Node:** A point where the function to be approximated by the interpolating polynomials is known.

**Sub-domain:** A portion of the total computational domain comprised between two points but probably containing several nodes.

**Finite element:** The specification of a sub-domain size and its corresponding interpolation functions.

**Canonical element:** A finite element of constant size and thus of known interpolation polynomials and to which elements of different size can be transformed to.

### Class activity
#### Problem 1
For the function  $f(x) = {x^3} + 4{x^2} - 10$ in the range $[-1.0, 1.0]$:

* Find values at nodal points corresponding to 4 sub-domains each one with 3 nodal points and using these values implement a local interpolation scheme using 2-nd order interpolation polynomials.

* Plot the interpolation polynomial in each sub-domain and the corresponding interpolating function $p(x)$.

* In the same plot compare $p(x)$ and $f(x)$. Additionally, plot the first derivative of the function obtained from $p(x)$ and $f(x)$.

#### Problem 2
For the Runge function defined by:

$$f(x) = \frac{1}{{1 + 25{x^2}}}$$

implement an interpolation scheme using local 1st-order Lagrange polynomials using:
* (i) sub-domains of constant size $\Delta x = 0.2$
* (ii) sub-domains whose size decreases towards the edges of the interval.

#### Problem 3
Using an independent script or a notebook implement a local interpolation scheme using a canonical element of size 2.0 and use it to approximate the Runge function discussed in the class notes.

```
from IPython.core.display import HTML
def css_styling():
    styles = open('./nb_style.css', 'r').read()
    return HTML(styles)
css_styling()
```

<link href='http://fonts.googleapis.com/css?family=Fenix' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Source+Code+Pro:300,400' rel='stylesheet' type='text/css'>

<style>

/*
Template for Notebooks for Modelación computacional.

Based on Lorena Barba template available at:

    https://github.com/barbagroup/AeroPython/blob/master/styles/custom.css
*/

/* Fonts */
@font-face {
font-family: "Computer Modern";
src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
}

/* Text */
div.cell{
width:800px;
margin-left:16% !important;
margin-right:auto;
}
h1 {
font-family: 'Alegreya Sans', sans-serif;
}
h2 {
font-family: 'Fenix', serif;
}
h3{
font-family: 'Fenix', serif;
margin-top:12px;
margin-bottom: 3px;
}
h4{
font-family: 'Fenix', serif;
}
h5 {
font-family: 'Alegreya Sans', sans-serif;
}
div.text_cell_render{
font-family: 'Alegreya Sans',Computer Modern, "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif;
line-height: 135%;
font-size: 120%;
width:600px;
margin-left:auto;
margin-right:auto;
}
.CodeMirror{
font-family: "Source Code Pro";
font-size: 90%;
}
/* .prompt{
display: None;
}*/
.text_cell_render h1 {
font-weight: 200;
font-size: 50pt;
line-height: 100%;
color:#CD2305;
margin-bottom: 0.5em;
margin-top: 0.5em;
display: block;
}
.text_cell_render h5 {
font-weight: 300;
font-size: 16pt;
color: #CD2305;
font-style: italic;
margin-bottom: .5em;
margin-top: 0.5em;
display: block;
}
.warning{
color: rgb( 240, 20, 20 )
}
</style>

```

```


#  Interpolación de Lagrange

```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import Image
```

## 1. Ejemplo 3 puntos

```python
#Hacemos la interpolación con tres puntos de la tabla
#(205,1724.3);(210,1907.7);(215,2105.9)
X=np.array([205,210,215])
Y=np.array([1724.3,1907.7,2105.9])

```

```python
datos=pd.read_csv("vapor_Saturado.csv",delimiter=",")
print(datos)
X=datos['T(°C)']
```

       T(°C)  P_sat(kPa)  v_f(m^3/kg)  v_g(m^3/kg)
    0    205      1724.3     0.000116      0.11508
    1    210      1907.7     0.001173      0.10429
    2    215      2105.9     0.001180      0.09468

## Formula para los coeficientes de interpolación:
x: Punto a interpolar ; n: número de puntos-1
    $$ L_{i}(x)=\Pi_{j=0,j\neq i}^{n}\frac{(x-x_{j})}{(x_{i}-x_{j})} $$ $ i=0,...,n$
$$ p(x)=\sum_{i=0}^{n}y_{i}*L_{i}(x)$$

```python
#Coeficientes de p(x
#x: punto a interpolar
#X: datos en x, Y datos en y
def L0(x,X):#i=0, j=1,2
    return (x-X[1])*(x-X[2])/((X[0]-X[1])*(X[0]-X[2]))
def L1(x,X):#i=1,j=0,2
    return (x-X[0])*(x-X[2])/((X[1]-X[0])*(X[1]-X[2]))
def L2(x,X):#i=1,j=0,1
    return (x-X[0])*(x-X[1])/((X[2]-X[0])*(X[2]-X[1]))
```

```python
#sumatoria para el polinomio
def p_lagrange(x,X,Y):
    return L0(x,X)*Y[0]+L1(x,X)*Y[1]+L2(x,X)*Y[2]
```

```python
x=207.5#K; valor a interpolar
p_lagrange(x,X,Y)
```

    1814.1499999999999

```python
import sympy as sy
```

```python
x=sy.symbols('x')
```

```python
L0(x,X)
```

$\displaystyle \frac{\left(x - 215\right) \left(x - 210\right)}{50}$

```python
print(L1(x,X))
```

    -(x - 215)*(x - 205)/25

```python
print(L2(x,X))
```

    (x - 210)*(x - 205)/50

```python
print(p_lagrange(x,X,Y))
```

    34.486*(x - 215)*(x - 210) - 76.308*(x - 215)*(x - 205) + 42.118*(x - 210)*(x - 205)

```python
sy.expand(p_lagrange(x,X,Y))
```

$\displaystyle 0.295999999999992 x^{2} - 86.1599999999962 x + 6947.69999999972$

## 2. Interpolación de P vs T  y generalización programa Lagrange

```python
#base del polinomio L_i
def L(i,x,X,n):#i=0, j=1,2
    prod=1
    for j in range(n):
        if(i!=j):
            prod*=(x-X[j])/(X[i]-X[j])
    return prod
#Calculo de los polinomios haciendo una sumatoria de los productos L_iy_i
def p_lagrange(x,X,Y):
    n=len(X)
    suma=0
    for i in range(n):
#         print(type(L(i,x,X,n)))
#         print(type(Y[i]))
        suma+=L(i,x,X,n)*Y[i]
    return suma
```

## Datos
Tomados de la tabla A-5 del Cengel para las propiedades del agua saturada, obtenidos mediante cálculoscon la formulación de la IAPWS para las propiedades del agua.
https://doi.org/10.1063/1.1461829 \
http://www.iapws.org/newform.html

```python
datos=pd.read_csv("thermodynamical_tables_water.csv",delimiter="\t",header=[0,1,2])
datos
```

      Temp. (°C)
      Sat. press. (kPa)
      Specific volume  (m3/kg)
      Unnamed: 3_level_0
      Internal energy  (kJ/kg)
      Unnamed: 5_level_0
      Unnamed: 6_level_0
      Enthalpy  (kJ/kg)
      Unnamed: 8_level_0
      Unnamed: 9_level_0
      Entropy  (kJ/kg·K)
      Unnamed: 11_level_0
      Unnamed: 12_level_0

      Temp.
      Sat. press.
      Sat. liquid.
      Sat.vapor.
      Sat. liquid.
      Evap.
      Sat.vapor.
      Sat. liquid.
      Evap.
      Sat.vapor.
      Sat. liquid.
      Evap.
      Sat.vapor.

      T
      Psat
      v_f
      v_g
      u_f
      u_fg
      u_g
      h_f
      h_fg
      h_g
      s_f
      s_fg
      s_g

      0
      0.01
      0.6117
      0.001000
      206.000000
      0.000
      2374.9
      2374.90
      0.001
      2500.9
      2500.9
      0.0000
      9.1556
      9.1556

      1
      5.00
      0.8725
      0.001000
      147.030000
      21.019
      2360.8
      2381.80
      21.020
      2489.1
      2510.1
      0.0763
      8.9487
      9.0249

      2
      10.00
      1.2281
      0.001000
      106.220000
      42.020
      2346.6
      238.87
      42.022
      2477.2
      2519.2
      0.1511
      8.7488
      8.8999

      3
      15.00
      1.7057
      0.001001
      77.885000
      62.980
      2332.5
      2395.50
      62.982
      2465.4
      2528.3
      0.2245
      8.5559
      8.7803

      4
      20.00
      2.3392
      0.001002
      57.762000
      83.913
      2318.4
      2402.30
      83.915
      2453.5
      2537.4
      0.2965
      8.3696
      8.6661

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      71
      355.00
      17570.0000
      0.001808
      0.007872
      1682.200
      706.4
      2388.60
      1714.000
      812.9
      2526.9
      3.8442
      1.2942
      5.1384

      72
      360.00
      18666.0000
      0.001895
      0.006950
      1726.200
      625.7
      2351.90
      1761.500
      720.1
      2481.6
      3.9165
      1.1373
      5.0537

      73
      365.00
      19822.0000
      0.002015
      0.006009
      1777.200
      526.4
      2303.60
      1817.200
      605.5
      2422.7
      4.0004
      0.9489
      4.9493

      74
      370.00
      21044.0000
      0.002217
      0.004953
      1844.500
      385.6
      2230.10
      1891.200
      443.1
      2334.3
      4.1119
      0.6890
      4.8009

      75
      373.95
      22064.0000
      0.003106
      0.003106
      2015.700
      0.0
      2015.70
      2084.300
      0.0
      2084.3
      4.4070
      0.0000
      4.4070

76 rows × 13 columns

Opcional: Modificamos los indices para que queden los multiíndices de forma adecuada

```python
a=[x[j] for x in datos.columns for j in range(3)]
a=np.reshape(a,[int(len(a)/3),3])
a[3][0]=a[2][0]
a[5][0]=a[4][0]
a[6][0]=a[4][0]
a[8][0]=a[7][0]
a[9][0]=a[7][0]
a[11][0]=a[10][0]
a[12][0]=a[10][0]
a=[tuple(a[i]) for i in range(len(a))]
col_names =pd.MultiIndex.from_tuples(a)
datos.columns=col_names
```

```python
datos
```

      Temp. (°C)
      Sat. press. (kPa)
      Specific volume  (m3/kg)
      Internal energy  (kJ/kg)
      Enthalpy  (kJ/kg)
      Entropy  (kJ/kg·K)

      Temp.
      Sat. press.
      Sat. liquid.
      Sat.vapor.
      Sat. liquid.
      Evap.
      Sat.vapor.
      Sat. liquid.
      Evap.
      Sat.vapor.
      Sat. liquid.
      Evap.
      Sat.vapor.

      T
      Psat
      v_f
      v_g
      u_f
      u_fg
      u_g
      h_f
      h_fg
      h_g
      s_f
      s_fg
      s_g

      0
      0.01
      0.6117
      0.001000
      206.000000
      0.000
      2374.9
      2374.90
      0.001
      2500.9
      2500.9
      0.0000
      9.1556
      9.1556

      1
      5.00
      0.8725
      0.001000
      147.030000
      21.019
      2360.8
      2381.80
      21.020
      2489.1
      2510.1
      0.0763
      8.9487
      9.0249

      2
      10.00
      1.2281
      0.001000
      106.220000
      42.020
      2346.6
      238.87
      42.022
      2477.2
      2519.2
      0.1511
      8.7488
      8.8999

      3
      15.00
      1.7057
      0.001001
      77.885000
      62.980
      2332.5
      2395.50
      62.982
      2465.4
      2528.3
      0.2245
      8.5559
      8.7803

      4
      20.00
      2.3392
      0.001002
      57.762000
      83.913
      2318.4
      2402.30
      83.915
      2453.5
      2537.4
      0.2965
      8.3696
      8.6661

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      71
      355.00
      17570.0000
      0.001808
      0.007872
      1682.200
      706.4
      2388.60
      1714.000
      812.9
      2526.9
      3.8442
      1.2942
      5.1384

      72
      360.00
      18666.0000
      0.001895
      0.006950
      1726.200
      625.7
      2351.90
      1761.500
      720.1
      2481.6
      3.9165
      1.1373
      5.0537

      73
      365.00
      19822.0000
      0.002015
      0.006009
      1777.200
      526.4
      2303.60
      1817.200
      605.5
      2422.7
      4.0004
      0.9489
      4.9493

      74
      370.00
      21044.0000
      0.002217
      0.004953
      1844.500
      385.6
      2230.10
      1891.200
      443.1
      2334.3
      4.1119
      0.6890
      4.8009

      75
      373.95
      22064.0000
      0.003106
      0.003106
      2015.700
      0.0
      2015.70
      2084.300
      0.0
      2084.3
      4.4070
      0.0000
      4.4070

76 rows × 13 columns

```python
T=datos["Temp. (°C)"]["Temp."]["T"]
P=datos["Sat. press. (kPa)"]["Sat. press."]["Psat"]
v_f=datos['Specific volume  (m3/kg)']['Sat. liquid.']['v_f']
v_g=datos['Specific volume  (m3/kg)']['Sat.vapor.']['v_g']
s_f=datos['Entropy  (kJ/kg·K)']['Sat. liquid.']['s_f']
s_fg=datos['Entropy  (kJ/kg·K)']['Evap.']['s_fg']
s_g=datos['Entropy  (kJ/kg·K)']['Sat.vapor.']['s_g']
T_inter=np.linspace(7.5,372.5,74)
```

### Tabla interpolación P vs T

```python
P_pred_complete=p_lagrange(T_inter,T,P)#[p_lagrange(t,T,P) for t in T_inter]
results_complete=pd.DataFrame({"T(°C)":T_inter,"P predicted (kPa)":P_pred_complete})
P_pred_pag914=p_lagrange(T_inter[40:],np.array(T[40:]),np.array(P[40:]))#[p_lagrange(t,T,P) for t in T_inter]
# P_pred_pag914=p_lagrange(T_inter[50:-20],np.array(T[50:-20]),np.array(P[50:-20]))
# results_pag914=pd.DataFrame({"T(°C)":T_inter[40:],"P predicted (kPa)":P_pred_pag914})
print(results_complete)
```

        T(°C)  P predicted (kPa)
    0     7.5      -5.813055e+15
    1    12.5       2.033932e+14
    2    17.5      -1.010549e+13
    3    22.5       6.550517e+11
    4    27.5      -5.267777e+10
    ..    ...                ...
    69  352.5      -6.488619e+11
    70  357.5       9.860509e+12
    71  362.5      -1.932409e+14
    72  367.5       5.182995e+15
    73  372.5      -1.688322e+17

    [74 rows x 2 columns]

### 5. a) Gráfica

```python
plt.figure()
plt.plot(T_inter[12:-13],P_pred_complete[12:-13])
plt.scatter(T,P)
plt.xlabel('T(°C)')
plt.ylabel('P(kPa)')
plt.savefig("P_vs_T_completo.png")
plt.figure()
plt.plot(T_inter[41:-2],P_pred_pag914[1:-2])
plt.xlabel('T(°C)')
plt.ylabel('P(kPa)')
# plt.plot(T_inter[50:-20],P_pred_pag914[:])
plt.scatter(T[40:],P[40:])
plt.savefig("P_vs_T_pag_914.png")
```

### 3. Tabla interpolación $v_f$ vs T

```python
v_f_pred_complete=p_lagrange(T_inter,T,v_f)#[p_lagrange(t,T,P) for t in T_inter]
results_complete=pd.DataFrame({"T(°C)":T_inter,"v_f predicted (m^3/kg)":v_f_pred_complete})
v_f_pred_pag914=p_lagrange(T_inter[40:],np.array(T[40:]),np.array(v_f[40:]))#[p_lagrange(t,T,P) for t in T_inter]
# results_pag914=pd.DataFrame({"T(°C)":T_inter[40:],"P predicted (kPa)":P_pred_pag914})
print(results_complete)
```

        T(°C)  v_f predicted (m^3/kg)
    0     7.5            5.462886e+09
    1    12.5           -1.866936e+08
    2    17.5            9.045329e+06
    3    22.5           -5.707461e+05
    4    27.5            4.459041e+04
    ..    ...                     ...
    69  352.5            2.138649e+06
    70  357.5           -3.202643e+07
    71  362.5            6.189650e+08
    72  367.5           -1.638380e+10
    73  372.5            5.270369e+11

    [74 rows x 2 columns]

### 5. b) Gráfica

```python
plt.figure()
plt.plot(T_inter[13:-15],v_f_pred_complete[13:-15])
plt.scatter(T,v_f)
plt.savefig("v_f_vs_T_completo.png")
plt.xlabel('T(°C)')
plt.ylabel('v_f(m^3/kg)')
plt.figure()
plt.plot(T_inter[42:-3],v_f_pred_pag914[2:-3])
plt.scatter(T[40:],v_f[40:])
plt.xlabel('T(°C)')
plt.ylabel('v_f(m^3/kg)')
plt.savefig("v_f_vs_T_pag_914.png")
```

### 4. Tabla interpolación $v_g$ vs T

```python
v_g_pred_complete=p_lagrange(T_inter,T,v_g)#[p_lagrange(t,T,P) for t in T_inter]
results_complete=pd.DataFrame({"T(°C)":T_inter,"v_g predicted (m^3/kg)":v_g_pred_complete})
v_g_pred_pag914=p_lagrange(T_inter[40:],np.array(T[40:]),np.array(v_g[40:]))#[p_lagrange(t,T,P) for t in T_inter]
# results_pag914=pd.DataFrame({"T(°C)":T_inter[40:],"P predicted (kPa)":P_pred_pag914})
print(results_complete)
```

        T(°C)  v_g predicted (m^3/kg)
    0     7.5           -3.061732e+11
    1    12.5            1.071279e+10
    2    17.5           -5.322977e+08
    3    22.5            3.450948e+07
    4    27.5           -2.775827e+06
    ..    ...                     ...
    69  352.5           -3.610596e+07
    70  357.5            5.480133e+08
    71  362.5           -1.072732e+10
    72  367.5            2.874121e+11
    73  372.5           -9.352780e+12

    [74 rows x 2 columns]

### 5. c) Gráfica

```python
plt.figure()
plt.plot(T_inter[13:-13],v_g_pred_complete[13:-13])
plt.scatter(T,v_g)
plt.xlabel('T(°C)')
plt.ylabel('v_g(m^3/kg)')
plt.savefig("v_g_vs_T_completo.png")
plt.figure()
plt.plot(T_inter[41:-2],v_g_pred_pag914[1:-2])
plt.scatter(T[40:],v_g[40:])
plt.xlabel('T(°C)')
plt.ylabel('v_g(m^3/kg)')
plt.savefig("v_g_vs_T_pag_914.png")
```

## Laboratorio métodos

### 3. Tabla interpolación $s_f$ vs T

```python
s_f_pred_complete=p_lagrange(T_inter,T,s_f)#[p_lagrange(t,T,P) for t in T_inter
results_complete=pd.DataFrame({"T(°C)":T_inter,"s_f predicted (m^3/kg)":s_f_pred_complete})
print(results_complete)
```

        T(°C)  s_f predicted (m^3/kg)
    0     7.5            5.202882e+12
    1    12.5           -1.823609e+11
    2    17.5            9.076901e+09
    3    22.5           -5.894807e+08
    4    27.5            4.749684e+07
    ..    ...                     ...
    69  352.5            4.737520e+08
    70  357.5           -7.232524e+09
    71  362.5            1.423440e+11
    72  367.5           -3.833041e+12
    73  372.5            1.253216e+14

    [74 rows x 2 columns]

### 5. b) Gráfica

```python
plt.figure()
plt.plot(T_inter[13:-14],s_f_pred_complete[13:-14])
plt.scatter(T,s_f)
plt.xlabel('T(°C)')
plt.ylabel('s_f(kJ/(kg.K))')
plt.savefig("s_f_vs_T_completo.png")
```

```python
s_fg_pred_complete=p_lagrange(T_inter,T,s_fg)#[p_lagrange(t,T,P) for t in T_inter

results_complete=pd.DataFrame({"T(°C)":T_inter,"s_fg predicted (m^3/kg)":s_fg_pred_complete})
print(results_complete)
plt.figure()
plt.plot(T_inter[13:-14],s_fg_pred_complete[13:-14])
plt.scatter(T,s_fg)
plt.xlabel('T(°C)')
plt.ylabel('s_fg(kJ/(kg.K))')
plt.savefig("s_fg_vs_T_completo.png")
```

### 4. Tabla interpolación $s_g$ vs T

```python
s_g_pred_complete=p_lagrange(T_inter,T,s_g)#[p_lagrange(t,T,P) for t in T_inter]
results_complete=pd.DataFrame({"T(°C)":T_inter,"s_g predicted (m^3/kg)":s_g_pred_complete})

print(results_complete)
```

        T(°C)  s_g predicted (m^3/kg)
    0     7.5           -6.050236e+12
    1    12.5            2.125737e+11
    2    17.5           -1.060843e+10
    3    22.5            6.909016e+08
    4    27.5           -5.584118e+07
    ..    ...                     ...
    69  352.5           -5.085413e+08
    70  357.5            7.763265e+09
    71  362.5           -1.527888e+11
    72  367.5            4.114423e+12
    73  372.5           -1.345295e+14

    [74 rows x 2 columns]

### 5. c) Gráfica

```python
plt.figure()
plt.plot(T_inter[12:-13],s_g_pred_complete[12:-13])
plt.scatter(T,s_g)
plt.xlabel('T(°C)')
plt.ylabel('s_f(kJ/(kg.K))')
plt.savefig("s_g_vs_T_completo.png")
```

## 6 a) Resultados y análisis

Observamos en prácticamente todas las gráficas que los polinomios de interpolación de Lagrange muestran resultados similares a los valores proporcionados en el apéndice, sin embargo en los puntos extremos el polinomio de Lagrange tiene a diverger oscilatoriamente. Este fenómeno se conoce como el fenómeno de Runge, el cual ocurre porque aunque exista un polinomio de interpolación a la función verdadera. no se conoce si el polinomio de Lagrange se acercará a dicho polinomio. Además, al tener un número considerable de datos n=76, el polinomio interpolado va a tener una derivada de alto orden que va a ir aumentando si aumentamos el número de puntos y nos alejamos del origen, con lo cual las oscilaciones van a aumentar a medida que nos alejamos de los datos.

```python
plt.figure()
plt.plot(s_f_pred_complete[15:-16],T_inter[15:-16],c='r',label="Interpolación s_f")
plt.scatter(s_f,T,c='tab:blue',label='Datos')
plt.plot(s_g_pred_complete[15:-16],T_inter[15:-16],c='g',label="Interpolación s_g")
plt.scatter(s_g,T,c='tab:blue')
plt.ylabel('T (°C)')
plt.xlabel('s (kJ/(kg.K))')
plt.legend()
plt.savefig("diagrama_TS_completo.png")
```

```python
Image(url= "TS_diagram_cengel.png", width=600, height=600)
```

Por otro lado, al considerar el diagrama TS (T vs s_f y T vs s_g) tanto para los datos como para el polinomido de interpolación, vemos que se puede reproducir el diagrama TS para la región del vapor saturado y que corresponde a la curva de color negro en el Cengel p 924. Para las regiones interiores a la curva se debe conocer los porcentajes que hay en la mezcla de f (fluido) con g (gas) de agua. Si miramos la interpolación, vemos que no el polinomio no puede predecir valores de temperatura bajos ni valores cercanos al punto crítico (donde coexisten las fases)

## 6 b) Conclusiones

* Exceptuando los primeros 16 y los últimos 16 datos, el polinomios de interpolación de Lagrange muestran resultados similares a los valores proporcionados en el apéndice.
* En los puntos extremos el polinomio de Lagrange presenta el fenómeno de Runge, observándose oscilaciones.
* El polinomio de Lagrange es útil para una cantidad baja de puntos, como 3 puntos.
* Se puede revisar el error porcentual para conocer el margen de error con respecto a los teóricos y no solo estimar la diferencia en orden de magnitud o gráficamente.
* Se pueden realizar pruebas con diferente cantidad de puntos para conocer cuando es suficientemente buena la interpolación de Lagrange.
* Los puntos de la entropía proporcionan una mejor interpretación de los datos para el análisis del estudiante puesto que existen diagramas TS en la literatura, así como diagramas HS (diagrama de Mollier)
* Para una primera instancia se recomienda realizar análisis con las tablas de gas ideal para estudiantes novicios, para los otros se pueden dejar estos datos y/o analizar las tablas de vapor sobrecalentada (curvas de nivel)

```python

```


# Finite Difference Time Domain (FDTD) Electromagnetics

(lightly adapted from: Understanding the Finite-Difference Time-Domain Method, John B. Schneider, http://www.eecs.wsu.edu/~schneidj/ufdtd, 2010.)

The FDTD method employs finite differences as approximations to both
the spatial and temporal derivatives that appear in Maxwell's
equations (specifically Ampere's and Faraday's laws).

## Maxwell's equations

Maxwell's equations in differential form are:

\begin{equation}
\nabla \cdot \mathbf{D} = \rho \quad \text{(Gauss's Law)}
\end{equation}

\begin{equation}
\nabla \cdot \mathbf{B} = 0 \quad \text{(Gauss's Law for magentism)}
\end{equation}

\begin{equation}
\nabla \times \mathbf{E} = -\frac{\partial\mathbf{B}}{\partial t} \quad \text{(Faraday's Law)}
\end{equation}

\begin{equation}
\nabla \times \mathbf{H} = \mathbf{J} + \frac{\partial\mathbf{D}}{\partial t} \quad   \text{(Ampere's Law)}
\end{equation}

where: $\mathbf{D}$ is the electric displacement field, $\rho$ is the electric charge density, $\mathbf{B}$ is the magnetic flux density, $\mathbf{E}$ is the electric field, $\mathbf{H}$ is the magnetic field, and $\mathbf{J}$ is the electric current density.

In a region with no sources and no currents, they become:

\begin{equation}
\nabla \cdot \mathbf{D} = 0
\end{equation}

\begin{equation}
\nabla \cdot \mathbf{B} = 0
\end{equation}

\begin{equation}
\nabla \times \mathbf{E} = -\frac{\partial\mathbf{B}}{\partial t}
\end{equation}

\begin{equation}
\nabla \times \mathbf{H} = \frac{\partial\mathbf{D}}{\partial t}
\end{equation}

For materials without polarization and magnetization:

\begin{equation}
\mathbf{D} = \epsilon \mathbf{E}
\end{equation}

\begin{equation}
\mathbf{B} = \mu \mathbf{H}
\end{equation}

And the equations become

\begin{equation}
\nabla \cdot \mathbf{E} = 0
\end{equation}

\begin{equation}
\nabla \cdot \mathbf{H} = 0
\end{equation}

\begin{equation}
\nabla \times \mathbf{E} = -\mu \frac{\partial\mathbf{H}}{\partial t}
\end{equation}

\begin{equation}
\nabla \times \mathbf{H} = \epsilon \frac{\partial\mathbf{E}}{\partial t}
\end{equation}

(Note that these equations are written in terms of $\mathbf{E}$ and $\mathbf{H}$, the common practice in computational electromagnetics, rather than $\mathbf{E}$ and $\mathbf{B}$, which is common in other fields.)

## Maxwell's equations in 1D

Consider a one-dimensional space where there are only variations in
the $x$ direction.  Assume that the electric field only has a $z$
component.  In this case Faraday's law can be written

\begin{equation}
  -\mu \frac{\partial \mathbf{H}}{\partial t} =
  \nabla \times \mathbf{E} =
  \left|
  \begin{array}{ccc}
     \hat{\mathbf{a}}_{x} & \hat{\mathbf{a}}_{y} & \hat{\mathbf{a}}_{z} \\
     \frac{\partial}{\partial x} & 0 & 0 \\
     0 & 0 & E_z
  \end{array}
  \right|
  = -\hat{\mathbf{a}}_{y}\frac{\partial E_z}{\partial x}.
 \label{eq:faradayVec}
\end{equation}

Thus $H_y$ must be the only non-zero component of the magnetic field
which is time varying.  (Since the right-hand side of this equation
has only a $y$ component, the magnetic field may have non-zero
components in the $x$ and $z$ directions, but they must be static.  We
will not be concerned with static fields here.)  Knowing this,
Ampere's law can be written

\begin{equation}
  \epsilon \frac{\partial \mathbf{E}}{\partial t} =
  \nabla \times \mathbf{H} =
  \left|
  \begin{array}{ccc}
     \hat{\mathbf{a}}_{z} & \hat{\mathbf{a}}_{y} & \hat{\mathbf{a}}_{z} \\
     \frac{\partial}{\partial x} & 0 & 0 \\
     0 & H_y & 0
  \end{array}
  \right|
  = \hat{\mathbf{a}}_{z}\frac{\partial H_y}{\partial x}.
 \label{eq:ampereVec}
\end{equation}

The two scalar equations obtained from the previous equations are

\begin{eqnarray}
  \mu \frac{\partial H_y}{\partial t} &=&
    \frac{\partial E_z}{\partial x},  \label{eq:faradayScalar}
  \\
  \epsilon \frac{\partial E_z}{\partial t} &=&
    \frac{\partial H_y}{\partial x}. \label{eq:ampereScalar}
\end{eqnarray}

The first equation gives the temporal derivative of the magnetic field
in terms of the spatial derivative of the electric field.  Conversely,
the second equation gives the temporal derivative of the electric
field in terms of the spatial derivative of the magnetic field.  As
will be shown, the first equation will be used to advance the magnetic
field in time while the second will be used to advance the electric
field.  A method in which one field is advanced and then the other,
and then the process is repeated, is known as a leap-frog method.

## Central differencing

Consider the
Taylor series expansions of the function $f(x)$ expanded about the
point $x_0$ with an offset of $\pm\delta/2$:

\begin{eqnarray}
  f\!\left(x_0+\frac{\delta}{2}\right) &=&
    f(x_0) + \frac{\delta}{2} f'(x_0) +
        \frac{1}{2!}\left(\frac{\delta}{2}\right)^2 f''(x_0) +
        \frac{1}{3!}\left(\frac{\delta}{2}\right)^3 f'''(x_0) + \ldots,
  \\
  f\!\left(x_0-\frac{\delta}{2}\right) &=&
    f(x_0) - \frac{\delta}{2} f'(x_0) +
        \frac{1}{2!}\left(\frac{\delta}{2}\right)^2 f''(x_0) -
        \frac{1}{3!}\left(\frac{\delta}{2}\right)^3 f'''(x_0) + \ldots
\end{eqnarray}

where the primes indicate differentiation.

Subtracting the second
equation from the first yields

\begin{equation}
  f\!\left(x_0+\frac{\delta}{2}\right) -
  f\!\left(x_0-\frac{\delta}{2}\right) =
   \delta f'(x_0) +
        \frac{2}{3!}\left(\frac{\delta}{2}\right)^3 f'''(x_0) + \ldots
\end{equation}

Dividing by $\delta$ produces

\begin{equation}
  \frac{f\!\left(x_0+\frac{\delta}{2}\right) -
        f\!\left(x_0-\frac{\delta}{2}\right)}{\delta} =
   f'(x_0) +
        \frac{1}{3!}\frac{\delta^2}{2^2} f'''(x_0) + \ldots
\end{equation}

Thus the term on the left is equal to the derivative of the function
at the point $x_0$ plus a term which depends on $\delta^2$ plus an
infinite number of other terms which are not shown.  For the terms
which are not shown, the next would depend on $\delta^4$ and all
subsequent terms would depend on even higher powers of $\delta$.

Rearranging slightly, this relationship is often stated as

\begin{equation}
  \left.\frac{df(x)}{dx}\right|_{x=x_0} =
  \frac{f\!\left(x_0+\frac{\delta}{2}\right) -
        f\!\left(x_0-\frac{\delta}{2}\right)}{\delta} + O(\delta^2).
\end{equation}

The "big-Oh" term represents all the terms that are not explicitly
shown and the value in parentheses, i.e., $\delta^2$, indicates the
lowest order of $\delta$ in these hidden terms.  If $\delta$ is
sufficiently small, a reasonable approximation to the derivative may
be obtained by simply neglecting all the terms represented by the
"big-Oh" term.

Thus, the central-difference approximation is given
by

\begin{equation}
  \left.\frac{df(x)}{dx}\right|_{x=x_0} \approx
  \frac{f\!\left(x_0+\frac{\delta}{2}\right) -
        f\!\left(x_0-\frac{\delta}{2}\right)}{\delta}.
\end{equation}

Note that the central difference provides an approximation of the
derivative of the function at $x_0$, but the function is not actually
sampled there.  Instead, the function is sampled at the neighboring
points $x_0+\delta/2$ and $x_0-\delta/2$.  Since the lowest power of
$\delta$ being ignored is second order, the central difference is said
to have second-order accuracy or second-order behavior.  This implies
that if $\delta$ is reduced by a factor of $10$, the error in the
approximation should be reduced by a factor of $100$ (at least
approximately).  In the limit as $\delta$ goes to zero, the
approximation becomes exact.

## Applying central differing
The next step is to replace the derivatives in
the previous equations with finite
differences.  To do this, space and time need to be discretized.  The
following notation will be used to indicate the location where the
fields are sampled in space and time

\begin{eqnarray}
  E_z(x,t) \!&=&\! E_z(m\Delta_x, q\Delta_t) = E_z^{q}\!\left[m\right], \\
  H_y(x,t) \!&=&\! H_y(m\Delta_x, q\Delta_t) = H_y^{q}\!\left[m\right],
\end{eqnarray}

where $\Delta_x$ is the spatial offset between sample points and $\Delta_t$
is the temporal offset.  The index $m$ corresponds to the spatial
step, effectively the spatial location, while the index $q$
corresponds to the temporal step.  When written as a superscript $q$
still represents the temporal step---it is not an exponent.  When
implementing FDTD algorithms we will see that the spatial indices are
used as array indices while the temporal index, which is essentially a
global parameter, is not explicitly specified for each field location.
Hence, it is reasonable to keep the spatial indices as an explicit
argument while indicating the temporal index separately.

## Gridding
Although we only have one spatial dimension, time can be thought of as another dimension. Thus this is effectively a form of two-dimensional problem. The question now is: How should the electric and magnetic field sample points, also known as nodes, be arranged in space and time? The answer is shown in the figure below. The electric-field nodes are shown as circles and the magnetic- field nodes as triangles. Assume that all the fields below the dashed line are known—they are considered to be in the past—while the fields above the dashed line are future fields and hence unknown. The FDTD algorithm provides a way to obtain the future fields from the past fields.

The arrangement of electric- and magnetic-field nodes in space and time. The electric- field nodes are shown as circles and the magnetic-field nodes as triangles. The indicated point is where the difference equation is expanded to obtain an update equation for $H_y$.

Consider Faraday's
law at the space-time point $((m+1/2)\Delta_x,q\Delta_t)$

\begin{equation}
  \left.\mu
  \frac{\partial H_y}{\partial t}\right|_{(m+1/2)\Delta_x,q\Delta_t}
  =
  \left.\frac{\partial E_z}{\partial x}\right|_{(m+1/2)\Delta_x,q\Delta_t}.
  \label{eq:faradayDiscrete}
\end{equation}

The temporal derivative is replaced by a finite difference involving
$H_y^{q+\frac{1}{2}}\!\left[m+\frac{1}{2}\right]$ and $H_y^{q-\frac{1}{2}}\!\left[m+\frac{1}{2}\right]$
(i.e., the magnetic field at a fixed location but two different times)
while the spatial derivative is replaced by a finite difference
involving $E_z^{q}\!\left[m+1\right]$ and $E_z^{q}\!\left[m\right]$ (i.e., the
electric field at two different locations but one time).  This yields

\begin{equation}
  \mu\frac{H_y^{q+\frac{1}{2}}\!\!\left[m+\frac{1}{2}\right] -
           H_y^{q-\frac{1}{2}}\!\!\left[m+\frac{1}{2}\right]} {\Delta_t} =
  \frac{E_z^{q}\!\left[m+1\right] - E_z^{q}\!\left[m\right]}{\Delta_x}.
  \label{eq:faradayFdtd1D}
\end{equation}

Solving this for $H_y^{q+\frac{1}{2}}\!\!\left[m+\frac{1}{2}\right]$ yields:

\begin{equation}
  H_y^{q+\frac{1}{2}}\!\!\left[m+\frac{1}{2}\right] = H_y^{q-\frac{1}{2}}\!\!\left[m+\frac{1}{2}\right] +
  \frac{\Delta_t}{\mu\Delta_x}
  \left(E_z^{q}\!\left[m+1\right] - E_z^{q}\!\left[m\right]\right).
  \label{eq:updateHy}
\end{equation}

This is known as an update equation, specifically the update equation
for the $H_y$ field.  It is a generic equation which can be applied to
any magnetic-field node.  It shows that the future value of $H_y$
depends on only its previous value and the neighboring electric
fields.

After applying this equation to all the magnetic-field
nodes, the dividing line between future and past values has advanced a
half time-step.  The space-time grid thus appears as shown in the figure below which is identical to the figure above  except for the advancement of the past/future
dividing line.

Space-time after updating the magnetic field. The dividing line between future and past values has moved forward a half temporal step. The indicated point is where the difference equation is written to obtain an update equation for $E_z$.

Starting with Ampere's law, a smilar update equation for the $E_z$ field can be derived:

\begin{equation}
  E_z^{q+1}\!\left[m\right] = E_z^{q}\!\left[m\right] +
  \frac{\Delta_t}{\epsilon\Delta_x}
   \left(H_y^{q+\frac{1}{2}}\!\!\left[m+\frac{1}{2}\right] - H_y^{q+\frac{1}{2}}\!\!\left[m-\frac{1}{2}\right]\right).
\end{equation}

The indices in this equation are generic so that the same
equation holds for every $E_z$ node.  Similar to the update equation for
the magnetic field, here we see that the future value of $E_z$ depends
on only its past value and the value of the neighboring magnetic
fields.

After applying this equation to every electric-field node in the
grid, the dividing line between what is known and what is unknown
moves forward another one-half temporal step.  One is essentially back
to the situation depicted in the first figure: the future
fields closest to the dividing line between the future and past are
magnetics fields.  They would be updated again, then the electric
fields would be updated, and so on.

## Turning this into a computer program

You will have noticed indices are either an integer ($m$) or an integer plus one half ($m+\frac{1}{2}$). While we coud use these indices in a list in some languages, common practice is to simply to use integers.  If we take the figure above, and compress the time axis so that we just view the space axis, we can pick a standard way to express the indices of the $E_z$ and $H_y$ fields as follows (note that we also need to choose variable names that don't involve subscripts).

A one-dimensional FDTD space showing the assumed spatial arrangement of the electric- and magnetic-field nodes in the arrays ez and hy. Note that an electric-field node is assumed to exist to the left of the magnetic-field node with the same index.

And the update equations then are:

```  hy[m] = hy[m] + (ez[m + 1] - ez[m]) / imp0```

and

```  ez[m] = ez[m] + (hy[m] - hy[m - 1]) * imp0```

```imp0``` is the impedance of free space:
$imp_0 = \frac{E_z}{H_y} = \mu_0 c_0 = \sqrt{\frac{\mu_0}{\epsilon_0}} = \frac{1}{\epsilon_0 c_0}$

where
$\mu_0$ is the magnetic constant,
$\epsilon_0$ is the electric constant,
and $c_0$ is the speed of light in free space.

```python

```


###### Content under Creative Commons Attribution license CC-BY 4.0, code under MIT license (c)2014 L.A. Barba, C.D. Cooper, G.F. Forsyth.

# Spreading out

Welcome to the second lesson of the course module: "_Spreading out: parabolic PDEs."_ We're studying the heat equation in one spatial dimension:

$$
\begin{equation}
\frac{\partial T}{\partial t} = \alpha \frac{\partial^2 T}{\partial x^2}
\end{equation}
$$

where $\alpha$ is the thermal diffusivity and $T$ is the temperature.

In the previous lesson, we reviewed the numerical solution of the 1D diffusion equation with a forward-time, centered-space scheme: an _explicit_ scheme. What does that mean?

The solution for $T$ at time step $t^{n+1}$ was calculated using different combinations of $T$ values from the *previous* time step $t^n$.  We have complete knowledge of the parts that feed into the solution update at each spatial point.

*Implicit* methods work differently: we will use more data from the "future" in the update, including several values of $T$ at $t^{n+1}$. This will make the scheme more difficult to apply, but there are several reasons why it may be worth the effort.

In lesson 1, we discussed two disadvantages of explicit methods: (1) boundary effects drag behind by one time step; (2) stability requirements constrain the time-step to very small values. Both of these issues are resolved by implicit schemes.

## Implicit schemes

Let's move things around a bit and try combining the Euler time step with an evaluation of the spatial derivative on the *updated* solution at $t^{n+1}$. The discretized form of the equation is now as follows:

$$
\begin{equation}
\frac{T_{i}^{n+1}-T_{i}^{n}}{\Delta t}=\alpha\frac{T_{i+1}^{n+1}-2T_{i}^{n+1}+T_{i-1}^{n+1}}{\Delta x^2}
\end{equation}
$$

The stencil for this discretization doesn't look anything like the other stencils we've used until now. Check it out.

.
#### Figure 1. Stencil of the implicit central scheme.

From the previous time-step, we only know $T_i^{n}$, but what about $T_i^{n+1}$, $T_{i-1}^{n+1}$ and $T_{i+1}^{n+1}$?  What can we do?

No need to panic! Let's start by putting what we *do know* on the right-hand side of the equation and what we *don't know* on the left. We get:

$$
\begin{equation}
-T_{i-1}^{n+1} + \left( 2 + \frac{\Delta x^2}{\alpha\Delta t}\right) T_{i}^{n+1} - T_{i+1}^{n+1} = T_{i}^{n}\frac{\Delta x^2}{\alpha\Delta t}
\end{equation}
$$

It looks like there are a lot of unknowns and just one equation!

What does it look like with $i=1$?

$$
\begin{equation}
-T_{0}^{n+1} + \left( 2 + \frac{\Delta x^2}{\alpha\Delta t}\right) T_{1}^{n+1} - T_{2}^{n+1} = T_{1}^{n}\frac{\Delta x^2}{\alpha\Delta t}
\end{equation}
$$

and $i=2$?

$$
\begin{equation}
-T_{1}^{n+1} + \left( 2 + \frac{\Delta x^2}{\alpha\Delta t}\right) T_{2}^{n+1} - T_{3}^{n+1} = T_{2}^{n}\frac{\Delta x^2}{\alpha\Delta t}
\end{equation}
$$

What about $i=3$?

$$
\begin{equation}
-T_{2}^{n+1} + \left( 2 + \frac{\Delta x^2}{\alpha\Delta t}\right) T_{3}^{n+1} - T_{4}^{n+1} = T_{3}^{n}\frac{\Delta x^2}{\alpha\Delta t}
\end{equation}
$$

Can you see the common element across equations? Here's a little help:

$T_{i}^{n+1}$ also appears in the equation for $T_{i-1}^{n+1}$ and $T_{i+1}^{n+1}$. We might have enough equations if we apply this for all $i$-values at the same time, don't you think? In fact, this is a linear system of equations for the unknown values $T_{i}^{n+1}$ on the spatial grid.

### What about boundary conditions?

Let's look at the boundary points of the example from the previous lesson with a Dirichlet BC at $x=0$ and a Neumann BC at $x=1$, discretizing with $N$ mesh points.

The value $T_0^{n+1}$ is known at every time-step from the BC, so putting all unknown terms on the left-hand side of the equation and the known values on the right side yields the following for the $i=1$ equation:

$$
\begin{equation}
-T_{2}^{n+1} + \left( 2 + \frac{\Delta x^2}{\alpha\Delta t}\right) T_{1}^{n+1} = T_{1}^{n}\frac{\Delta x^2}{\alpha\Delta t} + T_{0}^{n+1}
\end{equation}
$$

That was easy!

On the other hand, for $i=N-2$, the equation reads

$$
\begin{equation}
-T_{N-3}^{n+1} + \left( 2 + \frac{\Delta x^2}{\alpha\Delta t}\right) T_{N-2}^{n+1} - T_{N-1}^{n+1} = T_{N-2}^{n}\frac{\Delta x^2}{\alpha\Delta t}
\end{equation}
$$

The discretized Neumann boundary condition on the right side of the rod is

$$
\begin{equation}
\frac{T^{n}_{N-1} - T^{n}_{N-2}}{\Delta x} = q
\end{equation}
$$

But we can just as easily write that at time step $n+1$ (the boundary conditions apply at every time-step):

$$
\begin{equation}
\frac{T^{n+1}_{N-1} - T^{n+1}_{N-2}}{\Delta x} = q
\end{equation}
$$

Inserting the Neumann boundary condition in the equation for $i=N-2$ yields

$$
\begin{equation}
    -T_{N-3}^{n+1} + \left( 1 + \frac{\Delta x^2}{\alpha\Delta t} \right) T_{N-2}^{n+1} = T_{N-2}^{n} \frac{\Delta x^2}{\alpha\Delta t} + \Delta x q
\end{equation}
$$

Make sure you work this out with pen and paper: it's important to recognize where these terms come from!

Now we can write the linear system of equations in matrix form as follows:

$$
[A][x] = [b]+[b]_{b.c.}
$$

where the matrix of coefficients $[A]$ is a sparse matrix—most of the matrix elements are zero—with three non-zero diagonals. We write below the system expanded out, so you can see the structure of the matrix, with $\sigma=\frac{\alpha\Delta t}{\Delta x^2}$:

$$
\begin{align}
    \left[
        \begin{array}{cccccc}
            \left( 2 + \frac{1}{\sigma} \right) & -1 & 0 & \cdots & & 0 \\
            -1 & \left( 2 + \frac{1}{\sigma} \right) & -1 & 0 & \cdots & 0 \\
            0 & & \ddots & & & \vdots \\
            \vdots & & & & \left( 2 + \frac{1}{\sigma} \right) & \\
            0 & \cdots & & & -1 & \left( 1 + \frac{1}{\sigma} \right)
        \end{array}
    \right] \cdot
    \left[
        \begin{array}{c}
            T_1^{n+1} \\
            T_2^{n+1} \\
            \vdots \\ \\
            T_{N-2}^{n+1}
        \end{array}
    \right] =
    \left[
        \begin{array}{c}
            T_1^n \frac{1}{\sigma} \\
            T_2^{n} \frac{1}{\sigma} \\
            \vdots \\ \\
            T_{N-2}^{n} \frac{1}{\sigma}
        \end{array}
    \right] +
    \begin{bmatrix}
        T_0^{n+1} \\
        0 \\
        \vdots \\
        0 \\
        q\Delta x
    \end{bmatrix}
\end{align}
$$

Notice that the Dirichlet boundary condition adds only a term to the right-hand side of the system.  The Neumann boundary condition both adds a term to the right-hand side and modifies the matrix $[A]$.

### Problem set up

We'll re-use the problem from lesson 1: we have a graphite rod, with [thermal diffusivity](http://en.wikipedia.org/wiki/Thermal_diffusivity) $\alpha=1.22\times10^{-3} {\rm m}^2/{\rm s}$, length $L=1{\rm m}$, and temperature held at $T=100{\rm C}$ on the left end, $x=0$, and $0{\rm C}$ everywhere else initially. We'll compute the evolution of temperature on the length of the rod.

Let's start like we did in the previous lesson: import your libraries and set up the discretization.

```python
import numpy
from matplotlib import pyplot
%matplotlib inline
```

```python
# Set the font family and size to use for Matplotlib figures.
pyplot.rcParams['font.family'] = 'serif'
pyplot.rcParams['font.size'] = 16
```

```python
# Set parameters.
L = 1.0  # length of the rod
nx = 51  # number of locations on the rod
dx = L / (nx - 1)  # distance between two consecutive locations
alpha = 1.22e-3  #  thermal diffusivity of the rod
q = 0.0  # temperature gradient on the right side of the rod

# Define the locations along the rod.
x = numpy.linspace(0.0, L, num=nx)

# Set the initial temperature along the rod.
T0 = numpy.zeros(nx)
T0[0] = 100.0
```

### Solving a linear system

We need to solve the linear system of equations written above to advance the solution in time. Luckily, we can rely on our friends from SciPy who have developed some nice linear solvers, so we don't need to write our own.

From `scipy.linalg`, let's import `solve`: a function to solve linear systems. Make sure to explore the documentation of [`scipy.linalg`](https://docs.scipy.org/doc/scipy/reference/linalg.html). We'll need to define our own custom functions to generate the coefficient matrix and the right-hand side of the linear system. You should carefully study the code below.

```python
from scipy import linalg
```

```python
def lhs_operator(N, sigma):
    """
    Computes and returns the implicit operator
    of the system for the 1D diffusion equation.
    We use backward Euler method, Dirichlet condition
    on the left side of the domain and zero-gradient
    Neumann condition on the right side.

    Parameters
    ----------
    N : integer
        Number of interior points.
    sigma : float
        Value of alpha * dt / dx**2.

    Returns
    -------
    A : numpy.ndarray
        The implicit operator as a 2D array of floats
        of size N by N.
    """
    # Setup the diagonal of the operator.
    D = numpy.diag((2.0 + 1.0 / sigma) * numpy.ones(N))
    # Setup the Neumann condition for the last element.
    D[-1, -1] = 1.0 + 1.0 / sigma
    # Setup the upper diagonal of the operator.
    U = numpy.diag(-1.0 * numpy.ones(N - 1), k=1)
    # Setup the lower diagonal of the operator.
    L = numpy.diag(-1.0 * numpy.ones(N - 1), k=-1)
    # Assemble the operator.
    A = D + U + L
    return A
```

```python
def rhs_vector(T, sigma, qdx):
    """
    Computes and returns the right-hand side of the system
    for the 1D diffusion equation, using a Dirichlet condition
    on the left side and a Neumann condition on the right side.

    Parameters
    ----------
    T : numpy.ndarray
        The temperature distribution as a 1D array of floats.
    sigma : float
        Value of alpha * dt / dx**2.
    qdx : float
        Value of the temperature flux at the right side.

    Returns
    -------
    b : numpy.ndarray
        The right-hand side of the system as a 1D array of floats.
    """
    b = T[1:-1] / sigma
    # Set Dirichlet condition.
    b[0] += T[0]
    # Set Neumann condition.
    b[-1] += qdx
    return b
```

Next, we'll define a function that steps in time using the implicit central-space scheme.  Remember that for an implicit method, a step in time is performed by solving the entire linear system. This is a fundamental difference between implicit and explicit methods, and implies a considerable computational cost.

```python
def btcs_implicit(T0, nt, dt, dx, alpha, q):
    """
    Computes and returns the temperature along the rod
    after a given number of time steps.

    The function uses Euler implicit in time,
    central differencing in space, a Dirichlet condition
    on the left side, and a Neumann condition on the
    right side.

    Parameters
    ----------
    T0 : numpy.ndarray
        The initial temperature distribution as a 1D array of floats.
    nt : integer
        Number of time steps to compute.
    dt : float
        Time-step size.
    dx : float
        Distance between two consecutive locations.
    alpha : float
        Thermal diffusivity of the rod.
    q : float
        Value of the temperature gradient on the right side.

    Returns
    -------
    T : numpy.ndarray
        The temperature distribution as a 1D array of floats.
    """
    sigma = alpha * dt / dx**2
    # Create the implicit operator of the system.
    A = lhs_operator(len(T0) - 2, sigma)
    # Integrate in time.
    T = T0.copy()
    for n in range(nt):
        # Generate the right-hand side of the system.
        b = rhs_vector(T, sigma, q * dx)
        # Solve the system with scipy.linalg.solve.
        T[1:-1] = linalg.solve(A, b)
        # Apply the Neumann boundary condition.
        T[-1] = T[-2] + q * dx
    return T
```

We solve the linear system for every time step, but the $A$ matrix does not change. Thus, you can generate it only once and then use it for all time steps. Let's try this out!

```python
# Set the time-step size based on CFL limit.
sigma = 0.5
dt = sigma * dx**2 / alpha  # time-step size
nt = 1000  # number of time steps to compute

# Compute the temperature along the rod.
T = btcs_implicit(T0, nt, dt, dx, alpha, q)
```

Now plot the solution!

```python
# Plot the temperature along the rod.
pyplot.figure(figsize=(6.0, 4.0))
pyplot.xlabel('Distance [m]')
pyplot.ylabel('Temperature [C]')
pyplot.grid()
pyplot.plot(x, T, color='C0', linestyle='-', linewidth=2)
pyplot.xlim(0.0, L)
pyplot.ylim(0.0, 100.0);
```

Not too impressive, this looks just like the result from *explicit* forward in time, centered in space for $\alpha\frac{\Delta t}{\Delta x^2} = \frac{1}{2}$.

But try $\alpha\frac{\Delta t}{\Delta x^2} = 5$, which violates the stability condition of the *explicit* scheme:

```python
# Increase the CFL number.
sigma = 5.0
dt = sigma * dx**2 / alpha  # time-step size
nt = 100  # number of time steps to compute

# Compute the temperature along the rod.
T = btcs_implicit(T0, nt, dt, dx, alpha, q)

# Plot the temperature along the rod.
pyplot.figure(figsize=(6.0, 4.0))
pyplot.xlabel('Distance [m]')
pyplot.ylabel('Temperature [C]')
pyplot.grid()
pyplot.plot(x, T, color='C0', linestyle='-', linewidth=2)
pyplot.xlim(0.0, L)
pyplot.ylim(0.0, 100.0);
```

**It didn't blow up!**

We were not able to use such a large time step with the explicit scheme. You can try out other values of `sigma` and you'll get a stable solution. In fact, this is an *unconditionally stable* scheme—the most valuable feature of implicit methods is that they give stable solutions without a constraint on the choice of time step.

Using the implicit scheme, we can always advance in time using larger time steps. But each time step requires the solution of a linear system, which is computationally expensive. This is the trade-off between explicit and implicit methods.
To experiment further, set different values of the Neumann boundary flux and see if the solution behaves as you expect.

##### A word of warning

Implicit methods allow you to use significantly larger time steps, because they are not subject to stability constraints. But that doesn't mean you can use just _any_ large time step! Remember that Euler's method is a first-order method, so the _accuracy_ gets worse as you increase the time step, in direct proportion. In fact, you can lose the ability to capture the correct physics if your time step is too large. Numerical stability does not imply accuracy!

##### Dig deeper

You see how matrix `A` is mostly full of zeros?  We call such a matrix *sparse*, and there are many ways to make more efficient calculations taking advantage of their particular structure. First of all, you can optimize the memory usage. Check out SciPy's [sparse-matrix storage formats](https://docs.scipy.org/doc/scipy/reference/sparse.html): you don't need too store $(N-2)^2$ elements! For example, a `coo_matrix` format stores only $3*N_\text{nonzero}$, where $N_\text{nonzero}$ is the number of non-zero elements in `A`. Make sure to explore this topic a little more. It's an important topic in numerical PDEs.

---
###### The cell below loads the style of the notebook

```python
from IPython.core.display import HTML
css_file = '../../styles/numericalmoocstyle.css'
HTML(open(css_file, 'r').read())
```

<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Arvo:400,700,400italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=PT+Mono' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Shadows+Into+Light' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Nixie+One' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Source+Code+Pro' rel='stylesheet' type='text/css'>
<style>

@font-face {
    font-family: "Computer Modern";
    src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
}

#notebook_panel { /* main background */
    background: rgb(245,245,245);
}

div.cell { /* set cell width */
    width: 750px;
}

div #notebook { /* centre the content */
    background: #fff; /* white background for content */
    width: 1000px;
    margin: auto;
    padding-left: 0em;
}

#notebook li { /* More space between bullet points */
    margin-top:0.8em;
}

/* draw border around running cells */
div.cell.border-box-sizing.code_cell.running {
    border: 1px solid #111;
}

/* Put a solid color box around each cell and its output, visually linking them*/
div.cell.code_cell {
    background-color: rgb(256,256,256);
    border-radius: 0px;
    padding: 0.5em;
    margin-left:1em;
    margin-top: 1em;
}

div.text_cell_render{
    font-family: 'Alegreya Sans' sans-serif;
    line-height: 140%;
    font-size: 125%;
    font-weight: 400;
    width:600px;
    margin-left:auto;
    margin-right:auto;
}

/* Formatting for header cells */
.text_cell_render h1 {
    font-family: 'Nixie One', serif;
    font-style:regular;
    font-weight: 400;
    font-size: 45pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.5em;
    margin-top: 0.5em;
    display: block;
}

.text_cell_render h2 {
    font-family: 'Nixie One', serif;
    font-weight: 400;
    font-size: 30pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.1em;
    margin-top: 0.3em;
    display: block;
}

.text_cell_render h3 {
    font-family: 'Nixie One', serif;
    margin-top:16px;
    font-size: 22pt;
    font-weight: 600;
    margin-bottom: 3px;
    font-style: regular;
    color: rgb(102,102,0);
}

.text_cell_render h4 {    /*Use this for captions*/
    font-family: 'Nixie One', serif;
    font-size: 14pt;
    text-align: center;
    margin-top: 0em;
    margin-bottom: 2em;
    font-style: regular;
}

.text_cell_render h5 {  /*Use this for small titles*/
    font-family: 'Nixie One', sans-serif;
    font-weight: 400;
    font-size: 16pt;
    color: rgb(163,0,0);
    font-style: italic;
    margin-bottom: .1em;
    margin-top: 0.8em;
    display: block;
}

.text_cell_render h6 { /*use this for copyright note*/
    font-family: 'PT Mono', sans-serif;
    font-weight: 300;
    font-size: 9pt;
    line-height: 100%;
    color: grey;
    margin-bottom: 1px;
    margin-top: 1px;
}

.CodeMirror{
    font-family: "Source Code Pro";
    font-size: 90%;
}

.alert-box {
    padding:10px 10px 10px 36px;
    margin:5px;
}

.success {
    color:#666600;
    background:rgb(240,242,229);
}
</style>


## depth averaged log-law in open channel

### 開水路において水深平均化された対数則の導出

まず、完全粗面の円管流れの対数則による流速分布を示すと次のとおりとなる。

\begin{align}
\frac{u}{u_*}  = \frac{1}{\kappa}\log_e{\frac{z}{k}} + A_r = \frac{1}{\kappa \log_{10}{e}}\log_{10}{\frac{z}{k}}+ A_r
\end{align}

ここに、$\kappa$:カルマン定数、$k$:粗度高さ、$z$：壁面からの距離である。
また、$A_r$はNikuradseの実験より8.5である。

次に水深平均流速$V$に対する対数則を導出する。上式を水深積分すると次のとおりである。

\begin{align}
\frac{V}{u_*} &= \frac{1}{h}\int_0^h \frac{u}{u_*} dz \nonumber \\
 &= \frac{1}{\kappa}\log_e{\frac{h}{k}} - \frac{1}{\kappa} + A_r  \\
 &= \frac{1}{\kappa \log_{10}{e}}\log_{10}{\frac{h}{k}}- \frac{1}{\kappa}+ A_r
\end{align}

これが開水路において水深平均化された対数則である。

取り扱いやすいように次式のように変形することがある。

\begin{align}
\frac{V}{u_*} &= \frac{1}{\kappa}\log_e{\frac{h}{k}} - \frac{1}{\kappa} + A_r  = \frac{1}{\kappa}\log_e \left({e^{\kappa A_r-1}\frac{h}{k}} \right)
\end{align}

また、自然河道を対象とする場合水深$h$を径深$R$とする。

### manning-stricklerによる近似形

manning則を用いて式変形を行うと流速係数$\phi = V/u_*$は以下のように示される。
本式は自然河道の場合$\phi$が8～25であることを条件とした近似式である。

\begin{align}
\phi &= \frac{V}{u_*} = \frac{k^{1/6}}{n \sqrt{g}} \left(\frac{R}{k} \right)^{1/6} \nonumber \\
\frac{k^{1/6}}{n \sqrt{g}} & \sim 7.66
\end{align}

### 対数則について思うこと

教科書等では開水路の対数則は以下のような式形が多く見られる。

\begin{align}
\frac{V}{u_*}  =  5.75\log_{10}{\frac{h}{k}}+ 6.0
\end{align}

この書き方をすると定数がたくさんあるように見えるのでやめて欲しいです。前項の書き方のほうが実験定数は$A_r$のみであることがわかりやすいと思う。

### 参考：対数則の導出

乱流のせん断応力は次式で示される。

\begin{align}
\tau_0 = \rho (\nu + \epsilon) \dfrac{du}{dz}
\end{align}

Prandtlの混合距離の考え方を用いると、

\begin{align}
\epsilon &= l^2\left|\dfrac{du}{dz}\right| \\
l &= \kappa z
\end{align}

となる。
さらに、$\nu \ll \epsilon$および$u_* = \sqrt{\tau_0/\rho}$を用いると次のように整理できる。

\begin{align}
u_*^2 &= \kappa^2 z^2 \left( \dfrac{du}{dz} \right)^2 \\
\dfrac{du}{dz} &= \dfrac{u_*}{\kappa z}
\end{align}

この式を$z=0$から$z$まで積分して、$z=0$の流速を$A_r u_*$と与えると冒頭の対数則が得られる。

### 参考：対数則の数値計算上のテクニック

数値計算で対数則を用いる場合、$h$が$k$以下となることがある。
この領域では本来対数則を用いることはできないが、便宜的に次のように取り扱う。

\begin{align}
 \frac{V}{u_*} = \begin{cases}
    \dfrac{1}{\kappa}\left(\log_e{\dfrac{h}{k}} - 1\right) + A_r & \left(\log_e{\dfrac{h}{k}} \ge 1\right) \\
     A_r & \left(\log_e{\dfrac{h}{k}} \lt 1 \right) \\
  \end{cases}
\end{align}

これにより、河床近傍の流速が$A_r u_*$となることを満足する。

### 参考文献

水理学 1 椿東一郎
https://www.amazon.co.jp/dp/4627420609


# Instrucciones

- El desafío es individual, por lo cual se considera que todo el código entregado es de elaboración propia y no ha sido compartido de forma alguna.
- Las consultas sobre el desafío se deben realizar por medio de la plataforma Aula. **No está permitido publicar código en consultas de Aula**.
- El desafío debe ser realizado en Jupyter Notebook (Python3) utilizando este archivo como base.
- Debe utilizar arreglos de ```NumPy``` junto a las funciones y bibliotecas que se ponen a disposición en este archivo.
- Se debe respetar las firmas de las funciones, sus parámetros y retornos. Para eso se incluye un detalle de cada función tanto de las entregadas como las que deberá implementar. En caso de no seguir estas instrucciones, el desafío no podrá evaluarse.
- Se evaluará la correcta utilización de librerias ```NumPy```, ```SciPy```, entre otras, así como la correcta implementación de algoritmos de forma vectorizada.
- Evitar la impresión de mensajes salvo que se solicite en el enunciado.
- El archivo de entrega debe denominarse **ROL-desafio-numero.ipynb**. **De no respetarse este formato existirá un descuento de 50 puntos**
- La fecha de entrega es el **martes 16 de junio a las 12:00 hrs**,  posteriormente existirá un descuento lineal hasta las 13:00 hrs del mismo día.

# Interpolación Polinomial
# Introducción

En este desafío estudiaremos la **matriz de Vandermonde** como método de interporlación. Sea $P(x)$ el polinomio de interpolación definido como:

\begin{equation}
    P(x) = a_0x^0 + a_1x^1 + \cdots + a_{n-1}x^{n-1}= \sum_{i=0}^{n-1}a_ix^i,
\end{equation}

entonces la **matriz de Vandermonde** emerge del problema de obtener los coeficientes $a_i$ para $n$ puntos $(x_i, y_i)$.
Por ejemplo si tenemos los puntos $(x_1, y_1), (x_2, y_2), \cdots, (x_n, y_n)$, podemos generar las siguientes $n$ ecuaciones:

\begin{equation}
    \begin{split}
        a_0x_1^0 + a_1x_1^1 + \cdots + a_{n-1}x_1^{n-1} &= y_1 \\
        a_0x_2^0 + a_1x_2^1 + \cdots + a_{n-1}x_2^{n-1} &= y_2 \\
            \vdots     & \\
        a_0x_n^0 + a_1x_n^1 + \cdots + a_{n-1}x_n^{n-1} &= y_n \\
    \end{split},
\end{equation}

entonces la **matriz de Vandermonde** queda definida como:

\begin{equation}
    V =
    \begin{bmatrix}
        1 & x_1 & x_1^2 & \cdots & x_1^{n-1} \\
        1 & x_2 & x_2^2 & \cdots & x_2^{n-1} \\
        \vdots & \vdots & \vdots & & \vdots \\
        1 & x_n & x_n^2 & \cdots & x_n^{n-1} \\
    \end{bmatrix}.
\end{equation}

Luego para obtener los coeficientes $a_i$, debemos resolver el siguiente problema:

\begin{equation}
    \begin{split}
        V\, \mathbf{a} & = \mathbf{y} \\
        \begin{bmatrix}
        1 & x_1 & x_1^2 & \cdots & x_1^{n-1} \\
        1 & x_2 & x_2^2 & \cdots & x_2^{n-1} \\
        \vdots & \vdots & \vdots & & \vdots \\
        1 & x_n & x_n^2 & \cdots & x_n^{n-1} \\
    \end{bmatrix}
    \begin{bmatrix}
        a_0 \\ a_1 \\ \vdots \\ a_{n-1}
    \end{bmatrix} & =
    \begin{bmatrix}
        y_1 \\ y_2 \\ \vdots \\ y_{n}
    \end{bmatrix}
    \end{split}
\end{equation}

```python
import numpy as np
import matplotlib.pyplot as plt
```

Su primera tarea será implementar la función llamada ```interpolateVandermonde(x, y)``` que reciba como input un arreglo con los puntos $x_i$ y otro arreglo con los puntos $y_i$. Luego, su función deberá realizar una interpolación utilizando la matriz de Vandermonde y finalmente retornar un arreglo con los coeficientes resultantes de la interpolación.

```python
def interpolateVandermonde(x, y):
    """
    Compute Polynomial coefficients using Vandermonde Matrix

    Parameters
    -----------
    x     : (n, ) array
            Data x_i to interpolate
    y     : (n, ) array
            Data y_i to interpolate

    Returns
    -------
    a     : (n, ) array
            Coefficients of the interpolating polynomial
    """
    a = ...
    return a
```

Construya la función ```interpolatingPolynomial(a)``` que reciba como **input** los coeficientes $a_i$ obtenidos de la función ```interpolateVandermonde(x, y)```. Su implementación debe retornar una función que permita evaluar el polinomio $P(x)$ para cualquier valor de $x$. Asegúrese que su función $P(x)$ pueda evaluar arreglos ```numpy.array```.

```python
def interpolatingPolynomial(a):
    """
    Build interpolating polynomial using a coefficients

    Parameters
    -----------
    a     : (n, ) array
            Coefficients of the interpolating polynomial

    Returns
    -------
    P     : Function
            Interpolation polynomial P(x)
    """
    P = ...
    return P
```

Suponga que generamos una grilla fina a partir de $x$ que denominaremos $x_e$. Diremos de esta forma que nuestra función numérica $f(x)$ es el vector $\mathbf{y}=f(x_e)$, y el vector $\mathbf{p}=P(x_e)$ corresponde a nuestro vector obtenido por función interpoladora $P(x)$.
Para poder analizar la calidad de la interpolación se pone a diposición la función ```error(y, p)``` que recibe como **input** el vector $\mathbf{y}$ y el vector $\mathbf{p}$. Esta función tiene como **output** el error definido como:

\begin{equation}
    error_{int} = \frac{\|\mathbf{y} - \mathbf{p}\|_{\infty}}{\|\mathbf{y}\|_{\infty}}
\end{equation}

```python
def error(y, p):
    """
    Compute interpolation error

    Parameters
    -----------
    y     : (n, ) array
            y = f(x)
    p     : (n, ) array
            p = P(x)

    Returns
    -------
    err   : float
            Interpolation error
    """
    return np.linalg.norm(y - p, np.inf) / np.linalg.norm(y, np.inf)
```

# Pruebas

## LEER POR FAVOR

Todo lo que aparece **desde esta celda hacia abajo** se incluyó para que pueda probar sus implementaciones y ver sus resultados. Una vez que verifique que está funcionando correctamente **POR FAVOR COMENTAR LOS CÓDIGOS QUE GENEREN GRÁFICOS**.

Para que pueda probar su implementación se ponen a disposición las siguientes funciones:

```python
f1 = lambda x: 2 * x + 3
f2 = lambda x: np.sin(x) ** 3
f3 = lambda x: np.exp(x)
f4 = lambda x: np.exp(-x ** 2)
f5 = lambda x: np.sinc(x)
```

## Datos a interpolar

Aquí puede escoger $f_i$, $i=\{1, 2, \dots, 5\}$.

```python
x_a, x_b = -2 * np.pi, 2 * np.pi # Domain
f = f2 # Select function to evaluate
```

Se generarán puntos equispaciados $x_i$ en un intervalo $[x_a,x_b]$ y luego se evaluarán dichos puntos en una función $f$ para generar puntos $y_i$.

```python
N_i = 10
x_i = np.linspace(x_a, x_b, N_i)
y_i = f(x_i)
```

## Interpolación

Acá debe realizar su interpolación.

```python
a = interpolateVandermonde(x_i, y_i)
P = interpolatingPolynomial(a)
```

## Datos a evaluar

Se genera una grilla fina $\mathbf{x}$ para poder evaluar la calidad de su interpolación.

```python
N_e = 200
x_e = np.linspace(x_a, x_b, N_e)
y_e = f(x_e)
```

## Visualización

Gráfico de $f(x)$ y $P(x)$.

```python
plt.figure(figsize=(12, 6))
plt.plot(x_e, y_e, 'b-', label="f(x)")
plt.plot(x_i, y_i, 'rd', label="Puntos para interpolar")
plt.plot(x_e, P(x_e), '.-', label=r'$P(x)$')
plt.grid(True)
plt.legend()
plt.xlabel(r"$x$")
plt.ylabel(r"$y$")
plt.show()
```

```python
N = 10
Ns = 10 * np.arange(1, N + 1) # Number of nodes for interpolation
errors = np.zeros(Ns.shape[-1])
```

```python
# Compute error between interpolating polynomial and "real function"
for i in range(Ns.shape[-1]):
    # Interpolation nodes
    x_i = np.linspace(x_a, x_b, Ns[i])
    y_i = f(x_i)
    # Interpolation
    a = interpolateVandermonde(x_i, y_i)
    P = interpolatingPolynomial(a)
    p = P(x_e) # Polynomial evaluation
    errors[i] = error(y_e, p)
```

```python
plt.figure(figsize=(12, 6))
plt.plot(Ns, errors, 'b-.')
plt.grid(True)
plt.yscale('log')
plt.ylabel("Error")
plt.xlabel("Grado Polinomio")
plt.show()
```


# Scientific computing

- Numpy: advanced array operations, multidimensional arrays
- Scipy: scientific computing by examples
    - singular value decomposition, with scipy.linalg
    - scipy.signal and scipy.fftpack: Signal theory
    - scipy.optimize: Local and global optimization, fitting and root finding
    - scipy.interpolate: Cubic interpolation
    - scipy.integrate: Integration and ODE solvers
    - scipy.ndimage - Image processing
- Simpy: symbolic math

## Numpy

- using numpy improves RAM space and speed
- numpy enforces strong typing, while Python is a dynamic typed language
- translates in Numpy using less heap space for representing data
- multidimensional array operations are the core of scientific computing

Further reading:
- https://docs.scipy.org/doc/numpy/user/basics.html

```python
import numpy as np
L = range(1000)
%timeit [i**2 for i in L]
a = np.arange(1000)
%timeit a**2
print(type(L[1]))
print(a.dtype)
```

    293 µs ± 1.46 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
    1.46 µs ± 19.3 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
    <class 'int'>
    int32

```python
import numpy as np
##Get help!
#np.lookfor('create array')
#np.array?
#np.arr*?
```

```python
a = np.array([0, 1, 2, 3])
print("a:\n", a)
b = np.array([[0, 1, 2], [3, 4, 5]])
print("b:\n", b)

print("b.shape:\n", b.shape)
```

    a:
     [0 1 2 3]
    b:
     [[0 1 2]
     [3 4 5]]
    b.shape:
     (2, 3)

#### Data types

There are 5 basic numerical types representing booleans (bool), integers (int), unsigned integers (uint) floating point (float) and complex. Those with numbers in their name indicate the bitsize of the type (i.e. how many bits are needed to represent a single value in memory).

```python
print("np.float32(1.0) :", np.float32(1.0))
print("np.arange(3, dtype=np.uint8) :", np.arange(3, dtype=np.uint8))
```

    np.float32(1.0) : 1.0
    np.arange(3, dtype=np.uint8) : [0 1 2]

```python
z = np.array([1, 2, 3], dtype='f')
print(z)
```

    [1. 2. 3.]

```python
z = np.arange(3, dtype=np.uint8)
print(z)
print(z.astype(float))
print(z.dtype)
```

    [0 1 2]
    [0. 1. 2.]
    uint8

#### Array creation

```python
# extrinsic
x = np.array([2,3,1,0])
print(x)
print()
x = np.array([[ 1., 2.], [ 0., 0.], [ 1., 3.]])
print(x)
```

    [2 3 1 0]

    [[1. 2.]
     [0. 0.]
     [1. 3.]]

```python
#intrinsic
b = np.arange(1, 9, 2)
print(b)
c = np.linspace(0, 1, 6)
print(c)
```

    [1 3 5 7]
    [0.  0.2 0.4 0.6 0.8 1. ]

```python
print(np.arange(35).reshape(5,7))
```

    [[ 0  1  2  3  4  5  6]
     [ 7  8  9 10 11 12 13]
     [14 15 16 17 18 19 20]
     [21 22 23 24 25 26 27]
     [28 29 30 31 32 33 34]]

```python
x = np.random.rand(35).reshape(5,7)
print(x)
```

    [[0.9670321  0.41898259 0.20363724 0.35449615 0.16035759 0.71796865
      0.91560078]
     [0.7941912  0.11327121 0.39895543 0.1938332  0.25015112 0.7301424
      0.8902718 ]
     [0.38559724 0.80936697 0.87522364 0.2348649  0.50799691 0.96275272
      0.25653361]
     [0.11539546 0.29497229 0.82328139 0.47741048 0.08609102 0.39704512
      0.5578069 ]
     [0.72175903 0.77159655 0.4940947  0.16196267 0.405733   0.1140635
      0.94903288]]

```python
%pylab inline
import matplotlib.pyplot as plt

image = np.random.rand(30, 30)
plt.imshow(image, cmap=plt.cm.hot)
plt.colorbar()
```

#### Indexing, slicing and selection

```python
a = np.arange(10)
print(a)
print(a[0], a[2], a[-1], a[-3])
print(a[2:5], a[2:], a[:-2], a[::2], a[2::2])
```

    [0 1 2 3 4 5 6 7 8 9]
    0 2 9 7
    [2 3 4] [2 3 4 5 6 7 8 9] [0 1 2 3 4 5 6 7] [0 2 4 6 8] [2 4 6 8]

```python
a = np.diag(np.arange(3))
a[2, 1] = 10 # !third line, !second column

print(a)
print(a[1, 1])
#print(a[1])
#print(a[:,1], a[1,:])
#print(a[1:,1:])
```

    [[ 0  0  0]
     [ 0  1  0]
     [ 0 10  2]]
    1

```python
# array indexes
x = np.arange(10,1,-1)
print(x)
print()
print(x[np.array([3,3,-3,8])])
print()
print(x[np.array([[1,1],[2,3]])])
```

    [10  9  8  7  6  5  4  3  2]

    [7 7 4 2]

    [[9 9]
     [8 7]]

```python
# 10 random numbers 0 - 20
a = np.random.randint(0, 20, 10)
print(a)
print(a%3==0)
print(a[a%3==0])

a[a % 3 == 0] = -1
print(a)
```

    [18 10  3 15 12  1  6  2  3  4]
    [ True False  True  True  True False  True False  True False]
    [18  3 15 12  6  3]
    [-1 10 -1 -1 -1  1 -1  2 -1  4]

Task:
- What does this do:

```python
# How does it work?
# Print the primes!
def get_primes():
    primes = np.ones((100,), dtype=bool)
    primes[:2] = 0
    N_max = int(np.sqrt(len(primes)))
    for j in range(2, N_max):
        primes[2*j::j] = 0
    return primes
print(get_primes())

```

    [False False  True  True False  True False  True False False False  True
     False  True False False False  True False  True False False False  True
     False False False False False  True False  True False False False False
     False  True False False False  True False  True False False False  True
     False False False False False  True False False False False False  True
     False  True False False False False False  True False False False  True
     False  True False False False False False  True False False False  True
     False False False False False  True False False False False False False
     False  True False False]

#### Broadcasting, assignment, structured arrays

```python
a = np.arange(10)
b = a
print(np.may_share_memory(a, b))

a = np.arange(10)
c = a.copy()  # force a copy
print(np.may_share_memory(a, c))
```

    True
    False

```python
#Array operations
a = np.array([1, 2, 3, 4])
print("a: ", a)
print("a + 1, 2**a: ", a + 1, 2**a)
print ("2**(a + 1) - a: ", 2**(a + 1) - a)
```

    a:  [1 2 3 4]
    a + 1, 2**a:  [2 3 4 5] [ 2  4  8 16]
    2**(a + 1) - a:  [ 3  6 13 28]

```python
a = np.array([1, 2, 3, 4])
b = np.ones(4)+1
print("a: ",a)
print("b: ",b)
print("a - b, a * b: ", a - b, a * b)
```

    a:  [1 2 3 4]
    b:  [2. 2. 2. 2.]
    a - b, a * b:  [-1.  0.  1.  2.] [2. 4. 6. 8.]

```python
c = np.ones((3, 3))
print(c)
print(2*c + 1)
```

    [[1. 1. 1.]
     [1. 1. 1.]
     [1. 1. 1.]]
    [[3. 3. 3.]
     [3. 3. 3.]
     [3. 3. 3.]]

```python
# matrix multiplication
a = np.ones((3, 2)) + 1
b = np.ones((2, 3)) + 1
c = a.dot(b)
print(a, b, c, sep="\n\n")
```

    [[2. 2.]
     [2. 2.]
     [2. 2.]]

    [[2. 2. 2.]
     [2. 2. 2.]]

    [[8. 8. 8.]
     [8. 8. 8.]
     [8. 8. 8.]]

```python
a = np.arange(5)
print(np.sin(a))
print(np.log(a))
print(np.exp(a))
```

    [ 0.          0.84147098  0.90929743  0.14112001 -0.7568025 ]
    [      -inf 0.         0.69314718 1.09861229 1.38629436]
    [ 1.          2.71828183  7.3890561  20.08553692 54.59815003]

    D:\programs\anaconda\envs\biopy37\lib\site-packages\ipykernel_launcher.py:3: RuntimeWarning: divide by zero encountered in log
      This is separate from the ipykernel package so we can avoid doing imports until

```python
# shape manipulation
x = np.array([1, 2, 3])
y = x[:, np.newaxis]
z = x[np.newaxis, :]
print(x, y, z, sep='\n\n')
```

    [1 2 3]

    [[1]
     [2]
     [3]]

    [[1 2 3]]

```python
# flatten
a = np.array([[1, 2, 3], [4, 5, 6]])
print(a)
print(a.ravel())
```

    [[1 2 3]
     [4 5 6]]
    [1 2 3 4 5 6]

```python
# sorting matrices
a = np.array([[4, 3, 5], [1, 2, 1]])
b = np.sort(a, axis=1) #sorting per row
print(a)
print(b)
```

    [[4 3 5]
     [1 2 1]]
    [[3 4 5]
     [1 1 2]]

```python
# sorting arguments
a = np.array([4, 3, 1, 2])
j = np.argsort(a)
print(j, a[j])
```

    [2 3 1 0] [1 2 3 4]

#### Reductions

```python
# unidimensional
x = np.array([1, 2, 3, 4])
print(np.sum(x), x.sum(), x.sum(axis=0))
```

    10 10 10

```python
# multidimensional
x = np.array([[1, 1], [2, 2]])
print(x)
print(x.sum(axis=0))   # rows (first dimension)
print(x.sum(axis=1))   # columns (second dimension)
```

    [[1 1]
     [2 2]]
    [3 3]
    [2 4]

```python
x = np.array([1, 3, 2])
print(x)
print(x.min(), x.max(), x.argmin(), x.argmax())

print(np.all([True, True, False]), np.any([True, True, False]))

x = np.array([1, 2, 3, 1])
y = np.array([[1, 2, 3], [5, 6, 1]])
print(x.mean(), x.std(), np.median(x), np.median(y, axis=-1))
```

    [1 3 2]
    1 3 0 1
    False True
    1.75 0.82915619758885 1.5 [2. 5.]

```python
a = np.zeros((100, 100))
print(np.any(a != 0))

a = np.array([1, 2, 3, 2])
b = np.array([2, 2, 3, 2])
c = np.array([6, 4, 4, 5])
print(((a <= b) & (b <= c)).all())
```

    False
    True

**Tricksy task:**
- Replace all values greater than 25 with 9 and all values smaller than 10 with 29.

```python
a = np.random.randint(0, 50, 10)
print(a)
```

    [ 3  9 33 19 46 34 39 39 32 27]

### Sympy

Symbolic math is sometimes important, especially if we are weak at calculus or if we need to perform automated calculus on long formulas. We are briefly going through a few test cases, to get the feel of it. Symbolic math is especially developed for [Mathematica](http://www.wolfram.com/mathematica/?source=nav), or [Sage](http://www.sagemath.org/) which is an open-source equivalent.

```python
import sympy
print sympy.sqrt(8)
import math
print math.sqrt(8)
```

    2*sqrt(2)
    2.82842712475

```python
from sympy import symbols
x, y, z, t = symbols('x y z t')
expr = x + 2*y
print expr
print x * expr
from sympy import expand, factor, simplify
expanded_expr = expand(x*expr)
print expanded_expr
print factor(expanded_expr)
exp = expanded_expr.subs(x, z**t)
print exp
print simplify(exp)
```

    x + 2*y
    x*(x + 2*y)
    x**2 + 2*x*y
    x*(x + 2*y)
    2*y*z**t + z**(2*t)
    z**t*(2*y + z**t)

In the scipy.optimize paragraph we needed the Hessian matrix for a function f. Here is how you can obtain it in sympy:

```python
import sympy
x, y = sympy.symbols('x y')
f = .5*(1 - x)**2 + (y - x**2)**2
h = sympy.hessian(f, [x,y])
print(h)
from IPython.display import Latex
Latex(sympy.latex(h))

```

    Matrix([[12*x**2 - 4*y + 1.0, -4*x], [-4*x, 2]])

\left[\begin{matrix}12 x^{2} - 4 y + 1.0 & - 4 x\\- 4 x & 2\end{matrix}\right]

```python
from IPython.display import HTML
HTML('')
```

```python

```


# Simple pendulum using Lagrange's equation

Defines a LagrangianPendulum class that is used to generate basic pendulum plots from solving Lagrange's equations.

* Last revised 17-Mar-2019 by Dick Furnstahl (furnstahl.1@osu.edu).

## Euler-Lagrange equation

For a simple pendulum, the Lagrangian with generalized coordinate $\phi$ is

$\begin{align}
  \mathcal{L} = \frac12 m L^2 \dot\phi^2 - mgL(1 - \cos\phi)
\end{align}$

The Euler-Lagrange equation is

$\begin{align}
 \frac{d}{dt}\frac{\partial\mathcal{L}}{\partial \dot\phi} = \frac{\partial\mathcal L}{\partial\phi}
 \quad\Longrightarrow\quad
 m L^2 \ddot \phi = -mgL\sin\phi
  \ \mbox{or}\ \ddot\phi = - \omega_0^2\sin\phi = 0
  \;.
\end{align}$

## Hamilton's equations

The generalized momentum corresponding to $\phi$ is

$\begin{align}
   \frac{\partial\mathcal{L}}{\partial \dot\phi} = m L^2 \dot\phi \equiv p_\phi
   \;.
\end{align}$

We can invert this equation to find $\dot\phi = p_\phi / m L^2$.
Constructing the Hamiltonian by Legendre transformation we find

$\begin{align}
  \mathcal{H} &= \dot\phi p_\phi  - \mathcal{L} \\
              &= \frac{p_\phi^2}{m L^2} - \frac12 m L^2 \dot\phi^2 + mgL(1 - \cos\phi) \\
              &= \frac{p_\phi^2}{2 m L^2} + mgL(1 - \cos\phi)
              \;.
\end{align}$

Thus $\mathcal{H}$ is simply $T + V$.  Hamilton's equations are

$\begin{align}
  \dot\phi &= \frac{\partial\mathcal{H}}{\partial p_\phi} = \frac{p_\phi}{m L^2} \\
  \dot p_\phi &= -\frac{\partial\mathcal{H}}{\partial \phi} = -mgL \sin\phi
  \;.
\end{align}$

```python
%matplotlib inline
```

```python
import numpy as np
from scipy.integrate import odeint, solve_ivp

import matplotlib.pyplot as plt
```

```python
# The dpi (dots-per-inch) setting will affect the resolution and how large
#  the plots appear on screen and printed.  So you may want/need to adjust
#  the figsize when creating the figure.
plt.rcParams['figure.dpi'] = 100.    # this is the default for notebook

# Change the common font size (smaller when higher dpi)
font_size = 10
plt.rcParams.update({'font.size': font_size})
```

## Pendulum class and utility functions

```python
class LagrangianPendulum():
    """
    Pendulum class implements the parameters and Lagrange's equations for
     a simple pendulum (no driving or damping).

    Parameters
    ----------
    L : float
        length of the simple pendulum
    g : float
        gravitational acceleration at the earth's surface
    omega_0 : float
        natural frequency of the pendulum (\sqrt{g/l} where l is the
        pendulum length)
    mass : float
        mass of pendulum

    Methods
    -------
    dy_dt(t, y)
        Returns the right side of the differential equation in vector y,
        given time t and the corresponding value of y.
    """
    def __init__(self, L=1., mass=1., g=1.
                ):
        self.L = L
        self.g = g
        self.omega_0 = np.sqrt(g/L)
        self.mass = mass

    def dy_dt(self, t, y):
        """
        This function returns the right-hand side of the diffeq:
        [dphi/dt d^2phi/dt^2]

        Parameters
        ----------
        t : float
            time
        y : float
            A 2-component vector with y[0] = phi(t) and y[1] = dphi/dt

        Returns
        -------

        """
        return [y[1], -self.omega_0**2 * np.sin(y[0]) ]

    def solve_ode(self, t_pts, phi_0, phi_dot_0,
                  abserr=1.0e-9, relerr=1.0e-9):
        """
        Solve the ODE given initial conditions.
        Specify smaller abserr and relerr to get more precision.
        """
        y = [phi_0, phi_dot_0]
        solution = solve_ivp(self.dy_dt, (t_pts[0], t_pts[-1]),
                             y, t_eval=t_pts,
                             atol=abserr, rtol=relerr)
        phi, phi_dot = solution.y

        return phi, phi_dot
```

```python
def plot_y_vs_x(x, y, axis_labels=None, label=None, title=None,
                color=None, linestyle=None, semilogy=False, loglog=False,
                ax=None):
    """
    Generic plotting function: return a figure axis with a plot of y vs. x,
    with line color and style, title, axis labels, and line label
    """
    if ax is None:        # if the axis object doesn't exist, make one
        ax = plt.gca()

    if (semilogy):
        line, = ax.semilogy(x, y, label=label,
                            color=color, linestyle=linestyle)
    elif (loglog):
        line, = ax.loglog(x, y, label=label,
                          color=color, linestyle=linestyle)
    else:
        line, = ax.plot(x, y, label=label,
                    color=color, linestyle=linestyle)

    if label is not None:    # if a label if passed, show the legend
        ax.legend()
    if title is not None:    # set a title if one if passed
        ax.set_title(title)
    if axis_labels is not None:  # set x-axis and y-axis labels if passed
        ax.set_xlabel(axis_labels[0])
        ax.set_ylabel(axis_labels[1])

    return ax, line
```

```python
def start_stop_indices(t_pts, plot_start, plot_stop):
    start_index = (np.fabs(t_pts-plot_start)).argmin()  # index in t_pts array
    stop_index = (np.fabs(t_pts-plot_stop)).argmin()  # index in t_pts array
    return start_index, stop_index
```

```python

```

## Make simple pendulum plots

```python
# Labels for individual plot axes
phi_vs_time_labels = (r'$t$', r'$\phi(t)$')
phi_dot_vs_time_labels = (r'$t$', r'$d\phi/dt(t)$')
state_space_labels = (r'$\phi$', r'$d\phi/dt$')

# Common plotting time (generate the full time then use slices)
t_start = 0.
t_end = 50.
delta_t = 0.001

t_pts = np.arange(t_start, t_end+delta_t, delta_t)

L = 1.
g = 1.
mass = 1.

# Instantiate a pendulum
p1 = LagrangianPendulum(L=L, g=g, mass=mass)

```

```python
# both plots: same initial conditions
phi_0 = (3./4.)*np.pi
phi_dot_0 = 0.
phi, phi_dot = p1.solve_ode(t_pts, phi_0, phi_dot_0)

# start the plot!
fig = plt.figure(figsize=(15,5))
overall_title = 'Simple pendulum from Lagrangian:  ' + \
                rf' $\omega_0 = {p1.omega_0:.2f},$' + \
                rf'  $\phi_0 = {phi_0:.2f},$' + \
                rf' $\dot\phi_0 = {phi_dot_0:.2f}$' + \
                '\n'     # \n means a new line (adds some space here)
fig.suptitle(overall_title, va='baseline')

# first plot: phi plot
ax_a = fig.add_subplot(1,3,1)

start, stop = start_stop_indices(t_pts, t_start, t_end)
plot_y_vs_x(t_pts[start : stop], phi[start : stop],
            axis_labels=phi_vs_time_labels,
            color='blue',
            label=None,
            title=r'$\phi(t)$',
            ax=ax_a)

# second plot: phi_dot plot
ax_b = fig.add_subplot(1,3,2)

start, stop = start_stop_indices(t_pts, t_start, t_end)
plot_y_vs_x(t_pts[start : stop], phi_dot[start : stop],
            axis_labels=phi_vs_time_labels,
            color='blue',
            label=None,
            title=r'$\dot\phi(t)$',
            ax=ax_b)

# third plot: state space plot from t=30 to t=50
ax_c = fig.add_subplot(1,3,3)

start, stop = start_stop_indices(t_pts, t_start, t_end)
plot_y_vs_x(phi[start : stop], phi_dot[start : stop],
            axis_labels=state_space_labels,
            color='blue',
            label=None,
            title='State space',
            ax=ax_c)

fig.tight_layout()
fig.savefig('simple_pendulum_Lagrange.png', bbox_inches='tight')
```

Now trying the power spectrum, plotting only positive frequencies and cutting off the lower peaks:

```python
start, stop = start_stop_indices(t_pts, t_start, t_end)
signal = phi[start:stop]
power_spectrum = np.abs(np.fft.fft(signal))**2
freqs = np.fft.fftfreq(signal.size, delta_t)
idx = np.argsort(freqs)

fig_ps = plt.figure(figsize=(5,5))
ax_ps = fig_ps.add_subplot(1,1,1)
ax_ps.semilogy(freqs[idx], power_spectrum[idx], color='blue')
ax_ps.set_xlim(0, 1.)
ax_ps.set_ylim(1.e5, 1.e11)
ax_ps.set_xlabel('frequency')
ax_ps.set_title('Power Spectrum')

fig_ps.tight_layout()
```

```python

```