# ¿Cómo medir rendimiento y riesgo en un portafolio? II

> La clase pasada y la presente, están dedicadas a obtener medidas de rendimiento y riesgo en un portafolio.

> Vimos que podemos obtener los rendimientos de un portafolio mediante la relación $r_p=\sum_{i=1}^{n}w_ir_i$, y una vez teniendo los rendimientos del portafolio, lo podemos tratar como un activo individual.

> Por otra parte, vimos que si conocemos los rendimientos esperados de cada activo que conforma el portafolio $E[r_i]$, podemos calcular el rendimiento esperado del portafolio como el promedio ponderado de los rendimientos esperados de los activos $E[r_p]=\sum_{i=1}^{n}w_iE[r_i]$.

> Sin embargo, vimos que esto no es válido para la medida de riesgo (desviación estándar). Es decir, la varianza (o volatilidad, o desviación estándar) no es el promedio ponderado de las varianzas individuales. Anticipamos que esto es clave en el concepto de **diversificación**.

**Objetivos:**
- Medir el riesgo en un portafolio a partir del riesgo de cada uno de los activos que lo conforman.

*Referencias*
- Notas del curso "Portfolio Selection and Risk Management", Rice University, disponible en Coursera.
___

## 1. Midiendo el riesgo en un portafolio

### 1.1. Volatilidad de un portafolio

Retomamos el ejemplo qur veníamos trabajando la clase pasada...

**Ejemplo.** Supongamos que tenemos inversión en activos de Toyota, Walmart y Pfizer. Tenemos cuatro posibles estados económicos:

```python
import numpy as np
import pandas as pd
```

```python
# Creamos tabla
tabla = pd.DataFrame(columns=['Prob', 'Toyota', 'Walmart', 'Pfizer'],
                     index=['Expansion', 'Normal', 'Recesion', 'Depresion'])
tabla.index.name = 'Estado'
tabla['Prob']=np.array([0.1, 0.4, 0.3, 0.2])
tabla['Toyota']=np.array([0.06, 0.075, 0.02, -0.03])
tabla['Walmart']=np.array([0.045, 0.055, 0.04, -0.01])
tabla['Pfizer']=np.array([0.025, -0.005, 0.01, 0.13])

tabla.round(4)
```

      Prob
      Toyota
      Walmart
      Pfizer

      Estado

      Expansion
      0.1
      0.060
      0.045
      0.025

      Normal
      0.4
      0.075
      0.055
      -0.005

      Recesion
      0.3
      0.020
      0.040
      0.010

      Depresion
      0.2
      -0.030
      -0.010
      0.130

```python
## Rendimientos esperados
# Toyota
ErT = (tabla["Prob"] * tabla["Toyota"]).sum()
# Walmart
ErW = (tabla["Prob"] * tabla["Walmart"]).sum()
# Pfizer
ErP = (tabla["Prob"] * tabla["Pfizer"]).sum()
# Mostrar
ErT, ErW, ErP
```

    (0.036, 0.036500000000000005, 0.029500000000000002)

```python
## Volatilidad
# Toyota
sT = (tabla["Prob"] * (tabla["Toyota"] - ErT)**2).sum()**0.5
# Walmart
sW = (tabla["Prob"] * (tabla["Walmart"] - ErW)**2).sum()**0.5
# Pfizer
sP = (tabla["Prob"] * (tabla["Pfizer"] - ErP)**2).sum()**0.5
# Mostrar
sT, sW, sP
```

    (0.04017461885320133, 0.02408837894089181, 0.05111017511220246)

```python
# Portafolio 0.5Toyota+0.5Pfizer
tabla["PortTP"] = 0.5 * tabla["Toyota"] + 0.5 * tabla["Pfizer"]
tabla
```

      Prob
      Toyota
      Walmart
      Pfizer
      PortTP

      Estado

      Expansion
      0.1
      0.060
      0.045
      0.025
      0.0425

      Normal
      0.4
      0.075
      0.055
      -0.005
      0.0350

      Recesion
      0.3
      0.020
      0.040
      0.010
      0.0150

      Depresion
      0.2
      -0.030
      -0.010
      0.130
      0.0500

```python
# Rendimiento portafolio (como activo individual)
ErTP = (tabla["Prob"] * tabla["PortTP"]).sum()
# Rendimiento portafolio (como suma ponderada de rendimientos individuales)
ErTP_ = 0.5 * ErT + 0.5 * ErP
ErTP, ErTP_
```

    (0.03275, 0.03275)

```python
# Volatilidad del portafolio
sTP = (tabla["Prob"] * (tabla["PortTP"] - ErTP)**2).sum()**0.5
sTP
```

    0.012867109232457772

```python
# Notar que sTP < 0.5 * sT + 0.5 * sP
# la volatilidad del portafolio siempre es menor
# a la suma ponderada de las volatilidades individuales
0.5 * sT + 0.5 * sP
```

    0.045642396982701894

**Actividad.** Encontrar la volatilidad del portafolio formado $0.5$ Toyota y $0.5$ Walmart.

```python
# Encontrar los rendimientos del portafolio en cada estado de la economía
tabla["PortTW"] = 0.5 * tabla["Toyota"] + 0.5 * tabla["Walmart"]
tabla
```

      Prob
      Toyota
      Walmart
      Pfizer
      PortTP
      PortTW

      Estado

      Expansion
      0.1
      0.060
      0.045
      0.025
      0.0425
      0.0525

      Normal
      0.4
      0.075
      0.055
      -0.005
      0.0350
      0.0650

      Recesion
      0.3
      0.020
      0.040
      0.010
      0.0150
      0.0300

      Depresion
      0.2
      -0.030
      -0.010
      0.130
      0.0500
      -0.0200

```python
# Encontrar el rendimiento esperado del portafolio
ErTW = (tabla["Prob"] * tabla["PortTW"]).sum()
ErTW
```

    0.036250000000000004

```python
# Encontrar la volatilidad de Toyota, Walmart y el portafolio
sTW = (tabla["Prob"] * (tabla["PortTW"] - ErTW)**2).sum()**0.5
sTW
```

    0.03164747225293041

```python
# Notar que sTW < 0.5 * sT + 0.5 * sW
# la volatilidad del portafolio siempre es menor
# a la suma ponderada de las volatilidades individuales
0.5 * sT + 0.5 * sW
```

    0.03213149889704657

### 1.2. Midiendo el co-movimiento entre instrumentos

- Una vez más, concluimos que la volatilidad (varianza) **NO** es el promedio ponderado de las varianzas individales.

- Por el contrario, la varianza de los rendimientos de un portafolio está afectada por el movimiento relativo de un activo individual respecto a otro.

- Por tanto, necesitamos definir las medidas de **covarianza** y **correlación**, que nos permiten evaluar las fluctuaciones relativas entre los activos.

#### Covarianza:

Es una medida el movimiento relativo entre dos instrumentos.

Matemáticamente, si tenemos dos activos $A_1$ y $A_2$ cuyos rendimientos son $r_1$ y $r_2$, respectivamente, entonces la covarianza de los rendimientos de los activos es

$$\text{cov}(r_1,r_2)=\sigma_{12}=\sum_{j=1}^{m}p_j(r_{1j}-E[r_1])(r_{2j}-E[r_2]).$$

$$\text{cov}(r_2,r_1)=\sigma_{21}=\sum_{j=1}^{m}p_j(r_{2j}-E[r_2])(r_{1j}-E[r_1]) = \sigma_{12}.$$

Podemos notar fácilmente que la covarianza de los rendimientos de un activo con los rendimientos del mismo activo corresponde a la varianza

$$\text{cov}(r_1,r_1)=\sigma_{11}=\sum_{j=1}^{m} p_j(r_{1j}-E[r_1])(r_{1j}-E[r_1])=\sigma_1^2=\text{var}(r_1).$$

**Ejemplo.** Calcular la covarianza entre los rendimientos de Toyota y Pfizer.

```python
# Mostrar tabla
tabla
```

      Prob
      Toyota
      Walmart
      Pfizer
      PortTP
      PortTW

      Estado

      Expansion
      0.1
      0.060
      0.045
      0.025
      0.0425
      0.0525

      Normal
      0.4
      0.075
      0.055
      -0.005
      0.0350
      0.0650

      Recesion
      0.3
      0.020
      0.040
      0.010
      0.0150
      0.0300

      Depresion
      0.2
      -0.030
      -0.010
      0.130
      0.0500
      -0.0200

```python
# Calcular la covarianza
covTP = (tabla["Prob"] * (tabla["Toyota"] - ErT) * (tabla["Pfizer"] - ErP)).sum()
covTP
```

    -0.0017820000000000004

El signo negativo en la covarianza nos dice que los rendimientos se mueven de manera contraria:

```python
from matplotlib import pyplot as plt
```

```python
plt.plot(tabla.index, tabla["Toyota"])
plt.plot(tabla.index, tabla["Pfizer"])
```

**Actividad.** Calcular la covarianza entre los rendimientos de Toyota y Walmart.

```python
# Calcular la covarianza
covTW = (tabla["Prob"] * (tabla["Toyota"] - ErT) * (tabla["Walmart"] - ErW)).sum()
covTW
```

    0.0009060000000000001

El signo positivo en la covarianza nos dice que los rendimientos se mueven en sentidos similares:

```python
plt.plot(tabla.index, tabla["Toyota"])
plt.plot(tabla.index, tabla["Walmart"])
```

¿Qué nos dice este número?
- El signo nos dice las direcciones relativas entre los rendimientos de cada activo. Por ejemplo, la covarianza entre los rendimientos de Toyota y Pfizer es negativa... ver los rendimientos.
- La magnitud de la covarianza no nos dice mucho acerca de la fuerza con que se relacionan o no estos rendimientos.

**Correlación:**

Un posible problema de la covarianza es que la magnitud de esta medida no nos dice mucho acerca de la fuerza de los movimientos relativos. La *correlación* es una medida normalizada del movimiento relativo entre los rendimientos de dos activos.

Matemáticamente,

$$\text{corr}(r_1,r_2)=\rho_{12}=\rho_{21}=\frac{\sigma_{12}}{\sigma_1\sigma_{2}}.$$

$$\sigma_{12} = \rho_{12} \sigma_{1} \sigma_{2}$$

Propiedades:

- Podemos notar fácilmente que la correlación de los rendimientos de un activo con los rendimientos del mismo activo es $1$: $$\text{corr}(r_1,r_1)=\rho_{11}=\frac{\sigma_{11}}{\sigma_1\sigma_1}=\frac{\sigma_{1}^2}{\sigma_1\sigma_1}=1.$$
- El signo de la correlación y la covarianza es el mismo.
- La correlación satisface: $$-1\leq\rho_{12}\leq 1.$$

**Ejemplo.** Calcular la correlación entre los rendimientos de Toyota y Pfizer.

```python
corrTP = covTP / (sT * sP)
corrTP
```

    -0.8678577889660902

```python
sTP, 0.5 * sT + 0.5 * sP
```

    (0.012867109232457772, 0.045642396982701894)

**Actividad.** Calcular la correlación entre los rendimientos de Toyota y Walmart.

```python
corrTW = covTW / (sT * sW)
corrTW
```

    0.9362004766821975

```python
sTW, 0.5 * sT + 0.5 * sW
```

    (0.03164747225293041, 0.03213149889704657)

**Conclusión.**
- Es una medida normalizada de la fluctuación relativa de los rendimientos de dos activos.
- En los ejemplos que vimos, sería conveniente invertir en el portafolio de Toyota y Pfizer puesto que su correlación es negativa, y esto impactaría positivamente en la diversificación del riesgo.

___

## 2. Uniendo todo...

- Entonces, vimos mediante ejemplos que el riesgo en un portafolio se ve afectado significativamente por como los rendimientos de los activos se mueven relativamente.
- Este movimiento relativo lo medimos mediante la covarianza o la correlación.
- Si se mueven de una manera que no están perfectamente correlacionados ($\rho<1$), entonces el riesgo del portafolio siempre será menor que el promedio ponderado de los riesgos individuales.

## Ésta es la razón por la que combinar activos en un portafolio permite diversificar el riesgo...

Entonces, ¿cómo podemos incorporar esta medida en el cálculo de la varianza del portafolio?
-  Ver en el tablero...

$$
\sigma_p^2 = \sum_{i=1}^{n} \sum_{k=1}^{n} w_i w_k \sigma_{ik} = w^T \Sigma w
$$

- ¿Cómo sería para dos activos? ($\sigma_{12} = \rho_{12} \sigma_{1} \sigma_{2}$)

\begin{align}
\sigma_p^2 & = w_1^2 \sigma_1^2 + w_2^2 \sigma_2^2 + 2 w_1 w_2 \sigma_{12} \\
           & = w_1^2 \sigma_1^2 + w_2^2 \sigma_2^2 + 2 w_1 w_2 \rho_{12} \sigma_1 \sigma_2
\end{align}

**Ejemplo.** Calcular por fórmula para el portafolio de Toyota y Pfizer. Comparar.

```python
sTP
```

    0.012867109232457772

```python
w1, w2 = 0.5, 0.5
sTP_ = (w1**2 * sT**2 + w2**2 * sP**2 + 2 * w1 * w2 * covTP)**0.5
sTP_
```

    0.012867109232457763

**Actividad.** Calcular por fórmula para el portafolio de Toyota y Walmart. Comparar.

```python
sTW
```

    0.03164747225293041

```python
w1, w2 = 0.5, 0.5
sTW_ = (w1**2 * sT**2 + w2**2 * sW**2 + 2 * w1 * w2 * covTW)**0.5
sTW_
```

    0.03164747225293041

## 2.1.  Ver en el tablero...
### Matriz de varianza covarianza.
### Matriz de correlación.

# Anuncios parroquiales

## 1. Recordar quiz la siguiente clase. Temas: Clases 6 y 7.
## 2. Tarea: revisar archivo "Tarea4_MidiendoRendimientoRiesgo" en clase.

Created with Jupyter by Esteban Jiménez Rodríguez.


<p align="center">

</p>

## Subsurface Data Analytics

## Interactive Demonstration of Machine Learning Norms

#### Michael Pyrcz, Associate Professor, University of Texas at Austin

##### [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1) | [GeostatsPy](https://github.com/GeostatsGuy/GeostatsPy)

### PGE 383 Exercise: Interactive Demonstration of Machine Learning Norms

Here's a simple workflow, demonstration of predictive machine learning model norms. We use a:

* linear regression model

* 1 preditor feature and 1 response feature

for an high interpretability model/ simple illustration.

#### Norms

Given a vector of error over the $n_{train}$ training data.

\begin{equation}
\Delta y \rightarrow \Delta y_i, \forall i = 1,\dots,n_{train}
\end{equation}

We require a summarization of the error as a single value, this is a norm.

A norm has the following properties:

* norm of a vector maps the vaector values to a summary measures $\rightarrow [0,\infty)$

Common norms include, Manhattan, Euclidean and the general p-norm.

**Manhattan Norm** is defined as:

\begin{equation}
||\Delta y||_1 = \sum_{i=1}^{n_{train}} |\Delta y_i|
\end{equation}

**Euclidean Norm** is defined as:

\begin{equation}
||\Delta y||_2 = \sqrt{ \sum_{i=1}^{n_{train}} \left( \Delta y_i \right)^2 }
\end{equation}

**p-Norm** is defined as:

\begin{equation}
||\Delta y||_p = \left( \sum_{i=1}^{n_{train}} \left( \Delta y_i \right)^p \right)^{\frac{1}{p}}
\end{equation}

#### Workflow Goals

Learn the basics of machine learning training, tuning for model generalization while avoiding model overfit.

This includes:

* Demonstrate model training and tuning by hand with an interactive exercies

* Demonstrate the role of data error in leading to model overfit with complicated models

#### Import Required Packages

We will also need some standard packages. These should have been installed with Anaconda 3.

```python
%matplotlib inline
import sys                                              # supress output to screen for interactive variogram modeling
import io
import numpy as np                                      # arrays and matrix math
import pandas as pd                                     # DataFrames
import matplotlib.pyplot as plt                         # plotting
from scipy.optimize import minimize                     # linear regression training by-hand with variable norms
from ipywidgets import interactive                      # widgets and interactivity
from ipywidgets import widgets
from ipywidgets import Layout
from ipywidgets import Label
from ipywidgets import VBox, HBox
```

#### Declare Functions

We have functions to perform linear regression for any norm. The code was modified from [N. Wouda](https://stackoverflow.com/questions/51883058/l1-norm-instead-of-l2-norm-for-cost-function-in-regression-model).
* I modified the original functions for a general p-norm linear regression method

```python
def predict(X, params):                                 # linear prediction
    return X.dot(params)

def loss_function(params, X, y, p):                     # custom p-norm, linear regression cost function
    return np.sum(np.power(np.abs(y - predict(X, params)),p))
```

#### Interactive Dashboard

This code designed the interactive dashboard, prediction model and plots

```python
# widgets and dashboard
l = widgets.Text(value='                                       Machine Learning Norms Demo, Prof. Michael Pyrcz, The University of Texas at Austin',layout=Layout(width='950px', height='30px'))

p_norm = widgets.FloatSlider(min=0.1, max = 10, value=1.0, step = 0.2, description = '$L^{p}$',orientation='horizontal', style = {'description_width': 'initial'}, continuous_update=False)
n = widgets.IntSlider(min=15, max = 80, value=30, step = 1, description = 'n',orientation='horizontal', style = {'description_width': 'initial'}, continuous_update=False)
std = widgets.FloatSlider(min=0.0, max = .95, value=0.00, step = 0.05, description = 'Error (St.Dev.)',orientation='horizontal',style = {'description_width': 'initial'}, continuous_update=False)
xn = widgets.FloatSlider(min=0, max = 1.0, value=0.5, step = 0.05, description = '$X_{n+1}$',orientation='horizontal',style = {'description_width': 'initial'}, continuous_update=False)
yn = widgets.FloatSlider(min=0, max = 1.0, value=0.5, step = 0.05, description = '$Y_{n+1}$',orientation='horizontal', style = {'description_width': 'initial'}, continuous_update=False)

ui1 = widgets.HBox([p_norm,n,std],)
ui2 = widgets.HBox([xn,yn],)
ui = widgets.VBox([l,ui1,ui2],)

def run_plot(p_norm,n,std,xn,yn):                       # make data, fit models and plot

    np.random.seed(73073)                               # set random number seed for repeatable results

    X_seq = np.linspace(0,100.0,1000)                   # make data and add noise
    X_seq = np.asarray([np.ones((len(X_seq),)), X_seq]).T
    X = np.random.rand(n)*0.5
    y = X*X + 0.0 # fit a parabola
    y = y + np.random.normal(loc = 0.0,scale=std,size=n) # add noise
    X = np.asarray([np.ones((n,)), X]).T                 # concatenate a vector of 1's for the constant term

    X = np.vstack([X,[1,xn]]); y = np.append(y,yn)       # add the user specified data value to X and y

    x0 = [0.5,0.5]                                       # initial guess of model parameters
    p = 2.0
    output_l2 = minimize(loss_function, x0, args=(X, y, p)) # train the L2 norm linear regression model
    p = 1.0
    output_l1 = minimize(loss_function, x0, args=(X, y, p)) # train the L1 norm linear regression model
    p = 3.0
    output_l3 = minimize(loss_function, x0, args=(X, y, p)) # train the L3 norm linear regression model

    p = p_norm
    output_lcust = minimize(loss_function, x0, args=(X, y, p)) # train the p-norm linear regression model

    y_hat_l1 = predict(X_seq, output_l1.x)               # predict over the range of X for all models
    y_hat_l2 = predict(X_seq, output_l2.x)
    y_hat_l3 = predict(X_seq, output_l3.x)
    y_hat_lcust = predict(X_seq, output_lcust.x)

    plt.subplot(111)                                     # plot the results
    plt.scatter(X[:(n-1),1],y[:(n-1)],s=20,facecolor = 'yellow', edgecolor = 'black', alpha = 0.4)
    plt.scatter(X[n,1],y[n],s=40,marker='^',facecolor = 'orange', edgecolor = 'black', alpha = 0.4)
    plt.plot(X_seq[:,1],y_hat_l1,c = 'blue',alpha = 0.3,label = "L1 Norm")
    plt.plot(X_seq[:,1],y_hat_l2,c = 'red',alpha = 0.3,label = "L2 Norm")
    plt.plot(X_seq[:,1],y_hat_l3,c = 'green',alpha = 0.3,label = "L3 Norm")
    plt.plot(X_seq[:,1],y_hat_lcust,c = 'black',alpha = 1.0,label = "L"+ str(p_norm) + " Norm")
    plt.xlabel(r'Predictor Feature, $X_{1}$'); plt.ylabel(r'Response Feature, $y$'); plt.title('Linear Regression with Various Norms')
    plt.xlim([0.0,1.0]); plt.ylim([0.0,1.0])
    plt.legend(loc = 'lower right')

    plt.subplots_adjust(left=0.0, bottom=0.0, right=1.5, top=1.6, wspace=0.9, hspace=0.3)
    plt.show()

# connect the function to make the samples and plot to the widgets
interactive_plot = widgets.interactive_output(run_plot, {'p_norm':p_norm,'n':n,'std':std,'xn':xn,'yn':yn})
interactive_plot.clear_output(wait = True)               # reduce flickering by delaying plot updating
```

### Interactive Machine Learning Norms Demonstation

#### Michael Pyrcz, Associate Professor, The University of Texas at Austin

Observe the impact of choice of norm with variable number of sample data, the data noise, and an outlier!

### The Inputs

* **p-norm** - 1 = Manhattan norm, 2 = Euclidean norm, etc., **n** - number of data, **Error** - random error in standard deviations
* **$x_{n+1}$**, **$y_{n+1}$** - x and y location of an additional data value

```python
display(ui, interactive_plot)                           # display the interactive plot
```

    VBox(children=(Text(value='                                       Machine Learning Norms Demo, Prof. Michael P…

    Output(outputs=({'output_type': 'display_data', 'data': {'text/plain': '<Figure size 432x288 with 1 Axes>', 'i…

#### Comments

This was a basic demonstration of machining learning norms. I have many other demonstrations and even basics of working with DataFrames, ndarrays, univariate statistics, plotting data, declustering, data transformations and many other workflows available at https://github.com/GeostatsGuy/PythonNumericalDemos and https://github.com/GeostatsGuy/GeostatsPy.

#### The Author:

### Michael Pyrcz, Associate Professor, University of Texas at Austin
*Novel Data Analytics, Geostatistics and Machine Learning Subsurface Solutions*

With over 17 years of experience in subsurface consulting, research and development, Michael has returned to academia driven by his passion for teaching and enthusiasm for enhancing engineers' and geoscientists' impact in subsurface resource development.

For more about Michael check out these links:

#### [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

#### Want to Work Together?

I hope this content is helpful to those that want to learn more about subsurface modeling, data analytics and machine learning. Students and working professionals are welcome to participate.

* Want to invite me to visit your company for training, mentoring, project review, workflow design and / or consulting? I'd be happy to drop by and work with you!

* Interested in partnering, supporting my graduate student research or my Subsurface Data Analytics and Machine Learning consortium (co-PIs including Profs. Foster, Torres-Verdin and van Oort)? My research combines data analytics, stochastic modeling and machine learning theory with practice to develop novel methods and workflows to add value. We are solving challenging subsurface problems!

* I can be reached at mpyrcz@austin.utexas.edu.

I'm always happy to discuss,

*Michael*

Michael Pyrcz, Ph.D., P.Eng. Associate Professor The Hildebrand Department of Petroleum and Geosystems Engineering, Bureau of Economic Geology, The Jackson School of Geosciences, The University of Texas at Austin

#### More Resources Available at: [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)


# dyPolyChord paper results

This notebook contains the code used to make the Gaussian mixture model results with `dyPolyChord`. For more information see the dynamic nested sampling paper ([Higson et al., 2017](https://arxiv.org/abs/1704.03459)) and the [dyPolyChord module](https://github.com/ejhigson/dyPolyChord).

This notebook requires nested sampling run data which you can generate using:
* ``make_gaussian_mix_runs.py`` for likelihood_name = 'gaussian_mix_4comp_4sep'.
* ``make_fit_runs.py`` for likelihood_name = 'fit'.

See the module docstrings for information about the random seeding used for the numerical results in the paper.

The ``bsr`` package is also required; it can be downloaded at https://github.com/ejhigson/bsr.

### Select likelihood to run

Must be 'fit' or 'gaussian_mix_4comp_4sep'

```python
likelihood_name = 'fit'
assert likelihood_name in ['gaussian_mix_4comp_4sep', 'fit']
```

### Set up imports

```python
import functools
import numpy as np
import copy
import pandas as pd
import scipy.interpolate
import matplotlib.pyplot as plt
import nestcheck.data_processing
import nestcheck.diagnostics_tables
import nestcheck.estimators as e
import nestcheck.plots
import nestcheck.pandas_functions
import dyPolyChord.output_processing
import dyPolyChord.python_likelihoods as likelihoods
import bsr.plotting
import bsr.data
import bsr.likelihoods
import bsr.basis_functions
import matplotlib
%matplotlib inline
%autoreload 2

# Set plot size, font and fontsize to match LaTeX template
# NB A4 paper is 8.27 × 11.69 inches (=210 × 297 mm)
# STCO
# ----
# Font (=caption font): \OT1/ptm/m/n/10 (= times size 10)
# Footnote font: \OT1/cmr/m/n/10
# Abstract font: \OT1/cmr/m/n/10.95
textwidth = 6.85066 * 0.99  # make 1% smaller to ensure everything fits
textheight = 9.2144 * 0.99  # make 1% smaller to ensure everything fits
colwidth = 3.30719
# Thesis
# ------
# textwidth = 5.1
# Matplotlib setup
# ----------------
fontsize = 8.5  # matplotlib default is 10
matplotlib.rc('text', usetex=True)
matplotlib.rc('font', family='serif', serif='Times New Roman', size=fontsize)

# Get estimator functions
# -----------------------

latex_str_map = {'mathrm{log}': 'log',
                 'std': 'St.Dev.\\',
                 'St.Dev. ': 'St.Dev.\\ ',
                 'mean': 'Mean',
                 'true values': 'Analytic values',
                 'None': ''}
```

### Get fit likelihood object

```python
# set up
fit_func = bsr.basis_functions.gg_1d
nfunc = 4
y_error_sigma = 0.05
x_error_sigma= 0.05
npoints = 120
data = bsr.data.generate_data(
    fit_func, nfunc, y_error_sigma, x_error_sigma=x_error_sigma, npoints=npoints)
fit_likelihood_obj = bsr.likelihoods.FittingLikelihood(
    data, fit_func, nfunc, adaptive=False)
```

### Get estimator function list

```python
def likelihood_calls(run, logw=None, simulate=False):
    return run['output']['nlike']

def fit_mean(ns_run, logw=None, simulate=False, like=fit_likelihood_obj, x1=0.5):
    if logw is None:
        logw = nestcheck.ns_run_utils.get_logw(ns_run, simulate=simulate)
    w_rel = np.exp(logw - logw.max())
    return like.fit_mean(ns_run['theta'], x1)

estimator_list = [likelihood_calls,
                  e.count_samples,
                  e.logz]

estimator_names = ['likelihood calls'] + [e.get_latex_name(est) for est in estimator_list[1:]]

if likelihood_name == 'gaussian_mix_4comp_4sep':
    estimator_list += [e.param_mean,
                       functools.partial(e.param_mean, param_ind=1),
                       functools.partial(e.param_mean, param_ind=2),
                       functools.partial(e.param_mean, param_ind=3),
                       functools.partial(e.param_mean, param_ind=4),
                       functools.partial(e.param_mean, param_ind=5),
                       e.param_squared_mean,
                       functools.partial(e.param_cred, probability=0.5),
                       functools.partial(e.param_cred, probability=0.84),
                       e.r_mean,
                       functools.partial(e.r_cred, probability=0.5),
                       functools.partial(e.r_cred, probability=0.84)]
    estimator_names = ['likelihood calls'] + [e.get_latex_name(est) for est in estimator_list[3:]]
elif likelihood_name == 'fit':
    estimator_list += [functools.partial(e.param_mean, param_ind=ind)
                       for ind in range(0, fit_likelihood_obj.ndim)]
    estimator_names += [r'$\overline{{{}}}$'.format(name[1:-1])
                        for name in fit_likelihood_obj.get_param_latex_names()]
    for x in np.linspace(0.0, 1.0, 11):
        estimator_list += [functools.partial(fit_mean, x1=x)]
        estimator_names += ['$y({:.1f})$'.format(x)]
    for x in [0.25, 0.75]:
        estimator_list += [functools.partial(fit_mean, x1=x)]
        estimator_names += ['$y({:.2f})$'.format(x)]
estimator_names
```

### Analytic Results for Gaussian mixture model

#### Convolutions of Gaussians formulae

From [Petersen and Pedersen (2018)](http://www.cim.mcgill.ca/~dudek/417/Papers/matrixOperations.pdf), Section 8.1.8, the product of two Gaussians is

\begin{equation}
\mathcal{N}_x(\mu_1,\Sigma_1) \times \mathcal{N}_x(\mu_2,\Sigma_2) = c_c\mathcal{N}_x(\mu_c,\Sigma_c),
\end{equation}

where

\begin{align}
c_c & = \mathcal{N}_{\mu_1}(\mu_2,(\Sigma_1+\Sigma_2)), \\
\mu_c & = (\Sigma_1^{-1}+\Sigma_2^{-1})^{-1} (\Sigma_1^{-1}\mu_1 + \Sigma_2^{-1}\mu_2), \\
\Sigma_c & = (\Sigma_1^{-1}+\Sigma_2^{-1})^{-1}.
\end{align}

#### Application to the mixture with a Gaussian prior

The posterior for the mixture of 4 unit Gaussians with a Gaussian prior which was used in the paper can be found by convolving the prior with each component to produce a sum of 4 shifted Gaussians.

In our case we have $\Sigma_m = \mathcal{I}$ for the components and $\Sigma_\pi = \mathcal{I} \frac{1}{\sigma_\pi^2}$, $\mu_\pi = \vec{0}$ for the prior. Hence for each component the convolution with the prior is a spherically symmetric Gaussian with

\begin{equation}
\mu_m = \mu_m \frac{\sigma_\pi^2}{1 + \sigma_\pi^2}$.
\end{equation}

Furthermore, for every component

\begin{equation}
\sigma_c = \frac{\sigma_\pi}{\sqrt{1 + \sigma_\pi^2}}
\end{equation}

and

\begin{equation}
c_c = \mathcal{N}_{0}\left(\mu_1,1 + \frac{1}{\sigma_\pi^2}\right),
\end{equation}

which gives spherically symmetric Gaussian with $\sigma = \sqrt{1 + \sigma_\pi^2}$. Furthermore we have $|\mu|=\mathrm{sep}=4$ for all components so $c_c$ is the same for all components and the Bayesian evidence is

\begin{equation}
\mathcal{Z} = c_c.
\end{equation}

```python
def gaussian_mix_logz(sep=4, dim=10, prior_scale=10):
    rad = np.zeros(dim)
    rad[0] = sep
    const_sigma = np.sqrt(1 + (prior_scale ** 2))
    return likelihoods.Gaussian(sigma=const_sigma)(rad)[0]

def gaussian_mix_means(sep=4, dim=10, prior_scale=10, weights=np.asarray([0.4, 0.3, 0.2, 0.1])):
    positions = [(0, sep), (0, -sep), (sep, 0), (-sep, 0)][:len(weights)]
    factor = prior_scale / np.sqrt(1 + (prior_scale ** 2))
    thetas = np.zeros(dim)
    comp_pos = np.asarray([[0,  4],
                         [0,  -4],
                         [4,  0],
                         [-4, 0]])
    for i in range(2):
        thetas[i] = np.sum(comp_pos[:, i] * weights) * factor
    return thetas

true_values_dict = {e.get_latex_name(e.logz): gaussian_mix_logz()}
param_means = gaussian_mix_means()
for i, val in enumerate(param_means):
    true_values_dict[e.get_latex_name(e.param_mean, param_ind=i)] = val
true_values_dict
```

### Get Data

```python
start_run = 0  # zero indexed
n_run = 20
# Shared Settings
pc_run_dict = {}
method_names = []
for dynamic_goal in [None, 0, 0.25, 1]:
    if dynamic_goal is None:
        key = 'standard'
    else:
        key = 'dynamic $G=' + str(dynamic_goal) + '$'
    method_names.append(key)
    if likelihood_name == 'gaussian_mix_4comp_4sep':
        root = dyPolyChord.output_processing.settings_root(
            likelihood_name,
            'gaussian',  # prior name
            10,  # dimensions
            dynamic_goal=dynamic_goal,
            nrepeats=50,
            prior_scale=10,
            ninit=200, init_step=100,
            nlive_const=500).replace('.', '_')
    elif likelihood_name == 'fit':
        root = fit_likelihood_obj.get_file_root(2000, 400, dynamic_goal=dynamic_goal)
    print(root)
    pc_run_dict[key] = nestcheck.data_processing.batch_process_data(
        [root + '_' + str(i).zfill(3) for i in range(start_run + 1, start_run + n_run + 1)],
        process_func=nestcheck.data_processing.process_polychord_run,
        func_kwargs={}, errors_to_handle=OSError,
        save_name='cache/' + root + '_' + str(start_run + 1) + '_to_' + str(start_run + n_run),
        load=False, save=True)
```

### Plot nlive as a function of Log X

```python
# Calculate logL(logX) and logX(logL) numerically by combining many runs together to get accurate
# logX values for each point logL, then interpolate
comb = nestcheck.ns_run_utils.combine_ns_runs(pc_run_dict['standard'][:100])
logx = nestcheck.ns_run_utils.get_logx(comb['nlive_array'])
logl_given_logx = scipy.interpolate.interp1d(logx, comb['logl'], bounds_error=False, fill_value=-np.inf)
logx_given_logl = scipy.interpolate.interp1d(comb['logl'], logx, bounds_error=False, fill_value=-np.inf)
plot_dict = {}
for method in method_names:
    plot_dict[method] = pc_run_dict[method][:10]
```

```python
# Make the plot
if likelihood_name == 'gaussian_mix_4comp_4sep':
    logx_min = -35
    ymax = 3000
elif likelihood_name == 'fit':
    ymax = 14000
    logx_min = -55
fig = nestcheck.plots.plot_run_nlive(method_names, plot_dict, logx_min=logx_min, ymax=ymax,
                                     logx_given_logl=logx_given_logl,
                                     logl_given_logx=logl_given_logx,
                                     figsize=(textwidth, 2.2))
# For thesis, use left=0.1
adjust_dict = {'left': 0.085, 'right': 0.99, 'bottom': 0.185, 'top': 0.975}
fig.subplots_adjust(**adjust_dict)
fig.savefig('plots/nlive_' + likelihood_name + '.pdf')
```

### Triangle Plot Diagram

This cell makes an accurate plot of the posterior numerically by combining evenly weighted samples from a lot of nested sampling runs. It requires the `getdist` package, which is available on PyPI and at [https://github.com/cmbant/getdist](https://github.com/cmbant/getdist).

```python
param_limits = {}
if likelihood_name == 'gaussian_mix_4comp_4sep':
    names = [r'\theta_\hat{' + str(i) + r'}' for i in range(1, 11)]
    for name in names:
        param_limits[name] = [-8, 8]
    names_to_plot = names[:4]
elif likelihood_name == 'fit':
    names = fit_likelihood_obj.get_param_latex_names()
    names = [name[1:-1] for name in names]
    for name in names:
        param_limits[name] = [0, 1]
    names_to_plot = names[:4]
try:
    from getdist import plots, MCSamples
    np.random.seed(0)
    evens = []
    for run in pc_run_dict['dynamic $G=1$']:
        logw = nestcheck.ns_run_utils.get_logw(run)
        w_rel = np.exp(logw - logw.max())
        temp = run['theta'][np.where(w_rel > np.random.random(w_rel.shape))[0], :]
        evens.append(temp)
    samples = MCSamples(samples=np.vstack(evens), names=names, labels=names)
    g = plots.getSubplotPlotter(width_inch=colwidth)
    g.triangle_plot([samples], names_to_plot, shaded=True, param_limits=param_limits)
    # Save as a png as due to the very large number of samples this takes up less memory than a pdf
    g.fig.savefig('plots/triangle_plot.png', dpi=400)
except ImportError:
    print('Install getdist to make this plot')
```

### Colour contour diagram

```python
if likel
    run = pc_run_dict['dynamic $G=1$'][0]
    print('Run uses {} samples'.format(['logl'].shape[0]))
    fig = bsr.plotting.plot_runs(
        fit_likelihood_obj,
        run,
        nfunc_list=[nfunc], plot_data=True,
        figsize=(6.5, 1.9),
        subplot_height=1.2,
        subplot_width=1.8)
    fig.savefig('plots/fit_fgivenx.pdf')
```

### Results Tables

### Efficiency gain DataFrame

```python
# Generate efficiency gain df
true_values = np.full(len(estimator_names), np.nan)
if likelihood_name == 'gaussian_mix_4comp_4sep':
    include_true_values=True
    include_rmse=True
    for i, name in enumerate(estimator_names):
        try:
            true_values[i] = true_values_dict[name]
        except KeyError:
            pass
elif likelihood_name == 'fit':
    include_true_values=False
    include_rmse=False
method_values = []
for method in method_names:
    method_values.append([nestcheck.ns_run_utils.run_estimators(run, estimator_list)
                          for run in pc_run_dict[method]])
eff_df = nestcheck.pandas_functions.efficiency_gain_df(
    method_names, method_values, estimator_names,
    true_values=true_values, include_true_values=include_true_values,
    include_rmse=include_rmse)
eff_df
```

```python
eff_df.xs('std efficiency gain', level='calculation type')[estimator_names[2:3] + estimator_names[-13:]]
```

### Implementation Errors DataFrame

```python
# diagnostic tests df
error_summary_dict = {}
n_simulate = 10
for method in method_names:
    save_name = ('cache/' + likelihood_name + '_4func_' + method.replace('$', '').replace('=', '_')
                 .replace(' ', '_') + '_' + str(n_simulate) + 'sim')
    print(save_name)
    df_temp = nestcheck.diagnostics_tables.run_list_error_values(
        pc_run_dict[method], estimator_list[1:], estimator_names[1:], n_simulate=n_simulate, parallel=True,
        load=False, save=True, thread_pvalue=False, bs_stat_dist=False, save_name=save_name)
    # error_values_dict[method] = df_temp
    error_summary_dict[method] = nestcheck.diagnostics_tables.error_values_summary(df_temp)
# values_df = pd.concat(error_values_dict)
diag_df = pd.concat(error_summary_dict)
```

```python
# show implementation errors
diag_df.xs('implementation std', level='calculation type')[estimator_names[2:]]
```

### Get paper results tables

```python
# columns to use in paper
if likelihood_name == 'gaussian_mix_4comp_4sep':
    cols = [
        'samples',
        '$\\mathrm{log} \\mathcal{Z}$',
        '$\\overline{\\theta_{\\hat{1}}}$',
        '$\\overline{\\theta_{\\hat{2}}}$',
        '$\\mathrm{median}(\\theta_{\\hat{1}})$',
        '$\\mathrm{C.I.}_{84\\%}(\\theta_{\\hat{1}})$',
        '$\\overline{|\\theta|}$']
elif likelihood_name == 'fit':
    cols = [
        'samples',
        '$\\mathrm{log} \\mathcal{Z}$',
        '$y(0.1)$',
        '$y(0.3)$',
        '$y(0.5)$',
        '$y(0.7)$',
        '$y(0.9)$']
```

```python
# Paper Table 2
paper_eff_df = nestcheck.pandas_functions.paper_format_efficiency_gain_df(eff_df[cols])
try:
    import texunc
    texunc.print_latex_df(paper_eff_df, min_dp=1, min_dp_no_error=2, max_power=5, str_map=latex_str_map)
except ImportError:
    print('Install texunc to get the results tables in the format used in the paper LaTeX file.')
paper_eff_df
```

```python
# Paper Table 3
imp_df = copy.deepcopy(diag_df[cols[1:]].xs('implementation std', level='calculation type', drop_level=False))
row_names = (imp_df.index.get_level_values(1).astype(str) + ' ' +
             imp_df.index.get_level_values(0).astype(str))
row_names = row_names.str.replace('dynamic', '').str.replace('implementation', 'Implementation')
imp_df.index = [row_names, imp_df.index.get_level_values(2)]
try:
    import texunc
    texunc.print_latex_df(imp_df, str_map=latex_str_map)
except ImportError:
    print('Install texunc to get the results tables in the format used in the paper LaTeX file.')
imp_df
```

```python
# Paper Table 7
rmse_df = copy.deepcopy(eff_df.loc[:, ~np.isnan(eff_df.loc[('true values', '' , 'value'), :].values)])
row_names = (rmse_df.index.get_level_values(0).astype(str) + ' ' +
             rmse_df.index.get_level_values(1).astype(str))
row_names = row_names.str.replace('dynamic', '').str.replace('rmse', 'RMSE')
rmse_df.index = [row_names, rmse_df.index.get_level_values(2)]
try:
    import texunc
    texunc.print_latex_df(rmse_df, str_map=latex_str_map,  min_dp_no_error=4, max_power=5)
except ImportError:
    print('Install texunc to get the results tables in the format used in the paper LaTeX file.')
rmse_df
```


# Heat transfer for pipes

```python
"""
importing the necessary libraries, do not modify
"""
%matplotlib inline

from IPython.display import clear_output

import schemdraw as schem
import schemdraw.elements as e

import matplotlib.pyplot as plt
import numpy as np
import math
import scipy.constants as sc

import sympy as sym

```

Fig. 1: Illustration of internal convection.

The above sketch illustrates the focus of this notebook: How to quantify the heat transfer between a pipe, in which a fluid flows, and its surroundings. The heat transfer from the outer surface of the pipe to the outer flow is to defined in the previous chapter, external convection. In the following, this notebook establishes the tools necessary to solve the internal convection problem.

## Entry flow and fully developed internal flow

Fig. 2: Pipe flow nomenclature.

### Python module

For internal flow, the module is loaded as:

```python
from Libraries import HT_internal_convection as intconv

```

As an example, consider the flow of water in a pipe of diameter $D=10$ cm, length $L=10$m. The water thermodynamic properties are estimated at $T_f=50^\circ$C. The bulk velocity is $U_m=2$m/s.

```python
from Libraries import thermodynamics as thermo
T_f = 50 #C
waterflow = thermo.Fluid('water',T_f,"C")

L_pipe = 10. #m
D_pipe = 0.1 #m
Um_pipe = 2 #m/s
```

```python
?intconv.PipeFlow
```

```python
pipe = intconv.PipeFlow(D= D_pipe, L=L_pipe,
                        rho=waterflow.rho, nu=waterflow.nu, Um=Um_pipe)
```

 Fig. 3. Friction factor in pipe flow as a function of Re and relative surface roughness.

A uniform flow entering a pipe (Fig. 2) first experiences streamwise variation of velocity to accommodate the wall boundary conditions. A boundary layer, of thickness $\delta$, forms on the wall and grows until its edge reaches the pipe centerline. This region is the hydrodynamic entrance region. Beyond that point, the flow becomes fully developed, which means that

 In the laminar regime, the velocity profile is only a function of $r$,

 In the turbulent regime, the mean velocity profile is only a function of $r$.

Friction drag or the force exerted by the flow onto the pipe wall governs the pressure gradient necessary to generate a desired flowrate. Calculation of the friction drag leads to the design of the mechanical force creating the pressure gradient. In fully developed (laminar or turbulent) regimes, the pressure gradient may be determined by

$$
-\frac{\Delta\overline{P}}{L}=f\,\frac{1}{D}\,\frac{\rho U_m^2}{2}\;\;\;\;(\mathrm{Eq. 1})
$$

where $D=2R$ and $L$ are the diameter and length of the pipe, respectively, and $f$ is the friction factor. The bulk velocity or average velocity is

$$
U_m=\frac{\dot{m}}{\rho A_c}\;\;\;\;(\mathrm{Eq. 2})
$$

where $\dot{m}$ is the mass flux
$$
\dot{m}=\int_0^{2\pi}\int_0^R\rho \overline{u}(r)\,r\,dr d\theta=2\pi\int_0^R\rho \overline{u}(r)\,r\,dr\;\;\;\;(\mathrm{Eq. 3})
$$
and $A_c=\pi R^2$
The Reynolds number of the flow is based on the bulk velocity and pipe diameter:

$$
Re_D=\frac{\rho U_mD}{\mu}=\frac{4\dot{m}}{\pi D\mu}\;\;\;\;(\mathrm{Eq. 4})
$$

The friction factor in the laminar regime is rigorously derived:
$$
f = \frac{64}{Re_D}\;\;\;\;(\mathrm{Eq. 5})
$$

and is valid up to the critical Reynolds number $Re_{D,c}$, which in most pipe is around 2,000. Be aware that in certain research facilities, the flow can remain laminar for Reynolds numbers up to 10,000. The Reynolds 2,000 is not absolute, universal property, but is the best guess from most engineering applications.

Beyond the critical Reynolds number, $f$ is a function of the roughness to diameter ratio $\varepsilon=e/D$ (e is typically the standard deviation of the roughness height) and the Reynolds number. A trustworthy empirical correlation is the Colebrook formula:

$$
\frac{1}{\sqrt{f}}=-2\log_{10}\left[\frac{\varepsilon}{3.7}+\frac{2.51}{Re_D\sqrt{f}}\right]\;\;\;\;(\mathrm{Eq. 6})
$$

which is solved below for a range of relative roughness $\varepsilon$.

Often there is a need to determine the pump or blower power $P$ necessary to move the flow at a prescribed pressure drop:

$$
P=\frac{\dot{m}}{\rho}\Delta p= \underbrace{(\Delta p)A_c}_\text{force}\cdot U_m\;\;\;\;(\mathrm{Eq. 7})
$$

### Example of functions
Going back to our library, let's explore how to determine some of the properties defined above:

Reynolds number:

```python
print("Re= %1.2e" %pipe.Re)

```

    Re= 3.61e+05

Mass flow rate:

```python
print("mass flowrate= %1.1f kg/s" %pipe.mdot)
```

    mass flowrate= 15.5 kg/s

Compute the friction factor:

```python
pipe.f_turbulent()
# pipe.f_laminar()
```

    Pipe wall is assumed to be hydrodynamically smooth

```python
print("f= %1.5f" %pipe.f)
```

    f= 0.01397

The mean pressure gradient is:

```python
print("-dP/dx= %1.0f Pa/m" %pipe.dPdx)
```

    -dP/dx= 2760 Pa/m

## Heat transfer by internal convection

The temperature is expected to vary both in the streamwise direction and in the radial direction. To reduce the complexity of the problem, we define the mean temperature as:
$$
T_m=\frac{1}{\dot{m}C_p}\int_{A_c}\rho\,u\,C_p\, T\,dA_c\;\;\;\;(\mathrm{Eq. 8})
$$
where $\dot{m}$ is the mass flow rate, $rho$ and $C_p$ are the density and specific heat of the fluid and $A_c$ is the cross-sectional area of the pipe.
### Overall energy balance

Applying the conservation of energy to a pipe flow, between its inlet and outlet, irrespective of its thermal boundary conditions at the wall or its flow condition (turbulent or laminar), yields the general equation for the convection heat rate between the fluid and the pipe's wall:

$$
q_\text{conv}=\dot{m}C_p(T_{m,o}-T_{m,i})\;\;\;\;(\mathrm{Eq. 9})
$$

This equation and others below, derived from this equation, is general and can be useful to design systems with target heat loss or gains.

The local heat flux may be now expressed as:
$$
q_s''=h(T_s-T_m)\;\;\;\;(\mathrm{Eq. 10})
$$
where $h$ is the local convection heat transfer coefficient and $T_s$ is the surface temperature on the inner wall of the pipe. The variation of temperature in the fully developed flow can be shown to be governed by the following ODE:

$$
\frac{dT_m}{dx}=\frac{P}{\dot{m}C_p}h(T_s-T_m)\;\;\;\;(\mathrm{Eq. 11})
$$

where $P$ is the perimeter of the pipe.
If the local heat flux is maintained constant over the length of the pipe $L$, the total heat rate is

$$
q_\text{conv}=(PL)q_s''\, \text{$q_s''=$constant}\;\;\;\;(\mathrm{Eq. 2})
$$

and the streamwise distribution of the mean temperature is linear:
$$
T_m(x)=T_{m,i}+\frac{q_s''P}{\dot{m}C_p}x,\, \text{$q_s''=$constant}\;\;\;\;(\mathrm{Eq. 13})
$$
For the case of constant wall temperature $T_s$, the temperature distribution is the solution of the above ODE, thus of exponential nature. For practical applications, you most always need to compute the overall heat transfer and the outlet mean temperature $T_{m,o}$. The integration of the above ODE for $x=0$ to $x=L$ yields

$$
\frac{T_s-T_{m,o}}{T_s-T_{m,i}}=\exp\left(-\frac{PL}{\dot{m}C_p}\overline{h}\right),\, \text{$T_s=$constant}\;\;\;\;(\mathrm{Eq. 14})
$$

where

$$
\overline{h}=\frac{1}{L}\int_0^L h(x)dx\;\;\;\;(\mathrm{Eq. 15})
$$

If you must compute the mean temperature at $x$ an integration from $0$ to $x$ yields
T_mx_Ts_constant(T_s,T_mi,P,L,mdot,Cp,hbar,x)

$$
\frac{T_s-T_{m}(x)}{T_s-T_{m,i}}=\exp\left(-\frac{PL}{\dot{m}C_p}\overline{h}_x\right),\, \text{$T_s=$constant}\;\;\;\;(\mathrm{Eq. 16})
$$

where
$$
\overline{h}_x=\frac{1}{L}\int_0^x h(x')dx'\;\;\;\;(\mathrm{Eq. 17})
$$
The computation of the total heat transfer rate can be shown to write:

$$
q_\text{conv}=\overline{h}(PL)\Delta T_\text{lm},\, \text{$T_s=$constant}\;\;\;\;(\mathrm{Eq. 18})
$$

with the log mean temperature
log_mean_temperature(T_s,T_o,T_i)

$$
\Delta T_\text{lm}=\cfrac{T_{m,i}-T_{m,o}}{\ln\left(\cfrac{T_s-T_{m,o}}{T_s-T_{m,i}}\right)}\;\;\;\;(\mathrm{Eq. 19})
$$

In many problem, $T_s$ is not defined but the outside ambient temperature $T_\infty$, the thermal conductivity of the pipe is known. One needs to determine the total resistance of the system $R_\text{tot}$, which requires calculating the heat transfer coefficient of the forced or natural convection, occuring on the outside of the pipe, the radiation coefficient if needed, the thermal resistance due by conduction within the pipe, which may include multiple components in the presence of insulation for example, and the internal convection heat transfer coefficient (to be defined below). In such cases, the variation of temperature between inlet and outlet becomes:
T_mo_T_infty(T_infty,T_mi,P,L,mdot,Cp,R_tot)

$$
\frac{T_\infty-T_{m,o}}{T_\infty-T_{m,i}}=\exp\left(-\frac{1}{\dot{m}C_pR_\text{tot}}\right)\;\;\;\;(\mathrm{Eq. 20})
$$

and the total heat transfer rate is

$$
q=\frac{\Delta T_\text{lm}}{R_\text{tot}}\;\;\;\;(\mathrm{Eq. 21})
$$

The equations derived in this cell enable:

 The computation of the internal convection heat transfer coefficient if $T_{m,i}$ and $T_{m,o}$ are known.

 The computation of $T_{m,i}$ or $T_{m,o}$ if one is known and $\overline{h}$ is known

 The computation of the required mass flux to achieve given $T_{m,i}$ and $T_{m,o}$, albeit through an iterative process

## Correlations for convection heat transfer coefficients in internal pipe flows

Here we detailed only the correlations for fully developed flows. For laminar flows, the nusselt numbers are constant, thus the library HT_internal_convection provides directly $\overline{h}$:
laminar_isoflux()

$$
Nu=\frac{hD}{k}=4.36,\, \text{$q_s''=$constant}
$$

laminar_isothermal()

$$
Nu=\frac{hD}{k}=4.36,\, \text{$q_s''=$constant}
$$

```python
pipe.laminar_isoflux()
print("Nu= %1.2f for laminar isoflux" %pipe.Nu)
pipe.laminar_isothermal()
print("Nu= %1.2f for laminar isothermal" %pipe.Nu)
```

    Nu= 4.36 for laminar isoflux
    Nu= 3.66 for laminar isothermal

In turbulent flows, there is a choice of correlations:
Dittus_Boelter(Re,Pr,mode)

$$
Nu=\frac{hD}{k}=0.023Re^{4/5}Pr^n
$$

with mode being either 'cooling' or 'heating'

```python
pipe.Dittus_Boelter(mode='cooling',Pr=waterflow.Pr)
print("Nu= %1.0f for cooling" %pipe.Nu)
pipe.Dittus_Boelter(mode='heating',Pr=waterflow.Pr)
print("Nu= %1.0f for heating" %pipe.Nu)
```

    Nu= 940 for cooling
    Nu= 1067 for heating

Sieder_Tate(Re,Pr,mu,mu_s)

$$
Nu=\frac{hD}{k}=0.027Re^{4/5}Pr^{1/3}\left(\cfrac{\mu}{\mu_s}\right)^{0.14}
$$

```python
T_s = 75 #C
watersurface = thermo.Fluid('water',thermo.C2K(T_s))
pipe.Sieder_Tate(mu=waterflow.mu,mu_s=watersurface.mu,Pr=waterflow.Pr)
print("Nu= %1.0f" %pipe.Nu)
```

    Nu= 1213

Gnielinski(Re,Pr,f)

$$
Nu=\frac{hD}{k}=\frac{(f/8)(Re-1000)Pr}{1+12.7(f/8)^{1/2}(Pr^{2/3}-1)}
$$

```python
pipe.Gnielinski(f=pipe.f, Pr=waterflow.Pr)
print("Nu= %1.0f" %pipe.Nu)
```

    Nu= 1311

Skupinski(Re,Pr)

$$
Nu=\frac{hD}{k}=4.82+0.0185\left(Re\,Pr\right)^{0.827},\, \text{$q_s''=$constant}
$$

```python
pipe.Skupinski(Pr=waterflow.Pr)
print("Nu= %1.0f" %pipe.Nu)
```

    Nu= 2089

Seban(Re,Pr)

$$
Nu=\frac{hD}{k}=5.0+0.025\left(Re\,Pr\right)^{0.8},\, \text{$T_s=$constant}
$$

```python
pipe.Seban(Pr=waterflow.Pr)
print("Nu= %1.0f" %pipe.Nu)
```

    Nu= 1931

## Natural convection around cylinder

Fig. 4: Illustration of the flow induced by natural convection around a cylinder. Insert shows the angular distribution of the local Nu.

In a fluid entirely at rest, a heated surface transfers its heat via pure conduction. Natural convection is the enhanced heat transfer between a body of fluid at rest (at infinity) and a heated surface through the creation of a convective flow driven by buoyancy forces. Fig. 4 illustrates a natural convection flow occuring around a cylinder. The fluid at the bottom of the cylinder $\theta=0$ becomes buoyant through heat transfer between the cylinder and the fluid and rises along the surface of the cylinder. This process creates two boundary layers that merge at $\theta = \pi$ to create a vertical jet-like flow, also called a plume. Plumes are characteristic flows of natural convection, i.e. they are found irrespective of the geometry of the heated object.

The library is called in the following way:

```python
from Libraries import HT_natural_convection as natconv
```

The non-dimensional numbers relevant to natural convection are:
the Grashof number
Grashof(g,beta,DT,D,nu)

$$
Gr = \frac{g\beta(\Delta T)D^3}{\nu^2}
$$

and the Rayleigh number
Rayleigh(g,beta,DT,D,nu,alpha)

$$
Ra = Gr.Pr= \frac{g\beta(\Delta T)D^3}{\nu\alpha}
$$

where $g$ is the gravity magnitude, $\beta$ is the volumetric thermal expansion coefficient at a given pressure $p$
$$
\beta = -\frac{1}{\rho}\left(\frac{\partial\rho}{\partial T}\right)_p
$$
$\Delta T$ is the absolute temperature difference between the heated surface temperature $T_s$ and the fluid temperature at infinity $T_\infty$, $\Delta T= \vert T_s-T_\infty\vert$, $D$ is the characteristic length of the system (here the diameter) and $\nu$ and $\alpha$ are the kinematic viscosity and the thermal diffusivity, both of dimensions $\text{m$^2$/s}$.
Note that for the ideal gas law
$$
p =\rho \frac{R}{M}T\text{ or } \rho = \frac{p}{\frac{R}{M}T}
$$
thus the expansion coefficient is

$$
\beta = \frac{1}{T}\text{ for an ideal gas, $T$ in K}
$$

For a liquid, $\beta$ must be interpolated from a table. All thermodynamics quantities involved are to be defined at the film temperature which is the arithmetic mean

$$
T_f=\frac{T_s+T_\infty}{2}
$$

```python
#air
T_infty = 10#C
T_s = 50#C
D = 0.1#m
T_f = (T_s+T_infty)/2
airflow = thermo.Fluid('air',T_f,"C")
Gr= natconv.Gr(beta=airflow.beta,D=D,DT=T_s-T_infty,nu=airflow.nu)
print('Natural convection Gr= %1.2e'%Gr)
Ra= natconv.Ra(alpha=airflow.alpha,beta=airflow.beta,D=D,DT=T_s-T_infty,nu=airflow.nu)
print('Natural convection Ra= %1.2e'%Ra)
```

    Natural convection Gr= 5.03e+06
    Natural convection Ra= 3.57e+06

The Grashof and Rayleigh number quantify the ratio of buoyancy to viscous forces. When they are large enough, a convective flow sets in and the heat transfer increases in comparison to pure conduction. The Nusselt number, ratio of convective to conduction heat transfer (i.e. $>1$ in the presence of a convection flow) is typically a power law of the Rayleigh number. In the case of the flow around a cylinder with isothermal surface temperature, there are two correlations:
Morgan(Ra)

$$
\overline{Nu}=\frac{\overline{h}D}{k}=C\,Ra^n
$$

Churchill-Chu(Ra,Pr)

$$
\overline{Nu}=\frac{\overline{h}D}{k}=\left[0.60+\frac{0.387Ra^{1/6}}{\left[1+\left(\frac{0.559}
{Pr}\right)^{9/16}\right]^{8/27}}
\right]^2
$$

Both are valid for $Ra\leq10^{12}$. The Nusselt is averaged over the perimeter of the cylinder to account for the angular variation of heat transfer discussed earlier. The heat transfer from natural convection from a heated cylinder of diameter $D$ and length $L$ is

$$
q=\overline{h}(\pi DL)(T_s-T_\infty)=\frac{1}{R_\text{th,conv}}(T_s-T_\infty)
$$

where $R_\text{th,conv}$ may computed with R_th_convection(h,A)

```python
airnatconv = natconv.HorizontalCylinder(correlation='Morgan',Ra=Ra)
print("Morgan correlation: Nu= %1.2f" %airnatconv.Nu)
airnatconv = natconv.HorizontalCylinder(correlation='Churchill-Chu',Ra=Ra,Pr=airflow.Pr)
print("Churchill-Chu correlation: Nu= %1.2f" %airnatconv.Nu)
```

    Morgan correlation: Nu= 20.87
    Churchill-Chu correlation: Nu= 20.91

```python
font = {'family' : 'serif',
        #'color'  : 'black',
        'weight' : 'normal',
        'size'   : 14,
        }

from matplotlib.ticker import FormatStrFormatter
plt.rc('font', **font)
N = 100
Ra = np.logspace(5,12,N)
Nu_Morgan = np.zeros(N)
Nu_ChurchillChu = np.zeros(N)
Pr = 1.0
for i in range(N):
    flow = natconv.HorizontalCylinder(correlation='Morgan',Ra=Ra[i])
    Nu_Morgan[i] = flow.Nu
    flow = natconv.HorizontalCylinder(correlation='Churchill-Chu',Ra=Ra[i],Pr=Pr)
    Nu_ChurchillChu[i] = flow.Nu

plt.loglog(Ra,Nu_Morgan, label = r"Morgan",lw = 2)
plt.loglog(Ra,Nu_ChurchillChu, label = r"Churchill-Chu", lw= 2)
plt.xlabel(r"$Ra$")
plt.ylabel(r"$Nu$")
plt.legend(loc=3, bbox_to_anchor=[0., 1.01], ncol=2, shadow=False, fancybox=True)
plt.show()
```

```python
plt.plot(Ra,np.abs(Nu_Morgan-Nu_ChurchillChu)/Nu_ChurchillChu,lw = 2)
plt.xlabel(r"$Ra$")
plt.ylabel(r"$\vert Nu_{M}-Nu_{CC}\vert/Nu_{CC}$")
plt.show()
```

## Assignment

 Read this entire notebook. Using the textbook, add restrictions and range of validity for the above correlations when applicable. Add the entry length Nu correlation for laminar flow

 Add a section on entrance flow

 How should the entrance flow region be treated in turbulent flows?

Solve 8.31, 8.36, 8.43

### 8.31

To cool a summer home without using a vapor-compression refrigeration cycle, air is routed through a plastic pipe ($k=0.15\text{ W/m.K}$, $D_i=0.15\text{ m}$, $D_o=0.17\text{ m}$) that is submerged in an adjoini
ng body of water. The water temperature is nominally at $T_\infty= 17^\circ\text{C}$, and a convection coefﬁcient of $h_o\approx 1500\text{ W/m$^2$. K}$ is maintained at the outer surface of the pipe.

If air from the home enters the pipe at a temperature of $T_{m,i}= 29^\circ\text{C}$ and a volumetric ﬂow rate of $\dot{\forall}_i= 0.025\text{ m$^3$/s}$, what pipe length $L$ is needed to provide a discharge temperature of $T_{m,o}=21^\circ\text{C}$? What is the fan power required
to move the air through this length of pipe if its inner surface is smooth?

#### Solution

The length of the pipe is the given by solving
$$
\frac{T_\infty-T_{m,o}}{T_\infty-T_{m,i}}=\exp\left(-\frac{1}{\dot{m}C_pR_\text{tot}}\right)
$$
for the target outlet temperature $T_{m,o}$. First, assuming 1D, steady convection on the outside of the pipe, we must solve for $R'_{tot}$. Since
$$
R_{tot}=\frac{R'_{tot}}{L}
$$
the pipe length is
$$
L=-\dot{m}C_pR'_\text{tot}\ln\frac{T_\infty-T_{m,o}}{T_\infty-T_{m,i}}
$$

```python
from Libraries import HT_thermal_resistance as res

Rp = []
Rp.append(res.Resistance("$R'_{conv,i}$","W/m"))
Rp.append(res.Resistance("$R'_{cond,pipe}$","W/m"))
Rp.append(res.Resistance("$R'_{conv,o}$","W/m"))

d = schem.Drawing()
d.add(e.DOT, label = r"$T_{m,i}$")
d.add(e.RES, d = 'right', label = Rp[0].name)
d.add(e.DOT, label = r"$T_{s,i}$")
R1 = d.add(e.RES, d = 'right', label = Rp[1].name)
d.add(e.DOT, label = r"$T_{s,o}$")
d.add(e.RES, d='right', label = Rp[2].name)
d.add(e.DOT, label="$T_\infty$")
L1 = d.add(e.LINE, toplabel = "$q'$", endpts = [[-2.25, 0], [-0.25, 0]])
d.labelI(L1, arrowofst = 0)
d.draw()
```

```python
from Libraries import thermodynamics as thermo
from Libraries import HT_internal_convection as intconv

k_pipe = 0.15 #W/m.K
Di = 0.15 #m
Do = 0.17 #m
T_infty = 17. #C
h_o = 1500 #W/m^2.K
T_mi = 29 #C
T_mo = 21 #C

Qdot = 0.025 #m^3/s
T_m = (T_mi + T_mo)/2

airi = thermo.Fluid('air',T_mi,"C")
airm = thermo.Fluid('air', T_m,"C")
airflow = intconv.PipeFlow(D=Di, L = 1., mdot = airi.rho*Qdot, nu = airm.nu, rho = airi.rho)
airflow.Dittus_Boelter(mode='cooling',Pr=airm.Pr)
print("Re=%.0f" %airflow.Re)
print("Nu=%.0f" %airflow.Nu)
hbar_i = airflow.Nu*airm.k/Di
print("hbar,i=%.2f W/m^2.K" %hbar_i)
Rp[0].convection(hbar_i,np.pi*Di)
Rp[1].cond_cylinder(k = k_pipe,ra=Di,rb=Do,L=1)
Rp[2].convection(400,A=np.pi*Do)

Rptot = 0
for i in range(3):
    Rptot += Rp[i].R

# def L_given_other_params(T_infty,T_mo,T_mi,mdot,Cp,Rptot):
#     return -mdot*Cp*Rptot*np.log((T_infty -T_mo)/(T_infty - T_mi))
# L = L_given_other_params(T_infty,T_mo,T_mi,airi.rho*Qdot,airm.Cp,Rptot)
L = intconv.L_given_other_params(T_infty,T_mo,T_mi,airi.rho*Qdot,airm.Cp,Rptot)
print("Length needed to achieve T_mo=%.0f C is %.1f m" %(T_mo,L))
```

    Re=13625
    Nu=42
    hbar,i=7.32 W/m^2.K
    Length needed to achieve T_mo=21 C is 13.8 m

```python
from Libraries import HT_natural_convection as natconv
T_f = (T_infty + T_m)/2
water = thermo.Fluid("water",T_f,"C")
Ra = natconv.Ra(beta=water.beta,DT=T_m - T_infty, D=Do,nu=water.nu,alpha = water.alpha)
print("Ra=%.2e" %(Ra))
waterconv = natconv.HorizontalCylinder("Churchill-Chu",Ra,water.Pr)
print("Nu=%.0f" %waterconv.Nu)
print("For natural convection, h_o=%.0f W/m^2.K" %(waterconv.Nu*water.k/Do))
# waterforced = extconv.CircularCylinder()
```

    Ra=5.94e+08
    Nu=124
    For natural convection, h_o=436 W/m^2.K

This little exercise demonstrates that natural convection does not achieve the cooling capacity assumed in the problem ($h_o=1500\mathrm{W}/\mathrm{m}^2.K$)

```python
from Libraries import HT_natural_convection as natconv
?natconv.HorizontalCylinder
```

### 8.36

Hot water at mean temperature $T_m=50\text{$^\circ$C}$ is routed from one building in which it is generated to an adjoining building in which it is used for space heating. Transfer between the buildings occurs in a steel pipe ($k=60\text{ W/m.K}$) of $100 \text{ mm}$ outside diameter and 8-mm wall thickness. During the winter, representative environmental conditions involve air at $T_\infty= -5^\circ \mathrm{C}$ and $V_\infty=3\text{ m/s}$ in cross ﬂow over the pipe.
Using the Churchill Bernstein and Dittus Boehler correlations, calculate the total heat transfer rate per unit length $q'$, the daily energy cost $Q'=q'\times 24\text{ h/d}$ per meter and the cost per day and per meter assuming an electricity cost of $\text{\$}0.05\text{/kW.h}$.

**FYI:** This is the Churchill-Bernstein correlation which you can call with the `from Libraries import HT_external_convection as extconv` `airflow=extconv.CircularCylinder('Churchill-Bernstein',Re,Pr)`
$$
Nu_D = \frac{hD}{k_f}=0.3+\frac{0.62Re_D^{1/2}Pr^{1/3}}{\left[1+\left(\frac{0.4}{Pr}\right)^{2/3}\right]^{1/4}}\left[1+\left(\frac{Re_D}{282,000}\right)^{5/8}\right]^{4/5}
$$

The heat transfer problem in any cross sectional area of the pipe is
$$
q' = \frac{T_m - T _\infty}{R'_{tot}}
$$

with

$$
R'_{tot}= R'_{conv,int} + R'_{cond,p}+R'_{conv,ext}
$$

We must find the convection coefficients $h_{int}$ and $h_{ext}$, using the appropriate correlations.

```python
Tm = 50 #C
Um = 0.5 #m/s

Di = 0.084 #m
Do = 0.1 #m
kp = 60 #W/m.K

T_infty = -5 #C
U_infty = 3 #m/s

from Libraries import HT_thermal_resistance as res
Rp = []
Rp.append(res.Resistance("$R'_{conv,int}","W/m"))
Rp.append(res.Resistance("$R'_{cond,p}","W/m"))
Rp.append(res.Resistance("$R'_{conv,ext}","W/m"))

# internal convection
from Libraries import thermodynamics as thermo
from Libraries import HT_internal_convection as intconv

water = thermo.Fluid('water',Tm,"C")
pipeflow = intconv.PipeFlow(D=Di,L=1,Um=Um,nu=water.nu)
print("Re_D_pipe= %.0f" %pipeflow.Re)
pipeflow.Dittus_Boelter(mode='cooling',Pr=water.Pr)
hint = pipeflow.Nu*water.k/Di
print("hint=%.1f W/m^2.K" %hint)
Rp[0].convection(h=hint,A=np.pi*Di)

# conduction
Rp[1].cond_cylinder(k=kp,ra=Di,rb=Do,L=1.)

# external convection

#guess for surface temperature at D=Do
T_so = 49.21 #C
T_f = (T_infty + T_so)/2
air = thermo.Fluid('air',T_f,"C")
Re_air = U_infty * Do/air.nu
# print(Re_air)
from Libraries import HT_external_convection as extconv
airflow = extconv.CircularCylinder('Churchill-Bernstein',Re_air,air.Pr)
hext = airflow.Nu*air.k/Do
print("hext=%.1f W/m^2.K" %hext)
Rp[2].convection(h=hext,A=np.pi*Do)

# total thermal resistance
Rptot = 0.
for i in range(3):
    Rptot += Rp[i].R

qp = (Tm - T_infty)/Rptot

print("Heat rate per unit length: %.0f W/m" %qp)

#New estimate of T_so
T_so = T_infty + qp*Rp[2].R
print("New T_so = %.2f C" %T_so)
```

    Re_D_pipe= 75882
    hint=2067.0 W/m^2.K
    hext=20.3 W/m^2.K
    Heat rate per unit length: 345 W/m
    New T_so = 49.21 C

```python
Tm = 50 #C
Um = 0.5 #m/s

Di = 0.084 #m
Do = 0.1 #m
kp = 60 #W/m.K

T_infty = -5 #C
U_infty = 3 #m/s

from Libraries import HT_thermal_resistance as res
Rp = []
Rp.append(res.Resistance("$R'_{conv,int}","W/m"))
Rp.append(res.Resistance("$R'_{cond,p}","W/m"))
Rp.append(res.Resistance("$R'_{conv,ext}","W/m"))

# internal convection
from Libraries import thermodynamics as thermo
from Libraries import HT_internal_convection as intconv

water = thermo.Fluid('water',Tm,"C")
pipeflow = intconv.PipeFlow(D=Di,L=1,Um=Um,nu=water.nu)
print("Re_D_pipe= %.0f" %pipeflow.Re)
pipeflow.Dittus_Boelter(mode='cooling',Pr=water.Pr)
hint = pipeflow.Nu*water.k/Di
print("hint=%.1f W/m^2.K" %hint)
Rp[0].convection(h=hint,A=np.pi*Di)

# conduction
Rp[1].cond_cylinder(k=kp,ra=Di,rb=Do,L=1.)

# external convection

# initial guess for surface temperature at D=Do
T_so = 0. #C
errT = np.inf
iteration = 0
while (errT > 1.0) and (iteration < 10):
    iteration += 1
    T_so_old = T_so
    T_f = (T_infty + T_so)/2
    air = thermo.Fluid('air',T_f,"C")
    Re_air = U_infty * Do/air.nu
    # print(Re_air)
    from Libraries import HT_external_convection as extconv
    airflow = extconv.CircularCylinder('Churchill-Bernstein',Re_air,air.Pr)
    hext = airflow.Nu*air.k/Do
    print("hext=%.1f W/m^2.K" %hext)
    Rp[2].convection(h=hext,A=np.pi*Do)

    # total thermal resistance
    Rptot = 0.
    for i in range(3):
        Rptot += Rp[i].R

    qp = (Tm - T_infty)/Rptot

    print("Heat rate per unit length: %.0f W/m" %qp)

    #New estimate of T_so
    T_so = T_infty + qp*Rp[2].R
    print("New T_so = %.2f C" %T_so)
    errT = abs(T_so - T_so_old)
    print("errT=%.3e" %errT)
```

    Re_D_pipe= 75882
    hint=2067.0 W/m^2.K
    hext=20.7 W/m^2.K
    Heat rate per unit length: 352 W/m
    New T_so = 49.19 C
    errT=4.919e+01
    hext=20.3 W/m^2.K
    Heat rate per unit length: 345 W/m
    New T_so = 49.21 C
    errT=1.580e-02

```python
Qp = qp*1e-3*24
print("Daily energy loss: %.3f kW.h/d/m" %Qp)
Cp = Qp * 0.05
print("Cost: $%.3f /m.d " %Cp)
```

    Daily energy loss: 8.289 kW.h/d/m
    Cost: $0.414 /m.d

### Problem
Water at a flow rate or $\dot{m}=0.215\mathrm{kg/s}$ is cooled from $70^\circ\mathrm{C}$ to $30^\circ\mathrm{C}$ by passing it through a thin-walled tube of diameter $D=50\mathrm{mm}$ and maintaining a coolant at $T_\infty=15^\circ\mathrm{C}$ in cross flow over the tube.
* (1) What is the required tube length if the coolant is air and its velocity is $V=20\mathrm{m/s}$
* (2) What is the tube length if the coolant is water and $V=2\mathrm{m/s}$

#### Assumptions
Steady state, 1-D heat transfer, negligible conduction resistance from the wall, negligible radiation

#### Parameters

Pipe flow mean temperature
$$T_m = \frac{T_{m,o}+T_{m,i}}{2}$$

Coolant film temperature (Surface temperature must be guessed at first)
$$
T_f = \frac{T_s+T_\infty}{2}
$$

```python
from Libraries import thermodynamics as thermo
mdot = 0.215 #kg/s
Tmi= 70 #C
Tmo = 30 #C
Tinf = 15 #C
Vair_out = 20 #m/s
Vwater_out = 2 #m/s
D = 0.05 #m
Tm = (70 + 30)/2 #C
water_in = thermo.Fluid('water',Tm,"C")
Ts = 30 #C (guess)
Tf = (Ts )
air_out = thermo.Fluid('air',Tf,"C")
water_out = thermo.Fluid('water',Tf,"C")

```

#### Thermal circuit

The thermal circuit of heat transfer from the inner fluid to the outer coolant fluid consists of two convection resistances under the assumptions above:

```python
from Libraries import HT_thermal_resistance as res
R = []
R.append(res.Resistance("$R_{conv,i}$", "W"))
R.append(res.Resistance("$R_{conv,o}$", "W"))

d = schem.Drawing()
d.add(e.DOT, label = r"$T_m$")
d.add(e.RES, d = 'right', label = R[0].name)
d.add(e.DOT, label = r"$T_{s}$")
R1 = d.add(e.RES, d = 'right', label = R[1].name)
d.add(e.DOT, label="$T_\infty$")
L1 = d.add(e.LINE, toplabel = "$q_{conv}$", endpts = [[-2.25, 0], [-0.25, 0]])
d.labelI(L1, arrowofst = 0)
d.draw()
```

The overall energy balance determines the convection heat rate (Eq. 9)
$$
q_{conv}=\dot{m}C_p(T_{m,i}-T_{m,o})
$$

```python
qconv = mdot*water_in.Cp*(Tmi - Tmo)
print("Convection heat rate: %.0f W" %qconv)
```

    Convection heat rate: 35960 W

The length of the pipe is obtained from Eq. 21
$$
q_{conv}=\frac{\pi D L}{R''_{tot}}\Delta T_{lm}
$$
where
$$R''_{tot} = R''_{conv,i} + R''_{conv,o}$$
and
$$
\Delta T_{lm}=\frac{T_{m,i} - T_{m,o}}{\ln \frac{T_{m,i}-T_\infty}{T_{m,o} - T_\infty}}
$$

Therefore
$$
L = \frac{qR''_{tot}}{\pi D\Delta T_{lm}}
$$

The internal convection coefficient is calculated using the Gnielinski correlation, the external with Churchill-Bernstein

```python
from Libraries import HT_internal_convection as intconv
from Libraries import HT_external_convection as extconv

internalflow = intconv.PipeFlow(D=D,nu=water_in.nu,mdot=mdot,rho=water_in.rho)
internalflow.f_turbulent(eps=0.)
internalflow.Gnielinski(f=internalflow.f,Pr=water_in.Pr)
print("Internal convection Nu = %0.1f" %internalflow.Nu)
hi = internalflow.Nu*water_in.k/D
print("Internal convection coefficient: %.0f W/m^2.K" %hi)
R[0].convection(hi,A=1)

DTlm = intconv.log_mean_temperature(Tinf,Tmo,Tmi)
```

    Pipe wall is assumed to be hydrodynamically smooth
    Internal convection Nu = 60.3
    Internal convection coefficient: 777 W/m^2.K

* (1) For Air:

```python
Reout = D*Vair_out/air_out.nu
externalflow = extconv.CircularCylinder('Churchill-Bernstein',Reout,air_out.Pr)
print("External convection Nu = %0.1f" %internalflow.Nu)
ho = externalflow.Nu*air_out.k/D
print("External convection coefficient: %.0f W/m^2.K" %ho)
R[1].convection(ho,A=1)
Rtot = R[0].R + R[1].R
L = qconv*Rtot/(np.pi*D*DTlm)
print("For air, L = %.1f m" %L)
```

    External convection Nu = 60.3
    External convection coefficient: 83 W/m^2.K
    For air, L = 98.6 m

* (2) For Water:

```python
Reout = D*Vwater_out/water_out.nu
externalflow = extconv.CircularCylinder('Churchill-Bernstein',Reout,air_out.Pr)
print("External convection Nu = %0.1f" %internalflow.Nu)
ho = externalflow.Nu*water_out.k/D
print("External convection coefficient: %.0f W/m^2.K" %ho)
R[1].convection(ho,A=1)
Rtot = R[0].R + R[1].R
L = qconv*Rtot/(np.pi*D*DTlm)
print("For water, L = %.1f m" %L)
```

    External convection Nu = 60.3
    External convection coefficient: 3083 W/m^2.K
    For water, L = 12.0 m

```python

```


Table of Contents

1  Getting Started

1.1  References

2  Extended Kalman Filter

2.1  EKF Theory

2.1.1  State variables

2.1.2  System model (Prediction step)

2.1.2.1  System model linearization

2.1.2.2  System model noise

2.1.3  Measurement model (Update step)

2.1.3.1  Measurement model linearization

2.1.3.2  Measurement model noise

2.1.4  Process flow

2.1.4.1  PREDICTION

2.1.4.2  CORRECTION (Update)

2.2  EKF Implementation

2.2.1  EXAMPLE 1

2.2.1.1  PLOT example1 with choosen simulated parameters

Tip: This notebook uses nbextension TOC, enable it installing pip install jupyter-contrib-nbextensions.

# Getting Started

This Notebook contains a brief theory and practical Python tests with the Extended Kalman Filter algorithm.

>**Author:** Iñaki Lorente

## References

1. Introduction to Autonomous Mobile Robots by Roland Siegwart, Illah Reza Nourbakhs and Davide Scaramuzza, MIT Press.
2. Probabilistic Robotics by Sebastian Thrun, Wolfram Burgard and Dieter Fox, MIT Press.http://www.probabilistic-robotics.org/
3. Atsushi Sakai project. PythonRobotics, https://atsushisakai.github.io/PythonRobotics/
4. Paul Balzer Kalman documentation, https://github.com/balzer82/Kalman
5. Kalman and Bayesian Filters in Python. Roger R Labbe Jr. https://drive.google.com/file/d/0By_SW19c1BfhSVFzNHc0SjduNzg/view

# Extended Kalman Filter

## EKF Theory

This is just an introduction of the EKF basics. All info can be studied in more detail using the references.

### State variables

Depending on the robot characteristics define the **State vector** with the correct number of states. This example shows only 3 states.
In this simple case we maintain the position x, y and orientation $\theta$ of the robot:

$$x = \begin{bmatrix} x \\ y \\ \theta \end{bmatrix}$$

The control input $u$ is the velocity $v$ and steering angle $\alpha$

$$u = \begin{bmatrix} v \\ \alpha \end{bmatrix}$$

Where:

   - $x$ and $y$ : 2D coordenates
   - $u$ : Input ($u = [v, \alpha]$, where $v$ is the velocity and $\alpha$ the angle)
   - $\theta$ : Orientation

### System model (Prediction step)

Using the EKF the system model is a nonlinear motion model plus noise.

$$x_t = g(x_{t-1}, u_t) + \epsilon_t$$

Where:

   - $g$ : Non-linear function over the state vector at time $t$. Can be used to compute the predicted state from the previous estimate
   - $u_t$ : Input ($u_t = [v, \alpha]$, where $v$ is the velocity and $w$ the gyro)
   - $x_{t-1}$ : Previous state
   - $\epsilon_t$ : Process noise at time $t$

Let $x_{t-1} = (x,y,\theta)^T$ be the initial pose of the robot, and suppose we keep the velocity constant at $(v, \alpha)^T$ for some time $\Delta_t$.

The center of the circle is then:

$$x_c = x - \frac{v}{\alpha}sin(\theta)$$

$$y_c = y + \frac{v}{\alpha}cos(\theta)$$

After $\Delta_t$ time of motion, our ideal robot will be at $x_t = (x', y', \theta')^T$ with:

$\begin{bmatrix}x'\\y'\\\theta' \end{bmatrix} = \begin{bmatrix} x_c + \frac{v}{\alpha}sin(\theta + \alpha \Delta_t)\\y_c - \frac{v}{\alpha}cos(\theta + \alpha \Delta_t)\\\theta + \alpha \Delta_t\end{bmatrix} = \begin{bmatrix}x\\y\\\theta \end{bmatrix} +
\begin{bmatrix} -\frac{v}{\alpha}sin(\theta) + \frac{v}{\alpha}sin(\theta + \alpha \Delta_t)\\\frac{v}{\alpha}cos(\theta) - \frac{v}{\alpha}cos(\theta + \alpha \Delta_t)\\ \alpha \Delta_t\end{bmatrix}$

>To compute the kinematics with non-constant velocities, it is therefore common practice to use small values for $\Delta_t$, and to approximate
the actual velocity by a constant within each time interval.

#### System model linearization

>$g$ cannot be applied to the covariance directly. A matrix of partial derivates (The Jacobian matrix) is computed.

At each time step, the Jacobian is evaluated with current predicted states. **This process linearizes the non-linear function $g$ around the current estimate**.

From the Robot motion formulation exposed before, we can get the **motion model**:

$$x_t = \begin{bmatrix}x\\y\\\theta \end{bmatrix} +
\begin{bmatrix} -\frac{v}{\alpha}sin(\theta) + \frac{v}{\alpha}sin(\theta + \alpha\Delta_t)\\\frac{v}{\alpha}cos(\theta) - \frac{v}{\alpha}cos(\theta + \alpha\Delta_t)\\ \alpha\Delta_t\end{bmatrix}$$

Its Jacobian is:

$G_t = \begin{bmatrix} 1 & 0 & -\frac{v_t}{\alpha_t}cos(\theta) + \frac{v_t}{\alpha_t}cos(\theta + \alpha_t\Delta_t)\\0 & 1 & -\frac{v_t}{\alpha_t}sin(\theta) + \frac{v_t}{\alpha_t}sin(\theta + \alpha \Delta_t)\\ 0 & 0 & 1\end{bmatrix}$

#### System model noise

We command a specific velocity and steering angle (using $u$), but we need to convert that into errors in $x, y, \theta$. It vary depending on velocity, so it will need to be recomputed for every prediction.

$$M_t = \begin{bmatrix}
        \alpha_1 v²_t + \alpha_2 \alpha²_t & 0 \\
        0 & \alpha_3 v²_t + \alpha_4 \alpha²_t
        \end{bmatrix}$$

The transformation from control space to state space is performed by another linear approximation. The Jacobian needed for this approximation, denoted $V_t$ is the derivate of the motion function $g$ with respect to the motion parameters, evaluated at $u_t$ and $u_{t-1}$

$$V_t = \frac{\partial g(u_t, u_{t-1})}{\partial u_t} = \begin{bmatrix}
\frac{\partial x'}{\partial v_t} & \frac{\partial x'}{\partial \alpha_t} \\
\frac{\partial y'}{\partial v_t} & \frac{\partial y'}{\partial \alpha_t} \\
\frac{\partial \theta'}{\partial v_t} & \frac{\partial \theta'}{\partial \alpha_t} \\
\end{bmatrix} =
\begin{bmatrix}
\frac{-sin \theta + sin(\theta + \alpha_t \Delta_t)}{\alpha_t} &
\frac{v_t(sin \theta - sin(\theta + \alpha_t \Delta_t))}{\alpha²_t} +
\frac{v_t(cos(\theta + \alpha_t \Delta_t)\Delta_t}{\alpha_t} \\
\frac{cos \theta - cos(\theta + \alpha_t \Delta_t)}{\alpha_t} &
-\frac{v_t(cos \theta - cos(\theta + \alpha_t \Delta_t))}{\alpha²_t} +
\frac{v_t(sin(\theta + \alpha_t \Delta_t)\Delta_t}{\alpha_t} \\
0 & \Delta_t
\end{bmatrix}
$$

The multiplication $V_t M_t V_t^T$ then provides an approximate mapping between the motion noise in control space to the motion noise in state space.

$$R_t = V_t M_t V_t^T$$

Using $R_t$ and $G_t$ we can get the error covariance ahead:

$$P_{t+1} = G_t P_{t-1} G_t^T + R_t$$

We have finished then the Prediction step.
For a real problem you have to carefully model your system with differential equations and then determine the most appropriate way to solve that system. The correct approach depends on the accuracy you require, how nonlinear the equations are, your processor budget, and numerical stability concerns.

### Measurement model (Update step)

The robot's sensor provides a noisy bearing and range measurement to multiple known locations in the landscape. The measurement model must convert the state $[xyθ]^T$ into a range and bearing to the landmark. If $p$ is the position of a landmark, the range $r$ is

$$r = \sqrt{(p_x - x)^2+(p_y - y)^2}$$

The sensor provides bering relative to the orientation of the robot:

$$\phi = arctan(\frac{p_y - y}{px - x}) - \theta$$

$$z_t = \begin{bmatrix} x \\ y \\ \theta \end{bmatrix}$$

$$z_t = h(x_t) + \delta_t =
\begin{bmatrix}
\sqrt{(p_x - x)^2+(p_y - y)^2} \\
arctan(\frac{p_y - y}{px - x}) - \theta
\end{bmatrix} + \delta_t
$$

Where:

   - $h$ : Non-linear function over the state vector at time $t$. Can be used to compute the predicted mesurement from the predicted state
   - $\delta_t$ : Observation noise at time $t$
   - $p$ : Position of a landmark
   - $r$ : range mesurement
   - $\phi$ : Bearing or heading direction

#### Measurement model linearization

Same way we did at State model part with function $g$, here we need to linearize the nonlineal function $h$

$$H_t = \frac{\partial h(\hat{x},p)}{\partial x_t} =
\begin{bmatrix}
\frac{-p_x + x}{\sqrt{(p_x - x)^2+(p_y - y)^2}} &
\frac{-p_y + y}{\sqrt{(p_x - x)^2+(p_y - y)^2}} & 0 \\
\frac{-p_y + y}{\sqrt{(p_x - x)^2+(p_y - y)^2}} &
-\frac{p_x + x}{\sqrt{(p_x - x)^2+(p_y - y)^2}} & -1
\end{bmatrix}$$

#### Measurement model noise

Additional mesurement noise from the previous measurement state:

$$Q_t = \begin{bmatrix}
\sigma_x^2 & 0 & 0 \\
 0 & \sigma_y^2 & 0 \\
 0 & 0 & \sigma_\phi^2
\end{bmatrix}$$

###  Process flow

#### PREDICTION

Project the state vector ahead :

$$x_{t+1} = g(x_t, u)$$

Project the error covariance ahead:

$$P_{t+1} = G_t P_{t-1} G_t^T + R_t$$

#### CORRECTION (Update)

Compute the Kalman Gain :

$$K_t = P_t H_t^T (H_t P_t H_t^T + Q_t)^-1$$

Update the estimate via mesurement :

$$x_t = x_t + K_t (z_t - h(x_t))$$

Update the error covariance :

$$P_t = (1-K_t H_t) P_t$$

Where:

- $K_t$ : Kalman Gain
- $P_t$ : Covariance matrix of the state at time t
- $Q_t$ : Covariance matrix of measurement noise at time t
- $R_t$ : Covariance matrix of system noise at time t
- $G_t$ : Jacobian of system function at time t
- $H_t$ : Jacobian of measurement function at time

## EKF Implementation

```python
import math
import cmath
import numpy as np
import sympy

class EKF():

    def __init__(self, dt, wheelbase):

        # Dimensions
        dim_x = 3
        dim_z = 2

        #######################
        # 0. INIT
        #######################
        self.dt = dt
        self.wheelbase = wheelbase

        self.x = np.zeros((dim_x,1))        # True trajectory state
        self.P = np.eye(dim_x)              # uncertainty covariance
        self.G = np.eye(dim_x)              # state transition matrix
        self.R = np.eye(dim_z)              # state uncertainty
        self.Q = np.eye(dim_x)              # process uncertainty

        self.K = np.zeros(self.x.shape)     # kalman gain
        self.S = np.zeros((dim_z, dim_z))   # system uncertainty
        self.y = np.zeros((dim_z, 1))       # residual
        self.I = np.eye(dim_x)              # identity matrix

        self.x_est = self.x                 # Estimated trajectory state

        ##################################
        # 1. FORUMLAS: Get sympy symbols
        ##################################
        x, y, theta, v, a, w, t, lx, ly = sympy.symbols('x, y, theta, v, a, w, t, lx, ly')

        # Get wheel information
        dist = v*t # distance
        beta = (dist/w)*sympy.tan(a) # (distance/wheelbase) * steering angle
        r  = w/sympy.tan(a) # radius

        ###################
        # 1.1 System model
        ###################

        # state = [x,y,theta] (pos_x, pos_y, yaw_rate)
        self.state_formula = sympy.Matrix([x,y,theta])

        # motion = [v,a] (velocity, steering angle)
        self.motion_formula = sympy.Matrix([v,a])

        # g = Non-linear function over the state vector at time t (predicted state)
        self.g_formula = sympy.Matrix([[x-beta*sympy.sin(theta)+r*sympy.sin(theta + beta)],
                          [y+r*sympy.cos(theta)-r*sympy.cos(theta + beta)],
                          [theta+beta]
                         ])

        # Jacobian of g evaluated using state
        self.G_formula = self.g_formula.jacobian(self.state_formula)

        # Jacobian of g evaluated using motion
        self.V_formula = self.g_formula.jacobian(self.motion_formula)

        ########################
        # 1.2 Measurement model
        ########################

        # It depends on the measurement sensor used
        # this case sensor provides noise bearing and range
        # based on landmarks

        # range
        r = sympy.sqrt((lx-x)**2 + (ly-y)**2)
        # bearing
        b = sympy.atan((ly-y)/(lx-x))

        # h = Non-linear function over the state vector at time t (predicted mesurement)
        self.h_landmark_formula = sympy.Matrix([[r],
                                                [b-theta]
                                               ])

        self.h_odom_formula = sympy.Matrix([[x],
                                            [y],
                                            [theta]
                                           ])

        # Jacobian of h evaluated using state
        self.H_landmark_formula = self.h_landmark_formula.jacobian(self.state_formula)

        self.H_odom_formula = self.h_odom_formula.jacobian(self.state_formula)

        ##################################
        # 2. Save values in a dictionary
        ##################################
        self.ekf_dict = {x: 0, y: 0, theta:0,
                         v:0, a:0,
                         t:dt, w:wheelbase}

        # This just creates a link to the dictionay keys
        # the value of the following variables are just
        # names (dictionary key names)
        self.x_x, self.x_y, self.theta = x, y, theta
        self.v, self.a = v, a

    def __repr__(self):
        return '\n'.join([
            'Extended Kalman Filter object',
            '------------------------------',
            'x: ', str(self.x),
            'P: ', str(self.P),
            'x_est: ', str(self.x_est),
            'G: ', str(self.G),
            'Q: ', str(self.Q),
            'R: ', str(self.R),
            'K: ', str(self.K),
            'y: ', str(self.y),
            'S: ', str(self.S)
            ])

    def predict(self, x, u):
        '''EKF Prediction step
                input  -> u = motion control [v,a]
                call   -> move function
                action -> get P based on new values
        '''

        self.x = x

        # Write values on EKF dictionary
        self.ekf_dict[self.x_x] = self.x[0,0]
        self.ekf_dict[self.x_y] = self.x[1,0]
        self.ekf_dict[self.theta] = self.x[2,0]
        self.ekf_dict[self.v] = u[0]
        self.ekf_dict[self.a] = u[1]

        # Get G,V values applying dicctionary values to formulas
        self.G = np.array(self.G_formula.evalf(subs=self.ekf_dict)).astype(float)
        self.V = np.array(self.V_formula.evalf(subs=self.ekf_dict)).astype(float)

        # M = Covariance of motion noise
        self.M = np.array([[u[0]**2, 0],
                           [0, u[1]**2]
                          ])

        self.Q = np.dot(self.V, self.M).dot(self.V.T)

        # P = Error covariance
        self.P = np.dot(self.G, self.P).dot(self.G.T) + self.Q

    def update_landmark_method(self, landmark, std_range, std_bearing):
        '''EKF Update step based on landmarks
                input  -> landmarks, sensor range and bearing
                action -> update P and state (x) based on measurements
        '''
        for lmark in landmark:
            hyp = (lmark[0] - self.x[0, 0])**2 + (lmark[1] - self.x[1, 0])**2
            dist = math.sqrt(hyp)

            # Get measurement h on last current state
            # This state (x) has predictions applied
            self.h = np.array([[dist],
                               [math.atan2(lmark[1] - self.x[1, 0], lmark[0] - self.x[0, 0]) - self.x[2, 0]]
                              ])

            # Get jacobian of h on last current state
            # This state (x) has predictions applied
            self.H = np.array([[-(lmark[0] - self.x[0, 0]) / dist, -(lmark[1] - self.x[1, 0]) / dist, 0],
                               [(lmark[1] - self.x[1, 0]) / hyp,  -(lmark[0] - self.x[0, 0]) / hyp, -1]
                              ])

            # Get measurement based on true state (x)
            # This state (x) is prior to predictions
            x, y = self.x[0, 0], self.x[1, 0]
            d = math.sqrt((lmark[0] - x)**2 + (lmark[1] - y)**2)
            a = math.atan2(lmark[1] - y, lmark[0] - x) - self.x[2, 0]
            self.z = np.array([[d + np.random.randn()*std_range],
                               [a + np.random.randn()*std_bearing]
                              ])

            # Covariance of measurement noise
            self.R = np.diag([std_range**2, std_bearing**2])

            # K = Kalman Gain
            PHT = np.dot(self.P, self.H.T)
            self.S = np.dot(self.H,PHT) + self.R
            self.K = PHT.dot(np.linalg.inv(self.S))

            # Update the estimate via mesurement (state)
            self.y = self.residual(self.z, self.h)
            self.x_est = self.x + np.dot(self.K, self.y)

            # Update Error covariance (P)
            self.P = (self.I - np.dot(self.K,self.H)).dot(self.P)
            #I_KH = self.I - np.dot(self.K, self.H)
            #self.P = np.dot(I_KH, self.P).dot(I_KH.T) + np.dot(self.K, self.R).dot(self.K.T)

        return self.x_est, self.P

    def update_odom_method(self, odom):
        '''EKF Update step based on odometry measurement
                input  -> [x,y,theta]
                action -> update P and state (x) based on measurements
        '''

        # Get h,H values applying dicctionary values to formulas
        self.h = np.array(self.h_odom_formula.evalf(subs=self.ekf_dict)).astype(float)
        self.H = np.array(self.H_odom_formula.evalf(subs=self.ekf_dict)).astype(float)

        # Get measurement based on true state (x)
        Qsim = np.diag([0.5, 0.5, 0.5])**2
        Rsim = np.diag([1.0, 1.0, np.deg2rad(30.0)])**2
        self.z = np.array([[self.x[0, 0] + np.random.randn() * Qsim[0, 0]],
                           [self.x[1, 0] + np.random.randn() * Qsim[1, 1]],
                           [self.x[2, 0] + np.random.randn() * Qsim[2, 2]],
                          ])

        # Covariance of measurement noise
        self.R = np.diag([odom[0]**2, odom[1]**2, odom[2]**2])

        # K = Kalman Gain
        PHT = np.dot(self.P, self.H.T)
        self.S = np.dot(self.H,PHT) + self.R
        self.K = PHT.dot(np.linalg.inv(self.S))

        # Update the estimate via mesurement (state)
        self.y = self.residual(self.z, self.h)
        self.x_est = self.x + np.dot(self.K, self.y)

        # Update Error covariance (P)
        self.P = (self.I - np.dot(self.K,self.H)).dot(self.P)
        #I_KH = self.I - np.dot(self.K, self.H)
        #self.P = np.dot(I_KH, self.P).dot(I_KH.T) + np.dot(self.K, self.R).dot(self.K.T)

        return self.x_est, self.P

    def residual(self, a, b):
        """ compute residual (a-b) between measurements containing
        [range, bearing]. Bearing is normalized to [-pi, pi)"""
        y = a - b
        y[1,0] = y[1,0] % (2 * np.pi)    # force in range [0, 2 pi)
        if y[1,0] > np.pi:               # move to [-pi, pi)
            y[1,0] -= 2 * np.pi
        return y

    def move(self, x, u, dt):
        '''SIM ROBOT MOVEMENT. To be used if we don't have odometry
           from robot. This function is mainly used for
           test purposes.
        '''
        theta = x[2, 0] # x = [x, y, theta] where 'theta' is the steer angle
        vel = u[0]      # u = [v, a] -> get velocity
        steering_angle = u[1] # u = [v, a] -> get steer angle
        dist = vel * dt # distance

        if abs(steering_angle) > 0.001: # is robot turning?
            beta = (dist / self.wheelbase) * math.tan(steering_angle)
            r = self.wheelbase / math.tan(steering_angle) # radius

            dx = np.array([[-r*math.sin(theta) + r*math.sin(theta + beta)],
                           [r*math.cos(theta) - r*math.cos(theta + beta)],
                           [beta]])
        else: # moving in straight line
            dx = np.array([[dist*cos(theta)],
                           [dist*sin(theta)],
                           [theta]])

        # Update current state
        self.x = x + dx

        return self.x

    def plot_covariance_ellipse(self, xEst, PEst):
        ''' Just a way to plot P covariance
        based on https://github.com/AtsushiSakai/PythonRobotics
        '''
        Pxy = PEst[0:2, 0:2]
        eigval, eigvec = np.linalg.eig(Pxy)

        if eigval[0] >= eigval[1]:
            bigind = 0
            smallind = 1
        else:
            bigind = 1
            smallind = 0

        t = np.arange(0, 2 * math.pi + 0.1, 0.1)
        a = cmath.sqrt(eigval[bigind])
        b = cmath.sqrt(eigval[smallind])
        x = [a * math.cos(it) for it in t]
        y = [b * math.sin(it) for it in t]
        angle = math.atan2(eigvec[bigind, 1], eigvec[bigind, 0])
        R = np.array([[math.cos(angle), math.sin(angle)],
                      [-math.sin(angle), math.cos(angle)]])
        fx = R.dot(np.array([[x, y]]))
        px = np.array(fx[0, :] + xEst[0, 0]).flatten()
        py = np.array(fx[1, :] + xEst[1, 0]).flatten()

        #plt.plot(px, py, "--g")

        return px, py
```

### EXAMPLE 1

```python
###############################################################
# Instantiate EKF with
#    - initial state x -> [x,y,theta]
#    - sample rate -> dt  (hz, i.e -> 1.0/50.0 = 50 Hz)
#    - wheelbase
#    - velocity
#    - steer_angle
###############################################################

def example1():
    #############
    # INIT
    #############
    ekf = EKF(dt=1.0,
              wheelbase=0.5)

    return ekf
```

#### PLOT example1 with choosen simulated parameters

```python
import matplotlib.pyplot as plt

def run_simulation(start_pos, u, std_range=0.3, std_bearing=0.1, landmark=True, landmarks=0):

    ekf = example1()   #USE EXAMPLE1

    ################
    # Plot Variables
    ################
    prediction_step=10
    sim_pos = start_pos.copy()
    track = []
    estimated = []
    covariance = []

    ################
    # Start example
    ################
    for i in range(200):
        sim_pos = ekf.move(sim_pos, u, 1.0/10.) # start simulation, move robot
        track.append(sim_pos)

        # Apply EKF every 'prediction_step' times
        if i % prediction_step == 0:
            ekf.predict(sim_pos, u)
            if landmark:
                estimated_ = ekf.update_landmark_method(landmarks, std_range, std_bearing)
            else:
                estimated_ = ekf.update_odom_method(sim_pos)

            estimated.append(estimated_[0])
            covariance.append([estimated_[0], estimated_[1]])

    ################
    # PLOT EXAMPLE
    ################

    if landmark:
        # Plot landmarks
        plt.figure()
        plt.scatter(landmarks[:, 0], landmarks[:, 1], marker='s', s=60, label='Landmarks')

    # Plot track (True trajectory)
    track = np.array(track)
    plt.plot(track[:, 0], track[:,1], color='k', linewidth =2, label='True trajectory')

    # Plot estimated (Estimated trajectory)
    estimated = np.array(estimated)
    plt.plot(estimated[:, 0], estimated[:, 1], color='r', linewidth =2, label='Estimated trajectory')

    # Plot covariance matrix estimation
    label_added =False
    for element in covariance:
        c = ekf.plot_covariance_ellipse(element[0], element[1])
        if not label_added:
            plt.plot(c[0], c[1], '--g', label='Covariance matrix')
            label_added = True
        else:
            plt.plot(c[0], c[1], '--g')

    # Plot general options

    plt.legend()
    plt.axis('equal')
    plt.title("EKF Robot localization")
    plt.show()
```

```python
#########################################
# USING LANDMARKS: SIMULATION PARAMETERS
#########################################
start_pos = np.array([[2, 6, .3]]).T # First State position
u = np.array([1.1, .01])             # Constant steering command (vel, steering angle radians)

std_range = 0.3     # Measurement range
std_bearing = 0.1   # Measurement bearing
landmarks = np.array([[5, 10], [10, 5]])  # Landmarks position

run_simulation(start_pos,
               u,
               std_range,
               std_bearing,
               landmark = True,
               landmarks = landmarks)
```

- This simulations runs a filter prediction based on 2 landmarks we can see how the last part of the filter estimation gets worst uncertinaty (covariance maxtrix of the process). This is because the distance to a landamark is getting farther.

```python
#########################################
# USING LANDMARKS: SIMULATION PARAMETERS
#########################################
start_pos = np.array([[2, 6, .3]]).T # First State position
u = np.array([1.1, .01])             # Constant steering command (vel, steering angle radians)

std_range = 0.3     # Measurement range
std_bearing = 0.1   # Measurement bearing
landmarks = np.array([[5, 10], [10, 5], [15, 15]])  # Landmarks position

run_simulation(start_pos,
               u,
               std_range,
               std_bearing,
               landmark = True,
               landmarks = landmarks)
```

- This second simulation runs a filter prediction based on 3 landmarks, we can se now how the uncertainity gets better results at the end of the filter estimation. Now the filer gets a landmark position close by and the update step of the filter gets better results.

```python
#########################################
# USING ODOMETRY: SIMULATION PARAMETERS
#########################################
start_pos = np.array([[2, 6, .3]]).T # First State position
u = np.array([1.1, .01])             # Constant steering command (vel, steering angle radians)

run_simulation(start_pos,
               u,
               landmark = False)
```

TODO: Covariance matrix is not well represented, check ellipse plot function