# Variational Principle  using Symbolic Mathematics in Python

## 1. Introduction

The variational principle tells us that we can use a trial wavefunction to solve the Schrodinger equation using the following theorem:

$${{\int {{\Psi ^*}\hat H{\rm{ }}\Psi } d\tau } \over {\int {{\Psi ^*}\Psi } d\tau }} \ge {E_0}$$

We will use Sympy to solve the particle in a box problem by guessing a trial wavefunction using variational principle

```python
import sympy as sym
```

This exercise is a bit more self-guided than the other notebooks we have done. One of the most useful things you can do is **open last week's notebook to remember the commands in sympy**. Also, remember that google is your friend:

1. [Sympy tutorial](https://docs.sympy.org/latest/tutorial/index.html)
2. [Stack Overflow](https://stackoverflow.com/search?q=sympy+)
3. [Stack Exchange](https://stackexchange.com/)

## 2. Particle in a box

The wave function that we pick for a particle in a box needs to have the following properties

1. single valued
1. normalizable
1. function and its first derivative are continuous
1. boundary condition that the wave function goes to zero at the ends of the box

Particle in a box: a is a classical particle, red is real part, blue is imaginary part.

This particle only expericnes kinetic energy between the box, so the Hamiltonian for this system is

$$\hat H = {{ - {\hbar ^2}} \over {2m}}{{{d^2}} \over {d{x^2}}} + \left\{ {\matrix{{V(x) = 0} & {0 < x < a}  \cr {V(x) = \infty } & {x < 0\text{ }{\rm{ or}}\;x > a}  \cr } } \right.$$

For our purposes, that means we can consider the Hamiltonian to be

$$\hat H = {{ - {\hbar ^2}} \over {2m}}{{{d^2}} \over {d{x^2}}}$$

as long as we keep the limits of integration to be $(0,a)$

### 2.1 Trial Wave function

Although the particle in box has a well known solution

[https://en.wikipedia.org/wiki/Particle_in_a_box](https://en.wikipedia.org/wiki/Particle_in_a_box)

(or check your favorite pchem book)

We are going to guess a trial wave function:

$$\Phi (x) = \left( {{x \over a} - {{{x^3}} \over a}} \right) + \alpha \left( {{{{x^5}} \over {{a^5}}} - {1 \over 2}\left( {{{{x^7}} \over {{a^7}}} + {{{x^7}} \over {{a^7}}}} \right)} \right)$$

### 2.2 Exercise: Variational Theorem

We are going to follow the following plan:

1. Solve for the energy of the trial wave function above

$${E_{trial}} = {{\int\limits_0^a {\Phi (x){{ - {\hbar ^2}} \over {2m}}{{{d^2}} \over {d{x^2}}}\Phi (x)dx} } \over {\int\limits_0^a {\Phi {{(x)}^2}dx} }}$$

Your answer will be a function of $ m,a,\text{and } \alpha$ We will use $\alpha$ as the parameter we vary to minimize the energy and make a new trial wave function.

2. Minimize the trial energy
We will use a first derivative of the trial energy $${d \over {d\alpha }}{E_{trial}}(\alpha )$$ to find the value of $\alpha$ that gives you the lowest energy

3. Plot your new wavefunction compared to the ground state particle in a box: $${\psi _{true}}(x) = {\left( {{2 \over a}} \right)^{1/2}}\sin {{n\pi x} \over a}$$ Plot as a function of $x/a$ from $0$ to $1$. Assuming this has $m=m_e$, and $a=a_0$ use atomic (theorist) units to plot the function.

4. Compare your trial energy to the actual energy (using atomic units)
$${E_{true}}(n = 1) = {{{\hbar ^2}{\pi ^2}} \over {2m{a^2}}}$$

```python
# #1 Solve for the energy of the trial wave function

import sympy as sym
x, m, a, al, phi, n= sym.symbols('x, m, a, al, phi, n')
phi= (x/a - ((x**3)/(a**3))) + al*(-0.5*(x**5/a**5)-(x**7/a**7))
hbar= -0.5
function1= phi*hbar
function2= sym.diff(phi,x,2)

eq1= sym.integrate(function1*function2, (x, 0,a))
eq2= sym.integrate(phi**2, (x,0,a))

etrial= eq1/eq2
print(etrial)
```

    (-3.02447552447552*al**2/a + 2.07142857142857*al/a + 0.2*(5.0*al - 3.0)/a + 0.111111111111111*(-2.5*al**2 - 24.0*al)/a + 1.0/a)/(0.143589743589744*a*al**2 - 0.111111111111111*a*al + 0.142857142857143*a*(1.0 - 1.0*al) + 0.0909090909090909*a*(0.25*al**2 + 2.0*al) - 0.0666666666666667*a)

```python

```

```python

```

```python
#2 Minimize trial energy
etrial2= etrial.subs(a, 1)
sym.diff(etrial2,al)
solutions=sym.solveset(sym.diff(etrial2,al))
print(solutions)

#Plug back into etrial
#3.62579006486358 gives us the lowest energy, so we will proceed with that to plot new wave function into phi
```

    {0.0963215392581175, 3.62579006486358}

```python
#Plug back into etrial
etrial2.subs(al,0.0963215392581175)
```

$\displaystyle 5.76895772898132$

```python
#Plug back into etrial
etrial2.subs(al, 3.62579006486358)
```

$\displaystyle -20.7615857663308$

```python
etrial2
```

$\displaystyle \frac{- 3.3022533022533 al^{2} + 0.404761904761905 al + 0.4}{0.166317016317016 al^{2} - 0.0721500721500721 al + 0.0761904761904761}$

```python
#3 plot new wavefunction

#sym.plot(etrial2, (al, 0,1))
#lowest particle
newphi= phi.subs(al,3.62579006486358)
newphi.subs(a,1)
groundparticle= (2**(0.5))* sym.sin((sym.pi*x))

sym.plot(newphi.subs(a,1), groundparticle, (x,0,1))

```

```python

```

```python
#4. Compare trial energy to actual energy
x, m, a, al= sym.symbols('x, m, a, al')
etrue= (sym.pi**2)/2
sym.plot(etrue, newphi.subs(a,1), (x,0,1))

#Trial energy is lower than actual energy
```

Your descriptions/explanations here

### 2.3 Exercise: New trial wavefunction

Determine the minimum energy of the particle in a box using a new trial wavefunction $$x^\alpha(x-a)^\alpha$$

1. Find the minimum energy, $E_{trial}$
2. Plot the new trial wavefunction and compare it to the true solution and the wavefunction you found above
3. Compare you new energy to the trial energy you found above
4. Which wavefunction is better? How do you know?

```python
# Find the minimum energy -- vary alpha to find minimum energy, take first derivative to get the lowest value
import sympy as sym
x, a, p, al= sym.symbols('x, a, p, al')
newtrial= (x**al)*(x-1)**al

newtrialdiff= sym.diff(newtrial,al)
solutions=sym.solveset(newtrial)
print(solutions)

#etrial2= etrial.subs(a, 1)
#sym.diff(etrial2,al)
#solutions=sym.solveset(sym.diff(etrial2,al))
#print(solutions)

```

```python
#2. Plot new trial function
import sympy as sym
x, a, p, al= sym.symbols('x, a, p, al')
newtrial= (x**al)*(x-1)**al

solutions= sym.solveset(newtrial)
print(solutions)

#etrial2= etrial.subs(a, 1)
#sym.diff(etrial2,al)
#solutions=sym.solveset(sym.diff(etrial2,al))
#print(solutions)

```

```python
#3. Compare new energy to trial energy above.

x, m, a, al= sym.symbols('x, m, a, al')
etrue= (sym.pi**2)/2
sym.plot(newphi.subs(a,1), (x,0,1))

```

```python
#4. Which wave function is better?
#Trial energy is better since lower in energy

```

Your descriptions/explanations here

### 2.4 Exercise: Design your own wavefunction!

**Now you get to make your own wavefunction!**

The only guidance I would give you is that it make sense mathematically and that it include $\alpha$ so that you can minimize the energy.

Remember that $a$ and $x$ are both length units, and that trigonometric, logarithmic, and exponential functions are all unitless

Using your new wavefunction:

1. Find the minimum energy, $E_{trial}$
2. Plot the new trial wavefunction and compare it to the true solution and the wavefunction you found above
3. Compare you new energy to the trial energy you found above
4. Which wavefunction is better? How do you know?

```python
# 1. Find minimum energy, Etrial
ownwave= -al*(x-3)**2

sym.diff(ownwave,al)
solutions= sym.solveset(sym.diff(ownwave,al))
print(solutions)

#alpha= 3 at lowest. Plug back into phi
etrial2.subs(al,3)

```

    {3}

$\displaystyle -20.7180676755403$

```python
# 2. New Trial function

ownwavephi1= phi.subs(al, 3)
ownwavephi2= ownwavephi1.subs(a,1)
print(ownwavephi)
sym.plot(ownwavephi2, (x,0,1))

```

```python
#3. Compare new energy to trial energy found above

newphi.subs(a,1)

sym.plot(ownwavephi2, groundparticle, (x,0,1))
```

```python
#4. Which wave function is better?
#Trial energy is better since lower in energy more reminescent of actual
```

Your descriptions/explanations here

# Reading Homework

Read the following sections in Kramer

- 4.2.3 Born-Oppenheimer approximation
- 4.3.2 Secular equation
- All of 4.5

For each subsection
- write down the subchapter name
- what was the most important idea
- draw an idea digram of the main idea

**Make sure to upload this to the assignment repository**

Example idea diagram:

```python

```


# Example 9.5: Modified Brayton Cycle (Cold-Air-Standard)

*John F. Maddox, Ph.D., P.E.

University of Kentucky - Paducah Campus

ME 321: Engineering Thermodynamics II
*

## Problem Statement

A Brayton cycle with intercooling, reheat, and regeneration has the following properties:
\begin{align}
T_1&=300\ \mathrm{K} & T_3&=350\ \mathrm{K} & T_6&=T_8=1400\ \mathrm{K} \\
p_1 &= 1\ \mathrm{bar}  & \frac{p_2}{p_1}&=\frac{p_4}{p_3}=\sqrt{10} & p_7&=\sqrt{10}\ \mathrm{bar}\\
\eta_{c,1} &= \eta_{c,2} = 0.8 & \eta_{t,1}&=\eta_{t,2}=0.8 & \eta_{reg}&= 0.85
\end{align}
Determine:
* (a) $w_{net}$
* (b) $q_{in}$
* (c) $\eta_{th}$
* (d) $BWR$

## Solution

__[Video Explanation](https://uky.yuja.com/V/Video?v=3074254&node=10465182&a=1163985616&autoplay=1)__

### Python Initialization
We'll start by importing the libraries we will use for our analysis and initializing dictionaries to hold the properties we will be usings.

```
from math import log, sqrt
from kilojoule.templates.kSI import *

air = idealgas.Properties('Air')
```

### Given Parameters
We now define variables to hold our known values.

```
T[1] = Quantity(300,'K')      # Compressor Inlet Temperature
T[3] = Quantity(350,'K')
T[6] = T[8] = Quantity(1400,'K')
p[1] = Quantity(1,'bar')      # Compressor Inlet pressure
r_p = Quantity(sqrt(10),' ')   # Pressure ratio
p[7] = Quantity(sqrt(10),'bar')
eta_c = Quantity(0.8,'')      # Compressor Isentropioc Efficiency
eta_t = Quantity(0.8,'')      # Turbine Isentropic Efficiency
eta_reg = Quantity(0.85,'')   # Regenerator effectiveness

Summary();
```

\begin{aligned}{ \eta_{c} }&={ 0.8\ }&{ \eta_{reg} }&={ 0.85\ }&{ \eta_{t} }&={ 0.8\ }\\{ r_{p} }&={ 3.1623\ }&{  }\end{aligned}

      T
      p

      unit
      K
      kPa

      1
      300.0
      100.0

      3
      350.0
      -

      6
      1400.0
      -

      7
      -
      316.227766

      8
      1400.0
      -

### Assumptions
- Cold-air-standard Analysis
  - Ideal gas
  - Constant specific heat (evaluated at $25^\circ\text{C}$)
- Isobaric heat exchagners: Intercooler, Combustors, Regeneator
- Negligible changes in kinetic energy
- Negligible changes in potential energy

```
# Ideal Gas
R = air.R

# Constant thermal properties evaluated at room temperature
T_room = Quantity(25,'degC')
c_v = air.Cv(T=T_room)
c_p = air.Cp(T=T_room)
k = air.k(T=T_room)

# Isobaric heat exchangers
p[2] = r_p*p[1]
p[3] = p[2]
p[4] = r_p*p[3]
p[5] = p[4]
p[6] = p[5]
p[8] = p[7]
p[9] = p[1]
p[10] = p[9]

Summary();
```

\begin{aligned}{ R }&={ 0.28705\ \frac{\mathrm{kJ}}{\left(\mathrm{K} \cdot \mathrm{kg}\right)} }&{ T_{room} }&={ 25\ \mathrm{°C} }&{ c_{p} }&={ 1.0047\ \frac{\mathrm{kJ}}{\left(\mathrm{K} \cdot \mathrm{kg}\right)} }\\{ c_{v} }&={ 0.71768\ \frac{\mathrm{kJ}}{\left(\mathrm{K} \cdot \mathrm{kg}\right)} }&{ \eta_{c} }&={ 0.8\ }&{ \eta_{reg} }&={ 0.85\ }\\{ \eta_{t} }&={ 0.8\ }&{ k }&={ 1.4\ }&{ r_{p} }&={ 3.1623\ }\\{  }\end{aligned}

      T
      p

      unit
      K
      kPa

      1
      300.0
      100.000000

      2
      -
      316.227766

      3
      350.0
      316.227766

      4
      -
      1000.000000

      5
      -
      1000.000000

      6
      1400.0
      1000.000000

      7
      -
      316.227766

      8
      1400.0
      316.227766

      9
      -
      100.000000

      10
      -
      100.000000

#### (a) $w_{net}$

```
%%showcalc
##### 1$\to$2) Non-ideal compression
# Isentropic exit state: Isentropic Ideal Gas Relations
p['2s'] = p[2]
T['2s'] = T[1]*r_p**((k-1)/k)

# Isentropic efficiency
T[2] = T[1] + (T['2s']-T[1])/eta_c

# First Law for compressor
w_1_to_2 = c_p*(T[1]-T[2])

##### 2$\to$3) Intercooling
q_2_to_3 = c_p*(T[3]-T[2])

##### 3$\to$4) Non-ideal compression
# Isentropic exit state: Isentropic Ideal Gas Relations
p['4s'] = p[4]
T['4s'] = T[3]*r_p**((k-1)/k)

# Isentropic efficiency
T[4] = T[3] + (T['4s']-T[3])/eta_c

# First Law for compressor
w_3_to_4 = c_p*(T[3]-T[4])

##### 4$\to$5) Regenerator
# Skip for now.  Need $T_9$ to analyze

##### 5$\to$6) Combustor 1
# Skip for now.  Need $T_5$ from regenerator analysis

##### 6$\to$7) Non-ideal expansion
# Isentropic exit state: Isentropic Ideal Gas Relations
p['7s'] = p[7]
T['7s'] = T[6]*(1/r_p)**((k-1)/k)

# Isentropic efficiency
T[7] = T[6] - eta_t*(T[6]-T['7s'])

# First law for turbine
w_6_to_7 = c_p*(T[6]-T[7])

##### 7$\to$8) Combustor 2
q_7_to_8 = c_p*(T[8]-T[7])

##### 8$\to$9) Non-ideal expansion
# Isentropic exit state: Isentropic Ideal Gas Relations
p['9s'] = p[9]
T['9s'] = T[8]*(1/r_p)**((k-1)/k)

# Isentropic efficiency
T[9] = T[8] - eta_t*(T[8]-T['9s'])

# First law for turbine
w_8_to_9 = c_p*(T[8]-T[9])

###### $w_{net}$
w_net = w_1_to_2 + w_3_to_4 + w_6_to_7 + w_8_to_9
```

#### 1$\to$2) Non-ideal compression

 Isentropic exit state: Isentropic Ideal Gas Relations

\begin{aligned}
  {p}_{ 2s } &=  {p}_{ 2 }  = 316.23\ \mathrm{kPa}
\end{aligned}

\begin{aligned}
  {T}_{ 2s } &=   {T}_{ 1 }  \cdot  r_{p}^\frac{  k - 1  }{ k }  \\
    &=  300\ \mathrm{K}  \cdot  3.1623\^\frac{  1.4\ - 1  }{ 1.4\ } \\
    &= 416.84\ \mathrm{K}
\end{aligned}

 Isentropic efficiency

\begin{aligned}
  {T}_{ 2 } &=  {T}_{ 1 } + \frac{  {T}_{ 2s } - {T}_{ 1 }  }{ \eta_{c} } \\
    &= 300\ \mathrm{K} + \frac{  416.84\ \mathrm{K} - 300\ \mathrm{K}  }{ 0.8\ }\\
    &= 446.05\ \mathrm{K}
\end{aligned}

 First Law for compressor

\begin{aligned}
  w_{1\to{}2} &=   c_{p}  \cdot  \left(  {T}_{ 1 } - {T}_{ 2 }  \right)  \\
    &=  1.0047\ \frac{\mathrm{kJ}}{\left(\mathrm{K} \cdot \mathrm{kg}\right)}  \cdot  \left(  300\ \mathrm{K} - 446.05\ \mathrm{K}  \right) \\
    &= -146.74\ \frac{\mathrm{kJ}}{\mathrm{kg}}
\end{aligned}

#### 2$\to$3) Intercooling

\begin{aligned}
  q_{2\to{}3} &=   c_{p}  \cdot  \left(  {T}_{ 3 } - {T}_{ 2 }  \right)  \\
    &=  1.0047\ \frac{\mathrm{kJ}}{\left(\mathrm{K} \cdot \mathrm{kg}\right)}  \cdot  \left(  350\ \mathrm{K} - 446.05\ \mathrm{K}  \right) \\
    &= -96.507\ \frac{\mathrm{kJ}}{\mathrm{kg}}
\end{aligned}

#### 3$\to$4) Non-ideal compression

 Isentropic exit state: Isentropic Ideal Gas Relations

\begin{aligned}
  {p}_{ 4s } &=  {p}_{ 4 }  = 1000.0\ \mathrm{kPa}
\end{aligned}

\begin{aligned}
  {T}_{ 4s } &=   {T}_{ 3 }  \cdot  r_{p}^\frac{  k - 1  }{ k }  \\
    &=  350\ \mathrm{K}  \cdot  3.1623\^\frac{  1.4\ - 1  }{ 1.4\ } \\
    &= 486.32\ \mathrm{K}
\end{aligned}

 Isentropic efficiency

\begin{aligned}
  {T}_{ 4 } &=  {T}_{ 3 } + \frac{  {T}_{ 4s } - {T}_{ 3 }  }{ \eta_{c} } \\
    &= 350\ \mathrm{K} + \frac{  486.32\ \mathrm{K} - 350\ \mathrm{K}  }{ 0.8\ }\\
    &= 520.4\ \mathrm{K}
\end{aligned}

 First Law for compressor

\begin{aligned}
  w_{3\to{}4} &=   c_{p}  \cdot  \left(  {T}_{ 3 } - {T}_{ 4 }  \right)  \\
    &=  1.0047\ \frac{\mathrm{kJ}}{\left(\mathrm{K} \cdot \mathrm{kg}\right)}  \cdot  \left(  350\ \mathrm{K} - 520.4\ \mathrm{K}  \right) \\
    &= -171.2\ \frac{\mathrm{kJ}}{\mathrm{kg}}
\end{aligned}

#### 4$\to$5) Regenerator

 Skip for now.  Need $T_9$ to analyze

#### 5$\to$6) Combustor 1

 Skip for now.  Need $T_5$ from regenerator analysis

#### 6$\to$7) Non-ideal expansion

 Isentropic exit state: Isentropic Ideal Gas Relations

\begin{aligned}
  {p}_{ 7s } &=  {p}_{ 7 }  = 316.23\ \mathrm{kPa}
\end{aligned}

\begin{aligned}
  {T}_{ 7s } &=   {T}_{ 6 }  \cdot  \left(\frac{ 1 }{ r_{p} }\right)^\frac{  k - 1  }{ k }  \\
    &=  1400\ \mathrm{K}  \cdot  \left(\frac{ 1 }{ 3.1623\ }\right)^\frac{  1.4\ - 1  }{ 1.4\ } \\
    &= 1007.6\ \mathrm{K}
\end{aligned}

 Isentropic efficiency

\begin{aligned}
  {T}_{ 7 } &=   {T}_{ 6 } -  \eta_{t}  \cdot  \left(  {T}_{ 6 } - {T}_{ 7s }  \right)   \\
    &=  1400\ \mathrm{K} -  0.8\  \cdot  \left(  1400\ \mathrm{K} - 1007.6\ \mathrm{K}  \right)  \\
    &= 1086.1\ \mathrm{K}
\end{aligned}

 First law for turbine

\begin{aligned}
  w_{6\to{}7} &=   c_{p}  \cdot  \left(  {T}_{ 6 } - {T}_{ 7 }  \right)  \\
    &=  1.0047\ \frac{\mathrm{kJ}}{\left(\mathrm{K} \cdot \mathrm{kg}\right)}  \cdot  \left(  1400\ \mathrm{K} - 1086.1\ \mathrm{K}  \right) \\
    &= 315.42\ \frac{\mathrm{kJ}}{\mathrm{kg}}
\end{aligned}

#### 7$\to$8) Combustor 2

\begin{aligned}
  q_{7\to{}8} &=   c_{p}  \cdot  \left(  {T}_{ 8 } - {T}_{ 7 }  \right)  \\
    &=  1.0047\ \frac{\mathrm{kJ}}{\left(\mathrm{K} \cdot \mathrm{kg}\right)}  \cdot  \left(  1400\ \mathrm{K} - 1086.1\ \mathrm{K}  \right) \\
    &= 315.42\ \frac{\mathrm{kJ}}{\mathrm{kg}}
\end{aligned}

#### 8$\to$9) Non-ideal expansion

 Isentropic exit state: Isentropic Ideal Gas Relations

\begin{aligned}
  {p}_{ 9s } &=  {p}_{ 9 }  = 100.0\ \mathrm{kPa}
\end{aligned}

\begin{aligned}
  {T}_{ 9s } &=   {T}_{ 8 }  \cdot  \left(\frac{ 1 }{ r_{p} }\right)^\frac{  k - 1  }{ k }  \\
    &=  1400\ \mathrm{K}  \cdot  \left(\frac{ 1 }{ 3.1623\ }\right)^\frac{  1.4\ - 1  }{ 1.4\ } \\
    &= 1007.6\ \mathrm{K}
\end{aligned}

 Isentropic efficiency

\begin{aligned}
  {T}_{ 9 } &=   {T}_{ 8 } -  \eta_{t}  \cdot  \left(  {T}_{ 8 } - {T}_{ 9s }  \right)   \\
    &=  1400\ \mathrm{K} -  0.8\  \cdot  \left(  1400\ \mathrm{K} - 1007.6\ \mathrm{K}  \right)  \\
    &= 1086.1\ \mathrm{K}
\end{aligned}

 First law for turbine

\begin{aligned}
  w_{8\to{}9} &=   c_{p}  \cdot  \left(  {T}_{ 8 } - {T}_{ 9 }  \right)  \\
    &=  1.0047\ \frac{\mathrm{kJ}}{\left(\mathrm{K} \cdot \mathrm{kg}\right)}  \cdot  \left(  1400\ \mathrm{K} - 1086.1\ \mathrm{K}  \right) \\
    &= 315.42\ \frac{\mathrm{kJ}}{\mathrm{kg}}
\end{aligned}

##### $w_{net}$

\begin{aligned}
  w_{net} &=  w_{1\to{}2} + w_{3\to{}4} + w_{6\to{}7} + w_{8\to{}9} \\
    &= -146.74\ \frac{\mathrm{kJ}}{\mathrm{kg}} + -171.2\ \frac{\mathrm{kJ}}{\mathrm{kg}} + 315.42\ \frac{\mathrm{kJ}}{\mathrm{kg}} + 315.42\ \frac{\mathrm{kJ}}{\mathrm{kg}}\\
    &= 312.9\ \frac{\mathrm{kJ}}{\mathrm{kg}}
\end{aligned}

```
states.display();
```

      T
      p

      unit
      K
      kPa

      1
      300.0
      100.000000

      2
      446.052996
      316.227766

      2s
      416.842397
      316.227766

      3
      350.0
      316.227766

      4
      520.395162
      1000.000000

      4s
      486.316129
      1000.000000

      5
      -
      1000.000000

      6
      1400.0
      1000.000000

      7
      1086.060042
      316.227766

      7s
      1007.575053
      316.227766

      8
      1400.0
      316.227766

      9
      1086.060042
      100.000000

      9s
      1007.575053
      100.000000

      10
      -
      100.000000

#### (b) Heat input

```
%%showcalc
##### 4$\to$5 and 9$\to$10) Regenerator
T[5] = T[4] + eta_reg*(T[9]-T[4])
T[10] = T[9] - (T[5]-T[4])

##### 5$\to$6) Combustor 1
q_5_to_6 = c_p*(T[6]-T[5])

##### 7$\to$8) Combustor 2
q_7_to_8 = c_p*(T[8]-T[7])

##### Heat input
q_in = q_5_to_6 + q_7_to_8
```

#### 4$\to$5 and 9$\to$10) Regenerator

\begin{aligned}
  {T}_{ 5 } &=  {T}_{ 4 } +  \eta_{reg}  \cdot  \left(  {T}_{ 9 } - {T}_{ 4 }  \right)  \\
    &= 520.4\ \mathrm{K} +  0.85\  \cdot  \left(  1086.1\ \mathrm{K} - 520.4\ \mathrm{K}  \right) \\
    &= 1001.2\ \mathrm{K}
\end{aligned}

\begin{aligned}
  {T}_{ 10 } &=   {T}_{ 9 } -  \left(  {T}_{ 5 } - {T}_{ 4 }  \right)  \\
    &=  1086.1\ \mathrm{K} - \left(  1001.2\ \mathrm{K} - 520.4\ \mathrm{K}  \right) \\
    &= 605.24\ \mathrm{K}
\end{aligned}

#### 5$\to$6) Combustor 1

\begin{aligned}
  q_{5\to{}6} &=   c_{p}  \cdot  \left(  {T}_{ 6 } - {T}_{ 5 }  \right)  \\
    &=  1.0047\ \frac{\mathrm{kJ}}{\left(\mathrm{K} \cdot \mathrm{kg}\right)}  \cdot  \left(  1400\ \mathrm{K} - 1001.2\ \mathrm{K}  \right) \\
    &= 400.68\ \frac{\mathrm{kJ}}{\mathrm{kg}}
\end{aligned}

#### 7$\to$8) Combustor 2

\begin{aligned}
  q_{7\to{}8} &=   c_{p}  \cdot  \left(  {T}_{ 8 } - {T}_{ 7 }  \right)  \\
    &=  1.0047\ \frac{\mathrm{kJ}}{\left(\mathrm{K} \cdot \mathrm{kg}\right)}  \cdot  \left(  1400\ \mathrm{K} - 1086.1\ \mathrm{K}  \right) \\
    &= 315.42\ \frac{\mathrm{kJ}}{\mathrm{kg}}
\end{aligned}

#### Heat input

\begin{aligned}
  q_{in} &=  q_{5\to{}6} + q_{7\to{}8} \\
    &= 400.68\ \frac{\mathrm{kJ}}{\mathrm{kg}} + 315.42\ \frac{\mathrm{kJ}}{\mathrm{kg}}\\
    &= 716.1\ \frac{\mathrm{kJ}}{\mathrm{kg}}
\end{aligned}

#### (c) Thermal efficiency

```
%%showcalc
##### Thermal efficiency
eta_th = w_net/q_in

##### Second Law efficiency
# The low temperature is assumed to be the intake temperature
T_L = T[1]
# The high temperature is approximated as the hottest temperature of the working fluid
T_H = T[6]
# Carnot efficiency
eta_th_Carnot = 1-T_L/T_H

# Second Law efficiency
eta_II = eta_th/eta_th_Carnot
```

#### Thermal efficiency

\begin{aligned}
  \eta_{th} &=  \frac{ w_{net} }{ q_{in} } \\
    &= \frac{ 312.9\ \frac{\mathrm{kJ}}{\mathrm{kg}} }{ 716.1\ \frac{\mathrm{kJ}}{\mathrm{kg}} }\\
    &= 0.43696\
\end{aligned}

#### Second Law efficiency

 The low temperature is assumed to be the intake temperature

\begin{aligned}
  T_{L} &=  {T}_{ 1 }  = 300\ \mathrm{K}
\end{aligned}

 The high temperature is approximated as the hottest temperature of the working fluid

\begin{aligned}
  T_{H} &=  {T}_{ 6 }  = 1400\ \mathrm{K}
\end{aligned}

 Carnot efficiency

\begin{aligned}
  \eta_{th,Carnot} &=   1 - \frac{ T_{L} }{ T_{H} }  \\
    &=  1 - \frac{ 300\ \mathrm{K} }{ 1400\ \mathrm{K} } \\
    &= 0.78571\
\end{aligned}

 Second Law efficiency

\begin{aligned}
  \eta_{II} &=  \frac{ \eta_{th} }{ \eta_{th,Carnot} } \\
    &= \frac{ 0.43696\ }{ 0.78571\ }\\
    &= 0.55613\
\end{aligned}

#### (d) Back-Work-Ratio

```
%%showcalc
##### work in
w_in = -(w_1_to_2 + w_3_to_4)

##### work out
w_out = w_6_to_7 + w_8_to_9

##### back work ratio
BWR = w_in/w_out
```

#### work in

\begin{aligned}
  w_{in} &=  -w_{1\to{}2} + w_{3\to{}4} \\
    &= -\left( -146.74\ \frac{\mathrm{kJ}}{\mathrm{kg}} + -171.2\ \frac{\mathrm{kJ}}{\mathrm{kg}} \right)\\
    &= 317.94\ \frac{\mathrm{kJ}}{\mathrm{kg}}
\end{aligned}

#### work out

\begin{aligned}
  w_{out} &=  w_{6\to{}7} + w_{8\to{}9} \\
    &= 315.42\ \frac{\mathrm{kJ}}{\mathrm{kg}} + 315.42\ \frac{\mathrm{kJ}}{\mathrm{kg}}\\
    &= 630.85\ \frac{\mathrm{kJ}}{\mathrm{kg}}
\end{aligned}

#### back work ratio

\begin{aligned}
  BWR &=  \frac{ w_{in} }{ w_{out} } \\
    &= \frac{ 317.94\ \frac{\mathrm{kJ}}{\mathrm{kg}} }{ 630.85\ \frac{\mathrm{kJ}}{\mathrm{kg}} }\\
    &= 0.50399\
\end{aligned}

#### Summary of Results

```
Summary()
Summary(['w_net','q_in','eta_th','BWR']);
```

\begin{aligned}{ BWR }&={ 0.50399\ }&{ R }&={ 0.28705\ \frac{\mathrm{kJ}}{\left(\mathrm{K} \cdot \mathrm{kg}\right)} }&{ T_{H} }&={ 1400\ \mathrm{K} }\\{ T_{L} }&={ 300\ \mathrm{K} }&{ T_{room} }&={ 25\ \mathrm{°C} }&{ c_{p} }&={ 1.0047\ \frac{\mathrm{kJ}}{\left(\mathrm{K} \cdot \mathrm{kg}\right)} }\\{ c_{v} }&={ 0.71768\ \frac{\mathrm{kJ}}{\left(\mathrm{K} \cdot \mathrm{kg}\right)} }&{ \eta_{II} }&={ 0.55613\ }&{ \eta_{c} }&={ 0.8\ }\\{ \eta_{reg} }&={ 0.85\ }&{ \eta_{t} }&={ 0.8\ }&{ \eta_{th} }&={ 0.43696\ }\\{ \eta_{th,Carnot} }&={ 0.78571\ }&{ k }&={ 1.4\ }&{ q_{2\to{}3} }&={ -96.507\ \frac{\mathrm{kJ}}{\mathrm{kg}} }\\{ q_{5\to{}6} }&={ 400.68\ \frac{\mathrm{kJ}}{\mathrm{kg}} }&{ q_{7\to{}8} }&={ 315.42\ \frac{\mathrm{kJ}}{\mathrm{kg}} }&{ q_{in} }&={ 716.1\ \frac{\mathrm{kJ}}{\mathrm{kg}} }\\{ r_{p} }&={ 3.1623\ }&{ w_{1\to{}2} }&={ -146.74\ \frac{\mathrm{kJ}}{\mathrm{kg}} }&{ w_{3\to{}4} }&={ -171.2\ \frac{\mathrm{kJ}}{\mathrm{kg}} }\\{ w_{6\to{}7} }&={ 315.42\ \frac{\mathrm{kJ}}{\mathrm{kg}} }&{ w_{8\to{}9} }&={ 315.42\ \frac{\mathrm{kJ}}{\mathrm{kg}} }&{ w_{in} }&={ 317.94\ \frac{\mathrm{kJ}}{\mathrm{kg}} }\\{ w_{net} }&={ 312.9\ \frac{\mathrm{kJ}}{\mathrm{kg}} }&{ w_{out} }&={ 630.85\ \frac{\mathrm{kJ}}{\mathrm{kg}} }&{  }\end{aligned}

      T
      p

      unit
      K
      kPa

      1
      300.000000
      100.000000

      2
      446.052996
      316.227766

      2s
      416.842397
      316.227766

      3
      350.000000
      316.227766

      4
      520.395162
      1000.000000

      4s
      486.316129
      1000.000000

      5
      1001.210310
      1000.000000

      6
      1400.000000
      1000.000000

      7
      1086.060042
      316.227766

      7s
      1007.575053
      316.227766

      8
      1400.000000
      316.227766

      9
      1086.060042
      100.000000

      9s
      1007.575053
      100.000000

      10
      605.244894
      100.000000

\begin{aligned}{ w_{net} }&={ 312.9\ \frac{\mathrm{kJ}}{\mathrm{kg}} }\\{ q_{in} }&={ 716.1\ \frac{\mathrm{kJ}}{\mathrm{kg}} }\\{ \eta_{th} }&={ 0.43696\ }\\{ BWR }&={ 0.50399\ }\\{  }\end{aligned}

### Plotting
*Note: The plotting library uses the property tables to draw the process paths, which inherently assumes variable specific heat (i.e. real-fluid or air-standard assumptions).  If the library is used to draw process paths between states were obtained using constant specific heat (cold-air-standard assumptions) There will be inconsistencies between the state points and the process paths.*

In order to plot the paths of the cycle on the $p$-$v$ diagram and states on the $T$-$s$ diagram, we need discrete values for the specifc enthalpy and entropy at each state rather than just the changes in properties we calculated above.  To do this we can pick an arbritrary value for the enthalpy and entropy at any state, then caclulate the enthalpy and entropy at the rest of the states relative to the reference point. For this case, we will look up the properties from the tables for state 1 and use that as our starting point.

```
# Reference point
v[1] = R*T[1]/p[1]
h[1] = air.h(T=T[1],p=p[1])
s[1] = air.s(T=T[1],p=p[1])

for i in ['2s',2,3,'4s',4,5,6,'7s',7,8,'9s',9,10]:
    v[i] = R*T[i]/p[i]
#     h[i] = h[1] + c_p*(T[i]-T[1])
#     s[i] = s[1] + c_p*log(T[i]/T[1]) - R*log(p[i]/p[1])
    h[i] = air.h(T=T[i])
    s[i] = air.s(T=T[i],p=p[i])

#ShowCalculations( comments=True);
states.display()
```

      T
      p
      v
      h
      s

      unit
      K
      kPa
      m³/kg
      kJ/kg
      kJ/K/kg

      1
      300.000000
      100.000000
      0.861158
      -2.407134
      6.707700

      2
      446.052996
      316.227766
      0.404900
      145.337367
      6.778262

      2s
      416.842397
      316.227766
      0.378385
      115.585390
      6.709279

      3
      350.000000
      316.227766
      0.317709
      47.921889
      6.532376

      4
      520.395162
      1000.000000
      0.149381
      221.687351
      6.606069

      4s
      486.316129
      1000.000000
      0.139598
      186.567062
      6.536272

      5
      1001.210310
      1000.000000
      0.287400
      745.062274
      7.314407

      6
      1400.000000
      1000.000000
      0.401874
      1211.923096
      7.706368

      7
      1086.060042
      316.227766
      0.985860
      842.451068
      7.738249

      7s
      1007.575053
      316.227766
      0.914616
      752.326962
      7.652121

      8
      1400.000000
      316.227766
      1.270836
      1211.923096
      8.036850

      9
      1086.060042
      100.000000
      3.117564
      842.451068
      8.068731

      9s
      1007.575053
      100.000000
      2.892271
      752.326962
      7.982603

      10
      605.244894
      100.000000
      1.737372
      310.142009
      7.424459

#### (a) $p$-$v$
Note: the isentropic lines do not line up exactly with the states in this diagram because the entropies were calculated for variable specific heat, but all other properties were calculated using the cold-air-standard assumptions (constant specific heat).

```
pv = air.pv_diagram()

# plot each state on the p,v diagram
for i in [1,2,3,4,5,6,7,8,9,10]:
    pv.plot_state(states[i],label_loc='north east')

# plot the process paths (Note: isentropic and non-ideal path not implemented for constant specific heat)
pv.plot_process(states[1],states[2],path='nonideal')
pv.plot_process(states[2],states[3],path='isobaric')
pv.plot_process(states[3],states[4],path='nonideal')
pv.plot_process(states[4],states[5],path='isobaric')
pv.plot_process(states[5],states[6],path='isobaric')
pv.plot_process(states[6],states[7],path='nonideal')
pv.plot_process(states[7],states[8],path='isobaric')
pv.plot_process(states[8],states[9],path='nonideal');
pv.plot_process(states[9],states[10],path='isobaric');
pv.plot_process(states[10],states[1],path='isobaric',linestyle='dashed');
```

#### (b) $T$-$s$ diagram
*Note: the process paths do not line up with the state points in this diagram because the paths were
calculated for variable specific heat, but the states were calculated using the
cold-air-standard assumptions (constant specific heat).  Therefore the errors resulting from the constant specific heat assumption are evident in the misalignment on the graph.  The larger inconsistencies at the higher temperatures illustrate how the constant specific heat assumption becomes increasing inaccurate as the temperature change of the fluid increases.*

```
Ts = air.Ts_diagram()

# plot each state on the p,v diagram
for i in [1,3]:
    Ts.plot_state(states[i],label_loc='west')
Ts.plot_state(states[4])
for i in [6,8,9]:
    Ts.plot_state(states[i],label_loc='east')
for i in [2,5,7,10]:
    Ts.plot_state(states[i],label_loc='south east')

# plot the process paths (Note: isentropic and non-ideal path not implemented for constant specific heat)
Ts.plot_process(states[1],states[2],path='nonideal')
Ts.plot_process(states[2],states[3],path='isobaric',label='intercooler',labelprops=dict(ha='left'))
Ts.plot_process(states[3],states[4],path='nonideal')
Ts.plot_process(states[4],states[5],path='isobaric',label='regenerator')
Ts.plot_process(states[5],states[6],path='isobaric',label='combustor')
Ts.plot_process(states[6],states[7],path='nonideal')
Ts.plot_process(states[7],states[8],path='isobaric',label='reheat')
Ts.plot_process(states[8],states[9],path='nonideal');
Ts.plot_process(states[9],states[10],path='isobaric',label='regenerator');
Ts.plot_process(states[10],states[1],path='isobaric',linestyle='dashed');
```

examples/ME321-Thermodynamics_II/02 - Chapter 9 Gas Power Cycles/Ex9.5 Modified Brayton Cycle (Cold-Air-Standard).ipynb


#Bermudan Swaption Valuation Under The Hull White Model

This notebook regroup the total code and all the documentation for the interest rate projet of the Course given at Centrale Marseille by Mr Abderrahim Ben Jazia .
Author Name : Audric Dongfack Kengfack

Email:(dongfackaudric@gmail.com)
 Linkedin : linkedin.com/in/audricdongfack

```
!pip install nelson_siegel_svensson
!pip install QuantLib
```

    Collecting nelson_siegel_svensson
      Downloading https://files.pythonhosted.org/packages/5b/4a/56beecc0a9ce94f97789a93442f7925c92dc5ed54352f5d97e461cee51c1/nelson_siegel_svensson-0.4.2-py2.py3-none-any.whl
    Requirement already satisfied: numpy>=1.14 in /usr/local/lib/python3.6/dist-packages (from nelson_siegel_svensson) (1.17.5)
    Requirement already satisfied: Click>=6.0 in /usr/local/lib/python3.6/dist-packages (from nelson_siegel_svensson) (7.0)
    Requirement already satisfied: matplotlib>=3.0 in /usr/local/lib/python3.6/dist-packages (from nelson_siegel_svensson) (3.1.3)
    Requirement already satisfied: scipy>=1.2 in /usr/local/lib/python3.6/dist-packages (from nelson_siegel_svensson) (1.4.1)
    Requirement already satisfied: kiwisolver>=1.0.1 in /usr/local/lib/python3.6/dist-packages (from matplotlib>=3.0->nelson_siegel_svensson) (1.1.0)
    Requirement already satisfied: python-dateutil>=2.1 in /usr/local/lib/python3.6/dist-packages (from matplotlib>=3.0->nelson_siegel_svensson) (2.6.1)
    Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.6/dist-packages (from matplotlib>=3.0->nelson_siegel_svensson) (0.10.0)
    Requirement already satisfied: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1 in /usr/local/lib/python3.6/dist-packages (from matplotlib>=3.0->nelson_siegel_svensson) (2.4.6)
    Requirement already satisfied: setuptools in /usr/local/lib/python3.6/dist-packages (from kiwisolver>=1.0.1->matplotlib>=3.0->nelson_siegel_svensson) (45.2.0)
    Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.6/dist-packages (from python-dateutil>=2.1->matplotlib>=3.0->nelson_siegel_svensson) (1.12.0)
    Installing collected packages: nelson-siegel-svensson
    Successfully installed nelson-siegel-svensson-0.4.2
    Collecting QuantLib
      Downloading https://files.pythonhosted.org/packages/4b/bc/6be352b3d81b5779fcc1bf3fb99801e43cb33832c368e9f73f4fa8dcbcb6/QuantLib-1.17-cp36-cp36m-manylinux1_x86_64.whl (17.4MB)
         |████████████████████████████████| 17.4MB 276kB/s
    Installing collected packages: QuantLib
    Successfully installed QuantLib-1.17

```
#Library
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
#===============================
#for the calibration part
import QuantLib as ql
from collections import namedtuple
import math
#===============================
sns.set()
%matplotlib inline

```

##Question 2 - Calibration of the Nelson Siegel Model

For this question, i built my dataset using data available on the ECB website. The data contains two columns, one representing maturities in years and another representing prices.
The link of the brut dataset is there :

https://www.ecb.europa.eu/stats/financial_markets_and_interest_rates/euro_area_yield_curves/html/index.en.html

```
#I load my   data
ZC_Yield_Curve = pd.read_csv("data_ZC_Yield_Curve.csv")
```

```
ZC_Yield_Curve.head(2)
```

      Maturity
      Price

      0
      0.25
      0.001486

      1
      0.50
      0.002666

```
ZC_Yield_Curve.Price #The Price Column
```

    0      0.001486
    1      0.002666
    2      0.004233
    3      0.005965
    4      0.007754
             ...
    115    0.040028
    116    0.039977
    117    0.039928
    118    0.039881
    119    0.039835
    Name: Price, Length: 120, dtype: float64

```
#Here we construct take a subset of my dataset (with just 2 columns)
 ZC_Yield_Curve_Data = pd.DataFrame(ZC_Yield_Curve, columns = ['Maturity', 'Price'])
```

```
ZC_Yield_Curve_Data.head(2)
```

      Maturity
      Price

      0
      0.25
      0.001486

      1
      0.50
      0.002666

```
#I draw the curve with the data
figure = ZC_Yield_Curve_Data.plot(x='Maturity', y = 'Price', title ="Zero Coupon Bond Yield Curve" )
figure.set_ylabel("Price")

```

First approach i  use the the nelson siegel library developpe by luphord luphord@protonmail.com . The link of the website is : https://nelson-siegel-svensson.readthedocs.io/en/latest/readme.html

```
from nelson_siegel_svensson import NelsonSiegelSvenssonCurve
from nelson_siegel_svensson.calibrate import calibrate_ns_ols

```

```
t = np.array(ZC_Yield_Curve_Data.Maturity)
y = np.array(ZC_Yield_Curve_Data.Price)

curve, status = calibrate_ns_ols(t, y, tau0=1.0)  # starting value of 1.0 for the optimization of tau
assert status.success
print(curve)
```

    NelsonSiegelCurve(beta0=0.04667636232318473, beta1=-0.04572289071926614, beta2=-0.039263323137185946, tau=1.2385573595407124)

```
yexamplet = NelsonSiegelSvenssonCurve(0.04667636232318473,-0.04572289071926614,-0.039263323137185946,0.15,1.2385573595407124,4.0)
texamplet = np.array(ZC_Yield_Curve_Data.Maturity)
plt.plot(texamplet, yexamplet(texamplet))
```

We define the [Nelson-Siegel](https://en.wikipedia.org/wiki/Fixed-income_attribution#Modeling_the_yield_curve) function:
$$y(t) = \beta_0 + \beta_1 \frac{1-\exp(-t/\lambda)}{t/\lambda} + \beta_2 (\frac{1-\exp(-t/\lambda)}{t/\lambda} - \exp(-t/\lambda))$$

```
#we define the nelson siegel function
def nelson_siegel(term, beta0, beta1, beta2,lambda_par):
    return (beta0 +beta1*(1-np.exp(-term/lambda_par))/(term/lambda_par)
    +beta2*((1-np.exp(-term/lambda_par))/(term/lambda_par)-np.exp(-term/lambda_par)))

```

```
# Comparison Graph between the true ZC_Yield_Curve and the fitted one
Fitted_ZC_Yield_Curve=[nelson_siegel(elem,0.04667636232318473,-0.04572289071926614,-0.039263323137185946,1.2385573595407124) for elem in t] #t is the array of maturity
plt.plot(t,Fitted_ZC_Yield_Curve)
plt.plot(t,y)
plt.legend(["Fitted_ZC_Yield_Curve","True ZC_Yield_Curve"])
plt.xlabel("Maturity")
plt.ylabel("Price")
plt.title("Zero Coupon Bond Yield Curve & Prediction")
plt.show()
```

##Question 3 - Instantaneous forward rate

The expresion of the instantaneous forward rate under the Nelson Siegel Characterization is :
$$ f(\tau) = \beta_{0} +\beta_{1}e^{\frac{-\tau}{\lambda}} +\beta_{2}(\frac{\tau}{\lambda})e^{\frac{-\tau}{\lambda}}$$

```
#definition of the instantaneous forward rate
def instantaneous_forward_rate(term, beta0, beta1, beta2,lambda_par):
    return (beta0 +beta1*np.exp(-term/lambda_par)
    +beta2*(term/lambda_par)*np.exp(-term/lambda_par))
```

```
#graph of the instantaneous forward rate
forward_rate_curve=[instantaneous_forward_rate(elem,0.04667636232318473,-0.04572289071926614,-0.039263323137185946,1.2385573595407124) for elem in t] #t is the array of maturity
plt.plot(t,forward_rate_curve)
plt.title("Instantaneous forward rate")
plt.xlabel("Maturity")
plt.ylabel("Forward rate")
```

##Question 4 - The parameter  $\theta$

The expresion of the parameter $\theta$ is given by :
$$\theta(\tau) =a\beta_{0} + (a-\frac{1}{\lambda})\beta_{1}e^{\frac{-\tau}{\lambda}} +(\frac{1}{\lambda}+\frac{\tau}{\lambda}(a-\frac{1}{\lambda}))\beta_{2}e^{\frac{-\tau}{\lambda}} + \frac{\sigma^{2}}{2a}(1-e^{-2a\tau})$$

```
#definition of parameter theta function
#recall that here we need to fixed a and sigma
def parameter_theta(term, beta0, beta1, beta2,lambda_par,a,sigma):
    return (a*beta0 +(a-1/lambda_par)*beta1*np.exp(-term/lambda_par)
    +(1/lambda_par + (term/lambda_par)*(a-1/lambda_par))*beta2*(term/lambda_par)*np.exp(-term/lambda_par) + (sigma**2/2*a)*(1-np.exp(-2*term/lambda_par)))
```

```
#graph of the instantaneous forward rate
#here we use a = 1 and sigma = 0.01
theta_curve=[parameter_theta(elem,0.04667636232318473,-0.04572289071926614,-0.039263323137185946,1.2385573595407124,1,0.01) for elem in t] #t is the array of maturity
plt.plot(t,theta_curve)
plt.title("Curve of the parameter theta")
plt.xlabel("Maturity")
plt.ylabel("theta value")
```

#Trinomial Tree

##Question 5 - Construction of the auxillary tree for a 10 years period (First stage)

Construct the auxilliary tree is simply contruct the  sparse matric of $R_{i,j}^{*}$.

To do that, we procede as follow.
first we get total nodes at  each time i. And second , in this each nodes we put the corresponding value $R_{i,j}^{*} = j \Delta R^{*}$.

This procedure for the first stage is well explain in  (chourdaskis,2008)

```
#Here we define the function get_nodes that takes an array of nodes at time i and return and array of nodes at time i+1

def get_nodes(array_nodes,a,dt):
  futur_array_nodes = []
  for j in array_nodes:
    k = np.round(j*np.exp(-a*dt),decimals=0)
    futur_array_nodes += [k-1,k,k+1]
  futur_array_nodes = list(set(futur_array_nodes))  #we set the list to avoid the repetition of any future node
  futur_array_nodes.sort() #we arrange our table in ascending order
  return futur_array_nodes

```

```
#now we represent the sparse matrix (auxilliary tree)
#array of different nodes we have at each time i < = 10 years, because we take here dt = 1
nodes = [[-1,0,1]]
initial_nodes = [-1,0,1]
for i in range(1,10):
  new_nodes = get_nodes(initial_nodes,0.1,1)
  nodes.append(new_nodes)
  initial_nodes = new_nodes

```

```
#tree
nodes
```

    [[-1, 0, 1],
     [-2.0, -1.0, 0.0, 1.0, 2.0],
     [-3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0],
     [-4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0],
     [-5.0, -4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0],
     [-6.0, -5.0, -4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
     [-6.0, -5.0, -4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
     [-6.0, -5.0, -4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
     [-6.0, -5.0, -4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
     [-6.0, -5.0, -4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0]]

```
#now we plot the tree
for time,elem in enumerate(nodes):
  for value in elem:
    plt.plot(time+1,value,'o',color="steelblue")
plt.plot(0,0,'o',color="steelblue")
plt.title("Graph")
plt.xlabel("Time i")
plt.ylabel("Space j")
```

```
#nodes size-nodes-element
sizenodeselem=[]
for elem in nodes:
  sizenodeselem.append(len(elem))
```

```
print(sizenodeselem)
```

    [3, 5, 7, 9, 11, 13, 13, 13, 13, 13]

```
#We construct the first stage
#According to the visualisation of the graph above
#We call
#Pu : Probability of an upward move
#Pd : Probability of a downward move
#Pm : Probability of a median move

#initialisation
#==================================
sigma = 0.01
a = 0.1
maturity = 10
steps = 10
dt = maturity/steps
jmax = 6
#===================================

#we define the process, the nodes and the probabilities of each node. the time start at date i = 1

Rstar = np.zeros((2*jmax+1,maturity))
pu = np.zeros((2*jmax+1,maturity))
pm = np.zeros((2*jmax+1,maturity))
pd = np.zeros((2*jmax+1,maturity))
#Compared to the previous graph, we only have a problem to define the probabilities
# of the extreme nodes at time 6 , 7 , 8 and 9
#which we are goind  to correct at the end of the algorithm

for i in range(1,6):
  jmaxx = sizenodeselem[i-1]
  for j in range(0,jmaxx):
    jreal =  (jmaxx-1)/2 -j
    #we compute the process and the probabilities of branching
    k = np.round(jreal*np.exp(-a*dt),decimals=0)
    deltaR = sigma*np.sqrt(3/(2*a)*(1-np.exp(-2*a*dt)))
    Mj = jreal*deltaR*np.exp(-a*dt)
    V = (sigma**2/(2*a))*(1-np.exp(-2*a*dt))
    netaj = Mj-k*deltaR
    Rstar[j,i]= jreal*deltaR
    pu[j,i] = 1/6+netaj**2/(6*V)+netaj/(2*np.sqrt(3*V))
    pm[j,i] = 2/3-netaj**2/(3*V)
    pd[j,i] = 1/6+netaj**2/(6*V)-netaj/(2*np.sqrt(3*V))

#we detail this part just to show that due to the mean reversion, the probability must change
#at the bottom of the tree in Jmax and -Jmax
#according to the project it is not realy the case
for i in range(6,10):
  jmaxx = 13
  for j in range(0,jmaxx):
    jreal = (jmaxx-1)/2 - j
    #we compute the process and the probabilities of branching
    k = np.round(jreal*np.exp(-a*dt),decimals=0)
    deltaR = sigma*np.sqrt(3/(2*a)*(1-np.exp(-2*a*dt)))
    Mj = jreal*deltaR*np.exp(-a*dt)
    V = (sigma**2/(2*a))*(1-np.exp(-2*a*dt))
    netaj = Mj-k*deltaR
    Rstar[j,i]= jreal*deltaR
    if j == 0 :
      pu[j,i] = 1/6 + netaj**2/(6*V)+netaj/(2*np.sqrt(3*V))
      pm[j,i] = 2/3 - netaj**2/(3*V)
      pd[j,i] = 1/6 + netaj**2/(6*V)-netaj/(2*np.sqrt(3*V))
    elif j==jmaxx:
      pu[j,i] = 1/6 + netaj**2/(6*V)-netaj/(2*np.sqrt(3*V))
      pm[j,i] = 2/3-netaj**2/(3*V)
      pd[j,i] = 1/6 + netaj**2/(6*V)-netaj/(2*np.sqrt(3*V))
    else :
      pu[j,i] = 1/6 + netaj**2/(6*V)+netaj/(2*np.sqrt(3*V))
      pm[j,i] = 2/3-netaj**2/(3*V)
      pd[j,i] = 1/6 + netaj**2/(6*V)-netaj/(2*np.sqrt(3*V))

```

```
print(Rstar[0,1],Rstar[1,1],Rstar[2,1])
```

    0.016489507887836655 0.0 -0.016489507887836655

##Question 6 - Construction of the process $Q_{i,j}$

```
#We construct the second stage process
Q = np.zeros((2*jmax+1,maturity))
theta = maturity*[0.0]
```

```
print(theta)
```

    [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]

```
#we define the function jmax
#this function give the jmax of the tree (optional function since we now the value of jmax (6))
#def j_max(nodes,a,dt):
  #return (len(nodes)-1)/2

```

```
# we first define the probability transition between the node (i,h) to the node (i+1,j)
#Pu : Probability of an upward move
#Pd : Probability of a downward move
#Pm : Probability of a median move
def q(i,h,j,a,sigma,dt):
    #jmax = j_max(nodes,a,dt) to generalize the function
    jmax = 6
    k = np.round(h*np.exp(-a*dt),decimals=0)
    deltaR = sigma*np.sqrt(3/(2*a)*(1-np.exp(-2*a*dt)))
    Mj = h*deltaR*np.exp(-a*dt)
    V = sigma**2/(2*a)*(1-np.exp(-2*a*dt))
    netaj = Mj-k*deltaR
    pu = 1/6+netaj**2/(6*V)+netaj/(2*np.sqrt(3*V))
    pm = 2/3-netaj**2/(3*V)
    pd = 1/6+netaj**2/(6*V)-netaj/(2*np.sqrt(3*V))

    if j>jmax or h>jmax or h<-jmax:
        return 0
    else :
        if i< jmax:
            if j == h-1:
                return pd
            elif j == h:
                return pm
            elif j == h+1:
                return pu
            else :
                return 0
        else :
            if -jmax< h <jmax:
                if j==h-1:
                    return pd
                elif j == h:
                    return pm
                elif j == h+1:
                    return pu
                else :
                    return 0

            elif h == jmax:
                if j == h:
                    return pu
                elif j == h-1:
                    return pm
                elif j == h-2:
                    return pd
                else :
                    return 0

            elif h == -jmax:
                if j == h:
                    return pd
                elif j == h+1:
                    return pm
                elif j == h+2:
                    return pu
                else :
                    return 0
```

```
#verification that the probability is well define
q(0,0,-1,0.1,0.01,1)
```

    0.16666666666666666

```
#we create list for the Zero coupond bond maturity base on data
ZC = [0.00596481,0.012907481,0.01847603,0.022517886,0.025528996,0.02799328,0.030214566,0.032326139,0.034348445,0.036244585]
```

```
#First time steps
#since dt = 1, devide by 1 don't affect the ratio
Q = np.zeros((2*jmax+1,maturity))
theta = maturity*[0.0]
Q[0,0] = 1
theta[0]= ZC[0]
```

```
#Second time steps
Q[0,1]=Q[0,0]*pu[0,1]*np.exp(-ZC[0]*dt)
Q[1,1]=Q[0,0]*pm[1,1]*np.exp(-ZC[0]*dt)
Q[2,1]=Q[0,0]*pd[2,1]*np.exp(-ZC[0]*dt)
addition = Q[0,1]*np.exp(-dt*Rstar[0,1])+Q[1,2]*np.exp(-dt*Rstar[1,1])+Q[2,1]*np.exp(-dt*Rstar[0,1])
theta[1]= -np.log(ZC[0]/addition)

```

```
print(theta)
```

    [0.00596481, 3.7019740810843857, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]

```

#other time steps
for i in range(2,10):
  jmaxx = sizenodeselem[i-1]
  somme = 0
  sommeun = 0
  for j in range(0,jmaxx):
    jreal = (jmaxx-1)/2 - j
    for h in range(0,jmaxx):
      hreal = (jmaxx-1)/2 - h
      somme = somme + Q[h,i-1]*q(i-1,hreal,jreal,a,sigma,dt)*np.exp(-1*(theta[i-1] + Rstar[h,i-1]))
    Q[j,i] = somme
    somme = 0
  #we compute the new theta(i)
  for j in range(0,jmaxx) :
    jreal = (jmaxx-1)/2 - j
    sommeun = sommeun + Q[j,i-1]*np.exp(-Rstar[j,i-1])
  theta[i] = -np.log(ZC[i]/sommeun)
  #and the loop go forward

```

```
#print(Q)
print(theta)
```

    [0.00596481, 3.7019740810843857, 3.8953116149927123, -0.03085961394642545, -4.090253098448104, -4.197945073785415, -0.23828912258777152, 3.8036718039353734, 3.9280681874265992, 0.022857721548726125]

## Contruction of the  process $R_{i,j}$

We have $R_{i,j} = R_{i,j}^{*} + \theta (i)$

```
#we define the matrix R
R = np.zeros((2*jmax+1,maturity))
R[0,0] = theta[0]
```

```
#maturity = 10
for i in range(1,10):
  jmaxx = sizenodeselem[i-1]
  for j in range(0,jmaxx):
    R[j,i] = Rstar[j,i] + theta[i]
```

```
print(R)
```

    [[ 5.96481000e-03  3.71846359e+00  3.92829063e+00  1.86089097e-02
      -4.02429507e+00 -4.11549753e+00 -1.39352075e-01  3.90260885e+00
       4.02700523e+00  1.21794769e-01]
     [ 0.00000000e+00  3.70197408e+00  3.91180112e+00  2.11940183e-03
      -4.04078457e+00 -4.13198704e+00 -1.55841583e-01  3.88611934e+00
       4.01051573e+00  1.05305261e-01]
     [ 0.00000000e+00  3.68548457e+00  3.89531161e+00 -1.43701061e-02
      -4.05727408e+00 -4.14847655e+00 -1.72331091e-01  3.86962984e+00
       3.99402622e+00  8.88157531e-02]
     [ 0.00000000e+00  0.00000000e+00  3.87882211e+00 -3.08596139e-02
      -4.07376359e+00 -4.16496606e+00 -1.88820599e-01  3.85314033e+00
       3.97753671e+00  7.23262452e-02]
     [ 0.00000000e+00  0.00000000e+00  3.86233260e+00 -4.73491218e-02
      -4.09025310e+00 -4.18145557e+00 -2.05310107e-01  3.83665082e+00
       3.96104720e+00  5.58367373e-02]
     [ 0.00000000e+00  0.00000000e+00  0.00000000e+00 -6.38386297e-02
      -4.10674261e+00 -4.19794507e+00 -2.21799615e-01  3.82016131e+00
       3.94455770e+00  3.93472294e-02]
     [ 0.00000000e+00  0.00000000e+00  0.00000000e+00 -8.03281376e-02
      -4.12323211e+00 -4.21443458e+00 -2.38289123e-01  3.80367180e+00
       3.92806819e+00  2.28577215e-02]
     [ 0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
      -4.13972162e+00 -4.23092409e+00 -2.54778630e-01  3.78718230e+00
       3.91157868e+00  6.36821366e-03]
     [ 0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
      -4.15621113e+00 -4.24741360e+00 -2.71268138e-01  3.77069279e+00
       3.89508917e+00 -1.01212942e-02]
     [ 0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
       0.00000000e+00 -4.26390311e+00 -2.87757646e-01  3.75420328e+00
       3.87859966e+00 -2.66108021e-02]
     [ 0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
       0.00000000e+00 -4.28039261e+00 -3.04247154e-01  3.73771377e+00
       3.86211016e+00 -4.31003100e-02]
     [ 0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
       0.00000000e+00  0.00000000e+00 -3.20736662e-01  3.72122426e+00
       3.84562065e+00 -5.95898179e-02]
     [ 0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
       0.00000000e+00  0.00000000e+00 -3.37226170e-01  3.70473476e+00
       3.82913114e+00 -7.60793258e-02]]

#Question 7 - Values of $\theta (i)$

Note here that practically, the determination of $\theta(i)$ is done at the same time as the construction of the values $Q(i,j)$.

```
#visualisation of the theta(i) graph
time=[1,2,3,4,5,6,7,8,9,10]
plt.plot(time,theta)
plt.title("Curve of the parameter theta calibrate on 10 year")
plt.xlabel("Maturity")
plt.ylabel("theta value")

```

#Question 8 - Tree for the libor Rate $L(T_{i},T_{i+1})$

$$
L(T_{i},T_{i+1}) = \frac{1}{\Delta T} (e^{R_{i}\Delta T} -1)
$$
Constructing the libor tree is therefore equivalent to constructing at each node the value of the corresponding libor.

```
#we define the matrix Libor Libor[.,i] is exactly the same as Libor[Ti,Ti+1] view at node (i,.)
#since DeltaT = 1
Libor = np.zeros((2*jmax+1,maturity))
Libor[0,0] = np.exp(R[0,0]) - 1
```

```
#maturity = 10
for i in range(1,10):
  jmaxx = sizenodeselem[i-1]
  for j in range(0,jmaxx):
    Libor[j,i] = np.exp(R[j,i]) - 1
```

```
print(Libor)
```

```

```

#Question 9 - Valuation procedure of the Swap between $(T_{0},T_{10}) = (0,10)$ with fixed leg $ 2\% $

At each J-node, the swap price is given by the following expression (We take the nominal equal to 1):
$$SwapPrice_{i,j} = [p_{u} SwapPrice_{i+1,k+1} + p_{m}SwapPrice_{i+1,k}+ p_{d} SwapPrice_{i+1,k-1}] + e^{-R_{i,j}}(Libor(T_{i},T_{i+1}) -K)$$

```
SwapPrice = np.zeros((2*jmax+1,maturity))
```

```
#we define the Swap price tree price of the swapp between (T0,T10)
#Delta t = 1
k = 0.02
for i in range(7,-1,-1):
  jmaxx = sizenodeselem[i-1]
  for j in range(0,jmaxx):
    jreal = (jmaxx-1)/2 - j
    kk = np.round(jreal*np.exp(-a*dt),decimals=0)
    ku =(jmaxx-1)/2-kk-1
    km = (jmaxx-1)/2-kk
    kd = (jmaxx-1)/2-kk+1
    if ku>=0 and km>=0 and kd>=0 and kd<=9:
      SwapPrice[j,i] = pu[j,i]*SwapPrice[int(ku),i+1]  + pm[j,i]*SwapPrice[int(km),i+1] + pd[j,i]*SwapPrice[int(kd),i+1] + np.exp(-R[j,i])*(Libor[j,i]-k)

```

```
#the swap price is given by the initial element SwapPrice[0,0]
#print(SwapPrice[0,0])
```

#Question 11 - Valuation Valuation of the caplet between $(T_{9},T_{10})$ fixed leg 2\%

```
CapletPrice = np.zeros((2*jmax+1,maturity))
```

```
#we only compute the caplet price at time T9
jmaxx = sizenodeselem[8]
for j in range(0,jmaxx):
  jreal = (jmaxx-1)/2 - j
  kk = np.round(jreal*np.exp(-a*dt),decimals=0)
  ku =(jmaxx-1)/2-kk-1
  km = (jmaxx-1)/2-kk
  kd = (jmaxx-1)/2-kk+1
  if ku>=0 and km>=0 and kd>=0 and kd<=9:
    CapletPrice[j,i] = pu[j,i]*CapletPrice[int(ku),i+1]  + pm[j,i]*CapletPrice[int(km),i+1] + pd[j,i]*CapletPrice[int(kd),i+1] + np.exp(-R[j,i])*max((Libor[j,i]-k),0)

```

```
#we have the caplet price at each node i,j. Since we have compute only in the time T9 we have CapletPrice[j,8] for each j-node
#Zero in our case
```

#Question  12- Valuation procedure of the European Swaption

At each J-node, the swaption price is given by the following expression[Using the tree] (We take the nominal equal to 1):
$$SwaptionPrice_{i,j} = [p_{u} SwaptionPrice_{i+1,k+1} + p_{m}SwaptionPrice_{i+1,k}+ p_{d} SwaptionPrice_{i+1,k-1}] + e^{-R_{i,j}}\max((Libor(T_{i},T_{i+1}) -K),0)$$

```
SwaptionPrice = np.zeros((2*jmax+1,maturity))
```

```
#we define the Swap price tree price of the swapp between (T0,T10)
#Delta t = 1
k = 0.02
for i in range(7,-1,-1):
  jmaxx = sizenodeselem[i-1]
  for j in range(0,jmaxx):
    jreal = (jmaxx-1)/2 - j
    kk = np.round(jreal*np.exp(-a*dt),decimals=0)
    ku =(jmaxx-1)/2-kk-1
    km = (jmaxx-1)/2-kk
    kd = (jmaxx-1)/2-kk+1
    #here we don't take into account the extrem node
    if ku>=0 and km>=0 and kd>=0 and kd<=9:
      SwaptionPrice[j,i] = pu[j,i]*SwaptionPrice[int(ku),i+1]  + pm[j,i]*SwaptionPrice[int(km),i+1] + pd[j,i]*SwaptionPrice[int(kd),i+1] + np.exp(-R[j,i])*max((Libor[j,i]-k),0)

```

```
#the price is given in the end by SwaptionPrice[0,0] at date T0
```

#Question 13 - Valuation of the Bermudan Swaption which can be exerciced at any time between $T_{0}$ and $T_{10}$

We have described our algorithm in the report. It is not yet implement  because it was done at the last minute.

#Question 14 - Calibration of the hull white model

Recall (Hull and white Model) :
\begin{equation}
dr_t = (\theta (t) - a r_t) dt + \sigma dW_t
\end{equation}
We want to determine $a$ and $\sigma$

```
#we  choose to determine Zero-Coupon bond curve at the same date as our data 31 january 2010
date = ql.Date(31, 1, 2010)
```

```
#now we download the term structure
reglage= ql.Date(31, 1, 2010)
ql.Settings.instance().evaluationDate = date;
term_structure = ql.YieldTermStructureHandle(
    ql.FlatForward(reglage,0.05,ql.Actual365Fixed())
    )
index = ql.Euribor1Y(term_structure)
```

```
#we now define the corresponding Swaption we need to calibrate the model (as we have say in the report)
#we take five of them
CalibrationData = namedtuple("CalibrationData",
                             "start, length, volatility")
data = [CalibrationData(1, 9, 0.1048),
        CalibrationData(2, 8, 0.0608),
        CalibrationData(3, 7, 0.1070),
        CalibrationData(4, 6, 0.1078),
        CalibrationData(5, 5, 0.1080)]
```

```
#calibration function/ the logic behind is describe in the report
def create_swaption_helpers(data, index, term_structure, engine):
    swaptions = []
    fixed_leg_tenor = ql.Period(1, ql.Years)
    fixed_leg_daycounter = ql.Actual360()
    floating_leg_daycounter = ql.Actual360()
    for d in data:
        vol_handle = ql.QuoteHandle(ql.SimpleQuote(d.volatility))
        helper = ql.SwaptionHelper(ql.Period(d.start, ql.Years),
                                   ql.Period(d.length, ql.Years),
                                   vol_handle,
                                   index,
                                   fixed_leg_tenor,
                                   fixed_leg_daycounter,
                                   floating_leg_daycounter,
                                   term_structure
                                   )
        helper.setPricingEngine(engine)
        swaptions.append(helper)
    return swaptions

def calibration_report(swaptions, data):
    print("-"*50)
    print(" %s %s %s %s %s" % ("ModelPrice", "MarketPrice", "ImpliedVol", "MarketVol", "RealError"))
    print("-"*50)
    cum_err = 0.0
    for i, s in enumerate(swaptions):
        model_price = s.modelValue()
        market_vol = data[i].volatility
        black_price = s.blackPrice(market_vol)
        rel_error = model_price/black_price - 1.0
        implied_vol = s.impliedVolatility(model_price,
                                          1e-5, 50, 0.0, 0.50)
        rel_error2 = implied_vol/market_vol-1.0
        cum_err += rel_error2*rel_error2

        print("%f %f %f %f %f" % (model_price, black_price, implied_vol, market_vol, rel_error))
    print("-"*50)
    print("Erreur cumulée : %15.5f" % math.sqrt(cum_err))
```

```
#valuation of the swaption an optimization
#we use here the JamshidianSwaptionEngine
#we choose the end_criteria in the order of 1e-8
model = ql.HullWhite(term_structure);
engine = ql.JamshidianSwaptionEngine(model)
swaptions = create_swaption_helpers(data, index, term_structure, engine)

optimization_method = ql.LevenbergMarquardt(1.0e-8,1.0e-8,1.0e-8)
end_criteria = ql.EndCriteria(10000, 100, 1e-8, 1e-8, 1e-8)
model.calibrate(swaptions, optimization_method, end_criteria)

a, sigma = model.params()
print("a = %f, sigma = %f" % (a, sigma))
```

    a = 0.000011, sigma = 0.004233

```
#Calibration Repport
print("a = %f, sigma = %f" % (a, sigma))
calibration_report(swaptions, data)
```

    a = 0.000011, sigma = 0.004233
    --------------------------------------------------
     ModelPrice MarketPrice ImpliedVol MarketVol RealError
    --------------------------------------------------
    0.011942 0.014411 0.086830 0.104800 -0.171347
    0.014614 0.010235 0.086840 0.060800 0.427828
    0.015247 0.018768 0.086887 0.107000 -0.187575
    0.014696 0.018217 0.086905 0.107800 -0.193285
    0.013339 0.016556 0.086943 0.108000 -0.194280
    --------------------------------------------------
    Erreur cumulée :         0.56899


Blankenbach Benchmark Case 1 (Scaled)
======

Steady isoviscous thermal convection
----

Two-dimensional, incompressible, bottom heated, steady isoviscous thermal convection in a 1 x 1 box, see case 1 of Blankenbach *et al.* 1989 for details.

**This example introduces:**
1. Loading/Saving variables to disk.
2. Defining analysis tools.
3. Finding a steady state.

**Keywords:** Stokes system, advective diffusive systems, analysis tools

**References**

B. Blankenbach, F. Busse, U. Christensen, L. Cserepes, D. Gunkel, U. Hansen, H. Harder, G. Jarvis, M. Koch, G. Marquart, D. Moore, P. Olson, H. Schmeling and T. Schnaubelt. A benchmark comparison for mantle convection codes. Geophysical Journal International, 98, 1, 23–38, 1989
http://onlinelibrary.wiley.com/doi/10.1111/j.1365-246X.1989.tb05511.x/abstract

```python
import underworld as uw
from underworld import function as fn
import glucifer
import math
import numpy as np
```

```python
import unsupported.scaling
from unsupported.scaling import pint
from unsupported.scaling import u
from unsupported.scaling import nonDimensionalize as nd
from unsupported.scaling import Dimensionalize as dim
```

Setup parameters
-----

```python
case = "a"
if(case=="a"):
    kinematicViscosity = 2.5e19 * u.meter**2 / u.second
elif(case=="b"):
    kinematicViscosity = 2.5e18 * u.meter**2 / u.second
else:
    kinematicViscosity = 2.5e17 * u.meter**2 / u.second

# Set grid resolution.
res = 64

boxHeight = 1e6 * u.meter
boxLength = 1e6 * u.meter

# Set max & min temperautres
tempMin =    273.15 * u.degK
tempMax = 1273.15 * u.degK

thermalDiffusion = 1e-6 * u.meter**2 / u.second
gravity = 10. * u.meter / u.second**2
density = 4e3 * u.kilogram / u.meter**3
Tref = 273.15 * u.degK
thermalExpansionCoef = 2.5e-5 * u.degK**(-1)
```

Raylegh number is $$Ra = \alpha g \Delta T h^3 / \kappa \nu$$

```python
def RayleighNumber(alpha, g, dT, H, kappa, nu):
    return alpha * g * dT * H**3 / kappa / nu
```

```python
print RayleighNumber(alpha = thermalExpansionCoef, g = gravity, dT= (tempMax - tempMin), H = boxHeight, kappa = thermalDiffusion, nu=kinematicViscosity)
Ra = RayleighNumber(alpha = thermalExpansionCoef, g = gravity, dT= (tempMax - tempMin), H = boxHeight, kappa = thermalDiffusion, nu=kinematicViscosity)
```

    10000.0 dimensionless

# Define a set of characteristic values

The K* factors will be used to make the parameter values dimensionless.

```python
KLength = boxHeight
KTemp = (tempMax - tempMin)
KTime = KLength**2 / thermalDiffusion
KMass = density * KLength**3

scaling = {"[time]": KTime,
           "[length]": KLength,
           "[mass]": KMass ,
           "[temperature]": KTemp,
           "[substance]": 1. * u.mole}
```

# Scale parameter values

```python
boxHeight = nd(boxHeight, scaling)
boxLength = nd(boxLength, scaling)

# Set max & min temperautres
tempMin =    nd(tempMin, scaling)
tempMax = nd(tempMax, scaling)

thermalDiffusion = nd(thermalDiffusion, scaling)
kinematicViscosity = nd(kinematicViscosity, scaling)
gravity = nd(gravity, scaling)
density = nd(density, scaling)
Tref = nd(273.15 *u.degK, scaling)
thermalExpansionCoef = nd(thermalExpansionCoef, scaling)
```

```python
print "boxHeight:", boxHeight
print "tempMin:", tempMin
print "tempMax:", tempMax
print "thermalDiffusion:", thermalDiffusion
print "kinematicViscosity:", kinematicViscosity
print "gravity:", gravity
print "density:", density
print "thermalExpansionCoef:", thermalExpansionCoef
```

    boxHeight: 1.0
    tempMin: 0.27315
    tempMax: 1.27315
    thermalDiffusion: 1.0
    kinematicViscosity: 2.5e+25
    gravity: 1e+31
    density: 1.0
    thermalExpansionCoef: 0.025

Check Rayleigh number using scaled values:

```python
(Ra == RayleighNumber(alpha = thermalExpansionCoef, g = gravity, dT= (tempMax - tempMin), H = boxHeight, kappa = thermalDiffusion, nu=kinematicViscosity))
```

    True

Set input and output file directory

```python
inputPath  = 'BBInput/'
outputPath = 'BBOutput/'
# Make output directory if necessary.
if uw.rank()==0:
    import os
    if not os.path.exists(outputPath):
        os.makedirs(outputPath)
```

Create mesh and variables
------

```python
mesh = uw.mesh.FeMesh_Cartesian( elementType = ("Q1/dQ0"),
                                 elementRes  = (res, res),
                                 minCoord    = (0., 0.),
                                 maxCoord    = (boxLength, boxHeight))

velocityField       = uw.mesh.MeshVariable( mesh=mesh,         nodeDofCount=2 )
pressureField       = uw.mesh.MeshVariable( mesh=mesh.subMesh, nodeDofCount=1 )
temperatureField    = uw.mesh.MeshVariable( mesh=mesh,         nodeDofCount=1 )
temperatureDotField = uw.mesh.MeshVariable( mesh=mesh,         nodeDofCount=1 )

# initialise velocity, pressure and temperatureDot field
velocityField.data[:]       = [0.,0.]
pressureField.data[:]       = 0.
temperatureField.data[:]    = 0.
temperatureDotField.data[:] = 0.
```

Set up material parameters and functions
-----

Set values and functions for viscosity, density and buoyancy force.

```python
# Set a constant viscosity.
viscosity = kinematicViscosity

# Create our density function.
densityFn = density * (1.0 - thermalExpansionCoef * (temperatureField - Tref))

# Define our vertical unit vector using a python tuple (this will be automatically converted to a function).
z_hat = ( 0.0, -1.0 )

# A buoyancy function.
buoyancyFn = densityFn * z_hat * gravity
```

Set initial temperature field
-----
The initial temperature field can be loaded from a pre-run steady state data set ( ``LoadFromFile = True`` ) or set to a sinusodial perterbation ( ``LoadFromFile = False`` ).

```python
# Steady state temperature field to be loaded from data file.
LoadFromFile = False
```

**If loading steady state data set**

Data is stored in h5 format from a 64\*64 grid resolution model.  Data has been saved for 3 different Rayleigh numbers, $Ra = 10^4$, $10^5$ or $10^6$.

Once loaded the data will need to be re-meshed onto a new grid, unless the new resolution is also 64\*64.

For more information on using meshes see the user guide.

```python
if(LoadFromFile == True):
    # Setup mesh and temperature field for 64*64 data file.
    mesh64 = uw.mesh.FeMesh_Cartesian( elementType = ("Q1/dQ0"),
                                       elementRes  = (64, 64),
                                       minCoord    = (0., 0.),
                                       maxCoord    = (boxLength, boxHeight),
                                       partitioned = False )
    temperatureField64  = uw.mesh.MeshVariable( mesh=mesh64, nodeDofCount=1 )

    # read in saved steady state temperature field data
    if( case == "a" ):
        temperatureField64.load(inputPath+'tempfield_inp_64_Ra1e4.h5')
        print('Loading 64*64 for Ra = 1e4')
    elif( case == "b" ):
        temperatureField64.load(inputPath+'tempfield_inp_64_Ra1e5.h5')
        print('Loading 64*64 for Ra = 1e5')
    else:
        temperatureField64.load(inputPath+'tempfield_inp_64_Ra1e6.h5')
        print('Loading 64*64 for Ra = 1e6')

    if( res==64 ): # no remeshing needed, copy directly
        temperatureField.data[:] = temperatureField64.data[:]
    else: # remeshing needed
        temperatureField.data[:] = temperatureField64.evaluate(mesh)
```

**If using sinusodial perturbation**

```python
if(LoadFromFile == False):
    temperatureField.data[:] = 0.
    pertStrength = nd(100 * u.degK, scaling) # 100 degK perturbation
    deltaTemp = tempMax - tempMin
    for index, coord in enumerate(mesh.data):
        pertCoeff = math.cos( math.pi * coord[0]/boxLength ) * math.sin( math.pi * coord[1]/boxLength )
        temperatureField.data[index] = tempMin + deltaTemp*(boxHeight - coord[1]) + pertStrength * pertCoeff
        temperatureField.data[index] = max(tempMin, min(tempMax, temperatureField.data[index]))
```

**Show initial temperature field**

```python
fig = glucifer.Figure()
fig.append( glucifer.objects.Surface(mesh, temperatureField) )
fig.show()
```

<style>
.lvctrl input { display: inline; }
.lvctrl input[type=range] { width: 200px; }
.lvctrl input[type=button] { width: 50px; }
.lvctrl input[type=number] { width: 120px; height: 28px; border-radius: 0; border: 1px dotted #999;}
.lvctrl pre { display: inline; }
.lvctrl p  { color: gray; margin: 0px}

/* Colour select */
.colourbg {display: inline-block; margin:5px 0px; width: 100px; height: 20px; border: 1px dotted #999; }
.colour {display: inline-block; width: 100%; height: 100%; margin: 0px; padding: 0px; border: none;}

.checkerboard {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAIElEQVQ4jWP4TwAcOHAAL2YYNWBYGEBIASEwasCwMAAALvidroqDalkAAAAASUVORK5CYII=");}

.palette {margin: 3px; border:1px solid #000; border: 1px dotted #000;}

</style>

Create boundary conditions
----------

Set temperature boundary conditions on the bottom ( ``MinJ`` ) and top ( ``MaxJ`` ).

```python
for index in mesh.specialSets["MinJ_VertexSet"]:
    temperatureField.data[index] = tempMax
for index in mesh.specialSets["MaxJ_VertexSet"]:
    temperatureField.data[index] = tempMin
```

Construct sets for the both horizontal and vertical walls. Combine the sets of vertices to make the ``I`` (left and right side walls) and ``J`` (top and bottom walls) sets.

```python
iWalls = mesh.specialSets["MinI_VertexSet"] + mesh.specialSets["MaxI_VertexSet"]
jWalls = mesh.specialSets["MinJ_VertexSet"] + mesh.specialSets["MaxJ_VertexSet"]

freeslipBC = uw.conditions.DirichletCondition( variable        = velocityField,
                                               indexSetsPerDof = (iWalls, jWalls) )
tempBC     = uw.conditions.DirichletCondition( variable        = temperatureField,
                                               indexSetsPerDof = (jWalls,) )
```

System setup
-----

**Setup a Stokes system**

```python
stokes = uw.systems.Stokes(    velocityField = velocityField,
                               pressureField = pressureField,
                               conditions    = [freeslipBC,],
                               fn_viscosity  = viscosity,
                               fn_bodyforce  = buoyancyFn )
# get the default stokes equation solver
solver = uw.systems.Solver( stokes )
```

**Create an advection diffusion system**

```python
advDiff = uw.systems.AdvectionDiffusion( phiField       = temperatureField,
                                         phiDotField    = temperatureDotField,
                                         velocityField  = velocityField,
                                         fn_diffusivity = thermalDiffusion,
                                         conditions     = [tempBC,] )
```

Analysis tools
-----

**Nusselt number**

The Nusselt number is the ratio between convective and conductive heat transfer

\\[
Nu = -h \frac{ \int_0^l \partial_z T (x, z=h) dx}{ \int_0^l T (x, z=0) dx}
\\]

```python
nuTop    = uw.utils.Integral( fn=temperatureField.fn_gradient[1],
                              mesh=mesh, integrationType='Surface',
                              surfaceIndexSet=mesh.specialSets["MaxJ_VertexSet"])

nuBottom = uw.utils.Integral( fn=temperatureField - tempMin,
                              mesh=mesh, integrationType='Surface',
                              surfaceIndexSet=mesh.specialSets["MinJ_VertexSet"])
```

```python
nu = - boxHeight * nuTop.evaluate()[0]/nuBottom.evaluate()[0]
print('Nusselt number = {0:.6f}'.format(nu))
```

    Nusselt number = 1.000000

**RMS velocity**

The root mean squared velocity is defined by intergrating over the entire simulation domain via

\\[
\begin{aligned}
v_{rms}  =  \sqrt{ \frac{ \int_V (\mathbf{v}.\mathbf{v}) dV } {\int_V dV} }
\end{aligned}
\\]

where $V$ denotes the volume of the box.

```python
intVdotV = uw.utils.Integral( fn.math.dot( velocityField, velocityField ), mesh )

vrms = boxHeight / thermalDiffusion * math.sqrt( boxHeight / (boxHeight * boxLength) * intVdotV.evaluate()[0] )
print('Initial vrms = {0:.3f}'.format(vrms))
```

    Initial vrms = 0.000

Main simulation loop
-----

If the initial conditions are loaded from file then this loop will only take a single step. If you would like to run the entire simulation from a small perturbation then change the ``LoadFromFile`` variable above to equal ``False``. Warning: the simulation will take a long time to get to steady state.

```python
#initialise time, step, output arrays
time = 0.
step = 0
timeVal = []
vrmsVal = []

# starting from steady state == True
if(LoadFromFile == True):
    step_end = 1
else:
    step_end = 5000

# output frequency
step_output = max(1,min(100, step_end/10))
epsilon    = 1.e-8

velplotmax = 0.0
nuLast     = -1.0
```

```python
# define an update function
def update():
    # Determining the maximum timestep for advancing the a-d system.
    dt = advDiff.get_max_dt()
    # Advect using this timestep size.
    advDiff.integrate(dt)
    return time+dt, step+1
```

```python
# Perform steps.
while step<=step_end:
    # Solving the Stokes system.
    solver.solve()

    # Calculate & store the RMS velocity and Nusselt number.
    vrms = math.sqrt( intVdotV.evaluate()[0] )
    nu   = - nuTop.evaluate()[0]/nuBottom.evaluate()[0]
    vrmsVal.append(vrms)
    timeVal.append(time)
    velplotmax = max(vrms, velplotmax)

    # print output statistics
    if step%(step_end/step_output) == 0:
        if(uw.rank()==0):
            print('steps = {0:6d}; time = {1:.3e}; v_rms = {2:.3f}; Nu = {3:.3f}; Rel change = {4:.3e}'
              .format(step, time, vrms, nu, abs((nu - nuLast)/nu)))
    # Check loop break conditions.
    if(abs((nu - nuLast)/nu) < epsilon):
        if(uw.rank()==0):
            print('steps = {0:6d}; time = {1:.3e}; v_rms = {2:.3f}; Nu = {3:.3f}; Rel change = {4:.3e}'
              .format(step, time, vrms, nu, abs((nu - nuLast)/nu)))
        break
    nuLast = nu

    # update
    time, step = update()
```

    steps =      0; time = 0.000e+00; v_rms = 17.899; Nu = 1.000; Rel change = 2.000e+00
    steps =     50; time = 5.881e-03; v_rms = 59.163; Nu = 1.642; Rel change = 1.707e-02
    steps =    100; time = 9.550e-03; v_rms = 84.441; Nu = 3.874; Rel change = 1.491e-02
    steps =    150; time = 1.276e-02; v_rms = 75.248; Nu = 6.527; Rel change = 5.836e-03
    steps =    200; time = 1.661e-02; v_rms = 50.758; Nu = 7.178; Rel change = 1.391e-03
    steps =    250; time = 2.217e-02; v_rms = 31.025; Nu = 5.943; Rel change = 5.075e-03
    steps =    300; time = 2.828e-02; v_rms = 24.925; Nu = 4.675; Rel change = 4.297e-03
    steps =    350; time = 3.438e-02; v_rms = 26.421; Nu = 3.932; Rel change = 2.567e-03
    steps =    400; time = 4.048e-02; v_rms = 33.302; Nu = 3.673; Rel change = 6.008e-05
    steps =    450; time = 4.651e-02; v_rms = 43.571; Nu = 3.952; Rel change = 2.701e-03
    steps =    500; time = 5.174e-02; v_rms = 50.362; Nu = 4.608; Rel change = 3.022e-03
    steps =    550; time = 5.658e-02; v_rms = 50.648; Nu = 5.194; Rel change = 1.632e-03
    steps =    600; time = 6.157e-02; v_rms = 46.424; Nu = 5.393; Rel change = 5.932e-05
    steps =    650; time = 6.703e-02; v_rms = 41.629; Nu = 5.208; Rel change = 1.167e-03
    steps =    700; time = 7.294e-02; v_rms = 39.186; Nu = 4.880; Rel change = 1.264e-03
    steps =    750; time = 7.901e-02; v_rms = 39.654; Nu = 4.654; Rel change = 5.706e-04
    steps =    800; time = 8.492e-02; v_rms = 41.773; Nu = 4.620; Rel change = 2.245e-04
    steps =    850; time = 9.057e-02; v_rms = 43.792; Nu = 4.729; Rel change = 6.120e-04
    steps =    900; time = 9.604e-02; v_rms = 44.604; Nu = 4.872; Rel change = 5.224e-04
    steps =    950; time = 1.015e-01; v_rms = 44.158; Nu = 4.960; Rel change = 1.762e-04
    steps =   1000; time = 1.070e-01; v_rms = 43.131; Nu = 4.959; Rel change = 1.501e-04
    steps =   1050; time = 1.126e-01; v_rms = 42.302; Nu = 4.901; Rel change = 2.831e-04
    steps =   1100; time = 1.183e-01; v_rms = 42.071; Nu = 4.838; Rel change = 2.105e-04
    steps =   1150; time = 1.240e-01; v_rms = 42.365; Nu = 4.807; Rel change = 4.218e-05
    steps =   1200; time = 1.297e-01; v_rms = 42.839; Nu = 4.815; Rel change = 9.284e-05
    steps =   1250; time = 1.353e-01; v_rms = 43.166; Nu = 4.844; Rel change = 1.293e-04
    steps =   1300; time = 1.409e-01; v_rms = 43.210; Nu = 4.871; Rel change = 8.062e-05
    steps =   1350; time = 1.465e-01; v_rms = 43.043; Nu = 4.881; Rel change = 3.237e-06
    steps =   1400; time = 1.521e-01; v_rms = 42.830; Nu = 4.874; Rel change = 4.976e-05
    steps =   1450; time = 1.577e-01; v_rms = 42.708; Nu = 4.860; Rel change = 5.674e-05
    steps =   1500; time = 1.634e-01; v_rms = 42.714; Nu = 4.849; Rel change = 2.926e-05
    steps =   1550; time = 1.690e-01; v_rms = 42.801; Nu = 4.847; Rel change = 5.451e-06
    steps =   1600; time = 1.746e-01; v_rms = 42.893; Nu = 4.851; Rel change = 2.529e-05
    steps =   1650; time = 1.802e-01; v_rms = 42.936; Nu = 4.857; Rel change = 2.412e-05
    steps =   1700; time = 1.859e-01; v_rms = 42.923; Nu = 4.862; Rel change = 9.748e-06
    steps =   1750; time = 1.915e-01; v_rms = 42.879; Nu = 4.862; Rel change = 5.245e-06
    steps =   1800; time = 1.971e-01; v_rms = 42.841; Nu = 4.860; Rel change = 1.219e-05
    steps =   1850; time = 2.027e-01; v_rms = 42.827; Nu = 4.857; Rel change = 9.902e-06
    steps =   1900; time = 2.083e-01; v_rms = 42.838; Nu = 4.855; Rel change = 2.796e-06
    steps =   1950; time = 2.140e-01; v_rms = 42.858; Nu = 4.855; Rel change = 3.478e-06
    steps =   2000; time = 2.196e-01; v_rms = 42.874; Nu = 4.857; Rel change = 5.659e-06
    steps =   2050; time = 2.252e-01; v_rms = 42.877; Nu = 4.858; Rel change = 3.906e-06
    steps =   2100; time = 2.308e-01; v_rms = 42.871; Nu = 4.858; Rel change = 5.442e-07
    steps =   2150; time = 2.365e-01; v_rms = 42.862; Nu = 4.858; Rel change = 1.984e-06
    steps =   2200; time = 2.421e-01; v_rms = 42.856; Nu = 4.858; Rel change = 2.535e-06
    steps =   2250; time = 2.477e-01; v_rms = 42.855; Nu = 4.857; Rel change = 1.468e-06
    steps =   2300; time = 2.533e-01; v_rms = 42.859; Nu = 4.857; Rel change = 6.574e-08
    steps =   2350; time = 2.589e-01; v_rms = 42.863; Nu = 4.857; Rel change = 1.042e-06
    steps =   2400; time = 2.646e-01; v_rms = 42.865; Nu = 4.857; Rel change = 1.099e-06
    steps =   2450; time = 2.702e-01; v_rms = 42.865; Nu = 4.858; Rel change = 5.162e-07
    steps =   2486; time = 2.742e-01; v_rms = 42.863; Nu = 4.858; Rel change = 9.493e-09

```python
fig.show()
```

Post analysis
-----

**Benchmark values**

The time loop above outputs $v_{rms}$ and $Nu$ as general statistics for the system. For comparison, the benchmark values for the RMS velocity and Nusselt number are shown below for different Rayleigh numbers. All benchmark values shown below were determined in Blankenbach *et al.* 1989 by extroplation of numerical results.

| $Ra$          | $v_{rms}$   | $Nu$  |   $q_1$  | $q_2$  |
| ------------- |:-------------:|:-----:|:-----:|:-----:|
| 10$^4$ | 42.865 |  4.884 | 8.059 | 0.589 |
| 10$^5$ | 193.215 | 10.535 | 19.079 | 0.723 |
| 10$^6$ | 833.990 | 21.972 | 45.964 | 0.877 |

For practicality, quick tests can be run to see that the results are close enough. Tests run using underworld gave

| $Ra$          | $v_{rms}$   | $Nu$  |
| ------------- |:-------------:|:-----:|
| 10$^4$ | 42.858 |  4.848 |
| 10$^5$ | 193.449 | 10.248 |
| 10$^6$ | 844.218 | 19.660 |

**Resulting pressure field**

Use the same method as above to plot the new temperature field. This can also be used to plot the pressure field, or any other data structures of interest.

```python
figtemp = glucifer.Figure()
figtemp.append( glucifer.objects.Surface( mesh, pressureField ) )
figtemp.show()
```

**Plot the velocity vector field**

For this example the velocity field is interesting to see. This is visualised in two ways, firstly plotting a surface colour map of the velocity magnitude, and secondly the velocity vectors at points on the mesh. For aesthetics the vector arrows are scaled by a little more than the maximum $v_{rms}$ value found in the time loop above.

```python
fig2 = glucifer.Figure()
velmagfield = uw.function.math.sqrt( uw.function.math.dot( velocityField, velocityField ) )
fig2.append( glucifer.objects.VectorArrows(mesh, velocityField/(2.5*velplotmax), arrowHead=0.2, scaling=0.1) )
fig2.append( glucifer.objects.Surface(mesh, temperatureField) )
fig2.show()
```

Parallel friendly post analysis
----

When running underworld in parallel the data of each mesh variable is spread across all the processors. However often we will want to calculate a quantity based on data at specific points that may not all be on the same processor.

A solution is presented here which consists of saving the data from all processors to file, then reloading the mesh variable data using a new non-partitioned mesh. This enables all the data to be available to each processor. We will the carry out the post analysis using the first processor.

**Save temperature, pressure and velocity data**

Save the basic mesh variable data to files using the HDF5 format. This is the same file type as is loaded above.

```python
mesh.save(outputPath+"mesh.h5")
temperatureField.save(outputPath+'tempfield.h5')
pressureField.save(outputPath+'presfield.h5')
velocityField.save(outputPath+'velfield.h5')
```

**Construct new mesh and variable on non-partitioned mesh**

Read saved mesh variable data into a new mesh variable where the information is not partitioned across multiple processors. This means that we can use a single processor to access all the data and calculate some quantities of interest.

```python
# build a non-partitioned mesh with same box size
mesh0 = uw.mesh.FeMesh_Cartesian( elementType = ("Q1/dQ0"),
                                  elementRes  = (res, res),
                                  minCoord    = (0., 0.),
                                  maxCoord    = (boxLength, boxHeight),
                                  partitioned = False )
# load previous mesh coordinate data onto new non-partitioned mesh
mesh0.load(outputPath+'mesh.h5')

# load T, P and V data onto the new mesh
# note that pressure is always on the submesh
temperatureField0  = uw.mesh.MeshVariable( mesh=mesh0, nodeDofCount=1 )
pressureField0     = uw.mesh.MeshVariable( mesh=mesh0.subMesh, nodeDofCount=1 )
velocityField0     = uw.mesh.MeshVariable( mesh=mesh0, nodeDofCount=2 )
temperatureField0.load(outputPath+"tempfield.h5")
pressureField0.load(outputPath+"presfield.h5")
velocityField0.load(outputPath+"velfield.h5")
```

**Temperature gradient**

The final benchmarks in the Blankenbach paper involve the temperature gradient in the vertical direction ($\frac{\partial T}{\partial z}$). This is easy to find using the underworld functions, as shown below.

```python
if(uw.rank()==0):
    tempgradField = temperatureField0.fn_gradient
    vertTGradField = - boxHeight * tempgradField[1] / tempMax # scaled for direct benchmarking below
```

**More benchmark values**

The vertical temperature gradient (above) is set up to be non-dimensional as per Blankenbach et al 1989. To compare to the benchmark values in their work the gradient is compared at the corners of the simulation box: $q_1$ at $x = 0$, $z = h$; $q_2$ at $x = l$, $z = h$; $q_3$ at $x = l$, $z = 0$; $q_4$ at $x = 0$, $z = 0$. Where $h$ = Box_Height and $l$ = Box_Length and the non-dimensional gradient field is given by
\\[
q = \frac{-h}{\Delta T} \left( \frac{\partial T}{\partial z} \right)
\\]

Provided the simulation is run to steady-state with sufficent resolution then the $q$ values should be close to the benchmark values given again below for different Rayleigh numbers.

| $Ra$          |    $q_1$  | $q_2$  |
| ------------- |:-----:|:-----:|
| 10$^4$ | 8.059 | 0.589 |
| 10$^5$ | 19.079 | 0.723 |
| 10$^6$ | 45.964 | 0.877 |

The underworld values for $128\times128$ close to convergence are

| $Ra$          |    $q_1$  | $q_2$  |
| ------------- |:-----:|:-----:|
| 10$^4$ | 8.020 | 0.589 |
| 10$^5$ | 18.409 | 0.719 |
| 10$^6$ | 36.769 | 0.831 |

```python
if(uw.rank()==0):
    q1 = vertTGradField.evaluate( (0., boxHeight))[0][0]
    q2 = vertTGradField.evaluate( (boxLength, boxHeight))[0][0]
    q3 = vertTGradField.evaluate( (boxLength, 0.))[0][0]
    q4 = vertTGradField.evaluate( (0., 0.))[0][0]

    print('Rayleigh number = {0:.1e}'.format(Ra))
    print('q1 = {0:.3f}; q2 = {1:.3f}'.format(q1, q2))
    print('q3 = {0:.3f}; q4 = {1:.3f}'.format(q3, q4))
```

**Save time and rms values**

The following command uses the ``numpy`` package save to text file function to output all $v_{RMS}$ values as a function of time. This is particularly useful if you have run the simulation from the perturbed initial condition rather than the saved data file, as you can see the system coming to steady state.

The format for this text file is:

    timeVal[0], vrmsVal[0]
    timeVal[1], vrmsVal[1]
    ...
    timeVal[N], vrmsVal[N]

```python
if(uw.rank()==0):
    np.savetxt(outputPath+'vrms.txt', np.c_[timeVal, vrmsVal], header="Time, VRMS" )
```

**Calculate stress values for benchmark comparison**

Determine stress field for whole box in dimensionless units (King 2009)
\begin{equation}
\tau_{ij} = \eta \frac{1}{2} \left[ \frac{\partial v_j}{\partial x_i} + \frac{\partial v_i}{\partial x_j}\right]
\end{equation}
which for vertical normal stress becomes
\begin{equation}
\tau_{zz} = \eta \frac{1}{2} \left[ \frac{\partial v_z}{\partial z} + \frac{\partial v_z}{\partial z}\right] = \eta \frac{\partial v_z}{\partial z}
\end{equation}
which is implemented for the whole box in the functions defined below.

```python
# get topography from non-partitioned stress tensor
if(uw.rank()==0):
    stresstensorFn = 2.* stokes.fn_viscosity*fn.tensor.symmetric( velocityField0.fn_gradient ) - (1.,1.,0.)*pressureField0
    verticalStressFn  = stresstensorFn[1]
    stress_zz_top     = -verticalStressFn.evaluate(mesh0.specialSets["MaxJ_VertexSet"])

# subtract the average value for benchmark.
    mean_sigma_zz_top = np.mean(stress_zz_top)
    sigma_zz_top      = stress_zz_top - mean_sigma_zz_top
```

Dimensionalise the stress from the vertical normal stress at the top of the box (King 2009)

$$
    \sigma_{t} = \frac{\eta_0 \kappa}{\rho g h^2}\tau _{zz} \left( x, z=h\right)
$$

where all constants have been defined above. Finally calculate the topography, defined using $h = \sigma_{top} / (\rho g)$.

```python
# Set parameters in SI units
if(uw.rank()==0):
    grav = 10        # m.s^-2
    height = 1.e6    # m
    rho  = 4.0e3     # g.m^-3
    kappa  = 1.0e-6  # m^2.s^-1

# dimensionalise
    dim_sigma_zz_top  = (eta0 * kappa / (height*height)) * sigma_zz_top

# find topography in [m]
    topography        = dim_sigma_zz_top / (rho * grav)
```

**Calculate x-coordinate at zero stress**

Calculate the zero point for the stress along the x-axis at the top of the box using the **interpolation function** from ``numpy``. Note that ``numpy`` requires that the first array input for ``np.interp`` must be increasing, so the negative of the topography is used.

```python
if(uw.rank()==0):
    xCoordFn = fn.input()[0]
    x = xCoordFn.evaluate(mesh0.specialSets["MinJ_VertexSet"])
    xIntercept = np.interp(0.0,-1.0*topography[:, 0],x[:, 0])
```

**Topography comparison**

Topography of the top boundary calculated in the left and right corners as given in Table 9 of Blankenbach et al 1989.

| $Ra$          |    $\xi_1$  | $\xi_2$  |  $x$ ($\xi = 0$) |
| ------------- |:-----------:|:--------:|:--------------:|
| 10$^4$  | 2254.02   | -2903.23  | 0.539372          |
| 10$^5$  | 1460.99   | -2004.20  | 0.529330          |
| 10$^6$  | 931.96   | -1283.80  | 0.506490          |

Underworld values for an "almost" converged $128\times128$ benchmark run

| $Ra$          |    $\xi_1$  | $\xi_2$  |  $x$ ($\xi = 0$) |
| ------------- |:-----------:|:--------:|:--------------:|
| 10$^4$  | 2243.93   | -2909.12  | 0.540373          |
| 10$^5$  | 1451.52   | -2005.83  | 0.530246          |
| 10$^6$  | 911.30    | -1282.47  | 0.509696          |

```python
if(uw.rank()==0):
    e1 = float(topography[0])
    e2 = float(topography[len(topography)-1])
    print('Rayleigh number = {0:.1e}'.format(Ra))
    print('Topography[x=0],[x=max] = {0:.2f}, {1:.2f}'.format(e1, e2))
    print('x(topo=0) = {0:.6f}'.format(xIntercept))
    # output a summary file with benchmark values (useful for parallel runs)
    np.savetxt(outputPath+'summary.txt', [Ra, e1, e2, xIntercept, q1, q2, q3, q4])
```