# Tutorial-IllinoisGRMHD: compute_tau_rhs_extrinsic_curvature_terms_and_TUPMUNU.C

## Authors: Leo Werneck & Zach Etienne

**This module is currently under development**

## In this tutorial module we explain how we evaluate $\partial_{t}\tilde{\tau}$, terms that depend on the extrinsic curvature, $K_{ij}$, and the energy-momentum tensor $T^{\mu\nu}$

### Required and recommended citations:

* **(Required)** Etienne, Z. B., Paschalidis, V., Haas R., Mösta P., and Shapiro, S. L. IllinoisGRMHD: an open-source, user-friendly GRMHD code for dynamical spacetimes. Class. Quantum Grav. 32 (2015) 175009. ([arxiv:1501.07276](http://arxiv.org/abs/1501.07276)).
* **(Required)** Noble, S. C., Gammie, C. F., McKinney, J. C., Del Zanna, L. Primitive Variable Solvers for Conservative General Relativistic Magnetohydrodynamics. Astrophysical Journal, 641, 626 (2006) ([astro-ph/0512420](https://arxiv.org/abs/astro-ph/0512420)).
* **(Recommended)** Del Zanna, L., Bucciantini N., Londrillo, P. An efficient shock-capturing central-type scheme for multidimensional relativistic flows - II. Magnetohydrodynamics. A&A 400 (2) 397-413 (2003). DOI: 10.1051/0004-6361:20021641 ([astro-ph/0210618](https://arxiv.org/abs/astro-ph/0210618)).

If using the version of `IllinoisGRMHD` with piecewise polytropic *or* tabulated (coming soon!) EOS support, then the following citation is also required:

* **(Required)** Etienne, Z. B., Werneck, L., Paschalidis, V., Haas R., Mösta P., and Shapiro, S. L., *IllinoisGRMHD github repository* (2019). Source Code URL: https://github.com/zachetienne/nrpytutorial/tree/master/IllinoisGRMHD/.

### Dependencies

The files generated in this tutorial notebook depend on the following files:

*  [GRHD/equations NRPy+ module](/edit/NRPyIGM/GRHD/equations.py) \[[**tutorial**](/notebooks/NRPyIGM/Tutorial-GRHD_Equations-Cartesian.ipynb)\]
*  [GRFFE/equations NRPy+ module](/edit/NRPyIGM/GRFFE/equations.py) \[[**tutorial**](/notebooks/NRPyIGM/Tutorial-GRFFE_Equations-Cartesian.ipynb)\]
*  [GRMHD/equations NRPy+ module](/edit/NRPyIGM/GRMHD/equations.py) \[[**tutorial**](/notebooks/NRPyIGM/Tutorial-GRMHD_Equations-Cartesian.ipynb)\]
* `IllinoisGRMHD_EoS_lowlevel_functs.C` \[[**tutorial**](Tutorial-IllinoisGRMHD__EoS_lowlevel_functs.ipynb)\]

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

This module is organized as follows

0. [Step 0](#src_dir): **Source directory creation**
1. [Step 1](#compute_extrinsic_curvature_terms_using_nrpy): **Computing the extrinsic curvature terms in $\partial_{t}\tilde{\tau}$ RHS using NRPy+**
    1. [Step 1.a](#load_python_nrpy_modules): *Load necessary Python/NRPy+ modules*
    1. [Step 1.b](#adm_3metric_from_conf_metric): *Compute ADM 3-metric quantities from conformal metric*
    1. [Step 1.c](#alpha_beta_gammadn_from_conf_metric_header): *The `ADM_3METRIC_in_terms_of_CONF_METRIC.h` file*
    1. [Step 1.d](#setting_primitives): *Setting primitive variables*
    1. [Step 1.e](#setting_eos_quantities): *Setting EOS quantities*
    1. [Step 1.f](#compute_smallb4_and_smallbsquared): *The `compute_smallb4U_and_smallbsquared_from_ADM_3METRIC_and_PRIMS.h` file*
    1. [Step 1.g](#grmhd_tupmunu): *Computing $T^{\mu\nu}_{\rm GRMHD}$*
    1. [Step 1.h](#compute_tau_rhs_extrinsic_curvature_header): *The `compute_tau_rhs_extrinsic_curvature_terms_and_TUPmunu.h` file*
1. [Step 2](#compute_tau_rhs_extrinsic_curvature_terms_and_tupmunu__c): **`compute_tau_rhs_extrinsic_curvature_terms_and_TUPMUNU.C`**
1. [Step 3](#code_validation): **Code validation**
1. [Step 4](#latex_pdf_output): **Output this notebook to $\LaTeX$-formatted PDF file**

# Step 0: Source directory creation \[Back to [top](#toc)\]
$$\label{src_dir}$$

We will now use the [cmdline_helper.py NRPy+ module](Tutorial-Tutorial-cmdline_helper.ipynb) to create the source directory within the `IllinoisGRMHD` NRPy+ directory, if it does not exist yet.

```python
# Step 0: Creation of the IllinoisGRMHD source directory
# Step 0a: 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)

# Step 0b: Load up cmdline_helper and create the directory
import cmdline_helper as cmd
outdir = os.path.join("..","src")
cmd.mkdir(outdir)
NRPy_headers_dir_path = os.path.join(outdir,"NRPy_generated_headers")
cmd.mkdir(NRPy_headers_dir_path)
```

# Step 1: Computing the extrinsic curvature terms in $\partial_{t}\tilde{\tau}$ RHS using NRPy+ \[Back to [top](#toc)\]
$$\label{compute_extrinsic_curvature_terms_using_nrpy}$$

$$
\partial_{t}\tilde\tau = \left[\rm Flux\ term\right] + s\ ,
$$

where

$$
s = \underbrace{\alpha\sqrt{\gamma}\left(T^{00}\beta^{i}\beta^{j} + 2T^{0i}\beta^{j} + T^{ij}\right)K_{ij}}_{\text{Extrinsic curvature terms}} - \underbrace{\left(T^{00}\beta^{i} + T^{0i}\right)\partial_{i}\alpha}_{\text{Will be implemented later}}\ .
$$

To compute the extrinsic curvature terms, we will be using the following NRPy+ modules:

* [GRHD.equations NRPy+ module](/edit/NRPyIGM/GRHD/equations.py): used to compute $u^{\mu}$ and the extrinsic curvature terms in $s$.
* [GRFFE.equations NRPy+ module](/edit/NRPyIGM/GRFFE/equations.py): used to compute $b^{\mu}$ and $b^{2}\equiv b^{\mu}b_{\mu}$.
* [GRMHD.equations NRPy+ module](/edit/NRPyIGM/GRMHD/equations.py): used to compute $T^{\mu\nu}_{\rm GRMHD} = T^{\mu\nu}_{\rm GRHD} + T^{\mu\nu}_{\rm GRFFE}$.

## Step 1.a: Load necessary Python/NRPy+ modules \[Back to [top](#toc)\]
$$\label{load_python_nrpy_modules}$$

We now load all necessary Python/NRPy+ modules needed to perform the calculation of $s$.

```python
# Imported needed Python modules
import time        # Python module: we will use the time() function to estimate code execution time
import sympy as sp # Python module: symbolic expressions capabilities

# Imported needed NRPy+ modules
from outputC import *                           # NRPy+ module: C code output routines
import indexedexp as ixp                        # NRPy+ module: funcionality to work with vectors/tensors
import IllinoisGRMHD_output_functions as IGMout # NRPy+ module: IllinoisGRMHD output file functions
import GRHD.equations  as GRHD                  # NRPy+ module: Collection GRHD related functions
import GRFFE.equations as GRFFE                 # NRPy+ module: Collection GRFFE related functions
import GRMHD.equations as GRMHD                 # NRPy+ module: Collection GRMHD related functions
```

## Step 1.b: Compute ADM 3-metric quantities from conformal metric \[Back to [top](#toc)\]
$$\label{adm_3metric_from_conf_metric}$$

We now compute the ADM 3 metric quantities $\left(\alpha,\beta^{i},\gamma_{ij},\gamma^{ij},\sqrt{\gamma}\right)$ from the conformal metric quantities $\left(\alpha-1,\beta^{i},\bar\gamma_{ij},\bar\gamma^{ij},\phi,\psi\right)$.

Setting $\alpha$ and $\beta^{i}$ is straightfoward. For $\gamma_{ij}$, $\gamma^{ij}$, and $\sqrt{\gamma}$ we have

\begin{align}
\gamma_{ij}   &= \psi^{4}\bar\gamma_{ij} \ ,\\
\gamma^{ij}   &= \psi^{-4}\bar\gamma^{ij}\ ,\\
\sqrt{\gamma} &= \psi^{6}\ ,
\end{align}

where the last equality holds true because $\sqrt{\bar\gamma}=1$.

```python
# Set DIM = 3
DIM = 3

# Set up alpha in terms of conformal metric
alpha_ito_CONF_METRIC = sp.Symbol("CONF_METRIC[CM_LAPM1]",real=True)+1.0

# Set up beta^{i} in terms of conformal metric
beta_ito_CONF_METRIC_U = ixp.zerorank1()
for i in range(DIM):
    beta_ito_CONF_METRIC_U[i] = sp.Symbol("CONF_METRIC[CM_SHIFT"+chr(ord('X')+i)+"]",real=True)

# Set up psi^{4}, psi^{-4}, and psi^{6}
psi4  = sp.Symbol("CONF_METRIC[CM_PSI]",real=True)**4
psim4 = 1/psi4
psi6  = sp.Symbol("CONF_METRIC[CM_PSI]",real=True)**6

# Set up gamma_{ij} and gamma^{ij} in terms of conformal metric
gamma_ito_CONF_METRIC_DD = ixp.zerorank2()
gamma_ito_CONF_METRIC_UU = ixp.zerorank2()
for i in range(DIM):
    for j in range(i,DIM):
        gamma_ito_CONF_METRIC_DD[i][j] = gamma_ito_CONF_METRIC_DD[j][i] = psi4  * sp.Symbol("CONF_METRIC[CM_GAMMATILDE"+chr(ord('X')+i)+chr(ord('X')+j)+"]",real=True)
        gamma_ito_CONF_METRIC_UU[i][j] = gamma_ito_CONF_METRIC_UU[j][i] = psim4 * sp.Symbol("CONF_METRIC[CM_GAMMATILDEUP"+chr(ord('X')+i)+chr(ord('X')+j)+"]",real=True)

# Set up sqrtgamma
sqrtgamma_ito_CONF_METRIC = psi6
```

## Step 1.c: The `ADM_3METRIC_in_terms_of_CONF_METRIC.h` file \[Back to [top](#toc)\]
$$\label{alpha_beta_gammadn_from_conf_metric_header}$$

We now generate the `ADM_3METRIC_in_terms_of_CONF_METRIC.h` file, which allows `IllinoisGRMHD` to populate the `ADM_3METRIC` array in terms of the `CONF_METRIC` array. The structure of the following code cell is the following:

1. We start by setting up variables that point to the `ADM_3METRIC` array.
1. We then set up a list of expressions and variables, to be converted into C code.
1. We then convert these lists to C code using the `outputC()` function from the [outputC NRPy+ module](/edit/NRPyIGM/outputC.py).
1. Finally, we output the resulting string to a `IllinoisGRMHD` header file using the `NRPy_IGM_write_to_file()` function from the [IllinoisGRMHD_output_functions NRPy+ module](/edit/NRPyIGM/IllinoisGRMHD/doc/IllinoisGRMHD_output_functions.py).

```python
# Declare basic ADM variables to be used by IllinoisGRMHD
# Set spatial dimension to 3

# Set up alpha
ADMalpha  = sp.Symbol("ADM_3METRIC[ALPHA]",real=True)
# Set up beta^{i}
ADMbetaU   = ixp.zerorank1()
for i in range(DIM):
    ADMbetaU[i] = sp.Symbol('ADM_3METRIC[BETA'+chr(ord('X')+i)+"]",real=True)

# Set up gamma_{ij}
ADMgammaDD = ixp.zerorank2()
for i in range(DIM):
    for j in range(i,DIM):
        ADMgammaDD[i][j] = ADMgammaDD[j][i] = sp.Symbol('ADM_3METRIC[GAMMA'+chr(ord('X')+i)+chr(ord('X')+j)+"]",real=True)

# Set up gamma^{ij}
ADMgammaUU = ixp.zerorank2()
for i in range(3):
    for j in range(i,3):
        ADMgammaUU[i][j] = ADMgammaUU[j][i] = sp.Symbol('ADM_3METRIC[GAMMAUP'+chr(ord('X')+i)+chr(ord('X')+j)+"]",real=True)

# Set up sqrt(gamma)
ADMsqrtgamma = sp.Symbol("ADM_3METRIC[SQRTGAMMA]",real=True)

# Set up expressions and variables lists
exprlist = [alpha_ito_CONF_METRIC]
varslist = [ADMalpha]
for i in range(DIM):
    exprlist.append(beta_ito_CONF_METRIC_U[i])
    varslist.append(ADMbetaU[i])
for i in range(DIM):
    for j in range(i,DIM):
        exprlist.append(gamma_ito_CONF_METRIC_DD[i][j])
        varslist.append(ADMgammaDD[i][j])
for i in range(DIM):
    for j in range(i,DIM):
        exprlist.append(gamma_ito_CONF_METRIC_UU[i][j])
        varslist.append(ADMgammaUU[i][j])
exprlist.append(sqrtgamma_ito_CONF_METRIC)
varslist.append(ADMsqrtgamma)

string = outputC(exprlist,varslist,"returnstring", params="outCverbose=False")

# Output to file
filename = "ADM_3METRIC_in_terms_of_CONF_METRIC.h"
filepath = os.path.join(NRPy_headers_dir_path,filename)
IGMout.NRPy_IGM_write_to_file(filepath,filename,string)
```

    Just generated the file: ../src/NRPy_generated_headers/ADM_3METRIC_in_terms_of_CONF_METRIC.h

## Step 1.d: Setting primitive variables \[Back to [top](#toc)\]
$$\label{setting_primitives}$$

At this stage in the calculation, we will have access to the `PRIMS` array, which is an array of pointers to the gridfunctions `prims`. Thus, we will set up our primitive variables $\left(\rho_{b},P,v^{i}B^{i}\right)$ using `PRIMS`. We will also set up the constant `sqrt4pi` which, as the name suggests, will be set to $\sqrt{4\pi}$ in the C code.

```python
# Set up primitives
# Set rho and P
rhob, P = sp.symbols("PRIMS[RHOB] PRIMS[PRESSURE]",real=True)

# Set v^{i} and B^{i}
vU         = ixp.zerorank1()
B_notildeU = ixp.zerorank1()
for i in range(DIM):
    vU[i]         = sp.Symbol("PRIMS[V"+chr(ord('X')+i)+"]",real=True)
    B_notildeU[i] = sp.Symbol("PRIMS[B"+chr(ord('X')+i)+"_CENTER]",real=True)

# Set sqrt(4*pi)
sqrt4pi = sp.symbols('sqrt4pi', real=True)
```

## Step 1.e: Setting EOS quantities \[Back to [top](#toc)\]
$$\label{setting_eos_quantities}$$

We now set up needed equation of state (EOS) quantities, namely $\left(\epsilon,P_{\rm cold},\epsilon_{\rm cold},\Gamma_{\rm th}\right)$. $\epsilon$ is not defined as a symbol, but instead it is computed using the hybrid EOS

$$
P = P_{\rm cold} + \left(\Gamma_{\rm th}-1\right)\rho_{b}\epsilon_{\rm th} = P_{\rm cold} + \left(\Gamma_{\rm th}-1\right)\rho_{b}\left(\epsilon - \epsilon_{\rm cold}\right)
\implies
\boxed{\epsilon = \epsilon_{\rm cold} + \frac{P-P_{\rm cold}}{\rho_{b}\left(\Gamma_{\rm th}-1\right)}}\ .
$$

```python
# Set up EOS quantities
# Set P_cold, eps_cold, Gamma_th
P_cold, eps_cold, Gamma_th = sp.symbols("P_cold eps_cold Gamma_th",real=True)

# Compute epsilon
epsilon = eps_cold + (P - P_cold)/(rhob * (Gamma_th - 1))
```

## Step 1.f: The `compute_smallb4U_and_smallbsquared_from_ADM_3METRIC_and_PRIMS.h` file \[Back to [top](#toc)\]
$$\label{compute_smallb4_and_smallbsquared}$$

We now compute $b^{\mu}$ and $b^{2}\equiv b^{\mu}b_{\mu}$ from the ADM quantities and primitives. This is done below in three separate steps:

1. Compute $u^{\mu}$ using the `u4U_in_terms_of_vU__rescale_vU_by_applying_speed_limit()` function from the [GRHD.equations NRPy+ module](/edit/NRPyIGM/GRHD/equations.py).
1. Compute $b^{\mu}$ using the `compute_smallb4U()` function from the [GRFFE.equations NRPy+ module](/edit/NRPyIGM/GRFFE/equations.py).
1. Compute $b^{2}$ using the `compute_smallbsquared()` function from the [GRFFE.equations NRPy+ module](/edit/NRPyIGM/GRFFE/equations.py).

Then, we generate the `compute_smallb4U_and_smallbsquared_from_ADM_3METRIC_and_PRIMS.h` file, which allow the quantities above to be used by `IllinoisGRMHD`.

```python
# Define smallb4U and smallbsquared to be used later when
# computing T^{\mu\nu}, thus simplifying the expression
# generation
smallbsquared = sp.Symbol("smallbsquared",real=True)
smallb4U = ixp.zerorank1(DIM=4)
for mu in range(4):
    smallb4U[mu] = sp.Symbol("smallb4U"+str(mu),real=True)

# Then compute u^{\mu}
GRHD.u4U_in_terms_of_vU__rescale_vU_by_applying_speed_limit(ADMalpha,ADMbetaU,ADMgammaDD, vU)
u4U = GRHD.u4U_ito_vU

# Compute b^{\mu}
GRFFE.compute_smallb4U(ADMgammaDD,ADMbetaU,ADMalpha, u4U,B_notildeU, sqrt4pi)

# Compute b^{\mu}b_{\mu}
GRFFE.compute_smallbsquared(ADMgammaDD,ADMbetaU,ADMalpha, GRFFE.smallb4U)

# Output b^{\mu} and b^{2} to string
string = outputC([GRFFE.smallb4U[0],GRFFE.smallb4U[1],GRFFE.smallb4U[2],GRFFE.smallb4U[3],GRFFE.smallbsquared],
                 [smallb4U[0],      smallb4U[1],      smallb4U[2],      smallb4U[3],      smallbsquared],
                 filename="returnstring", params="outCverbose=False")

# Output string to file
filename="compute_smallb4U_and_smallbsquared_from_ADM_3METRIC_and_PRIMS.h"
filepath = os.path.join(NRPy_headers_dir_path,filename)
IGMout.NRPy_IGM_write_to_file(filepath,filename,string)
```

    Just generated the file: ../src/NRPy_generated_headers/compute_smallb4U_and_smallbsquared_from_ADM_3METRIC_and_PRIMS.h

## Step 1.g: Computing $T^{\mu\nu}_{\rm GRMHD}$ \[Back to [top](#toc)\]
$$\label{grmhd_tupmunu}$$

Now that we have already defined and have access to the ADM quantities $\left(\alpha,\beta^{i},\gamma_{ij},\gamma^{ij},\sqrt{\gamma}\right)$, primitive variables $\left(P,\rho_{b},v^{i},B^{i}\right)$, EOS quantities $\left(P_{\rm cold},\epsilon_{\rm cold},\Gamma_{\rm th},\epsilon\right)$, fluid 4-velocity $u^{\mu}$, and the rescaled magnetic field measured by an observer comoving with the fluid, $\left(b^{\mu},b^{2}\right)$, we are ready to compute the GRMHD energy-momentum tensor:

$$
T^{\mu\nu}_{\rm GRMHD}
=
T^{\mu\nu}_{\rm GRHD} + T^{\mu\nu}_{\rm GRFFE}
=
\left(\rho_{b}h + b^{2}\right)u^{\mu}u^{\nu} + \left(P + \frac{b^{2}}{2}\right)g^{\mu\nu} - b^{\mu}b^{\nu}\ .
$$

This is done in a single step below, by calling the `compute_GRMHD_T4UU()` function from the [GRMHD.equations NRPy+ module](/edit/NRPyIGM/GRMHD/equations.py).

```python
# Compute the GRMHD energy-momentum tensor
GRMHD.compute_GRMHD_T4UU(ADMgammaDD, ADMbetaU, ADMalpha, rhob, P, epsilon, u4U, smallb4U, smallbsquared)
```

## Step 1.h: The `compute_tau_rhs_extrinsic_curvature_terms_and_TUPmunu.h` file \[Back to [top](#toc)\]
$$\label{compute_tau_rhs_extrinsic_curvature_header}$$

We now generate the `compute_tau_rhs_extrinsic_curvature_terms_and_TUPmunu.h` file, to be used by `IllinoisGRMHD` to compute the extrinsic curvature terms on the RHSs of the evolution equation of $\tilde{\tau}$. This is done in the following steps:

1. Define $K_{ij}$ according to `IllinoisGRMHD`'s notation.
1. Set $\partial_{i}\alpha=0$, since we are interested only in the extrinsic curvature terms in $s$.
1. Compute the curvature terms in $s$ using the `compute_s_source_term()` function from the [GRHD.equations NRPy+ module](/edit/NRPyIGM/GRHD/equations.py).
1. Add $T^{\mu\nu}$ and $s$ to expressions and variables lists.
1. We then convert these lists to C code using the `outputC()` function from the [outputC NRPy+ module](/edit/NRPyIGM/outputC.py).
1. Finally, we output the resulting string to a `IllinoisGRMHD` header file using the `NRPy_IGM_write_to_file()` function from the [IllinoisGRMHD_output_functions NRPy+ module](/edit/NRPyIGM/IllinoisGRMHD/doc/IllinoisGRMHD_output_functions.py).

```python
# Set K_{ij}
KLDD = ixp.zerorank2()
for i in range(DIM):
    for j in range(i,DIM):
        KLDD[i][j] = KLDD[j][i] = sp.Symbol('K'+chr(ord('x')+i)+chr(ord('x')+j)+"L",real=True)

# Set up the derivative of alpha to zero, as we wish
# only to compute the extrinsic curvature terms of
# \partial_{t}\tilde{\tau}
alpha_zero_derivD = ixp.zerorank1()
GRHD.compute_s_source_term(KLDD,ADMbetaU,ADMalpha, ADMsqrtgamma,alpha_zero_derivD, GRMHD.T4UU)

# Add T^{\mu\nu} to the variables/expressions lists
exprlist = []
varslist = []
counter = 0
for mu in range(4):
    for nu in range(mu,4):
        exprlist.append(GRMHD.T4UU[mu][nu])
        varslist.append("TUPMUNU["+str(counter)+"][index]")
        counter += 1

# Add tau source term to variables/expressions lists
exprlist.append(GRHD.s_source_term)
varslist.append("""
if(k<cctk_lsh[2]-cctk_nghostzones[2] && j<cctk_lsh[1]-cctk_nghostzones[1] && i<cctk_lsh[0]-cctk_nghostzones[0]) {

tau_rhs[index]""")

# Output expressions to string
string = outputC(exprlist,varslist,filename="returnstring", params="outCverbose=False", poststring="\n   }\n\n")

# Output string to file
filename = "compute_tau_rhs_extrinsic_curvature_terms_and_TUPmunu.h"
filepath = os.path.join(NRPy_headers_dir_path,filename)
IGMout.NRPy_IGM_write_to_file(filepath,filename,string)
```

    Just generated the file: ../src/NRPy_generated_headers/compute_tau_rhs_extrinsic_curvature_terms_and_TUPmunu.h

# Step 2: The `compute_tau_rhs_extrinsic_curvature_terms_and_TUPMUNU.C` file \[Back to [top](#toc)\]
$$\label{compute_tau_rhs_extrinsic_curvature_terms_and_tupmunu__c}$$

We now write the `compute_tau_rhs_extrinsic_curvature_terms_and_TUPMUNU.C` file, which puts together all the header files we set up this far. The overall structure of the file is the following:

* Looping over all gridpoints:
    1. Read in $K_{ij}$ from gridfunctions
    1. Read in `CONF_METRIC` quantities from input
    1. Compute `ADM_3METRIC` using `ADM_3METRIC_in_terms_of_CONF_METRIC.h`, generated in [Step 1.c](#alpha_beta_gammadn_from_conf_metric_header)
    1. Read in `PRIMS` from input
    1. Compute $b^{\mu}$ and $b^{2}$ using `compute_smallb4U_and_smallbsquared_from_ADM_3METRIC_and_PRIMS.h`, generated in [Step 1.f](#compute_smallb4_and_smallbsquared)
    1. Compute EOS quantities
    1. Compute $T^{\mu\nu}$ and $s$, using `compute_tau_rhs_extrinsic_curvature_terms_and_TUPmunu.h`, generated in [Step 1.h](#compute_tau_rhs_extrinsic_curvature_header)

```python
%%writefile $outdir/compute_tau_rhs_extrinsic_curvature_terms_and_TUPMUNU.C
static void compute_tau_rhs_extrinsic_curvature_terms_and_TUPMUNU
(const cGH *cctkGH,const int *cctk_lsh,const int *cctk_nghostzones,CCTK_REAL *dX, CCTK_REAL **CONF_METRIC_IN,gf_and_gz_struct *prims,
 CCTK_REAL **TUPMUNU,eos_struct &eos,
 CCTK_REAL *gupxy,CCTK_REAL *gupxz,CCTK_REAL *gupyz,
 CCTK_REAL *kxx,CCTK_REAL *kxy,CCTK_REAL *kxz,CCTK_REAL *kyy,CCTK_REAL *kyz,CCTK_REAL *kzz,
 CCTK_REAL *tau_rhs) {

  // Set sqrt4pi
  const CCTK_REAL sqrt4pi = sqrt(4.0*M_PI);

  // These loop extents must be consistent with add_fluxes_and_source_terms_to_hydro_rhss(), since we use TUPMUNU there as well.
#pragma omp parallel for
  for(int k=cctk_nghostzones[2];k<cctk_lsh[2]-(cctk_nghostzones[2]-1);k++) for(int j=cctk_nghostzones[1];j<cctk_lsh[1]-(cctk_nghostzones[1]-1);j++) for(int i=cctk_nghostzones[0];i<cctk_lsh[0]-(cctk_nghostzones[0]-1);i++) {
        int index = CCTK_GFINDEX3D(cctkGH,i,j,k);

        DECLARE_CCTK_PARAMETERS;

        // First we pull in needed hydrodynamic and metric variables from memory: PART 1.
        // Reading from main memory is a SLOW operation, usually resulting in
        //   cache misses, which
        //   will waste precious runtime. Note that cache misses will often not
        //   show up when using, e.g., gprof. The slowdown due to cache misses
        //   can more than double the amount of time in this routine, so instead
        //   of reading in variables from main memory multiple times, the below
        //   forces us to read in only once, storing in local variables
        //   U{p,m}{1,2,3}, METRIC{p,m}{1,2}, U, and METRIC.

        // Read in K_{ij} from gridfunctions
        CCTK_REAL KxxL = kxx[index];
        CCTK_REAL KxyL = kxy[index];
        CCTK_REAL KxzL = kxz[index];
        CCTK_REAL KyyL = kyy[index];
        CCTK_REAL KyzL = kyz[index];
        CCTK_REAL KzzL = kzz[index];

        // Read in conformal metric quantitites from gridfunctions
        CCTK_REAL CONF_METRIC[NUMVARS_FOR_CONF_METRIC_FACEVALS];
        for(int ii=0;ii<NUMVARS_FOR_CONF_METRIC_FACEVALS;ii++) CONF_METRIC[ii] = CONF_METRIC_IN[ii][index];

        // Compute ADM_3METRIC quantities from CONF_METRIC
        CCTK_REAL ADM_3METRIC[NUMVARS_FOR_ADM_3METRIC];
#include "NRPy_generated_headers/ADM_3METRIC_in_terms_of_CONF_METRIC.h"

        // The "vector" PRIMS represents the primitive variables: rho, P, vx, vy, vz, Bx, By, and Bz.
        CCTK_REAL PRIMS[8]; // 8 primitives in the set: {rho_b,P,vx,vy,vz,Bx,By,Bz}
        for(int ii=0;ii<8;ii++) PRIMS[ii] = prims[ii].gf[index];

        // Define b^{\mu} variables and compute b^{\mu} and b^{2} and compute them
        CCTK_REAL smallb4U0, smallb4U1, smallb4U2, smallb4U3, smallbsquared;
#include "NRPy_generated_headers/compute_smallb4U_and_smallbsquared_from_ADM_3METRIC_and_PRIMS.h"

        struct output_stats stats; stats.failure_checker=0;

        // Set EOS quantities for the computation of T^{\mu\nu}
        CCTK_REAL h=0,  P_cold,eps_cold,dPcold_drho,eps_th,Gamma_cold; /* <- Note that in setting h, we need to define several
                                                                        *    other variables, even though they will be unused later
                                                                        *    in this function. */
        compute_P_cold__eps_cold__dPcold_drho__eps_th__h__Gamma_cold(PRIMS,eos,Gamma_th,P_cold,eps_cold,dPcold_drho,eps_th,h,Gamma_cold);

        CCTK_REAL eps = eps_cold + eps_th;

        if((i==14) && (j==14) && (k==14)) {
          printf("eeee %e %e %e %e %e %e %e %e %e %e\n",PRIMS[PRESSURE],PRIMS[RHOB],P_cold,eps_cold,dPcold_drho,eps_th,Gamma_cold,h, eps, eps_cold + eps_th);
          printf("ffff ");
          for(int ll=0; ll<8; ll++) printf("%e ",PRIMS[ll]);
          printf("\n");
          printf("gggg ");
          for(int ll=0; ll<NUMVARS_FOR_CONF_METRIC_FACEVALS; ll++) printf("%e ",CONF_METRIC[ll]);
          printf("\n");
        }

        // Compute T^{\mu\nu} and the extrinsic curvatures in tau_rhs
#include "NRPy_generated_headers/compute_tau_rhs_extrinsic_curvature_terms_and_TUPmunu.h"

      }
}
```

    Overwriting ../src/compute_tau_rhs_extrinsic_curvature_terms_and_TUPMUNU.C

# Step 3: Code validation \[Back to [top](#toc)\]
$$\label{code_validation}$$

First we download the original `IllinoisGRMHD` source code and then compare it to the source code generated by this tutorial notebook.

```python
# # Verify if the code generated by this tutorial module
# # matches the original IllinoisGRMHD source code

# # First download the original IllinoisGRMHD source code
# import urllib
# from os import path

# original_IGM_file_url  = "https://bitbucket.org/zach_etienne/wvuthorns/raw/5611b2f0b17135538c9d9d17c7da062abe0401b6/IllinoisGRMHD/src/compute_tau_rhs_extrinsic_curvature_terms_and_TUPMUNU.C"
# original_IGM_file_name = "compute_tau_rhs_extrinsic_curvature_terms_and_TUPMUNU-original.C"
# original_IGM_file_path = os.path.join(IGM_src_dir_path,original_IGM_file_name)

# # Then download the original IllinoisGRMHD source code
# # We try it here in a couple of ways in an attempt to keep
# # the code more portable
# try:
#     original_IGM_file_code = urllib.request.urlopen(original_IGM_file_url).read().decode("utf-8")
#     # Write down the file the original IllinoisGRMHD source code
#     with open(original_IGM_file_path,"w") as file:
#         file.write(original_IGM_file_code)
# except:
#     try:
#         original_IGM_file_code = urllib.urlopen(original_IGM_file_url).read().decode("utf-8")
#         # Write down the file the original IllinoisGRMHD source code
#         with open(original_IGM_file_path,"w") as file:
#             file.write(original_IGM_file_code)
#     except:
#         # If all else fails, hope wget does the job
#         !wget -O $original_IGM_file_path $original_IGM_file_url

# # Perform validation
# Validation__compute_tau_rhs_extrinsic_curvature_terms_and_TUPMUNU__C  = !diff $original_IGM_file_path $outfile_path__compute_tau_rhs_extrinsic_curvature_terms_and_TUPMUNU__C

# if Validation__compute_tau_rhs_extrinsic_curvature_terms_and_TUPMUNU__C == []:
#     # If the validation passes, we do not need to store the original IGM source code file
#     !rm $original_IGM_file_path
#     print("Validation test for compute_tau_rhs_extrinsic_curvature_terms_and_TUPMUNU.C: PASSED!")
# else:
#     # If the validation fails, we keep the original IGM source code file
#     print("Validation test for compute_tau_rhs_extrinsic_curvature_terms_and_TUPMUNU.C: FAILED!")
#     # We also print out the difference between the code generated
#     # in this tutorial module and the original IGM source code
#     print("Diff:")
#     for diff_line in Validation__compute_tau_rhs_extrinsic_curvature_terms_and_TUPMUNU__C:
#         print(diff_line)
```

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

```python
latex_nrpy_style_path = os.path.join(nrpy_dir_path,"latex_nrpy_style.tplx")
#!jupyter nbconvert --to latex --template $latex_nrpy_style_path --log-level='WARN' Tutorial-IllinoisGRMHD__compute_tau_rhs_extrinsic_curvature_terms_and_TUPMUNU.ipynb
#!pdflatex -interaction=batchmode Tutorial-IllinoisGRMHD__compute_tau_rhs_extrinsic_curvature_terms_and_TUPMUNU.tex
#!pdflatex -interaction=batchmode Tutorial-IllinoisGRMHD__compute_tau_rhs_extrinsic_curvature_terms_and_TUPMUNU.tex
#!pdflatex -interaction=batchmode Tutorial-IllinoisGRMHD__compute_tau_rhs_extrinsic_curvature_terms_and_TUPMUNU.tex
!rm -f Tut*.out Tut*.aux Tut*.log
```


```c++
#pragma cling add_include_path("../../include")
#pragma cling add_include_path("../feltor/inc") // Feltor path
#define THRUST_DEVICE_SYSTEM THRUST_DEVICE_SYSTEM_CPP
#include <iostream>
#include "dg/algorithm.h"
```

    In file included from input_line_8:2:
    In file included from ../feltor/inc/dg/algorithm.h:8:
    ../feltor/inc/dg/backend/config.h:20:9: warning: NOTE: Fast std::fma(a,b,c) not activated! Using a*b+c instead! [-W#pragma-messages]
    #pragma message( "NOTE: Fast std::fma(a,b,c) not activated! Using a*b+c instead!")
            ^
    In file included from input_line_8:2:
    In file included from ../feltor/inc/dg/algorithm.h:11:
    In file included from ../feltor/inc/dg/topology/split_and_join.h:4:
    In file included from ../feltor/inc/dg/backend/blas1_dispatch_shared.h:12:
    In file included from ../feltor/inc/dg/backend/blas1_serial.h:6:
    In file included from ../feltor/inc/dg/backend/exblas/exdot_serial.h:25:
    In file included from ../feltor/inc/dg/backend/exblas/accumulate.h:19:
    ../feltor/inc/dg/backend/exblas/config.h:31:9: warning: WARNING: Instruction set below SSE4.1! Deactivating vectorization! [-W#pragma-messages]
    #pragma message("WARNING: Instruction set below SSE4.1! Deactivating vectorization!")
            ^
    In file included from input_line_8:2:
    In file included from ../feltor/inc/dg/algorithm.h:11:
    In file included from ../feltor/inc/dg/topology/split_and_join.h:4:
    In file included from ../feltor/inc/dg/backend/blas1_dispatch_shared.h:12:
    In file included from ../feltor/inc/dg/backend/blas1_serial.h:6:
    In file included from ../feltor/inc/dg/backend/exblas/exdot_serial.h:25:
    ../feltor/inc/dg/backend/exblas/accumulate.h:93:43: warning: shifting a negative signed value is undefined [-Wshift-negative-value]
            carrybit = (s ? 1ll << KRX : -1ll << KRX);
                                         ~~~~ ^
    In file included from input_line_8:2:
    In file included from ../feltor/inc/dg/algorithm.h:11:
    In file included from ../feltor/inc/dg/topology/split_and_join.h:4:
    In file included from ../feltor/inc/dg/backend/blas1_dispatch_shared.h:12:
    In file included from ../feltor/inc/dg/backend/blas1_serial.h:6:
    In file included from ../feltor/inc/dg/backend/exblas/exdot_serial.h:26:
    ../feltor/inc/dg/backend/exblas/ExSUM.FPE.hpp:143:46: warning: unknown attribute 'optimize' ignored [-Wunknown-attributes]
    template<typename T, int N, typename TRAITS> UNROLL_ATTRIBUTE
                                                 ^
    ../feltor/inc/dg/backend/exblas/config.h:42:41: note: expanded from macro 'UNROLL_ATTRIBUTE'
    #define UNROLL_ATTRIBUTE __attribute__((optimize("unroll-loops")))
                                            ^
    In file included from input_line_8:2:
    In file included from ../feltor/inc/dg/algorithm.h:11:
    In file included from ../feltor/inc/dg/topology/split_and_join.h:4:
    In file included from ../feltor/inc/dg/backend/blas1_dispatch_shared.h:12:
    In file included from ../feltor/inc/dg/backend/blas1_serial.h:6:
    In file included from ../feltor/inc/dg/backend/exblas/exdot_serial.h:26:
    ../feltor/inc/dg/backend/exblas/ExSUM.FPE.hpp:189:46: warning: unknown attribute 'optimize' ignored [-Wunknown-attributes]
    template<typename T, int N, typename TRAITS> UNROLL_ATTRIBUTE
                                                 ^
    ../feltor/inc/dg/backend/exblas/config.h:42:41: note: expanded from macro 'UNROLL_ATTRIBUTE'
    #define UNROLL_ATTRIBUTE __attribute__((optimize("unroll-loops")))
                                            ^
    In file included from input_line_8:2:
    In file included from ../feltor/inc/dg/algorithm.h:11:
    In file included from ../feltor/inc/dg/topology/split_and_join.h:4:
    In file included from ../feltor/inc/dg/backend/blas1_dispatch_shared.h:12:
    In file included from ../feltor/inc/dg/backend/blas1_serial.h:6:
    In file included from ../feltor/inc/dg/backend/exblas/exdot_serial.h:26:
    ../feltor/inc/dg/backend/exblas/ExSUM.FPE.hpp:221:46: warning: unknown attribute 'optimize' ignored [-Wunknown-attributes]
    template<typename T, int N, typename TRAITS> UNROLL_ATTRIBUTE
                                                 ^
    ../feltor/inc/dg/backend/exblas/config.h:42:41: note: expanded from macro 'UNROLL_ATTRIBUTE'
    #define UNROLL_ATTRIBUTE __attribute__((optimize("unroll-loops")))
                                            ^

# Timesteppers
This chapter deals with how to integrate differential equations of the form
\begin{align}
M(y,t) \frac{d}{dt}y = F(y,t)
\end{align}
where $M$ is the mass matrix and $F$ is the right hand side.

## Overview
Generally we need three things to solve an ODE numerically
 - **the stepper method** (there are several stepper classes each coming with a range of tableaus to chose from: Runge Kutta vs Multistep, explicit vs implicit and more) e.g. `dg::ExplicitMultistep<Vector>`
 - **the ode itself**, this has to be provided by the user as the right hand side functor or tuple of functors including the solver method if an implicit stepper is used and then tied to a single object typically using `std::tie`, e.g. `std::tie( ex, im, solve);`
 - **the timeloop** (adaptive timestep vs fixed timestep) -> can be implemented by the user or in Feltor represented by an instance of `dg::aTimeloop` class (useful if one wants to choose a method at runtime) e.g. `dg::AdaptiveTimeloop<Vector>`. The timeloop includes the **initial condition** and the integration boundaries (these are the paramters to the integrate function) `timeloop.integrate( t0, u0, t1, u1);`
 .

The `dg` library provides a wide selection of explicit, implicit and imex timesteppers:
- `dg::ERKStep` embedded Runge Kutta
- `dg::DIRKStep` diagonally implicit Runge Kutta
- `dg::ARKStep` additive Runge Kutta (imex)
- `dg::ExplicitMultistep`
- `dg::ImplicitMultistep`
- `dg::ImExMultistep`
-  see [doxygen documentation](https://feltor-dev.github.io/doc/dg/html/group__time.html) for a full list

The `dg::ERKStep`, `dg::DIRKStep` and `dg::ARKStep` (and in general any embedded method) can be used as a driver for an adaptive Timestepper
- `dg::Adaptive`
```{admonition} Vector type
All of the above classes are templates of the Vector type in use. Chosing an appropriate type for the integration variable(s) is usually the first decision to make when implementing a differential equation. Anything that works in the `dg::blas1` functions is allowed so check out the [Vectors](sec:vectors) chapter.
```

In order to implement any Runge-Kutta or multistep algorithm we need to be able to solve two general types of equations (see [theory guide on overleaf](https://www.overleaf.com/read/dfxncmnnpzfm)):
\begin{align}
    k =  M(y,t)^{-1} \cdot F(y,t) &\text{ given $t,y$ return $k$} \\
    M(y,t)\cdot ( y-y^*) - \alpha F(y,t) = 0 &\text{ given $\alpha, t, y^*$ return $y$}
\end{align}
For any explicit (part of the) equation we need to solve the first and for any implicit (part of the) equation  we need to solve both the first and the second equation.
````{note}
It is important to realize that the timestepper needs to know neither $F$ nor $M$, nor how they are implemented or how the implicit equation is solved.
The user just needs to provide oblique functor
objects with the signature
```cpp
// given t y write ydot
void operator()( value_type t, const ContainerType0& y, ContainerType1& ydot);
// given \alpha, t and y^* write a new y
void operator()( value_type alpha, value_type t, ContainerType& y, const ContainerType1& ystar);
```
````
More than one functor is needed for implicit or semi-implicit timesteppers. These are then expected as a `std::tuple` of functors.
Typically one would use `std::tie` to tie together two or more objects into a functor.

The timesteppers themselves are implemented using `dg::blas1` vector additions and all have a `step` method that advances the ode for one timestep
```cpp
dirk.step( std::tie( implicit_part, solver), t0, y0, t1, y1, dt);
```
This step method can be called in a loop to advance the ode for an arbitrary time. Or you can use one of the `dg::aTimeloop` classes
- `dg::AdaptiveTimeloop`
- `dg::SinglestepTimeloop`
- `dg::MultistepTimeloop`

These classes abstract the integration loop and can be called via a common interface
```cpp
timeloop.integrate( t0, y0, t1, y1);
```
This is useful especially if you want to choose various timesteppers at runtime.

We will now study a few case scenarios to clarify the above explanations.

## Integrate ODEs in Feltor - first timesteppers and simple time loops

As an example we solve the damped driven harmonic oscillator
\begin{align}
    \frac{d x}{d t} &= v \\
    \frac{d v}{d t} &= -2 \nu \omega_0 v - \omega_0^2 x + \sin (\omega_d t)
\end{align}
Since we have two scalar variables we will use a `std::array<double,2>` as the vector type to use.
Let us first choose some (somewhat random) parameters

```c++
const double damping = 0.2, omega_0 = 1.0, omega_drive = 0.9;
```

We know that we can solve this ODE analytically. This comes in handy to verify our implementation.

```c++
// We have an analytical solution
std::array<double,2> solution( double t)
{
    double tmp1 = (2.*omega_0*damping);
    double tmp2 = (omega_0*omega_0 - omega_drive*omega_drive)/omega_drive;
    double amp = 1./sqrt( tmp1*tmp1 + tmp2*tmp2);
    double phi = atan( 2.*omega_drive*omega_0*damping/(omega_drive*omega_drive-omega_0*omega_0));
    double x = amp*sin(omega_drive*t+phi)/omega_drive;
    double v = amp*cos(omega_drive*t+phi);
    return {x,v};
}
```

### Explicit Runge-Kutta - fixed step

First, show how to implement a simple timeloop with a fixed stepsize Runge Kutta integrator. We choose the classic 4-th order scheme, but consult the [documentation](https://feltor-dev.github.io/doc/dg/html/group__time.html) for an extensive list of available tableaus.

```c++
// The right hand side needs to be a callable function in Feltor.
// In modern C++ this can for example be a lambda function:
auto rhs = [&]( double t, const std::array<double,2>& y,
            std::array<double,2>& yp)
{
    //damped driven harmonic oscillator
    // x -> y[0] , v -> y[1]
    yp[0] = y[1];
    yp[1] = -2.*damping*omega_0*y[1] - omega_0*omega_0*y[0]
            + sin(omega_drive*t);
};
// Let us choose an initial condition and the integration boundaries
double t0 = 0., t1 = 1.;
const std::array<double,2> u0 = solution(t0);

// Here, we choose the classic Runge-Kutta scheme to solve
dg::RungeKutta<std::array<double,2>> rk("Runge-Kutta-4-4", u0);
// Now we are ready to construct a time-loop by repeatedly stepping
// the Runge Kutta solve with a constant timestep
double t = t0;
std::array<double,2> u1( u0);
unsigned N = 20;
for( unsigned i=0; i<N; i++)
    rk.step( rhs, t, u1, t, u1, (t1-t0)/(double)N );

// Now let us compute the error
const std::array<double,2> sol = solution(t1);
dg::blas1::axpby( 1., sol , -1., u1);
std::cout << "Norm of error is " <<sqrt(dg::blas1::dot( u1, u1))<<"\n";
```

    Norm of error is 8.17315e-08

```{note}
Notice how the above program consists of the three mentioned ingredients. We wrote the ode as a single functor `rhs` expecting to use the explicit single step Runge Kutta method `dg::RungeKutta`. We then wrote a timeloop starting from the solution at time `t0`.
```
```{seealso}
Lambdas are so useful that they appear at several places in the user-guide and the `dg` library generally accepts them in many places. If you are not familiar with them, a good start is to watch youtube! For example [Lambdas from Scratch - Arthur O'Dwye](https://www.youtube.com/watch?v=3jCOwajNch0)
```

### Implicit Multistep  - fixed step
In the next example we want to solve the same ode with an implicit multistep method:

```c++
// First we need to provide a solution method for the prototypical implicit equation
auto solve = [&]( double alpha, double t, std::array<double,2>& y,
            const std::array<double,2>& yp)
{
    // y - alpha RHS( t, y) = rho
    // can be solved analytically
    y[1] = ( yp[1] + alpha*sin(omega_drive*t) - alpha*omega_0*omega_0*yp[0])/
           (1.+2.*alpha*damping*omega_0+alpha*alpha*omega_0*omega_0);
    y[0] = yp[0] + alpha*y[1];
};
// Now we can construct a multistep method
dg::ImplicitMultistep<std::array<double,2>> multi("ImEx-BDF-3-3", u0);
// Let us choose the same initial conditions as before
t = t0; u1 = u0;
// Finally, we can construct a timeloop
multi.init( std::tie( rhs, solve), t, u0, (t1-t0)/(double)N);
for( unsigned i=0; i<N; i++)
    multi.step( std::tie(rhs, solve), t, u1);

dg::blas1::axpby( 1., sol , -1., u1);
std::cout << "Norm of error is " <<sqrt(dg::blas1::dot( u1, u1))<<"\n";
//dg::make_odeint( dirk, std::tie( rhs, solve), (t1-t0)/20.)->integrate( t0, u0, t1, u1);
```

    Norm of error is 3.60666e-05

```{note}
Notice the use of `std::tie` in the `init` and `step` methods. Since an implicit method needs both the implicit part `rhs` and an implicit solver `solve` to solve it.
```

### Embedded explicit Runge Kutta - adaptive step
As a last example we want to integrate the ode with an adaptive timestepper

```c++
dg::Adaptive<dg::ERKStep<std::array<double,2>>> adapt("Tsitouras11-7-4-5", u0);
t = t0; u1 = u0;
double dt = 1e-6;
while( t < t1)
{
    if( t + dt > t1)
        dt = t1 - t;
    adapt.step( rhs, t, u1, t, u1, dt, dg::pid_control, dg::fast_l2norm, 1e-6, 1e-6);
}

dg::blas1::axpby( 1., sol , -1., u1);
std::cout << "Norm of error is " <<sqrt(dg::blas1::dot( u1, u1))
    <<" with "<<adapt.nsteps()<<" steps \n";
```

    Norm of error is 2.66678e-07 with 8 steps

## Abstract timeloops with dg::aTimeloop
We have seen that the main difference between the three previous methods was how to construct the timeloop. Runge-Kutta was just a basic for loop, Multistep needed to be initialized before usage, while the adaptive stepper needed to run in a while loop and needed a bunch of additional parameters. We will now introduce
an abstract interface that lets you choose at runtime which integration method to use:

```cpp
using Vec = std::array<double,2>;
const unsigned* nsteps;

std::string stepper = "Adaptive";
std::cin >> stepper;

// A pointer to an abstract integrator
auto odeint = std::unique_ptr<dg::aTimeloop<Vec>>();

if( "Adaptive" == stepper)
{
    dg::Adaptive<dg::ERKStep<Vec>> adapt("Tsitouras11-7-4-5", u0);
    odeint = std::make_unique<dg::AdaptiveTimeloop<Vec>>( adapt,
                rhs, dg::pid_control, dg::fast_l2norm, 1e-6, 1e-6);
    nsteps = &adapt.nsteps();
}
else if( "Singlestep" == stepper)
{
    dg::RungeKutta<Vec> rk("Runge-Kutta-4-4", u0);
    odeint = std::make_unique<dg::SinglestepTimeloop<Vec>>( rk,
                                         rhs, (t1-t0)/(double)N);
    nsteps = &N;
}
else if( "Multistep" == stepper)
{
    dg::ExplicitMultistep<Vec> multi( "ImEx-BDF-3-3", u0);
    odeint = std::make_unique<dg::MultistepTimeloop<Vec>>( multi,
                        rhs, t0, u0, (t1-t0)/(double)N);
    nsteps = &N;
}
else
    std::cerr << "Stepper "<<stepper<<" not known!\n";

odeint -> integrate(t0, u0, t1, u1);

dg::blas1::axpby( 1., sol , -1., u1);
std::cout << "Norm of error is " <<sqrt(dg::blas1::dot( u1, u1))
    <<" with "<<*nsteps<<" steps \n";
```

In order to build more complicated examples we will first need to learn how geometry and derivatives work and how we can build advanced solvers. We will revisit the timesteppers in [Section PDEs](sec:pdes).

## Integrating DAEs - solver only implicit integrators
Consider the van der Pol oscillator

\begin{align}
    \dot x &= v \\
    \dot v &= \sigma ( 1-x^2) v -x
 \end{align}
 where $\sigma$ represents the stiffness parameter. For
very large values these equations become **differential-algebraic equations of index 1** (which is easily seen if one divides by sigma and sets $1/\sigma = 0$, the remaining equation does not have derivatives, hence algebraic, index 1 means it is solvable).
It is then a singular perturbation problem.
This behaviour is difficult for time-integrators insofar it reduces the error of the method to its so-called _stage-order_.

Furthermore, for higher $\sigma$ it becomes paramount to use implicit integrators and sometimes it might be desirable to completely avoid any explicit stage altogether (recall that for example EDIRK schems have an explicit first stage). For this reason the doxygen documentation highlights implicit integrators of that sort:
- all of the implicit multistep methods avoid calling the rhs directly, however, currently only the `dg::BDF_X_X` schemes can avoid it in the initialization phase as well
- of the DIRK schemes only the SDIRK and symplectic DIRK schemes do not call the rhs.

This means for such schemes we only need to implement a single functor, namely one that solves
\begin{align}
    M(y,t)\cdot ( y-y^*) - \alpha F(y,t) = 0 &\text{ given $\alpha, t, y^*$ return $y$}
\end{align}

Here we outline the structure of a possible implementation
```cpp

struct VanderPolSolver
{
     void operator()( double alpha, double t, std::array<double,2>& y, const std::array<double,2>& yp)
     {
         // solve y - alpha F(y,t) = yp
     }
}

auto doNotCall = [](double t, const auto& x, auto& y)
{
    // Just to be entirely certain
    assert(false && "This should never be called!");
}

VanderPolSolver solver( ...);
dg::DIRKStep<Vec> dirk( "SDIRK-4-2-3", y0);

odeint = std::make_unique<dg::SinglestepTimeloop<Vec>>( dirk,
                                                       std::tie( doNotCall, solver),
                                                       (t1-t0)/(double)N);
odeint -> integrate(t0, u0, t1, u1);
```


```python
import sys
import time
sys.path.append('source/')

import numpy as np
import matplotlib.pyplot as plt

import hypothesis as h
from plotting_tools import plot_probabilities

plt.rcParams['figure.figsize'] = 10, 5
```

# Analysis of the rank of the constrain matrix

If not stated otherwise, we assume that there is at most one measurement per time instance.

In this notebook we use a little rearanged definitions of the left and right parts of matrix:

Let us write $\pmb{f_n}^\top$ as $[1\ \pmb{g_n}^\top]^\top$, and similarly
$\tilde{\pmb{a}}_m^\top = [\pmb{a_m}^\top 1]^\top$. We can then
write the whole constraint vector as:
$$\begin{bmatrix}
\text{vect}(\tilde{\pmb{a}}_m\pmb{f}_n^\top)^\top & \text{vect}(
\pmb{g_n} \pmb{f_n}^\top)^\top
\end{bmatrix}$$
Then we have the needed constrains on the left, and constrains added due to the
regularisation on the right.
From the Lemmas from the paper we know that we can write:
$$\text{vect}(\pmb{f_n f_n^\top}) = \pmb{R} \pmb{f_n^e},$$
where $\pmb{R}$ is some (sparse) matrix, and $\pmb{f_n^e}$ is a vector
similar in structure to $\pmb{f_n}$, and can be written as a concatenation of  $\pmb{f_n}$ and $\pmb{f}_n^{r}$.
This means we can write our full system of equations as:
\begin{align}
b &=
\begin{bmatrix}
\text{vect}(\pmb{a_m f_n}^\top)^\top & (\pmb{f_n^e})^\top
\end{bmatrix}
\begin{bmatrix}
\pmb{C} \\
\pmb{R^\top L} \\\end{bmatrix}\\
\end{align}
Or using $\tilde{\pmb{a}}_m$:
\begin{align}
b &=
\begin{bmatrix}
\text{vect}(\tilde{\pmb{a}}_m \pmb{f}_n^\top)^\top & \pmb{f}_n^{r\top}
\end{bmatrix}
\begin{bmatrix}
\pmb{C} \\
\pmb{R}^\top \pmb{L} \\\end{bmatrix}
\end{align}

And so for us left hand side refers to $\text{vect}(\tilde{\pmb{a}}_m \pmb{f}_n^\top)^\top$ and right hand side refers to $\pmb{f}_n^{r\top}$.

## Rank vs number of measurements: simulations
In those experiments, the number of dimentions $D$, number of constrains $K$ and number of positions $N$ is fixed,
and for several different ranks the number of measurements is increased. The second experiment allows for many measurements per time instance. We can see that with at most one measurement per time we have better results for more than D+1 anchors, but for D+1 anchors there many measurements setup is better.

```python
plt.rcParams['figure.figsize'] = 10, 5

experiment_params={
    "n_dimensions": 2,
    "n_constraints": 5,
    "n_positions": 60,
    "n_repetitions": 10, # do 100 for smooth results
    "full_matrix": True,
    "n_anchors_list": [1, 2, 4, 8, 100],
    "one_per_time": True,
}

ranks, params = h.matrix_rank_experiment(experiment_params)

plot_probabilities(ranks, params)
plt.show()

experiment_params={
    "n_dimensions": 2,
    "n_constraints": 5,
    "n_positions": 20,
    "n_repetitions": 10, # do 100 for smooth results
    "full_matrix": True,
    "n_anchors_list": [1, 2, 4, 8, 100],
    "one_per_time": False,
}

ranks, params = h.matrix_rank_experiment(experiment_params)

plot_probabilities(ranks, params)
plt.show()
```

## Rank vs number of measurements: theory and simulations
First plots in this part focus on the rank of the left hand side of the matrix.

```python
n_constrains = 5
experiment_params={
    "n_dimensions": 2,
    "n_constraints": n_constrains,
    "n_repetitions": 50, # do 5000 for smooth results
    "full_matrix": True,
    "n_anchors_list": [1, 2, 3, 10],
    "n_positions": 40,
    "one_per_time": True,
}

start = time.time()
ranks, params = h.matrix_rank_experiment(experiment_params)
end = time.time()
print("elapsed time: {:.2f}s".format(end - start))
```

```python
n_measurement_list = params["second_list"]

start = time.time()
probabilities = []
for idx, n_anchors in enumerate(params["n_anchors_list"]):
    print("{} anchors".format(n_anchors))
    probabilities.append([h.probability_upper_bound(
        params["n_dimensions"],
        params["n_constraints"],
        n_measurements=n,
        position_wise=False,
        n_anchors=n_anchors,
        n_positions=np.Infinity
    ) for n in n_measurement_list])
probabilities = np.array(probabilities)
print("time: {:.2f}s".format(time.time()-start))
```

### Plot and save results

```python
import pickle
from plotting_tools import make_dirs_safe
directory = 'theory/'
make_dirs_safe(directory)
name = 'first_one_per_time_plots'
data = {"ranks": ranks, "probabilities":probabilities, "params": params}
pickle.dump(data, open( directory + name + '.p', "wb" ) )

plt.rcParams['figure.figsize'] = 10, 5
f, ax = plt.subplots()
for idx, n_anchors in enumerate(params["n_anchors_list"]):

    max_rank = params["max_rank"]
    mean =  np.mean(ranks[:, idx, :] >= max_rank, axis=-1)
    std = np.std(ranks[:, idx, :] >= max_rank, axis=-1)
    anchor_condition = np.mean(params["anchor_condition"][:, idx, :], axis=-1)
    frame_condition = np.mean(params["frame_condition"][:, idx, :], axis=-1)
    both_conditions = np.mean((params["anchor_condition"] * params["frame_condition"])[:, idx, :], axis=-1)

    p = plt.step(
        n_measurement_list,
        probabilities[idx],
        label="calculated prob. left side, {} anchors".format(n_anchors), # using the old model, what needs to be changed?
        linestyle=":",
        where='post')
    plt.step(
        n_measurement_list,
        mean,
        label="estimated prob., full matrix, {} anchors".format(n_anchors),
        color=p[-1].get_color(),
        where='post')

plt.xlabel("number of measurements")
plt.ylim(0)
plt.grid()
plt.legend()
matrix_name = "Full matrix" if params["full_matrix"] else "Left hand side"
plt.title(matrix_name + ", one measurement at time, complexity {}".format(params["n_constraints"]))
plt.savefig(directory + name + str(params["full_matrix"]) + '.pdf')

plt.show()
```


# The Relation between the SEIR Epidemic Model and Hawkes Processes
> Recently, a link between the SIR compartmental epidemic model and Hawkes-like processes was found. This opens up the possibility to use some of the rich theory related to Hawkes processes when analyzing spreads of diseases using the SIR model. As the SIR model is not capable of modeling latent periods – a feature inherent in Covid-19 and many other diseases – there is a need to extend the recently found link to more complex compartmental epidemic models such as the SEIR model. We have discovered this link and present it briefly in this blog post. For a detailed derivation of our results and a more in-depth discussion please refer to my master's thesis (and soon our paper).
- toc: false
- branch: master
- image: images/posts/Sir_Seir_ExpHawkesN_HypoexpHawkesN.png
- badges: true
- comments: true
- categories: [epidemic models]
- hide: false
- search_exclude: true

```python
#hide
import altair as alt
```

```python
#hide
from sim_params import *
```

```python
#hide
slider_beta = alt.binding_range(min=beta_min, max=beta_max, step=beta_step, name="infection rate \u03b2 ")
slider_sigma = alt.binding_range(min=gamma_min, max=gamma_max, step=gamma_step, name="incubation rate \u03c3 ")
slider_gamma = alt.binding_range(min=gamma_min, max=gamma_max, step=gamma_step, name="recovery rate \u03b3 ")

select_beta = alt.selection_single(
    fields=["beta"],
    bind={"beta": slider_beta},
    init={"beta": 1},
    name="parameter_beta"
)

select_gamma = alt.selection_single(
    fields=["gamma"],
    bind={"gamma": slider_gamma},
    init={"gamma": .1},
    name="parameter_gamma"
)

select_sigma = alt.selection_single(
    fields=["sigma"],
    bind={"sigma": slider_sigma},
    init={"sigma": .3},
    name="parameter_sigma"
)
```

```python
#hide
base_url = ("https://raw.githubusercontent.com/yogabonito/blog/"
            "master/_notebooks/fig/cumulative_infections/")
sir_data = {"source": base_url + "sir.json", "title": "SIR"}
seir_data = {"source": base_url + "seir.json", "title": "SEIR"}
exp_hawkesn_data = {
    "source": base_url + "exp_hawkesn.json",
    "title": "HawkesN process with exponential kernel"
}
hypoexp_and_erlang_hawkesn_data = {
    "source": base_url + "hypoexp_and_erlang_hawkesn.json",
    "title": "HawkesN process with hypoexponential/Erlang kernel"
}

sim_data = [sir_data, seir_data, exp_hawkesn_data, hypoexp_and_erlang_hawkesn_data]
```

```python
#hide
sir_chart, seir_chart, exp_hawkesn_chart, hypoexp_and_erlang_hawkesn_chart = (
    alt.Chart(data["source"]).mark_bar().encode(
        alt.X(
            "day:Q",
            scale=alt.Scale(domain=(1, t_max-5))
        ),
        alt.Y(
            "cumulative infections:Q",
            scale=alt.Scale(domain=(0, n))
        ),
        #x="day:Q"
        tooltip=["day:Q", "cumulative infections:Q"]
    ).properties(
        title=data["title"]
    )
    for data in sim_data
)

sir_chart, seir_chart, exp_hawkesn_chart, hypoexp_and_erlang_hawkesn_chart = (
    chart.add_selection(
        select_gamma
    ).transform_filter(
        select_gamma
    ).add_selection(
        select_beta
    ).transform_filter(
        select_beta
    )
    for chart in [sir_chart, seir_chart, exp_hawkesn_chart, hypoexp_and_erlang_hawkesn_chart]
)

seir_chart, hypoexp_and_erlang_hawkesn_chart = (
    chart.add_selection(
        select_sigma
    ).transform_filter(
        select_sigma
    )
    for chart in [seir_chart, hypoexp_and_erlang_hawkesn_chart]
)
```

## Introduction

Since their introduction in the 1970s, Hawkes processes have been successfully applied in many different fields, including seismology, finance, and information science. Alan G. Hawkes also considered his class of stochastic processes "a possible epidemic model in large populations"{% fn 1 %}.

However, a model assuming an infinitely large population is often not suitable when modeling the spread of diseases. That is why many researchers preferred compartmental epidemic models (such as the SIR or SEIR model) to Hawkes processes.

Only recently, it has been shown that there is a link between Hawkes-like processes and the SIR compartmental model.{% fn 2 %} As Rizoiu et al. put it, "[t]his is significant, as it
indicates that tools developed for one approach can be applied to the other." However, this link is restricted to the SIR compartmental model. As this model lacks the capability to model a latent period, many researchers turn to more complex models such as the SEIR model and variants thereof. This is especially true for diffusion analyses in the current Covid-19 crisis.

We have discovered a link between the SEIR model and Hawkes-like processes.{% fn 3 %} In this blog post, we explain it briefly and give an overview of all the models mentioned so far.

But before we dive into the theory, let's get a feel of the models we will talk about. The following plot shows four subplots, each containing the total number of infections (y-axis) that occured until a certain day (x-axis) in a population of 10 thousand people using one specific simulation model. When you hover over the bars, you will notice that many infection counts are not integers; that is because they are calculated as the mean of many simulations. Experiment with the sliders below the plot and see how they affect the spread of a disease.

```python
#hide_input
(sir_chart | seir_chart) & (exp_hawkesn_chart | hypoexp_and_erlang_hawkesn_chart)
```

Comparing the subplots, we notice three things:

> Note: The two subplots on the left look similar. This is a consequence of the finding of Rizoiu et al. that we mentioned above: There is a link between the SIR model and Hawkes-like processes.

> Note: The two subplots on the right look similar. This is a consequence of our finding: There is a link between the SEIR model and Hawkes-like processes.

> Important: The subplots on the left and on the right are **very** different! Also note, that you can adjust the latency in the models plotted on the right (higher values for $\sigma$ implying less latency). The models on the left, however, are not capable of modeling latency (and thus incubation time). This underlines the importance of extending the link found by Rizoiu et al. to other compartmental models.

## Hawkes Processes

A Hawkes process is a type of counting process. A counting process counts events. These events can, for example, be infections (in epidemiology) or earthquakes (in seismology). Whenever an event occurs, the counting process experiences a jump of size $1$. Let's have a look at a sample path of a Hawkes process $N_t$ in the following figure (the blue line).

You will notice that its jumps are not evenly distributed over the observed time interval $[0, 60]$. Why is that?

It's because the behavior of $N_t$ is dictated by its so-called conditional intensity $\lambda_t$ (the orange line). Whenever $\lambda_t$ is high we have a high probability of events. A low $\lambda_t$, on the other hand, leads to fewer jumps in $N_t$. This property is valid for all counting processes.

What makes Hawkes processes so special is that their conditional intensity $\lambda_t$ again depends on the counting process. In the figure above we see that the jumps in $N_t$ also increase the conditional intensity.

In general, the conditional intensity of a Hawkes process has the form
\begin{align}
\lambda_t
= \mu(t) + \sum\limits_{t_j < t} \nu(t-t_j)
,
\end{align}
where $t_j$ is an event time and $\mu(t)$ as well as $\nu(t-t_j)$ are both $\geq 0$.{% fn 4 %}
We call $\mu(t)$ the *background intensity*. It is independent of $N_t$.

$\nu(t - t_j)$, on the other hand, depends on $t_j$ and thus on the history of the counting process $N_t$. We call $\nu$ the *kernel* of the Hawkes process.

In the figure above, $\mu(t) = \mu$ was constant and $\nu$ was an exponential kernel, this means
\begin{align} \label{exphawkes}
\lambda_t
= \mu
+
\sum\limits_{t_j < t} \kappa \theta \text{e}^{-\theta (t - t_j)}.
\end{align}

Here, $\kappa$ is called the *scale* parameter and $\theta$ is called the *decay* parameter. You may notice that this kernel equals the density of the exponential distribution with parameter $\theta$ scaled by the parameter $\kappa$.

What is important is that the exponential kernel leads to immediate jumps in $\lambda_t$ when an event occurs. When modeling infections, this means that an infection immediately increases the probability of further infections. This also means, that there is no way of taking a latent period (which is similar to the incubation period{% fn 5 %}) into account. We will address this issue below. But first, let's head to the other class of models we discuss in this post.

## Compartmental Epidemic Models

### The SIR Model

#### Overview

Compartmental epidemic models (such as SIR and SEIR) model the spread of a disease within some population of size $N$ by dividing this population into groups (so-called compartments). In the SIR model these groups are called
 - S (group of all *susceptibles*),
 - I (group of all *infected*), and
 - R (group of all *recovered*).

Susceptibles are healthy. After becoming infected, they move to compartment I. After some time infected people recover from a disease and hence move to compartment R. The possible transitions between compartments are outlined in the following figure.

The exact laws that govern the rates at wich people transition from one compartment to the next can be expressed in various ways. In the so-called CTMC-SIR model this is achieved via infinitesimal transition probabilities. For brevity (and as they are not our main focus), we will not present them here.

Let $S_t$ be the number of people in compartment S at time $t$. We define $I_t$ and $R_t$ analogously. Now $S_t$, $I_t$, and $R_t$ are stochastic processes. Now we can establish a link between the SIR model and Hawkes processes by finding similarities in conditional intensities.

#### The link to Hawkes processes

How does a conditional intensity look like in the SIR model? This depends on which process in this model we are considering. As we are interested in infections we might be tempted to choose the process $I_t$. However, $I_t$ is not a counting process, so we cannot compute a conditional intensity. (A counting process counts and thus, it is monotonically increasing, but $I_t$ decreases when people transition from I to R.)

We can solve this issue by choosing the process $I_t + R_t$. This represents a counting process, which counts all infections that have occurred until time $t$. Now it is possible to derive the conditional intensity $\lambda_t^{SIR}$ from the infinitesimal transition probabilities of the SIR model.

Unfortunately, the result is not directly comparable to the conditional intensity of a Hawkes process. This is because a Hawkes process can only model infections, whereas the SIR model also contains recoveries as events. Therefore, Rizoiu et al. calculated an expected value, integrating over the recovery times, which are not observable in the Hawkes process. If we call the set of all recovery times $\mathcal{T}$, then the main result of Rizoiu et al. is
\begin{align} \label{sirhawkesn}
\mathbb{E}_{\mathcal{T}} \left(  \lambda_t^{SIR} \right)
= \left( 1 - \frac{I_t + R_t}{N} \right)
\sum\limits_{t_j < t} \beta \text{e}^{-\gamma (t - t_j)},
\end{align}
where $\beta$ and $\gamma$ are the parameters of the SIR model. (They also show up in the infinitesimal transition probabilities of the model.) $\beta$ is governing the rate at which infections take place, whereas $\gamma$ determines how fast people recover.

Now this looks pretty much like the conditional intensity of a Hawkes process with exponential kernel! To see this more easily, just set $\mu = 0$,
\begin{align}
\kappa \theta = \beta,
\end{align}
and
\begin{align}
\theta = \gamma
\end{align}
in equation $\eqref{exphawkes}$ and compare it to $\eqref{sirhawkesn}$.
Now the only difference is the factor in front of the sum!

To eliminate this last difference between the two models, Rizoiu et al. defined the HawkesN process. This process is defined by its conditional intensity, which is the same as that of a Hawkes process, but without the background intensity $\mu(t)$ and with the additional factor $(1-N_t/N)$, where $N_t$ denotes the HawkesN process at time $t$. So compared to a Hawkes process, the HawkesN process lacks the parameter $\mu(t)$, but features the additional parameter $N$, which can be interpreted as the population's size.

This additional factor in the conditional intensity – let's call it HawkesN factor – has an important effect. As is the case with the Hawkes process, infections tend to increase the conditional intensity of the HawkesN process. However, as the number of events rises, this increasing effect declines for the HawkesN process and eventually, further events can even have a decreasing effect, since the factor in front of the sum decreases.

With diseases we make similar observations. At first, infections increase the probability of further infections. However, as more and more people become infected, the number of susceptibles declines and this lowers the probability of further infections, as there are fewer people left who can become infected.

> Note: Using the link found by Rizoiu et al. we can translate an SIR model easily into a HawkesN process and vice versa using the equations above. This allows one, for example, to estimate model parameters in one model and then make predictions using the other model.

### The SEIR Model

#### Overview

The SEIR model is a generalization of the SIR model, adding a compartment between S and I. This compartment is called E, which stands for *exposed*. Members of this group are infected, but are not able to infect others yet. After some time, people move on to compartment I, which means that they become infectious. The states and possible transitions are summarized in the following figure.

The additional compartment allows for modeling a latent period – a key characteristic of many infectious diseases. The duration of the latent period – that is the time a person spends in compartment E – depends on the incubation rate $\sigma$. The lower the value of $\sigma$, the longer the latent period. You can see this effect in the interactive chart in the introduction; just move the slider for $\sigma$ and see what happens.

#### The link to Hawkes processes

We have found that the SEIR model also has a close relationship to the HawkesN process. However, for the expectation of the conditional intensity $\lambda_t^{SEIR}$ to match that of a HawkesN process, the kernel of the HawkesN process must not be the exponential kernel, but rather the *hypoexponential* kernel or the *Erlang* kernel. The conditional intensity with a hypoexponential kernel looks like
\begin{align}
\lambda_t
= \left(
    1 - \frac{N_t}{N}
  \right)
  \sum_{t_j < t}
    \kappa \frac{\theta \sigma}{\theta - \sigma}
    \left(
      \text{e}^{-\sigma (t-t_j)} - \text{e}^{-\theta (t-t_j)}
    \right),
\end{align}
where everything that follows the scale parameter $\kappa$ is the density of a hypoexponential distribution.

You may notice that this formula does not work for the case $\sigma = \theta$, as we would divide by zero. We have shown that in this case the link between SEIR and HawkesN is established with the Erlang kernel, which leads to the conditional intensity
\begin{align}
\lambda_t
= \left(
    1 - \frac{N_t}{N}
  \right)
  \sum_{t_j < t}
    \kappa \theta^2
    (t-t_j)
    \text{e}^{-\theta (t-t_j)}
.
\end{align}

How do these formulas differ from the conditional intensity with an exponential kernel? Have a look at the following chart. You can select a sequence of event times below the chart and the plot will update accordingly.

```python
#hide_input
event_seq_ids = ["{0}  ",
                 "{0, 1, 2}  ",
                 "{0, 1, 2, ..., 9}  "]

lambda_t_unicode = "\u03BBₜ"

url_conditional_intensities = "https://raw.githubusercontent.com/yogabonito/blog/master/_notebooks/fig/conditional_intensities.json"

#url_conditional_intensities = "https://raw.githubusercontent.com/yogabonito/blog/7fe0ff549183263263ea8c41007d8ecde239c48c/_notebooks/fig/conditional_intensities.json"
#event_seq_ids = ["{0}",
#                 "{0, 1, 2}",
#                 "{0, 1, 2, ..., 9}"]

selection_kernel = alt.selection_multi(fields=['kernel'], bind='legend')

event_seq_radio = alt.binding_radio(options=event_seq_ids,
                                    name="Event times: ")
event_seq_select = alt.selection_single(fields=["event sequence"],
                                        bind=event_seq_radio,
                                        name="event Sequence",
                                        init={"event sequence": event_seq_ids[1]})

alt.Chart(url_conditional_intensities).mark_line().encode(
    x="t:Q",
    #y=lambda_t_unicode+":Q",
    y=alt.Y(
        lambda_t_unicode + ":Q",
        scale=alt.Scale(domain=(0, 3.5))
    ),
    color="kernel:N",
    tooltip=["t:Q", lambda_t_unicode + ":Q", "kernel:N"],
    opacity=alt.condition(selection_kernel, alt.value(1), alt.value(0.2)),
).add_selection(
    selection_kernel
).add_selection(
    event_seq_select
).transform_filter(
    event_seq_select
)
```

We can see that a high value of the parameter $\sigma$ leads to a conditional intensity that resembles that with an exponential kernel. That is because the hypoexponential kernel converges to the exponential one – a property inherited from the densities of the hypoexponential and the exponential distribution:
\begin{align}
\lim_{\sigma \to \infty}
\frac{\theta \sigma}{\theta - \sigma}
    \left(
      \text{e}^{-\sigma (t-t_j)} - \text{e}^{-\theta (t-t_j)}
    \right)
= \theta \text{e}^{-\theta (t - t_j)}.
\end{align}

The same relationship exists between the SEIR model and the SIR model. When $\sigma$ goes to infinity, the time spent in compartment E goes to zero, thus eliminating the need for this compartment.

Now, let's have a look at smaller values of $\sigma$. They lead to a large delay with which the conditional intensity increases. In the SEIR model a lower $\sigma$ means that people spend more time in compartment E where they are not yet able to infect others. Thus, an infection does not immediately increase the probability of further infections but only does so with some delay.

If we select the event sequence $\{0, 1, 2, \ldots, 9\}$ below the chart, we can see that the conditional intensity drops to zero at $t=9$. This is because in this example we assumed a population of only ten people. At $t=9$, the $10^{\text{th}}$ event occurs, so the factor $(1 - 10 / 10)$ in the conditional intensity of a HawkesN process becomes zero.

In an S(E)IR model, this $10^{\text{th}}$ infection event has the same effect. As soon as everybody left the group of susceptibles, there is nobody left who can get infected.

The event sequence with ten events nicely reflects how infections can have an increasing effect on the conditional intensity at first. However, we can also see that the last two events decrease $\lambda_t$ – just as discussed above when we defined the HawkesN factor. Note that this decreasing effect eventually prevails irrespective of the kernel function used.

## Summary & Discussion

In this blog post we have briefly presented the link between the SEIR model and Hawkes processes, which can be seen as a generalization of Rizoiu et al.'s work. This means, that now this link can be used when modeling diseases that feature a latent period, during which infected people are not infectious.

This, however, does not imply that the SEIR model is suitable for every disease and one has to keep in mind that the latent period is not necesserily the same as the incubation period{% fn 5 %}. This distinction is crucial for many diseases (e.g., Covid-19) and can complicate the the application of the SEIR model.

Nonetheless, even in the case of Covid-19 there are works showing the application of the SEIR model. To this end, sometimes the model is adapted. One such adjustment can be the incorporation of quarantine measures.

Further generalizations of the link between SEIR and HawkesN are possible and we will continue our research on this topic in order to offer tools related to Hawkes-processes to a wider range of compartmental epidemic models.

## References

{{ 'Hawkes (1971): "Spectra of some self-exciting and mutually exciting point processes"' | fndetail: 1 }}

{{ 'Rizoiu et al. (2018): "SIR-Hawkes: Linking Epidemic Models and Hawkes Processes to Model Diffusions in Finite Populations"' | fndetail: 2 }}

{{ 'https://diglib.tugraz.at/download.php?id=5d4a774705898&location=aleph' | fndetail: 3 }}

{{ 'https://mathworld.wolfram.com/HawkesProcess.html' | fndetail: 4 }}

{{ 'https://en.wikipedia.org/wiki/Incubation_period' | fndetail: 5 }}

```python

```


https://yangtavares.com/2017/07/31/creating-a-simple-overlay-for-pynq-z1-board-from-vivado-hlx/

```python
#This notebook also uses the `(some) LaTeX environments for Jupyter`
#https://github.com/ProfFan/latex_envs wich is part of the
#jupyter_contrib_nbextensions package

from myhdl import *
from myhdlpeek import Peeker
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline

from sympy import *
init_printing()

import itertools

from IPython.display import clear_output

#https://github.com/jrjohansson/version_information
%load_ext version_information
%version_information myhdl, myhdlpeek, numpy, pandas, matplotlib, sympy, itertools, IPython
```

Software	Version

Python	3.6.2 64bit [GCC 4.4.7 20120313 (Red Hat 4.4.7-1)]

IPython	6.2.1

OS	Linux 4.15.0 30 generic x86_64 with debian stretch sid

myhdl	0.10

myhdlpeek	0.0.6

numpy	1.13.3

pandas	0.23.3

matplotlib	2.1.0

sympy	1.1.2.dev

itertools	The 'itertools' distribution was not found and is required by the application

IPython	6.2.1

Sun Sep 16 17:50:29 2018 MDT

```python
#helper  functions to read in the .v and .vhd generated files into python
def VerilogTextReader(loc, printresult=True):
    with open(f'{loc}.v', 'r') as vText:
        VerilogText=vText.read()
    if printresult:
        print(f'***Verilog modual from {loc}.v***\n\n', VerilogText)
    return VerilogText

def VHDLTextReader(loc, printresult=True):
    with open(f'{loc}.vhd', 'r') as vText:
        VerilogText=vText.read()
    if printresult:
        print(f'***VHDL modual from {loc}.vhd***\n\n', VerilogText)
    return VerilogText

def ConstraintXDCTextReader(loc, printresult=True):
    with open(f'{loc}.xdc', 'r') as xdcText:
        ConstraintText=xdcText.read()
    if printresult:
        print(f'***Constraint file from {loc}.xdc***\n\n', ConstraintText)
    return ConstraintText
```

# IP  ClockDivider

## Non SoC Test

```python
@block
def ClockDivider(Divisor, clkOut, clk,rst):
    """
    Simple Clock Divider based on the Digilint Clock Divider
    https://learn.digilentinc.com/Documents/262

    Input:
        Divisor(32 bit): the clock frequncy divide by value
        clk(bool): The input clock
        rst(bool): clockDivider Reset

    Ouput:
        clkOut(bool): the divided clock ouput
        count(32bit): the value of the internal counter
    """

    count_i=Signal(modbv(0)[32:])
    @always(clk.posedge, rst.posedge)
    def counter():
        if rst:
            count_i.next=0
        elif count_i==(Divisor-1):
            count_i.next=0
        else:
            count_i.next=count_i+1

    clkOut_i=Signal(bool(0))
    @always(clk.posedge, rst.posedge)
    def clockTick():
        if rst:
            clkOut_i.next=0
        elif count_i==(Divisor-1):
            clkOut_i.next=not clkOut_i
        else:
            clkOut_i.next=clkOut_i

    @always_comb
    def OuputBuffer():
        clkOut.next=clkOut_i

    return instances()

```

```python
RefClkFreq=125e6
TargetClkFreq=40
DivsionFactor=int(RefClkFreq/TargetClkFreq)
DivsionFactor
```

```python
Peeker.clear()
clk=Signal(bool(0)); Peeker(clk, 'clk')
Divisor=Signal(intbv(DivsionFactor)[32:]); Peeker(Divisor, 'Divisor')
clkOut=Signal(bool(0)); Peeker(clkOut, 'clkOut')
rst=Signal(bool(0)); Peeker(rst, 'rst')

DUT=ClockDivider(Divisor, clkOut, clk,rst)
DUT.convert()
VerilogTextReader('ClockDivider');
```

    ***Verilog modual from ClockDivider.v***

     // File: ClockDivider.v
    // Generated by MyHDL 0.10
    // Date: Sun Sep 16 17:50:31 2018

    `timescale 1ns/10ps

    module ClockDivider (
        Divisor,
        clkOut,
        clk,
        rst
    );
    // Simple Clock Divider based on the Digilint Clock Divider
    // https://learn.digilentinc.com/Documents/262
    //
    // Input:
    //     Divisor(32 bit): the clock frequncy divide by value
    //     clk(bool): The input clock
    //     rst(bool): clockDivider Reset
    //
    // Ouput:
    //     clkOut(bool): the divided clock ouput
    //     count(32bit): the value of the internal counter

    input [31:0] Divisor;
    output clkOut;
    wire clkOut;
    input clk;
    input rst;

    reg [31:0] count_i;
    reg clkOut_i;

    always @(posedge clk, posedge rst) begin: CLOCKDIVIDER_COUNTER
        if (rst) begin
            count_i <= 0;
        end
        else if (($signed({1'b0, count_i}) == ($signed({1'b0, Divisor}) - 1))) begin
            count_i <= 0;
        end
        else begin
            count_i <= (count_i + 1);
        end
    end

    always @(posedge clk, posedge rst) begin: CLOCKDIVIDER_CLOCKTICK
        if (rst) begin
            clkOut_i <= 0;
        end
        else if (($signed({1'b0, count_i}) == ($signed({1'b0, Divisor}) - 1))) begin
            clkOut_i <= (!clkOut_i);
        end
        else begin
            clkOut_i <= clkOut_i;
        end
    end

    assign clkOut = clkOut_i;

    endmodule

```python
ConstraintXDCTextReader('ClockAXI');
```

    ***Constraint file from ClockAXI.xdc***

     ## Switches
    set_property -dict {PACKAGE_PIN M20 IOSTANDARD LVCMOS33} [get_ports {sws_2bits_tri_i[0]}]
    set_property -dict {PACKAGE_PIN M19 IOSTANDARD LVCMOS33} [get_ports {sws_2bits_tri_i[1]}]

    ## Buttons
    set_property -dict {PACKAGE_PIN D19 IOSTANDARD LVCMOS33} [get_ports {btns_4bits_tri_i[0]}]
    set_property -dict {PACKAGE_PIN D20 IOSTANDARD LVCMOS33} [get_ports {btns_4bits_tri_i[1]}]
    set_property -dict {PACKAGE_PIN L20 IOSTANDARD LVCMOS33} [get_ports {btns_4bits_tri_i[2]}]
    set_property -dict {PACKAGE_PIN L19 IOSTANDARD LVCMOS33} [get_ports {btns_4bits_tri_i[3]}]

    ## LEDs
    set_property -dict {PACKAGE_PIN R14 IOSTANDARD LVCMOS33} [get_ports {clkOut}] ## leds_4bits_tri_o[0]
    ##set_property -dict {PACKAGE_PIN P14 IOSTANDARD LVCMOS33} [get_ports {leds_4bits_tri_o[1]}]
    ##set_property -dict {PACKAGE_PIN N16 IOSTANDARD LVCMOS33} [get_ports {leds_4bits_tri_o[2]}]
    ##set_property -dict {PACKAGE_PIN M14 IOSTANDARD LVCMOS33} [get_ports {leds_4bits_tri_o[3]}]

## AXI SoC

The IP Project is `myClockDividerIP_v1_0`
After adding the Verilog Clock Divider Module under sources there are two addintal modules that where created with the IP that are the AXI Lite Slave IP Connection Header `myClockDividerIP_v1_0.v` and the AXI Slave BUS controler `myClockDividerIP_v1_0_S00_AXI_inst.v`

```python

```


# How To Find Out Whether To Pursue A Career In Quantum Computing — Today!

This post appeared first on [Medium](https://pyqml.medium.com/how-to-find-out-whether-to-pursue-a-career-in-quantum-computing-today-d1c7093811e8) and my weekly [email-course](https://pyqml.substack.com/p/how-to-find-out-whether-to-pursue?s=w).

Quantum computing is one of the most hyped topics today! But, let’s face it — it is not precisely a beginner-friendly field. You need to be highly proficient in math, physics, and computer science.

This is a severe problem for anyone interested in pursuing a career in this field because you’ll probably need to have already completed your university studies before you can get your hands on actual problems in the field.
That’s not exactly the least risky way to find out whether or not a career in quantum computing is right for you.

So, what do you do instead? — How about working on a specific problem in the field that someone is willing to spend $100,000\$$ on?

Here’s the deal. IBM just announced its second Quantum Open Science Prize. They ask for a solution to a quantum simulation problem. They want us to simulate a Heisenberg model Hamiltonian for a three-particle system on IBM Quantum’s 7-qubit Jakarta system using Trotterization.

Don’t worry — yet. Of course, IBM explains what a Heisenberg model Hamiltonian and Trotterization are in the material appended to the announcement.
The Heisenberg Hamiltonian $H_{Heis}$ is defined as

$
\begin{equation}
H_{\text{Heis}} = \sum_{\langle ij \rangle}^{N} J \left(\sigma_x^{(i)}\sigma_x^{(j)} + \sigma_y^{(i)}\sigma_y^{(j)} + \sigma_z^{(i)}\sigma_z^{(j)}\right).
\end{equation}
$

with 𝑁 is the number of spin-1/2 particles in the model, 𝜎𝑥, 𝜎𝑦, and 𝜎𝑧 are Pauli operators, and the 𝑖 and 𝑗 superscripts label which qubit they act on.

Trotterization refers to a method of simulating the sum-of-terms of a Hamiltonian by simulating each one separately for a small-time slice.

Everything’s clear. — Of course, it’s not! Do worry — now.

I appreciate all the effort the IBM folks invested in the material preparation. But it clearly addresses a single audience: quantum physicists!

So, we’re back where we started. You’d have to earn your physics degree before you could get your hands dirty on the task.

Don’t give up that fast. You can learn a lot from this announcement alone on whether a career in quantum computing is right for you or not.

First, let’s talk about all this physics jargon. You better get used to it. I mean, you don’t need to understand all of it right away. But, if you end up in a quantum computing career eventually, you’ll need to work yourself through these equations. It’s comparable to being a software developer and reading someone else’s code. It’s part of the job.
Second, let’s look at the code. IBM provides a working example of the code they expect from us. It’s a lot longer than the following snippet, but this gives you the first impression of it.

```python
### YOUR TROTTERIZATION GOES HERE -- START (beginning of example)

# Parameterize variable t to be evaluated at t=pi later
t = Parameter('t')
# Build a subcircuit for XX(t) two-qubit gate
XX_qr = QuantumRegister(2)
XX_qc = QuantumCircuit(XX_qr, name='XX')

XX_qc.ry(np.pi/2,[0,1])

...

# Convert custom quantum circuit into a gate
ZZ = ZZ_qc.to_instruction()
Combine subcircuits into single gate representing one ($n=1$) trotter step.

# Combine subcircuits into a single multiqubit gate representing a single trotter step
num_qubits = 3

Trot_qr = QuantumRegister(num_qubits)
Trot_qc = QuantumCircuit(Trot_qr, name='Trot')

for i in range(0, num_qubits - 1):
    Trot_qc.append(ZZ, [Trot_qr[i], Trot_qr[i+1]])
    Trot_qc.append(YY, [Trot_qr[i], Trot_qr[i+1]])
    Trot_qc.append(XX, [Trot_qr[i], Trot_qr[i+1]])

# Convert custom quantum circuit into a gate
Trot_gate = Trot_qc.to_instruction()

### YOUR TROTTERIZATION GOES HERE -- FINISH (end of example)
```

The code is as good as the mathematical equation. You don’t need to understand it right away, but you certainly must be willing to work yourself through it.

Third, let’s strip away all these distracting details and look at what they really want. IBM says they want us to simulate a three-particle system using Trotterization. But wait! They provide us with a perfect example. We can run it on our laptop, and it produces a fidelity (that is the performance score) of almost 1.0 — that is the best we can get. So, maybe simulating a three-particle system using Trotterization is not the tricky part.

The real problem is that this code works only on a fault-tolerant quantum device. We get this if we simulate a quantum computer on our laptop. It works pretty well for a handful of qubits. But, our computer won’t simulate anything beyond twenty qubits. And, the best classical supercomputer in the world won’t be able to simulate anything beyond fifty qubits.

So, when we want to simulate such a fifty-particle quantum system, we need to run it on an actual quantum device. Unfortunately, real quantum devices are not fault-tolerant but noisy. And the problem with noise is that it compounds and propagates. The more qubits and gates we use, the bigger the noise becomes. It inevitably ruins any meaningful computation and produces nothing but random garbage.

So, let’s get back to the three-particle system that we can simulate classically. Our task is to simulate it on a real and noisy quantum device — their seven-qubit Jakarta system. So, instead of three fault-tolerant qubits, we get seven noisy ones. But, of course, seven physical qubits are far from enough to run a circuit that uses three fault-tolerant qubits. Estimations say we need almost a thousand current noisy qubits to get a single fault-tolerant one.
Consequently, we can’t simulate a three-particle system perfectly on an actual quantum device. So the open question is, what’s the best we can achieve?

Essentially, IBM says:

    Here’s a pretty cool quantum algorithm that simulates particles. But it doesn’t work on our current quantum devices because they are too noisy. Please help us to come up with an algorithm that works on our noisy devices.

So, IBM’s challenge is not all about simulating a Heisenberg model Hamiltonian for a three-particle system. Instead, it is about how to do it on a noisy device. Therefore, how to cope with quantum errors will not only be a significant part of the solution, but it is the third part you’ll need to cope with when you pursue a career in quantum computing.

Are you still thinking about joining this field? Great! Then, there’s but one missing part. You need to get your hands on the problem and solve it. It’ll give you first-hand experience in what it means to be in the field of quantum computing.

Did I miss something? Oh, right. What about all the prerequisites? How are you supposed to know anything about a Heisenberg model Hamiltonian, Trotterization, and the quantum error correction?

Maybe, I can help you out. I plan to dissect this challenge in my weekly posts. The submission deadline is April 16, 2022. So, we have quite some time to get through all of this.

Are you in? We’ll start next week!

```python

```