### UNIVERSIDADE FEDERAL DO AMAZONAS

 **INSTITUTO DE EDUCAÇÃO, AGRICULTURA E AMBIENTE**

 CURSO ENGENHARIA AMBIENTAL

**AULA 3 - OPERAÇÕES UNITÁRIAS**
---
---
---

**Professora Dra Agnes Cristina Oliveira Mafra**

**3. BALANÇO DE MASSA COM REAÇÃO**
---

**Tópicos Abordados nesta aula**
* Equações Químicas;
* Conservação da Massa com reaçao química;
* Cálculos de números de mols limitante e excesso;

**Alguns Objetivos de Aprendizagem**
* Balancear uma equação química.
* Reconhecer que o número de átomos de cada elemento é conservado em uma reação química.
* Calcular a composição de saída de um reator químico.

**Introdução às reações químicas**
--

**Assista o vídeo abaixo e acompanhe os exercícios propostos para esta aula escrevendo o conteúdo em seu caderno.**

```python
from IPython.display import YouTubeVideo
YouTubeVideo("MWucuQ-jUT8",560,350,start=0,end=5000,rel=0)
```

**Laboratório Virtual de Balanceamento de Equações Químicas**
---

[Laboratório Virtual de Balanceamento de Equações Químicas (PHET)](https://phet.colorado.edu/pt_BR/simulation/balancing-chemical-equations)

**Clique no link do laboratório virtual e comece pela INTRODUÇÃO.**

**Use a ferramenta de balança e faça testes no balanço das reações químicas apresentadas abaixo (Síntese de amônia, Hidrólise e Queima de metado).**

**Usando a ferramenta de igualdade e gráfico você também pode verificar suas suposições.**

**Vamos usar a opção de JOGOS!!!???** =)

**Selecione o seu nível e comece a jogar!!! Confira seus resultados!!**

*Não esqueça de nos contar seus resultados!!*

**Balanço em reatores químicos**
---

**REAGENTE LIMITANTE**

* Qualquer balanço deve ser calculado em uma Base de Cálculo. Para reações químicas a Base de Cálculo a ser adotada deve estar relacionada ao Reagente Limite. O reagente limite é aquele que aparece em menor proporção em relação à proporção estequiometricamente requerida;

* Os cálculos que envolvem reações químicas, são muitas vezes, facilitados quando realizados em base molar. Contudo, é importante lembrar que o número de mols final não é necessariamente igual ao número total de mols, inicialmente, alimentados à reação;

* Assim sendo, temos que: “A massa global de um sistema é mantida durante uma transformação química”; porém, “o número de mols total de um sistema não é mantido em processos que envolvem reações químicas”.

**O reagente consumido em primeiro lugar numa reação química é designado reagente limitante. Os outros reagentes dizem-se em excesso.**

**DETERMINAÇÃO DO REAGENTE LIMITANTE**

* O “Reagente Limite’’ é aquele que ‘’Limita’’ a reação por esgotar-se antes dos demais reagentes. Ou seja, aquele que se apresenta em uma proporção menor em relação a estequiometria da reação;

* A menor relação entre número de mols alimentados e número de mols estequiométricos (nAl/nest) define o reagente limitante,ou seja, O reagente limitante define a extensão da reação.

**Exercício Resolvido 1 - A**
---

Se 80,4 Kg.mols de $FeSO_4$, 35,1 Kg.mols de $KMnO_4$ e 135,7 Kg.mols de $H_2SO_4$ são alimentados num reator químico. Qual é o reagente limitante?

```python
from IPython.display import YouTubeVideo
YouTubeVideo("HGil3aApf7E",560,315,start=0,end=5000,rel=0)
```

*Resolução:*

**Passo 1 )** Montar a reação e balancear a equação;

**Passo 2 )** Montar a tabela abaixo com o número de mols estequiométricos e alimentados no processo;

**Passo 3 )** Fazer a razão entre número de mols alimentados e estequiométrico;

**Passo 4 )** Comparar a razão calculada com o número de mols estequiométrico:

**Será o reagente LIMITANTE quando a RAZÃO < n_estequiométrico**

\begin{align*}
\\10 FeSO_4 + 2 KMnO_4 + 8 H_2SO_4 \Rightarrow 5 Fe_2(SO_4)_3 +2 MnSO_4 + K_2SO_4 + 8 H_2O
\end{align*}

| Parâmetros         | | Reagentes  |  |
| :-------------: | :-----------: | :-----------: | :-----------: |
|                 | KMnO4 | H2SO4 |  FeSO4
|nestequiométrico  | 2    |  8     |  10       |
|nalimentado | 35.1   | 135.7 |  80.4     |
|Razão = nalimentado/nestequiométrico | 17.6  | 17    |  **8.04**    |

**Você notou que o reagente $FeSO_4$ apresentou a razão 8.04, que é menor que o $n_{estequiométrico}$ de 10 mols?? Portanto, o $FeSO_4$ é o reagente LIMITANTE!!**

**DETERMINAÇÃO DO REAGENTE EM EXCESSO**

* O percentual de excesso é calculado através da razão entre a "sobra" de reagente e o que reagiu;

\begin{align*}
\\Excesso_A=\frac{{n}_{alimentadoA}-{n}_{reageA}}{{m}_{reageA}}\\
\end{align*}

**Exercício Resolvido 1 - B**
---

Qual a % em excesso do $FeSO_4$ e $H_2SO_4$ no exemplo anterior?

*Resolução*

*Continuando os Passos:*

**Passo 5 )** Calcular o número de mols total de reagem efetivamente no sistema, ou seja, se há um limitante é este que irá "mandar" nos cálculos!!

**Passo 6 )** Calcular conforme a fórmula o percentual de excesso de cada reagente, lembrando que não há excesso no reagente limitante;

**Passo 7 )** Coloque todos os valores na tabela para facilitar a sua visualização.

| Parâmetros         | | Reagentes  |  |
| :-------------: | :-----------: | :-----------: | :-----------: |
|                 | KMnO4 | H2SO4 |  FeSO4
|nestequiométrico  | 2    |  8     |  10       |
|nalimentado | 35.1   | 135.7 |  80.4     |
|Razão = nalimentado/nestequiométrico | 17.6  | 17    |  **8.04**    |
| nreage| 16.1| 64.3 | **80.4** |
| Excesso(%)| 118| 111 | 0 |

*Os cálculos do termo reage são realizados da seguinte maneira:*

* Como o limitante é o $FeSO_4$ o valor do alimentado é igual ao termo reage.
* Faz-se a regra de três :

\begin{align*}
\\8 mols de H_2SO_4  \Longleftrightarrow  10 mols de FeSO_4\\
\\x mols de H_2SO_4 \Longleftrightarrow  80.4 mols de FeSO_4\\
\end{align*}

```python
import sympy
from sympy.solvers import solve
from sympy import Symbol
import math
x = Symbol ('x')
sol1 = solve (10*x-80.4*8,x)
print('Nreage de H2SO4 =', sol1)
```

    Nreage de H2SO4 = [64.3200000000000]

\begin{align*}
\\2 mols de KMnO_4  \Longleftrightarrow  10 mols de FeSO_4\\
\\x mols de KMnO_4  \Longleftrightarrow  80.4 mols de FeSO_4\\
\end{align*}

```python
import sympy
from sympy.solvers import solve
from sympy import Symbol
x = Symbol ('x')
sol2 = solve (10*x-80.4*2,x)
print('Nreage de KMnO4 =', sol2)
```

    Nreage de KMnO4 = [16.0800000000000]

Cálculo dos Excessos:

```python
Excesso_H2SO4 = 100*(135.7-64.3)/64.3
print(Excesso_H2SO4)
```

    111.04199066874027

```python
Excesso_KMnO4 = 100*(35.1-16.1)/16.1
print(Excesso_KMnO4)
```

    118.01242236024844

**Você coneguiu compreender? Refaça o cálculo dos percentuais à mão!!! Para a sua completa compreensão.**

**Exercício Resolvido 1 - C**
---

Quanto será a composição dos produtos, considerando exemplo
anterior?

\begin{align*}
\\10 FeSO_4 + 2 KMnO_4 + 8 H_2SO_4 \Rightarrow 5 Fe_2(SO_4)_3 +2 MnSO_4 + K_2SO_4 + 8 H_2O
\end{align*}

**Equação Geral**

***ENTRA - SAI + GERADO - CONSUMO = ACÚMULO***

Para os REAGENTES : SAI = ENTRA - CONSUMO

Para os PRODUTOS : SAI = ENTRA + REAGE

* Vamos fazer os cálculos para os reagentes primeiro ???

```python
#Para os REAGENTES : SAI = ENTRA - CONSUMO

#N_final_KMnO4 = N_alimentado_KMnO4 - N_reage_KMnO4
N_final_KMnO4 = 35.1 - 16.1
print('N_final_KMnO4 =', N_final_KMnO4)

#N_final_H2SO4 = N_alimentado_H2SO4 - N_reage_H2SO4
N_final_H2SO4 = 135.7 - 64.3
print('N_final_H2SO4 =',N_final_H2SO4)

#N_final_FeSO4 = N_alimentado_FeSO4 - N_reage_FeSO4
N_final_FeSO4 = 80.4 - 80.4
print('N_final_FeSO4 =',N_final_FeSO4)

```

    N_final_KMnO4 = 19.0
    N_final_H2SO4 = 71.39999999999999
    N_final_FeSO4 = 0.0

| Parâmetros         | | Reagentes  |  |
| :-------------: | :-----------: | :-----------: | :-----------: |
|                 | KMnO4 | H2SO4 |  FeSO4
|nestequiométrico  | 2    |  8     |  10       |
|nalimentado | 35.1   | 135.7 |  80.4     |
|Razão = nalimentado/nestequiométrico | 17.6  | 17    |  **8.04**    |
| nreage| 16.1| 64.3 | **80.4** |
| Excesso(%)| 118| 111 | 0 |
| nfinal| 19.0| 71.4 | **0.0** |

* Agora vamos fazer os cálculos para os produtos!

Bom!! Se você pensar que todo o reagente limitante reagiu efetivamente então, podemos fazer os cálculos das quantidades de produtos formados.

| Parâmetros         | | Reagentes  | | |  |  Produtos  |  |   |
| :-------------: | :-----------: | :-----------: | :-----------: | :-----------:  |:-----------: | :-----------:  | :-----------:  |:-----------: |
|                 | KMnO4 | H2SO4 |  FeSO4 | **----------->** | Fe2(SO4)3  |MnSO4 | K2SO4  | H2 |
|nestequiométrico  | 2    |  8     |  10  | | 5 |2 | 1 | 8 |
|nalimentado | 35.1   | 135.7 |  80.4   |  | 0 | 0 | 0 | 0 |
|Razão = nalimentado/nestequiométrico | 17.6  | 17  |  **8.04**  | | 0 | 0 | 0 | 0 |
| nreage| 16.1| 64.3 | **80.4** | | 40.2 | 16.1 | 8.0 | 64.3 |
| Excesso(%)| 118| 111 | 0 | |  0 | 0 | 0 | 0 |
| nfinal| 19.0| 71.4 | **0.0** |  | 40.2 | 16.1 | 8.0 | 64.3 |

```python
import sympy
from sympy.solvers import solve
from sympy import Symbol
x = Symbol ('x')
sol3 = solve (10*x-80.4*5,x)
print('N_reage de Fe2(SO4)3 =', sol3)
```

    N_reage de Fe2(SO4)3 = [40.2000000000000]

```python
import sympy
from sympy.solvers import solve
from sympy import Symbol
x = Symbol ('x')
sol4 = solve (10*x-80.4*2,x)
print('N_reage de MnSO4 =', sol4)
```

    N_reage de MnSO4 = [16.0800000000000]

```python
import sympy
from sympy.solvers import solve
from sympy import Symbol
x = Symbol ('x')
sol5 = solve (10*x-80.4*1,x)
print('N_reage de K2SO4 =', sol5)
```

    N_reage de K2SO4 = [8.04000000000000]

```python
import sympy
from sympy.solvers import solve
from sympy import Symbol
x = Symbol ('x')
sol6 = solve (10*x-80.4*8,x)
print('N_reage de H2 =', sol6)
```

    N_reage de H2 = [64.3200000000000]

**Para os produtos N_reage = N_final**

**Exercício Proposto 1:**
---

*Abra um novo notebook Colab e resolva o exercício proposto.*

Um reator é alimentado com 105 Kg.mols de $H_2O_2$, 37 Kg.mols de $KMnO_4$ e 500 Kg.mols de $H_2SO_4$.

\begin{align*}
\\5 H_2O_2 + 2 KMnO_4 + 3 H_2SO_4 \Rightarrow 5 O_2 +2 MnSO_4 + K_2SO_4 + 8 H_2O
\end{align*}

a) Determine qual o reagente limitante.

b) Determine a porcentagem em excesso dos demais reagentes.

c) Calcule quantos Kg.mols de $O_2$ serão produzidos.

**Gabarito:**
---

a) KMnO4

b) H2O2 = 13.5% ; H2SO4 = 800.1%

c) 92.5 Kg.mols de O2

**Referências Bibliográficas e Leituras Complementares**:
---

[BADINO & CRUZ, 2017](http://livresaber.sead.ufscar.br:8080/jspui/bitstream/123456789/2682/1/EA_Baldino%26Cruz_MassaEnergiaProcessosQuimicos.pdf)

[Notas Jupyter jckantor](https://nbviewer.jupyter.org/github/jckantor/CBE20255/blob/master/notebooks/01.01-Units-and-Engineering-Calculations.ipynb)

[Notas de aula BM_Com reação](https://pt.slideshare.net/neoson/aula-13-balano-de-massa-prof-nelson-area-1-290411)

[PHET - LAB VIRTUAL](https://phet.colorado.edu/pt_BR/)


```python
%matplotlib widget
from bmcs_beam.beam_bc.boundary_conditions import BoundaryConditions
from bmcs_beam.beam_bc.mq_profile import MQPProfile
from bmcs_beam.beam_bc.beam_design import BeamDesign

from bmcs_beam.bending.deflection_profile import DeflectionProfile
from bmcs_cross_section.mkappa.mkappa import MKappa

from bmcs_utils.api import InteractiveWindow
from sympy.physics.continuum_mechanics.beam import Beam
import bmcs_utils.api as bu

```

```python
mc = MKappa(low_kappa=-0.000014, high_kappa=0.00003)
bd = BeamDesign()
css = bd.cross_section_shape
csl = bd.cross_section_layout
csl.reinforcement.z_j = [10]
dp = DeflectionProfile(mc=mc)
dp.bd
bu.InteractiveWindow([css, csl, bd, mc.matrix, dp.bc, dp, dp.mc, mc], tight_layout=True).interact()
```

    Solve for eps_bot_t

    VBox(children=(Output(), Tab(children=(VBox(children=(GridBox(children=(FloatText(value=200.0, description='\\…


In order to successfully complete this assignment you must do the required reading, watch the provided videos and complete all instructions.  The embedded Google form must be entirely filled out and submitted on or before **11:59pm on Sunday January 12**.  Students must come to class the next day prepared to discuss the material covered in this assignment. answer

# Pre-Class Assignment: Optimization

We define Optimization to be a class of problems that try to find the "best" solution over a range of possible solutions.  Typically "best" means finding the maximum (or minimum) value of some sort of calculation.

### Goals for today's pre-class assignment

1. [What are we trying to optimize?](#What_are_we_trying_to_optimize)
1. [Common Optimization Approaches](#Common_Optimization_Approaches)
4. [Assignment wrap-up](#Assignment_wrap-up)

----

# 1. What are we trying to optimize?

The following are some example optimization problems we may use in computational and data sciences.

* **Mathematical Models:** Often in science we have designed a mathematical model which we then try to fit to experimental data. Sometimes this can be done directly (regression) but often requires trying different parameters. Model optimization (also called parameter optimization) is the process of searching through the model parameters to find the best ones that fit the data.

* **Machine Learning:** In Machine learning where we have a set of input data that correspond to expected outputs and we use the input/output data to "train" machine learning algorithms and minimize the error between the estimated output and the true output. These models tend to be more general (like neural networks) such that the algorithms not only search the parameters of the mathematical models but such over different models themselves.

* **Hyperparameter Optimization:** Algorithms can often have lots of input options that "tune" how the algorithm performs on a particular problem.  [Hyperparameter optimization](https://en.wikipedia.org/wiki/Hyperparameter_optimization) is the process of searching though all of these tunable parameters to find one that allow for best performance of the algorithm. They are called hyperparameter when the algorithms that are being optimized are machine learning algorithms that can be doing their own optimization (i.e. we are trying to learn the best machine learning algorithm). I like the term meta-optimization but that is just me.

* **Algorithmic Performance Optimization:** Another common application of optimization in computational and data science is optimizing the performance of an algorithm. Typically this means speeding up the calculations but may also include optimizing for resources such as computer memory.

----

# 2.  Common Optimization Approaches
When setting up an optimization problem there are two major factors that need to be consider. First, how is the "search space" defined for the problem and second, what is the "fitness" function that is being optimized.  For the purposes of this class we will consider the following example where we want to minimize the function $3x^2 - 10x + 4$ over the range [-10,10].

Let's consider the following approaches:

- [Analytic solutions](#Analytic_solutions)
- [Brute Force Methods](#Brute_Force_Methods)
- [Iterative Methods](#Iterative_Methods)
- [Random_Methods](#Random_Methods)

## Analytic solutions

Remember from basic calculus, if the search space for your problem can be represented as a analytic function $f$ that is being minimized it may be possible to directly calculate the minimum of that function.  First you would need to calculate the value of the function at the search space boundaries and then compare the boundary values with the values for which the derivative of $f$ is zero.

For more on this see [Mimimization and Maximization Refresher](https://mathinsight.org/minimization_maximization_refresher).

Although a powerful approach, analytic solutions only tend to work with problems that have a search space that are mathematically well defined.

We will use symbolic python (```sympy```) for this analitic example.  I don't have a great video on sympy but some of you may find the following video interesting.

✅ **DO THIS:** Watch the following video.  It is a little long but explains how to make Math look good in videos and has a short example of ```sympy``` (You may want to watch this one at double the speed):

```python
from IPython.display import YouTubeVideo
YouTubeVideo("qgSa7n_zQ3A",width=640,height=320, cc_load_policy=True)
```

The Following cell uses symbolic python library to represent a simple parabola and then find the minimum by solving for when the derivative is equal to zero. First we define our symbols and our function as follows:

```python
%matplotlib inline
import sympy as sym
sym.init_printing()
x = sym.symbols('x')
y = 3*x**2 - 10*x + 4
y
```

Plot the function over the default range:

```python
p1 = sym.plot(y)
```

Calculate the first derivative using the ```diff``` function:

```python
dy = sym.diff(y)
dy
```

Solve for ```x``` analytically:

```python
sym.solve(dy,x)
```

## Brute Force Methods

Assuming there is no easy analytic solution. Probably the most straightforward approach to finding the best solution is to use brute force methods which try to calculate all possible solutions and then pick the one that is the best. These methods require considerable computation power and are only feasible for small problems.

✅ **DO THIS:** Please watch this video on the basics of ```numpy```. This is included as a review to people still new to python and numpy.  Feel free to skip or run at double speed.

```python
from IPython.display import YouTubeVideo
YouTubeVideo("_hbWtNgstlI",width=640,height=320, cc_load_policy=True)
```

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

x = np.linspace(-10,10,1000)
y = 3*x**2 - 10*x + 4
plt.plot(x,y)

```

```python
i = np.argmin(y)
x[i]
```

## Iterative Methods

(Note: This section is a review of Gradient Decent (A type of optimization) from [0924_NN_in-class-assignment](0924_NN_in-class-assignment.ipynb) where we talked about Neural Networks.)

Gradient Decent is an example of an iterative method.  For iterative methods you start by making some sort of guess as to the "best" solution and then refine the guess with better and better solutions.

>Gradient descent is an algorithm that minimizes functions. Given a function defined by a set of parameters, gradient descent starts with an initial set of parameter values and iteratively moves toward a set of parameter values that minimize the function. This iterative minimization is achieved using calculus, taking steps in the negative direction of the function gradient.

From: https://spin.atomicobject.com/2014/06/24/gradient-descent-linear-regression/

```python
%matplotlib inline
import matplotlib.pylab as plt
from IPython.display import display, clear_output
import time
def show_animation(delay=0.01):
    fig = plt.gcf()
    time.sleep(delay)       # Sleep for half a second to slow down the animation
    clear_output(wait=True) # Clear output for dynamic display
    display(fig)            # Reset display
    fig.clear()             # Prevent overlapping and layered plots
```

```python
import numpy as np

# Define the function (parabola)
def f1(x):
    return 3*x**2 - 10*x + 4

#Derivative of F1 (also called the gradient)
def grad_f1(x):
    return 6*x - 10

# Choose the x points
x = np.array([i for i in range(-1000, 1000)])

#Plot the funciton
plt.plot(x, f1(x))
plt.ylim(-10,100)
plt.xlim(-10,10)
```

```python
##Gradient Decent

x0 = 7                                 # starting point for gradient descent
eta = 0.05                             # step size (a.k.a learning rate)
p = .0001                              # chosen precision
maxIter = 30                           # maximum number of iterations

former_min = x0
iterative_mins = [former_min]
iterCounter = 1

while True:
    new_min = former_min - eta * grad_f1(former_min)

    iterative_mins.append(new_min)
    if abs(former_min - new_min) <= p:
        print('Local min of function is %f' %new_min)
        print('Number of iterations: %d' %iterCounter)
        break
    else:
        former_min = new_min

    if iterCounter == maxIter:
        print('Local min not reached')
        break
    else:
        iterCounter += 1

    plt.figure(0)
    plt.plot(x, f1(x), lw=3)
    plt.ylim(-10,100)
    plt.xlim(-10,10)
    plt.title('Iterative descent to minimum:'+str(new_min))
    plt.plot(iterative_mins, f1(np.array(iterative_mins)), marker='o')
    show_animation(0.5)
```

```python
plt.plot(iterative_mins)
plt.title("Iterative change in minimum")
```

## Random Methods

Also called [Monte Carlo methods](https://en.wikipedia.org/wiki/Monte_Carlo_method), random methods are just that, random. These methods randomly start and different points within a search space. With enough random samples distributed across the search space we may be able to assume (for some problems) that we found a good solution. Monte Carlo Methods are often combined with iterative methods to come up with fairly robust solutions (there is no guarantee of optimality).

```python
from random import uniform

searchspace = [-10,10]
best_y = 100000
best_x = []
for i in range(1000):
    x = uniform(-10,10)
    y = 3*x**2 - 10*x + 4
    if y < best_y:
        best_y = y
        best_x = x
best_x
```

----

# 3. Optimization Libraries

There is no one "correct" optimization method for all problems.  You need to pick the method that best fits the problem at hand. The```SciPy``` module has an optimization library built in. Explore what it can do using the following code:

```python
import scipy.optimize as optimize
```

```python
dir(optimize)
```

```python
help(optimize)
```

```python
optimize.show_options()
```

✅ **DO THIS:**  See if you can figure out how to use the optimization library to solve the example problem provided.

✅ **QUESTION:** Why do you think these automated solution libraries (ex: ```scipy.optimize```) do not work well for the problem of Algorithmic Performance Optimization?  What is special about performance optimization that makes it particularly hard to optimize?

Put your answer to the above question here. Think about your answer and come to class ready to discuss your thoughts.

----

# 4. Portfolio Optimization with Machine Learning

Watch the following video about *Portfolio Optimization* and how to make profit with portfolio optimization and machine learning.

```python
from IPython.display import YouTubeVideo
YouTubeVideo("xR3Bm7BKEJw",width=640,height=360)
```

----

# 4. Assignment wrap-up

Please fill out the form that appears when you run the code below.  **You must completely fill this out in order to receive credit for the assignment!**

[Direct Link to Google Form](https://cmse.msu.edu/cmse802-pc-survey)

If you have trouble with the embedded form, please make sure you log on with your MSU google account at [googleapps.msu.edu](https://googleapps.msu.edu) and then click on the direct link above.

✅ **Assignment-Specific QUESTION:** Where you able to get the optimization library to work for the simple example?

Put your answer to the above question here

✅ **QUESTION:**  Summarize what you did in this assignment.

Put your answer to the above question here

✅ **QUESTION:**  What questions do you have, if any, about any of the topics discussed in this assignment after working through the jupyter notebook?

Put your answer to the above question here

✅ **QUESTION:**  How well do you feel this assignment helped you to achieve a better understanding of the above mentioned topic(s)?

Put your answer to the above question here

✅ **QUESTION:** What was the **most** challenging part of this assignment for you?

Put your answer to the above question here

✅ **QUESTION:** What was the **least** challenging part of this assignment for you?

Put your answer to the above question here

✅ **QUESTION:**  What kind of additional questions or support, if any, do you feel you need to have a better understanding of the content in this assignment?

Put your answer to the above question here

✅ **QUESTION:**  Do you have any further questions or comments about this material, or anything else that's going on in class?

Put your answer to the above question here

✅ **QUESTION:** Approximately how long did this pre-class assignment take?

Put your answer to the above question here

```python
from IPython.display import HTML
HTML(
"""

"""
)
```

---------
### Congratulations, we're done!

To get credit for this assignment you must fill out and submit the above Google From on or before the assignment due date.

### Course Resources:

- [Syllabus](https://docs.google.com/document/d/e/2PACX-1vTW4OzeUNhsuG_zvh06MT4r1tguxLFXGFCiMVN49XJJRYfekb7E6LyfGLP5tyLcHqcUNJjH2Vk-Isd8/pub)
- [Preliminary Schedule](https://docs.google.com/spreadsheets/d/e/2PACX-1vRsQcyH1nlbSD4x7zvHWAbAcLrGWRo_RqeFyt2loQPgt3MxirrI5ADVFW9IoeLGSBSu_Uo6e8BE4IQc/pubhtml?gid=2142090757&single=true)
- [Course D2L Page](https://d2l.msu.edu/d2l/home/912152)

© Copyright 2020,  Michigan State University Board of Trustees


# Computing the 4-Velocity Time-Component $u^0$, the Magnetic Field Measured by a Comoving Observer $b^{\mu}$, and the Poynting Vector $S^i$

## Authors: Zach Etienne & Patrick Nelson

[comment]: <> (Abstract: TODO)

**Notebook Status:**  Validated

**Validation Notes:** This module has been validated against a trusted code (the hand-written smallbPoynET in WVUThorns_diagnostics, which itself is based on expressions in IllinoisGRMHD... which was validated against the original GRMHD code of the Illinois NR group)

### NRPy+ Source Code for this module: [u0_smallb_Poynting__Cartesian.py](../edit/u0_smallb_Poynting__Cartesian/u0_smallb_Poynting__Cartesian.py)

[comment]: <> (Introduction: TODO)

# Table of Contents
$$\label{toc}$$

This notebook is organized as follows

1. [Step 1](#u0bu): Computing $u^0$ and $b^{\mu}$
    1. [Step 1.a](#4metric): Compute the 4-metric $g_{\mu\nu}$ and its inverse $g^{\mu\nu}$ from the ADM 3+1 variables, using the [`BSSN.ADMBSSN_tofrom_4metric`](../edit/BSSN/ADMBSSN_tofrom_4metric.py) ([**tutorial**](Tutorial-ADMBSSN_tofrom_4metric.ipynb)) NRPy+ module
    1. [Step 1.b](#u0): Compute $u^0$ from the Valencia 3-velocity
    1. [Step 1.c](#uj): Compute $u_j$ from $u^0$, the Valencia 3-velocity, and $g_{\mu\nu}$
    1. [Step 1.d](#gamma): Compute $\gamma=$ `gammaDET` from the ADM 3+1 variables
    1. [Step 1.e](#beta): Compute $b^\mu$
1. [Step 2](#poynting_flux): Defining the Poynting Flux Vector $S^{i}$
    1. [Step 2.a](#g): Computing $g^{i\nu}$
    1. [Step 2.b](#s): Computing $S^{i}$
1. [Step 3](#code_validation): Code Validation against `u0_smallb_Poynting__Cartesian` NRPy+ module
1. [Step 4](#appendix): Appendix: Proving Eqs. 53 and 56 in [Duez *et al* (2005)](https://arxiv.org/pdf/astro-ph/0503420.pdf)
1. [Step 5](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file

# Step 1: Computing $u^0$ and $b^{\mu}$ \[Back to [top](#toc)\]
$$\label{u0bu}$$

First some definitions. The spatial components of $b^{\mu}$ are simply the magnetic field as measured by an observer comoving with the plasma $B^{\mu}_{\rm (u)}$, divided by $\sqrt{4\pi}$. In addition, in the ideal MHD limit, $B^{\mu}_{\rm (u)}$ is orthogonal to the plasma 4-velocity $u^\mu$, which sets the $\mu=0$ component.

Note also that $B^{\mu}_{\rm (u)}$ is related to the magnetic field as measured by a *normal* observer $B^i$ via a simple projection (Eq 21 in [Duez *et al* (2005)](https://arxiv.org/pdf/astro-ph/0503420.pdf)), which results in the expressions (Eqs 23 and 24 in [Duez *et al* (2005)](https://arxiv.org/pdf/astro-ph/0503420.pdf)):

\begin{align}
\sqrt{4\pi} b^0 = B^0_{\rm (u)} &= \frac{u_j B^j}{\alpha} \\
\sqrt{4\pi} b^i = B^i_{\rm (u)} &= \frac{B^i + (u_j B^j) u^i}{\alpha u^0}\\
\end{align}

$B^i$ is related to the actual magnetic field evaluated in IllinoisGRMHD, $\tilde{B}^i$ via

$$B^i = \frac{\tilde{B}^i}{\gamma},$$

where $\gamma$ is the determinant of the spatial 3-metric.

The above expressions will require that we compute
1. the 4-metric $g_{\mu\nu}$ from the ADM 3+1 variables
1. $u^0$ from the Valencia 3-velocity
1. $u_j$ from $u^0$, the Valencia 3-velocity, and $g_{\mu\nu}$
1. $\gamma$ from the ADM 3+1 variables

## Step 1.a: Compute the 4-metric $g_{\mu\nu}$ and its inverse $g^{\mu\nu}$ from the ADM 3+1 variables, using the [`BSSN.ADMBSSN_tofrom_4metric`](../edit/BSSN/ADMBSSN_tofrom_4metric.py) ([**tutorial**](Tutorial-ADMBSSN_tofrom_4metric.ipynb)) NRPy+ module \[Back to [top](#toc)\]
$$\label{4metric}$$

We are given $\gamma_{ij}$, $\alpha$, and $\beta^i$ from ADMBase, so let's first compute

$$
g_{\mu\nu} = \begin{pmatrix}
-\alpha^2 + \beta^k \beta_k & \beta_i \\
\beta_j & \gamma_{ij}
\end{pmatrix}.
$$

```python
# Step 1: Initialize needed Python/NRPy+ modules
import sympy as sp                         # SymPy: The Python computer algebra package upon which NRPy+ depends
import NRPy_param_funcs as par             # NRPy+: Parameter interface
import indexedexp as ixp                   # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support
from outputC import outputC                # NRPy+: Basic C code output functionality
import BSSN.ADMBSSN_tofrom_4metric as AB4m # NRPy+: ADM/BSSN <-> 4-metric conversions

# Set spatial dimension = 3
DIM=3

thismodule = "smallbPoynET"

# Step 1.a: Compute the 4-metric $g_{\mu\nu}$ and its inverse
#           $g^{\mu\nu}$ from the ADM 3+1 variables, using the
#           BSSN.ADMBSSN_tofrom_4metric NRPy+ module
import BSSN.ADMBSSN_tofrom_4metric as AB4m
gammaDD,betaU,alpha = AB4m.setup_ADM_quantities("ADM")
AB4m.g4DD_ito_BSSN_or_ADM("ADM",gammaDD,betaU,alpha)
g4DD = AB4m.g4DD
AB4m.g4UU_ito_BSSN_or_ADM("ADM",gammaDD,betaU,alpha)
g4UU = AB4m.g4UU
```

## Step 1.b: Compute $u^0$ from the Valencia 3-velocity \[Back to [top](#toc)\]
$$\label{u0}$$

According to Eqs. 9-11 of [the IllinoisGRMHD paper](https://arxiv.org/pdf/1501.07276.pdf), the Valencia 3-velocity $v^i_{(n)}$ is related to the 4-velocity $u^\mu$ via

\begin{align}
\alpha v^i_{(n)} &= \frac{u^i}{u^0} + \beta^i \\
\implies u^i &= u^0 \left(\alpha v^i_{(n)} - \beta^i\right)
\end{align}

Defining $v^i = \frac{u^i}{u^0}$, we get

$$v^i = \alpha v^i_{(n)} - \beta^i,$$

and in terms of this variable we get

\begin{align}
g_{00} \left(u^0\right)^2 + 2 g_{0i} u^0 u^i + g_{ij} u^i u^j &= \left(u^0\right)^2 \left(g_{00} + 2 g_{0i} v^i + g_{ij} v^i v^j\right)\\
\implies u^0 &= \pm \sqrt{\frac{-1}{g_{00} + 2 g_{0i} v^i + g_{ij} v^i v^j}} \\
&= \pm \sqrt{\frac{-1}{(-\alpha^2 + \beta^2) + 2 \beta_i v^i + \gamma_{ij} v^i v^j}} \\
&= \pm \sqrt{\frac{1}{\alpha^2 - \gamma_{ij}\left(\beta^i + v^i\right)\left(\beta^j + v^j\right)}}\\
&= \pm \sqrt{\frac{1}{\alpha^2 - \alpha^2 \gamma_{ij}v^i_{(n)}v^j_{(n)}}}\\
&= \pm \frac{1}{\alpha}\sqrt{\frac{1}{1 - \gamma_{ij}v^i_{(n)}v^j_{(n)}}}
\end{align}

Generally speaking, numerical errors will occasionally drive expressions under the radical to either negative values or potentially enormous values (corresponding to enormous Lorentz factors). Thus a reliable approach for computing $u^0$ requires that we first rewrite the above expression in terms of the Lorentz factor squared: $\Gamma^2=\left(\alpha u^0\right)^2$:
\begin{align}
u^0 &= \pm \frac{1}{\alpha}\sqrt{\frac{1}{1 - \gamma_{ij}v^i_{(n)}v^j_{(n)}}}\\
\implies \left(\alpha u^0\right)^2 &= \frac{1}{1 - \gamma_{ij}v^i_{(n)}v^j_{(n)}} \\
\implies \gamma_{ij}v^i_{(n)}v^j_{(n)} &= 1 - \frac{1}{\left(\alpha u^0\right)^2} \\
&= 1 - \frac{1}{\Gamma^2}
\end{align}

In order for the bottom expression to hold true, the left-hand side must be between 0 and 1. Again, this is not guaranteed due to the appearance of numerical errors. In fact, a robust algorithm will not allow $\Gamma^2$ to become too large (which might contribute greatly to the stress-energy of a given gridpoint), so let's define $\Gamma_{\rm max}$, the largest allowed Lorentz factor.

Then our algorithm for computing $u^0$ is as follows:

If
$$R=\gamma_{ij}v^i_{(n)}v^j_{(n)}>1 - \frac{1}{\Gamma_{\rm max}^2},$$
then adjust the 3-velocity $v^i$ as follows:

$$v^i_{(n)} = \sqrt{\frac{1 - \frac{1}{\Gamma_{\rm max}^2}}{R}}v^i_{(n)}.$$

After this rescaling, we are then guaranteed that if $R$ is recomputed, it will be set to its ceiling value $R=R_{\rm max} = 1 - \frac{1}{\Gamma_{\rm max}^2}$.

Then, regardless of whether the ceiling on $R$ was applied, $u^0$ can be safely computed via

$$
u^0 = \frac{1}{\alpha \sqrt{1-R}}.
$$

```python
ValenciavU = ixp.register_gridfunctions_for_single_rank1("AUX","ValenciavU",DIM=3)

# Step 1: Compute R = 1 - 1/max(Gamma)
R = sp.sympify(0)
for i in range(DIM):
    for j in range(DIM):
        R += gammaDD[i][j]*ValenciavU[i]*ValenciavU[j]

GAMMA_SPEED_LIMIT = par.Cparameters("REAL",thismodule,"GAMMA_SPEED_LIMIT",10.0) # Default value based on
                                                                                # IllinoisGRMHD.
                                                                                # GiRaFFE default = 2000.0
Rmax = 1 - 1/(GAMMA_SPEED_LIMIT*GAMMA_SPEED_LIMIT)

rescaledValenciavU = ixp.zerorank1()
for i in range(DIM):
    rescaledValenciavU[i] = ValenciavU[i]*sp.sqrt(Rmax/R)

rescaledu0 = 1/(alpha*sp.sqrt(1-Rmax))
regularu0 =  1/(alpha*sp.sqrt(1-R))

computeu0_Cfunction  = """
/* Function for computing u^0 from Valencia 3-velocity. */
/* Inputs: ValenciavU[], alpha, gammaDD[][], GAMMA_SPEED_LIMIT (C parameter) */
/* Output: u0=u^0 and velocity-limited ValenciavU[] */\n\n"""

computeu0_Cfunction += outputC([R,Rmax],["const double R","const double Rmax"],"returnstring",
                               params="includebraces=False,CSE_varprefix=tmpR,outCverbose=False")

computeu0_Cfunction += "if(R <= Rmax) "
computeu0_Cfunction += outputC(regularu0,"u0","returnstring",
                               params="includebraces=True,CSE_varprefix=tmpnorescale,outCverbose=False")
computeu0_Cfunction += " else "
computeu0_Cfunction += outputC([rescaledValenciavU[0],rescaledValenciavU[1],rescaledValenciavU[2],rescaledu0],
                               ["ValenciavU0","ValenciavU1","ValenciavU2","u0"],"returnstring",
                               params="includebraces=True,CSE_varprefix=tmprescale,outCverbose=False")

print(computeu0_Cfunction)
```

    /* Function for computing u^0 from Valencia 3-velocity. */
    /* Inputs: ValenciavU[], alpha, gammaDD[][], GAMMA_SPEED_LIMIT (C parameter) */
    /* Output: u0=u^0 and velocity-limited ValenciavU[] */

    const double R = ((ValenciavU0)*(ValenciavU0))*gammaDD00 + 2*ValenciavU0*ValenciavU1*gammaDD01 + 2*ValenciavU0*ValenciavU2*gammaDD02 + ((ValenciavU1)*(ValenciavU1))*gammaDD11 + 2*ValenciavU1*ValenciavU2*gammaDD12 + ((ValenciavU2)*(ValenciavU2))*gammaDD22;
    const double Rmax = 1 - 1/((GAMMA_SPEED_LIMIT)*(GAMMA_SPEED_LIMIT));
    if(R <= Rmax) {
       u0 = 1/(alpha*sqrt(-((ValenciavU0)*(ValenciavU0))*gammaDD00 - 2*ValenciavU0*ValenciavU1*gammaDD01 - 2*ValenciavU0*ValenciavU2*gammaDD02 - ((ValenciavU1)*(ValenciavU1))*gammaDD11 - 2*ValenciavU1*ValenciavU2*gammaDD12 - ((ValenciavU2)*(ValenciavU2))*gammaDD22 + 1));
    }
     else {
       const double tmprescale_1 = sqrt((1 - 1/((GAMMA_SPEED_LIMIT)*(GAMMA_SPEED_LIMIT)))/(((ValenciavU0)*(ValenciavU0))*gammaDD00 + 2*ValenciavU0*ValenciavU1*gammaDD01 + 2*ValenciavU0*ValenciavU2*gammaDD02 + ((ValenciavU1)*(ValenciavU1))*gammaDD11 + 2*ValenciavU1*ValenciavU2*gammaDD12 + ((ValenciavU2)*(ValenciavU2))*gammaDD22));
       ValenciavU0 = ValenciavU0*tmprescale_1;
       ValenciavU1 = ValenciavU1*tmprescale_1;
       ValenciavU2 = ValenciavU2*tmprescale_1;
       u0 = fabs(GAMMA_SPEED_LIMIT)/alpha;
    }

## Step 1.c: Compute $u_j$ from $u^0$, the Valencia 3-velocity, and $g_{\mu\nu}$ \[Back to [top](#toc)\]
$$\label{uj}$$

The basic equation is

\begin{align}
u_j &= g_{\mu j} u^{\mu} \\
&= g_{0j} u^0 + g_{ij} u^i \\
&= \beta_j u^0 + \gamma_{ij} u^i \\
&= \beta_j u^0 + \gamma_{ij} u^0 \left(\alpha v^i_{(n)} - \beta^i\right) \\
&= u^0 \left(\beta_j + \gamma_{ij} \left(\alpha v^i_{(n)} - \beta^i\right) \right)\\
&= \alpha u^0 \gamma_{ij} v^i_{(n)} \\
\end{align}

```python
u0 = par.Cparameters("REAL",thismodule,"u0",1e300) # Will be overwritten in C code. Set to crazy value to ensure this.

uD = ixp.zerorank1()
for i in range(DIM):
    for j in range(DIM):
        uD[j] += alpha*u0*gammaDD[i][j]*ValenciavU[i]
```

## Step 1.d: Compute $b^\mu$ \[Back to [top](#toc)\]
$$\label{beta}$$

We compute $b^\mu$ from the above expressions:

\begin{align}
\sqrt{4\pi} b^0 = B^0_{\rm (u)} &= \frac{u_j B^j}{\alpha} \\
\sqrt{4\pi} b^i = B^i_{\rm (u)} &= \frac{B^i + (u_j B^j) u^i}{\alpha u^0}\\
\end{align}

$B^i$ is exactly equal to the $B^i$ evaluated in IllinoisGRMHD/GiRaFFE.

Pulling this together, we currently have available as input:
+ $\tilde{B}^i$
+ $u_j$
+ $u^0$,

with the goal of outputting now $b^\mu$ and $b^2$:

```python
M_PI = par.Cparameters("#define",thismodule,"M_PI","")
BU = ixp.register_gridfunctions_for_single_rank1("AUX","BU",DIM=3)

# uBcontraction = u_i B^i
uBcontraction = sp.sympify(0)
for i in range(DIM):
    uBcontraction += uD[i]*BU[i]

# uU = 3-vector representing u^i = u^0 \left(\alpha v^i_{(n)} - \beta^i\right)
uU = ixp.zerorank1()
for i in range(DIM):
    uU[i] = u0*(alpha*ValenciavU[i] - betaU[i])

smallb4U = ixp.zerorank1(DIM=4)
smallb4U[0] = uBcontraction/(alpha*sp.sqrt(4*M_PI))
for i in range(DIM):
    smallb4U[1+i] = (BU[i] + uBcontraction*uU[i])/(alpha*u0*sp.sqrt(4*M_PI))
```

# Step 2: Defining the Poynting Flux Vector $S^{i}$ \[Back to [top](#toc)\]
$$\label{poynting_flux}$$

The Poynting flux is defined in Eq. 11 of [Kelly *et al.*](https://arxiv.org/pdf/1710.02132.pdf) (note that we choose the minus sign convention so that the Poynting luminosity across a spherical shell is $L_{\rm EM} = \int (-\alpha T^i_{\rm EM\ 0}) \sqrt{\gamma} d\Omega = \int S^r \sqrt{\gamma} d\Omega$, as in [Farris *et al.*](https://arxiv.org/pdf/1207.3354.pdf):

$$
S^i = -\alpha T^i_{\rm EM\ 0} = -\alpha\left(b^2 u^i u_0 + \frac{1}{2} b^2 g^i{}_0 - b^i b_0\right)
$$

## Step 2.a: Computing $S^{i}$ \[Back to [top](#toc)\]
$$\label{s}$$

Given $g^{\mu\nu}$ computed above, we focus first on the $g^i{}_{0}$ term by computing
$$
g^\mu{}_\delta = g^{\mu\nu} g_{\nu \delta},
$$
and then the rest of the Poynting flux vector can be immediately computed from quantities defined above:
$$
S^i = -\alpha T^i_{\rm EM\ 0} = -\alpha\left(b^2 u^i u_0 + \frac{1}{2} b^2 g^i{}_0 - b^i b_0\right)
$$

```python
# Step 2.a.i: compute g^\mu_\delta:
g4UD = ixp.zerorank2(DIM=4)
for mu in range(4):
    for delta in range(4):
        for nu in range(4):
            g4UD[mu][delta] += g4UU[mu][nu]*g4DD[nu][delta]

# Step 2.a.ii: compute b_{\mu}
smallb4D = ixp.zerorank1(DIM=4)
for mu in range(4):
    for nu in range(4):
        smallb4D[mu] += g4DD[mu][nu]*smallb4U[nu]

# Step 2.a.iii: compute u_0 = g_{mu 0} u^{mu} = g4DD[0][0]*u0 + g4DD[i][0]*uU[i]
u_0 = g4DD[0][0]*u0
for i in range(DIM):
    u_0 += g4DD[i+1][0]*uU[i]

# Step 2.a.iv: compute b^2, setting b^2 = smallb2etk, as gridfunctions with base names ending in a digit
#          are forbidden in NRPy+.
smallb2etk = sp.sympify(0)
for mu in range(4):
    smallb2etk += smallb4U[mu]*smallb4D[mu]

# Step 2.a.v: compute S^i
PoynSU = ixp.zerorank1()
for i in range(DIM):
    PoynSU[i] = -alpha * (smallb2etk*uU[i]*u_0 + sp.Rational(1,2)*smallb2etk*g4UD[i+1][0] - smallb4U[i+1]*smallb4D[0])
```

# Step 3: Code Validation against `u0_smallb_Poynting__Cartesian` NRPy+ module \[Back to [top](#toc)\]
$$\label{code_validation}$$

Here, as a code validation check, we verify agreement in the SymPy expressions for u0, smallbU, smallb2etk, and PoynSU between

1. this tutorial and
2. the NRPy+ [u0_smallb_Poynting__Cartesian module](../edit/u0_smallb_Poynting__Cartesian/u0_smallb_Poynting__Cartesian.py).

```python
import sys
import u0_smallb_Poynting__Cartesian.u0_smallb_Poynting__Cartesian as u0etc
u0etc.compute_u0_smallb_Poynting__Cartesian(gammaDD,betaU,alpha,ValenciavU,BU)

if u0etc.computeu0_Cfunction != computeu0_Cfunction:
    print("FAILURE: u0 C code has changed!")
    sys.exit(1)
else:
    print("PASSED: u0 C code matches!")

for i in range(4):
    print("u0etc.smallb4U["+str(i)+"] - smallb4U["+str(i)+"] = "
              + str(u0etc.smallb4U[i]-smallb4U[i]))

print("u0etc.smallb2etk - smallb2etk = " + str(u0etc.smallb2etk-smallb2etk))

for i in range(DIM):
    print("u0etc.PoynSU["+str(i)+"] - PoynSU["+str(i)+"] = "
              + str(u0etc.PoynSU[i]-PoynSU[i]))
```

    PASSED: u0 C code matches!
    u0etc.smallb4U[0] - smallb4U[0] = 0
    u0etc.smallb4U[1] - smallb4U[1] = 0
    u0etc.smallb4U[2] - smallb4U[2] = 0
    u0etc.smallb4U[3] - smallb4U[3] = 0
    u0etc.smallb2etk - smallb2etk = 0
    u0etc.PoynSU[0] - PoynSU[0] = 0
    u0etc.PoynSU[1] - PoynSU[1] = 0
    u0etc.PoynSU[2] - PoynSU[2] = 0

# Step 4: Appendix: Proving Eqs. 53 and 56 in [Duez *et al* (2005)](https://arxiv.org/pdf/astro-ph/0503420.pdf)
$$\label{appendix}$$

$u^\mu u_\mu = -1$ implies

\begin{align}
g^{\mu\nu} u_\mu u_\nu &= g^{00} \left(u_0\right)^2 + 2 g^{0i} u_0 u_i + g^{ij} u_i u_j = -1 \\
\implies &g^{00} \left(u_0\right)^2 + 2 g^{0i} u_0 u_i + g^{ij} u_i u_j + 1 = 0\\
& a x^2 + b x + c = 0
\end{align}

Thus we have a quadratic equation for $u_0$, with solution given by

\begin{align}
u_0 &= \frac{-b \pm \sqrt{b^2 - 4 a c}}{2 a} \\
&= \frac{-2 g^{0i}u_i \pm \sqrt{\left(2 g^{0i} u_i\right)^2 - 4 g^{00} (g^{ij} u_i u_j + 1)}}{2 g^{00}}\\
&= \frac{-g^{0i}u_i \pm \sqrt{\left(g^{0i} u_i\right)^2 - g^{00} (g^{ij} u_i u_j + 1)}}{g^{00}}\\
\end{align}

Notice that (Eq. 4.49 in [Gourgoulhon](https://arxiv.org/pdf/gr-qc/0703035.pdf))
$$
g^{\mu\nu} = \begin{pmatrix}
-\frac{1}{\alpha^2} & \frac{\beta^i}{\alpha^2} \\
\frac{\beta^i}{\alpha^2} & \gamma^{ij} - \frac{\beta^i\beta^j}{\alpha^2}
\end{pmatrix},
$$
so we have

\begin{align}
u_0 &= \frac{-\beta^i u_i/\alpha^2 \pm \sqrt{\left(\beta^i u_i/\alpha^2\right)^2 + 1/\alpha^2 (g^{ij} u_i u_j + 1)}}{1/\alpha^2}\\
&= -\beta^i u_i \pm \sqrt{\left(\beta^i u_i\right)^2 + \alpha^2 (g^{ij} u_i u_j + 1)}\\
&= -\beta^i u_i \pm \sqrt{\left(\beta^i u_i\right)^2 + \alpha^2 \left(\left[\gamma^{ij} - \frac{\beta^i\beta^j}{\alpha^2}\right] u_i u_j + 1\right)}\\
&= -\beta^i u_i \pm \sqrt{\left(\beta^i u_i\right)^2 + \alpha^2 \left(\gamma^{ij}u_i u_j + 1\right) - \beta^i\beta^j u_i u_j}\\
&= -\beta^i u_i \pm \sqrt{\alpha^2 \left(\gamma^{ij}u_i u_j + 1\right)}\\
\end{align}

Now, since

$$
u^0 = g^{\alpha 0} u_\alpha = -\frac{1}{\alpha^2} u_0 + \frac{\beta^i u_i}{\alpha^2},
$$

we get

\begin{align}
u^0 &= \frac{1}{\alpha^2} \left(u_0 + \beta^i u_i\right) \\
&= \pm \frac{1}{\alpha^2} \sqrt{\alpha^2 \left(\gamma^{ij}u_i u_j + 1\right)}\\
&= \pm \frac{1}{\alpha} \sqrt{\gamma^{ij}u_i u_j + 1}\\
\end{align}

By convention, the relativistic Gamma factor is positive and given by $\alpha u^0$, so we choose the positive root. Thus we have derived Eq. 53 in [Duez *et al* (2005)](https://arxiv.org/pdf/astro-ph/0503420.pdf):

$$
u^0 = \frac{1}{\alpha} \sqrt{\gamma^{ij}u_i u_j + 1}.
$$

Next we evaluate

\begin{align}
u^i &= u_\mu g^{\mu i} \\
&= u_0 g^{0 i} + u_j g^{i j}\\
&= u_0 \frac{\beta^i}{\alpha^2} + u_j \left(\gamma^{ij} - \frac{\beta^i\beta^j}{\alpha^2}\right)\\
&= \gamma^{ij} u_j + u_0 \frac{\beta^i}{\alpha^2} - u_j \frac{\beta^i\beta^j}{\alpha^2}\\
&= \gamma^{ij} u_j + \frac{\beta^i}{\alpha^2} \left(u_0  - u_j \beta^j\right)\\
&= \gamma^{ij} u_j - \beta^i u^0,\\
\implies v^i &= \frac{\gamma^{ij} u_j}{u^0} - \beta^i
\end{align}

which is equivalent to Eq. 56 in [Duez *et al* (2005)](https://arxiv.org/pdf/astro-ph/0503420.pdf). Notice in the last step, we used the above definition of $u^0$.

# Step 5: Output this notebook to $\LaTeX$-formatted PDF file \[Back to [top](#toc)\]
$$\label{latex_pdf_output}$$

The following code cell converts this Jupyter notebook into a proper, clickable $\LaTeX$-formatted PDF file. After the cell is successfully run, the generated PDF may be found in the root NRPy+ tutorial directory, with filename
[Tutorial-u0_smallb_Poynting-Cartesian.pdf](Tutorial-u0_smallb_Poynting-Cartesian.pdf) (Note that clicking on this link may not work; you may need to open the PDF file through another means.)

```python
import cmdline_helper as cmd    # NRPy+: Multi-platform Python command-line interface
cmd.output_Jupyter_notebook_to_LaTeXed_PDF("Tutorial-u0_smallb_Poynting-Cartesian")
```

    [pandoc warning] Duplicate link reference `[comment]' "source" (line 22, column 1)
    Created Tutorial-u0_smallb_Poynting-Cartesian.tex, and compiled LaTeX file
        to PDF file Tutorial-u0_smallb_Poynting-Cartesian.pdf


# Visualising the Poisson-Boltzmann Equation for Potentiometric Sensors: Changes in Surface Potential due to Changes in Surface Charge
#### Benjamin Lowe, 10th January 2018

In the field of potentiometric sensing, there is interest in predicting the relationship between molecules binding to the surface and the change in the surface potential. With this relationship well understood, it would be possible to rationally designing sensors for enhanced response. One of the simplest and most widely used models for modelling surface potentials is the Poisson-Boltzmann equation.
- In some Poisson-Bolzmann-based models, the binding of a biomolecule is treated as simply adding to the surface charg. This is what will be considered in this iPython notebook:

i.e. The change in surface potential ($\Delta\psi_{\text{bio}}$) can be calculated before and after analyte binding, assuming that the intrinsic (i.e. silanolate, in this work) surface charge ($\sigma_{\text{surf}}$) is additive with the charge contribution from DNA ($\sigma_{\text{DNA}}$). In other words, the biomolecule is assumed to be an infinitely thin smeared layer of charge on the surface which does not change the intrinsic charge, i.e. the biomolecule does not affect the charging of silanolate groups:

\begin{equation}\Delta\psi=\psi(\sigma_{\text{surf}}+\sigma_{\text{bio}})-\phi(\sigma_{\text{surf}})\end{equation}
- Finite-size interactions are neglected, meaning that the model becomes invalid at very high ionic strength (due to electrolyte collisions) or very high surface charge (due to build up of unrealistically high concentrations of ions at the surface). These effects are sometimes corrected for my the introduction of a Stern layer, which is not considered within this iPython notebook.
- Effects due to ion-ion correlation, which are particularly important for multivalent ions

The Poisson-Boltzmann Equation has proved accurate for describing the properties of specific systems such as those monovalent 1:1 valence, low-ionic strength electrolyte systems with low surface charges. Unfortunately the systems often relevant to potentiometric biosensing are very different to this with:

* highly charged oxides surface e.g. such as silica at pH 7.4
* large bulky biomolecules which cannot be treated as a continuum accurately due to steric interactions
* highly charged biomolecules (e.g. DNA)
* High ionic strengths (e.g. blood samples or Phosphate Buffered Saline for biosensing have a ionic strength close to 0.15 M)

Whilst these assumptions are far from realistic when modelling highly charged biomolecular analyte such as DNA molecule binding, their ease-of-computation makes them an appealing starting point for understanding the potential-surface charge relationship.

```python
#The commands in this cell will be used for all simulations in the notebook, therefore please run this cell first.
%pylab inline
import pandas
import sympy
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
sympy.init_printing()
#%matplotlib  add to each cell if you wish to view using matplotlib GUI rather than in-line ipython figures
#%reset -f # clear the namespace
mycolormap="Blues"
example_colors=sns.color_palette(mycolormap, n_colors=255)

def plot_heatmap(data, xspan, yspan, label, ionic_stren):
    df = pandas.DataFrame(np.array(data), columns=xspan, index=yspan)
    df = df[df.columns].astype(float)
    fig=plt.figure()
    ax = fig.add_subplot(111)
    a=plt.contourf(xspan,yspan,df,25,extend='both',cmap=mycolormap) #vmax=100
    cbar=plt.colorbar(a) #ticks=v
    cbar.ax.set_ylabel(label)
    plt.xlabel("Bare Surface Charge (C/m$^2$)")
    plt.ylabel("Bio Surface Charge (C/m$^2$)")
    plt.title("ionic strength={0:.3f} M".format(ionic_stren))
    plt.tight_layout()
    return fig, ax
```

    Populating the interactive namespace from numpy and matplotlib

## Experimental Data: The Surface Charge of Oxides and Charge Densities
### For the simulations in this notebook, experimental data is used to provide a sensible range of parameters for the silanolate surface charge density, biomolecule charge density and the ionic strength.

A common oxide material for a sensor surface is silica. The surface of silica charges due to protonation/deprotonation reactions with H$^{+}$ and OH$^{-}$ in water ([B. M. Lowe, C.-K. Skylaris and N. G. Green, J. Colloid Interf. Sci., 2015, 451, 231–244.](http://www.sciencedirect.com/science/article/pii/S0021979715003240))

__Based on INTERFACE papers: [Emami, F. S. et al. Chem. Mater. 2014, 26](http://dx.doi.org/10.1021/cm500365c)__

__e.g. Quartz surfaces, silica nanoparticles >200 nm size, silica at pH > 9__
__with Q2 and Q2/Q3 Hydroxyl terminated surface with silanol density of 9.4 to 4.7 per nm2 have a surface charge of:__
* pH 2: ~0 per nm2
* pH 5: ~0.5 per nm2
* pH 7: ~1.0 per nm2
* pH >9: ~1.5 per nm2

__e.g. Most silica glasses, porous silica, silica nanoparticles <200 nm size__
__with Q3 silanol groups with surface silanol density of 4.7 per nm2__
* pH 3: ~0 per nm2 (0%)
* pH 5: ~0.3 per nm2 (6%)
* pH 7: ~0.6 per nm2 (13%)
* pH > 9: ~0.9 per nm2 (20%)

__Silica surfaces and nanoparticles annealed at 200-1000 °C__
* Q3/Q4 and Q4 with silanol density 4.7 to 0 per nm2
* pH 4: ~0 per nm2
* pH 7: 0-0.6 per nm2 depending on Q3 content

[WolframAlpha](http://www.wolframalpha.com/input/?i=0.2+Coulombs+per+meter+squared+to+elementary+charges+per+nanometer+squared) is convenient for Unit Conversions: e.g. 0.2 C/(m^2) is 1.25 elementary charges/(nm^2)

__The ionic strength dependence of surface charging based on Titration Experiments of silica colloids can be found in P. Somasundaran.
Reference: '[Encyclopedia of Surface and Colloid Science](http://books.google.co.uk/books?id=P6jMToZhv_EC&pg=PA1863&dq=effect+of+salt+on+silica+surface+charge&hl=en&sa=X&ei=uXfbU-6ALseY1AWPhYHYBg&ved=0CCEQ6AEwAA#v=onepage&q=effect%20of%20salt%20on%20silica%20surface%20charge&f=false)', Chapter Title:'Electric Charge Density of Silica, Alumina, and Related Surfaces' Volume 3, 2006,  pg 1863, Fig 6. __

For example:
* For 0.1 M, pH=7.5, surface charge ~-0.03 C/m^2 to -0.11 C/m^2 depending on reference
* For 0.01 M pH=7.6, surface charge ~0.05 to 0.03 C/m^2 depending on reference

So a sensible range of parameters would be:
* Intrinsic Surface Charge: sigma_0 -0.01 --> -0.25  C/m^2 for silica
* Ionic Strength: ionic_molar --> 0.01 --> 1M
* Biomolecular Charge: sigma_bio --> (4*1e18)*e #C/m^2 (1 proteins per 1 nm^-2, with 4 charges. --> 0.65 C)

## Methodology
First the surface potential is obtained as a function of surface charge. The change in surface potential is calculated before and after analyte binding, assuming that the silanolate (i.e. intrinsic surface charge) ($\sigma_{\text{surf}}$) is additive with the charge contribution from DNA $\sigma_{\text{bio}}$ , i.e. assuming biomolecule is an infinitely thin smeared layer on the surface:$\Delta\phi=\phi(\sigma_{\text{surf}}+\sigma_{\text{bio}})-\phi(\sigma_{\text{surf}}$)

## Poisson-Boltzmann Equation/Gouy-Chapmann Model.
#### The potential as a function of surface charge for this equation is often called the 'Grahame Equation'. The following equation shows the difference in surface potential due to a change in surface charge by $\sigma_{\text{bio}}$ from the initial surface charge.

```python
####References that use this equation:#####
#Uslu, F. and Ingebrandt, S. and Mayer, D. and Böcker-Meffert, S. and Odenthal, M. and Offenhäusser, A., "Labelfree Fully Electronic Nucleic Acid Detection System Based on a Field-Effect Transistor Device", Biosens. Bioelectron. 19, 12 (2004), pp. 1723--1731.
#Jayant, Krishna and Auluck, Kshitij and Rodriguez, Sergio and Cao, Yingqiu and Kan, Edwin C., "Programmable Ion-Sensitive Transistor Interfaces. III. Design Considerations, Signal Generation, and Sensitivity Enhanc…", Phys. Rev. E 89, 5 (2014), pp. 052817.
#Sakata, Toshiya and Miyahara, Yuji, "DNA Sequencing Based on Intrinsic Molecular Charges", Angew. Chem. Int. Ed. 45, 14 (2006), pp. 2225--2228.
#Fritz, Jürgen and Cooper, Emily B. and Gaudet, Suzanne and Sorger, Peter K. and Manalis, Scott R., "Electronic Detection of DNA by Its Intrinsic Molecular Charge", PNAS 99, 22 (2002), pp. 14142--14146.
###########################################
psi_0, T, sigma_0, sigma_bio, n_0, z, e, k, epsilon_medium, avo_L = sympy.symbols("psi_0, T, sigma_0, sigma_bio, n_0, z, e, k, epsilon_medium, avo_L")
delta_psi=(2*k*T)/(z*e)*sympy.asinh(((sigma_0+sigma_bio)/sympy.sqrt(8*epsilon_medium*k*T*n_0*avo_L))) - (2*k*T)/(z*e)*sympy.asinh(((sigma_0)/sympy.sqrt(8*epsilon_medium*k*T*n_0*avo_L)))
delta_psi
```

```python
#Set Global Constants for this iPython notebook
from scipy.constants import epsilon_0, elementary_charge, N_A, Boltzmann, pi
T=298.15 #K
k=Boltzmann #J/K
e=elementary_charge #Coulombs
z=1 #electrolyte valence
avo_L=N_A #mol^-1
epsilon_medium=epsilon_0*78.5 #free space * relative permitivity of water: F/m or m-3 kg-1 s4 A2 or C/epsilon_0
```

```python
def delta_psi_monovalent(x,y, ionic_stren):
    """This function find the change in surface potential due to addition of additional charge to the surface, e.g. representing biomolecule binding in a BioFET
    The model is the Poisson-Boltzmann Equation (i.e. the Grahame equation solved for potential) for a monovalent 1:1 electrolyte
    Input: The Ionic Strength (in Molar), 'ionic_stren'
    Input: 'x' list of native surface charges (C/m^2)
    Input:  'y' list of biomolecular charges (C/m^2)
    Returns: The change in surface potential (in mV) due to a biomolecule charge 'y' added to a bare silica surface charge 'x' (C/m^2)

    Note that, unlike the Debye-Hückel equation, this equation has not been linearised by assuming an inverse hyperbolic sine (asinh) term can be apprximated to x^2.
    Linearising results in the simpler, linear, Debye-Huckel equation, and the concept of a Debye Length
    """
    sigma_bio=y
    sigma_0=x
    n_0=ionic_stren*1000 # mol/m^3
    t1=(2*k*T)/(z*e)*sympy.asinh(((sigma_0+sigma_bio)/sympy.sqrt(8*epsilon_medium*k*T*n_0*avo_L)))
    t2=(2*k*T)/(z*e)*sympy.asinh(((sigma_0          )/sympy.sqrt(8*epsilon_medium*k*T*n_0*avo_L)))
    delta_psi=t1-t2
    return delta_psi*1000 #mV

xchoice="Bare Surface Charge (C/m$^2$)"
ychoice="Ionic Strength (M)"

#xspan=np.arange(-0.01,-0.301,-0.01) #Bare Surface Charge (C/m$^2$)
#yspan=np.arange(-0.04, 0.041, 0.001) #Bio Surface Charge (C/m$^2$)

xspan=np.arange(-0.01,-1.0,-0.01) #Bare Surface Charge (C/m$^2$)
yspan=np.arange(-0.04, 0.041, 0.01) #Bio Surface Charge (C/m$^2$)

#Select ionic strengths in the following list
for ionic_stren in [0.03,0.3,3]: #0.1627 M = 1x PBS ionic strength

    data=[]
    for y in yspan:
        row=[]
        for x in xspan:
            row.append(delta_psi_monovalent(x,y, ionic_stren))
        data.append(row)

    fig, ax = plot_heatmap(data, xspan, yspan, 'Surface Potential Shift $\Delta$$_{P-B,Bio}$V$_{surf}$ (mV)', ionic_stren)

```

## Debye-Hückel Equation
Note: Due to the linearisation this equation is only strictily valid at low surface potentials compared to thermal energy
e.g. at 300 K with 1:1 electrolyte, valid approximately below [25 mV](https://www.wolframalpha.com/input/?i=boltzmann+constant+*+300+K%2Felementary+charge++in+mV)

```python
def delta_psi_linear_anyvalent(y, ionic_stren):
    """Calculates the change in surface potential due to addition of additional charge to the surface, e.g. representing biomolecule binding in a BioFET
    The model is the  Debye-Hückel (or linearised Poisson_Boltzmann equation)
    Input: The Ionic Strength (in Molar), 'ionic_stren'
    Input:  'y' list of biomolecular charges to plot (C/m^2)
    Returns: The change in surface potential (in mV) due to a biomolecule charge 'y' added to a bare silica surface charge 'x' (C/m^2)

    Note that the Debye-Hückel equation is SurfaceCharge/(permitivity*Debye Length) and thus is linear with respect to Surface Charge.
    Thus the difference in surface charge after addition relative to before addition is the same value regardless of the surface charge,
    and thus the equation shows now surface charge dependence on the signal due to biomolecules.
    """
    number_density_ionic_strength=(ionic_stren*1000.)*avo_L
    kappa=np.sqrt((2*number_density_ionic_strength*(e**2))/(epsilon_medium*k*T))
    sigma_bio=y

    delta_psi=(sigma_bio)/(epsilon_medium*kappa)
    return delta_psi*1000 #mV

xchoice="Bare Surface Charge (C/m$^2$)"
ychoice="Ionic Strength (M)"

#xspan=np.arange(-0.01,-0.3,-0.01) #Bare Surface Charge (C/m$^2$). --> has no effect in this model.
yspan=np.arange(-0.04, 0.04, 0.001) #Bio Surface Charge (C/m$^2$)

#Select ionic strengths in the following list
for ionic_stren in [0.03,0.3,3]: #0.1627 M = 1x PBS ionic strength

    data=[]
    for y in yspan:
        row=[]
        for x in xspan:
            row.append(delta_psi_linear_anyvalent(y, ionic_stren))
        data.append(row)

    fig, ax = plot_heatmap(data, xspan, yspan, 'Surface Potential Shift $\Delta$$_{D-H,Bio}$V$_{surf}$ (mV)', ionic_stren)

```

## Numerical Solution of Poisson-Boltzmann Equation for Mixed Valence Electrolyte
#### e.g. Phosphate Buffered Solution (PBS) 1x PBS buffer: 138 mM NaCl, 2.7 mM KCl, 1.9 mM, NaH$_2$PO$_4$, 8.1 mM Na$_2$HPO$_4$
Note: This is the full (non-linearised) Poisson-Boltzmann Equation. This equation cannot accurately simulate multivalent ions due to correlation between ions results in effects such as 'charge inversion' observed in experiments. [B. M. Lowe, Y. Maekawa, Y. Shibuta, T. Sakata, C.-K. Skylaris and N. G. Green, Phys. Chem. Chem. Phys., 2016, 19, 2687.](http://pubs.rsc.org/en/content/articlelanding/2017/cp/c6cp04101a)

```python
x = sympy.symbols("x") # The potential to be solved for numerically.

def delta_psi_multivalent(sigma_0,sigma_bio, dilution=1):
    """
    Calculates the change in surface potential due to addition of additional charge to the surface, e.g. representing biomolecule binding in a BioFET
    *Numerically* solves the Poisson-Boltzmann equation for a multivalent system such as Phosphate Buffered Saline
    The 'system' variable sets the composition of the electrolyte per ion in (ionic strength (M), elementary charge sign and magnitude (q)) tuples,
    e.g. NaCl at 0.5 M would simply be (0.5, 1), (0.5,-1) for Na+ and Cl-
    Input: 'sigma_0' - The surface charge (C/m^2)
    Input: 'sigma_bio' - The biomolecular charge (C/m^2)
    Input: 'dilution' - A constant multiplying factor to adjust ionic strength (162.7 mM) of the system
    Returns: Change in surface potential due to biomolecule charge being added to surface charge (mV)
    e.g. The default 'system' is PBS at standard ionic strength (1X) thus if 'dilution' is 0.1, it represents 0.1X PBS at 16.27 mM
    """
    #PBS at 1X ionic strength = 0.1627 M : Reference: N. Lloret, R. S. Frederiksen, T. C. Møller, N. I. Rieben, S. Upadhyay, L. D. Vico, J. H. Jensen, J. Nygård and K. L. Martinez, Nanotechnology, 2013, 24, 035501. (supplementary information)
    system=[(0.138, 1), (0.138,-1), (0.0027, 1), (0.0027, -1), (0.0019, 1), (0.0019, -1), (0.00405, 1), (0.00405, 1), (0.0081, -2)]

    #Solving Grahame Equation, Equation 1.90 from Physics of Soft Colloidal Matter, {Interaction potentials and colloidal stability}, Peter Lang, Fall term 2007/08, pg 35
        #http://arachnoid.com/IPython/equation_solving.html
    sumed=0
    for conc, charge in system:
        number_dens=conc*dilution*1000.*avo_L
        sumed+=number_dens*(sympy.exp((-charge*e*x)/(k*T))-1)

    prefactor=(2*epsilon_medium*k*T)
    psi_multivalent=sympy.nsolve((prefactor)*sumed - ((sigma_0)**2),x,-1e-3, solver='newton') #1e-3 is x0, initial guess. Solve for sympy symbol x. http://docs.sympy.org/0.7.0/modules/mpmath/calculus/optimization.html
    psi_multivalent_bio=sympy.nsolve((prefactor)*sumed - ((sigma_0+sigma_bio)**2),x,-1e-3, solver='newton')
    delta_psi=psi_multivalent_bio-psi_multivalent
    return delta_psi*1000 #mV

xspan=np.arange(-0.01,-0.31,-0.01) #Bare Surface Charge (C/m$^2$)
yspan=np.arange(-0.04, 0.04, 0.01) #Bio Surface Charge (C/m$^2$)

#Select ionic strengths in the following list
for dilution in [0.03/0.1627,0.3/0.1627,3/0.1627]:

    data=[]
    for current_sigmabio in yspan:
        row=[]
        for current_sigma0 in xspan:
            row.append(delta_psi_multivalent(current_sigma0,current_sigmabio,dilution))
        data.append(row)

    fig, ax = plot_heatmap(data, xspan, yspan, 'Surface Potential Shift $\Delta$$_{P-B-mixed,Bio}$V$_{surf}$ (mV)',dilution*0.1627)
```

## Numerical Solution to the Modified Poisson-Boltzmann Equation
[M. S. Kilic, M. Z. Bazant and A. Ajdari, Phys. Rev. E, 2007, 75, 021502.](http://link.aps.org/doi/10.1103/PhysRevE.75.021502)

```python
x = sympy.symbols("x") # The potential to be solved for numerically.

def delta_psi_mpb(sigma_0,sigma_bio, ionic_stren):
    n_0=ionic_stren*1000.0 # mol/m^3
    number_density_ionic_strength=n_0*avo_L
    kappa=sympy.sqrt((2*n_0*avo_L*(e**2))/(epsilon_medium*k*T))
    debyel=1/kappa

    a=184*1e-12 #meters, Size of the ion (in meters, Table 1 paper , Na+) 125 is KCl. "Numerical Simulation of the Electrical Double Layer Based on the
                #  Poisson-Boltzmann Models for AC Electroosmosis Flows Pascale Pham*(1), Matthieu Howorth(1), Anne Planat-Chrétien(1) and Sedat Tardu(

    nu=2*(a**3)*number_density_ionic_strength # Packing parameter, unitless
    #Note sinh and not asinh is used as this is not the analytical solution for potential, but the expression for charge density in the edl i.e. surface charge
    #Note that q_edl is positive here, but as I want q_surface I've put a negative sign in front which cancels with the negative sign in the paper to make no sign.
    eqn=(sympy.sign(x)*2*e*z*number_density_ionic_strength*debyel*sympy.sqrt((2/nu)*sympy.log(1+2*nu*(sympy.sinh(z*e*x/2/k/T))**2)))-sigma_0
    eqn_bio=(sympy.sign(x)*2*e*z*number_density_ionic_strength*debyel*sympy.sqrt((2/nu)*sympy.log(1+2*nu*(sympy.sinh(z*e*x/2/k/T))**2)))-(sigma_0+sigma_bio)
    psi=sympy.nsolve(eqn, x, -1e-3, solver='secant') #http://docs.sympy.org/0.7.0/modules/mpmath/calculus/optimization.html
    psi_bio=sympy.nsolve(eqn_bio, x, -1e-3, solver='secant')
    delta_psi=psi_bio-psi
    return delta_psi*1000 #mV

#xspan=np.arange(-0.01,-0.31,-0.01) #Bare Surface Charge (C/m$^2$)
#yspan=np.arange(-0.04, 0.041, 0.01) #Bio Surface Charge (C/m$^2$)
xspan=np.arange(-0.01,-1.0,-0.01) #Bare Surface Charge (C/m$^2$)
yspan=np.arange(-0.04, 0.041, 0.01) #Bio Surface Charge (C/m$^2$)

#Select ionic strengths in the following list
for ionic_stren in [0.03,0.3,3]:

    data=[]
    for current_sigmabio in yspan:
        row=[]
        for current_sigma0 in xspan:

            row.append(delta_psi_mpb(current_sigma0,current_sigmabio,ionic_stren))
        data.append(row)

    fig, ax = plot_heatmap(data, xspan, yspan, 'Surface Potential Shift $\Delta$$_{P-B-mixed,Bio}$V$_{surf}$ (mV)',ionic_stren)
```

# Comparison between the Surface Charge/Surface Potential Relationship for the Modified Poisson-Boltzmann and Poisson-Boltzmann Equation

```python
x = sympy.symbols("x") # The potential to be solved for numerically.

def delta_psi_mpb(sigma_0, ionic_stren, SCALE=1):
    """Modified Poisson Boltzmann Equation, as shown in previous cell but without "biomolecule".
    Return  the surface potential as a function of surface charge 'x'.
    #SCALE parameter is a scale factor for the size of ions in the model (empirical parameter) """

    n_0=ionic_stren*1000.0 # mol/m^3
    number_density_ionic_strength=n_0*avo_L
    kappa=sympy.sqrt((2*n_0*avo_L*(e**2))/(epsilon_medium*k*T))
    debyel=1/kappa

    a=184*1e-12 #meters, Size of the ion (in meters, Table 1 paper, Na+) 125 is KCl
    nu=2*((a*SCALE)**3)*number_density_ionic_strength # Packing parameter, unitless

    #Note that q_edl is positive here, but as I want q_surface I've put a negative sign in front which cancels with the negative sign in the paper to make no sign.
    sinhterm=sympy.sinh(z*e*x/(2*k*T))**2
    innerrootterm=(2/nu)*sympy.log(1+2*nu*sinhterm)
    eqn=(  sympy.sign(x)*2*e*z*number_density_ionic_strength*debyel*sympy.sqrt(innerrootterm)  )  -  sigma_0
    psi=sympy.nsolve(eqn, x, 0.1, solver='secant') #http://docs.sympy.org/0.7.0/modules/mpmath/calculus/optimization.html
    print(".",end="")
    return psi*1000 #mV

def delta_psi_monovalent(x, ionic_stren):
    """Poisson Boltzmann Equation, as shown in previous cell but without "biomolecule".
    Return  the surface potential as a function of surface charge 'x'.
    The model is the Poisson-Boltzmann Equation (i.e. the Grahame equation solved for potential) for a monovalent 1:1 electrolyte
    Input: The Ionic Strength (in Molar), 'ionic_stren'
    Input: 'x' list of native surface charges (C/m^2)
    Returns: The change in surface potential (in mV)
    Note that, unlike the Debye-Hückel equation, this equation has not been linearised by assuming an inverse hyperbolic sine (asinh) term can be apprximated to x^2.
    Linearising results in the simpler, linear, Debye-Huckel equation, and the concept of a Debye Length
    """
    sigma_0=x
    n_0=ionic_stren*1000 # mol/m^3
    psi=(2*k*T)/(z*e)*sympy.asinh(((sigma_0)/sympy.sqrt(8*epsilon_medium*k*T*n_0*avo_L)))

    return psi*1000 #mV

a=delta_psi_mpb(0.005, 0.9)
b=delta_psi_monovalent(0.005,0.3)
xspan=np.arange(0.01,100,0.1)
xspan2=np.arange(0.01,100,0.1)

ionic_stren=0.3

######The below commented code can be used to get scale factors to
#compare to scale axis to being dimensionless, like the figures in
#original Kilic et al. Modified Poisson Boltzmann equation paper
#n_0=ionic_stren*1000.0 # mol/m^3
#number_density_ionic_strength=n_0*avo_L
#kappa=sympy.sqrt((2*n_0*avo_L*(e**2))/(epsilon_medium*k*T))
#debyel=1/kappa
#factor=1/(2*elementary_charge*number_density_ionic_strength*debyel)
#a=184*2e-12 #meters, Size of the ion (in meters, Table 1 paper, Na+) 125 is KCl
#nu=2*(a**3)*number_density_ionic_strength # Packing parameter, unitless
#####

pot_mpb=[delta_psi_mpb(chg, ionic_stren) for chg in xspan]
pot_mpb_tmp=[delta_psi_mpb(chg, ionic_stren, SCALE=10) for chg in xspan]
pot_grahame=[delta_psi_monovalent(chg, ionic_stren) for chg in xspan2]

plt.plot(pot_mpb, xspan, '-o', label='Modified Poisson-Boltzmann Equation (Small a)')
plt.plot(pot_mpb_tmp, xspan, '-o', label='Modified Poisson-Boltzmann Equation (Large a)')
plt.plot(pot_grahame, xspan2, '-o', label='Poisson-Boltzmann Equation')
plt.legend()
plt.xlabel("Surface Potential (mV)")
plt.ylabel("Surface Charge (C/m$^{2}$)")
plt.yscale("log")
plt.xlim(0,1000)
```

```python
def debyel(dilution):
    """Calculates Debye Length (in nanometers) for dilutionX PBS. e.g. diluton=0.1 = 0.1X Phosphate-Buffered Saline.  """
    system=[(0.138, 1), (0.138,-1), (0.0027, 1), (0.0027, -1), (0.0019, 1), (0.0019, -1), (0.00405, 1), (0.00405, 1), (0.0081, -2)]

    #Solving Grahame Equation, Equation 1.90 from Physics of Soft Colloidal Matter, {Interaction potentials and colloidal stability}, Peter Lang, Fall term 2007/08, pg 35
    #http://arachnoid.com/IPython/equation_solving.html
    sumed=0
    dilution=0.1
    for conc, charge in system:
        number_dens=conc*dilution*1000.*avo_L
        sumed+=number_dens*(e**2)
    result=sympy.sqrt((epsilon_medium*k*T)/sumed)

    return result*1e9

print(debyel(1))
```

    2.47805360801351


     Histograms of Oriented Gradients (HOG)

Introduction

As we saw with the ORB algorithm, we can use keypoints in images to do keypoint-based matching to detect objects in images. These type of algorithms work great when you want to detect objects that have a lot of consistent internal features that are not affected by the background. For example, these algorithms work well for facial detection because faces have a lot of consistent internal features that don’t get affected by the image background, such as the eyes, nose, and mouth. However, these type of algorithms don’t work so well when attempting to do more general object recognition, say for example, pedestrian detection in images. The reason is that people don’t have consistent internal features, like faces do, because the body shape and style of every person is different (see Fig. 1). This means that every person is going to have a different set of internal features, and so we need something that can more generally describe a person.

Fig. 1. - Pedestrians.

One option is to try to detect pedestrians by their contours instead.  Detecting objects in images by their contours (boundaries) is very challenging because we have to deal with the difficulties brought about by the contrast between the background and the foreground. For example, suppose you wanted to detect a pedestrian in an image that is walking in front of a white building and she is wearing a white coat and black pants (see Fig. 2). We can see in Fig. 2, that since the background of the image is mostly white, the black pants are going to have a very high contrast, but the coat, since it is white as well, is going to have very low contrast. In this case, detecting the edges of pants is going to be easy but detecting the edges of the coat is going to be very difficult. This is where **HOG** comes in. HOG stands for **Histograms of Oriented Gradients** and it was first introduced by Navneet Dalal and Bill Triggs in 2005.

Fig. 2. - High and Low Contrast.

The HOG algorithm works by creating histograms of the distribution of gradient orientations in an image and then normalizing them in a very special way. This special normalization is what makes HOG so effective at detecting the edges of objects even in cases where the contrast is very low. These normalized histograms are put together into a feature vector, known as the HOG descriptor, that can be used to train a machine learning algorithm, such as a Support Vector Machine (SVM), to detect objects in images based on their boundaries (edges). Due to its great success and reliability, HOG has become one of the most widely used algorithms in computer vison for object detection.

In this notebook, you will learn:

* How the HOG algorithm works
* How to use OpenCV to create a HOG descriptor
* How to visualize the HOG descriptor.

# The HOG Algorithm

As its name suggests, the HOG algorithm, is based on creating histograms from the orientation of image gradients. The HOG algorithm is implemented in a series of steps:

1. Given the image of particular object, set a detection window (region of interest) that covers the entire object in the image (see Fig. 3).

2. Calculate the magnitude and direction of the gradient for each individual pixel in the detection window.

3. Divide the detection window into connected *cells* of pixels, with all cells being of the same size (see Fig. 3). The size of the cells is a free parameter and it is usually chosen so as to match the scale of the features that want to be detected. For example, in a 64 x 128 pixel detection window, square cells 6 to 8 pixels wide are suitable for detecting human limbs.

4. Create a Histogram for each cell, by first grouping the gradient directions of all pixels in each cell into a particular number of orientation (angular) bins; and then adding up the gradient magnitudes of the gradients in each angular bin (see Fig. 3). The number of bins in the histogram is a free parameter and it is usually set to 9 angular bins.

5. Group adjacent cells into *blocks* (see Fig. 3). The number of cells in each block is a free parameter and all blocks must be of the same size. The distance between each block (known as the stride) is a free parameter but it is usually set to half the block size, in which case you will get overlapping blocks (*see video below*).  The HOG algorithm has been shown empirically to work better with overlapping blocks.

6. Use the cells contained within each block to normalize the cell histograms in that block (see Fig. 3). If you have overlapping blocks this means that most cells will be normalized with respect to different blocks (*see video below*). Therefore, the same cell may have several different normalizations.

7. Collect all the normalized histograms from all the blocks into a single feature vector called the HOG descriptor.

8. Use the resulting HOG descriptors from many images of the same type of object to train a machine learning algorithm, such as an SVM, to detect those type of objects in images. For example, you could use the HOG descriptors from many images of pedestrians to train an SVM to detect pedestrians in images. The training is done with both positive a negative examples of the object you want detect in the image.

9. Once the SVM has been trained, a sliding window approach is used to try to detect and locate objects in images.  Detecting an object in the image entails finding the part of the image that looks similar to the HOG pattern learned by the SVM.

Fig. 3. - HOG Diagram.

Vid. 1. - HOG Animation.

# Why The HOG Algorithm Works

As we learned above, HOG creates histograms by adding the magnitude of the gradients in particular orientations in localized portions of the image called *cells*.  By doing this we guarantee that stronger gradients will contribute more to the magnitude of their respective angular bin, while the effects of weak and randomly oriented gradients resulting from noise are minimized. In this manner the histograms tell us the dominant gradient orientation of each cell.

### Dealing with contrast

Now, the magnitude of the dominant orientation can vary widely due to variations in local illumination and the contrast between the background and the foreground.

To account for the background-foreground contrast differences, the HOG algorithm tries to detect edges locally.  In order to do this, it defines groups of cells, called **blocks**, and normalizes the histograms using this local group of cells. By normalizing locally, the HOG algorithm can detect the edges in each block very reliably; this is called **block normalization**.

In addition to using block normalization, the HOG algorithm also uses overlapping blocks to increase its performance. By using overlapping blocks, each cell contributes several independent components to the final HOG descriptor, where each component corresponds to a cell being normalized with respect to a different block. This may seem redundant but, it has been shown empirically that by normalizing each cell several times with respect to different local blocks, the performance of the HOG algorithm increases dramatically.

### Loading Images and Importing Resources

The first step in building our HOG descriptor is to load the required packages into Python and to load our image.

We start by using OpenCV to load an image of a triangle tile. Since, the `cv2.imread()` function loads images as BGR we will convert our image to RGB so we can display it with the correct colors. As usual we will convert our BGR image to Gray Scale for analysis.

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

# Set the default figure size
plt.rcParams['figure.figsize'] = [17.0, 7.0]

# Load the image
image = cv2.imread('./images/triangle_tile.jpeg')

# Convert the original image to RGB
original_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# Convert the original image to gray scale
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# Print the shape of the original and gray scale images
print('The original image has shape: ', original_image.shape)
print('The gray scale image has shape: ', gray_image.shape)

# Display the images
plt.subplot(121)
plt.imshow(original_image)
plt.title('Original Image')
plt.subplot(122)
plt.imshow(gray_image, cmap='gray')
plt.title('Gray Scale Image')
plt.show()
```

    The original image has shape:  (250, 250, 3)
    The gray scale image has shape:  (250, 250)

    <matplotlib.figure.Figure at 0x7f479612be10>

# Creating The HOG Descriptor

We will be using OpenCV’s `HOGDescriptor` class to create the HOG descriptor. The parameters of the HOG descriptor are setup using the `HOGDescriptor()` function. The parameters of the `HOGDescriptor()` function and their default values are given below:

`cv2.HOGDescriptor(win_size = (64, 128),
                  block_size = (16, 16),
                  block_stride = (8, 8),
                  cell_size = (8, 8),
                  nbins = 9,
                  win_sigma = DEFAULT_WIN_SIGMA,
                  threshold_L2hys = 0.2,
                  gamma_correction = true,
                  nlevels = DEFAULT_NLEVELS)`

Parameters:

* **win_size** – *Size*
Size of detection window in pixels (*width, height*). Defines the region of interest. Must be an integer multiple of cell size.

* **block_size** – *Size*
Block size in pixels (*width, height*). Defines how many cells are in each block. Must be an integer multiple of cell size and it must be smaller than the detection window. The smaller the block the finer detail you will get.

* **block_stride** – *Size*
Block stride in pixels (*horizontal, vertical*). It must be an integer multiple of cell size. The `block_stride` defines the distance between adjecent blocks, for example, 8 pixels horizontally and 8 pixels vertically. Longer `block_strides` makes the algorithm run faster (because less blocks are evaluated) but the algorithm may not perform as well.

* **cell_size** – *Size*
Cell size in pixels (*width, height*). Determines the size fo your cell. The smaller the cell the finer detail you will get.

* **nbins** – *int*
Number of bins for the histograms. Determines the number of angular bins used to make the histograms. With more bins you capture more gradient directions. HOG uses unsigned gradients, so the angular bins will have values between 0 and 180 degrees.

* **win_sigma** – *double*
Gaussian smoothing window parameter. The performance of the HOG algorithm can be improved by smoothing the pixels near the edges of the blocks by applying a Gaussian spatial window to each pixel before computing the histograms.

* **threshold_L2hys** – *double*
L2-Hys (Lowe-style clipped L2 norm) normalization method shrinkage. The L2-Hys method is used to normalize the blocks and it consists of an L2-norm followed by clipping and a renormalization. The clipping limits the maximum value of the descriptor vector for each block to have the value of the given threshold (0.2 by default). After the clipping the descriptor vector is renormalized as described in *IJCV*, 60(2):91-110, 2004.

* **gamma_correction** – *bool*
Flag to specify whether the gamma correction preprocessing is required or not. Performing gamma correction slightly increases the performance of the HOG algorithm.

* **nlevels** – *int*
Maximum number of detection window increases.

As we can see, the  `cv2.HOGDescriptor()`function supports a wide range of parameters. The first few arguments (`block_size, block_stride, cell_size`, and `nbins`) are probably the ones you are most likely to change. The other parameters can be safely left at their default values and you will get good results.

In the code below, we will use the `cv2.HOGDescriptor()`function to set the cell size, block size, block stride, and the number of bins for the histograms of the HOG descriptor. We will then use `.compute(image)`method to compute the HOG descriptor (feature vector) for the given `image`.

```python
# Specify the parameters for our HOG descriptor

# Cell Size in pixels (width, height). Must be smaller than the size of the detection window
# and must be chosen so that the resulting Block Size is smaller than the detection window.
cell_size = (6, 6)

# Number of cells per block in each direction (x, y). Must be chosen so that the resulting
# Block Size is smaller than the detection window
num_cells_per_block = (2, 2)

# Block Size in pixels (width, height). Must be an integer multiple of Cell Size.
# The Block Size must be smaller than the detection window
block_size = (num_cells_per_block[0] * cell_size[0],
              num_cells_per_block[1] * cell_size[1])

# Calculate the number of cells that fit in our image in the x and y directions
x_cells = gray_image.shape[1] // cell_size[0]
y_cells = gray_image.shape[0] // cell_size[1]

# Horizontal distance between blocks in units of Cell Size. Must be an integer and it must
# be set such that (x_cells - num_cells_per_block[0]) / h_stride = integer.
h_stride = 1

# Vertical distance between blocks in units of Cell Size. Must be an integer and it must
# be set such that (y_cells - num_cells_per_block[1]) / v_stride = integer.
v_stride = 1

# Block Stride in pixels (horizantal, vertical). Must be an integer multiple of Cell Size
block_stride = (cell_size[0] * h_stride, cell_size[1] * v_stride)

# Number of gradient orientation bins
num_bins = 9

# Specify the size of the detection window (Region of Interest) in pixels (width, height).
# It must be an integer multiple of Cell Size and it must cover the entire image. Because
# the detection window must be an integer multiple of cell size, depending on the size of
# your cells, the resulting detection window might be slightly smaller than the image.
# This is perfectly ok.
win_size = (x_cells * cell_size[0] , y_cells * cell_size[1])

# Print the shape of the gray scale image for reference
print('\nThe gray scale image has shape: ', gray_image.shape)
print()

# Print the parameters of our HOG descriptor
print('HOG Descriptor Parameters:\n')
print('Window Size:', win_size)
print('Cell Size:', cell_size)
print('Block Size:', block_size)
print('Block Stride:', block_stride)
print('Number of Bins:', num_bins)
print()

# Set the parameters of the HOG descriptor using the variables defined above
hog = cv2.HOGDescriptor(win_size, block_size, block_stride, cell_size, num_bins)

# Compute the HOG Descriptor for the gray scale image
hog_descriptor = hog.compute(gray_image)
```

    The gray scale image has shape:  (250, 250)

    HOG Descriptor Parameters:

    Window Size: (246, 246)
    Cell Size: (6, 6)
    Block Size: (12, 12)
    Block Stride: (6, 6)
    Number of Bins: 9

# Number of Elements In The HOG Descriptor

The resulting HOG Descriptor (feature vector), contains the normalized histograms from all cells from all blocks in the detection window concatenated in one long vector. Therefore, the size of the HOG feature vector will be given by the total number of blocks in the detection window, multiplied by the number of cells per block, times the number of orientation bins:

\begin{equation}
\mbox{total_elements} = (\mbox{total_number_of_blocks})\mbox{ } \times \mbox{ } (\mbox{number_cells_per_block})\mbox{ } \times \mbox{ } (\mbox{number_of_bins})
\end{equation}

If we don’t have overlapping blocks (*i.e.* the `block_stride`equals the `block_size`), the total number of blocks can be easily calculated by dividing the size of the detection window by the block size. However, in the general case we have to take into account the fact that we have overlapping blocks. To find the total number of blocks in the general case (*i.e.* for any `block_stride` and `block_size`), we can use the formula given below:

\begin{equation}
\mbox{Total}_i = \left( \frac{\mbox{block_size}_i}{\mbox{block_stride}_i} \right)\left( \frac{\mbox{window_size}_i}{\mbox{block_size}_i} \right) - \left [\left( \frac{\mbox{block_size}_i}{\mbox{block_stride}_i} \right) - 1 \right]; \mbox{  for  } i = x,y
\end{equation}

Where Total$_x$, is the total number of blocks along the width of the detection window, and Total$_y$, is the total number of blocks along the height of the detection window. This formula for Total$_x$ and Total$_y$, takes into account the extra blocks that result from overlapping. After calculating Total$_x$ and Total$_y$, we can get the total number of blocks in the detection window by multiplying Total$_x$  $\times$ Total$_y$. The above formula can be simplified considerably because the `block_size`, `block_stride`, and `window_size`are all defined in terms of the `cell_size`. By making all the appropriate substitutions and cancelations the above formula reduces to:

\begin{equation}
\mbox{Total}_i = \left(\frac{\mbox{cells}_i - \mbox{num_cells_per_block}_i}{N_i}\right) + 1\mbox{  }; \mbox{  for  } i = x,y
\end{equation}

Where cells$_x$ is the total number of cells along the width of the detection window, and cells$_y$, is the total number of cells along the height of the detection window. And $N_x$ is the horizontal block stride in units of `cell_size` and $N_y$ is the vertical block stride in units of `cell_size`.

Let's calculate what the number of elements for the HOG feature vector should be and check that it matches the shape of the HOG Descriptor calculated above.

```python
# Calculate the total number of blocks along the width of the detection window
tot_bx = np.uint32(((x_cells - num_cells_per_block[0]) / h_stride) + 1)

# Calculate the total number of blocks along the height of the detection window
tot_by = np.uint32(((y_cells - num_cells_per_block[1]) / v_stride) + 1)

# Calculate the total number of elements in the feature vector
tot_els = (tot_bx) * (tot_by) * num_cells_per_block[0] * num_cells_per_block[1] * num_bins

# Print the total number of elements the HOG feature vector should have
print('\nThe total number of elements in the HOG Feature Vector should be: ',
      tot_bx, 'x',
      tot_by, 'x',
      num_cells_per_block[0], 'x',
      num_cells_per_block[1], 'x',
      num_bins, '=',
      tot_els)

# Print the shape of the HOG Descriptor to see that it matches the above
print('\nThe HOG Descriptor has shape:', hog_descriptor.shape)
print()
```

    The total number of elements in the HOG Feature Vector should be:  40 x 40 x 2 x 2 x 9 = 57600

    The HOG Descriptor has shape: (57600, 1)

# Visualizing The HOG Descriptor

We can visualize the HOG Descriptor by plotting the histogram associated with each cell as a collection of vectors. To do this, we will plot each bin in the histogram as a single vector whose magnitude is given by the height of the bin and its orientation is given by the angular bin that its associated with. Since any given cell might have multiple histograms associated with it, due to the overlapping blocks, we will choose to average all the histograms for each cell to produce a single histogram for each cell.

OpenCV has no easy way to visualize the HOG Descriptor, so we have to do some manipulation first in order to visualize it. We will start by reshaping the HOG Descriptor in order to make our calculations easier.  We will then compute the average histogram of each cell and finally we will convert the histogram bins into vectors. Once we have the vectors, we plot the corresponding vectors for each cell in an image.

The code below produces an interactive plot so that you can interact with the figure. The figure contains:
* the grayscale image,
* the HOG Descriptor (feature vector),
* a zoomed-in portion of the HOG Descriptor, and
* the histogram of the selected cell.

**You can click anywhere on the gray scale image or the HOG Descriptor image to select a particular cell**. Once you click on either image a *magenta* rectangle will appear showing the cell you selected. The Zoom Window will show you a zoomed in version of the HOG descriptor around the selected cell; and the histogram plot will show you the corresponding histogram for the selected cell. The interactive window also has buttons at the bottom that allow for other functionality, such as panning, and giving you the option to save the figure if desired. The home button returns the figure to its default value.

**NOTE**: If you are running this notebook in the Udacity workspace, there is around a 2 second lag in the interactive plot. This means that if you click in the image to zoom in, it will take about 2 seconds for the plot to refresh.

```python
%matplotlib notebook

import copy
import matplotlib.patches as patches

# Set the default figure size
plt.rcParams['figure.figsize'] = [9.8, 9]

# Reshape the feature vector to [blocks_y, blocks_x, num_cells_per_block_x, num_cells_per_block_y, num_bins].
# The blocks_x and blocks_y will be transposed so that the first index (blocks_y) referes to the row number
# and the second index to the column number. This will be useful later when we plot the feature vector, so
# that the feature vector indexing matches the image indexing.
hog_descriptor_reshaped = hog_descriptor.reshape(tot_bx,
                                                 tot_by,
                                                 num_cells_per_block[0],
                                                 num_cells_per_block[1],
                                                 num_bins).transpose((1, 0, 2, 3, 4))

# Print the shape of the feature vector for reference
print('The feature vector has shape:', hog_descriptor.shape)

# Print the reshaped feature vector
print('The reshaped feature vector has shape:', hog_descriptor_reshaped.shape)

# Create an array that will hold the average gradients for each cell
ave_grad = np.zeros((y_cells, x_cells, num_bins))

# Print the shape of the ave_grad array for reference
print('The average gradient array has shape: ', ave_grad.shape)

# Create an array that will count the number of histograms per cell
hist_counter = np.zeros((y_cells, x_cells, 1))

# Add up all the histograms for each cell and count the number of histograms per cell
for i in range (num_cells_per_block[0]):
    for j in range(num_cells_per_block[1]):
        ave_grad[i:tot_by + i,
                 j:tot_bx + j] += hog_descriptor_reshaped[:, :, i, j, :]

        hist_counter[i:tot_by + i,
                     j:tot_bx + j] += 1

# Calculate the average gradient for each cell
ave_grad /= hist_counter

# Calculate the total number of vectors we have in all the cells.
len_vecs = ave_grad.shape[0] * ave_grad.shape[1] * ave_grad.shape[2]

# Create an array that has num_bins equally spaced between 0 and 180 degress in radians.
deg = np.linspace(0, np.pi, num_bins, endpoint = False)

# Each cell will have a histogram with num_bins. For each cell, plot each bin as a vector (with its magnitude
# equal to the height of the bin in the histogram, and its angle corresponding to the bin in the histogram).
# To do this, create rank 1 arrays that will hold the (x,y)-coordinate of all the vectors in all the cells in the
# image. Also, create the rank 1 arrays that will hold all the (U,V)-components of all the vectors in all the
# cells in the image. Create the arrays that will hold all the vector positons and components.
U = np.zeros((len_vecs))
V = np.zeros((len_vecs))
X = np.zeros((len_vecs))
Y = np.zeros((len_vecs))

# Set the counter to zero
counter = 0

# Use the cosine and sine functions to calculate the vector components (U,V) from their maginitudes. Remember the
# cosine and sine functions take angles in radians. Calculate the vector positions and magnitudes from the
# average gradient array
for i in range(ave_grad.shape[0]):
    for j in range(ave_grad.shape[1]):
        for k in range(ave_grad.shape[2]):
            U[counter] = ave_grad[i,j,k] * np.cos(deg[k])
            V[counter] = ave_grad[i,j,k] * np.sin(deg[k])

            X[counter] = (cell_size[0] / 2) + (cell_size[0] * i)
            Y[counter] = (cell_size[1] / 2) + (cell_size[1] * j)

            counter = counter + 1

# Create the bins in degress to plot our histogram.
angle_axis = np.linspace(0, 180, num_bins, endpoint = False)
angle_axis += ((angle_axis[1] - angle_axis[0]) / 2)

# Create a figure with 4 subplots arranged in 2 x 2
fig, ((a,b),(c,d)) = plt.subplots(2,2)

# Set the title of each subplot
a.set(title = 'Gray Scale Image\n(Click to Zoom)')
b.set(title = 'HOG Descriptor\n(Click to Zoom)')
c.set(title = 'Zoom Window', xlim = (0, 18), ylim = (0, 18), autoscale_on = False)
d.set(title = 'Histogram of Gradients')

# Plot the gray scale image
a.imshow(gray_image, cmap = 'gray')
a.set_aspect(aspect = 1)

# Plot the feature vector (HOG Descriptor)
b.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 5)
b.invert_yaxis()
b.set_aspect(aspect = 1)
b.set_facecolor('black')

# Define function for interactive zoom
def onpress(event):

    #Unless the left mouse button is pressed do nothing
    if event.button != 1:
        return

    # Only accept clicks for subplots a and b
    if event.inaxes in [a, b]:

        # Get mouse click coordinates
        x, y = event.xdata, event.ydata

        # Select the cell closest to the mouse click coordinates
        cell_num_x = np.uint32(x / cell_size[0])
        cell_num_y = np.uint32(y / cell_size[1])

        # Set the edge coordinates of the rectangle patch
        edgex = x - (x % cell_size[0])
        edgey = y - (y % cell_size[1])

        # Create a rectangle patch that matches the the cell selected above
        rect = patches.Rectangle((edgex, edgey),
                                  cell_size[0], cell_size[1],
                                  linewidth = 1,
                                  edgecolor = 'magenta',
                                  facecolor='none')

        # A single patch can only be used in a single plot. Create copies
        # of the patch to use in the other subplots
        rect2 = copy.copy(rect)
        rect3 = copy.copy(rect)

        # Update all subplots
        a.clear()
        a.set(title = 'Gray Scale Image\n(Click to Zoom)')
        a.imshow(gray_image, cmap = 'gray')
        a.set_aspect(aspect = 1)
        a.add_patch(rect)

        b.clear()
        b.set(title = 'HOG Descriptor\n(Click to Zoom)')
        b.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 5)
        b.invert_yaxis()
        b.set_aspect(aspect = 1)
        b.set_facecolor('black')
        b.add_patch(rect2)

        c.clear()
        c.set(title = 'Zoom Window')
        c.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 1)
        c.set_xlim(edgex - cell_size[0], edgex + (2 * cell_size[0]))
        c.set_ylim(edgey - cell_size[1], edgey + (2 * cell_size[1]))
        c.invert_yaxis()
        c.set_aspect(aspect = 1)
        c.set_facecolor('black')
        c.add_patch(rect3)

        d.clear()
        d.set(title = 'Histogram of Gradients')
        d.grid()
        d.set_xlim(0, 180)
        d.set_xticks(angle_axis)
        d.set_xlabel('Angle')
        d.bar(angle_axis,
              ave_grad[cell_num_y, cell_num_x, :],
              180 // num_bins,
              align = 'center',
              alpha = 0.5,
              linewidth = 1.2,
              edgecolor = 'k')

        fig.canvas.draw()

# Create a connection between the figure and the mouse click
fig.canvas.mpl_connect('button_press_event', onpress)
plt.show()
```

    The feature vector has shape: (57600, 1)
    The reshaped feature vector has shape: (40, 40, 2, 2, 9)
    The average gradient array has shape:  (41, 41, 9)

    <IPython.core.display.Javascript object>

# Understanding The Histograms

Let's take a look at a couple of snapshots of the above figure to see if the histograms for the selected cell make sense. Let's start looking at a cell that is inside a triangle and not near an edge:

Fig. 4. - Histograms Inside a Triangle.

In this case, since the triangle is nearly all of the same color there shouldn't be any dominant gradient in the selected cell. As we can clearly see in the Zoom Window and the histogram, this is indeed the case. We have many gradients but none of them clearly dominates over the other.

Now let’s take a look at a cell that is near a horizontal edge:

Fig. 5. - Histograms Near a Horizontal Edge.

Remember that edges are areas of an image where the intensity changes abruptly. In these cases, we will have a high intensity gradient in some particular direction. This is exactly what we see in the corresponding histogram and Zoom Window for the selected cell.  In the Zoom Window, we can see that the dominant gradient is pointing up, almost at 90 degrees, since that’s the direction in which there is a sharp change in intensity. Therefore, we should expect to see the 90-degree bin in the histogram to dominate strongly over the others. This is in fact what we see.

Now let’s take a look at a cell that is near a vertical edge:

Fig. 6. - Histograms Near a Vertical Edge.

In this case we expect the dominant gradient in the cell to be horizontal, close to 180 degrees, since that’s the direction in which there is a sharp change in intensity. Therefore, we should expect to see the 170-degree bin in the histogram to dominate strongly over the others. This is what we see in the histogram but we also see that there is another dominant gradient in the cell, namely the one in the 10-degree bin. The reason for this, is because the HOG algorithm is using unsigned gradients, which means 0 degrees and 180 degrees are considered the same. Therefore, when the histograms are being created, angles between 160 and 180 degrees, contribute proportionally to both the 10-degree bin and the 170-degree bin. This results in there being two dominant gradients in the cell near the vertical edge instead of just one.

To conclude let’s take a look at a cell that is near a diagonal edge.

Fig. 7. - Histograms Near a Diagonal Edge.

To understand what we are seeing, let’s first remember that gradients have an *x*-component, and a *y*-component, just like vectors. Therefore, the resulting orientation of a gradient is going to be given by the vector sum of its components. For this reason, on vertical edges the gradients are horizontal, because they only have an x-component, as we saw in Figure 4. While on horizontal edges the gradients are vertical, because they only have a y-component, as we saw in Figure 3. Consequently, on diagonal edges, the gradients are also going to be diagonal because both the *x* and *y* components are non-zero.  Since the diagonal edges in the image are close to 45 degrees, we should expect to see a dominant gradient orientation in the 50-degree bin. This is in fact what we see in the histogram but, just like in Figure 4., we see there are two dominant gradients instead of just one. The reason for this is that when the histograms are being created, angles that are near the boundaries of bins, contribute proportionally to the adjacent bins.  For example, a gradient with an angle of 40 degrees, is right in the middle of the 30-degree and 50-degree bin. Therefore, the magnitude of the gradient is split evenly into the 30-degree and 50-degree bin. This results in there being two dominant gradients in the cell near the diagonal edge instead of just one.

Now that you know how HOG is implemented, in the workspace you will find a notebook named *Examples*. In there, you will be able set your own paramters for the HOG descriptor for various images. Have fun!

```python

```