Enable Equation Numbering in $\LaTeX$

```javascript
%%javascript
    MathJax.Hub.Config({
      TeX: { equationNumbers: { autoNumber: "AMS" } }
    });
```

    <IPython.core.display.Javascript object>

## Math Fundamentals

First we define this definition
\begin{equation}
(\vec{w} \cdot \vec{u} + b) \geq 0 \implies +
\end{equation}

We add $y_i$ for mathematical convenience

\begin{equation}
y_i ( \vec{x}_i \cdot \vec{w} + b) -1 = 0
\end{equation}

Calculate the width of the street

\begin{equation}
width = (\vec{x}_+ - \vec{x}_-) \cdot \frac{\vec{w}}{||\vec{w}||}
\end{equation}

$$ width = \frac{\vec{x}_+ \cdot \vec{w} - \vec{x}_- \cdot \vec{w}}{||\vec{w}||}$$

$$ \vec{x}_i \cdot \vec{w} = \frac{1}{y_i} - b  $$

$$ \vec{x}_+ \cdot \vec{w} = 1 - b , \vec{x}_- \cdot \vec{w} = -1 - b $$

$$ width = \frac{1 - b - (-1 - b)}{||\vec{w}||}$$

$$ width = \frac{1 - b + 1 + b}{||\vec{w}||}$$

And we end up with

\begin{equation}
width = \frac{2}{||\vec{w}||}
\end{equation}

Now we have to maximaze that width so:

$$ Max(width) \rightarrow Max \big( \frac{2}{||\vec{w}||} \big) \rightarrow Max \big( \frac{1}{||\vec{w}||} \big) $$

$$ Max \big( \frac{1}{||\vec{w}||} \big) \rightarrow Min (||\vec{w}||) \rightarrow Min \big( \frac{1}{2} ||\vec{w}|| \big) \rightarrow Min \big( \frac{1}{2} {||\vec{w}||}^2 \big)$$

Applying Lagrange Multipliers

\begin{equation}
L = \frac{1}{2} {||\vec{w}||}^2 - \sum{ \alpha_i [y_i ( \vec{x}_i \cdot \vec{w} + b) -1]}
\end{equation}

$$
\frac{\partial L}{\partial \vec{w}} = \vec{w} - \sum{\alpha_i y_i x_i} = 0 \implies \vec{w} = \sum{\alpha_i y_i x_i}
$$

$$
\frac{\partial L}{\partial b} = - \sum{\alpha_i y_i} = 0 \implies \sum{\alpha_i y_i} = 0
$$

\begin{equation}
L = \frac{1}{2} \big( \sum{\alpha_i y_i \vec{x}_i}\big) \big(\sum{\alpha_i y_i \vec{x}_i}\big) - \sum{ \alpha_i [y_i ( \vec{x}_i \cdot \sum{\alpha_i y_i \vec{x}_i} + b) -1]}
\end{equation}

\begin{equation}
L = \frac{1}{2} \big( \sum{\alpha_i y_i \vec{x}_i}\big) \big(\sum{\alpha_i y_i \vec{x}_i}\big) - \big( \sum{ \alpha_i y_i \vec{x}_i} \big) \cdot \big(\sum{ \alpha_i y_i \vec{x}_i} \big) - \sum{ \alpha_i y_i b} + \sum{ \alpha_i}
\end{equation}

\begin{equation}
L = \sum{ \alpha_i} - \frac{1}{2} \sum_i \sum_j \alpha_i \alpha_j y_i y_j \vec{x}_i \cdot \vec{x}_j
\end{equation}

\begin{equation}
\sum{\alpha_i y_i \vec{x}_i} \cdot \vec{u} + b \geq 0 \implies +
\end{equation}

So we conclude that the minimization is basically the dot product

## Initialization

```python
import pandas as pd
import matplotlib.pyplot as plt
import bqplot.pyplot as bplt
import numpy as np
```

```python
class Support_Vector_Machine():

    def __init__(self, visualization=True):
        self.visualization = visualization
        self.colors = {1: 'r', -1: 'b'}
        if self.visualization:
            self.fig = plt.figure()
            self.ax = self.fig.add_subplot(1, 1, 1)

    def fit(self, data):
        self.data = data

        # { ||w|| : [w, b]}
        opt_dict = {}
        transforms = [
            [1, 1],
            [-1, 1],
            [-1, -1],
            [1, -1],
        ]

        all_data = np.array(list(self.data.values())).flatten()

        self.max_feature_value = max(all_data)
        self.min_feature_value = min(all_data)
        all_data = None

        step_sizes = [self.max_feature_value * 0.1,
                      self.max_feature_value * 0.01,
                      self.max_feature_value * 0.001,
                      ]

        b_range_multiple = 5

        b_multiple = 5

        latest_optimum = self.max_feature_value * 10

        limit = self.max_feature_value * b_range_multiple

        for step in step_sizes:
            w = np.array([latest_optimum, latest_optimum])
            while True:
                for b in np.arange(-limit, limit, step * b_multiple):
                    for transformation in transforms:
                        w_t = w * transformation
                        found_option = True
                        # weakest link
                        # y_i(x_i.w +b) >= 1
                        for i in self.data:
                            for xi in self.data[i]:
                                yi = i
                                if not yi * (np.dot(w_t, xi) + b) >= 1:
                                    found_option = False
                                    break
                            else:
                                continue
                            break

                        if found_option:
                            opt_dict[np.linalg.norm(w_t)] = [w_t, b]

                if w[0] < 0:
                    break
                w = w - step

            norms = sorted(n for n in opt_dict)

            self.w, self.b = opt_dict[norms[0]]

            latest_optimum = self.w[0] - step * 2

        for i in self.data:
            for xi in self.data[i]:
                print(f'{xi} : {yi * (np.dot(self.w, xi) + self.b)}')

        print(f'b: {self.b} | w: {self.w}')
        print(f'||w|| : {np.linalg.norm(self.w)}')
        if np.dot(self.w, np.array([1,0])) == 0:
            print("angle:",90)
        else:
            print(f'angle: {np.degrees(np.arctan(self.w[1]/self.w[0]))}')

    def predict(self, features):
        classification = np.sign(np.dot(np.array(features), self.w) + self.b)
        if classification != 0 and self.visualization:
            self.ax.scatter(features[0], features[1], marker='*',
                            s=200, c=self.colors[classification])
        return classification

    def visualize(self):
        [[self.ax.scatter(x[0], x[1], s=100, color=self.colors[i])
          for x in data_dict[i]]
         for i in data_dict]

        xs, ys = zip(*[j for i in data_dict.values() for j in i])

        x_max, y_max = max(xs), max(ys)
        x_min, y_min = min(xs), min(ys)

        x_margin = x_max*0.2
        y_margin = y_max*0.2

        xlist = np.linspace(x_min-x_margin, x_max+x_margin, 100)
        ylist = np.linspace(y_min-y_margin, y_max+y_margin, 100)

        X,Y = np.meshgrid(xlist, ylist)

        F = self.w[0]*X + self.w[1]*Y + self.b  #  Hyperplane Equation
        plt.contour(X, Y, F, [1], colors = 'r', linestyles = 'solid')
        plt.contour(X, Y, F, [-1], colors = 'b', linestyles = 'solid')
        plt.contour(X, Y, F, [0], colors = 'k', linestyles = 'dashed')

        plt.show()
```

```python
all(np.arange(-100, 100, 1) + 5 > 50)
```

    False

```python
data_dict = {
    -1:[
        [1,7],
        [2,8],
        [3,8],
    ],
    1:[
        [5, 1],
        [6, -1],
        [7, 3],
    ]
}
```

```python
%timeit np.array([j for i in [[key] * len(values) for key, values in data_dict.items()] for j in i])
```

    4.65 µs ± 156 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

```python
%timeit np.array([[key] * len(values) for key, values in data_dict.items()]).flatten()
```

    6.57 µs ± 70.8 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

```python
%%timeit
yi = []
for key, lenght in zip(data_dict.keys(), map(len, data_dict.values())):
    yi.append([key] * lenght)
np.array(yi).flatten()
```

    7.36 µs ± 176 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

```python
yi = np.array([j for i in [[key] * len(values) for key, values in data_dict.items()] for j in i])

yi = np.array([[key] * len(values) for key, values in data_dict.items()]).flatten()

yi
```

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

```python
%timeit np.array([j for i in [values for key, values in data_dict.items()] for j in i])
```

    5.81 µs ± 222 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

```python
%timeit np.array([j for i in [values for values in data_dict.values()] for j in i])
```

    5.35 µs ± 42.4 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

```python
%timeit [i for values in data_dict.values() for i in values]
```

    875 ns ± 35 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

```python
xi = np.array([j for i in [values for key, values in data_dict.items()] for j in i])

xi = [i for values in data_dict.values() for i in values]

xi
```

    [[0, 0], [1, 0], [0.8660254, 0.5], [2, 2]]

```python
np.array([k for i in list(data_dict.values()) for j in i for k in j])
```

    array([ 0.       ,  0.       ,  1.       ,  1.       ,  0.8660254,  0.5      ])

```python
[3] * 3
```

    [3, 3, 3]

```python
[j for i in [values for key, values in data_dict.items()] for j in i]
```

    [array([0, 0]), array([1, 1]), array([ 0.8660254,  0.5      ])]

```python
a = np.array([1,1])
b = np.array([
    [1,1],
    [0,1],
    [0,0],
    [1,1],
    [1,1],
])
np.dot(b, a) > 1
```

    array([ True, False, False,  True,  True], dtype=bool)

```python
svm = Support_Vector_Machine()
svm.fit(data_dict)

predict_us = [
    [0, 4],
    [1, 3],
    [3, 4],
    [3, 5],
    [5, 5],
    [5, 6],
    [6, -2],
    [5, 8],
]

for p in predict_us:
    svm.predict(p)
svm.visualize()
```

```python
svm = Support_Vector_Machine()
svm.fit(data_dict)
svm.visualize()
```

```python
svm = Support_Vector_Machine()
svm.fit(data_dict)
svm.visualize()
```

```python
v = np.array([1, 0])
angle = 60
angle_r = np.radians(angle)
m = np.array([
    [np.cos(angle_r), -np.sin(angle_r)],
    [np.sin(angle_r), np.cos(angle_r)]
])

nv = np.matmul(m, v.T)

nv[np.abs(nv) < 1e-3] = 0

nv
```

    array([ 0.5      ,  0.8660254])

```python
v = np.array([1, 1])
m = np.array([0.8660254,  0.5])
d = m * v
d, np.degrees(np.arctan(d[1]/d[0]))
```

    (array([ 0.8660254,  0.5      ]), 30.000000108416181)

```python
import numpy as np

def func(X):
    w = X[0]
    b = X[1]
    L = X[2] # this is the multiplier. lambda is a reserved keyword in python
    return 1/np.linalg.norm(w) + L * ()
```

```python
def dfunc(X):
    dLambda = np.zeros(len(X))
    h = 1e-3 # this is the step size used in the finite difference.
    for i in range(len(X)):
        dX = np.zeros(len(X))
        dX[i] = h
        dLambda[i] = (func(X+dX)-func(X-dX))/(2*h);
    return dLambda
```

```python
from scipy.optimize import fsolve

X1 = fsolve(dfunc, [1, 1, 0])
print(X1, func(X1))

# this is the min
X2 = fsolve(dfunc, [-1, -1, 0])
print(X2, func(X2))
```

    [ 0.70710678  0.70710678 -0.70710678] 1.41421356237
    [-0.70710678 -0.70710678  0.70710678] -1.41421356237


#### Example $\int x^2.e^{3x}.dx$
(from http://www.mathcentre.ac.uk/resources/uploaded/mc-ty-parts-2009-1.pdf)

$\ds \begin{equation} \label{eq3}
\begin{split}
\text{using formula} \\
\int u.\frac{d(v)}{dx} & = u.v - \int v.\frac{d(u)}{dx} dx \\
\text{we assign} \\
u & = x^2 \\
\frac{dv}{dx} & = e^{3x} \\
\text{we calculate} \\
v & = \int \frac{dv}{dx} dx = \int e^{3x} dx = \frac{1}{3}e^{3x} \\
\frac{du}{dx} & = \frac{d(x^2)}{dx} = 2x \\
\text{then apply to formula} \\
\int x^2e^{3x} & = x^2\frac{e^{3x}}{3} - \int \frac{e^{3x}}{3}2x dx \\
& = \frac{1}{3}x^2e^{3x} - \int \frac{2}{3}xe^{3x} dx \\
\\
\text{using formula again} \\
\int u.\frac{d(v)}{dx} & = u.v - \int v.\frac{d(u)}{dx} dx \\
\text{we assign} \\
u & = \frac{2}{3}x \\
\frac{dv}{dx} & = e^{3x} \\
\text{we calculate} \\
v & = \int \frac{dv}{dx} dx = \int e^{3x} dx = \frac{1}{3}e^{3x} \\
\frac{du}{dx} & = \frac{d(\frac{2}{3}x)}{dx} = \frac{2}{3} \\
\text{then apply to formula} \\
\int \frac{2}{3}xe^{3x} dx & = \frac{2}{3}x\frac{1}{3}e^{3x} - \int \Big( \frac{1}{3}e^{3x} . \frac{2}{3} \Big)   dx \\
& = \frac{2}{9}x.e^{3x} - \int \frac{2}{9}e^{3x} dx \\
& = \frac{2}{9}x.e^{3x} - \frac{2}{27}e^{3x} \\
\\
\text{replace with earlier calc} \\
& = \frac{1}{3}x^2e^{3x} - \frac{2}{9}x.e^{3x} - \frac{2}{27}e^{3x} + C \\
& = \Big( \frac{1}{3}x^2 - \frac{2}{9}x - \frac{2}{27} \Big)e^{3x} + C \\
& = \frac{9x^2 - 6x - 2}{27}e^{3x} + C \\
\text{completing the square} \\
& = \frac{(3x-1)^2 - 3}{27}e^{3x} + C \\
\\
\end{split}
\end{equation}$

----

### Integration by Parts
#### Example $\int x.cos(x).dx$
(from http://www.mathcentre.ac.uk/resources/uploaded/mc-ty-parts-2009-1.pdf)
$\newcommand{\dv}{\displaystyle \frac{d}{dx}}$
$\newcommand{\ds}{\displaystyle}$
$\ds \begin{equation} \label{eq2}
\begin{split}
\text{using formula} \\
\int u.\frac{d(v)}{dx} & = u.v - \int v.\frac{d(u)}{dx} dx \\
\text{we assign} \\
u & = x \\
\frac{dv}{dx} & = cos(x) \\
\text{we calculate} \\
v & = \int \frac{dv}{dx} dx = \int cos(x) dx = sin(x) \\
\frac{du}{dx} & = \frac{d(x)}{dx} = 1 \\
\text{then apply to formula} \\
\int x.cos(x).dx & = x.sin(x) - \int sin(x).1.dx \\
 & = x.sin(x) - cos(x) + C \\
\end{split}
\end{equation}$

----

```python

```


# Particle swarm optimization for the `ackley` function

```python
# Import dependencies.
import sympy as sp
import numpy as np
import matplotlib as mpl
import ipywidgets as widgets
import modules.oiplib as oiplib
```

```python
# Configure plotting.
sp.interactive.init_printing(use_unicode=False, wrap_line=False, no_global=True)
%matplotlib notebook
```

```python
# Define symbolic variables of the ackley function.
#x, y = sp.symbols("x y")

# Define the function.
f, x, y = oiplib.ackley();
# display(f)
```

```python
size = 5
```

```python
# Define a plot range and plot the function with sympy:
yMin, yMax, xMin, xMax = -size, size, -size, size
plot = sp.plotting.plot3d(f, (x, xMin, xMax), (y, yMin, yMax));
```

    <IPython.core.display.Javascript object>

```python
swarm = oiplib.ParticleSwarm(oiplib.ackley_function, (xMin, yMin), (xMax, yMax))
```

```python
swarm.run()
```

```python
fig2 = mpl.pyplot.figure(2)
ax = fig2.add_subplot(111, projection='3d')

def plot_func(iteration):
    xp = []
    yp = []
    zp = []
    for particle in swarm.particles:
        xp.append(particle.coordinatesX[iteration])
        yp.append(particle.coordinatesY[iteration])
        zp.append(particle.coordinatesZ[iteration])

    span = 5
    stepSize = 0.1

    xa = np.arange(-span, span+stepSize, stepSize)
    ya = np.arange(-span, span+stepSize, stepSize)
    xa, ya = np.meshgrid(xa, ya)
    za = np.zeros(xa.shape)

    it = np.nditer(za, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        i, j = it.multi_index
        za[i, j] = swarm.func(xa[i, j], ya[i, j])
        it.iternext()

    ax.cla()

    ax.set_xbound(-size, size)
    ax.set_ybound(-size, size)
    ax.set_zbound(0, size*3)
    ax.set_autoscale_on(False)
    ax.set_xlabel('X')
    ax.set_ylabel('Y')
    ax.set_zlabel('Z')

    surf = ax.plot_surface(xa, ya, za, color=(0, 0, 0, 0.2), rcount=span*2+1, ccount=span*2+1)
    scatter = ax.scatter(xp, yp, zp, c=zp, cmap="RdYlGn_r")

widgets.interact(plot_func, iteration=widgets.IntSlider(value=0, min=0, max=len(swarm.particles[0].coordinatesZ)-1, step=1), continuous_update=False);
```

    <IPython.core.display.Javascript object>

    interactive(children=(IntSlider(value=0, description='iteration', max=83), Output()), _dom_classes=('widget-in…

```python
fig3 = mpl.pyplot.figure(3, figsize=(9, 9/3))
ax1 = fig3.add_subplot(1, 3, 1, projection='3d')
ax2 = fig3.add_subplot(1, 3, 2, projection='3d')
ax3 = fig3.add_subplot(1, 3, 3, projection='3d')

def subplot_func(axes, iteration):
    xp = []
    yp = []
    zp = []
    for particle in swarm.particles:
        xp.append(particle.coordinatesX[iteration])
        yp.append(particle.coordinatesY[iteration])
        zp.append(particle.coordinatesZ[iteration])

    span = 5
    stepSize = 0.1

    xa = np.arange(-span, span+stepSize, stepSize)
    ya = np.arange(-span, span+stepSize, stepSize)
    xa, ya = np.meshgrid(xa, ya)
    za = np.zeros(xa.shape)

    it = np.nditer(za, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        i, j = it.multi_index
        za[i, j] = swarm.func(xa[i, j], ya[i, j])
        it.iternext()

    axes.cla()

    axes.set_xbound(-size, size)
    axes.set_ybound(-size, size)
    axes.set_zbound(0, size*3)
    axes.set_autoscale_on(False)
    axes.set_xlabel('X')
    axes.set_ylabel('Y')
    axes.set_zlabel('Z')

    surf = axes.plot_surface(xa, ya, za, color=(0, 0, 0, 0.2), rcount=span*2+1, ccount=span*2+1)
    scatter = axes.scatter(xp, yp, zp, c=zp, cmap="RdYlGn_r")

subplot_func(ax1, 0)
subplot_func(ax2, 10)
subplot_func(ax3, len(swarm.particles[0].coordinatesZ) - 1)

mpl.pyplot.tight_layout()
```

    <IPython.core.display.Javascript object>

```python

```


# Lake model

see also [this general introduction to the workbench](https://waterprogramming.wordpress.com/2017/11/01/using-the-exploratory-modelling-workbench/) as a source of inspiration for completing the assignment below

## the lake model
The exploratory modeling workbench includes an example folder. This folder contains a variety of examples that demonstrate the functionality of the workbench. Many of these examples have been drawn from published cases. Here, we use the Lake Problem as an example for demonstrating some of the key functionality of the workbench.

We demonstrate some of the key capabilities of the exploratory modeling workbench using the Lake problem. The lake problem is a stylized and hypothetical decision problem where the population of a city has to decide on the amount of annual pollution it will put into a lake. It the pollution in the lake passes a threshold, it will suffer irreversible eutrophication.

\begin{equation}
    X_{(t+1)}=X_t+a_t+\frac{(X_t^q)}{(1+X_t^q )}- bX_t+\epsilon_t
\end{equation}

where $X_t$ is the pollution at time $t$, $a_t$ is the rate of anthropogenic pollution at time $t$, $b$ is the lake’s natural removal rate, $q$ is the lake's natural recycling rate, $\epsilon_t$ is the rate of natural pollution at time $t$. The rate of anthropogenic pollution $a_t$ is the decision variable and is somewhere between 0, and 0.1. So $a_t \in [0,0.1]$. The natural pollution $\epsilon_t$ is modeled, following Singh et al. (2015), as a log normal distribution with mean $\mu$ and standard deviation $\sigma$.
There are four outcomes of interest. The first is the average concentration of phosphor in the lake.

\begin{equation}
    f_{phosphorus}=  \frac{1}{\left\vert{T}\right\vert} \sum\limits_{t\in{T}} X_t
\end{equation}

where $\left\vert{T}\right\vert$ is the cardinality of the set of points in time.
The second objective is the economic benefit derived from polluting the lake. Following Singh et al. (2015), this is defined as the discounted benefit of pollution mines the costs of having a polluted lake

\begin{equation}
    f_{economic} = \sum\limits_{t \in {T}}\alpha a_t \delta^t
\end{equation}

where $\alpha$ is the utility derived from polluting and $\delta$ is the discount rate. By default, $\alpha$ is 0.4.
The third objective is related to the year over year change in the anthropogenic pollution rate.

\begin{equation}
    f_{inertia} =\frac{1}{\left\vert{T}\right\vert-1} \sum\limits_{t=1}^{\left\vert{T}\right\vert} I(|a_{t}-a_{t-1} |>\tau)
\end{equation}

where $I$ is an indicator function that is 0 if the statement is false, and 1 if the statement is true, $\tau$ is the threshold that is deemed undesirable, and is for illustrative purposes et to 0.2. Effectively, f_{inertia} is the fraction of years where the absolute value of the change in anthropogenic pollution is larger then $\tau$.
The fourth objective is the fraction of years where the pollution in the lake is below the critical threshold.

\begin{equation}
    f_{reliability} =  \frac{1}{\left\vert{T}\right\vert} \sum\limits_{t \in T}I(X_{t}<X_{crit} )
\end{equation}

where $I$ is an indicator function that is 0 if the statement is false, and 1 if the statement is true, $X_{crit}$ is the critical threshold of pollution and is a function of both $b$ and $q$.

The lake problem is characterized by both stochastic uncertainty and deep uncertainty. The stochastic uncertainty arises from the natural inflow. To reduce this stochastic uncertainty, multiple replications are performed and the average over the replication is taken. Deep uncertainty is presented by uncertainty about the mean $\mu$ and standard deviation $sigma$ of the lognormal distribution characterizing the natural inflow, the natural removal rate of the lake $\beta$, the natural recycling rate of the lake $q$, and the discount rate $\delta$. The table below specifies the ranges for the deeply uncertain factors, as well as their best estimate or default values.

## Assignment
1. Given the Python implementation of the lake problem in lakemodel_function.py, adapt this code and connect it to the workbench

for the uncertainties, use the following table

|Parameter	|Range	        |Default value|
|-----------|--------------:|------------:|
|$\mu$    	|0.01 – 0.05	|0.02         |
|$\sigma$	|0.001 – 0.005 	|0.0017       |
|$b$      	|0.1 – 0.45	    |0.42         |
|$q$	    |2 – 4.5	    |2            |
|$\delta$	|0.93 – 0.99	|0.98         |

For now, assume that for each year a release decision is made. The release is between 0 and 0.1. Carefully look at line 24 in lake_model.py to identify the name to use for each lever.

```python
from lakemodel_function import lake_problem

from ema_workbench import (Model, RealParameter, ScalarOutcome)

#instantiate the model
lake_model = Model('lakeproblem', function=lake_problem)
lake_model.time_horizon = 100 # used to specify the number of timesteps

#specify uncertainties
lake_model.uncertainties = [RealParameter('mean', 0.01, 0.05),
                            RealParameter('stdev', 0.001, 0.005),
                            RealParameter('b', 0.1, 0.45),
                            RealParameter('q', 2.0, 4.5),
                            RealParameter('delta', 0.93, 0.99)]

# set levers, one for each time step
lake_model.levers = [RealParameter(f"l{i}", 0, 0.1) for i in
                     range(lake_model.time_horizon)] # we use time_horizon here

#specify outcomes
lake_model.outcomes = [ScalarOutcome('max_P'),
                       ScalarOutcome('utility'),
                       ScalarOutcome('inertia'),
                       ScalarOutcome('reliability')]
```

2. Explore the behavior of the system in the absence of any release using 1000 scenarios, and the default sampling approach.
    * visualize the outcomes of interest, are there any apparent trade-offs?
    * can you visually identify the uncertainties that drive system behavior?

# from ema_workbench import Policy, perform_experiments
from ema_workbench import ema_logging

ema_logging.log_to_stderr(ema_logging.INFO)

policy = Policy("no release", **{l.name:0 for l in lake_model.levers})
n_scenarios = 1000
results = perform_experiments(lake_model, n_scenarios, policy)

```python
experiments, outcomes = results

sns.pairplot(pd.DataFrame.from_dict(outcomes))
plt.show()
```

```python
from ema_workbench.analysis import pairs_plotting

pairs_plotting.pairs_scatter(experiments, outcomes)

fig = plt.gcf()
fig.set_size_inches(8,8)

plt.show()
```

3. Explore the behavior of the system over 1000 scenarios for 4 randomly sampled candidate strategies.
    * visualize the outcomes of interest
    * what can you say about how the release decision influences the system?

```python
n_scenarios = 1000
n_policies = 4
results = perform_experiments(lake_model, n_scenarios, n_policies)
```

    [MainProcess/INFO] performing 1000 scenarios * 4 policies * 1 model(s) = 4000 experiments
    [MainProcess/INFO] performing experiments sequentially
    [MainProcess/INFO] 400 cases completed
    [MainProcess/INFO] 800 cases completed
    [MainProcess/INFO] 1200 cases completed
    [MainProcess/INFO] 1600 cases completed
    [MainProcess/INFO] 2000 cases completed
    [MainProcess/INFO] 2400 cases completed
    [MainProcess/INFO] 2800 cases completed
    [MainProcess/INFO] 3200 cases completed
    [MainProcess/INFO] 3600 cases completed
    [MainProcess/INFO] 4000 cases completed
    [MainProcess/INFO] experiments finished

```python
experiments, outcomes = results
policies = experiments['policy']

data = pd.DataFrame.from_dict(outcomes)
data['policy'] = policies

sns.pairplot(data, hue='policy', vars=outcomes.keys(), diag_kind='hist')
plt.show()
```

4. If you have not used parallelization in the foregoing, try to adapt your code to use parallelization. The workbench comes with two evaluators for parallelization. The `MultiProcessingingEvaluator` and the `IpyparallelEvaluator`. When can you use each? Adapt your code from above and sue the `MultiProcessingingEvaluator`. Use the time library to check how much faster the computation for 1000 scenarios completes.

Using multiprocessing within a jupyter notebook is tricky. On linux it will work in general just fine. On mac it depends on the version of Mac OS and the version of Python. If you are on the latest version of Mac OS in combination with Python 3.8, it might work but no guarantees. On older versions of Python it should work fine. On Windows it is always a problem.

The underlying explanation is quite technical. It has to do with how your operating system creates the additional python processes. On windows, and the latest version of Mac OS in combination with Python 3.8. A completely new Python process is spawned. This new process does **not** inherit what is defined in memory of the parent process. The new child process will try to replicate what is in memory of the parent process by executing many of the import statements that have also been executed within the python process. Thus, if you define a model in the main process, it is not guaranteed to be known in the child processes. This is in particular true if you define the model within a jupyter notebook. Then the child processes will **never** know this function. Within jupyter notebooks, therefore, the best practice is to define your model within a .py file and import this .py file into the notebook. Now, each of the child processes will also execute this import statement and thus know the function.

```python
from ema_workbench import MultiprocessingEvaluator

n_scenarios = 1000
n_policies = 4

with MultiprocessingEvaluator(lake_model) as evaluator:
    results = evaluator.perform_experiments(n_scenarios, n_policies)
```

    [MainProcess/INFO] pool started
    [MainProcess/INFO] performing 1000 scenarios * 4 policies * 1 model(s) = 4000 experiments
    [MainProcess/INFO] 400 cases completed
    [MainProcess/INFO] 800 cases completed
    [MainProcess/INFO] 1200 cases completed
    [MainProcess/INFO] 1600 cases completed
    [MainProcess/INFO] 2000 cases completed
    [MainProcess/INFO] 2400 cases completed
    [MainProcess/INFO] 2800 cases completed
    [MainProcess/INFO] 3200 cases completed
    [MainProcess/INFO] 3600 cases completed
    [MainProcess/INFO] 4000 cases completed
    [MainProcess/INFO] experiments finished
    [MainProcess/INFO] terminating pool

The ipyparallel Evaluator uses the ipyparallel library to suport multiprocessing. Working with this is a bit more involved than using multiprocessing. You first need to start ipcluster in cmd or a terminal:

```
ipcluster start

```

next, you need a client to interact with this cluster.

```python
import ipyparallel

client = ipyparallel.Client()
client.ids
```

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

```python
%%px --local

import math
import numpy as np

from scipy.optimize import brentq

def lake_problem(b=0.42, q=2.0, mean=0.02, stdev=0.0017, delta=0.98,
                 alpha=0.4, nsamples=100, steps=100, l0=0, l1=0, l2=0, l3=0,
                 l4=0, l5=0, l6=0, l7=0, l8=0, l9=0, l10=0, l11=0, l12=0, l13=0,
                 l14=0, l15=0, l16=0, l17=0, l18=0, l19=0, l20=0, l21=0, l22=0,
                 l23=0, l24=0, l25=0, l26=0, l27=0, l28=0, l29=0, l30=0, l31=0,
                 l32=0, l33=0, l34=0, l35=0, l36=0, l37=0, l38=0, l39=0, l40=0,
                 l41=0, l42=0, l43=0, l44=0, l45=0, l46=0, l47=0, l48=0, l49=0,
                 l50=0, l51=0, l52=0, l53=0, l54=0, l55=0, l56=0, l57=0, l58=0,
                 l59=0, l60=0, l61=0, l62=0, l63=0, l64=0, l65=0, l66=0, l67=0,
                 l68=0, l69=0, l70=0, l71=0, l72=0, l73=0, l74=0, l75=0, l76=0,
                 l77=0, l78=0, l79=0, l80=0, l81=0, l82=0, l83=0, l84=0, l85=0,
                 l86=0, l87=0, l88=0, l89=0, l90=0, l91=0, l92=0, l93=0, l94=0,
                 l95=0, l96=0, l97=0, l98=0, l99=0,):
    decisions = np.array([l0, l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13,
                          l14, l15, l16, l17, l18, l19, l20, l21, l22, l23, l24, l25,
                          l26, l27, l28, l29, l30, l31, l32, l33, l34, l35, l36, l37,
                          l38, l39, l40, l41, l42, l43, l44, l45, l46, l47, l48, l49,
                          l50, l51, l52, l53, l54, l55, l56, l57, l58, l59, l60, l61,
                          l62, l63, l64, l65, l66, l67, l68, l69, l70, l71, l72, l73,
                          l74, l75, l76, l77, l78, l79, l80, l81, l82, l83, l84, l85,
                          l86, l87, l88, l89, l90, l91, l92, l93, l94, l95, l96, l97,
                          l98, l99])

    Pcrit = brentq(lambda x: x**q/(1+x**q) - b*x, 0.01, 1.5)
    nvars = len(decisions)
    X = np.zeros((nvars,))
    average_daily_P = np.zeros((nvars,))
    decisions = np.array(decisions)
    reliability = 0.0

    for _ in range(nsamples):
        X[0] = 0.0

        natural_inflows = np.random.lognormal(
                math.log(mean**2 / math.sqrt(stdev**2 + mean**2)),
                math.sqrt(math.log(1.0 + stdev**2 / mean**2)),
                size = nvars)

        for t in range(1,nvars):
            X[t] = (1-b)*X[t-1] + X[t-1]**q/(1+X[t-1]**q) + decisions[t-1] +\
                    natural_inflows[t-1]
            average_daily_P[t] += X[t]/float(nsamples)

        reliability += np.sum(X < Pcrit)/float(nsamples*nvars)

    max_P = np.max(average_daily_P)
    utility = np.sum(alpha*decisions*np.power(delta,np.arange(nvars)))
    inertia = np.sum(np.abs(np.diff(decisions)) > 0.02)/float(nvars-1)

    return max_P, utility, inertia, reliability

```

```python
#instantiate the model
lake_model = Model('lakeproblem', function=lake_problem)
lake_model.time_horizon = 100 # used to specify the number of timesteps

#specify uncertainties
lake_model.uncertainties = [RealParameter('mean', 0.01, 0.05),
                            RealParameter('stdev', 0.001, 0.005),
                            RealParameter('b', 0.1, 0.45),
                            RealParameter('q', 2.0, 4.5),
                            RealParameter('delta', 0.93, 0.99)]

# set levers, one for each time step
lake_model.levers = [RealParameter(f"l{i}", 0, 0.1) for i in
                     range(lake_model.time_horizon)] # we use time_horizon here

#specify outcomes
lake_model.outcomes = [ScalarOutcome('max_P'),
                       ScalarOutcome('utility'),
                       ScalarOutcome('inertia'),
                       ScalarOutcome('reliability')]
```

And now we are ready to execute our code, note that IpyparallelEvaluator requires the client as interface to the cluster.

```python
from ema_workbench import  IpyparallelEvaluator

n_scenarios = 1000
n_policies = 4

with IpyparallelEvaluator(lake_model, client=client) as evaluator:
    results = evaluator.perform_experiments(n_scenarios, n_policies)
```

    [MainProcess/INFO] performing experiments using ipyparallel
    [MainProcess/INFO] performing 1000 scenarios * 4 policies * 1 model(s) = 4000 experiments
    [MainProcess/INFO] 400 cases completed
    [MainProcess/INFO] 800 cases completed
    [MainProcess/INFO] 1200 cases completed
    [MainProcess/INFO] 1600 cases completed
    [MainProcess/INFO] 2000 cases completed
    [MainProcess/INFO] 2400 cases completed
    [MainProcess/INFO] 2800 cases completed
    [MainProcess/INFO] 3200 cases completed
    [MainProcess/INFO] 3600 cases completed
    [MainProcess/INFO] 4000 cases completed
    [MainProcess/INFO] experiments finished

```python

```

epa1361_open/Week 1-2 - general intro to exploratory modelling/assignment 2 - model answer.ipynb


```python
import sympy as sy
import numpy as np
```

```python
w, a, b = sy.symbols('w, a, b', real=True, positive=True)
s = sy.symbols('s', real=False)
Kd, Kp, Ki = sy.symbols('Kd, Kp, Ki', real=True, positive=True)

```

```python
p1 = -w
p2 = -np.sqrt(2)/2*w + sy.I*np.sqrt(2)/2*w
p3 = -np.sqrt(2)/2*w - sy.I*np.sqrt(2)/2*w
```

```python
sy.simplify(sy.expand((s-p2)*(s-p3)))
```

    s**2 + 1.4142135623731*s*w + 1.0*w**2

```python
Ac = sy.simplify(sy.expand((s-p1)*(s-p2)*(s-p3)))
```

    s**3 + 2.41421356237309*s**2*w + 2.4142135623731*s*w**2 + 1.0*w**3

```python
A = s**2 + a*s -w*w
B = b
R = s
S = Kd*s**2 + Kp*s + Ki
LHS = sy.collect(sy.expand(A*R+B*S), s)
print LHS
```

    Ki*b + s**3 + s**2*(Kd*b + a) + s*(Kp*b - w**2)

```python

```


```python
import sympy as sm
import sympy.physics.mechanics as me
me.init_vprinting()
```

```python
m, g, kt, kl, l = sm.symbols('m, g, k_t, k_l, l')
q1, q2, q3 = me.dynamicsymbols('q1, q2, q3')
u1, u2, u3 = me.dynamicsymbols('u1, u2, u3')
```

```python
N = me.ReferenceFrame('N')
A = me.ReferenceFrame('A')
B = me.ReferenceFrame('B')

A.orient_axis(N, q1, N.z)
B.orient_axis(A, q2, A.x)

A.set_ang_vel(N, u1*N.z)
B.set_ang_vel(A, u2*A.x)
```

```python
O = me.Point('O')
Ao = me.Point('A_O')
Bo = me.Point('B_O')
Q = me.Point('Q')

Ao.set_pos(O, l/2*A.x)
Bo.set_pos(O, l*A.x)
Q.set_pos(Bo, q3*B.y)

O.set_vel(N, 0)
Ao.v2pt_theory(O, N, A)
Bo.v2pt_theory(O, N, A)
Q.set_vel(B, u3*B.y)
Q.v1pt_theory(Bo, N, B)
```

```python
qd_repl = {q1.diff(): u1, q2.diff(): u2, q3.diff(): u3}
qd_repl
```

```python
Q.set_acc(N, Q.acc(N).xreplace(qd_repl))
Q.acc(N)
```

```python
v_Ao_1 = Ao.vel(N).diff(u1, N)
v_Ao_2 = Ao.vel(N).diff(u2, N)
v_Ao_3 = Ao.vel(N).diff(u3, N)

v_Ao_1, v_Ao_2, v_Ao_3
```

```python
v_Bo_1 = Bo.vel(N).diff(u1, N)
v_Bo_2 = Bo.vel(N).diff(u2, N)
v_Bo_3 = Bo.vel(N).diff(u3, N)

v_Bo_1, v_Bo_2, v_Bo_3
```

```python
v_Q_1 = Q.vel(N).diff(u1, N)
v_Q_2 = Q.vel(N).diff(u2, N)
v_Q_3 = Q.vel(N).diff(u3, N)

v_Q_1, v_Q_2, v_Q_3
```

```python
w_A_1 = A.ang_vel_in(N).diff(u1, N)
w_A_2 = A.ang_vel_in(N).diff(u2, N)
w_A_3 = A.ang_vel_in(N).diff(u3, N)

w_A_1, w_A_2, w_A_3
```

```python
w_B_1 = B.ang_vel_in(N).diff(u1, N)
w_B_2 = B.ang_vel_in(N).diff(u2, N)
w_B_3 = B.ang_vel_in(N).diff(u3, N)

w_B_1, w_B_2, w_B_3
```

```python
I = m*l**2/12
I_A_Ao = I*me.outer(A.y, A.y) + I*me.outer(A.z, A.z)
I_B_Bo = I*me.outer(B.x, B.x) + I*me.outer(B.z, B.z)

I_A_Ao, I_B_Bo
```

```python
F = me.dynamicsymbols('F')

R_Ao = m*g*N.x
R_Bo = m*g*N.x - F*B.y
R_Q = m/4*g*N.x + F*B.y

R_Ao, R_Bo, R_Q
```

```python
T_A = -kt*q1*N.z + kt*q2*B.x
T_B = -kt*q2*B.x

T_A, T_B
```

```python
Rs_Ao = -m*Ao.acc(N)
Rs_Bo = -m*Bo.acc(N)
Rs_Q = -m/4*Q.acc(N)
```

```python
Ts_A = -(me.dot(A.ang_acc_in(N), I_A_Ao) +
         me.dot(me.cross(A.ang_vel_in(N), I_A_Ao), A.ang_vel_in(N)))
Ts_A
```

```python
Ts_B = -(me.dot(B.ang_acc_in(N), I_B_Bo) +
         me.dot(me.cross(B.ang_vel_in(N), I_B_Bo), B.ang_vel_in(N)))
Ts_B
```

```python
Fr = sm.Matrix([
    v_Ao_1.dot(R_Ao) + v_Bo_1.dot(R_Bo) + v_Q_1.dot(R_Q) +
    w_A_1.dot(T_A) + w_B_1.dot(T_B),
    v_Ao_2.dot(R_Ao) + v_Bo_2.dot(R_Bo) + v_Q_2.dot(R_Q) +
    w_A_2.dot(T_A) + w_B_2.dot(T_B),
    v_Ao_3.dot(R_Ao) + v_Bo_3.dot(R_Bo) + v_Q_3.dot(R_Q) +
    w_A_3.dot(T_A) + w_B_3.dot(T_B),
])
```

```python
Frs = sm.Matrix([
    v_Ao_1.dot(Rs_Ao) + v_Bo_1.dot(Rs_Bo) + v_Q_1.dot(Rs_Q) +
    w_A_1.dot(Ts_A) + w_B_1.dot(Ts_B),
    v_Ao_2.dot(Rs_Ao) + v_Bo_2.dot(Rs_Bo) + v_Q_2.dot(Rs_Q) +
    w_A_2.dot(Ts_A) + w_B_2.dot(Ts_B),
    v_Ao_3.dot(Rs_Ao) + v_Bo_3.dot(Rs_Bo) + v_Q_3.dot(Rs_Q) +
    w_A_3.dot(Ts_A) + w_B_3.dot(Ts_B),
])
```

```python
kanes_eqs = Fr + Frs
kanes_eqs
```

```python
u = sm.Matrix([u1, u2, u3])
t = me.dynamicsymbols._t
ud = u.diff(t)
```

```python
Md = Frs.jacobian(ud)
Md
```

```python
ud_zero_repl = {u1.diff(): 0, u2.diff(): 0, u3.diff(): 0}
```

```python
gd = Frs.xreplace(ud_zero_repl) + Fr
gd
```

```python
me.find_dynamicsymbols(Md)
```

```python
me.find_dynamicsymbols(gd)
```

```python
Md.free_symbols | gd.free_symbols
```

```python
q = sm.Matrix([q1, q2, q3])
q
```

```python
u
```

```python
p = sm.Matrix([g, kt, l, m])
p
```

```python
# specified inputs
r = sm.Matrix([F])
r
```

```python
eval_dyn = sm.lambdify((q, u, p, r), (Md, gd))
```

```python
import numpy as np

q_vals = np.array([
    np.deg2rad(25.0),  # q1, rad
    np.deg2rad(5.0),  # q2, rad
    0.1,  # q3, m
])

u_vals = np.array([
    0.1,  # u1, rad/s
    2.2,  # u2, rad/s
    0.3,  # u3, m/s
])

p_vals = np.array([
    9.81,  # g, m/s**2
    0.01,  # kt, Nm/rad
    0.6,  # l, m
    1.0,  # m, kg
])

r_vals = np.array([100.0])  # Newtons
```

```python
Md_vals, gd_vals = eval_dyn(q_vals, u_vals, p_vals, r_vals)
```

```python
Md_vals
```

```python
gd_vals
```

```python
np.linalg.solve?
```

```python
ud_vals = np.linalg.solve(-Md_vals, np.squeeze(gd_vals))
ud_vals
```

```python
qd_vals = u_vals
qd_vals
```

```python
xd_vals = np.hstack((qd_vals, ud_vals))
xd_vals
```

```python
def eval_rhs(t, x, p, r):
    q = x[:3]
    u = x[3:]
    Md, gd = eval_dyn(q, u, p, r)
    ud = np.linalg.solve(-Md, np.squeeze(gd))
    qd = u
    return np.hstack((qd, ud))
```

```python
eval_rhs(2.1, np.hstack((q_vals, u_vals)), p_vals, r_vals)
```

```python
def eval_rhs(t, x, p):

    q = x[:3]
    u = x[3:]
    #r = np.array([1.0*np.sin(2*np.pi*t)])
    r = np.array([-2.0*q[2]])
    Md, gd = eval_dyn(q, u, p, r)
    ud = np.linalg.solve(-Md, np.squeeze(gd))
    qd = u
    return np.hstack((qd, ud))
```

```python
eval_rhs(2.1, np.hstack((q_vals, u_vals)), p_vals)
```

```python
from scipy.integrate import solve_ivp
```

```python
x0 = np.hstack((q_vals, u_vals))
t0 = 0.0
tf = 4.0

sol = solve_ivp(eval_rhs, (t0, tf), x0, args=(p_vals, ))
```

```python
sol.t
```

```python
sol.y
```

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

```python
sol.t.shape
```

```python
sol.y.shape
```

```python
%matplotlib notebook
```

```python
plt.plot(sol.t, np.transpose(sol.y))

plt.xlabel('Time [s]')

plt.legend(['q1', 'q2', 'q3', 'u1', 'u2', 'u3'])
```

```python
def plot_results(ts, xs):
    """Returns the array of axes of a 4 panel plot of the state trajectory
    versus time.

    Parameters
    ==========
    ts : array_like, shape(m,)
       Values of time.
    xs : array_like, shape(m, 6)
       Values of the state trajectories corresponding to ``ts`` in order
       [q1, q2, q3, u1, u2, u3].

    Returns
    =======
    axes : ndarray, shape(4,)
       Matplotlib axes for each panel.

    """

    fig, axes = plt.subplots(4, 1, sharex=True)

    fig.set_size_inches((10.0, 6.0))

    axes[0].plot(ts, np.rad2deg(xs[:, :2]), '.-')
    axes[1].plot(ts, xs[:, 2], '.-')
    axes[2].plot(ts, np.rad2deg(xs[:, 3:5]), '.-')
    axes[3].plot(ts, xs[:, 5], '.-')

    axes[0].legend([me.vlatex(q[0], mode='inline'),
                    me.vlatex(q[1], mode='inline')])
    axes[1].legend([me.vlatex(q[2], mode='inline')])
    axes[2].legend([me.vlatex(u[0], mode='inline'),
                    me.vlatex(u[1], mode='inline')])
    axes[3].legend([me.vlatex(u[2], mode='inline')])

    axes[0].set_ylabel('Angle [deg]')
    axes[1].set_ylabel('Distance [m]')
    axes[2].set_ylabel('Angular Rate [deg/s]')
    axes[3].set_ylabel('Speed [m/s]')

    axes[3].set_xlabel('Time [s]')

    fig.tight_layout()

    return axes
```

```python
plot_results(sol.t, np.transpose(sol.y))
```

```python
x0 = np.hstack((q_vals, u_vals))
t0 = 0.0
tf = 4.0

ts = np.linspace(t0, tf, num=100)

sol = solve_ivp(eval_rhs, (t0, tf), x0, args=(p_vals, ), t_eval=ts)
```

```python
plot_results(sol.t, np.transpose(sol.y))
```

```python
M = me.ReferenceFrame('M')
M.orient_axis(N, sm.pi/2, N.z)
```

```python
Bl = me.Point('B_l')
Br = me.Point('B_r')
Bl.set_pos(Bo, -l/2*B.y)
Br.set_pos(Bo, l/2*B.y)
```

```python
coordinates = O.pos_from(O).to_matrix(M)
for point in [Bo, Q, Bl, Br]:
    coordinates = coordinates.row_join(point.pos_from(O).to_matrix(M))

eval_point_coords = sm.lambdify((q, p), coordinates)
eval_point_coords(q_vals, p_vals)
```

```python
# initial configuration of the points
x, y, z = eval_point_coords(q_vals, p_vals)

# create a figure
fig = plt.figure()
fig.set_size_inches((10.0, 10.0))

# setup the subplots
ax_top = fig.add_subplot(2, 2, 1)
ax_3d = fig.add_subplot(2, 2, 2, projection='3d')
ax_front = fig.add_subplot(2, 2, 3)
ax_right = fig.add_subplot(2, 2, 4)

# common line and marker properties for each panel
line_prop = {
    'color': 'black',
    'marker': 'o',
    'markerfacecolor': 'blue',
    'markersize': 10,
}

# top view
lines_top, = ax_top.plot(x, z, **line_prop)
ax_top.set_xlim((-0.5, 0.5))
ax_top.set_ylim((0.5, -0.5))
ax_top.set_title('Top View')
ax_top.set_xlabel('x')
ax_top.set_ylabel('z')
ax_top.set_aspect('equal')

# 3d view
lines_3d, = ax_3d.plot(x, z, y, **line_prop)
ax_3d.set_xlim((-0.5, 0.5))
ax_3d.set_ylim((0.5, -0.5))
ax_3d.set_zlim((-0.8, 0.2))
ax_3d.set_xlabel('x')
ax_3d.set_ylabel('z')
ax_3d.set_zlabel('y')

# front view
lines_front, = ax_front.plot(x, y, **line_prop)
ax_front.set_xlim((-0.5, 0.5))
ax_front.set_ylim((-0.8, 0.2))
ax_front.set_title('Front View')
ax_front.set_xlabel('x')
ax_front.set_ylabel('y')
ax_front.set_aspect('equal')

# right view
lines_right, = ax_right.plot(z, y, **line_prop)
ax_right.set_xlim((0.5, -0.5))
ax_right.set_ylim((-0.8, 0.2))
ax_right.set_title('Right View')
ax_right.set_xlabel('z')
ax_right.set_ylabel('y')
ax_right.set_aspect('equal')

fig.tight_layout()
```

```python
from matplotlib.animation import FuncAnimation
```

```python
xs = np.transpose(sol.y)
def animate(i):
    x, y, z = eval_point_coords(xs[i, :3], p_vals)
    lines_top.set_data(x, z)
    lines_3d.set_data_3d(x, z, y)
    lines_front.set_data(x, y)
    lines_right.set_data(z, y)
```

```python
animate(20)
```

```python
ani = FuncAnimation(fig, animate, len(sol.t))
```

```python
from IPython.display import HTML

HTML(ani.to_html5_video())
```

```python
HTML(ani.to_jshtml(fps=30))
```

```python

```


```
%matplotlib inline
from IPython.html.widgets import interact,fixed
import toolz
from scipy import stats
import sympy
from sympy import S, init_printing, Sum, summation,expand
from sympy import stats as st
from sympy.abc import k
import re
```

## Maximum likelihood estimation

$$\hat{p} = \frac{1}{n} \sum_{i=1}^n X_i$$

$$ \mathbb{E}(\hat{p}) = p $$

$$ \mathbb{V}(\hat{p}) = \frac{p(1-p)}{n} $$

```
sympy.plot( k*(1-k),(k,0,1),ylabel='$n^2 \mathbb{V}$',xlabel='p',fontsize=28)
```

```
b=stats.bernoulli(p=.8)
samples=b.rvs(100)
print var(samples)
print mean(samples)
```

    0.1971
    0.73

```
def slide_figure(n=100,m=1000,p=0.8):
    fig,ax=subplots()
    ax.axis(ymax=25)
    b=stats.bernoulli(p=p)
    v=iter(b.rvs,2)
    samples=b.rvs(n*1000)
    tmp=(samples.reshape(n,-1).mean(axis=0))
    ax.hist(tmp,normed=1)
    ax1=ax.twinx()
    ax1.axis(ymax=25)
    ax1.plot(linspace(0,1,200),stats.norm(mean(tmp),std(tmp)).pdf(linspace(0.0,1,200)),lw=3,color='r')
```

```
interact(slide_figure, n=(100,500),p=(0.01,1,.05),m=fixed(500));
```

## Maximum A-Posteriori (MAP) Estimation

```
from sympy.abc import p,n,k

sympy.plot(st.density(st.Beta('p',3,3))(p),(p,0,1) ,xlabel='p')
```

### Maximize the MAP function to get form of estimator

```
obj=sympy.expand_log(sympy.log(p**k*(1-p)**(n-k) * st.density(st.Beta('p',6,6))(p)))
```

```
sol=sympy.solve(sympy.simplify(sympy.diff(obj,p)),p)[0]
print sol
```

    (k + 5)/(n + 10)

$ \hat{p}_{MAP} = \frac{(5+\sum_{i=1}^n X_i )}{(n + 10)}  $

with corresponding expectation

$ \mathbb{E}  = \frac{(5+n p )}{(n + 10)} $

which is a biased estimator

```
n=5
def show_bias(n=30):
    sympy.plot(p,(5+n*p)/(n+10),(p,0,1),aspect_ratio=1,title='more samples reduce bias')

interact(show_bias,n=(10,500,10));
```

Compute the variance of the MAP estimator

```
sum(sympy.var('x1:10'))
expr=((5+(sum(sympy.var('x1:10'))))/(n+10))
```

```
def apply_exp(expr):
    tmp=re.sub('x[\d]+\*\*2','p',str(expand(expr)))
    tmp=re.sub('x[\d]+\*x[\d]+','p**2',tmp)
    tmp=re.sub('x[\d]+','p',tmp)
    return sympy.sympify(tmp)
```

```
ex2 = apply_exp(expr**2)
print ex2
```

    8*p**2/25 + 11*p/25 + 1/9

```
tmp=sympy.simplify(ex2 - (apply_exp(expr))**2 )
sympy.plot(tmp,p*(1-p)/10,(p,0,1))
```

### General case

```
def generate_expr(num_samples=10,alpha=6):
    n = sympy.symbols('n')
    obj=sympy.expand_log(sympy.log(p**k*(1-p)**(n-k) * st.density(st.Beta('p',alpha,alpha))(p)))
    sol=sympy.solve(sympy.simplify(sympy.diff(obj,p)),p)[0]
    expr=sol.replace(k,(sum(sympy.var('x1:%d'%(num_samples)))))
    expr=expr.subs(n,num_samples)
    ex2 = apply_exp(expr**2)
    ex =  apply_exp(expr)
    return (ex,sympy.simplify(ex2-ex**2))
```

```
num_samples=10
X_bias,X_v = generate_expr(num_samples,alpha=2)
p1=sympy.plot(X_v,(p,0,1),show=False,line_color='b',ylim=(0,.03),xlabel='p')
X_bias,X_v = generate_expr(num_samples,alpha=6)
p2=sympy.plot(X_v,(p,0,1),show=False,line_color='r',xlabel='p')
p3=sympy.plot(p*(1-p)/num_samples,(p,0,1),show=False,line_color='g',xlabel='p')
p1.append(p2[0])
p1.append(p3[0])
p1.show()
```

```

```


```python
from epipack import SymbolicEpiModel
import sympy as sy
```

```python
u, v, k, f = sy.symbols("u v k f")
```

```python
GS = SymbolicEpiModel([u,v])

GS.set_processes([
    # third-order coupling
    (u, v, v, v, v),
    # birth and death
    (None, f, u),
    (u, f, None),
    (v, f+k, None),
],ignore_rate_position_checks=True)
```

    /Users/bfmaier/Seafile/forschung/disease_dynamics/epipack/epipack/symbolic_epi_models.py:572: UserWarning: events do not sum to zero for each column:-f*u + f - v*(f + k)
      warnings.warn("events do not sum to zero for each column:" + str(test_sum))

    <epipack.symbolic_epi_models.SymbolicEpiModel at 0x1647136d0>

```python
GS.ODEs_jupyter()
```

$\displaystyle \frac{d}{d t} u = - f u + f - u v^{2}$

$\displaystyle \frac{d}{d t} v = v \left(- f - k + u v\right)$

```python
GS.find_fixed_points()
```

$\displaystyle \left\{\left( 1, \  0\right), \left( - \frac{- f + \left(f + k\right) \left(\frac{f}{2 \left(f + k\right)} - \frac{\sqrt{- f \left(4 f^{2} + 8 f k - f + 4 k^{2}\right)}}{2 \left(f + k\right)}\right)}{f}, \  \frac{f}{2 \left(f + k\right)} - \frac{\sqrt{- f \left(4 f^{2} + 8 f k - f + 4 k^{2}\right)}}{2 \left(f + k\right)}\right), \left( - \frac{- f + \left(f + k\right) \left(\frac{f}{2 \left(f + k\right)} + \frac{\sqrt{- f \left(4 f^{2} + 8 f k - f + 4 k^{2}\right)}}{2 \left(f + k\right)}\right)}{f}, \  \frac{f}{2 \left(f + k\right)} + \frac{\sqrt{- f \left(4 f^{2} + 8 f k - f + 4 k^{2}\right)}}{2 \left(f + k\right)}\right)\right\}$

```python
GS.jacobian()
```

$\displaystyle \left[\begin{matrix}- f - v^{2} & - 2 u v\\v^{2} & - f - k + 2 u v\end{matrix}\right]$

```python
GS.get_eigenvalues_at_disease_free_state({"u":1})
```

    {-f: 1, -f - k: 1}

```python
print(GS.ODEs())
print()
print(GS.find_fixed_points())
print()
print(GS.jacobian())
print()
print(GS.get_eigenvalues_at_disease_free_state({"u":1}))
```

    [Eq(Derivative(u, t), -f*u + f - u*v**2), Eq(Derivative(v, t), v*(-f - k + u*v))]

    FiniteSet((1, 0), (-(-f + (f + k)*(f/(2*(f + k)) - sqrt(-f*(4*f**2 + 8*f*k - f + 4*k**2))/(2*(f + k))))/f, f/(2*(f + k)) - sqrt(-f*(4*f**2 + 8*f*k - f + 4*k**2))/(2*(f + k))), (-(-f + (f + k)*(f/(2*(f + k)) + sqrt(-f*(4*f**2 + 8*f*k - f + 4*k**2))/(2*(f + k))))/f, f/(2*(f + k)) + sqrt(-f*(4*f**2 + 8*f*k - f + 4*k**2))/(2*(f + k))))

    Matrix([[-f - v**2, -2*u*v], [v**2, -f - k + 2*u*v]])

    {-f: 1, -f - k: 1}

```python

```


# The Riemann Solution on $\tilde{S}_i$ using HLLE
### Author: Patrick Nelson

This notebook documents the function from the original `GiRaFFE` that calculates the flux for $\tilde{S}_i$ according to the method of Harten, Lax, von Leer, and Einfeldt (HLLE), assuming that we have calculated the values of the velocity and magnetic field on the cell faces according to the piecewise-parabolic method (PPM) of [Colella and Woodward (1984)](https://crd.lbl.gov/assets/pubs_presos/AMCS/ANAG/A141984.pdf), modified for the case of GRFFE.

**Notebook Status:**  Validated against the original `GiRaFFE`

**Validation Notes:** Everything should be written at this point, but we need to finish the rest of the algorithm before we can validate this.

The differential equations that `GiRaFFE` evolves are written in conservation form, and thus have two different terms that contribute to the time evolution of some quantity: the flux term and the source term. The PPM method is what the original `GiRaFFE` uses to handle the flux term; hopefully, using this instead of finite-differencing will fix some of the problems we've been having with `GiRaFFE_HO`.

In GRFFE, the evolution equation for the Poynting flux $\tilde{S}_i$ is given as
$$
\boxed{\partial_t \tilde{S}_i + \underbrace{ \partial_j \left( \alpha \sqrt{\gamma} T^j_{{\rm EM} i} \right)}_{\rm Flux\ term} = \underbrace{\frac{1}{2} \alpha \sqrt{\gamma} T^{\mu \nu}_{\rm EM} \partial_i g_{\mu \nu}}_{\rm Source\ term}.}
$$
We can then see that, if we rewrite this, the right-hand side (RHS) describing the time evolution $\partial_t \tilde{S}_i$ consists of two terms: the flux term and the source term. The flux term in particular can be tricky, as it may be discontinuous due to shocks or other sharp features. This presents difficulties when we take finite-difference derivatives of that term, leading to the Gibbs phenomenon. So, we implement a different algorithm to take the derivative.

It is the flux term $\alpha \sqrt{\gamma} T^j_{{\rm EM} i} = \alpha \sqrt{\gamma} g_{i \mu} T^{\mu i}_{\rm EM}$ itself that concerns us here, where $T^{\mu \nu}_{\rm EM} = b^2 u^\mu u^\nu + \frac{1}{2} b^2 g^{\mu \nu} - b^\mu b^\nu$; the following functions will compute this value so that we can easily take its derivative later. Having reconstructed the values of $v^i_{(n)}$ and $B^i$ on the cell faces, we will now compute the value of the flux of $\tilde{S}_i$ on each face. For each component of $\tilde{S}_i$ in each direction, we compute the flux as
$$
{\rm st\_j\_flux} = \frac{c_{\rm min} f_{\rm R} + c_{\rm max} f_{\rm L} - c_{\rm min} c_{\rm max} ({\rm st\_j\_r}-{\rm st\_j\_l})}{c_{\rm min} + c_{\rm max}},
$$
where
$$
f = \alpha \sqrt{\gamma} \left( (\rho+b^2)(u^0 v^j) u_i + (P+\frac{1}{2}b^2) \delta_{ji} - b^j b_i \right)
$$
and
$$
{\rm st\_j} = \alpha \sqrt{\gamma} \left( (\rho+b^2) u^0 u_i - b^t b_i \right).
$$
Here, $i$ is direction in which we are computing the flux, and $j$ is the component of the momentum we are computing it for. Note that these two quantities are computed on both the left and right sides of the cell face. We will be able to draw heavily on the module [Tutorial-u0_smallb_Poynting-Cartesian.ipynb](Tutorial-u0_smallb_Poynting-Cartesian.ipynb) to compute $u^0$, $u^i$, and $b^\mu$, as well as the index-lowered forms of those vectors. Critically, these quantities depend on the Valencia 3-velocity $v^i_{(n)}$ and magnetic field $B^i$. We will not be using the normal gridfunctions for these, but rather the ones that we have previosly calculated on the left and right sides of the cell faces using the [Piecewise Parabolic Method](Tutorial-GiRaFFE_HO_Ccode_library-PPM.ipynb).

The speeds $c_\min$ and $c_\max$ are characteristic speeds that waves can travel through the plasma. In GRFFE, the expressions defining them reduce a function of only the metric quantities. $c_\min$ is the negative of the minimum amongst the speeds $c_-$ and $0$ and $c_\max$ is the maximum amongst the speeds $c_+$ and $0$. The speeds $c_\pm = \sqrt{b^2-4ac}$ must be calculated on both the left and right faces, where
$$a = 1/\alpha^2,$$
$$b = 2 \beta^i / \alpha^2$$
and $$c = g^{ii} - (\beta^i)^2/\alpha^2.$$

Another point to consider is that since we are working on cell faces, not at the cell center, we can't use the normal metric values that we store. We will instead use the value of the metric interpolated onto the cell face, which in this tutorial, we will assume has been previously done.

The algorithm for finite-volume methods in general is as follows:

1. The Reconstruction Step - Piecewise Parabolic Method
    1. Within each cell, fit to a function that conserves the volume in that cell using information from the neighboring cells
        * For PPM, we will naturally use parabolas
    1. Use that fit to define the state at the left and right interface of each cell
    1. Apply a slope limiter to mitigate Gibbs phenomenon
1. Interpolate the value of the metric gridfunctions on the cell faces
1. **Solving the Riemann Problem - Harten, Lax, (This notebook, $\tilde{S}_i$ only)**
    1. **Use the left and right reconstructed states to calculate the unique state at boundary**
1. Use the unique state to estimate the derivative in the cell

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

This notebook is organized as follows

1. [Step 1](#prelim): Preliminaries
1. [Step 2](#s_i_flux): The $\tilde{S}_i$ function
    1. [Step 2.a](#define): The function declaration: inputs, outputs, and macros
    1. [Step 2.b](#speed_limit): Speed-limit the velocities
    1. [Step 2.c](#smallb): Computing $b^\mu$, the magnetic field in the comoving fluid frame
    1. [Step 2.d](#hydro_speed): GRFFE characteristic wave speeds
    1. [Step 2.e](#fluxes): Compute the HLLE fluxes
1. [Step 3](#code_validation): Code Validation against `GiRaFFE_HO.GiRaFFE_Higher_Order_v2` NRPy+ Module
1. [Step 4](#derive_speed): Complete Derivation of the Wave Speeds
1. [Step 5](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file

# Step 1: Preliminaries \[Back to [top](#toc)\]
$$\label{prelim}$$

This first block of code just sets up a subdirectory within `GiRaFFE_standalone_Ccodes/` to which we will write the C code. We will also import the core NRPy+ functionality and register the needed gridfunctions. Doing so will let NRPy+ figure out where to read and write data from/to.

```python
# Step 0: Add NRPy's directory to the path
# https://stackoverflow.com/questions/16780014/import-file-from-parent-directory
import os,sys
nrpy_dir_path = os.path.join("..")
if nrpy_dir_path not in sys.path:
    sys.path.append(nrpy_dir_path)

from outputC import *            # NRPy+: Core C code output module
import finite_difference as fin  # NRPy+: Finite difference C code generation module
import NRPy_param_funcs as par   # NRPy+: Parameter interface
import grid as gri               # NRPy+: Functions having to do with numerical grids
import loop as lp                # NRPy+: Generate C code loops
import indexedexp as ixp         # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support
import reference_metric as rfm   # NRPy+: Reference metric support
import cmdline_helper as cmd     # NRPy+: Multi-platform Python command-line interface
import shutil, os, sys           # Standard Python modules for multiplatform OS-level functions

outdir = "GiRaFFE_standalone_Ccodes/"
cmd.mkdir(outdir)
outdir += "PPM/"
cmd.mkdir(outdir)

par.set_parval_from_str("grid::DIM", 3)
DIM = par.parval_from_str("grid::DIM")

thismodule = "GiRaFFE_NRPy_Ccode_library-Stilde-flux"

```

We will also create the gridfunctions within NRPy+ so that it knows how to access them. Note that we declare different gridfunctions than normal, since we want to use the values of those gridfunctions interpolated on the cell faces.

```python
# These are the standard gridfunctions we've used before.
#ValenciavU = ixp.register_gridfunctions_for_single_rank1("AUXEVOL","ValenciavU",DIM=3)
#gammaDD = ixp.register_gridfunctions_for_single_rank2("AUXEVOL","gammaDD","sym01")
#betaU = ixp.register_gridfunctions_for_single_rank1("AUXEVOL","betaU")
#alpha = gri.register_gridfunctions("AUXEVOL",["alpha"])
#AD = ixp.register_gridfunctions_for_single_rank1("EVOL","AD",DIM=3)
#BU = ixp.register_gridfunctions_for_single_rank1("AUXEVOL","BU",DIM=3)

# We will pass values of the gridfunction on the cell faces into the function. This requires us
# to declare them as C parameters in NRPy+. We will denote this with the _face infix/suffix.
alpha_face,gammadet_face = gri.register_gridfunctions("AUXEVOL",["alpha_face","gammadet_face"])
gamma_faceDD = ixp.register_gridfunctions_for_single_rank2("AUXEVOL","gamma_faceDD","sym01")
gamma_faceUU = ixp.register_gridfunctions_for_single_rank2("AUXEVOL","gamma_faceUU","sym01")
beta_faceU = ixp.register_gridfunctions_for_single_rank1("AUXEVOL","beta_faceU")

# We'll need some more gridfunctions, now, to represent the reconstructions of BU and ValenciavU
# on the right and left faces
Valenciav_rU = ixp.register_gridfunctions_for_single_rank1("AUXEVOL","Valenciav_rU",DIM=3)
B_rU = ixp.register_gridfunctions_for_single_rank1("AUXEVOL","B_rU",DIM=3)
Valenciav_lU = ixp.register_gridfunctions_for_single_rank1("AUXEVOL","Valenciav_lU",DIM=3)
B_lU = ixp.register_gridfunctions_for_single_rank1("AUXEVOL","B_lU",DIM=3)

# ...and some more for the fluxes we calculate here. These three gridfunctions will each store
# the momentum flux of one component of StildeD in one direction; we'll be able to reuse them
# as we loop over each direction, reducing our memory costs.
Stilde_fluxD = ixp.register_gridfunctions_for_single_rank1("AUXEVOL","Stilde_fluxD",DIM=3)
```

# Step 2: The $\tilde{S}_i$ function \[Back to [top](#toc)\]
$$\label{s_i_flux}$$

## Step 2.a: The function declaration: inputs, outputs, and macros \[Back to [top](#toc)\]
$$\label{define}$$

First, we give the function definition. This function will take as parameters the integers `i0`, `i1`, and `i2`; these indicate a point on the grid. The function also needs the array of gridfunctions (which we use here for the reconstructed face values (see [the previous tutorial](Tutorial-GiRaFFE_HO_Ccode_library-PPM.ipynb) and the interpolated metric quantities on the cell interfaces). We also calculate quantities related to the conformal factor $\psi$ (including the square root of the determinant of the metric $\sqrt{\gamma}$) and the lapse $\alpha$; these lines will be handled in Python so the NRPy+ can optimize them.

```python
# This product occurs very frequently, so we'll set it now. Conveniently,
# we store the metric determinant gamma directly.
alpha_sqrt_gamma = alpha_face*sp.sqrt(gammadet_face)

# We'll also compute some powers of the conformal factor psi:
# Since psi^6 = sqrt(gammadet) by definition,
psi = sp.sqrt(gammadet_face)**(1.0/6.0)
psim4 = psi**(-4.0)
```

## Step 2.b: Speed-limit the velocities \[Back to [top](#toc)\]
$$\label{speed_limit}$$

Next, we compute the speed-limited velocities on the faces. This function is found in `inlined_functions.C`, however, we will replace it with our own that does the same thing (and is already compatible with the rest of our code). The original function also counts how often the speed had to be limited. We also introduce a variation of the previous speed limiter that allows the process to be incorporated directly into the final expressions that we will pass to `FD_outputC()`.

### Step 2.b.i: Streamlining $u^0$ \[Back to [top](#toc)\]
$$\label{u0_no_if}$$

When we calculate $u^0$, we first check the Valencia 3-velocity and limit its speed if necessary. This is done as explained in the tutorial found [here](Tutorial-u0_smallb_Poynting-Cartesian.ipynb):

>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}}.
$$

However, we can once again eliminate the `if` statement used here to streamline our code generation. Note that when we violate the speed limit, we rescale by a factor $\sqrt{R_{\rm max}/R}$, where $R_{\rm max}=1 - \frac{1}{\Gamma_{\rm max}}$. It thus follows that, by letting $R_\max$ instead equal $\min(R,1 - \frac{1}{\Gamma_{\rm max}})$, we can *always* rescale by that same factor. In the case where we previously would not have rescaled, $\sqrt{R_{\rm max}/R}=1$, and we have not changed anything we didn't want to.

```python
# This function rewrites the original version from Tutorial-u0_smallb_Poynting-Cartesian.ipynb
# without the if statement.
def compute_u0_noif(gammaDD,alpha,ValenciavU):
    # Inputs:  Metric gammaDD, lapse alpha, Valencia 3-velocity ValenciavU
    # Outputs: u^0, speed-limited ValenciavU

    # R = gamma_{ij} v^i v^j
    R = par.Cparameters("#define",thismodule,"TINYDOUBLE",1e-100)
    # We initialize R to TINYDOUBLE instead of 0 to avoid a 0/0 below,
    #    in the case that ValenciavU == 0 for all Valencia 3-velocities.
    # "Those tiny *doubles* make me warm all over
    #  with a feeling that I'm gonna love you till the end of time."
    #    - Adapted from Connie Francis' "Tiny Bubbles"
    for i in range(DIM):
        for j in range(DIM):
            R += gammaDD[i][j]*ValenciavU[i]*ValenciavU[j]

    # The default value isn't terribly important here, since we'll overwrite in the main C code
    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_{\max}^2
    Rmax = 1 - 1/(GAMMA_SPEED_LIMIT*GAMMA_SPEED_LIMIT)
    # Now, we set Rmax = min(Rmax,R):
    # If Rmax>R, then Rmax = 0.5*(Rmax+R-Rmax+R) = R
    # If R>Rmax, then Rmax = 0.5*(Rmax+R+Rmax-R) = Rmax
    # If R==TINYDOUBLE, then Rmax = 0.5*(Rmax-Rmax) = 0, since, e.g., 10 +/- 1e-100 = 10 exactly in double precision    Rmax =  sp.Rational(1,2)*(Rmax+R-nrpyAbs(Rmax-R))
    Rmax =  sp.Rational(1,2)*(Rmax+R-nrpyAbs(Rmax-R))

    # With our rescaled Rmax, v^i = sqrt{Rmax/R} v^i
    rescaledValenciavU = ixp.zerorank1()
    for i in range(DIM):
        # If R == TINYDOUBLE, then Rmax=0,R=1e-100, and we get Rmax/R=0.
        #   If your velocities are of order 1e-100 and this is physically
        #   meaningful, there must be something wrong with your unit conversion.
        rescaledValenciavU[i] = ValenciavU[i]*sp.sqrt(Rmax/R)

    # We stick with the "rescaled" version since we redefine Rmax as detailed above
    # u^0 = 1/(alpha-sqrt(1-R_{\max}))
    rescaledu0 = 1/(alpha*sp.sqrt(1-Rmax))
    return rescaledValenciavU,rescaledu0

```

## Step 2.c: Computing $b^\mu$, the magnetic field in the comoving fluid frame \[Back to [top](#toc)\]
$$\label{smallb}$$

Now, we will compute the magnetic field in the comoving fluid frame $b^\mu$ , as defined in [Gammie's paper](https://arxiv.org/pdf/astro-ph/0301509.pdf).  We will also lower the index of the four velocities on the faces $u_\mu$.

Conveniently enough, the NRPy+ code that does this has already been written, and is found in `u0_smallb_Poynting__Cartesian.py`, which is documented [here](Tutorial-u0_smallb_Poynting-Cartesian.ipynb). We will call the function at least twice, once each for left and right side of the face. If we use the old version of the speed-limiting algorithm, we will need to call it a third time so that we generate a more generic version of `computeu0_Cfunction.h`.

```python
# We already did something like this in GiRaFFE_Higher_Order_v2; we'll
# paste that code here, but using the face variables. This assumes that
# we've already interpolated the metric quantities on the faces.
import u0_smallb_Poynting__Cartesian.u0_smallb_Poynting__Cartesian as u0b

# Using the function coded above, we find the speed-limited v^i and u^0 on both
# left and right faces.
Valenciav_rU,u4upperZero_r = compute_u0_noif(gamma_faceDD,alpha_face,Valenciav_rU)
Valenciav_lU,u4upperZero_l = compute_u0_noif(gamma_faceDD,alpha_face,Valenciav_lU)

# After importing the relevant module, we'll run the function contained therein
# with the interpolated metric face values and the reconstructed velocities
# and magnetic fields. Then, we'll simply read in the expressions that that
# function set, renaming them as convenient and necessary. In particular, we
# add _r and _l tags and replace u0 with u4upperZero, which is necessary to avoid
# NRPy+ interpreting u0 as something it's not.
# LEFT
u0b.compute_u0_smallb_Poynting__Cartesian(gamma_faceDD,beta_faceU,alpha_face,Valenciav_lU,B_lU)
u_lD = ixp.zerorank1()
u_lU = ixp.zerorank1()

for i in range(DIM):
    u_lD[i] = u0b.uD[i].subs(u0b.u0,u4upperZero_l)
    u_lU[i] = u0b.uU[i].subs(u0b.u0,u4upperZero_l)

smallb4_lU = ixp.zerorank1(DIM=4)
smallb4_lD = ixp.zerorank1(DIM=4)
for mu in range(4):
    smallb4_lU[mu] = u0b.smallb4U[mu].subs(u0b.u0,u4upperZero_l)
    smallb4_lD[mu] = u0b.smallb4D[mu].subs(u0b.u0,u4upperZero_l)

smallb2_l = u0b.smallb2etk.subs(u0b.u0,u4upperZero_l)

# We must repeat this process on both faces.
# RIGHT
u0b.compute_u0_smallb_Poynting__Cartesian(gamma_faceDD,beta_faceU,alpha_face,Valenciav_rU,B_rU)
u_rD = ixp.zerorank1()
u_rU = ixp.zerorank1()

for i in range(DIM):
    u_rD[i] = u0b.uD[i].subs(u0b.u0,u4upperZero_r)
    u_rU[i] = u0b.uU[i].subs(u0b.u0,u4upperZero_r)

smallb4_rU = ixp.zerorank1(DIM=4)
smallb4_rD = ixp.zerorank1(DIM=4)
for mu in range(4):
    smallb4_rU[mu] = u0b.smallb4U[mu].subs(u0b.u0,u4upperZero_r)
    smallb4_rD[mu] = u0b.smallb4D[mu].subs(u0b.u0,u4upperZero_r)

smallb2_r = u0b.smallb2etk.subs(u0b.u0,u4upperZero_r)

```

## Step 2.d: GRFFE characteristic wave speeds \[Back to [top](#toc)\]
$$\label{hydro_speed}$$

Next, we will find the speeds at which the hydrodynamics waves propagate. We start from the speed of light (since FFE deals with very diffuse plasmas), which is $c=1.0$ in our chosen units. We then find the speeds $c_+$ and $c_-$ on each face with the function `find_cp_cm`; then, we find minimum and maximum speeds possible from among those.

Below is the source code for `find_cp_cm`, edited to work with the NRPy+ version of GiRaFFE. One edit we need to make in particular is to the term `psim4*gupii` in the definition of `c`; that was written assuming the use of the conformal metric $\tilde{g}^{ii}$. Since we are not using that here, and are instead using the ADM metric, we should not multiply by $\psi^{-4}$.

```c
static inline void find_cp_cm(REAL &cplus,REAL &cminus,const REAL v02,const REAL u0,
                              const REAL vi,const REAL lapse,const REAL shifti,
                              const REAL gammadet,const REAL gupii) {
  const REAL u0_SQUARED=u0*u0;
  const REAL ONE_OVER_LAPSE_SQUARED = 1.0/(lapse*lapse);
  // sqrtgamma = psi6 -> psim4 = gammadet^(-1.0/3.0)
  const REAL psim4 = pow(gammadet,-1.0/3.0);
  //Find cplus, cminus:
  const REAL a = u0_SQUARED * (1.0-v02) + v02*ONE_OVER_LAPSE_SQUARED;
  const REAL b = 2.0* ( shifti*ONE_OVER_LAPSE_SQUARED * v02 - u0_SQUARED * vi * (1.0-v02) );
  const REAL c = u0_SQUARED*vi*vi * (1.0-v02) - v02 * ( gupii -
                                                               shifti*shifti*ONE_OVER_LAPSE_SQUARED);
  REAL detm = b*b - 4.0*a*c;
  //ORIGINAL LINE OF CODE:
  //if(detm < 0.0) detm = 0.0;
  //New line of code (without the if() statement) has the same effect:
  detm = sqrt(0.5*(detm + fabs(detm))); /* Based on very nice suggestion from Roland Haas */

  cplus = 0.5*(detm-b)/a;
  cminus = -0.5*(detm+b)/a;
  if (cplus < cminus) {
    const REAL cp = cminus;
    cminus = cplus;
    cplus = cp;
  }
}
```
Comments documenting this have been excised for brevity, but are reproduced in $\LaTeX$ [below](#derive_speed).

We could use this code directly, but there's substantial improvement we can make by changing the code into a NRPyfied form. Note the `if` statement; NRPy+ does not know how to handle them, so we must eliminate it if we want to leverage NRPy+'s full power. (Calls to `fabs()` are also cheaper than `if` statements.) This can be done if we rewrite this, taking inspiration from the other eliminated `if` statement documented in the above code block:
```c
  cp = 0.5*(detm-b)/a;
  cm = -0.5*(detm+b)/a;
  cplus  = 0.5*(cp+cm+fabs(cp-cm));
  cminus = 0.5*(cp+cm-fabs(cp-cm));
```
This can be simplified further, by substituting `cp` and `cm` into the below equations and eliminating terms as appropriate. First note that `cp+cm = -b/a` and that `cp-cm = detm/a`. Thus,
```c
  cplus  = 0.5*(-b/a + fabs(detm/a));
  cminus = 0.5*(-b/a - fabs(detm/a));
```
This fulfills the original purpose of the `if` statement in the original code because we have guaranteed that $c_+ \geq c_-$.

This leaves us with an expression that can be much more easily NRPyfied. So, we will rewrite the following in NRPy+, making only minimal changes to be proper Python. However, it turns out that we can make this even simpler. In GRFFE, $v_0^2$ is guaranteed to be exactly one. In GRMHD, this speed was calculated as $$v_{0}^{2} = v_{\rm A}^{2} + c_{\rm s}^{2}\left(1-v_{\rm A}^{2}\right),$$ where the Alfvén speed $v_{\rm A}^{2}$ $$v_{\rm A}^{2} = \frac{b^{2}}{\rho_{b}h + b^{2}}.$$ So, we can see that when the density $\rho_b$ goes to zero, $v_{0}^{2} = v_{\rm A}^{2} = 1$. Then
\begin{align}
a &= (u^0)^2 (1-v_0^2) + v_0^2/\alpha^2 \\
&= 1/\alpha^2 \\
b &= 2 \left(\beta^i v_0^2 / \alpha^2 - (u^0)^2 v^i (1-v_0^2)\right) \\
&= 2 \beta^i / \alpha^2 \\
c &= (u^0)^2 (v^i)^2 (1-v_0^2) - v_0^2 \left(g^{ii} - (\beta^i)^2/\alpha^2\right) \\
&= g^{ii} - (\beta^i)^2/\alpha^2,
\end{align}
are simplifications that should save us some time; we can see that $a \geq 0$ is guaranteed. Note that we also force `detm` to be positive. Thus, `detm/a` is guaranteed to be positive itself, rendering the calls to `nrpyAbs()` superfluous. Furthermore, we eliminate any dependence on the Valencia 3-velocity and the time compoenent of the four-velocity, $u^0$. This leaves us free to solve the quadratic in the familiar way: $$c_\pm = \frac{-b \pm \sqrt{b^2-4ac}}{2a}$$.

```python
# We'll write this as a function so that we can calculate the expressions on-demand for any choice of i
def find_cp_cm(lapse,shifti,gupii):
    # Inputs:  u0,vi,lapse,shift,gammadet,gupii
    # Outputs: cplus,cminus

    # a = 1/(alpha^2)
    a = 1/(lapse*lapse)
    # b = 2 beta^i / alpha^2
    b = 2 * shifti /(lapse*lapse)
    # c = -g^{ii} + (beta^i)^2 / alpha^2
    c = - gupii + shifti*shifti/(lapse*lapse)

    # Now, we are free to solve the quadratic equation as usual. We take care to avoid passing a
    # negative value to the sqrt function.
    detm = b*b - 4*a*c
    detm = sp.sqrt(sp.Rational(1,2)*(detm + nrpyAbs(detm)))
    cplus  = sp.Rational(1,2)*(-b/a + detm/a)
    cminus = sp.Rational(1,2)*(-b/a - detm/a)
    return cplus,cminus

```

We will now write a function in NRPy+ similar to the one used in the old `GiRaFFE`, allowing us to generate the expressions with less need to copy-and-paste code; the key difference is that this one will be in Python, and generate optimized C code integrated into the rest of the operations. Notice that since we eliminated the dependence on velocities, none of the input quantities are different on either side of the face. So, this function won't really do much besides guarantee that `cmax` and `cmin` are positive and negative, respectively, but we'll leave the machinery here since it is likely to be a useful guide to somebody who wants to something similar. We use the same technique as above to replace the `if` statements inherent to the `MAX()` and `MIN()` functions.

```python
# We'll write this as a function, and call it within HLLE_solver, below.
def find_cmax_cmin(flux_dirn,gamma_faceUU,beta_faceU,alpha_face,gammadet_face):
    # Inputs:  flux direction flux_dirn, Inverse metric gamma_faceUU, shift beta_faceU,
    #          lapse alpha_face, metric determinant gammadet_face
    # Outputs: maximum and minimum characteristic speeds cmax and cmin
    # First, we need to find the characteristic speeds on each face
    cpr,cmr = find_cp_cm(alpha_face,beta_faceU[flux_dirn],gamma_faceUU[flux_dirn][flux_dirn])
    cpl,cml = find_cp_cm(alpha_face,beta_faceU[flux_dirn],gamma_faceUU[flux_dirn][flux_dirn])

    # The following algorithms have been verified with random floats:

    # Now, we need to set cmax to the larger of cpr,cpl, and 0
    cmax = sp.Rational(1,2)*(cpr+cpl+nrpyAbs(cpr-cpl))
    cmax = sp.Rational(1,2)*(cmax+nrpyAbs(cmax))

    # And then, set cmin to the smaller of cmr,cml, and 0
    cmin =  sp.Rational(1,2)*(cmr+cml-nrpyAbs(cmr-cml))
    cmin = -sp.Rational(1,2)*(cmin-nrpyAbs(cmin))
    return cmax,cmin

```

## Step 2.e: Compute the HLLE fluxes \[Back to [top](#toc)\]
$$\label{fluxes}$$

Finally, we can compute the flux in each direction. This momentum flux in the $m$ direction is defined as $\alpha \sqrt{\gamma} T^m_{\ \ j}$, based on the input `flux_dirn`. We have already defined $\alpha \sqrt{\gamma}$, so all we need to do is calculate $T^m_{\ \ j}$, where $T^{\mu \nu}_{\rm EM} = b^2 u^\mu u^\nu + \frac{1}{2} b^2 g^{\mu \nu} - b^\mu b^\nu$. In doing this index-lowering operation, recall that $g^{\mu \nu} g_{\nu \alpha} = \delta^\mu_\alpha$. We will do so in accordance with the method published by [Harten, Lax, and von Leer](https://epubs.siam.org/doi/pdf/10.1137/1025002)  and [Einfeldt](https://epubs.siam.org/doi/10.1137/0725021) (hereafter HLLE) to solve the Riemann problem. So, we define $f(u) = T^m_{\ \ j}$ on each face as
$$
f = \alpha \sqrt{\gamma} \left( (\rho+b^2)(u^0 v^m) u_j + (P+\frac{1}{2}b^2) \delta^m_j - b^m b_j \right);
$$
Because $\rho = P = 0$ in GRFFE and $u^0 v^m = u^m$ in general (since $v^m$ is the drift velocity here), this simplifies to
$$
f = \alpha \sqrt{\gamma} \left( b^2 u^m u_j + \frac{1}{2}b^2 \delta^m_j - b^m b_j \right).
$$
We use $j$ to correspond to the component of the flux we are calculating; that is, $j=1$ corresponds to $x$, and so forth (however, remember that in a NRPy+ 3-vector, the numbers will still run from 0 to 2). $\delta^i_j$ is the standard Kronecker delta. We also define `st_x_r` and `st_x_l` (called $u_{\rm R}$ and $u_{\rm L}$ in HLL; we'll not do that, since we're using $u$ for four-velocity):
$$
{\rm st\_j} = \alpha \sqrt{\gamma} \left( (\rho+b^2) u^0 u_j - b^0 b_j \right),
$$
which, in GRFFE, simplifies to
$$
{\rm st\_j} = \alpha \sqrt{\gamma} \left( b^2 u^0 u_j - b^0 b_j \right),
$$

and combine based on eq. 3.15 in the HLLE paper,
$$
{\rm st\_j\_flux} = \frac{c_{\rm min} f_{\rm R} + c_{\rm max} f_{\rm L} - c_{\rm min} c_{\rm max} ({\rm st\_x\_r}-{\rm st\_x\_l})}{c_{\rm min} + c_{\rm max}}
$$

We'll write the HLLE step as a function so that we can loop over `flux_dirn` and `mom_comp` and write each version needed as we need it.

```python
# Note the Kronecker delta symbol in the above; let's create it real quick:
# We'll zero-offset everything here, unlike in the original GiRaFFE.
kronecker_delta = ixp.zerorank2()
for i in range(DIM):
    kronecker_delta[i][i] = 1

# We'll rewrite this assuming that we've passed the entire reconstructed
# gridfunctions. You could also do this with only one point, but then you'd
# need to declare everything as a Cparam in NRPy+

def HLLE_solver(flux_dirn, mom_comp):
    # This solves the Riemann problem for the mom_comp component of the momentum
    # flux StildeD in the flux_dirn direction.

    # There's a caveat here to consider with the indices here: remember the general rule
    # that a latin index must be incremented if it's in a 4-vector.
    # f = alpha sqrt{gamma} ( b^2 u^m u_j + (1/2) b^2 delta^m_j - b^m b_j )
    Fr = alpha_sqrt_gamma * (smallb2_r * u_rU[flux_dirn] * u_rD[mom_comp]
                             + smallb2_r * kronecker_delta[flux_dirn][mom_comp] / 2.0
                             - smallb4_rU[flux_dirn+1] * smallb4_rD[mom_comp+1])

    # f = alpha sqrt{gamma} ( b^2 u^m u_j + (1/2) b^2 delta^m_j - b^m b_j )
    Fl = alpha_sqrt_gamma * (smallb2_l * u_lU[flux_dirn] * u_lD[mom_comp]
                             + smallb2_l * kronecker_delta[flux_dirn][mom_comp] / 2.0
                             - smallb4_lU[flux_dirn+1] * smallb4_lD[mom_comp+1])

    # st_j = alpha sqrt{gamma} ( b^2 u^0 u_j - b^0 b_j )
    st_j_r = alpha_sqrt_gamma * (smallb2_r*u4upperZero_r*u_rD[mom_comp] - smallb4_rU[0]*smallb4_rD[mom_comp+1])
    st_j_l = alpha_sqrt_gamma * (smallb2_l*u4upperZero_l*u_lD[mom_comp] - smallb4_lU[0]*smallb4_lD[mom_comp+1])

    cmaxL,cminL = find_cmax_cmin(flux_dirn,gamma_faceUU,beta_faceU,alpha_face,gammadet_face)

    # st_j_flux = (c_\min f_R + c_\max f_L - c_\min c_\max ( st_j_r - st_j_l )) / (c_\min + c_\max)
    return (cminL*Fr + cmaxL*Fl - cminL*cmaxL*(st_j_r-st_j_l) )/(cmaxL + cminL)

```

Finally, now that we have the equations written, we can write out the files. After writing a brief post string (to close the function), we will loop over `flux_dirn`, creating one C file for each flux direction. In each file, we will include the math to calculate each momentum-flux component `mom_comp` in that direction.

We have written the function `HLLE_solver()` so that we can easily compute the flux as specified by those two indices.

```python
def calculate_Stilde_flux(flux_dirn,Stilde_fluxD):
    for mom_comp in range(DIM):
        Stilde_fluxD[mom_comp] = HLLE_solver(flux_dirn,mom_comp)
    return Stilde_fluxD

```

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

Here, as a code validation check, we verify agreement in the SymPy expressions for the $\texttt{GiRaFFE}$ evolution equations and auxiliary quantities we intend to use between
1. this tutorial and
2. the NRPy+ [GiRaFFE_HO.GiRaFFE_NRPy_Ccode_library_Stilde_flux](../edit/GiRaFFE_HO/GiRaFFE_NRPy_Ccode_library_Stilde_flux.py) module.

```python
# Reset the list of gridfunctions, as registering a gridfunction
#   twice will spawn an error.
gri.glb_gridfcs_list = []

import GiRaFFE_HO.Stilde_flux as stflux

print("Consistency check between GiRaFFE_Higher_Order tutorial and NRPy+ module: ALL SHOULD BE ZERO.")

for i in range(DIM):
    print("Checking the flux in direction "+str(i))
    Stilde_fluxD = calculate_Stilde_flux(i,Stilde_fluxD)
    gri.glb_gridfcs_list = []
    stflux.calculate_Stilde_flux(i,False)
    for j in range(DIM):
        print("Stilde_fluxD["+str(j)+"] - stflux.Stilde_fluxD["+str(j)+"] = "\
              + str(Stilde_fluxD[j] - stflux.Stilde_fluxD[j]))

# Stilde_fluxD = calculate_Stilde_flux(i,Stilde_fluxD)
# print("u4upperZero_r - stflux.u4upperZero_r = "\
#               + str(u4upperZero_r - stflux.u4upperZero_r))

# for j in range(DIM):
#     print("Valenciav_rU["+str(j)+"] - stflux.Valenciav_rU["+str(j)+"] = "\
#           + str(Valenciav_rU[j] - stflux.Valenciav_rU[j]))
```

    Consistency check between GiRaFFE_Higher_Order tutorial and NRPy+ module: ALL SHOULD BE ZERO.
    Checking the flux in direction 0
    Stilde_fluxD[0] - stflux.Stilde_fluxD[0] = 0
    Stilde_fluxD[1] - stflux.Stilde_fluxD[1] = 0
    Stilde_fluxD[2] - stflux.Stilde_fluxD[2] = 0
    Checking the flux in direction 1
    Stilde_fluxD[0] - stflux.Stilde_fluxD[0] = 0
    Stilde_fluxD[1] - stflux.Stilde_fluxD[1] = 0
    Stilde_fluxD[2] - stflux.Stilde_fluxD[2] = 0
    Checking the flux in direction 2
    Stilde_fluxD[0] - stflux.Stilde_fluxD[0] = 0
    Stilde_fluxD[1] - stflux.Stilde_fluxD[1] = 0
    Stilde_fluxD[2] - stflux.Stilde_fluxD[2] = 0

# Step 4: Complete Derivation of the Wave Speeds \[Back to [top](#toc)\]
$$\label{derive_speed}$$

This computes phase speeds in the direction given by flux_dirn. Note that we replace the full dispersion relation with a simpler one, which overestimates the maximum speeds by a factor of ~2. See full discussion around Eqs. 49 and 50 in [Duez, et al.](http://arxiv.org/pdf/astro-ph/0503420.pdf). In summary, we solve the dispersion relation (in, e.g., the $x$-direction) with a wave vector of $k_\mu = (-\omega,k_x,0,0)$. So, we solve the approximate dispersion relation $\omega_{\rm cm}^2 = [v_A^2 + c_s^2 (1-v_A^2)]k_{\rm cm}^2$ for the wave speed $\omega/k_x$, where the sound speed $c_s = \sqrt{\Gamma P/(h \rho_0)}$, the Alfvén speed $v_A = 1$ (in GRFFE), $\omega_{\rm cm} = -k_\mu k^\mu$ is the frequency in the comoving frame, $k_{\rm cm}^2 = K_\mu K^\mu$ is the wavenumber squared in the comoving frame, and $K_\mu = (g_{\mu\nu} + u_\mu u_\nu)k^\nu$ is the part of the wave vector normal to the four-velocity $u^\mu$. See below for a complete derivation.

What follows is a complete derivation of the quadratic we solve.
\begin{align}
w_{\rm cm} &= (-k_0 u^0 - k_x u^x) \\
k_{\rm cm}^2 &= K_{\mu} K^{\mu}, \\
K_{\mu} K^{\mu} &= (g_{\mu a} + u_{\mu} u_a) k^a  g^{\mu b} [ (g_{c b} + u_c u_b) k^c ] \\
\rightarrow g^{\mu b} (g_{c b} + u_{c} u_{b}) k^c &= (\delta^{\mu}_c + u_c u^{\mu} ) k^c \\
                 &= (g_{\mu a} + u_{\mu} u_a) k^a  (\delta^{\mu}_c + u_c u^{\mu} ) k^c \\
                 &=[(g_{\mu a} + u_{\mu} u_a) \delta^{\mu}_c + (g_{\mu a} + u_{\mu} u_a) u_c u^{\mu} ] k^c k^a \\
                 &=[(g_{c a} + u_c u_a) + (u_c u_a -  u_a u_c] k^c k^a \\
                 &=(g_{c a} + u_c u_a) k^c k^a \\
                 &= k_a k^a + u^c u^a k_c k_a \\
k^a = g^{\mu a} k_{\mu} &= g^{0 a} k_0 + g^{x a} k_x \\
k_a k^a &= k_0 g^{0 0} k_0 + k_x k_0 g^{0 x} + g^{x 0} k_0 k_x + g^{x x} k_x k_x \\
         &= g^{00} (k_0)^2 + 2 g^{x0} k_0 k_x + g^{xx} (k_x)^2 \\
u^c u^a k_c k_a &= (u^0 k_0 + u^x k_x) (u^0 k_0 + u^x k_x) = (u^0 k_0)^2 + 2 u^x k_x u^0 k_0 + (u^x k_x)^2 \\
(k_0 u0)^2  + 2 k_x u^x k_0 u^0 + (k_x u^x)^2 &= v_0^2 [ (u^0 k_0)^2 + 2 u^x k_x u^0 k_0 + (u^x k_x)^2 + g^{00} (k_0)^2 + 2 g^{x0} k_0 k_x + g^{xx} (k_x)^2] \\
(1-v_0^2) (u^0 k_0 + u^x k_x)^2 &= v_0^2 (g^{00} (k_0)^2 + 2 g^{x0} k_0 k_x + g^{xx} (k_x)^2) \\
(1-v_0^2) (u^0 k_0/k_x + u^x)^2 &= v_0^2 (g^{00} (k_0/k_x)^2 + 2 g^{x0} k_0/k_x + g^{xx}) \\
(1-v_0^2) (u^0 X + u^x)^2 &= v_0^2 (g^{00} X^2 + 2 g^{x0} X + g^{xx}) \\
(1-v_0^2) ((u^0)^2 X^2 + 2 u^x (u^0) X + (u^x)^2) &= v_0^2 (g^{00} X^2 + 2 g^{x0} X + g^{xx}) \\
&X^2 ( (1-v_0^2) (u^0)^2 - v_0^2 g^{00}) + X (2 u^x u^0 (1-v_0^2) - 2 v_0^2 g^{x0}) + (1-v_0^2) (u^x)^2 - v_0^2 g^{xx} \\
a &= (1-v_0^2) (u^0)^2 - v_0^2 g^{00} = (1-v_0^2) (u^0)^2 + v_0^2/\alpha^2 \leftarrow {\rm VERIFIED} \\
b &= 2 u^x u^0 (1-v_0^2) - 2 v_0^2 \beta^x/\alpha^2 \leftarrow {\rm VERIFIED,\ } X\rightarrow -X, {\rm because\ } X = -w/k_1, {\rm \ and\ we\ are\ solving\ for} -X. \\
c &= (1-v_0^2) (u^x)^2 - v_0^2 (g^{xx}\psi^{-4} - (\beta^x/\alpha)^2) \leftarrow {\rm VERIFIED} \\
v_0^2 &= v_A^2 + c_s^2 (1 - v_A^2) \\
\end{align}

# 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-GiRaFFE_HO_Ccode_library-Stilde-flux.pdf](Tutorial-GiRaFFE_HO_Ccode_library-Stilde-flux.pdf) (Note that clicking on this link may not work; you may need to open the PDF file through another means.)

```python
!jupyter nbconvert --to latex --template latex_nrpy_style.tplx --log-level='WARN' Tutorial-GiRaFFE_NRPy_Ccode_library-Stilde-flux.ipynb
!pdflatex -interaction=batchmode Tutorial-GiRaFFE_NRPy_Ccode_library-Stilde-flux.tex
!pdflatex -interaction=batchmode Tutorial-GiRaFFE_NRPy_Ccode_library-Stilde-flux.tex
!pdflatex -interaction=batchmode Tutorial-GiRaFFE_NRPy_Ccode_library-Stilde-flux.tex
!rm -f Tut*.out Tut*.aux Tut*.log
```

    [NbConvertApp] Converting notebook Tutorial-GiRaFFE_NRPy_Ccode_library-Stilde-flux.ipynb to latex
    [NbConvertApp] Writing 19720600 bytes to Tutorial-GiRaFFE_NRPy_Ccode_library-Stilde-flux.tex
    This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/Debian) (preloaded format=pdflatex)
     restricted \write18 enabled.
    entering extended mode
    This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/Debian) (preloaded format=pdflatex)
     restricted \write18 enabled.
    entering extended mode
    This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/Debian) (preloaded format=pdflatex)
     restricted \write18 enabled.
    entering extended mode


[@ggruszczynski](https://github.com/ggruszczynski)

```python
from sympy import fourier_transform, exp, sqrt, pi, cos, simplify
from sympy.abc import x, k, t, symbols
from sympy import init_printing
init_printing(use_unicode=False, wrap_line=False)

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

Diffusion part 2: matrix approach; implicit and explicit scheme
-----
***

The one-dimensional diffusion equation is:

$$\frac{\partial u}{\partial t}= \nu \frac{\partial^2 u}{\partial x^2}$$

The first thing you should notice is that —unlike the previous two simple equations we have studied— this equation has a second-order derivative. We first need to learn what to do with it!

### Discretizing $\frac{\partial ^2 u}{\partial x^2}$

The second-order derivative can be represented geometrically as the line tangent to the curve given by the first derivative.  We will discretize the second-order derivative with a Central Difference scheme: a combination of Forward Difference and Backward Difference of the first derivative.  Consider the Taylor expansion of $u_{i+1}$ and $u_{i-1}$ around $u_i$:

$u_{i+1} = u_i + \Delta x \frac{\partial u}{\partial x}\bigg|_i + \frac{\Delta x^2}{2} \frac{\partial ^2 u}{\partial x^2}\bigg|_i + \frac{\Delta x^3}{3!} \frac{\partial ^3 u}{\partial x^3}\bigg|_i + O(\Delta x^4)$

$u_{i-1} = u_i - \Delta x \frac{\partial u}{\partial x}\bigg|_i + \frac{\Delta x^2}{2} \frac{\partial ^2 u}{\partial x^2}\bigg|_i - \frac{\Delta x^3}{3!} \frac{\partial ^3 u}{\partial x^3}\bigg|_i + O(\Delta x^4)$

If we add these two expansions, you can see that the odd-numbered derivative terms will cancel each other out.  If we neglect any terms of $O(\Delta x^4)$ or higher (and really, those are very small), then we can rearrange the sum of these two expansions to solve for our second-derivative.

$u_{i+1} + u_{i-1} = 2u_i+\Delta x^2 \frac{\partial ^2 u}{\partial x^2}\bigg|_i + O(\Delta x^4)$

Then rearrange to solve for $\frac{\partial ^2 u}{\partial x^2}\bigg|_i$ and the result is:

$$\frac{\partial ^2 u}{\partial x^2}=\frac{u_{i+1}-2u_{i}+u_{i-1}}{\Delta x^2} + O(\Delta x^4)$$

### Back to Step 3

We can now write the discretized version of the diffusion equation in 1D:

$$\frac{u_{i}^{n+1}-u_{i}^{n}}{\Delta t}=\nu\frac{u_{i+1}^{n}-2u_{i}^{n}+u_{i-1}^{n}}{\Delta x^2}$$

As before, we notice that once we have an initial condition, the only unknown is $u_{i}^{n+1}$, so we re-arrange the equation solving for our unknown:

$$u_{i}^{n+1}=u_{i}^{n}+\underbrace{\frac{\nu\Delta t}{\Delta x^2}}_{\beta}(u_{i+1}^{n}-2u_{i}^{n}+u_{i-1}^{n})$$

$$u_{i}^{n+1}=\beta u_{i-1}^{n} + u_{i}^{n}(1- 2 \beta) +\beta u_{i+1}^{n}$$

The above discrete equation allows us to write a program to advance a solution in time. But we need an initial condition. Let's continue using our favorite: the hat function. So, at $t=0$, $u=2$ in the interval $0.5\le x\le 1$ and $u=1$ everywhere else. We are ready to number-crunch!

```python
# %matplotlib inline

nx = 128
domain_length = 64
dx = domain_length / (nx-1)
xspace = np.linspace(0, domain_length, nx)

nt = 200                # the number of timesteps we want to calculate
nu = 5                  # the value of viscosity
sigma = .2              # sigma is a parameter, we'll learn more about it later
dt = sigma * dx**2 / nu # dt is defined using sigma ... more later!

u_IC = 0*np.ones(nx)                 # numpy function ones()
u_IC[int((nx-1)/4):int(nx/2 + 1)] = 1 # setting u = 2 between 0.5 and 1 as per our I.C.s

plt.plot(xspace, u_IC)
```

# Matrix approach

## Explicit, central FD - matrix approach

Notice, that the scheme

$$u_{i}^{n+1}=\beta u_{i-1}^{n} + u_{i}^{n}(1- 2 \beta) +\beta u_{i+1}^{n}$$

where $\beta = \frac{\nu\Delta t}{\Delta x^2}$ can be formulated as:

$$
\boldsymbol{u}^{n+1} = \mathbb{A} \boldsymbol{u}^n
$$

Observe, that $ \mathbb{A} $ have a tridiagonal structure:

$$
\begin{bmatrix}
A_{0,0} & A_{0,1} & A_{0,2} & \\
\beta  & 1 - 2 \beta& \beta \\
0 & \beta  & 1- 2 \beta & \beta \\
0 & 0 & \beta & 1- 2 \beta & \beta \\
... \\
... \\
... \\
0 & 0 & 0 & 0 & 0 & \beta & 1- 2 \beta & \beta \\
0 & 0 & 0 & 0 & 0 & A_{n,n-2} & A_{n,n-1}  & A_{n,n}\\
\end{bmatrix}
\quad
$$

**Hint:**

Fill the corners of the matrix using asymmetric stencils:

* forward FD for $A_{0,0}, A_{0,1}$ and $A_{0,2}$

$$\frac{u_{i}^{n+1}-u_{i}^{n}}{\Delta t}=\nu\frac{u_i^{n}  -2u_{i+1}^{n} + u_{i+2}^{n}}{\Delta x^2}$$

$$u_{i}^{n+1} = (1 + \beta ) u_{i}^{n} - 2\beta u_{i+1}^{n} + \beta u_{i+2}^{n} $$

* backward FD for $A_{n,n}, A_{n,n-1}$ and $A_{n,n-2}$

$$\frac{u_{i}^{n+1}-u_{i}^{n}}{\Delta t}=\nu\frac{u_i^{n}  -2u_{i-1}^{n} + u_{i-2}^{n}}{\Delta x^2}$$

$$u_{i}^{n+1} = (1 + \beta ) u_{i}^{n} - 2\beta u_{i-1}^{n} + \beta u_{i-2}^{n} $$

```python
# explicit central FD
import numpy as np
np.set_printoptions(precision=3, suppress=True)

un_ecfd = u_IC.copy()

A = np.zeros((nx, nx))

Beta_FD = dt * nu / (dx**2)
last_index_in_matrix = nx -1

# the BC  - use one sided FD
A[0, 0] = 1+Beta_FD     # forward FD
A[0, 1] = -2*Beta_FD     # forward FD
A[0, 2] = Beta_FD      # forward FD
A[last_index_in_matrix, last_index_in_matrix-2] = Beta_FD   # backward FD
A[last_index_in_matrix, last_index_in_matrix-1] = -2*Beta_FD  # backward FD
A[last_index_in_matrix, last_index_in_matrix] =  1+Beta_FD   # backward FD
for i in range(1, last_index_in_matrix):
    A[i, i-1] = Beta_FD        # left of the diagonal
    A[i, i] = 1 - 2*Beta_FD     # the diagonal
    A[i, i+1] = Beta_FD         # right of the diagonal

# print(f"determinant A: {np.linalg.det(A)}")
# w, v = np.linalg.eig(A)  # calculate the eigenvalues and eigenvectors
# plt.plot(np.arange(0, len(w)), w)  # plot the eigenvalues

for n in range(nt):  #loop for values of n from 0 to nt, so it will run nt times
    un_ecfd = A@un_ecfd
    # un_ecfd = np.dot(A,un_ecfd) # alternative way of doing the same

plt.plot(xspace, un_ecfd)

```

## Implicit, central FD

The laplace operator is calculated using values from the future ($u^{n+1}$).

$$\frac{u_{i}^{n+1}-u_{i}^{n}}{\Delta t}=\nu\frac{u_{i+1}^{n+1}-2u_{i}^{n+1}+u_{i-1}^{n+1}}{\Delta x^2}$$

$$u_{i}^{n+1}=u_{i}^{n}+\underbrace{\frac{\nu\Delta t}{\Delta x^2}}_{\beta}(u_{i+1}^{n+1}-2u_{i}^{n+1}+u_{i-1}^{n+1})$$

$$
-\beta u_{i-1}^{n+1} + (1+2 \beta)u_{i}^{n+1} - \beta u_{i+1}^{n+1} = u_{i}^{n}
$$

Notice, that the scheme can be formulated as:

$$
 \mathbb{A}  \boldsymbol{u}^{n+1} =\boldsymbol{u}^n
$$

Observe, that $ \mathbb{A} $ have a tridiagonal structure:

$$
\begin{bmatrix}
A_{0,0} & A_{0,1} & A_{0,2} & \\
-\beta  & 1+2 \beta & -\beta \\
0 & -\beta  & 1+2 \beta & -\beta \\
0 & 0 &- \beta & 1+2 \beta & -\beta \\
... \\
... \\
... \\
0 & 0 & 0 & 0 & 0 & -\beta & 1+2 \beta & -\beta \\
0 & 0 & 0 & 0 & 0 & A_{n,n-2} & A_{n,n-1}  & A_{n,n}\\
\end{bmatrix}
\quad
$$

**Hint:**

Fill the corners of the matrix using asymmetric stencils:
* forward FD for $A_{0,0}, A_{0,1}$ and $A_{0,2}$

$$\frac{u_{i}^{n+1}-u_{i}^{n}}{\Delta t}=\nu\frac{u_i^{n+1}  -2u_{i+1}^{n+1} + u_{i+2}^{n+1}}{\Delta x^2}$$

$$u_{i}^{n+1} (1-  \underbrace{\nu\frac{\Delta}{\Delta x^2}}_{\beta} ) + 2 \beta u_{i+1}^{n+1} - \beta u_{i+2}^{n+1} = u_{i}^{n} $$

* backward FD for $A_{n,n}, A_{n,n-1}$ and $A_{n,n-2}$

$$\frac{u_{i}^{n+1}-u_{i}^{n}}{\Delta t}=\nu\frac{u_i^{n+1}  -2u_{i-1}^{n+1} + u_{i-2}^{n+1}}{\Delta x^2}$$

$$u_{i}^{n+1} (1-  \underbrace{\nu\frac{\Delta}{\Delta x^2}}_{\beta} ) + 2 \beta u_{i-1}^{n+1} - \beta u_{i-2}^{n+1} = u_{i}^{n} $$

```python
# implicit central FD
import numpy as np
np.set_printoptions(precision=3, suppress=True)

un_icfd = u_IC.copy()
A = np.zeros((nx, nx))

Beta_FD = dt * nu / (dx**2)
# nt += 100
last_index_in_matrix = nx -1
# the BC  - use one sided FD
A[0, 0] = 1-Beta_FD     # forward FD
A[0, 1] = 2*Beta_FD     # forward FD
A[0, 2] = -Beta_FD      # forward FD
A[last_index_in_matrix, last_index_in_matrix-2] = -Beta_FD   # backward FD
A[last_index_in_matrix, last_index_in_matrix-1] = 2*Beta_FD  # backward FD
A[last_index_in_matrix, last_index_in_matrix] =  1-Beta_FD   # backward FD
for i in range(1, last_index_in_matrix):
    A[i, i-1] = -Beta_FD        # left of the diagonal
    A[i, i] = 1 + 2*Beta_FD     # the diagonal
    A[i, i+1] = -Beta_FD         # right of the diagonal

A_inv = np.linalg.inv(A)

# print(f"determinant A_inv: {np.linalg.det(A_inv)}")
# w, v = np.linalg.eig(A_inv)
# plt.plot(np.arange(0, len(w)), w) # plot the eigenvalues

for n in range(nt):  #loop for values of n from 0 to nt, so it will run nt times
    un_icfd = A_inv@un_icfd
    # alternative way of doing the same:
    #     un_icfd = np.dot(A_inv,un_icfd)
    #     b = un_icfd.copy()
    #     un_icfd = np.linalg.solve(A, b) # u(t+1)

plt.plot(xspace, un_icfd)
# A[-4:,-4:]

```