# Start-to-Finish Example: [TOV](https://en.wikipedia.org/wiki/Tolman%E2%80%93Oppenheimer%E2%80%93Volkoff_equation) Neutron Star Simulation: The "Hydro without Hydro" Test

## Authors: Zach Etienne & Phil Chang
### Formatting improvements courtesy Brandon Clark

## This module sets up initial data for a neutron star on a spherical numerical grid, using the approach [documented in the previous NRPy+ module](Tutorial-Start_to_Finish-BSSNCurvilinear-Setting_up_TOV_initial_data.ipynb), and then evolves these initial data forward in time. The aim is to reproduce the results from [Baumgarte, Hughes, and Shapiro]( https://arxiv.org/abs/gr-qc/9902024) (which were performed using Cartesian grids); demonstrating that the extrinsic curvature and Hamiltonian constraint violation converge to zero with increasing numerical resolution

**This module has been validated to exhibit convergence to zero of the Hamiltonian constraint violation at the expected order to the exact solution (see [plot](#convergence) at bottom). Note that convergence in the region causally influenced by the surface of the star will possess lower convergence order due to the sharp drop to zero in $T^{\mu\nu}$.**

### NRPy+ Source Code for this module:

* [TOV/TOV_Solver.py](../edit/TOV/TOV_Solver.py); ([**NRPy+ Tutorial module reviewing mathematical formulation and equations solved**](Tutorial-ADM_Initial_Data-TOV.ipynb)); ([**start-to-finish NRPy+ Tutorial module demonstrating that initial data satisfy Hamiltonian constraint**](Tutorial-Start_to_Finish-BSSNCurvilinear-Setting_up_TOV_initial_data.ipynb)): Tolman-Oppenheimer-Volkoff (TOV) initial data; defines all ADM variables and nonzero $T^{\mu\nu}$ components in Spherical basis.
* [BSSN/ADM_Numerical_Spherical_or_Cartesian_to_BSSNCurvilinear.py](../edit/BSSN/ADM_Numerical_Spherical_or_Cartesian_to_BSSNCurvilinear.py); [\[**tutorial**\]](Tutorial-ADM_Initial_Data-Converting_Numerical_ADM_Spherical_or_Cartesian_to_BSSNCurvilinear.ipynb): *Numerical* Spherical ADM$\to$Curvilinear BSSN converter function
* [BSSN/BSSN_constraints.py](../edit/BSSN/BSSN_constraints.py); [\[**tutorial**\]](Tutorial-BSSN_constraints.ipynb): Hamiltonian constraint in BSSN curvilinear basis/coordinates

## Introduction:
Here we use NRPy+ to generate initial data for a [simple polytrope TOV star](https://en.wikipedia.org/wiki/Tolman%E2%80%93Oppenheimer%E2%80%93Volkoff_equation).

The entire algorithm is outlined below, with NRPy+-based components highlighted in green.

1. Allocate memory for gridfunctions, including temporary storage for the RK4 time integration.
1. Set gridfunction values to initial data (**This module**).
1. Evolve the system forward in time using RK4 time integration. At each RK4 substep, do the following:
    1. Evaluate BSSN RHS expressions.
    1. Apply singular, curvilinear coordinate boundary conditions [*a la* the SENR/NRPy+ paper](https://arxiv.org/abs/1712.07658)
    1. Apply constraints on conformal 3-metric: $\det{\bar{\gamma}_{ij}}=\det{\hat{\gamma}_{ij}}$
1. At the end of each iteration in time, output the Hamiltonian constraint violation.
1. Repeat above steps at two numerical resolutions to confirm convergence to zero.

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

This module is organized as follows

1. [Step 1](#initializenrpy): Set core NRPy+ parameters for numerical grids and reference metric
1. [Step 2](#adm_id_tov): Set up ADM initial data for polytropic TOV Star
    1. [Step 2.a](#tov_interp): Interpolating the TOV data file as needed
    1. [Step 2.b](#source): Compute source terms $S_{ij}$, $S_{i}$, $S$, and $\rho$
    1. [Step 2.c](#jacobian): Jacobian transformation on the ADM/BSSN source terms
    1. [Step 2.d](#tensor): Rescale tensorial quantities
1. [Step 3](#adm_id_spacetime): Take care of the ADM spacetime quantity initial data conversion
1. [Step 4](#bssn_source): Set up the BSSN right-hand-side (RHS) expressions, and add the *rescaled* $T^{\mu\nu}$ source terms
1. [Step 5](#hydro_hydro): Validating "Hydro without Hydro" test satisfies the Hamiltonian constraint
    1. [Step 5.a](#ham_const_output): Output the Hamiltonian Constraint
    1. [Step 5.b](#apply_bcs): Apply singular, curvilinear coordinate boundary conditions
    1. [Step 5.c](#enforce3metric): Enforce conformal 3-metric $\det{\bar{\gamma}_{ij}}=\det{\hat{\gamma}_{ij}}$ constraint
1. [Step 6](#mainc): TOV_Playground.c: The Main C Code
1. [Step 7](#visual): Data Visualization Animations
1. [Step 8](#convergence): Validation: Convergence of numerical errors (Hamiltonian constraint violation) to zero
1. [Step 9](#latex_pdf_output): Output this module to $\LaTeX$-formatted PDF file

# Step 1: Set core NRPy+ parameters for numerical grids and reference metric \[Back to [top](#toc)\]
$$\label{initializenrpy}$$

```python
# First we import needed core NRPy+ modules
from outputC import *
import NRPy_param_funcs as par
import grid as gri
import loop as lp
import indexedexp as ixp
import finite_difference as fin
import reference_metric as rfm
import BSSN.BSSN_quantities as Bq
import BSSN.BSSN_RHSs as rhs
import BSSN.BSSN_gauge_RHSs as gaugerhs
import BSSN.BSSN_T4UUmunu_vars as BTmunu

thismodule = "TOV_ID_setup"

# Set spatial dimension (must be 3 for BSSN)
DIM = 3
par.set_parval_from_str("grid::DIM",DIM)

# Then we set the coordinate system for the numerical grid
par.set_parval_from_str("reference_metric::CoordSystem","Spherical")
rfm.reference_metric() # Create ReU, ReDD needed for rescaling B-L initial data, generating BSSN RHSs, etc.

# Then we set the phi axis to be the symmetry axis; i.e., axis "2", corresponding to the i2 direction.
#      This sets all spatial derivatives in the phi direction to zero.
par.set_parval_from_str("indexedexp::symmetry_axes","2")

# We will need the h_{ij} quantities defined within BSSN_RHSs
#    below when we enforce the gammahat=gammabar constraint
par.set_parval_from_str("BSSN.BSSN_quantities::EvolvedConformalFactor_cf","W")

par.set_parval_from_str("BSSN.BSSN_gauge_RHSs::ShiftEvolutionOption", "Frozen")
par.set_parval_from_str("BSSN.BSSN_gauge_RHSs::LapseEvolutionOption", "HarmonicSlicing")
rhs.BSSN_RHSs()
gaugerhs.BSSN_gauge_RHSs()

#################
# Next output C headers related to the numerical grids we just set up:
#################

# First output the coordinate bounds xxmin[] and xxmax[]:
with open("BSSN/xxminmax.h", "w") as file:
    file.write("const REAL xxmin[3] = {"+str(rfm.xxmin[0])+","+str(rfm.xxmin[1])+","+str(rfm.xxmin[2])+"};\n")
    file.write("const REAL xxmax[3] = {"+str(rfm.xxmax[0])+","+str(rfm.xxmax[1])+","+str(rfm.xxmax[2])+"};\n")

# Generic coordinate NRPy+ file output, Part 2: output the conversion from (x0,x1,x2) to Cartesian (x,y,z)
outputC([rfm.xxCart[0],rfm.xxCart[1],rfm.xxCart[2]],["xCart[0]","xCart[1]","xCart[2]"],
        "BSSN/xxCart.h")

# Next output the proper distance between gridpoints in given coordinate system.
#     This is used to find the minimum timestep.
dxx     = ixp.declarerank1("dxx",DIM=3)
ds_dirn = rfm.ds_dirn(dxx)
outputC([ds_dirn[0],ds_dirn[1],ds_dirn[2]],["ds_dirn0","ds_dirn1","ds_dirn2"],"BSSN/ds_dirn.h")

# Register the Hamiltonian as a gridfunction, to be used later.
H = gri.register_gridfunctions("AUX","H")
```

    initialize_param() minor warning: Did nothing; already initialized parameter reference_metric::M_PI
    initialize_param() minor warning: Did nothing; already initialized parameter reference_metric::RMAX
    initialize_param() minor warning: Did nothing; already initialized parameter reference_metric::M_PI
    initialize_param() minor warning: Did nothing; already initialized parameter reference_metric::RMAX
    initialize_param() minor warning: Did nothing; already initialized parameter reference_metric::M_PI
    initialize_param() minor warning: Did nothing; already initialized parameter reference_metric::RMAX
    Wrote to file "BSSN/xxCart.h"
    Wrote to file "BSSN/ds_dirn.h"

# Step 2: Set up ADM initial data for polytropic TOV Star \[Back to [top](#toc)\]
$$\label{adm_id_tov}$$

As documented [in the TOV Initial Data NRPy+ Tutorial Module](Tutorial-TOV_Initial_Data.ipynb) ([older version here](Tutorial-GRMHD_UnitConversion.ipynb)), we will now set up TOV initial data, storing the densely-sampled result to file (***Courtesy Phil Chang***).

The TOV solver uses an ODE integration routine provided by scipy, so we first make sure that scipy is installed:

```python
!pip install scipy > /dev/null
```

Next we call the [TOV.TOV_Solver() function](../edit/TOV/TOV_Solver.py) ([NRPy+ Tutorial module](Tutorial-ADM_Initial_Data-TOV.ipynb)) to set up the initial data, using the default parameters for initial data. This function outputs the solution to a file named "outputTOVpolytrope.txt".

```python
import TOV.TOV_Solver as TOV
TOV.TOV_Solver()
```

    Just generated a TOV star with R_Schw = 0.9565681425227097 , M = 0.14050303285288188 , M/R_Schw = 0.1468824086931645 .

## Step 2.a: Interpolating the TOV data file as needed \[Back to [top](#toc)\]
$$\label{tov_interp}$$

The TOV data file just written stored $\left(r,\rho(r),P(r),M(r),e^{\nu(r)}\right)$, where $\rho(r)$ is the total mass-energy density (cf. $\rho_{\text{baryonic}}$).

**METRIC DATA IN TERMS OF ADM QUANTITIES**

The [TOV line element](https://en.wikipedia.org/wiki/Tolman%E2%80%93Oppenheimer%E2%80%93Volkoff_equation) in *Schwarzschild coordinates* is written (in the $-+++$ form):
$$
ds^2 = - c^2 e^\nu dt^2 + \left(1 - \frac{2GM}{rc^2}\right)^{-1} dr^2 + r^2 d\Omega^2.
$$

In *isotropic coordinates* with $G=c=1$ (i.e., the coordinate system we'd prefer to use), the ($-+++$ form) line element is written:
$$
ds^2 = - e^{\nu} dt^2 + e^{4\phi} \left(d\bar{r}^2 + \bar{r}^2 d\Omega^2\right),
$$
where $\phi$ here is the *conformal factor*.

The ADM 3+1 line element for this diagonal metric in isotropic spherical coordinates is given by:
$$
ds^2 = (-\alpha^2 + \beta_k \beta^k) dt^2 + \gamma_{\bar{r}\bar{r}} d\bar{r}^2 + \gamma_{\theta\theta} d\theta^2+ \gamma_{\phi\phi} d\phi^2,
$$

from which we can immediately read off the ADM quantities:
\begin{align}
\alpha &= e^{\nu(\bar{r})/2} \\
\beta^k &= 0 \\
\gamma_{\bar{r}\bar{r}} &= e^{4\phi}\\
\gamma_{\theta\theta} &= e^{4\phi} \bar{r}^2 \\
\gamma_{\phi\phi} &= e^{4\phi} \bar{r}^2 \sin^2 \theta \\
\end{align}

**STRESS-ENERGY TENSOR $T^{\mu\nu}$**

We will also need the stress-energy tensor $T^{\mu\nu}$. [As discussed here](https://en.wikipedia.org/wiki/Tolman%E2%80%93Oppenheimer%E2%80%93Volkoff_equation), the stress-energy tensor is diagonal:

\begin{align}
T^t_t &= -\rho \\
T^i_j &= P \delta^i_j \\
\text{All other components of }T^\mu_\nu &= 0.
\end{align}

Since $\beta^i=0$ the inverse metric expression simplifies to (Eq. 4.49 in [Gourgoulhon](https://arxiv.org/pdf/gr-qc/0703035.pdf)):
$$
g^{\mu\nu} = \begin{pmatrix}
-\frac{1}{\alpha^2} & \frac{\beta^i}{\alpha^2} \\
\frac{\beta^i}{\alpha^2} & \gamma^{ij} - \frac{\beta^i\beta^j}{\alpha^2}
\end{pmatrix} =
\begin{pmatrix}
-\frac{1}{\alpha^2} & 0 \\
0 & \gamma^{ij}
\end{pmatrix},
$$

and since the 3-metric is diagonal we get

\begin{align}
\gamma^{\bar{r}\bar{r}} &= e^{-4\phi}\\
\gamma^{\theta\theta} &= e^{-4\phi}\frac{1}{\bar{r}^2} \\
\gamma^{\phi\phi} &= e^{-4\phi}\frac{1}{\bar{r}^2 \sin^2 \theta}.
\end{align}

Thus raising $T^\mu_\nu$ yields a diagonal $T^{\mu\nu}$

\begin{align}
T^{tt} &= -g^{tt} \rho = \frac{1}{\alpha^2} \rho = e^{-\nu(\bar{r})} \rho \\
T^{\bar{r}\bar{r}} &= g^{\bar{r}\bar{r}} P = \frac{1}{e^{4 \phi}} P \\
T^{\theta\theta} &= g^{\theta\theta} P = \frac{1}{e^{4 \phi}\bar{r}^2} P\\
T^{\phi\phi} &= g^{\phi\phi} P = \frac{1}{e^{4\phi}\bar{r}^2 \sin^2 \theta} P
\end{align}

As all input quantities are functions of $r$, we will simply read the solution from file and interpolate it to the values of $r$ needed by the initial data.

1. First we define functions ID_TOV_ADM_quantities() and ID_TOV_TUPMUNU() that call the [1D TOV interpolator function](../edit/TOV/tov_interp.h) to evaluate the ADM spacetime quantities and $T^{\mu\nu}$, respectively, at any given point $(r,\theta,\phi)$ in the Spherical basis. All quantities are defined as above.
1. Next we will construct the BSSN/ADM source terms $\{S_{ij},S_{i},S,\rho\}$ in the Spherical basis
1. Then we will perform the Jacobian transformation on $\{S_{ij},S_{i},S,\rho\}$ to the desired (xx0,xx1,xx2) basis
1. Next we call the *Numerical* Spherical ADM$\to$Curvilinear BSSN converter function to conver the above ADM quantities to the rescaled BSSN quantities in the desired curvilinear coordinate system: [BSSN/ADM_Numerical_Spherical_or_Cartesian_to_BSSNCurvilinear.py](../edit/BSSN/ADM_Numerical_Spherical_or_Cartesian_to_BSSNCurvilinear.py); [\[**tutorial**\]](Tutorial-ADM_Initial_Data-Converting_Numerical_ADM_Spherical_or_Cartesian_to_BSSNCurvilinear.ipynb).

```python
with open("BSSN/ID_TOV_ADM_quantities.h", "w") as file:
    file.write("""
// This function takes as input either (x,y,z) or (r,th,ph) and outputs
//   all ADM quantities in the Cartesian or Spherical basis, respectively.
void ID_TOV_ADM_quantities(
                     const REAL xyz_or_rthph[3],

                     const ID_inputs other_inputs,

                     REAL *gammaDD00,REAL *gammaDD01,REAL *gammaDD02,REAL *gammaDD11,REAL *gammaDD12,REAL *gammaDD22,
                     REAL *KDD00,REAL *KDD01,REAL *KDD02,REAL *KDD11,REAL *KDD12,REAL *KDD22,
                     REAL *alpha,
                     REAL *betaU0,REAL *betaU1,REAL *betaU2,
                     REAL *BU0,REAL *BU1,REAL *BU2) {
      const REAL r  = xyz_or_rthph[0];
      const REAL th = xyz_or_rthph[1];
      const REAL ph = xyz_or_rthph[2];

      REAL rho,P,M,expnu,exp4phi;
      TOV_interpolate_1D(r,other_inputs.Rbar,other_inputs.Rbar_idx,other_inputs.interp_stencil_size,
                         other_inputs.numlines_in_file,
                         other_inputs.r_Schw_arr,other_inputs.rho_arr,other_inputs.P_arr,other_inputs.M_arr,
                         other_inputs.expnu_arr,other_inputs.exp4phi_arr,other_inputs.rbar_arr,
                         &rho,&P,&M,&expnu,&exp4phi);

      *alpha = sqrt(expnu);
      // \gamma_{rbar rbar} = exp(4 phi)
      *gammaDD00 = exp4phi;
      // \gamma_{thth} = r^2 * exp(4 phi)
      *gammaDD11 = r*r * exp4phi;
      // \gamma_{phph} = r^2 sin^2(th) * exp(4 phi)
      *gammaDD22 = r*r*sin(th)*sin(th) * exp4phi;

      // All other quantities ARE ZERO:
      *gammaDD01 = 0.0;
      *gammaDD02 = 0.0;
      *gammaDD12 = 0.0;

      *KDD00 = 0.0;
      *KDD01 = 0.0;
      *KDD02 = 0.0;
      *KDD11 = 0.0;
      *KDD12 = 0.0;
      *KDD22 = 0.0;

      *betaU0 = 0.0;
      *betaU1 = 0.0;
      *betaU2 = 0.0;

      *BU0 = 0.0;
      *BU1 = 0.0;
      *BU2 = 0.0;
}\n""")

with open("BSSN/ID_TOV_TUPMUNU.h", "w") as file:
    file.write("""
// This function takes as input either (x,y,z) or (r,th,ph) and outputs
//   all ADM quantities in the Cartesian or Spherical basis, respectively.
void ID_TOV_TUPMUNU(const REAL xyz_or_rthph[3], const ID_inputs other_inputs,

                    REAL *T4UU00,REAL *T4UU01,REAL *T4UU02,REAL *T4UU03,
                    /**/         REAL *T4UU11,REAL *T4UU12,REAL *T4UU13,
                    /**/                      REAL *T4UU22,REAL *T4UU23,
                    /**/                                   REAL *T4UU33) {
      const REAL r  = xyz_or_rthph[0];
      const REAL th = xyz_or_rthph[1];
      const REAL ph = xyz_or_rthph[2];

      REAL rho,P,M,expnu,exp4phi;
      TOV_interpolate_1D(r,other_inputs.Rbar,other_inputs.Rbar_idx,other_inputs.interp_stencil_size,
                         other_inputs.numlines_in_file,
                         other_inputs.r_Schw_arr,other_inputs.rho_arr,other_inputs.P_arr,other_inputs.M_arr,
                         other_inputs.expnu_arr,other_inputs.exp4phi_arr,other_inputs.rbar_arr,
                         &rho,&P,&M,&expnu,&exp4phi);

      //T^tt = e^(-nu) * rho
      *T4UU00 = rho / expnu;
      //T^rr = P / exp4phi
      *T4UU11 = P / exp4phi;
      //T^thth = P / (r^2 * exp4phi)
      *T4UU22 = P / (r*r * exp4phi);
      //T^phph = P / (r^2 * sin^2(th) * exp4phi)
      *T4UU33 = P / (r*r * sin(th)*sin(th) * exp4phi);

      // All other components ARE ZERO:
      *T4UU01 = 0; *T4UU02 = 0; *T4UU03 = 0;
      /**/         *T4UU12 = 0; *T4UU13 = 0;
      /**/                      *T4UU23 = 0;
}\n""")
```

## Step 2.b: Compute source terms $S_{ij}$, $S_{i}$, $S$, and $\rho$ \[Back to [top](#toc)\]
$$\label{source}$$

We compute source terms $S_{ij}$, $S_{i}$, $S$, and $\rho$, via Eqs. 10 of [Baumgarte, Montero, Cordero-Carrión, and Müller](https://arxiv.org/pdf/1211.6632.pdf):

\begin{array}
\ S_{ij} &= \gamma_{i \mu} \gamma_{j \nu} T^{\mu \nu} \\
S_{i} &= -\gamma_{i\mu} n_\nu T^{\mu\nu} \\
S &= \gamma^{ij} S_{ij} \\
\rho &= n_\mu n_\nu T^{\mu\nu},
\end{array}

ID_TOV_TUPMUNU() provides numerical values for $T^{\mu\nu}$, but we do not have $\gamma_{\mu \nu}$ or $n_\mu$ directly. So here we will construct the latter quantities.

First, B&S Eq. 2.27 defines $\gamma_{\mu \nu}$ as:

$$\gamma_{\mu\nu} = g_{\mu\nu} + n_\mu n_\nu$$

where

$$
n_\mu = \{-\alpha,0,0,0\}.
$$

So we will first need to construct the 4-metric based on ADM quantities. This is provided by Eq 4.47 in [Gourgoulhon](https://arxiv.org/pdf/gr-qc/0703035.pdf):
$$
g_{\mu\nu} = \begin{pmatrix}
-\alpha^2 + \beta^k \beta_k & \beta_i \\
\beta_j & \gamma_{ij}
\end{pmatrix}.
$$

```python
gammaDD = ixp.declarerank2("gammaDD", "sym01",DIM=3)
betaU   = ixp.declarerank1("betaU",DIM=3)
alpha   = sp.symbols("alpha")

# To get \gamma_{\mu \nu} = gammabar4DD[mu][nu], we'll need to construct the 4-metric, using Eq. 2.122 in B&S:

# Eq. 2.121 in B&S
betaD = ixp.zerorank1()
for i in range(DIM):
    for j in range(DIM):
        betaD[i] += gammaDD[i][j]*betaU[j]

# Now compute the beta contraction.
beta2 = sp.sympify(0)
for i in range(DIM):
    beta2 += betaU[i]*betaD[i]

# Eq. 2.122 in B&S
g4DD = ixp.zerorank2(DIM=4)
g4DD[0][0] = -alpha**2 + beta2
for i in range(DIM):
    g4DD[i+1][0] = g4DD[0][i+1] = betaD[i]
for i in range(DIM):
    for j in range(DIM):
        g4DD[i+1][j+1] = gammaDD[i][j]
```

Now let's construct $\gamma_{\mu\nu}$=gamma4DD\[mu\]\[nu\] via $\gamma_{\mu\nu} = g_{\mu\nu} + n_\mu n_\nu$:

```python
n4D = ixp.zerorank1(DIM=4)
n4D[0] = -alpha

gamma4DD = ixp.zerorank2(DIM=4)
for mu in range(4):
    for nu in range(4):
        gamma4DD[mu][nu] = g4DD[mu][nu] + n4D[mu]*n4D[nu]
```

We now have all we need to construct the BSSN source terms in the current basis (for TOV, the Spherical basis):

\begin{array}
\ S_{ij} &= \gamma_{i \mu} \gamma_{j \nu} T^{\mu \nu} \\
S_{i} &= -\gamma_{i\mu} n_\nu T^{\mu\nu} \\
S &= \gamma^{ij} S_{ij} \\
\rho &= n_\mu n_\nu T^{\mu\nu},
\end{array}

```python
T4UU = ixp.declarerank2("T4UU", "sym01",DIM=4)

SDD = ixp.zerorank2()
SD  = ixp.zerorank1()
S   = sp.sympify(0)
rho = sp.sympify(0)

for i in range(DIM):
    for j in range(DIM):
        for mu in range(4):
            for nu in range(4):
                SDD[i][j] += gamma4DD[i+1][mu]*gamma4DD[j+1][nu] * T4UU[mu][nu]

for i in range(DIM):
    for mu in range(4):
        for nu in range(4):
            SD[i] += -gamma4DD[i+1][mu]*n4D[nu] * T4UU[mu][nu]

gammaUU, gammaDET = ixp.symm_matrix_inverter3x3(gammaDD)
for i in range(DIM):
    for j in range(DIM):
        S += gammaUU[i][j]*SDD[i][j]

for mu in range(4):
    for nu in range(4):
        rho += n4D[mu]*n4D[nu] * T4UU[mu][nu]
```

## Step 2.c: Jacobian transformation on the ADM/BSSN source terms \[Back to [top](#toc)\]
$$\label{jacobian}$$

The following discussion holds for either Spherical or Cartesian input data, so for simplicity let's just assume the data are given in Spherical coordinates.

All ADM tensors and vectors are in the Spherical coordinate basis $x^i_{\rm Sph} = (r,\theta,\phi)$, but we need them in the curvilinear coordinate basis $x^i_{\rm rfm}= ({\rm xx0},{\rm xx1},{\rm xx2})$ set by the "reference_metric::CoordSystem" variable. Empirically speaking, it is far easier to write $(x({\rm xx0},{\rm xx1},{\rm xx2}),y({\rm xx0},{\rm xx1},{\rm xx2}),z({\rm xx0},{\rm xx1},{\rm xx2}))$ than the inverse, so we will compute the Jacobian matrix

$$
{\rm Jac\_dUSph\_dDrfmUD[i][j]} = \frac{\partial x^i_{\rm Sph}}{\partial x^j_{\rm rfm}},
$$

via exact differentiation (courtesy SymPy), and the inverse Jacobian
$$
{\rm Jac\_dUrfm\_dDSphUD[i][j]} = \frac{\partial x^i_{\rm rfm}}{\partial x^j_{\rm Sph}},
$$

using NRPy+'s ${\rm generic\_matrix\_inverter3x3()}$ function. In terms of these, the transformation of ADM/BSSN source terms from Spherical to "reference_metric::CoordSystem" coordinates may be written:

\begin{align}
S^{\rm rfm}_{ij} &=
\frac{\partial x^\ell_{\rm Sph}}{\partial x^i_{\rm rfm}}
\frac{\partial x^m_{\rm Sph}}{\partial x^j_{\rm rfm}} S^{\rm Sph}_{\ell m}\\
S^{\rm rfm}_{i} &=
\frac{\partial x^\ell_{\rm Sph}}{\partial x^i_{\rm rfm}}
S^{\rm Sph}_{\ell}
\end{align}

```python
# TOV initial data are given in Spherical coordinates.
CoordType_in = "Spherical"

SSphorCartDD = ixp.zerorank2()
SSphorCartD  = ixp.zerorank1()

# Copy what we had written above, which was in Spherical coordinates, to the new tensors SSphorCartDD / SSphorCartD:
for i in range(3):
    SSphorCartD[i] = SD[i]
    for j in range(3):
        SSphorCartDD[i][j] = SDD[i][j]

# Zero out the original tensors; we're going to store the result to SD and SDD:
SDD = ixp.zerorank2()
SD  = ixp.zerorank1()

# Make sure that rfm.reference_metric() has been called.
#    We'll need the variables it defines throughout this module.
if rfm.have_already_called_reference_metric_function == False:
    print("Error. Called Convert_Spherical_ADM_to_BSSN_curvilinear() without")
    print("       first setting up reference metric, by calling rfm.reference_metric().")
    exit(1)

r_th_ph_or_Cart_xyz_oID_xx = []
if CoordType_in == "Spherical":
    r_th_ph_or_Cart_xyz_oID_xx = rfm.xxSph
elif CoordType_in == "Cartesian":
    r_th_ph_or_Cart_xyz_oID_xx = rfm.xxCart
else:
    print("Error: Can only convert ADM Cartesian or Spherical initial data to BSSN Curvilinear coords.")
    exit(1)

# Next apply Jacobian transformations to convert into the (xx0,xx1,xx2) basis

# rho and S are scalar, so no Jacobian transformations are necessary.

Jac_dUSphorCart_dDrfmUD = ixp.zerorank2()
for i in range(DIM):
    for j in range(DIM):
        Jac_dUSphorCart_dDrfmUD[i][j] = sp.diff(r_th_ph_or_Cart_xyz_oID_xx[i],rfm.xx[j])

Jac_dUrfm_dDSphorCartUD, dummyDET = ixp.generic_matrix_inverter3x3(Jac_dUSphorCart_dDrfmUD)

for i in range(DIM):
    for j in range(DIM):
        SD[i] += Jac_dUSphorCart_dDrfmUD[j][i] * SSphorCartD[j]
        for k in range(DIM):
            for l in range(DIM):
                SDD[i][j] += Jac_dUSphorCart_dDrfmUD[k][i]*Jac_dUSphorCart_dDrfmUD[l][j] * SSphorCartDD[k][l]
```

## Step 2.d: Rescale tensorial quantities \[Back to [top](#toc)\]
$$\label{tensor}$$

We rescale tensorial quantities according to the prescription described in the [BSSN in curvilinear coordinates tutorial module](Tutorial-BSSNCurvilinear.ipynb) (also [Ruchlin *et al.*](https://arxiv.org/pdf/1712.07658.pdf)):

Since $\text{ReD[i]}=1/\text{ReU[i]}$, we have

\begin{align}
s_{ij} &= S_{ij} /\text{ReDD[i][j]}\\
s_{i} &= S_i \text{ReU[i]}
\end{align}

```python
# Finally rescale the tensorial quantities:
sD = ixp.zerorank1()
sDD = ixp.zerorank2()
for i in range(DIM):
    sD[i] = SD[i] * rfm.ReU[i] # ReD[i] = 1/ReU[i]
    for j in range(DIM):
        sDD[i][j] = SDD[i][j] / rfm.ReDD[i][j]
```

Next we use NRPy+ to write a C function that reads in $T^{\mu\nu}$ in the given (Spherical) basis and outputs the source terms $\{S_{ij},S_{i},S,\rho\}$ in the (xx0,xx1,xx2) basis:

```python
with open("BSSN/ID_TOV_BSSN_Source_Terms.h", "w") as file:
    file.write("""void ID_TOV_BSSN_Source_Terms(
                    REAL xx0xx1xx2[3],

                    const REAL gammaDD00,const REAL gammaDD01,const REAL gammaDD02,
                    /**/                 const REAL gammaDD11,const REAL gammaDD12,
                    /**/                                      const REAL gammaDD22,

                    const REAL betaU0,const REAL betaU1,const REAL betaU2,
                    const REAL alpha,

                    const REAL T4UU00,const REAL T4UU01,const REAL T4UU02,const REAL T4UU03,
                    /**/              const REAL T4UU11,const REAL T4UU12,const REAL T4UU13,
                    /**/                                const REAL T4UU22,const REAL T4UU23,
                    /**/                                                  const REAL T4UU33,

                    REAL *sDD00,REAL *sDD01,REAL *sDD02,
                    /**/        REAL *sDD11,REAL *sDD12,
                    /**/                    REAL *sDD22,

                    REAL *sD0, REAL *sD1, REAL *sD2,

                    REAL *S, REAL *rho) {

    const REAL xx0 = xx0xx1xx2[0];
    const REAL xx1 = xx0xx1xx2[1];
    const REAL xx2 = xx0xx1xx2[2];\n""")
outCparams = "preindent=1,outCfileaccess=a,outCverbose=False,includebraces=False"

outputC([sDD[0][0],sDD[0][1],sDD[0][2],sDD[1][1],sDD[1][2],sDD[2][2],
         sD[0],sD[1],sD[2], S, rho],
        ["*sDD00","*sDD01","*sDD02","*sDD11","*sDD12","*sDD22",
         "*sD0","*sD1","*sD2","*S","*rho"], "BSSN/ID_TOV_BSSN_Source_Terms.h",outCparams)

with open("BSSN/ID_TOV_BSSN_Source_Terms.h", "a") as file:
    file.write("}\n")
```

    Appended to file "BSSN/ID_TOV_BSSN_Source_Terms.h"

# Step 3: Take care of the ADM spacetime quantity initial data conversion \[Back to [top](#toc)\]
$$\label{adm_id_spacetime}$$

This is an automated process, taken care of by [BSSN.ADM_Numerical_Spherical_or_Cartesian_to_BSSNCurvilinear](../edit/BSSN.ADM_Numerical_Spherical_or_Cartesian_to_BSSNCurvilinear.py), and documented [in this tutorial module](Tutorial-ADM_Initial_Data-Converting_Numerical_ADM_Spherical_or_Cartesian_to_BSSNCurvilinear.ipynb).

```python
import BSSN.ADM_Numerical_Spherical_or_Cartesian_to_BSSNCurvilinear as AtoBnum
AtoBnum.Convert_Spherical_or_Cartesian_ADM_to_BSSN_curvilinear("Spherical","ID_TOV_ADM_quantities")
```

    Appended to file "BSSN/ID_ADM_xx0xx1xx2_to_BSSN_xx0xx1xx2__ALL_BUT_LAMBDAs.h"
    Appended to file "BSSN/ID_ADM_xx0xx1xx2_to_BSSN_xx0xx1xx2__ALL_BUT_LAMBDAs.h"

# Step 4: Set up the BSSN right-hand-side (RHS) expressions, and add the *rescaled* $T^{\mu\nu}$ source terms \[Back to [top](#toc)\]
$$\label{bssn_source}$$

BSSN.BSSN_RHSs() currently sets up the RHSs assuming a spacetime vacuum: $T^{\mu\nu}=0$. (This might seem weird, but remember that *black hole spacetimes are vacuum spacetimes*.) Here we add the $T^{\mu\nu}$ source terms to these equations.

Equations 9a-9e in [Baumgarte, Montero, Cordero-Carrión, and Müller](https://arxiv.org/pdf/1211.6632.pdf) provide the necessary prescription:

\begin{align}
\partial_t \bar{\gamma}_{ij} &= \left[\partial_t \bar{\gamma}_{ij}\right]_{\rm vacuum}\\
\partial_t \bar{A}_{ij} &= \left[\partial_t \bar{A}_{ij}\right]_{\rm vacuum} - 8\pi \alpha e^{-4\phi} \left[S_{ij}\right]^{\rm TF} \\
\partial_t \phi &= \left[\partial_t \phi \right]_{\rm vacuum} \\
\partial_t K &= \left[\partial_t K \right]_{\rm vacuum} + 4\pi\alpha (\rho + S) \\
\partial_t \bar{\Lambda}^i &= \left[\partial_t \bar{\Lambda}^i \right]_{\rm vacuum} -16 \pi\alpha \bar{\gamma}^{ij} S_j \\
\end{align}

The rescaled versions are
\begin{align}
\partial_t h_{ij} &= \left[\partial_t h_{ij}\right]_{\rm vacuum}\\
\partial_t a_{ij} &= \left[\partial_t a_{ij}\right]_{\rm vacuum} - 8\pi \alpha e^{-4\phi} \left[s_{ij}\right]^{\rm TF} \\
\partial_t \phi &= \left[\partial_t \phi \right]_{\rm vacuum} \\
\partial_t K &= \left[\partial_t K \right]_{\rm vacuum} + 4\pi\alpha (\rho + S) \\
\partial_t \lambda^i &= \left[\partial_t \lambda^i \right]_{\rm vacuum} -16 \pi\alpha \bar{\gamma}^{ij} s_j \\
\end{align}

For consistency with the trace-free operation on the BSSN RHSs, we remove the trace with respect to the barred metric:

$$
\left[S_{ij}\right]^{\rm TF} = S_{ij} - \frac{1}{3} \bar{\gamma}_{ij} \left(\bar{\gamma}^{kl} S_{kl}\right)
$$

```python
# Add BSSN's T^{mu nu} source terms rho,S,sD,sDD to BSSN RHSs

# First declare BSSN T^{mu nu} source terms rho,S,sD,sDD as gridfunctions
BTmunu.define_BSSN_T4UUmunu_rescaled_source_terms()
rho = BTmunu.rho
S   = BTmunu.S
sD  = BTmunu.sD
sDD = BTmunu.sDD

M_PI = par.Cparameters("REAL",thismodule,"M_PI") # M_PI is pi as defined in C

# FIRST RHS OF a_{ij} EQUATION
# First un-rescale in terms of known quantities s_{ij} and the rescaling matrix:
SDD = ixp.zerorank2()
for i in range(DIM):
    for j in range(DIM):
        SDD[i][j] = sDD[i][j]*rfm.ReDD[i][j]

Bq.BSSN_basic_tensors() # provides gammabarDD
gammabarDD = Bq.gammabarDD
Bq.gammabar__inverse_and_derivs() # provides gammabarUU
gammabarUU = Bq.gammabarUU
Strace = sp.sympify(0)
for k in range(DIM):
    for l in range(DIM):
        Strace += gammabarUU[k][l]*SDD[k][l]

STFDD = ixp.zerorank2()
for i in range(DIM):
    for j in range(DIM):
        STFDD[i][j] = SDD[i][j] - sp.Rational(1,3) * gammabarDD[i][j] * Strace

# Finally rescale:
sTFDD = ixp.zerorank2()
for i in range(DIM):
    for j in range(DIM):
        sTFDD[i][j] = STFDD[i][j] / rfm.ReDD[i][j]

Bq.declare_BSSN_gridfunctions_if_not_declared_already() # Defines alpha
Bq.phi_and_derivs() # Defines exp_m4phi
exp_m4phi = Bq.exp_m4phi
for i in range(DIM):
    for j in range(DIM):
        rhs.a_rhsDD[i][j] += -8*M_PI*Bq.alpha*exp_m4phi*sTFDD[i][j]

# THEN RHS OF trK EQUATION
rhs.trK_rhs += 4*M_PI*Bq.alpha*(S + rho)

# FINALLY RHS OF lambda^i EQUATION
for i in range(DIM):
    for j in range(DIM):
        rhs.lambda_rhsU[i] += -16*M_PI*Bq.alpha*gammabarUU[i][j]*sD[j]
```

```python
import time
print("Generating C code for BSSN RHSs in "+par.parval_from_str("reference_metric::CoordSystem")+" coordinates.")
start = time.time()

BSSN_evol_rhss = [ \
                  lhrh(lhs=gri.gfaccess("rhs_gfs","aDD00"),rhs=rhs.a_rhsDD[0][0]),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","aDD01"),rhs=rhs.a_rhsDD[0][1]),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","aDD02"),rhs=rhs.a_rhsDD[0][2]),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","aDD11"),rhs=rhs.a_rhsDD[1][1]),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","aDD12"),rhs=rhs.a_rhsDD[1][2]),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","aDD22"),rhs=rhs.a_rhsDD[2][2]),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","alpha"),rhs=gaugerhs.alpha_rhs),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","betU0"),rhs=gaugerhs.bet_rhsU[0]),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","betU1"),rhs=gaugerhs.bet_rhsU[1]),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","betU2"),rhs=gaugerhs.bet_rhsU[2]),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","cf"),   rhs=rhs.cf_rhs),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","hDD00"),rhs=rhs.h_rhsDD[0][0]),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","hDD01"),rhs=rhs.h_rhsDD[0][1]),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","hDD02"),rhs=rhs.h_rhsDD[0][2]),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","hDD11"),rhs=rhs.h_rhsDD[1][1]),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","hDD12"),rhs=rhs.h_rhsDD[1][2]),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","hDD22"),rhs=rhs.h_rhsDD[2][2]),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","lambdaU0"),rhs=rhs.lambda_rhsU[0]),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","lambdaU1"),rhs=rhs.lambda_rhsU[1]),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","lambdaU2"),rhs=rhs.lambda_rhsU[2]),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","trK"),  rhs=rhs.trK_rhs),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","vetU0"),rhs=gaugerhs.vet_rhsU[0]),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","vetU1"),rhs=gaugerhs.vet_rhsU[1]),
                  lhrh(lhs=gri.gfaccess("rhs_gfs","vetU2"),rhs=gaugerhs.vet_rhsU[2]) ]

betaU = Bq.betaU
BSSN_RHSs_string = fin.FD_outputC("returnstring",BSSN_evol_rhss, params="outCverbose=False",upwindcontrolvec=betaU)
end = time.time()
print("Finished in "+str(end-start)+" seconds.")

with open("BSSN/BSSN_RHSs.h", "w") as file:
    file.write(lp.loop(["i2","i1","i0"],["NGHOSTS","NGHOSTS","NGHOSTS"],
                       ["NGHOSTS+Nxx[2]","NGHOSTS+Nxx[1]","NGHOSTS+Nxx[0]"],
                       ["1","1","1"],["const REAL invdx0 = 1.0/dxx[0];\n"+
                                      "const REAL invdx1 = 1.0/dxx[1];\n"+
                                      "const REAL invdx2 = 1.0/dxx[2];\n"+
                                      "#pragma omp parallel for",
                                      "    const REAL xx2 = xx[2][i2];",
                                      "        const REAL xx1 = xx[1][i1];"],"",
                                     "const REAL xx0 = xx[0][i0];\n"+BSSN_RHSs_string))
```

    Generating C code for BSSN RHSs in Spherical coordinates.
    Finished in 69.25046420097351 seconds.

# Step 5 Validating "Hydro without Hydro" test satisfies the Hamiltonian constraint \[Back to [top](#toc)\]
$$\label{hydro_hydro}$$

We will validate that the "Hydro without Hydro" test satisfies the Hamiltonian constraint, modulo numerical finite differencing error

## Step 5.a: Output the Hamiltonian constraint \[Back to [top](#toc)\]
$$\label{ham_const_output}$$

First output the Hamiltonian constraint [as documented in the corresponding NRPy+ tutorial module](Tutorial-BSSN_constraints.ipynb)

```python
import BSSN.BSSN_constraints as bssncon
bssncon.output_C__Hamiltonian_h(add_T4UUmunu_source_terms=True)
```

    initialize_param() minor warning: Did nothing; already initialized parameter reference_metric::M_PI
    initialize_param() minor warning: Did nothing; already initialized parameter reference_metric::RMAX
    Generating optimized C code for Hamiltonian constraint. May take a while, depending on CoordSystem.
    Finished in 10.932046890258789 seconds.
    Output C implementation of Hamiltonian constraint to BSSN/Hamiltonian.h

## Step 5.b: Apply singular, curvilinear coordinate boundary conditions \[Back to [top](#toc)\]
$$\label{apply_bcs}$$

Next apply singular, curvilinear coordinate boundary conditions [as documented in the corresponding NRPy+ tutorial module](Tutorial-Start_to_Finish-Curvilinear_BCs.ipynb)

```python
import CurviBoundaryConditions.CurviBoundaryConditions as cbcs
cbcs.Set_up_CurviBoundaryConditions()
```

    Wrote to file "CurviBoundaryConditions/gridfunction_defines.h"
    Wrote to file "CurviBoundaryConditions/set_parity_conditions.h"
    Wrote to file "CurviBoundaryConditions/xxCart.h"
    Wrote to file "CurviBoundaryConditions/xxminmax.h"
    Wrote to file "CurviBoundaryConditions/Cart_to_xx.h"

## Step 5.c: Enforce conformal 3-metric $\det{\bar{\gamma}_{ij}}=\det{\hat{\gamma}_{ij}}$ constraint \[Back to [top](#toc)\]
$$\label{enforce3metric}$$

Then enforce conformal 3-metric $\det{\bar{\gamma}_{ij}}=\det{\hat{\gamma}_{ij}}$ constraint (Eq. 53 of [Ruchlin, Etienne, and Baumgarte (2018)](https://arxiv.org/abs/1712.07658)), as [documented in the corresponding NRPy+ tutorial module](Tutorial-BSSN-Enforcing_Determinant_gammabar_equals_gammahat_Constraint.ipynb)

Applying curvilinear boundary conditions should affect the initial data at the outer boundary, and will in general cause the $\det{\bar{\gamma}_{ij}}=\det{\hat{\gamma}_{ij}}$ constraint to be violated there. Thus after we apply these boundary conditions, we must always call the routine for enforcing the $\det{\bar{\gamma}_{ij}}=\det{\hat{\gamma}_{ij}}$ constraint:

```python
import BSSN.Enforce_Detgammabar_Constraint as EGC
EGC.output_Enforce_Detgammabar_Constraint_Ccode()
```

    initialize_param() minor warning: Did nothing; already initialized parameter reference_metric::M_PI
    initialize_param() minor warning: Did nothing; already initialized parameter reference_metric::RMAX
    Output C implementation of det(gammabar) constraint to file BSSN/enforce_detgammabar_constraint.h

# Step 6: TOV_Playground.c: The Main C Code \[Back to [top](#toc)\]
$$\label{mainc}$$

```python
# Part P0: Set the number of ghost cells, from NRPy+'s FD_CENTDERIVS_ORDER
with open("BSSN/NGHOSTS.h", "w") as file:
    file.write("// Part P0: Set the number of ghost zones, from NRPy+'s FD_CENTDERIVS_ORDER\n")
    # Upwinding in BSSN requires that NGHOSTS = FD_CENTDERIVS_ORDER/2 + 1 <- Notice the +1.
    file.write("#define NGHOSTS "+str(int(par.parval_from_str("finite_difference::FD_CENTDERIVS_ORDER")/2)+1)+"\n")
```

```python
%%writefile BSSN/TOV_Playground.c

// Part P1: Import needed header files
#include "NGHOSTS.h" // A NRPy+-generated file, which is set based on FD_CENTDERIVS_ORDER.
#include "stdio.h"
#include "stdlib.h"
#include "math.h"
#include "time.h"
#include "stdint.h" // Needed for Windows GCC 6.x compatibility

// Part P2: Add needed #define's to set data type, the IDX4() macro, and the gridfunctions
// Part P2a: set REAL=double, so that all floating point numbers are stored to at least ~16 significant digits.
#define REAL double

// Step P3: Set free parameters for the numerical grid
const REAL RMAX    = 3.0;

// Time coordinate parameters
const REAL t_final =  0.5; /* Final time is set so that at t=t_final,
                            * data at the origin have not been corrupted
                            * by the approximate outer boundary condition */
const REAL CFL_FACTOR = 0.5; // Set the CFL Factor

// Step P3b: Free parameters for the spacetime evolution
const REAL eta = 1.0; // Gamma-driving shift condition parameter.

// Step P4: Implement the algorithm for upwinding.
//          *NOTE*: This upwinding is backwards from
//          usual upwinding algorithms, because the
//          upwinding control vector in BSSN (the shift)
//          acts like a *negative* velocity.
#define UPWIND_ALG(UpwindVecU) UpwindVecU > 0.0 ? 1.0 : 0.0

typedef struct __ID_inputs {
    REAL Rbar;
    int Rbar_idx;
    int interp_stencil_size;
    int numlines_in_file;
    REAL *r_Schw_arr,*rho_arr,*P_arr,*M_arr,*expnu_arr,*exp4phi_arr,*rbar_arr;
} ID_inputs;

// Part P4b: Declare the IDX4(gf,i,j,k) macro, which enables us to store 4-dimensions of
//           data in a 1D array. In this case, consecutive values of "i"
//           (all other indices held to a fixed value) are consecutive in memory, where
//           consecutive values of "j" (fixing all other indices) are separated by
//           Nxx_plus_2NGHOSTS[0] elements in memory. Similarly, consecutive values of
//           "k" are separated by Nxx_plus_2NGHOSTS[0]*Nxx_plus_2NGHOSTS[1] in memory, etc.
#define IDX4(g,i,j,k) \
( (i) + Nxx_plus_2NGHOSTS[0] * ( (j) + Nxx_plus_2NGHOSTS[1] * ( (k) + Nxx_plus_2NGHOSTS[2] * (g) ) ) )
#define IDX3(i,j,k) ( (i) + Nxx_plus_2NGHOSTS[0] * ( (j) + Nxx_plus_2NGHOSTS[1] * (k) ) )
// Assuming idx = IDX3(i,j,k). Much faster if idx can be reused over and over:
#define IDX4pt(g,idx)   ( (idx) + (Nxx_plus_2NGHOSTS[0]*Nxx_plus_2NGHOSTS[1]*Nxx_plus_2NGHOSTS[2]) * (g) )

// Part P4c: Set #define's for BSSN gridfunctions. C code generated above
#include "../CurviBoundaryConditions/gridfunction_defines.h"

#define LOOP_REGION(i0min,i0max, i1min,i1max, i2min,i2max) \
  for(int i2=i2min;i2<i2max;i2++) for(int i1=i1min;i1<i1max;i1++) for(int i0=i0min;i0<i0max;i0++)

// Step P5: Function for converting uniform grid coord
//         (xx[0][i0],xx[1][i1],xx[2][i2]) to
//          corresponding Cartesian coordinate.
void xxCart(REAL *xx[3],const int i0,const int i1,const int i2, REAL xCart[3]) {
    REAL xx0 = xx[0][i0];
    REAL xx1 = xx[1][i1];
    REAL xx2 = xx[2][i2];
#include "xxCart.h"
}

// Step P6: Include basic functions needed to impose curvilinear
//          parity and boundary conditions.
#include "../CurviBoundaryConditions/curvilinear_parity_and_outer_boundary_conditions.h"

// Step P7: Function for enforcing the gammabar=gammahat constraint:
#include "enforce_detgammabar_constraint.h"

// Step P8: Find the CFL-constrained timestep
REAL find_timestep(const int Nxx_plus_2NGHOSTS[3],const REAL dxx[3],REAL *xx[3], const REAL CFL_FACTOR) {
  const REAL dxx0 = dxx[0], dxx1 = dxx[1], dxx2 = dxx[2];
  REAL dsmin = 1e38; // Start with a crazy high value... close to the largest number in single precision.
  LOOP_REGION(NGHOSTS,Nxx_plus_2NGHOSTS[0]-NGHOSTS, NGHOSTS,Nxx_plus_2NGHOSTS[1]-NGHOSTS, NGHOSTS,Nxx_plus_2NGHOSTS[2]-NGHOSTS) {
    const REAL xx0 = xx[0][i0], xx1 = xx[1][i1], xx2 = xx[2][i2];
    REAL ds_dirn0, ds_dirn1, ds_dirn2;
#include "ds_dirn.h"
#define MIN(A, B) ( ((A) < (B)) ? (A) : (B) )
    // Set dsmin = MIN(dsmin, ds_dirn0, ds_dirn1, ds_dirn2);
    dsmin = MIN(dsmin,MIN(ds_dirn0,MIN(ds_dirn1,ds_dirn2)));
  }
  return dsmin*CFL_FACTOR;
}

// Part P9: Declare all functions for setting up TOV initial data.
/* Routines to interpolate the TOV solution and convert to ADM & T^{munu}: */
#include "../TOV/tov_interp.h"
#include "ID_TOV_ADM_quantities.h"
#include "ID_TOV_TUPMUNU.h"

/* Next perform the basis conversion and compute all needed BSSN quantities */
#include "ID_TOV_BSSN_Source_Terms.h"
#include "ID_ADM_xx0xx1xx2_to_BSSN_xx0xx1xx2__ALL_BUT_LAMBDAs.h"
#include "ID_BSSN__ALL_BUT_LAMBDAs.h"
#include "ID_BSSN_lambdas.h"

// Part P10: Declare function for computing the Hamiltonian
//           constraint violation, which should converge to
//           zero with increasing numerical resolution.
void Hamiltonian_constraint(const int Nxx[3],const int Nxx_plus_2NGHOSTS[3],const REAL dxx[3], REAL *xx[3],
                            REAL *in_gfs, REAL *aux_gfs) {
#include "Hamiltonian.h"
}

// Step P11: Declare the function to evaluate the BSSN RHSs
void rhs_eval(const int Nxx[3],const int Nxx_plus_2NGHOSTS[3],const REAL dxx[3], REAL *xx[3],
              REAL *aux_gfs,const REAL *in_gfs,REAL *rhs_gfs) {
#include "BSSN_RHSs.h"
}

// main() function:
// Step 0: Read command-line input, set up grid structure, allocate memory for gridfunctions, set up coordinates
// Step 1: Set up scalar wave initial data
// Step 2: Evolve scalar wave initial data forward in time using Method of Lines with RK4 algorithm,
//         applying quadratic extrapolation outer boundary conditions.
// Step 3: Output relative error between numerical and exact solution.
// Step 4: Free all allocated memory
int main(int argc, const char *argv[]) {
    // Step 0a: Read command-line input, error out if nonconformant
    if(argc != 4 || atoi(argv[1]) < NGHOSTS) {
        fprintf(stderr,"Error: Expected three command-line arguments: ./TOV_Playground Nx0 Nx1 Nx2,\n");
        fprintf(stderr,"where Nx[0,1,2] is the number of grid points in the 0, 1, and 2 directions.\n");
        fprintf(stderr,"Nx[] MUST BE larger than NGHOSTS (= %d)\n",NGHOSTS);
        exit(1);
    }
    // Step 0b: Set up numerical grid structure, first in space...
    if(atoi(argv[1])%2 != 0 || atoi(argv[2])%2 != 0 || atoi(argv[2])%2 != 0) {
        fprintf(stderr,"Error: Cannot guarantee a proper cell-centered grid if number of grid cells not set to even number.\n");
        fprintf(stderr,"       For example, in case of angular directions, proper symmetry zones will not exist.\n");
        exit(1);
    }
    const int Nxx[3] = { atoi(argv[1]), atoi(argv[2]), atoi(argv[3]) };
    const int Nxx_plus_2NGHOSTS[3] = { Nxx[0]+2*NGHOSTS, Nxx[1]+2*NGHOSTS, Nxx[2]+2*NGHOSTS };
    const int Nxx_plus_2NGHOSTS_tot = Nxx_plus_2NGHOSTS[0]*Nxx_plus_2NGHOSTS[1]*Nxx_plus_2NGHOSTS[2];
#include "xxminmax.h"

    /* TOV INPUT ROUTINE */
    // Open the data file:
    char filename[100];
    sprintf(filename,"./outputTOVpolytrope.txt");
    FILE *in1Dpolytrope = fopen(filename, "r");
    if (in1Dpolytrope == NULL) {
        fprintf(stderr,"ERROR: could not open file %s\n",filename);
        exit(1);
    }
    // Count the number of lines in the data file:
    int numlines_in_file = count_num_lines_in_file(in1Dpolytrope);
    // Allocate space for all data arrays:
    REAL *r_Schw_arr  = (REAL *)malloc(sizeof(REAL)*numlines_in_file);
    REAL *rho_arr     = (REAL *)malloc(sizeof(REAL)*numlines_in_file);
    REAL *P_arr       = (REAL *)malloc(sizeof(REAL)*numlines_in_file);
    REAL *M_arr       = (REAL *)malloc(sizeof(REAL)*numlines_in_file);
    REAL *expnu_arr   = (REAL *)malloc(sizeof(REAL)*numlines_in_file);
    REAL *exp4phi_arr = (REAL *)malloc(sizeof(REAL)*numlines_in_file);
    REAL *rbar_arr    = (REAL *)malloc(sizeof(REAL)*numlines_in_file);

    // Read from the data file, filling in arrays
    if(read_datafile__set_arrays(in1Dpolytrope, r_Schw_arr,rho_arr,P_arr,M_arr,expnu_arr,exp4phi_arr,rbar_arr) == 1) {
        fprintf(stderr,"ERROR WHEN READING FILE %s!\n",filename);
        exit(1);
    }
    fclose(in1Dpolytrope);
    REAL Rbar = -100;
    int Rbar_idx = -100;
    for(int i=1;i<numlines_in_file;i++) {
        if(rho_arr[i-1]>0 && rho_arr[i]==0) { Rbar = rbar_arr[i-1]; Rbar_idx = i-1; }
    }
    if(Rbar<0) {
        fprintf(stderr,"Error: could not find rbar=Rbar from data file.\n");
        exit(1);
    }

    ID_inputs TOV_in;
    TOV_in.Rbar = Rbar;
    TOV_in.Rbar_idx = Rbar_idx;

    const int interp_stencil_size = 12;
    TOV_in.interp_stencil_size = interp_stencil_size;
    TOV_in.numlines_in_file = numlines_in_file;

    TOV_in.r_Schw_arr = r_Schw_arr;
    TOV_in.rho_arr = rho_arr;
    TOV_in.P_arr = P_arr;
    TOV_in.M_arr = M_arr;
    TOV_in.expnu_arr = expnu_arr;
    TOV_in.exp4phi_arr = exp4phi_arr;
    TOV_in.rbar_arr = rbar_arr;
    /* END TOV INPUT ROUTINE */

    // Step 0c: Allocate memory for gridfunctions
    REAL *evol_gfs = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot);
    REAL *next_in_gfs = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot);
    REAL *aux_gfs  = (REAL *)malloc(sizeof(REAL) * NUM_AUX_GFS * Nxx_plus_2NGHOSTS_tot);
    REAL *k1_gfs = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot);
    REAL *k2_gfs = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot);
    REAL *k3_gfs = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot);
    REAL *k4_gfs = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot);

    // Step 0d: Set up space and time coordinates
    // Step 0d.i: Set \Delta x^i on uniform grids.
    REAL dxx[3];
    for(int i=0;i<3;i++) dxx[i] = (xxmax[i] - xxmin[i]) / ((REAL)Nxx[i]);

    // Step 0d.ii: Set up uniform coordinate grids
    REAL *xx[3];
    for(int i=0;i<3;i++) {
        xx[i] = (REAL *)malloc(sizeof(REAL)*Nxx_plus_2NGHOSTS[i]);
        for(int j=0;j<Nxx_plus_2NGHOSTS[i];j++) {
            xx[i][j] = xxmin[i] + ((REAL)(j-NGHOSTS) + (1.0/2.0))*dxx[i]; // Cell-centered grid.
        }
    }

    // Step 0d.iii: Set timestep based on smallest proper distance between gridpoints and CFL factor
    REAL dt = find_timestep(Nxx_plus_2NGHOSTS, dxx,xx, CFL_FACTOR);
    //printf("# Timestep set to = %e\n",(double)dt);
    int N_final = (int)(t_final / dt + 0.5); // The number of iterations in time.
                                             //Add 0.5 to account for C rounding down integers.

    // Step 0e: Find ghostzone mappings and parities:
    gz_map *bc_gz_map = (gz_map *)malloc(sizeof(gz_map)*Nxx_plus_2NGHOSTS_tot);
    parity_condition *bc_parity_conditions = (parity_condition *)malloc(sizeof(parity_condition)*Nxx_plus_2NGHOSTS_tot);
    set_up_bc_gz_map_and_parity_conditions(Nxx_plus_2NGHOSTS,xx,dxx,xxmin,xxmax,  bc_gz_map, bc_parity_conditions);

    // Step 1: Set up initial data to an exact solution at time=0:
    ID_BSSN__ALL_BUT_LAMBDAs(Nxx_plus_2NGHOSTS,xx,TOV_in, evol_gfs);
    apply_bcs(Nxx, Nxx_plus_2NGHOSTS, bc_gz_map,bc_parity_conditions,NUM_EVOL_GFS,evol_gf_parity, evol_gfs);
    enforce_detgammabar_constraint(Nxx_plus_2NGHOSTS, xx, evol_gfs);
    ID_BSSN_lambdas(Nxx, Nxx_plus_2NGHOSTS, xx,dxx, evol_gfs);
    apply_bcs(Nxx, Nxx_plus_2NGHOSTS, bc_gz_map,bc_parity_conditions,NUM_EVOL_GFS,evol_gf_parity, evol_gfs);
    enforce_detgammabar_constraint(Nxx_plus_2NGHOSTS, xx, evol_gfs);
{
    #pragma omp parallel for
    for(int i2=0; i2<Nxx_plus_2NGHOSTS[2]; i2++) {
        const REAL xx2 = xx[2][i2];
        for(int i1=0; i1<Nxx_plus_2NGHOSTS[1]; i1++) {
            const REAL xx1 = xx[1][i1];
            for(int i0=0; i0<Nxx_plus_2NGHOSTS[0]; i0++) {
                const REAL xx0 = xx[0][i0];
                REAL xx0xx1xx2[3] = {xx0,xx1,xx2};
                REAL gammaDD00,gammaDD01,gammaDD02,gammaDD11,gammaDD12,gammaDD22;
                REAL KDD00,KDD01,KDD02,KDD11,KDD12,KDD22;
                REAL alpha,betaU0,betaU1,betaU2;
                REAL BU0,BU1,BU2;
                REAL xyz_or_rthph[3] =  {xx0,xx1,xx2}; //FIXME

                // FIRST INTERPOLATE TO SET THE ADM AND TMUNU QUANTITIES
                ID_TOV_ADM_quantities(xyz_or_rthph, TOV_in,

                    &gammaDD00,&gammaDD01,&gammaDD02,&gammaDD11,&gammaDD12,&gammaDD22,
                    &KDD00,&KDD01,&KDD02,&KDD11,&KDD12,&KDD22,
                    &alpha,  &betaU0,&betaU1,&betaU2,  &BU0,&BU1,&BU2);

                REAL T4UU00,T4UU01,T4UU02,T4UU03,
                /**/        T4UU11,T4UU12,T4UU13,
                /**/               T4UU22,T4UU23,
                /**/                      T4UU33;

                ID_TOV_TUPMUNU(xyz_or_rthph, TOV_in,

                    &T4UU00,&T4UU01,&T4UU02,&T4UU03,
                    /**/    &T4UU11,&T4UU12,&T4UU13,
                    /**/            &T4UU22,&T4UU23,
                    /**/                    &T4UU33);

                const int idx = IDX3(i0,i1,i2);
                // THEN EVALUATE THE BSSN SOURCE TERMS
                ID_TOV_BSSN_Source_Terms(xx0xx1xx2,
                                         gammaDD00, gammaDD01, gammaDD02,
                    /**/                            gammaDD11, gammaDD12,
                    /**/                                       gammaDD22,

                    betaU0,betaU1,betaU2, alpha,

                    T4UU00, T4UU01, T4UU02, T4UU03,
                    /**/    T4UU11, T4UU12, T4UU13,
                    /**/            T4UU22, T4UU23,
                    /**/                    T4UU33,

                    &aux_gfs[IDX4pt(SDD00GF,idx)], &aux_gfs[IDX4pt(SDD01GF,idx)], &aux_gfs[IDX4pt(SDD02GF,idx)],
                    /**/                           &aux_gfs[IDX4pt(SDD11GF,idx)], &aux_gfs[IDX4pt(SDD12GF,idx)],
                    /**/                                                          &aux_gfs[IDX4pt(SDD22GF,idx)],

                    &aux_gfs[IDX4pt(SD0GF,idx)], &aux_gfs[IDX4pt(SD1GF,idx)], &aux_gfs[IDX4pt(SD2GF,idx)],

                    &aux_gfs[IDX4pt(SGF,idx)], &aux_gfs[IDX4pt(RHOGF,idx)]);
            }
        }
    }
}

    // Step 2: Evaluate Hamiltonian constraint violation
    Hamiltonian_constraint(Nxx,Nxx_plus_2NGHOSTS,dxx, xx, evol_gfs, aux_gfs);

    apply_bcs(Nxx, Nxx_plus_2NGHOSTS, bc_gz_map,bc_parity_conditions,NUM_AUX_GFS,aux_gf_parity, aux_gfs);

    // Step 3: Start the timer, for keeping track of how fast the simulation is progressing.
#ifdef __linux__ // Use high-precision timer in Linux.
    struct timespec start, end;
    clock_gettime(CLOCK_REALTIME, &start);
#elif _WIN32     // Resort to low-resolution, standards-compliant timer in Linux
    // http://www.cplusplus.com/reference/ctime/time/
    time_t start_timer,end_timer;
    time(&start_timer); // Resolution of one second...
#endif

    // Step 4: Integrate the initial data forward in time using the Method of Lines and RK4
    for(int n=0;n<=N_final;n++) { // Main loop to progress forward in time.
        /***************************************************/
        /* Implement RK4 for Method of Lines timestepping: */
        /***************************************************/
        /* -= RK4: Step 1 of 4 =- */
        /* First evaluate k1 = RHSs expression             */
        rhs_eval(Nxx,Nxx_plus_2NGHOSTS,dxx, xx, aux_gfs, evol_gfs, k1_gfs);
        /* Next k1 -> k1*dt, and then set the input for    */
        /*    the next RHS eval call to y_n+k1/2           */
        #pragma omp parallel for
        for(int i=0;i<Nxx_plus_2NGHOSTS_tot*NUM_EVOL_GFS;i++) {
            k1_gfs[i] *= dt;
            next_in_gfs[i] = evol_gfs[i] + k1_gfs[i]*0.5;
        }
        /* Finally, apply boundary conditions to           */
        /* next_in_gfs, so its data are set everywhere.    */
        apply_bcs(Nxx, Nxx_plus_2NGHOSTS, bc_gz_map,bc_parity_conditions,NUM_EVOL_GFS,evol_gf_parity, next_in_gfs);
        enforce_detgammabar_constraint(Nxx_plus_2NGHOSTS, xx, next_in_gfs);

         /* Step 3: Output 2D data file, for visualization */
         if(n%20 == 0) {
            // Evaluate Hamiltonian constraint violation
            Hamiltonian_constraint(Nxx,Nxx_plus_2NGHOSTS,dxx, xx, evol_gfs, aux_gfs);

    //        const int i1mid = Nxx_plus_2NGHOSTS[1]/2;
            char filename[100];
            sprintf(filename,"out%d-%08d.txt",Nxx[0],n);
            FILE *out2D = fopen(filename, "w");
            LOOP_REGION(NGHOSTS,Nxx_plus_2NGHOSTS[0]-NGHOSTS, NGHOSTS,Nxx_plus_2NGHOSTS[1]-NGHOSTS, NGHOSTS,Nxx_plus_2NGHOSTS[2]-NGHOSTS) {
                const int idx = IDX3(i0,i1,i2);
                REAL xx0 = xx[0][i0];
                REAL xx1 = xx[1][i1];
                REAL xx2 = xx[2][i2];
                REAL xCart[3];
    #include "xxCart.h"
                fprintf(out2D,"%e %e %e %e\n",
                        xCart[1],xCart[2],
                        log10(fabs(evol_gfs[IDX4pt(TRKGF,idx)])+1e-10),log10(fabs(aux_gfs[IDX4pt(HGF,idx)])));
//                        aux_gfs[IDX4pt(SGF,idx)],log10(fabs(aux_gfs[IDX4pt(HGF,idx)])));
            }
            fclose(out2D);
        }
        /* -= RK4: Step 2 of 4 =- */
        rhs_eval(Nxx,Nxx_plus_2NGHOSTS,dxx, xx, aux_gfs, next_in_gfs, k2_gfs);
        #pragma omp parallel for
        for(int i=0;i<Nxx_plus_2NGHOSTS_tot*NUM_EVOL_GFS;i++) {
            k2_gfs[i] *= dt;
            next_in_gfs[i] = evol_gfs[i] + k2_gfs[i]*0.5;
        }
        apply_bcs(Nxx, Nxx_plus_2NGHOSTS, bc_gz_map,bc_parity_conditions,NUM_EVOL_GFS,evol_gf_parity, next_in_gfs);
        enforce_detgammabar_constraint(Nxx_plus_2NGHOSTS, xx, next_in_gfs);

        /* -= RK4: Step 3 of 4 =- */
        rhs_eval(Nxx,Nxx_plus_2NGHOSTS,dxx, xx, aux_gfs, next_in_gfs, k3_gfs);
        #pragma omp parallel for
        for(int i=0;i<Nxx_plus_2NGHOSTS_tot*NUM_EVOL_GFS;i++) {
            k3_gfs[i] *= dt;
            next_in_gfs[i] = evol_gfs[i] + k3_gfs[i];
        }
        apply_bcs(Nxx, Nxx_plus_2NGHOSTS, bc_gz_map,bc_parity_conditions,NUM_EVOL_GFS,evol_gf_parity, next_in_gfs);
        enforce_detgammabar_constraint(Nxx_plus_2NGHOSTS, xx, next_in_gfs);

        /* -= RK4: Step 4 of 4 =- */
        rhs_eval(Nxx,Nxx_plus_2NGHOSTS,dxx, xx, aux_gfs, next_in_gfs, k4_gfs);
        #pragma omp parallel for
        for(int i=0;i<Nxx_plus_2NGHOSTS_tot*NUM_EVOL_GFS;i++) {
            k4_gfs[i] *= dt;
            evol_gfs[i] += (1.0/6.0)*(k1_gfs[i] + 2.0*k2_gfs[i] + 2.0*k3_gfs[i] + k4_gfs[i]);
        }
        apply_bcs(Nxx, Nxx_plus_2NGHOSTS, bc_gz_map,bc_parity_conditions,NUM_EVOL_GFS,evol_gf_parity, evol_gfs);
        enforce_detgammabar_constraint(Nxx_plus_2NGHOSTS, xx, evol_gfs);

        /* Step 3: Output 2D data file, for visualization */
/*
        if(n%5 == 0) {
            // Evaluate Hamiltonian constraint violation
            Hamiltonian_constraint(Nxx,Nxx_plus_2NGHOSTS,dxx, xx, evol_gfs, aux_gfs);

    //        const int i1mid = Nxx_plus_2NGHOSTS[1]/2;
            char filename[100];
            sprintf(filename,"out%d-%08d.txt",Nxx[0],n);
            FILE *out2D = fopen(filename, "w");
            LOOP_REGION(NGHOSTS,Nxx_plus_2NGHOSTS[0]-NGHOSTS, NGHOSTS,Nxx_plus_2NGHOSTS[1]-NGHOSTS, NGHOSTS,Nxx_plus_2NGHOSTS[2]-NGHOSTS) {
                const int idx = IDX3(i0,i1,i2);
                REAL xx0 = xx[0][i0];
                REAL xx1 = xx[1][i1];
                REAL xx2 = xx[2][i2];
                REAL xCart[3];
    #include "xxCart.h"
                fprintf(out2D,"%e %e %e %e\n",
                        xCart[1],xCart[2],
                        evol_gfs[IDX4pt(CFGF,idx)],log10(fabs(aux_gfs[IDX4pt(HGF,idx)])));
            }
            fclose(out2D);
        }
*/
        /* Validation: Output Hamiltonian constraint violation */
        if(n==N_final-1) {
            // Evaluate Hamiltonian constraint violation
            Hamiltonian_constraint(Nxx,Nxx_plus_2NGHOSTS,dxx, xx, evol_gfs, aux_gfs);
            char filename[100];
            sprintf(filename,"out%d.txt",Nxx[0]);
            FILE *out2D = fopen(filename, "w");
            const int i0MIN=NGHOSTS; // In spherical, r=Delta r/2.
            const int i1mid=Nxx_plus_2NGHOSTS[1]/2;
            const int i2mid=Nxx_plus_2NGHOSTS[2]/2;
            LOOP_REGION(NGHOSTS,Nxx_plus_2NGHOSTS[0]-NGHOSTS, NGHOSTS,Nxx_plus_2NGHOSTS[1]-NGHOSTS, NGHOSTS,Nxx_plus_2NGHOSTS[2]-NGHOSTS) {
                REAL xx0 = xx[0][i0];
                REAL xx1 = xx[1][i1];
                REAL xx2 = xx[2][i2];
                REAL xCart[3];
    #include "xxCart.h"
                int idx = IDX3(i0,i1,i2);
//                fprintf(out2D,"%e %e %e %e\n",xCart[1],xCart[2], evol_gfs[IDX4pt(TRKGF,idx)],log10(fabs(aux_gfs[IDX4pt(HGF,idx)])));
                fprintf(out2D,"%e %e %e %e\n",xCart[1],xCart[2], log10(fabs(evol_gfs[IDX4pt(TRKGF,idx)])+1e-10),log10(fabs(aux_gfs[IDX4pt(HGF,idx)])));
            }
            fclose(out2D);
        }
        // Progress indicator printing to stdout
        // Measure average time per iteration
#
#ifdef __linux__ // Use high-precision timer in Linux.
    clock_gettime(CLOCK_REALTIME, &end);
    const long long unsigned int time_in_ns = 1000000000L * (end.tv_sec - start.tv_sec) + end.tv_nsec - start.tv_nsec;
#elif _WIN32     // Resort to low-resolution, standards-compliant timer in Windows
    time(&end_timer); // Resolution of one second...
    REAL time_in_ns = difftime(end_timer,start_timer)*1.0e9+0.5; // Round up to avoid divide-by-zero.
#endif
        const REAL s_per_iteration_avg = ((REAL)time_in_ns / (REAL)n) / 1.0e9;

        const int iterations_remaining = N_final - n;
        const REAL time_remaining_in_mins = s_per_iteration_avg * (REAL)iterations_remaining / 60.0;

        const REAL num_RHS_pt_evals = (REAL)(Nxx[0]*Nxx[1]*Nxx[2]) * 4.0 * (REAL)n; // 4 RHS evals per gridpoint for RK4
        const REAL RHS_pt_evals_per_sec = num_RHS_pt_evals / ((REAL)time_in_ns / 1.0e9);

        // Progress indicator printing to stderr
        fprintf(stderr,"%c[2K", 27); // Clear the line
        fprintf(stderr,"It: %d t=%.2f | %.1f%%; ETA %.0f s | t/h %.2f | gp/s %.2e\r",  // \r is carriage return, move cursor to the beginning of the line
               n, n * (double)dt, (double)(100.0 * (REAL)n / (REAL)N_final),
               (double)time_remaining_in_mins*60, (double)(dt * 3600.0 / s_per_iteration_avg), (double)RHS_pt_evals_per_sec);
        fflush(stderr); // Flush the stderr buffer
    } // End main loop to progress forward in time.
    fprintf(stderr,"\n"); // Clear the line.

    /* Step 4: Free all allocated memory */
    free(bc_gz_map);
    free(bc_parity_conditions);
    free(r_Schw_arr);
    free(rho_arr);
    free(P_arr);
    free(M_arr);
    free(expnu_arr);
    free(exp4phi_arr);
    free(rbar_arr);

    free(k4_gfs);
    free(k3_gfs);
    free(k2_gfs);
    free(k1_gfs);
    free(aux_gfs);
    free(next_in_gfs);
    free(evol_gfs);
    for(int i=0;i<3;i++) free(xx[i]);
    return 0;
}
```

    Overwriting BSSN/TOV_Playground.c

```python
!rm -f TOV_Playground out96-00*.txt out96-00*.txt.png
#!gcc -Ofast -march=native -ftree-parallelize-loops=2 -fopenmp BSSN/TOV_Playground.c -o TOV_Playground -lm
#!taskset -c 0,1 ./TOV_Playground 96 96 96 > out96.txt
!gcc -fopenmp -O BSSN/TOV_Playground.c -o TOV_Playground -lm
!taskset -c 0,1,2,3 ./TOV_Playground 96 16 2 > out96.txt

# Windows Code
# import os
# N_physical_cores = 4
# !gcc -Ofast -march=native -fopenmp BSSN/TOV_Playground.c -o TOV_Playground -lm
# # !icc -O2 -g BSSN/TOV_Playground.c -o TOV_Playground -lm
# # !valgrind ./TOV_Playground 4 4 4
# for resolution in ["96 16 2", "48 8 2"]:
#     script = ""
#     check_for_taskset = !which taskset >/dev/null && echo $?
#     if check_for_taskset == ['0']:
#         script += "taskset -c 0"
#         for i in range(N_physical_cores-1):
#             script += ","+str(i+1)
#         script += " "
#     exec_string = os.path.join(".", "TOV")
#     script += exec_string +"_Playground "+resolution+" > out"+resolution[:2]+".txt"
#     print("Executing `"+script+"`...")
#     os.system(script)
# print("Finished this code cell.")

```

    It: 326 t=0.50 | 100.0%; ETA 0 s | t/h 169.11 | gp/s 3.76e+05

<a id='visual'></a>

# Step 7: Data Visualization Animations \[Back to [top](#toc)\]
$$\label{visual}$$

**Special thanks to Terrence Pierre Jacques. His work with the first versions of these scripts greatly contributed to the scripts as they exist below.**

```python
## VISUALIZATION ANIMATION, PART 1: Generate PNGs, one per frame of movie ##

import numpy as np
from scipy.interpolate import griddata
import matplotlib.pyplot as plt
from matplotlib.pyplot import savefig
from IPython.display import HTML
import matplotlib.image as mgimg

import glob
import sys
from matplotlib import animation

globby = glob.glob('out96-00*.txt')
file_list = []
for x in sorted(globby):
    file_list.append(x)

bound=1.5
pl_xmin = -bound
pl_xmax = +bound
pl_ymin = -bound
pl_ymax = +bound

N_interp_pts  = 300
N_interp_ptsj = 300j

for filename in file_list:
    fig = plt.figure()
    x,y,other,Ham = np.loadtxt(filename).T #Transposed for easier unpacking

    plotquantity = Ham
    plotdescription = "Numerical Soln."
    plt.title("Single Neutron Star (Ham. constraint)")
    plt.xlabel("y/M")
    plt.ylabel("z/M")

    grid_x, grid_y = np.mgrid[pl_xmin:pl_xmax:N_interp_ptsj, pl_ymin:pl_ymax:N_interp_ptsj]
    points = np.zeros((len(x), 2))
    for i in range(len(x)):
        # Zach says: No idea why x and y get flipped...
        points[i][0] = y[i]
        points[i][1] = x[i]

    grid = griddata(points, plotquantity, (grid_x, grid_y), method='nearest')
    gridcub = griddata(points, plotquantity, (grid_x, grid_y), method='cubic')
    im = plt.imshow(grid, extent=(pl_xmin,pl_xmax, pl_ymin,pl_ymax))
    #plt.pcolormesh(grid_y,grid_x, grid, vmin=-8, vmax=0) # Set colorbar range from -8 to 0
    ax = plt.colorbar()
    ax.set_label(plotdescription)
    savefig(filename+".png",dpi=150)
    plt.close(fig)
    sys.stdout.write("%c[2K" % 27)
    sys.stdout.write("Processing file "+filename+"\r")
    sys.stdout.flush()
```

    Processing file out96-00000320.txt

```python
## VISUALIZATION ANIMATION, PART 2: Combine PNGs to generate movie ##

# https://stackoverflow.com/questions/14908576/how-to-remove-frame-from-matplotlib-pyplot-figure-vs-matplotlib-figure-frame
# https://stackoverflow.com/questions/23176161/animating-pngs-in-matplotlib-using-artistanimation

fig = plt.figure(frameon=False)
ax = fig.add_axes([0, 0, 1, 1])
ax.axis('off')

myimages = []

for i in range(len(file_list)):
    img = mgimg.imread(file_list[i]+".png")
    imgplot = plt.imshow(img)
    myimages.append([imgplot])

ani = animation.ArtistAnimation(fig, myimages, interval=100,  repeat_delay=1000)
plt.close()
ani.save('SingleNS.mp4', fps=5,dpi=150)
```

```python
## VISUALIZATION ANIMATION, PART 3: Display movie as embedded HTML5 (see next cell) ##

# https://stackoverflow.com/questions/18019477/how-can-i-play-a-local-video-in-my-ipython-notebook
```

```python
%%HTML

```

<a id='convergence'></a>

# Step 8: Validation: Convergence of numerical errors (Hamiltonian constraint violation) to zero \[Back to [top](#toc)\]
$$\label{convergence}$$

The equations behind these initial data solve Einstein's equations exactly, at a single instant in time. One reflection of this solution is that the Hamiltonian constraint violation should be exactly zero in the initial data.

However, when evaluated on numerical grids, the Hamiltonian constraint violation will *not* generally evaluate to zero due to the associated numerical derivatives not being exact. However, these numerical derivatives (finite difference derivatives in this case) should *converge* to the exact derivatives as the density of numerical sampling points approaches infinity.

In this case, all of our finite difference derivatives agree with the exact solution, with an error term that drops with the uniform gridspacing to the fourth power: $\left(\Delta x^i\right)^4$.

Here, as in the [Start-to-Finish Scalar Wave (Cartesian grids) NRPy+ tutorial](Tutorial-Start_to_Finish-ScalarWave.ipynb) and the [Start-to-Finish Scalar Wave (curvilinear grids) NRPy+ tutorial](Tutorial-Start_to_Finish-ScalarWaveCurvilinear.ipynb) we confirm this convergence.

First, let's take a look at what the numerical error looks like on the x-y plane at a given numerical resolution, plotting $\log_{10}|H|$, where $H$ is the Hamiltonian constraint violation:

```python
grid96 = griddata(points, plotquantity, (grid_x, grid_y), method='nearest')
grid96cub = griddata(points, plotquantity, (grid_x, grid_y), method='cubic')

# fig, ax = plt.subplots()

plt.clf()
plt.title("96^3 Numerical Err.: log_{10}|Ham|")
plt.xlabel("x/M")
plt.ylabel("y/M")

fig96cub = plt.imshow(grid96cub.T, extent=(pl_xmin,pl_xmax, pl_ymin,pl_ymax))
cb = plt.colorbar(fig96cub)
```

Next, we set up the same initial data but on a lower-resolution, $48\times 8\times 2$ grid (axisymmetric in the $\phi$ direction). Since the constraint violation (numerical error associated with the fourth-order-accurate, finite-difference derivatives) should converge to zero with the uniform gridspacing to the fourth power: $\left(\Delta x^i\right)^4$, we expect the constraint violation will increase (relative to the $96\times 16\times 2$ grid) by a factor of $\left(96/48\right)^4$. Here we demonstrate that indeed this order of convergence is observed as expected, *except* in the region causally influenced by the star's surface at $\bar{r}=\bar{R}\approx 0.8$ where the stress-energy tensor $T^{\mu\nu}$ sharply drops to zero.

```python
!rm -f out48-00*.txt out48-00*.txt.png
!taskset -c 0,1,2,3 ./TOV_Playground 48 8 2 > out48.txt

x48,y48,valuesother48,valuesHam48 = np.loadtxt('out48.txt').T #Transposed for easier unpacking
points48 = np.zeros((len(x48), 2))
for i in range(len(x48)):
    points48[i][0] = x48[i]
    points48[i][1] = y48[i]

grid48 = griddata(points48, valuesHam48, (grid_x, grid_y), method='cubic')

griddiff_48_minus_96 = np.zeros((N_interp_pts,N_interp_pts))
griddiff_48_minus_96_1darray = np.zeros(N_interp_pts*N_interp_pts)
gridx_1darray_yeq0 = np.zeros(N_interp_pts)
grid48_1darray_yeq0 = np.zeros(N_interp_pts)
grid96_1darray_yeq0 = np.zeros(N_interp_pts)
count = 0
outarray = []
for i in range(N_interp_pts):
    for j in range(N_interp_pts):
        griddiff_48_minus_96[i][j] = grid48[i][j] - grid96[i][j]
        griddiff_48_minus_96_1darray[count] = griddiff_48_minus_96[i][j]
        if j==N_interp_pts/2-1:
            gridx_1darray_yeq0[i] = grid_x[i][j]
            grid48_1darray_yeq0[i] = grid48[i][j] + np.log10((48./96.)**4)
            grid96_1darray_yeq0[i] = grid96[i][j]
        count = count + 1

plt.clf()
fig, ax = plt.subplots()
plt.title("Plot Demonstrating 4th-order Convergence")
plt.xlabel("x/M")
plt.ylabel("log10(Relative error)")

ax.plot(gridx_1darray_yeq0, grid96_1darray_yeq0, 'k-', label='Nr=96')
ax.plot(gridx_1darray_yeq0, grid48_1darray_yeq0, 'k--', label='Nr=48, mult by (48/96)^4')
ax.set_ylim([-9.5,-1.5])

legend = ax.legend(loc='lower right', shadow=True, fontsize='x-large')
legend.get_frame().set_facecolor('C1')
plt.show()
```

<a id='latex_pdf_output'></a>

# Step 9: Output this module 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-Start_to_Finish-BSSNCurvilinear-Neutron_Star-Hydro_without_Hydro.pdf](Tutorial-Start_to_Finish-BSSNCurvilinear-Neutron_Star-Hydro_without_Hydro.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 Tutorial-Start_to_Finish-BSSNCurvilinear-Neutron_Star-Hydro_without_Hydro.ipynb
!pdflatex -interaction=batchmode Tutorial-Start_to_Finish-BSSNCurvilinear-Neutron_Star-Hydro_without_Hydro.tex
!pdflatex -interaction=batchmode Tutorial-Start_to_Finish-BSSNCurvilinear-Neutron_Star-Hydro_without_Hydro.tex
!pdflatex -interaction=batchmode Tutorial-Start_to_Finish-BSSNCurvilinear-Neutron_Star-Hydro_without_Hydro.tex
!rm -f Tut*.out Tut*.aux Tut*.log
```

    [NbConvertApp] Converting notebook Tutorial-Start_to_Finish-BSSNCurvilinear-Neutron_Star-Hydro_without_Hydro.ipynb to latex
    [NbConvertApp] Writing 197032 bytes to Tutorial-Start_to_Finish-BSSNCurvilinear-Neutron_Star-Hydro_without_Hydro.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


```python
%load_ext autoreload
%autoreload 2
import sympy as smp
import numpy as np
import scipy.special as sp

import matplotlib
matplotlib.rcParams['pdf.fonttype'] = 42
matplotlib.rcParams['ps.fonttype'] = 42

import matplotlib.pyplot as plt
#from matplotlib import animation
%matplotlib inline

import seaborn as sns
color_names = ["windows blue",
               "red",
               "gold",
               "grass green"]
colors = sns.xkcd_palette(color_names)
sns.set_style("white")
```

    The autoreload extension is already loaded. To reload it, use:
      %reload_ext autoreload

```python
# Load results
ELBO_grep = np.loadtxt('results/grep_gammaDEF_elbo.txt')
ELBO_advi = np.loadtxt('results/faces_elbo_advi.csv')
ELBO_bbvi = np.loadtxt('results/faces_elbo_bbvi.csv')
ELBO_R1 = np.load('results/Olivette_Eta0.75_B4_corrTrue_ELBO.npy')
ELBO_R1 = ELBO_R1[ELBO_R1<0.]
```

```python
d_advi = 50
d_grep = 25
d_rsvi = 25

# Versus time
max_time = 75000
fig = plt.figure(figsize=(12, 9))
ax = fig.add_subplot(111)

# ADVI
ind = 2.*ELBO_advi[:,0] < max_time
#print np.sum(ind)/d_advi
E_plot = ELBO_advi[ind,:]
ax.semilogx(2.*E_plot[::d_advi,0],E_plot[::d_advi,1], color=colors[3],lw=4, label='$\\mathrm{ADVI}$')

# BBVI
ind = 2.*ELBO_bbvi[:,0] < max_time
#print np.sum(ind)
ax.semilogx(2.*ELBO_bbvi[ind,0],ELBO_bbvi[ind,1], color=colors[2],lw=4, label='$\\mathrm{BBVI}$')

# G-REP
ind = 2.*ELBO_grep[:,1] < max_time
#print np.sum(ind)/d_grep
E_plot = ELBO_grep[ind,:]
ax.semilogx(2.*E_plot[::d_grep,1],E_plot[::d_grep,2], color=colors[0],lw=4, label='$\\mathrm{G-REP}$')

# Rejection
time_rejection = 1. # Relative time versus ELBO_grep
time_vec = time_rejection*np.arange(ELBO_R1.shape[0])+time_rejection
ind = time_vec < max_time
#print np.sum(ind)/d_rsvi
time_vec = time_vec[ind]
E_plot = ELBO_R1[ind]
ax.semilogx(time_vec[::d_rsvi]+1,E_plot[::d_rsvi], color=colors[1],lw=4, label='$\\mathrm{RSVI}$')

ax.grid()
ax.set_ylim(-2.5e7, -0.5e7)
ax.set_xlim(1,max_time)
ax.yaxis.get_offset_text().set_fontsize(16)
ax.tick_params(axis='both', which='major', labelsize=16)
ax.tick_params(axis='both', which='minor', labelsize=16)
ax.set_xlabel("Time $[s]$", fontsize=22)
ax.set_ylabel('$\\mathrm{ELBO}$',fontsize=20)
ax.legend(loc='lower right',fontsize=18)
filename = 'plots/Olivette_time.pdf'
plt.savefig(filename, bbox_inches='tight',dpi=300)
```

```python

```

```python

```

ars-reparameterization/sparse gamma def/olivetti faces elbo.ipynb


# Intermediate Python I

Please indicate your name below, since you will need to submit this notebook completed latest the day after the datalab.

Don't forget to save your progress during the datalab to avoid any loss due to crashes.

```python
name=''
```

During the previous datalab we have learnt about the basic built-in features of python: we learnt about variable and data types, loops and conditions. We also mentioned before that there are thousands of open source packages which can be imported to python. Several of these libraries are considered rather standard (and preinstalled when using anaconda). If we would like to create a package executing less standard tasks (for example parsing nuclear data formats), it is worth browsing the world wide web for pre-existing solutions before reinvintenting the wheel. There is for example the nuclear engineering toolkit called Pyne which provides a lot of functionality relevant for nuclear scientists.

In the next two datalabs we are going to try some standard libraries, and get acquainted with various data formats.

Libraries reviewed today are:
- numpy which provides data containers and functions to perform numerical analysis, matrix operations, linear algebra and a lot more.
- matplotlib which provides visualization tools from simple 2D plots to advanced 3D visualizations
- scipy which provides scientific tools, for example fitting, ODE solvers, numerical integration
- re which allows using regular expressions. A regular expression (often called regex or regexp) is a search pattern specified as a sequence of characters. The `re` package provides regexp operations.

It is neither possible nor necessary to review all the features of these packages, rather we would like to provide you with the basic terminology and to cover the basic functionality of these packages. That said, what is imporant for now is to know about these packages, familiarize yourself with the basic principles of these tools. Later, in other exercises we are going to refer to functions and methods of these libraries. It is not required to keep all functionality of these libraries in your head, as you will advance you will often find yourself googling for solutions, reading through stackoverflow entries, and then with your basic understanding and the knowledge of the terminology you will be able to adapt the proposed solutions to your own needs.

There are several other mainstream libraries of python which we will not cover at all in this course. For example symPy allows for symbolic operations, or sklearn provides an interface to machine learning models. Nevertheless, once you got a feeling for using python, it will be relatively easy for you to use any other package according to your future needs.

## Numpy

We saw before how to organize data in arrays called lists. Lists are extremely flexible, they can store data of different types (eg. `[1,'one',True]`), and one can easily create multidimensional lists, or matrices (eg. `[[1,2,3],[4,5,6],[7,8,9]]`). Nevertheless, in some sense this flexibility is also a drawback. If we perform operations on a list (for example by looping over its elements, and performing some simple mathetmatics), Python needs to check the type of each element. For a compiled code, when the type is declared beforehand such checks are not needed, hence it runs faster. Numpy (numerical python) offers a more efficient data structure called *numpy array* (which stores elements of the same type) and provides related data operations. Numpy arrays are important building blocks of several other tools (eg. data science applications), and they make life easier, hence it is valuable to understand how to use them.

Besides the numpy array, numpy contains a lot of useful functions and libraries (operations and linear algebra on the arrays, random number generators etc), which we will not cover now, but we will use them later on.

The common way to import numpy is with

```python
import numpy as np
```

Here we state that we want to `import numpy`, and later we will refer to it as `np`, meaning that we can access functions within numpy with `np.functionName()`. One can use different reference names as well, or just simply `import numpy`, and refer to functions with `numpy.functionName()`. (We could import directly functions from numpy, for example `from numpy import cos`, or we could import everything from numpy with `from numpy import *`, however this is generally not adviced since some libraries might have functions with the same name, like there is a `cos` implementation both in `math` and numpy, and the two implementations do not have the same functionality (eg. the numpy version can use lists as an input, the math version cannot).

Let's look at numpy arrays.

What would you expect when you execute this?

```python
a=np.array([1,2,3])
a[0]=2.3
a
```

```python
b=np.array([0,1.2,'one'])
print(b)
b[0]=42.0
b
```

In the first case, python interprets the definition as the array only stores integers. Upon changing an element it will convert the float to an integer. You have to be careful, since we saw that in normal python a list like `[0,0,0]` can be updated with floats, but `np.array([0,0,0])` cannot. Nevertheless we will see that initializing an array with zeros should be done with `np.zeros()`.

In the second case essentially the same thing happens. Python cannot interpret `'one'` as a number, instead it broadcasts the string type to all other elements. Upon modifying an element, it is automatically converted to string.

One can however directly give a type to a numpy array (although when working with floats it is often just better to write out the decimals).

```python
c=np.array([1, 2, 3], dtype='float32')
print(c)
c[0]=2.3
c
```

Numpy provides several functions to initialize arrays (even 2D), which are probably familiar to Matlab users:

```python
a=np.zeros(3)
print(a)
A=np.zeros((3,3))
print(A)
b=np.ones(3)
print(b)
c=np.linspace(-5,5,11)
print(c)
```

```python
print(A.shape)
print(len(c))
```

Indexing of numpy arrays is similar as for lists, however for matrices we reach the elements with comma separated indices.

```python
B=np.arange(9)
print(B)
print(B[2:5])
print(B[2:8:2]) #start:stop:step
```

```python
C=B.reshape((3,3))
print(C)
print(C[:,1]) #second column
print(C[1,:]) #second row
print(C[1,1])
```

With numpy arrays we can easily perform aggregations, see the examples below. This is powerful, when needed to be performed on large arrays.

```python
print(np.min(c))
print(np.max(c))
print(np.sum(c))
print(np.mean(c))
```

And we can perform operations with the arrays, and apply mathematical functions on the arrays. Notice that doing the same with lists would require loops.

```python
print(c+100)
print(c+c)
print(3*c)
print(np.sin(c))
```

What makes numpy arrays even more powerful is that it is possible to mask them, ie. we can filter arrays based on conditions of other arrays

```python
energy=np.logspace(-3,7,20) #20 values between 1e-3 and 1e7
energy
```

```python
values=1/energy
values
```

```python
energy<1.0
```

```python
values[energy<1.0]
```

```python
values*(energy<1.0) #we multiply with bools, so ones and zeros. Keeps the same shape.
```

Last, we can mention that numpy also has some functions to read data in columns. For example if we open the file '2-U235_xs_cap.txt', we can see that it contains two columns of data:

```
    Incident energy (ev)	cross section (barn)
    1.0E-5	5960.097
    1.0625E-5	5782.129
    1.125E-5	5619.206
    1.1875E-5	5469.321
    1.25E-5	5330.822
    1.375E-5	5082.714
    1.5E-5	4866.305
    1.625E-5	4675.372
    1.75E-5	4505.28
    1.875E-5	4352.493
    ...
```

We do not need to write a parser to read this file, instead we can use the `np.loadtxt()` function.

```python
u235=np.loadtxt('02-sample.txt',skiprows=1) #we skip the first row, which is a header
u235
```

As said before, numpy has a lot of other functionality what we cannot cover in one single datalab, but later we will cover the ones needed for solving the exercises.

## Matplotlib

We saw previously how to do some mathematics with Python, but of course as aspiring scientists or engineers we would like to present our results visually as well. Matplotlib is a multi-platform data visualization library. It is designed to work with Numpy arrays. It will allow us to create 2D and 3D plots, and we can even create simple graphics as we will see. Matplotlib works well on several operating systems and it supports several output formats.

The basic syntax of Matplotlib will feel familiar to Matlab users, and indeed it is straightforward to create basic plots. However, creating more difficult plots can feel sometimes frustrating.

Besides Matplotlib, an other mainstream visualization library called Seaborn is also available in the Python universe. We are not going to use this in the course.

### Simple 2D plot

Let us first simply look at the basic features by plotting the exponential decay curve. The most simple example would look like this.

```python
import matplotlib.pyplot as plt

Thalf=14.9 #days
lam=np.log(2)/Thalf #1/days
N0=1e6 #number of atoms
time=np.linspace(0,200,50) #in days
atoms=N0*np.exp(-lam*time)

plt.figure()
plt.plot(time,atoms)
plt.show()
```

As an other simple example we can plot the content of the previously read file (which in fact is the neutron capture cross section of uranium-235. Here we use the `plt.loglog` function to have a logarithmic scale, and we set the labels of the axes.

```python
plt.figure()
plt.loglog(u235[:,0],u235[:,1])
plt.xlabel('energy (eV)')
plt.ylabel('cross section (barn)')
plt.show()
```

In the following we will show how to customize parts of the figure. With `plt.figure` we create a canvas (and can set a size), then `plt.plot` will plot the specified X and Y arrays against each other. Here we can set the color, the type of the curve (solid or dashed etc.), the markers, and marker size or line width (`lw`), and we can add a label. If more `plt.plot` commands would be included, more curves would appear on the canvas. We can include a label, which will be shown if a legend is created (`plt.legend`)

We can set the label on the x and y axis (`plt.xlabel`, `plt.ylabel`), and a title (`plt.title`). LaTeX expressions can be used where strings are given to describe content (`r'$a=3$'` will be rendered as LaTeX, `a=3` would be rendered as a normal string). With `xlim`, `ylim`, `xticks` and `yticks` we can set the boundaries of the plot and overwrite the default tick positions and labels.

We can include a grid (`plt.grid`), and we can we can include custom horizontal and vertical lines (`plt.axhline` and `plt.axvline`).

Note that all these commands are optional, and they have further parameters to tune. Almost anything is possible here. Don't be afraid to google for solutions when you want to make your plots pretty.

The `matplotlib.pyplot` library has several more plotting options for scatter plots, errorbars, histograms, barplots, charts etc. You can also change the axis to logarithmic (and there are functions readily available for this which might sound familiar to previous Matlab users: `plt.loglog`, `plt.semilogx`, `plt.semilogy`. We will use some of these later in the course.

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

Thalf=14.9 #days
lam=np.log(2)/Thalf #1/days
N0=1e6 #number of atoms

time=np.linspace(0,200,1000) #in days

plt.figure(figsize=(10,4))
plt.plot(time,N0*np.exp(-lam*time),'k-',lw=2,label=r'$N_0e^{-\lambda t}$')
plt.title('Exponential Decay')
plt.xlabel('Time',fontsize=14)
plt.ylabel('Number of nuclei',fontsize=14)

plt.xticks([(i)*Thalf for i in range(1,14)],        #list of values where ticks are added
           [r'$%dT_{1/2}$'%i for i in range(1,14)]) #list of strings written at the ticks

plt.yticks([N0/i for i in [1,2,4,8,16]],            #list of values where ticks are added
           [r'$N_0/%d$'%i for i in [1,2,4,8,16]])   #list of strings written at the ticks

plt.xlim(0,200.0)
plt.ylim(0,N0)

plt.grid()
plt.axhline(N0/2,xmin=0,xmax=Thalf/200.0,color='r',ls='--') #xmin and max between 0 and 1; ls: linestyle
plt.axvline(Thalf,ymin=0,ymax=1/2,color='r',ls='--')

plt.axhline(N0/4,xmin=0,xmax=2*Thalf/200.0,color='r',ls='--') #xmin and max between 0 and 1
plt.axvline(2*Thalf,ymin=0,ymax=1/4,color='r',ls='--')

plt.axhline(N0/8,xmin=0,xmax=3*Thalf/200.0,color='r',ls='--') #xmin and max between 0 and 1
plt.axvline(3*Thalf,ymin=0,ymax=1/8,color='r',ls='--')

plt.axhline(N0/16,xmin=0,xmax=4*Thalf/200.0,color='r',ls='--') #xmin and max between 0 and 1
plt.axvline(4*Thalf,ymin=0,ymax=1/16,color='r',ls='--')

# Notice that we could have used a for loop for this part
#for i in range(1,5):
#    plt.axhline(N0*0.5**i,xmin=0,xmax=i*Thalf/200.0,color='r',ls='--') #xmin and max between 0 and 1
#    plt.axvline(i*Thalf,ymin=0,ymax=0.5**i,color='r',ls='--')

plt.legend()
#plt.savefig('myfigure.png',dpi=300)
plt.show()
```

### Simple 2D graphics

Matplotlib allows us to create graphics as well, we can define Polygons, Circles etc and place them on the canvas. We can also combine these graphics elements with data plots.

In the following we will want to define a function which becomes zero on the edges of a hexagon (let's consider we have a hexagonal reactor, and the neutron population becomes zero on the boundary). We can use matplotlib to draw a hexagon to define our problem. Note, that here we also use `plt.annotate` to place text on our canvas, and we switched off the visibility of the right and top axes.

```python
import matplotlib.pyplot as plt
R=10.0
fig, ax = plt.subplots()
#defining the hexagon through its corners.
polygon=plt.Polygon([[-R,0.0],[-1/2*R,np.sqrt(3)/2*R],[1/2*R,np.sqrt(3)/2*R],
                    [R,0.0],[1/2*R,-np.sqrt(3)/2*R],[-1/2*R,-np.sqrt(3)/2*R]],facecolor='white',edgecolor='black')
ax.add_artist(polygon)
plt.axvline(0.0,color='black') #we draw a vertical line for y axis
plt.axhline(0.0,color='black') #we draw a horizontal line for x axis
plt.annotate(r'$y=\sqrt{3}R-\sqrt{3}x$',(R*0.8,R/2)) #write this expression to that location
plt.annotate(r'$y=-\sqrt{3}R+\sqrt{3}x$',(R*0.8,-R/2))
plt.annotate(r'$-R$',(-R*1.4,1))
plt.annotate(r'$R$',(R*1.,1))
plt.annotate(r'$y=\frac{\sqrt{3}}{2}R$',(0.,R))
plt.xlim(-R*2.5,R*2.5) #limit the x-axis between -R*2.5, R*2.5
plt.ylim(-R*2.5,R*2.5)
plt.gca().set_aspect('equal', adjustable='box')
plt.gca().spines['right'].set_visible(False)#Switch off frame on right
plt.gca().spines['top'].set_visible(False)#Switch off frame on top
plt.show()
```

### Simple 3D plot

We could construct a function from sines or cosines which disappears at the edge of a hexagon with a width $R$. One possible solution is

$$f(x,y)=\cos\big(\frac{\pi}{\sqrt{3}R}y\big)\cdot \cos\big(\frac{\pi}{2\sqrt{3}R}(y+\sqrt{3}x)\big)\cdot \cos\big(\frac{\pi}{2\sqrt{3}R}(y-\sqrt{3}x)\big)$$.

We first define a function `hexFunc()`, which will return zero if we are outside of the hexagon, and the mathematical function's value if we are inside. For this we will use a mask. **Note**: the backslash `\` character  tells the python interpreter that the code is continued in the next line, this improves legibility.

Then we can create a meshgrid, and evaluate the function at each of the grid points. Note that how concise numpy is, we didn't need to write any loop to evaluate the function at several grid points. Finally we can plot the surface with the `plot_surface` function.

We can also plot a 2D projection/representation of the surface: with `contour` and `contourf` we can visualize contour lines and filled contour lines. With `imshow` the values are considered to be pixels of an image.

```python
def hexFunc(x,y,R):
    """Function which disappears on the edge of a hexagon

    Parameters
    ----------
    X : ndarray
        meshgrid X values.
    Y : ndarray
        meshgrid Y values
    R : size of the hexagon
    """
    z = np.zeros(x.shape)
    mask = (y<=np.sqrt(3)/2*R) *  \
           (y>=-np.sqrt(3)/2*R) * \
           (y<=np.sqrt(3)*R-np.sqrt(3)*x) *  \
           (y>=-np.sqrt(3)*R-np.sqrt(3)*x) * \
           (y<=np.sqrt(3)*R+np.sqrt(3)*x) *  \
           (y>=-np.sqrt(3)*R+np.sqrt(3)*x)
    z[mask] =np.cos(y[mask]*np.pi/np.sqrt(3)/R)*\
             np.cos((y[mask]+np.sqrt(3)*x[mask])*np.pi/2/np.sqrt(3)/R)*\
             np.cos((y[mask]-np.sqrt(3)*x[mask])*np.pi/2/np.sqrt(3)/R)
    return z

from mpl_toolkits.mplot3d import Axes3D
import numpy as np

R=10 #cm
X = np.linspace(-R, R, 1000)
Y = np.linspace(-R, R, 1000)
Xn, Yn = np.meshgrid(X, Y)

Z=hexFunc(Xn,Yn,R)

fig = plt.figure()
ax = fig.gca(projection='3d')
surf=ax.plot_surface(Xn, Yn, Z, cmap="plasma")
fig.colorbar(surf)
plt.show()
```

```python
plt.figure()
plt.contour(Xn,Yn,Z)
plt.axis('equal')
plt.colorbar()
plt.show()
```

```python
plt.figure()
plt.contourf(Xn,Yn,Z)
plt.axis('equal')
plt.colorbar()
plt.show()
```

```python
plt.figure()
plt.imshow(Z)
plt.axis('equal')
plt.colorbar()
plt.show()
```

## Scipy

The name Scipy is a bit confusing: in a way Scipy is a whole ecosystem including numpy, matplotlib, pandas and other packages, but there is also a package called Scipy within this ecosystem. Probably one could spend a whole course just on covering the functionality of Scipy. It provides solutions to most of the engineering/science problems (fitting, optimization, integration, Fourier transform etc). Here we just use one illustrative example on how to solve simple coupled ODE systems with it by using the `solve_ivp` function from the `scipy.integrate` package, and later when we need some other functionality we will introduce other functions.

Let's solve the simple decay chain when a parent nuclide decays into a radioactive daughter: $P\rightarrow D \rightarrow$, with decay constants $\lambda_P$ and $\lambda_D$. The differential equations characterizing this process are

$$\frac{dN_P}{dt}=-\lambda_P N_P$$

$$\frac{dN_D}{dt}=\lambda_P N_P-\lambda_D N_D$$

with initial condition $N_P(t=0)=N_P(0)$ and $N_D(t=0)=0$.

The same system could be written in matrix form:

\begin{equation}
\frac{d}{dt}
\begin{pmatrix}
N_P \\ N_D
\end{pmatrix}
=
\begin{pmatrix}
-\lambda_P & 0 \\ \lambda_P & -\lambda_D
\end{pmatrix}
\begin{pmatrix}
N_P \\ N_D
\end{pmatrix}
\end{equation}

or with using vectors ($N=(N_P \: N_D)$) and matrices

\begin{equation}
\dot N=AN
\end{equation}

We will use the `scipy.integrate.solve_ivp` function to solve numerically this system (note that we do not import the whole scipy library, only this function, and remember that you can access the documentation with `?solve_ivp`) in the form of $\dot y=Ay$. The solver requires a function in the form `myDerivative(t,y)` which describes the derivative $Ay$. Optinally this function can have some extra arguments. Then this derivative function is passed to `solve_ivp`, along with the time span (time window in which the integration is performed), the initial conditions and the times at which $y$ is evaluated, and any arguments (`args`) needed by the derivative function.

`sol=solve_ivp(myDerivative,(Tstart,Tend),[InitialConditions],t_eval=TimesArray,args=(arguments))`

```python
?solve_ivp
```

```python
def derivDaughter(t,y,lp,ld):
    A=np.array([[-lp,0.0],[lp,-ld]])
    return np.dot(A,y)

from scipy.integrate import solve_ivp

TP=14.9 #d, parent, and all time units are considered to be in days
lP=np.log(2)/TP
TD=9.9
lD=np.log(2)/TD

N0=50
Tstart=0.0
Tend=90.0 #days
Neval=1001
T_eval=np.linspace(Tstart,Tend, Neval)
sol=solve_ivp(derivDaughter,(Tstart,Tend),[N0,0.0],t_eval=T_eval,args=(lP, lD))

plt.figure()
plt.plot(sol.t,sol.y[0],label='Ra')
plt.plot(sol.t,sol.y[1],label='Act')
plt.xlabel('time (d)')
plt.legend()
plt.ylabel('Number of nuclei')
plt.show()
```

```python
?solve_ivp
```

## find() and re

It is often required in science and engineering applications that one parses various files (for example outputs produced by a software, or data files), and need to extract some specific information.

Python provides a basic string method (https://www.w3schools.com/python/python_ref_string.asp), to find strings within strings. Take a look at the code below. Let's assume that some software gives the final results in the following format, and we are interested in the 'keff' value. We could locate the number by matching the `=` sign, and the word `with`. Of course this is not perfect. What happens if a word or substring appears more than once?

```python
resuStr='the final estimated keff = 1.04562 with an estimated standard deviation of 0.00057'
i=resuStr.find('=')
print(i)
j=resuStr.find('with')
print(j)
print(float(resuStr[i+1:j]))
```

A more accurate solution to problems like this is to use regular expressions. Regular expressions are not a part of python, it is an independent "language", which allows the description of a search pattern as a string. Nevertheless, python has a package `re` which let's python users apply regexp. Hardcore geeks probably know by heart all the possible regexp patterns, and I certainly advice you to [read up on them](https://www.rexegg.com/regex-quickstart.html), nevertheless for this course we will only need to read numbers from strings. And you can most often survive by googling "regexp integer within string" or similar phrases depending on your application.

One function of the `re` package is `re.findall`, the first input should be a regular expression as a string, and the second is the string in which you would like to perform the search. Take the regular expression below, which will match floating point numbers. The pattern `[+-]?\d+\.\d+` encodes a floating point number: in `[+-]?` the ? indicates zero or one occurence of the preceding element, which in this case is one of the characters in the `[]`. Outside of the bracket, the + indicates one or more  occurance of the preceding element, and `\d` indicates a digit, so `\d+` will match one or more digits. In total `\d+\.\d+` will look for one or more digits followed by a floating point and followed by one or more digits. This pattern will not match numbers written as '1.' or '.9932'.

```python
import re

re.findall('[+-]?\d+\.\d+',resuStr)
```

An other typical example a nuclear engineer faces often is that you'll need to split a string (for example the name of a nuclide), to get the symbol and the mass number of the nuclide.

```python
myNuc='U238'

re.split('(\d+)', myNuc)
```

# Exercises

## 1

Construct a function $f(x,y)$ which becomes zero at the edges of a rectangle centered around the origin and with a side length of $a$. First create a 2D graphic representing the rectangle, and annotate one vertical and one horizontal side by giving the equation of the side. Then, use `plot_surface` and `contour` to visualize the function.

Include the equation of the surface here with latex code:

And write your function and code for plotting below.

```python
#your code comes here
```

## 2

Consider the following decay chain $U-234 \rightarrow Th-230 \rightarrow Ra-226 \rightarrow Rn-222 \rightarrow$, which is part of the U-238 decay serie. The alpha decays have the following half-lifes: 245.5ky, 75.38ky, 1602y and 3.82d.  Use scipy, and matplotlib to plot the number of nuclides and the activity between 0 and 15 kyears if $N_{U-234}(t=0)=5000000$, and the daughters are not present at $t=0$.

- Inspect the half-lifes, what do you expect, what can cause numerical issues in this case?
- What is your expectation on how will the activity of Ra-226 and Rn-222 compare to each other?

```python
#your code comes here
```

## 3

The minimum energy required to disassemble the nucleus of an atom into its components is the binding energy. In this exercise you are going to compute the binding energy per nucleon for several nuclides in two ways:

1. based on the relative atomic mass of the nuclides
2. based on a semi-empirical formula.

If we substract the mass of the nucleus from the mass of the nucleons and express it in energy unit we get the binding energy:

$B(A,Z)=[Z\cdot m_p+(A-Z)\cdot m_n-M(A,Z)]c^2$

For a given nuclide one can estimate the binding energy with the semi-empirical Bethe–Weizsäcker formula, which has various forms in literature. Here you can use the following form:

$B(A,Z)=15.75A-94.8\frac{(A/2 - Z)^2}{A}-17.8A^{2/3}-0.71Z^2A^{-1/3}+34\delta A^{-3/4}$

where $\delta = 1$ for even-even nuclei, $\delta = -1$ for odd-odd nuclei and $\delta = 0$ otherwise.

(Note: that all terms in the formula have a physical meaning: volume term, assymmetry term, surface term, Coulomb term and pairing term. Make sure based on the text book that you understand these).

In the first formula one can express the neutron and proton mass in unified mass units (u). Also notice, that in most tabulated data (also here) you can only look up the relative atom mass of isotopes, and not the mass of the nucleus, thus the mass of the electrons needs to be taken into account as well. The reason for this is that for high Z isotopes it is difficult to remove all the electrons, so the neutral atoms are measured instead.

You are given the relative mass (in unified mass units) of several nuclides  (downloaded from https://physics.nist.gov/cgi-bin/Compositions/stand_alone.pl?ele=&ascii=html) as a python nested dictionary `nuclides`. Dictionaries are used to store data values in key:value pairs. It is basically an unorder array with "named" columns. In this case the outer keys are nuclide identifiers, and the values are also dictionaries, with the keys being Z, A and m.

Your task is to

1. implement two functions which calculate $\varepsilon=B(A,Z)/A$ with the two methods listed above.
2. apply the functions for the nuclides listed in `nuclides`
3. Plot the binding energy curves. Use solid line for the semi-empirical formula, and red dot markers `ro` for the results taking into account the relative mass
4. Find the nuclide with the highest binding energy per nucleon, and use `plt.annotate` to include the name of the nuclide in the figure above the corresponding marker.

```python
import numpy as np
import matplotlib.pyplot as plt
me=5.48579909070e-4 #u
mn=1.00866491588    #u
mp=1.007276466621   #u
muc2=931.49410242 #MeV

nuclides={'H2': {'Z': 1, 'A': 2, 'm': 2.01410177812},
        'H3': {'Z': 1, 'A': 3, 'm': 3.0160492779},
        'He3': {'Z': 2, 'A': 3, 'm': 3.0160293201},
        'He4': {'Z': 2, 'A': 4, 'm': 4.002603254},
        'Li6': {'Z': 3, 'A': 6, 'm': 6.0151228874},
        'O16': {'Z': 8, 'A': 16, 'm': 15.99491461957},
        'S34': {'Z': 16, 'A': 34, 'm': 33.96786701},
        'Fe56': {'Z': 26, 'A': 56, 'm': 55.9349375},
        'Ni62': {'Z': 28, 'A': 62, 'm': 61.92834537},
        'Kr84': {'Z': 36, 'A': 84, 'm': 83.9114977282},
        'Sn119': {'Z': 50, 'A': 119, 'm': 118.90331117},
        'Ti205': {'Z': 81, 'A': 205, 'm': 204.9744278},
        'U238': {'Z': 92, 'A': 238, 'm': 238.0507884}}

def BAZ(A,Z,m):
    """Function to calculate the binding energy per nucleon

    Parameters
    ----------
    A : int
        Mass number of nuclide
    Z : int
        Proton number of nuclide
    m : float
        Mass of the nuclide
    """
    eps=#YOUR CODE HERE
    return eps/A

def BAZ_BW(A,Z):
    """Function to calculate the binding energy per nucleon
    with the semi-empirical formula

    Parameters
    ----------
    A : int
        Mass number of nuclide
    Z : int
        Proton number of nuclide
    """
    #your code comes here
    return eps/A

#your code to apply these functions and to plot the results comes here.
```