```python
import math

import numpy as np
import scipy.optimize
import matplotlib.pyplot as plt
import scipy

import PeakTools

# %matplotlib notebook
%matplotlib inline
```

```python
PeakTools.harmonic_peak_height_ratio_klist_clist([1,2,3,4,5,6],.5, [.1,.1,.1,.1],[1,2,3,4])
```

    array([ 3.00918204,  2.25096114,  1.91768977,  1.72065197,  1.59839958,
            1.50625163])

```python
MAX_ORDER = 4
o_list = [[1], [2], [3],[4],[5],[6],[7], [1,2], [2,4], [1,3], [5,3,1]]
```

# Function to Fit
\begin{align}
    \frac{\tilde{E}_{NC}(\omega_{0,k+1})}{\tilde{E}_{NC}(\omega_{0,k})} = \frac{\epsilon(\omega_{0,k+1})}{\epsilon(\omega_{0,k})}	&= \left( \frac{\omega_{0,k+1}}{\omega_{0,k}}\right)^2 \frac{S }{k+1 } \frac{\left| 1 + c\sqrt{S} \left( 1  - \frac{k+1}{S} \right)\right|^2  }{\left| 1 + c \sqrt{S} \left( 1  - \frac{k}{S} \right) \right|^2 } \\
    &= \left( \frac{\omega_{0,k+1}}{\omega_{0,k}}\right)^2 \frac{S }{k+1 } \frac{1 + 2 |c|\cos \theta \sqrt{S} \left( 1  - \frac{k+1}{S} \right) + |c|^2\left( 1  - \frac{k+1}{S} \right)^2 }{1 + 2 |c|\cos \theta \sqrt{S} \left( 1  - \frac{k}{S} \right) + |c|^2\left( 1  - \frac{k}{S} \right)^2 }\\
0 &= \frac{\epsilon(\omega_{0,k+1})}{\epsilon(\omega_{0,k})} \left( \frac{\omega_{0,k}}{\omega_{0,k+1}}\right)^2 - \frac{S }{k+1 } \frac{1 + 2 |c|\cos \theta \sqrt{S} \left( 1  - \frac{k+1}{S} \right) + |c|^2\left( 1  - \frac{k+1}{S} \right)^2 }{1 + 2 |c|\cos \theta \sqrt{S} \left( 1  - \frac{k}{S} \right) + |c|^2\left( 1  - \frac{k}{S} \right)^2 }
\end{align}

# Citeable Tetracene in Toluene

```python
#From DataThief
ZERO_PAD = 100

tetracene_toluene_spectra  = np.loadtxt("spectra_csv/tetracene_citeable_toluene.csv", delimiter=",")

tetracene_toluene_wavelength_nm = tetracene_toluene_spectra[:,0]
tetracene_toluene_wavenumbers = 1 / (tetracene_toluene_wavelength_nm * 1e-7)
tetracene_toluene_abs = tetracene_toluene_spectra[:,1]
# tetracene_toluene_abs = -np.log(tetracene_toluene_abs)
tetracene_toluene_abs = tetracene_toluene_abs - np.min(tetracene_toluene_abs)
tetracene_toluene_abs = tetracene_toluene_abs/tetracene_toluene_wavenumbers**2
tetracene_toluene_abs = tetracene_toluene_abs /np.max(tetracene_toluene_abs)

tetracene_toluene_wavenumbers, tetracene_toluene_abs = PeakTools.left_zero_pad_spectra(tetracene_toluene_wavenumbers, tetracene_toluene_abs, ZERO_PAD)
```

```python

```

```python
tetracene_toluene_peak_energy_guesses = [21053.0, 22505.0, 24057.0, 25368.0 , 26658.3, 28168.][0:-1]
tetracene_toluene_peak_height_guesses = [.95, .76, .35, .14, .05, .014][0:-1]
width_guess_wavenumbers = 100.0
```

```python
tetracene_toluene_fit = PeakTools.SpectraFit(spectral_energies=tetracene_toluene_wavenumbers,
                           spectral_amplitudes=tetracene_toluene_abs,
                           approx_peak_energies = tetracene_toluene_peak_energy_guesses,
                           approx_peak_heights = tetracene_toluene_peak_height_guesses,
                           width_guess = width_guess_wavenumbers)
```

```python

```

```python
tetracene_toluene_fit.fit_and_observe_spectra()
```

```python
tetracene_toluene_fit.plot_spectral_fit(["voigt"])
# plt.title("Tetracene in Toluene")
plt.xlabel(r"$\omega$ / wavenumbers")
plt.ylabel(r"Normalized $K$")
plt.legend(loc=0)
plt.savefig("toluene_spectra_fit.png", dpi=900)
```

```python
tetracene_toluene_fit.explore_non_condon_orders(max_order=None, order_lists_to_calculate = o_list, method_str="voigt")
```

    [1] order non-condon perturbation calculation

    ---------VOIGT---------

    PeakTools.py:205: UserWarning: HO_overlap fed negative Huang Rhys factor!  performing absolute value operation
      warnings.warn("HO_overlap fed negative Huang Rhys factor!  performing absolute value operation")
    PeakTools.py:265: RuntimeWarning: invalid value encountered in divide
      return numerator / denominator

    Through running 50 lstsq attempts, 2 better models were found

     relative parameter errors for order[1]: [-4773478.5999605069]

    check for big off-diagonal values:
    [[  4.52582653e-04   1.00093845e+13]
     [  1.00093845e+13   3.27657599e+30]]

    HO/c model fit Found for voigt!  S=0.691133414304, c=[ -3.79205949e+08]
    Naively S=0.886141547618 without a non-condon correction
    peak height goddness of fit:
    r^2_condon = nan 	 r^2_model = 0.729396052821 	 improvement_factor = nan

    BEST METHOD WAS: voigt
    [2] order non-condon perturbation calculation

    ---------VOIGT---------

    Through running 50 lstsq attempts, 10 better models were found

     relative parameter errors for order[2]: [-0.061495124914203336]

    check for big off-diagonal values:
    [[  3.65793899e-02   6.45387596e-04]
     [  6.45387596e-04   1.36539220e-05]]

    HO/c model fit Found for voigt!  S=1.65721203597, c=[-0.06008804]
    Naively S=0.886141547618 without a non-condon correction
    peak height goddness of fit:
    r^2_condon = nan 	 r^2_model = 0.958162343631 	 improvement_factor = nan

    BEST METHOD WAS: voigt
    [1, 2] order non-condon perturbation calculation

    ---------VOIGT---------

    Through running 50 lstsq attempts, 6 better models were found

     relative parameter errors for order[1, 2]: [0.57708376700689012]

    check for big off-diagonal values:
    [[ 0.0307555   0.06210663 -0.00446449]
     [ 0.06210663  0.32754955 -0.0127071 ]
     [-0.00446449 -0.0127071   0.00301585]]

    HO/c model fit Found for voigt!  S=1.48917585577, c=[-1.47454894  0.0951626 ]
    Naively S=0.886141547618 without a non-condon correction
    peak height goddness of fit:
    r^2_condon = nan 	 r^2_model = 0.791769972957 	 improvement_factor = nan

    BEST METHOD WAS: voigt
    [2, 4] order non-condon perturbation calculation

    ---------VOIGT---------

    Through running 50 lstsq attempts, 2 better models were found

     relative parameter errors for order[2, 4]: [9.6068105506835355]

    check for big off-diagonal values:
    [[  8.45348483e-02   1.95498229e+01  -4.12161493e-01]
     [  1.95498229e+01   9.75673659e+03  -2.04279649e+02]
     [ -4.12161493e-01  -2.04279649e+02   4.27732270e+00]]

    HO/c model fit Found for voigt!  S=1.42787962841, c=[-10.34675596   0.21528154]
    Naively S=0.886141547618 without a non-condon correction
    peak height goddness of fit:
    r^2_condon = nan 	 r^2_model = 0.782918848916 	 improvement_factor = nan

    BEST METHOD WAS: voigt
    [1, 3] order non-condon perturbation calculation

    ---------VOIGT---------

    Through running 50 lstsq attempts, 3 better models were found

     relative parameter errors for order[1, 3]: [0.54161072439895974]

    check for big off-diagonal values:
    [[  8.50885637e-03   1.63044192e-01  -4.28615859e-03]
     [  1.63044192e-01   3.57656374e+00  -9.30212554e-02]
     [ -4.28615859e-03  -9.30212554e-02   2.42306891e-03]]

    HO/c model fit Found for voigt!  S=0.737948570315, c=[-3.83083747  0.09088571]
    Naively S=0.886141547618 without a non-condon correction
    peak height goddness of fit:
    r^2_condon = nan 	 r^2_model = 0.989470735081 	 improvement_factor = nan

    BEST METHOD WAS: voigt
    [5, 3, 1] order non-condon perturbation calculation

    ---------VOIGT---------

    Through running 50 lstsq attempts, 6 better models were found

     relative parameter errors for order[5, 3, 1]: [-0.0034268092625728457]

    check for big off-diagonal values:
    [[  9.59753560e-06   2.76723879e-08  -1.25271778e-06   1.06851637e-05]
     [  2.76723879e-08   9.26044731e-11  -4.29019101e-09   3.85477942e-08]
     [ -1.25271778e-06  -4.29019101e-09   2.01355695e-07  -1.86220667e-06]
     [  1.06851637e-05   3.85477942e-08  -1.86220667e-06   1.84894302e-05]]

    HO/c model fit Found for voigt!  S=0.713994164939, c=[-0.00136174  0.08132911 -1.25479226]
    Naively S=0.886141547618 without a non-condon correction
    peak height goddness of fit:
    r^2_condon = nan 	 r^2_model = 0.999999739669 	 improvement_factor = nan

    BEST METHOD WAS: voigt
    [5, 3, 1] order non-condon perturbation fit the spectra best!
    cost report:
    (0.00011048813702996451, [5, 3, 1])
    (0.0041538939587352367, [1, 3])
    (0.072869123099601293, [2, 4])
    (0.086939867392911999, [1, 2])
    (0.092142633196177798, [1])
    (0.15880202196668891, [2])

```python
tetracene_toluene_fit.fit_and_observe_non_condon_models(order=[2], plot_spectra=False, my_methods_list=["voigt"])
plt.savefig("toluene_ratio_fit_2.png", dpi=900)
```

```python
tetracene_toluene_fit.fit_and_observe_non_condon_models(order =[1,3], plot_spectra=False, my_methods_list=["voigt"])
plt.savefig("toluene_ratio_fit_13.png", dpi=900)
```

```python
tetracene_toluene_fit.fit_and_observe_non_condon_models(order =[2,4], plot_spectra=False, my_methods_list=["voigt"])
plt.savefig("toluene_ratio_fit_24.png", dpi=900)
```

```python
tetracene_toluene_fit.fit_and_observe_non_condon_models(order =[5,3,1], plot_spectra=False, my_methods_list=["voigt"])
plt.savefig("toluene_ratio_fit_531.png", dpi=900)
```

```python

```

# Tetracene in DCE
## Orange in the image below

```python
#From DataThief
tetracene_dce_spectra  = np.loadtxt("spectra_csv/tetracene_citeable_dichloroethene.csv", delimiter=",")

tetracene_dce_wavelength_nm = tetracene_dce_spectra[:,0]
tetracene_dce_wavenumbers = 1 / (tetracene_dce_wavelength_nm * 1e-7)
tetracene_dce_abs = tetracene_dce_spectra[:,1]
tetracene_dce_abs = tetracene_dce_abs - np.min(tetracene_dce_abs)
tetracene_dce_abs = tetracene_dce_abs/tetracene_dce_wavenumbers**2
tetracene_dce_abs = tetracene_dce_abs / np.max(tetracene_dce_abs)
tetracene_dce_abs = np.flipud(tetracene_dce_abs)
tetracene_dce_wavenumbers= np.flipud(tetracene_dce_wavenumbers)
```

```python
ZERO_PAD = 100

tetracene_dce_wavenumbers, tetracene_dce_abs = PeakTools.left_zero_pad_spectra(tetracene_dce_wavenumbers, tetracene_dce_abs, ZERO_PAD)
```

```python

```

```python
tetracene_dcm_peak_energy_guesses = [21053.0, 22505.0, 24057.0, 25368.0 , 26658.3, 28114.]
tetracene_dcm_peak_height_guesses = [.97, .82, .37, .15, .05, .01]
#maybe rpeplace with the toluene numbers.  they worked well enough
width_guess_wavenumbers = 100.0

```

```python
tetracene_dce_fit = PeakTools.SpectraFit(spectral_energies=tetracene_dce_wavenumbers,
                           spectral_amplitudes=tetracene_dce_abs,
                           approx_peak_energies = tetracene_dcm_peak_energy_guesses,
                           approx_peak_heights = tetracene_dcm_peak_height_guesses,
                           width_guess = width_guess_wavenumbers)
```

```python
tetracene_dce_fit.fit_and_observe_spectra()
```

```python
tetracene_dce_fit.plot_spectral_fit(["voigt"])
# plt.title("Tetracene in DCM")
plt.xlabel(r"$\omega$ / wavenumbers")
plt.ylabel(r"Normalized $K$")
plt.legend(loc=0)
plt.savefig("dcm_spectra_fit.png", dpi=900)
```

```python

```

```python
tetracene_dce_fit.explore_non_condon_orders(max_order = None, order_lists_to_calculate=o_list, method_str="voigt")
```

    [1] order non-condon perturbation calculation

    ---------VOIGT---------

    Through running 50 lstsq attempts, 10 better models were found

     relative parameter errors for order[1]: [-48166111.525665924]

    check for big off-diagonal values:
    [[  2.15171289e-02   2.13885885e+14]
     [  2.13885885e+14   3.04419119e+30]]

    HO/c model fit Found for voigt!  S=1.35096562295, c=[-36223830.72781194]
    Naively S=1.7534906858 without a non-condon correction
    peak height goddness of fit:
    r^2_condon = nan 	 r^2_model = -0.220423397127 	 improvement_factor = nan

    BEST METHOD WAS: voigt
    [2] order non-condon perturbation calculation

    ---------VOIGT---------

    Through running 50 lstsq attempts, 6 better models were found

     relative parameter errors for order[2]: [-0.019800458140437457]

    check for big off-diagonal values:
    [[  2.52166012e-03   2.01810589e-05]
     [  2.01810589e-05   1.26964804e-06]]

    HO/c model fit Found for voigt!  S=2.00647986552, c=[-0.0569071]
    Naively S=1.7534906858 without a non-condon correction
    peak height goddness of fit:
    r^2_condon = nan 	 r^2_model = 0.936745509298 	 improvement_factor = nan

    BEST METHOD WAS: voigt
    [1, 2] order non-condon perturbation calculation

    ---------VOIGT---------

    Through running 50 lstsq attempts, 5 better models were found

     relative parameter errors for order[1, 2]: [-11812748.070001459]

    check for big off-diagonal values:
    [[  1.28080006e-02  -7.80777768e+13  -2.89174384e+13]
     [ -7.80777768e+13   3.85476116e+30   1.42767655e+30]
     [ -2.89174384e+13   1.42767655e+30   5.28764366e+29]]

    HO/c model fit Found for voigt!  S=1.44297161968, c=[ -1.66206451e+08  -6.15573830e+07]
    Naively S=1.7534906858 without a non-condon correction
    peak height goddness of fit:
    r^2_condon = nan 	 r^2_model = 0.314900616826 	 improvement_factor = nan

    BEST METHOD WAS: voigt
    [2, 4] order non-condon perturbation calculation

    ---------VOIGT---------

    Through running 50 lstsq attempts, 8 better models were found

     relative parameter errors for order[2, 4]: [-86974.983028123432]

    check for big off-diagonal values:
    [[  7.87205223e-02   1.22129189e+11  -2.67225728e+09]
     [  1.22129190e+11   2.02431607e+24  -4.42933199e+22]
     [ -2.67225729e+09  -4.42933199e+22   9.69165941e+20]]

    HO/c model fit Found for voigt!  S=1.16912024154, c=[ 16358532.61468262   -357935.45498245]
    Naively S=1.7534906858 without a non-condon correction
    peak height goddness of fit:
    r^2_condon = nan 	 r^2_model = 0.785960156726 	 improvement_factor = nan

    BEST METHOD WAS: voigt
    [1, 3] order non-condon perturbation calculation

    ---------VOIGT---------

    Through running 50 lstsq attempts, 5 better models were found

     relative parameter errors for order[1, 3]: [-140046.92102288874]

    check for big off-diagonal values:
    [[  5.69017866e-02   1.80459028e+10  -4.74286689e+08]
     [  1.80459028e+10   4.87471093e+22  -1.28118509e+21]
     [ -4.74286689e+08  -1.28118509e+21   3.36724629e+19]]

    HO/c model fit Found for voigt!  S=0.641648088086, c=[ 1576523.16784459   -41434.66924874]
    Naively S=1.7534906858 without a non-condon correction
    peak height goddness of fit:
    r^2_condon = nan 	 r^2_model = 0.820639502511 	 improvement_factor = nan

    BEST METHOD WAS: voigt
    [5, 3, 1] order non-condon perturbation calculation

    ---------VOIGT---------

    Through running 50 lstsq attempts, 2 better models were found

     relative parameter errors for order[5, 3, 1]: [-0.10351509467424477]

    check for big off-diagonal values:
    [[  2.68335465e-02   5.98363086e-06  -7.26350866e-04   1.37337284e-02]
     [  5.98363086e-06   8.34710757e-09  -4.88452282e-07   2.20152261e-06]
     [ -7.26350866e-04  -4.88452282e-07   3.51210065e-05  -3.43918334e-04]
     [  1.37337284e-02   2.20152261e-06  -3.43918334e-04   7.73256289e-03]]

    HO/c model fit Found for voigt!  S=1.39282473537, c=[  2.02383562e-04   4.74199410e-03  -8.49489562e-01]
    Naively S=1.7534906858 without a non-condon correction
    peak height goddness of fit:
    r^2_condon = nan 	 r^2_model = 0.973139338649 	 improvement_factor = nan

    BEST METHOD WAS: voigt
    [5, 3, 1] order non-condon perturbation fit the spectra best!
    cost report:
    (0.012699573939259655, [5, 3, 1])
    (0.20183316236973925, [2, 4])
    (0.21361231794741312, [2])
    (0.28071831732569696, [1, 2])
    (0.40965355769217016, [1, 3])
    (1.1943486680223585, [1])

```python
tetracene_toluene_fit.fit_and_observe_non_condon_models([2], plot_spectra=False, my_methods_list=["voigt"])
plt.savefig("toluene_ratio_fit_2.png", dpi=900)
```

```python
tetracene_dce_fit.fit_and_observe_non_condon_models([2], plot_spectra=False, my_methods_list=["voigt"])
plt.savefig("dcm_ratio_fit_2.png", dpi=900)
```

```python

```

```python
order = [2,4]
tfs=True
method = "voigt"
tetracene_dce_fit.fit_and_observe_non_condon_models(order=order, plot_spectra=False, my_methods_list=[method])
plt.savefig("dcm_ratio_fit_24.png")
tetracene_toluene_fit.fit_and_observe_non_condon_models(order=order, plot_spectra=False, my_methods_list=[method])
plt.savefig("toluene_ratio_fit_24.png", dpi=900)

x = np.linspace(-3,3)
tfs=False
plt.figure()
tetracene_dce_fit.plot_transition_dipole(x, c_order = order, method_str=method, label_prefix = "DCM")
tetracene_toluene_fit.plot_transition_dipole(x, c_order = order, method_str=method,  label_prefix="Toluene")
plt.legend(loc=0)
plt.title(r"Transition Dipole From $H_{%s}$"% order)
plt.savefig("dipole_comparison_24.png", dpi=900)

```

```python
order = [1,3]
method = "voigt"
tfs=True
tetracene_dce_fit.fit_and_observe_non_condon_models(order=order, plot_spectra=False, my_methods_list=[method])
plt.savefig("dcm_ratio_fit_13.png")
tetracene_toluene_fit.fit_and_observe_non_condon_models(order=order, plot_spectra=False, my_methods_list=[method])
plt.savefig("toluene_ratio_fit_13.png", dpi=900)

```

```python
x = np.linspace(-3,3)
tfs=False
plt.figure()
tetracene_dce_fit.plot_transition_dipole(x, c_order = order, method_str=method, label_prefix = "DCM")
tetracene_toluene_fit.plot_transition_dipole(x, c_order = order, method_str=method, label_prefix="Toluene")
plt.legend(loc=0)
plt.title(r"Transition Dipole From $H_{%s}$"% order)
plt.savefig("dipole_comparison_13.png", dpi=900)
```

```python

```

```python
PeakTools.table_generator([1,3], tetracene_dce_fit, "DCM", tetracene_toluene_fit, "Toluene")
```

      & DCM  &  Toluene \\
    $S_{ (1, 3) }$ & 0.642  &  0.738 \\
    $c'_{ 1 }$ & 1576523.168  &  -3.831 \\
    $c'_{ 3 }$ & -41434.669  &  0.091 \\
    $r^2_{ (1, 3) }$ & -6.092  &  -7.094 \\

```python

```

```python
order = [5,3,1]
tfs=True
method = "voigt"
tetracene_dce_fit.fit_and_observe_non_condon_models(order=order, plot_spectra=False, my_methods_list=[method])
plt.savefig("dcm_ratio_fit_{}.png".format(*order))
tetracene_toluene_fit.fit_and_observe_non_condon_models(order=order, plot_spectra=False, my_methods_list=[method])
plt.savefig("toluene_ratio_fit_{}.png".format(*order), dpi=900)

x = np.linspace(-3,3)
tfs=False
plt.figure()
tetracene_dce_fit.plot_transition_dipole(x, c_order = order, method_str=method, label_prefix = "DCM")
tetracene_toluene_fit.plot_transition_dipole(x, c_order = order, method_str=method, label_prefix="Toluene")
plt.legend(loc=0)
plt.title(r"Transition Dipole From $H_{%s}$"% order)
plt.savefig("dipole_comparison_{}.png".format(*order), dpi=900)

```

```python
PeakTools.table_generator(order, tetracene_dce_fit, "DCM", tetracene_toluene_fit, "Toluene")
```

      & DCM  &  Toluene \\
    $S_{ (5, 3, 1) }$ & 1.393  &  0.714 \\
    $c'_{ 5 }$ & 0.000  &  -0.001 \\
    $c'_{ 3 }$ & 0.005  &  0.081 \\
    $c'_{ 1 }$ & -0.849  &  -1.255 \\
    $r^2_{ (5, 3, 1) }$ & -8.815  &  -6.996 \\

```python

```

```python
a = np.array([[i * k for i in range(4)] for k in range(9)])
```

```python
a.shape
```

    (9, 4)

```python

```

```python

```

```python

```


# Generating C Code to implement Method of Lines Timestepping for Explicit Runge Kutta Methods

## Authors: Zach Etienne & Brandon Clark

## This tutorial module generates three blocks of C Code in order to perform Method of Lines timestepping.

**Module Status:**  Validated

**Validation Notes:** This tutorial module has been confirmed to be self-consistent with its corresponding NRPy+ module, as documented [below](#code_validation). All Runge-Kutta Butcher tables were validated using truncated Taylor series in [a separate module](Tutorial-RK_Butcher_Table_Validation.ipynb). Finally, C-code implementation of RK4 was validated against a trusted version. C-code implementations of other RK methods seem to work as expected in the context of solving the scalar wave equation in Cartesian coordinates.

### NRPy+ Source Code for this module:
* [MoLtimestepping/C_Code_Generation.py](../edit/MoLtimestepping/C_Code_Generation.py)
* [MoLtimestepping/RK_Butcher_Table_Dictionary.py](../edit/MoLtimestepping/RK_Butcher_Table_Dictionary.py) ([**Tutorial**](Tutorial-RK_Butcher_Table_Dictionary.ipynb)) Stores the Butcher tables for the explicit Runge Kutta methods

## Introduction:

When numerically solving a partial differential equation initial-value problem, subject to suitable boundary conditions,  we implement Method of Lines to "integrate" the solution forward in time.

### The Method of Lines:

Once we have the initial data for a PDE, we "evolve it forward in time", using the [Method of Lines](https://reference.wolfram.com/language/tutorial/NDSolveMethodOfLines.html). In short, the Method of Lines enables us to handle
1. the **spatial derivatives** of an initial value problem PDE using **standard finite difference approaches**, and
2. the **temporal derivatives** of an initial value problem PDE using **standard strategies for solving ordinary differential equations (ODEs), like Runge Kutta methods** so long as the initial value problem PDE can be written in the first-order-in-time form
$$\partial_t \vec{f} = \mathbf{M}\ \vec{f},$$
where $\mathbf{M}$ is an $N\times N$ matrix containing only *spatial* differential operators that act on the $N$-element column vector $\vec{f}$. $\mathbf{M}$ may not contain $t$ or time derivatives explicitly; only *spatial* partial derivatives are allowed to appear inside $\mathbf{M}$.

You may find the next module [Tutorial-ScalarWave](Tutorial-ScalarWave.ipynb) extremely helpful as an example for implementing the Method of Lines for solving the Scalar Wave equation in Cartesian coordinates.

### Generating the C code:
This module describes how three C code blocks are written to implement Method of Lines timestepping for a specified RK method. The first block is dedicated to allocating memory for the appropriate number of grid function lists needed for the given RK method. The second block will implement the Runge Kutta numerical scheme based on the corresponding Butcher table. The third block will free up the previously allocated memory after the Method of Lines run is complete. These blocks of code are stored within the following three header files respectively

1. `MoLtimestepping/RK_Allocate_Memory.h`
1. `MoLtimestepping/RK_MoL.h`
1. `MoLtimestepping/RK_Free_Memory.h`

The generated code is then included in future Start-to-Finish example tutorial modules when solving PDEs numerically.

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

This module is organized as follows

1. [Step 1](#initializenrpy): Initialize needed Python/NRPy+ modules
1. [Step 2](#diagonal): Checking if Butcher Table is Diagonal
1. [Step 3](#ccode): Generating the C Code
    1. [Step 3.a](#allocate): Allocating Memory, `MoLtimestepping/RK_Allocate_Memory.h`
    1. [Step 3.b](#rkmol): Implementing the Runge Kutta Scheme for Method of Lines Timestepping,  `MoLtimestepping/RK_MoL.h`
    1. [Step 3.c](#free): Freeing Allocated Memory, `MoLtimestepping/RK_Free_Memory.h`
1. [Step 4](#code_validation): Code Validation against `MoLtimestepping.RK_Butcher_Table_Generating_C_Code` NRPy+ module
1. [Step 5](#latex_pdf_output): Output this module to $\LaTeX$-formatted PDF

# Step 1: Initialize needed Python/NRPy+ modules [Back to [top](#toc)\]
$$\label{initializenrpy}$$

Let's start by importing all the needed modules from Python/NRPy+:

```python
import sympy as sp
import NRPy_param_funcs as par
from MoLtimestepping.RK_Butcher_Table_Dictionary import Butcher_dict
```

# Step 2: Checking if a Butcher table is Diagonal [Back to [top](#toc)\]
$$\label{diagonal}$$

A diagonal Butcher table takes the form

$$\begin{array}{c|cccccc}
    0 & \\
    a_1 & a_1 & \\
    a_2 & 0 & a_2 & \\
    a_3 & 0 & 0 & a_3 & \\
    \vdots & \vdots & \ddots & \ddots & \ddots \\
    a_s & 0 & 0 & 0 & \cdots & a_s \\ \hline
     & b_1 & b_2 & b_3 & \cdots & b_{s-1} & b_s
\end{array}$$

where $s$ is the number of required predictor-corrector steps for a given RK method (see [Butcher, John C. (2008)](https://onlinelibrary.wiley.com/doi/book/10.1002/9780470753767)). One known diagonal RK method is the classic RK4 represented in Butcher table form as:

$$\begin{array}{c|cccc}
    0 & \\
    1/2 & 1/2 & \\
    1/2 & 0 & 1/2 & \\
    1 & 0 & 0 & 1 & \\ \hline
     & 1/6 & 1/3 & 1/3 & 1/6
\end{array} $$

Diagonal Butcher tables are nice when it comes to saving required memory space. Each new step for a diagonal RK method, when computing the new $k_i$, does not depend on the previous calculation, and so there are ways to save memory. Signifcantly so in large three-dimensional spatial grid spaces.

```python
def diagonal(key):
    diagonal = True #  Start with the Butcher table is diagonal
    Butcher = Butcher_dict[key][0]
    L = len(Butcher)-1 # Establish the number of rows to check for diagonal trait, all bust last row
    row_idx = 0 # Initialize the Butcher table row index
    for i in range(L): # Check all the desired rows
        for j in range(1,row_idx): # Check each element before the diagonal element in a row
            if Butcher[i][j] != sp.sympify(0): # If any element is non-zero, then the table is not diagonal
                diagonal = False
                break
        row_idx += 1 # Update to check the next row
    return diagonal

# State whether each Butcher table is diagonal or not
for key, value in Butcher_dict.items():
    if diagonal(key) == True:
        print("The RK method "+str(key)+" is diagonal!")
    else:
        print("The RK method "+str(key)+" is NOT diagonal!")
```

    The RK method Euler is diagonal!
    The RK method RK2 Heun is diagonal!
    The RK method RK2 MP is diagonal!
    The RK method RK2 Ralston is diagonal!
    The RK method RK3 is NOT diagonal!
    The RK method RK3 Heun is diagonal!
    The RK method RK3 Ralston is diagonal!
    The RK method SSPRK3 is NOT diagonal!
    The RK method RK4 is diagonal!
    The RK method DP5 is NOT diagonal!
    The RK method DP5alt is NOT diagonal!
    The RK method CK5 is NOT diagonal!
    The RK method DP6 is NOT diagonal!
    The RK method L6 is NOT diagonal!
    The RK method DP8 is NOT diagonal!

# Step 3: Generating the C Code [Back to [top](#toc)\]
$$\label{ccode}$$

The following sections build up the C code for implementing the Method of Lines timestepping algorithm for solving PDEs. To see what the C code looks like for a particular method, simply change the `RK_method` below, otherwise it will default to `"RK4"`.

## Step 3.a: Allocating Memory, `MoLtimestepping/RK_Allocate_Memory.h`  [Back to [top](#toc)\]
$$\label{allocate}$$

We define the function `RK_Allocate()` which generates the C code for allocating the memory for the appropriate number of grid function lists given a Runge Kutta method. The function writes the C code to the header file `MoLtimestepping/RK_Allocate_Memory.h`.

```python
# Choose a method to see the C code print out for
RK_method = "RK3 Ralston"
```

```python
def RK_Allocate(RK_method="RK4"):
    with open("MoLtimestepping/RK_Allocate_Memory"+str(RK_method).replace(" ", "_")+".h", "w") as file:
        file.write("// Code snippet allocating gridfunction memory for \""+str(RK_method)+"\" method:\n")
        # No matter the method we define gridfunctions "y_n_gfs" to store the initial data
        file.write("REAL *restrict y_n_gfs = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot);\n")
        if diagonal(RK_method) == True and "RK3" in RK_method:
            file.write("""REAL *restrict k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot);
REAL *restrict k2_or_y_nplus_a32_k2_gfs = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot);
REAL *restrict diagnostic_output_gfs = k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs;""")
        else:
            if diagonal(RK_method) == False: #  Allocate memory for non-diagonal Butcher tables
                # Determine the number of k_i steps based on length of Butcher Table
                num_k = len(Butcher_dict[RK_method][0])-1
                # For non-diagonal tables an intermediate gridfunction "next_y_input" is needed for rhs evaluations
                file.write("REAL *restrict next_y_input_gfs = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot);\n")
                for i in range(num_k): # Need to allocate all k_i steps for a given method
                    file.write("REAL *restrict k"+str(i+1)+"_gfs = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot);\n")
                file.write("REAL *restrict diagnostic_output_gfs = k1_gfs;\n")
            else: # Allocate memory for diagonal Butcher tables, which use a "y_nplus1_running_total gridfunction"
                file.write("REAL *restrict y_nplus1_running_total_gfs = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot);\n")
                if RK_method != 'Euler': # Allocate memory for diagonal Butcher tables that aren't Euler
                    # Need k_odd for k_1,3,5... and k_even for k_2,4,6...
                    file.write("REAL *restrict k_odd_gfs = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot);\n")
                    file.write("REAL *restrict k_even_gfs = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot);\n")
                file.write("REAL *restrict diagnostic_output_gfs = y_nplus1_running_total_gfs;\n")

RK_Allocate(RK_method)
print("This is the memory allocation C code for the "+str(RK_method)+" method: \n")
with open("MoLtimestepping/RK_Allocate_Memory"+str(RK_method).replace(" ", "_")+".h", "r") as file:
    print(file.read())
```

    This is the memory allocation C code for the RK3 Ralston method:

    // Code snippet allocating gridfunction memory for "RK3 Ralston" method:
    REAL *restrict y_n_gfs = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot);
    REAL *restrict k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot);
    REAL *restrict k2_or_y_nplus_a32_k2_gfs = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot);
    REAL *restrict diagnostic_output_gfs = k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs;

## Step 3.b: Implementing the Runge Kutta Scheme for Method of Lines Timestepping,  `MoLtimestepping/RK_MoL.h` [Back to [top](#toc)\]
$$\label{rkmol}$$

We define the function `RK_MoL()` which generates the C code for implementing Method of Lines using a specified Runge Kutta scheme. The function writes the C code to the header file `MoLtimestepping/RK_MoL.h`.

```python
def RK_MoL(RK_method,RHS_string, post_RHS_string):
    Butcher = Butcher_dict[RK_method][0] # Get the desired Butcher table from the dictionary
    num_steps = len(Butcher)-1 # Specify the number of required steps to update solution
    indent = "  "
    with open("MoLtimestepping/RK_MoL"+str(RK_method).replace(" ", "_")+".h", "w") as file:
        file.write("// Code snippet implementing "+RK_method+" algorithm for Method of Lines timestepping\n")
        # Diagonal RK3 only!!!
        if diagonal(RK_method) == True and "RK3" in RK_method:
            #  In a diagonal RK3 method, only 3 gridfunctions need be defined. Below implements this approach.
            file.write("""
// In a diagonal RK3 method like this one, only 3 gridfunctions need be defined. Below implements this approach.
// Using y_n_gfs as input, compute k1 and apply boundary conditions
"""+RHS_string.replace("RK_INPUT_GFS" ,"y_n_gfs").
               replace("RK_OUTPUT_GFS","k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs")+"""
LOOP_ALL_GFS_GPS(i) {
    // Store k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs now as
    //  the update for the next rhs evaluation y_n + a21*k1*dt:
    k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs[i] = ("""+sp.ccode(Butcher[1][1]).replace("L","")+""")*k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs[i]*dt + y_n_gfs[i];
}
// Apply boundary conditions to y_n + a21*k1*dt:
"""+post_RHS_string.replace("RK_OUTPUT_GFS","k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs")+"""

// Compute k2 using yn + a21*k1*dt
"""+RHS_string.replace("RK_INPUT_GFS" ,"k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs").
               replace("RK_OUTPUT_GFS","k2_or_y_nplus_a32_k2_gfs")+"""
LOOP_ALL_GFS_GPS(i) {
    // Reassign k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs to be
    //    the running total y_{n+1}
    k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs[i] = ("""+sp.ccode(Butcher[3][1]).replace("L","")+""")*(k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs[i] - y_n_gfs[i])/("""+sp.ccode(Butcher[1][1]).replace("L","")+""") + y_n_gfs[i];

    // Add a32*k2*dt to the running total
    k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs[i]+= ("""+sp.ccode(Butcher[3][2]).replace("L","")+""")*k2_or_y_nplus_a32_k2_gfs[i]*dt;

    // Store k2_or_y_nplus_a32_k2_gfs now as y_n + a32*k2*dt
    k2_or_y_nplus_a32_k2_gfs[i] = ("""+sp.ccode(Butcher[2][2]).replace("L","")+""")*k2_or_y_nplus_a32_k2_gfs[i]*dt + y_n_gfs[i];
}
// Apply boundary conditions to both y_n + a32*k2 (stored in k2_or_y_nplus_a32_k2_gfs)
//    ... and the y_{n+1} running total, as they have not been applied yet to k2-related gridfunctions:
"""+post_RHS_string.replace("RK_OUTPUT_GFS","k2_or_y_nplus_a32_k2_gfs")+"""
"""+post_RHS_string.replace("RK_OUTPUT_GFS","k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs")+"""

// Compute k3
"""+RHS_string.replace("RK_INPUT_GFS" ,"k2_or_y_nplus_a32_k2_gfs").
               replace("RK_OUTPUT_GFS","y_n_gfs")+"""
LOOP_ALL_GFS_GPS(i) {
    // Add k3 to the running total and save to y_n
    y_n_gfs[i] = k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs[i] + ("""+sp.ccode(Butcher[3][3]).replace("L","")+""")*y_n_gfs[i]*dt;
}
// Apply boundary conditions to the running total
"""+post_RHS_string.replace("RK_OUTPUT_GFS","y_n_gfs")+"\n")
        else:
            y_n           = "y_n_gfs"
            if diagonal(RK_method) == False:
                for s in range(num_steps):
                    next_y_input  = "next_y_input_gfs"

                    # If we're on the first step (s=0), we use y_n gridfunction as input.
                    #      Otherwise next_y_input is input. Output is just the reverse.
                    if s==0: # If on first step:
                        file.write(RHS_string.replace("RK_INPUT_GFS",y_n).replace("RK_OUTPUT_GFS","k"+str(s+1)+"_gfs")+"\n")
                    else:    # If on second step or later:
                        file.write(RHS_string.replace("RK_INPUT_GFS",next_y_input).replace("RK_OUTPUT_GFS","k"+str(s+1)+"_gfs")+"\n")
                    file.write("LOOP_ALL_GFS_GPS(i) {\n")
                    RK_update_string = ""
                    if s == num_steps-1: # If on final step:
                        RK_update_string += indent + y_n+"[i] += dt*("
                    else:                # If on anything but the final step:
                        RK_update_string += indent + next_y_input+"[i] = "+y_n+"[i] + dt*("
                    for m in range(s+1):
                        if Butcher[s+1][m+1] != 0:
                            if Butcher[s+1][m+1] != 1:
                                RK_update_string += " + k"+str(m+1)+"_gfs[i]*("+sp.ccode(Butcher[s+1][m+1]).replace("L","")+")"
                            else:
                                RK_update_string += " + k"+str(m+1)+"_gfs[i]"
                    RK_update_string += " );\n}\n"
                    file.write(RK_update_string)
                    if s == num_steps-1: # If on final step:
                        file.write(post_RHS_string.replace("RK_OUTPUT_GFS",y_n)+"\n")
                    else:                # If on anything but the final step:
                        file.write(post_RHS_string.replace("RK_OUTPUT_GFS",next_y_input)+"\n")
            else:
                y_nplus1_running_total = "y_nplus1_running_total_gfs"
                if RK_method == 'Euler': # Euler's method doesn't require any k_i, and gets its own unique algorithm
                    file.write(RHS_string.replace("RK_INPUT_GFS",y_n).replace("RK_OUTPUT_GFS",y_nplus1_running_total)+"\n")
                    file.write("LOOP_ALL_GFS_GPS(i) {\n")
                    file.write(indent + y_n+"[i] +=  "+y_nplus1_running_total+"[i]*dt;\n")
                    file.write("}\n")
                    file.write(post_RHS_string.replace("RK_OUTPUT_GFS",y_n)+"\n")
                else:
                    for s in range(num_steps):
                        # If we're on the first step (s=0), we use y_n gridfunction as input.
                        # and k_odd as output.
                        if s == 0:
                            rhs_input  = "y_n_gfs"
                            rhs_output = "k_odd_gfs"
                        # For the remaining steps the inputs and ouputs alternate between k_odd and k_even
                        elif s%2 == 0:
                            rhs_input = "k_even_gfs"
                            rhs_output = "k_odd_gfs"
                        else:
                            rhs_input = "k_odd_gfs"
                            rhs_output = "k_even_gfs"
                        file.write(RHS_string.replace("RK_INPUT_GFS",rhs_input).replace("RK_OUTPUT_GFS",rhs_output)+"\n")
                        file.write("LOOP_ALL_GFS_GPS(i) {\n")
                        if s == num_steps-1: # If on the final step
                            if Butcher[num_steps][s+1] !=0:
                                if Butcher[num_steps][s+1] !=1:
                                    file.write(indent+y_n+"[i] += "+y_nplus1_running_total+"[i] + "+rhs_output+"[i]*dt*("+sp.ccode(Butcher[num_steps][s+1]).replace("L","")+");\n")
                                else:
                                    file.write(indent+y_n+"[i] += "+y_nplus1_running_total+"[i] + "+rhs_output+"[i]*dt;\n")
                            file.write("}\n")
                            file.write(post_RHS_string.replace("RK_OUTPUT_GFS",y_n)+"\n")
                        else: # For anything besides the final step
                            if s == 0:
                                file.write(indent+y_nplus1_running_total+"[i] = "+rhs_output+"[i]*dt*("+sp.ccode(Butcher[num_steps][s+1]).replace("L","")+");\n")
                                file.write(indent+rhs_output+"[i] = "+y_n+"[i] + "+rhs_output+"[i]*dt*("+sp.ccode(Butcher[s+1][s+1]).replace("L","")+");\n")
                            else:
                                if Butcher[num_steps][s+1] !=0:
                                    if Butcher[num_steps][s+1] !=1:
                                        file.write(indent+y_nplus1_running_total+"[i] += "+rhs_output+"[i]*dt*("+sp.ccode(Butcher[num_steps][s+1]).replace("L","")+");\n")
                                    else:
                                        file.write(indent+y_nplus1_running_total+"[i] += "+rhs_output+"[i]*dt;\n")
                                if Butcher[s+1][s+1] !=0:
                                    if Butcher[s+1][s+1] !=1:
                                        file.write(indent+rhs_output+"[i] = "+y_n+"[i] + "+rhs_output+"[i]*dt*("+sp.ccode(Butcher[s+1][s+1]).replace("L","")+");\n")
                                    else:
                                        file.write(indent+rhs_output+"[i] = "+y_n+"[i] + "+rhs_output+"[i]*dt;\n")
                            file.write("}\n")
                            file.write(post_RHS_string.replace("RK_OUTPUT_GFS",rhs_output)+"\n")

RK_MoL(RK_method,"rhs_eval(Nxx,Nxx_plus_2NGHOSTS,dxx, RK_INPUT_GFS, RK_OUTPUT_GFS);",
      "")
print("This is the MoL timestepping RK scheme C code for the "+str(RK_method)+" method: \n")
with open("MoLtimestepping/RK_MoL"+str(RK_method).replace(" ", "_")+".h", "r") as file:
    print(file.read())
```

    This is the MoL timestepping RK scheme C code for the RK3 Ralston method:

    // Code snippet implementing RK3 Ralston algorithm for Method of Lines timestepping

    // In a diagonal RK3 method like this one, only 3 gridfunctions need be defined. Below implements this approach.
    // Using y_n_gfs as input, compute k1 and apply boundary conditions
    rhs_eval(Nxx,Nxx_plus_2NGHOSTS,dxx, y_n_gfs, k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs);
    LOOP_ALL_GFS_GPS(i) {
        // Store k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs now as
        //  the update for the next rhs evaluation y_n + a21*k1*dt:
        k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs[i] = (1.0/2.0)*k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs[i]*dt + y_n_gfs[i];
    }
    // Apply boundary conditions to y_n + a21*k1*dt:

    // Compute k2 using yn + a21*k1*dt
    rhs_eval(Nxx,Nxx_plus_2NGHOSTS,dxx, k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs, k2_or_y_nplus_a32_k2_gfs);
    LOOP_ALL_GFS_GPS(i) {
        // Reassign k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs to be
        //    the running total y_{n+1}
        k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs[i] = (2.0/9.0)*(k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs[i] - y_n_gfs[i])/(1.0/2.0) + y_n_gfs[i];

        // Add a32*k2*dt to the running total
        k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs[i]+= (1.0/3.0)*k2_or_y_nplus_a32_k2_gfs[i]*dt;

        // Store k2_or_y_nplus_a32_k2_gfs now as y_n + a32*k2*dt
        k2_or_y_nplus_a32_k2_gfs[i] = (3.0/4.0)*k2_or_y_nplus_a32_k2_gfs[i]*dt + y_n_gfs[i];
    }
    // Apply boundary conditions to both y_n + a32*k2 (stored in k2_or_y_nplus_a32_k2_gfs)
    //    ... and the y_{n+1} running total, as they have not been applied yet to k2-related gridfunctions:

    // Compute k3
    rhs_eval(Nxx,Nxx_plus_2NGHOSTS,dxx, k2_or_y_nplus_a32_k2_gfs, y_n_gfs);
    LOOP_ALL_GFS_GPS(i) {
        // Add k3 to the running total and save to y_n
        y_n_gfs[i] = k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs[i] + (4.0/9.0)*y_n_gfs[i]*dt;
    }
    // Apply boundary conditions to the running total

## Step 3.c: Freeing Allocated Memory, `MoLtimestepping/RK_Free_Memory.h` [Back to [top](#toc)\]
$$\label{free}$$

We define the function `RK_free()` which generates the C code for freeing the memory that was being occupied by the grid functions lists that had been allocated. The function writes the C code to the header file `MoLtimestepping/RK_Free_Memory.h`

```python
def RK_free(RK_method):
    L = len(Butcher_dict[RK_method][0])-1 # Useful when freeing k_i gridfunctions

    with open("MoLtimestepping/RK_Free_Memory"+str(RK_method).replace(" ", "_")+".h", "w") as file:
        file.write("// CODE SNIPPET FOR FREEING ALL ALLOCATED MEMORY FOR "+str(RK_method)+" METHOD:\n")
        if diagonal(RK_method) == True and "RK3" in RK_method:
            file.write("""
free(k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs);
free(k2_or_y_nplus_a32_k2_gfs);
free(y_n_gfs);""")
        else:
            file.write("free(y_n_gfs);\n")
            if diagonal(RK_method) == False: # Free memory for allocations made for non-diagonal cases
                file.write("free(next_y_input_gfs);\n")
                for i in range(L):
                    file.write("free(k"+str(i+1)+"_gfs);\n")
            else: # Free memory for allocations made for diagonal cases
                file.write("free(y_nplus1_running_total_gfs);\n")
                if RK_method != 'Euler':
                    file.write("free(k_odd_gfs);\n")
                    file.write("free(k_even_gfs);\n")

RK_free(RK_method)
print("This is the freeing allocated memory C code for the "+str(RK_method)+" method: \n")
with open("MoLtimestepping/RK_Free_Memory"+str(RK_method).replace(" ", "_")+".h", "r") as file:
    print(file.read())
```

    This is the freeing allocated memory C code for the RK3 Ralston method:

    // CODE SNIPPET FOR FREEING ALL ALLOCATED MEMORY FOR RK3 Ralston METHOD:

    free(k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs);
    free(k2_or_y_nplus_a32_k2_gfs);
    free(y_n_gfs);

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

As a code validation check, we verify agreement in the dictionary of Butcher tables between

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

We generate the header files for each RK method and check for agreement with the NRPY+ module.

```python
import sys
import MoLtimestepping.C_Code_Generation as MoLC

print("\n\n ### BEGIN VALIDATION TESTS ###")
import filecmp
fileprefix1 = "MoLtimestepping/RK_Allocate_Memory"
fileprefix2 = "MoLtimestepping/RK_MoL"
fileprefix3 = "MoLtimestepping/RK_Free_Memory"
for key, value in Butcher_dict.items():
    MoLC.MoL_C_Code_Generation(key,
                                "rhs_eval(Nxx,Nxx_plus_2NGHOSTS,dxx, RK_INPUT_GFS, RK_OUTPUT_GFS);",
                                "apply_bcs(Nxx,Nxx_plus_2NGHOSTS, RK_OUTPUT_GFS);")
    RK_Allocate(key)
    RK_MoL(key,
            "rhs_eval(Nxx,Nxx_plus_2NGHOSTS,dxx, RK_INPUT_GFS, RK_OUTPUT_GFS);",
            "apply_bcs(Nxx,Nxx_plus_2NGHOSTS, RK_OUTPUT_GFS);")
    RK_free(key)
    if filecmp.cmp(fileprefix1+str(key).replace(" ", "_")+".h" , fileprefix1+".h") == False:
        print("VALIDATION TEST FAILED ON files: "+fileprefix1+str(key).replace(" ", "_")+".h and "+ fileprefix1+".h")
        sys.exit(1)
    elif filecmp.cmp(fileprefix2+str(key).replace(" ", "_")+".h" , fileprefix2+".h") == False:
        print("VALIDATION TEST FAILED ON files: "+fileprefix2+str(key).replace(" ", "_")+".h and "+ fileprefix2+".h")
        sys.exit(1)
    elif filecmp.cmp(fileprefix3+str(key).replace(" ", "_")+".h" , fileprefix3+".h") == False:
        print("VALIDATION TEST FAILED ON files: "+fileprefix3+str(key).replace(" ", "_")+".h and "+ fileprefix3+".h")
        sys.exit(1)
    else:
        print("VALIDATION TEST PASSED on all files from "+str(key)+" method")
print("### END VALIDATION TESTS ###")
```

     ### BEGIN VALIDATION TESTS ###
    VALIDATION TEST PASSED on all files from Euler method
    VALIDATION TEST PASSED on all files from RK2 Heun method
    VALIDATION TEST PASSED on all files from RK2 MP method
    VALIDATION TEST PASSED on all files from RK2 Ralston method
    VALIDATION TEST PASSED on all files from RK3 method
    VALIDATION TEST PASSED on all files from RK3 Heun method
    VALIDATION TEST PASSED on all files from RK3 Ralston method
    VALIDATION TEST PASSED on all files from SSPRK3 method
    VALIDATION TEST PASSED on all files from RK4 method
    VALIDATION TEST PASSED on all files from DP5 method
    VALIDATION TEST PASSED on all files from DP5alt method
    VALIDATION TEST PASSED on all files from CK5 method
    VALIDATION TEST PASSED on all files from DP6 method
    VALIDATION TEST PASSED on all files from L6 method
    VALIDATION TEST PASSED on all files from DP8 method
    ### END VALIDATION TESTS ###

# Step 5: Output this module to $\LaTeX$-formatted PDF \[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-RK_Butcher_Table_Generating_C_Code.pdf](Tutorial-RK_Butcher_Table_Generating_C_Code.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-Method_of_Lines-C_Code_Generation.ipynb
!pdflatex -interaction=batchmode Tutorial-Method_of_Lines-C_Code_Generation.tex
!pdflatex -interaction=batchmode Tutorial-Method_of_Lines-C_Code_Generation.tex
!pdflatex -interaction=batchmode Tutorial-Method_of_Lines-C_Code_Generation.tex
!rm -f Tut*.out Tut*.aux Tut*.log
```

    [NbConvertApp] Converting notebook Tutorial-Method_of_Lines-C_Code_Generation.ipynb to latex
    [NbConvertApp] Writing 86658 bytes to Tutorial-Method_of_Lines-C_Code_Generation.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


# `GiRaFFE_NRPy`: Solving the Induction Equation

## Author: Patrick Nelson

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

**Notebook Status:**  Validated

**Validation Notes:** This code has been validated by showing that it converges to the exact answer at the expected order

### NRPy+ Source Code for this module:
* [GiRaFFE_NRPy/Afield_flux.py](../../edit/in_progress/GiRaFFE_NRPy/Afield_flux.py)

Our goal in this module is to write the code necessary to solve the induction equation
$$
\partial_t A_i = \underbrace{\epsilon_{ijk} v^j B^k}_{\rm Flux\ terms} - \underbrace{\partial_i \left(\alpha \Phi - \beta^j A_j \right)}_{\rm Gauge\ terms}.
$$
To properly handle the flux terms and avoiding problems with shocks, we cannot simply take a cross product of the velocity and magnetic field at the cell centers. Instead, we must solve the Riemann problem at the cell faces using the reconstructed values of the velocity and magnetic field on either side of the cell faces. The reconstruction is done using PPM (see [here](Tutorial-GiRaFFE_NRPy-PPM.ipynb)); in this module, we will assume that that step has already been done. Metric quantities are assumed to have been interpolated to cell faces, as is done in [this](Tutorial-GiRaFFE_NRPy-Metric_Face_Values.ipynb) tutorial.

Tóth's [paper](https://www.sciencedirect.com/science/article/pii/S0021999100965197?via%3Dihub), Eqs. 30 and 31, are one of the first implementations of such a scheme. The original GiRaFFE used a 2D version of the algorithm from [Del Zanna, et al. (2002)](https://arxiv.org/abs/astro-ph/0210618); but since we are not using staggered grids, we can greatly simplify this algorithm with respect to the version used in the original `GiRaFFE`. Instead, we will adapt the implementations of the algorithm used in [Mewes, et al. (2020)](https://arxiv.org/abs/2002.06225) and [Giacomazzo, et al. (2011)](https://arxiv.org/abs/1009.2468), Eqs. 3-11.

We first write the flux contribution to the induction equation RHS as
$$
\partial_t A_i = -E_i,
$$
where the electric field $E_i$ is given in ideal MHD (of which FFE is a subset) as
$$
-E_i = \epsilon_{ijk} v^j B^k,
$$
where $v^i$ is the drift velocity,  $B^i$ is the magnetic field, and $\epsilon_{ijk} = \sqrt{\gamma} [ijk]$ is the Levi-Civita tensor.
In Cartesian coordinates,
\begin{align}
-E_x &= [F^y(B^z)]_x = -[F^z(B^y)]_x \\
-E_y &= [F^z(B^x)]_y = -[F^x(B^z)]_y \\
-E_z &= [F^x(B^y)]_z = -[F^y(B^x)]_z, \\
\end{align}
where
$$
[F^i(B^j)]_k = \sqrt{\gamma} (v^i B^j - v^j B^i).
$$
To compute the actual contribution to the RHS in some direction $i$, we average the above listed field as calculated on the $+j$, $-j$, $+k$, and $-k$ faces. That is, at some point $(i,j,k)$ on the grid,
\begin{align}
-E_x(x_i,y_j,z_k) &= \frac{1}{4} \left( [F_{\rm HLL}^y(B^z)]_{x(i,j+1/2,k)}+[F_{\rm HLL}^y(B^z)]_{x(i,j-1/2,k)}-[F_{\rm HLL}^z(B^y)]_{x(i,j,k+1/2)}-[F_{\rm HLL}^z(B^y)]_{x(i,j,k-1/2)} \right) \\
-E_y(x_i,y_j,z_k) &= \frac{1}{4} \left( [F_{\rm HLL}^z(B^x)]_{y(i,j,k+1/2)}+[F_{\rm HLL}^z(B^x)]_{y(i,j,k-1/2)}-[F_{\rm HLL}^x(B^z)]_{y(i+1/2,j,k)}-[F_{\rm HLL}^x(B^z)]_{y(i-1/2,j,k)} \right) \\
-E_z(x_i,y_j,z_k) &= \frac{1}{4} \left( [F_{\rm HLL}^x(B^y)]_{z(i+1/2,j,k)}+[F_{\rm HLL}^x(B^y)]_{z(i-1/2,j,k)}-[F_{\rm HLL}^y(B^x)]_{z(i,j+1/2,k)}-[F_{\rm HLL}^y(B^x)]_{z(i,j-1/2,k)} \right). \\
\end{align}
Note the use of $F_{\rm HLL}$ here. This change signifies that the quantity output here is from the HLLE Riemann solver. Note also the indices on the fluxes. Values of $\pm 1/2$ indicate that these are computed on cell faces using the reconstructed values of $v^i$ and $B^i$ and the interpolated values of the metric gridfunctions. So,
$$
F_{\rm HLL}^i(B^j) = \frac{c_{\rm min} F_{\rm R}^i(B^j) + c_{\rm max} F_{\rm L}^i(B^j) - c_{\rm min} c_{\rm max} (B_{\rm R}^j-B_{\rm L}^j)}{c_{\rm min} + c_{\rm max}}.
$$

The speeds $c_\min$ and $c_\max$ are characteristic speeds that waves can travel through the plasma. In GRFFE, the expressions defining them reduce a function of only the metric quantities. $c_\min$ is the negative of the minimum amongst the speeds $c_-$ and $0$ and $c_\max$ is the maximum amongst the speeds $c_+$ and $0$. The speeds $c_\pm = \left. \left(-b \pm \sqrt{b^2-4ac}\right)\middle/ \left(2a\right) \right.$ must be calculated on both the left and right faces, where
$$a = 1/\alpha^2,$$
$$b = 2 \beta^i / \alpha^2$$
and $$c = g^{ii} - (\beta^i)^2/\alpha^2.$$
An outline of a general finite-volume method is as follows, with the current step in bold:
1. The Reconstruction Step - Piecewise Parabolic Method
    1. Within each cell, fit to a function that conserves the volume in that cell using information from the neighboring cells
        * For PPM, we will naturally use parabolas
    1. Use that fit to define the state at the left and right interface of each cell
    1. Apply a slope limiter to mitigate Gibbs phenomenon
1. Interpolate the value of the metric gridfunctions on the cell faces
1. **Solving the Riemann Problem - Harten, Lax, (This notebook, $E_i$ only)**
    1. **Use the left and right reconstructed states to calculate the unique state at boundary**

We will assume in this notebook that the reconstructed velocities and magnetic fields are available on cell faces as input. We will also assume that the metric gridfunctions have been interpolated on the metric faces.

Solving the Riemann problem, then, consists of two substeps: First, we compute the flux through each face of the cell. Then, we add the average of these fluxes to the right-hand side of the evolution equation for the vector potential.

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

This notebook is organized as follows

1. [Step 1](#prelim): Preliminaries
1. [Step 2](#a_i_flux): Computing the Magnetic Flux
    1. [Step 2.a](#hydro_speed): GRFFE characteristic wave speeds
    1. [Step 2.b](#fluxes): Compute the HLLE fluxes
1. [Step 3](#code_validation): Code Validation against `GiRaFFE_NRPy.Afield_flux` NRPy+ Module
1. [Step 4](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file

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

We begin by importing the NRPy+ core functionality. We also import the Levi-Civita symbol, the GRHD module, and the GRFFE module.

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

from outputC import outCfunction, outputC # NRPy+: Core C code output module
import sympy as sp               # SymPy: The Python computer algebra package upon which NRPy+ depends
import NRPy_param_funcs as par   # NRPy+: Parameter interface
import grid as gri               # NRPy+: Functions having to do with numerical grids
import indexedexp as ixp         # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support
import cmdline_helper as cmd     # NRPy+: Multi-platform Python command-line interface

thismodule = "GiRaFFE_NRPy-Afield_flux"

import GRHD.equations as GRHD
# import GRFFE.equations as GRFFE
```

# Step 2: Computing the Magnetic Flux \[Back to [top](#toc)\]
$$\label{a_i_flux}$$

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

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

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

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

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

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

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

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

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

    # Now, we are free to solve the quadratic equation as usual. We take care to avoid passing a
    # negative value to the sqrt function.
    detm = b*b - sp.sympify(4)*a*c

    import Min_Max_and_Piecewise_Expressions as noif
    detm = sp.sqrt(noif.max_noif(sp.sympify(0),detm))
    global cplus,cminus
    cplus  = sp.Rational(1,2)*(-b/a + detm/a)
    cminus = sp.Rational(1,2)*(-b/a - detm/a)
```

In flat spacetime, where $\alpha=1$, $\beta^i=0$, and $\gamma^{ij} = \delta^{ij}$, $c_+ > 0$ and $c_- < 0$. For the HLLE solver, we will need both `cmax` and `cmin` to be positive; we also want to choose the speed that is larger in magnitude because overestimating the characteristic speeds will help damp unwanted oscillations. (However, in GRFFE, we only get one $c_+$ and one $c_-$, so we only need to fix the signs here.) Hence, the following function.

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

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

    # The following algorithms have been verified with random floats:

    global cmax,cmin
    # Now, we need to set cmax to the larger of cpr,cpl, and 0

    import Min_Max_and_Piecewise_Expressions as noif
    cmax = noif.max_noif(cp,sp.sympify(0))

    # And then, set cmin to the smaller of cmr,cml, and 0
    cmin = -noif.min_noif(cm,sp.sympify(0))
```

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

Here, we we calculate the flux and state vectors for the electric field. The flux vector is here given as
$$
[F^i(B^j)]_k = \sqrt{\gamma} (v^i B^j - v^j B^i).
$$
Here, $v^i$ is the drift velocity and $B^i$ is the magnetic field.
This can be easily handled for an input flux direction $i$ with
$$
[F^j(B^k)]_i = \epsilon_{ijk} v^j B^k,
$$
where $\epsilon_{ijk} = \sqrt{\gamma} [ijk]$ and $[ijk]$ is the Levi-Civita symbol.

The state vector is simply the magnetic field $B^j$.

```python
def calculate_flux_and_state_for_Induction(field_comp,flux_dirn, gammaDD,betaU,alpha,ValenciavU,BU):
    # Define Levi-Civita symbol
    def define_LeviCivitaSymbol_rank3(DIM=-1):
        if DIM == -1:
            DIM = par.parval_from_str("DIM")

        LeviCivitaSymbol = ixp.zerorank3()

        for i in range(DIM):
            for j in range(DIM):
                for k in range(DIM):
                    # From https://codegolf.stackexchange.com/questions/160359/levi-civita-symbol :
                    LeviCivitaSymbol[i][j][k] = (i - j) * (j - k) * (k - i) * sp.Rational(1,2)
        return LeviCivitaSymbol
    GRHD.compute_sqrtgammaDET(gammaDD)
    # Here, we import the Levi-Civita tensor and compute the tensor with lower indices
    LeviCivitaDDD = define_LeviCivitaSymbol_rank3()
    for i in range(3):
        for j in range(3):
            for k in range(3):
                LeviCivitaDDD[i][j][k] *= GRHD.sqrtgammaDET

    global U,F
    # Flux F = \epsilon_{ijk} v^j B^k
    F = sp.sympify(0)
    for j in range(3):
        for k in range(3):
            F += LeviCivitaDDD[field_comp][j][k] * (alpha*ValenciavU[j]-betaU[j]) * BU[k]
    # U = B^i
    U = BU[flux_dirn]
```

Now, we write a standard HLLE solver based on eq. 3.15 in [the HLLE paper](https://epubs.siam.org/doi/pdf/10.1137/1025002),
$$
F^{\rm HLL} = \frac{c_{\rm min} F_{\rm R} + c_{\rm max} F_{\rm L} - c_{\rm min} c_{\rm max} (U_{\rm R}-U_{\rm L})}{c_{\rm min} + c_{\rm max}}
$$

```python
def HLLE_solver(cmax, cmin, Fr, Fl, Ur, Ul):
    # This solves the Riemann problem for the flux of E_i in one direction

    # F^HLL = (c_\min f_R + c_\max f_L - c_\min c_\max ( st_j_r - st_j_l )) / (c_\min + c_\max)
    return (cmin*Fr + cmax*Fl - cmin*cmax*(Ur-Ul) )/(cmax + cmin)
```

Here, we will use the function we just wrote to calculate the flux through a face. We will pass the reconstructed Valencia 3-velocity and magnetic field on either side of an interface to this function (designated as the "left" and "right" sides) along with the value of the 3-metric, shift vector, and lapse function on the interface. The parameter `flux_dirn` specifies which face through which we are calculating the flux. However, unlike when we used this method to calculate the flux term, the RHS of each component of $A_i$ does not depend on all three of the flux directions. Instead, the flux of one component of the $E_i$ field depends on flux through the faces in the other two directions. This will be handled when we generate the C function, as demonstrated in the example code after this next function.

Note that we allow the user to declare their own gridfunctions if they wish, and default to declaring basic symbols if they are not provided. The default names are chosen to imply interpolation of the metric gridfunctions and reconstruction of the primitives.

```python
def calculate_E_i_flux(flux_dirn,alpha_face=None,gamma_faceDD=None,beta_faceU=None,\
                       Valenciav_rU=None,B_rU=None,Valenciav_lU=None,B_lU=None):
    global E_fluxD
    E_fluxD = ixp.zerorank1()
    for field_comp in range(3):
        find_cmax_cmin(field_comp,gamma_faceDD,beta_faceU,alpha_face)
        calculate_flux_and_state_for_Induction(field_comp,flux_dirn, gamma_faceDD,beta_faceU,alpha_face,\
                                               Valenciav_rU,B_rU)
        Fr = F
        Ur = U
        calculate_flux_and_state_for_Induction(field_comp,flux_dirn, gamma_faceDD,beta_faceU,alpha_face,\
                                               Valenciav_lU,B_lU)
        Fl = F
        Ul = U
        E_fluxD[field_comp] += HLLE_solver(cmax, cmin, Fr, Fl, Ur, Ul)
```

Below, we will write some example code to use the above functions to generate C code for `GiRaFFE_NRPy`. We need to write our own memory reads and writes because we need to add contributions from *both* faces in a given direction, which is expressed in the code as adding contributions from adjacent gridpoints to the RHS, which is not something `FD_outputC` can handle. The `.replace()` function calls adapt these reads and writes to the different directions. Note that, for reconstructions in a given direction, the fluxes are only added to the other two components, as can be seen in the equations we are implementing.
\begin{align}
-E_x(x_i,y_j,z_k) &= \frac{1}{4} \left( [F_{\rm HLL}^y(B^z)]_{x(i,j+1/2,k)}+[F_{\rm HLL}^y(B^z)]_{x(i,j-1/2,k)}-[F_{\rm HLL}^z(B^y)]_{x(i,j,k+1/2)}-[F_{\rm HLL}^z(B^y)]_{x(i,j,k-1/2)} \right) \\
-E_y(x_i,y_j,z_k) &= \frac{1}{4} \left( [F_{\rm HLL}^z(B^x)]_{y(i,j,k+1/2)}+[F_{\rm HLL}^z(B^x)]_{y(i,j,k-1/2)}-[F_{\rm HLL}^x(B^z)]_{y(i+1/2,j,k)}-[F_{\rm HLL}^x(B^z)]_{y(i-1/2,j,k)} \right) \\
-E_z(x_i,y_j,z_k) &= \frac{1}{4} \left( [F_{\rm HLL}^x(B^y)]_{z(i+1/2,j,k)}+[F_{\rm HLL}^x(B^y)]_{z(i-1/2,j,k)}-[F_{\rm HLL}^y(B^x)]_{z(i,j+1/2,k)}-[F_{\rm HLL}^y(B^x)]_{z(i,j-1/2,k)} \right). \\
\end{align}
From this, we can see that when, for instance, we reconstruct and interpolate in the $x$-direction, we must add only to the $y$- and $z$-components of the electric field.

Recall that when we reconstructed the velocity and magnetic field, we constructed to the $i-1/2$ face, so the data at $i+1/2$ is stored at $i+1$.

```python
def generate_Afield_flux_function_files(out_dir,subdir,alpha_face,gamma_faceDD,beta_faceU,\
                                        Valenciav_rU,B_rU,Valenciav_lU,B_lU,inputs_provided=True):
    if not inputs_provided:
        # declare all variables
        alpha_face = sp.symbols(alpha_face)
        beta_faceU = ixp.declarerank1("beta_faceU")
        gamma_faceDD = ixp.declarerank2("gamma_faceDD","sym01")
        Valenciav_rU = ixp.declarerank1("Valenciav_rU")
        B_rU = ixp.declarerank1("B_rU")
        Valenciav_lU = ixp.declarerank1("Valenciav_lU")
        B_lU = ixp.declarerank1("B_lU")

    Memory_Read = """const double alpha_face = auxevol_gfs[IDX4S(ALPHA_FACEGF, i0,i1,i2)];
    const double gamma_faceDD00 = auxevol_gfs[IDX4S(GAMMA_FACEDD00GF, i0,i1,i2)];
    const double gamma_faceDD01 = auxevol_gfs[IDX4S(GAMMA_FACEDD01GF, i0,i1,i2)];
    const double gamma_faceDD02 = auxevol_gfs[IDX4S(GAMMA_FACEDD02GF, i0,i1,i2)];
    const double gamma_faceDD11 = auxevol_gfs[IDX4S(GAMMA_FACEDD11GF, i0,i1,i2)];
    const double gamma_faceDD12 = auxevol_gfs[IDX4S(GAMMA_FACEDD12GF, i0,i1,i2)];
    const double gamma_faceDD22 = auxevol_gfs[IDX4S(GAMMA_FACEDD22GF, i0,i1,i2)];
    const double beta_faceU0 = auxevol_gfs[IDX4S(BETA_FACEU0GF, i0,i1,i2)];
    const double beta_faceU1 = auxevol_gfs[IDX4S(BETA_FACEU1GF, i0,i1,i2)];
    const double beta_faceU2 = auxevol_gfs[IDX4S(BETA_FACEU2GF, i0,i1,i2)];
    const double Valenciav_rU0 = auxevol_gfs[IDX4S(VALENCIAV_RU0GF, i0,i1,i2)];
    const double Valenciav_rU1 = auxevol_gfs[IDX4S(VALENCIAV_RU1GF, i0,i1,i2)];
    const double Valenciav_rU2 = auxevol_gfs[IDX4S(VALENCIAV_RU2GF, i0,i1,i2)];
    const double B_rU0 = auxevol_gfs[IDX4S(B_RU0GF, i0,i1,i2)];
    const double B_rU1 = auxevol_gfs[IDX4S(B_RU1GF, i0,i1,i2)];
    const double B_rU2 = auxevol_gfs[IDX4S(B_RU2GF, i0,i1,i2)];
    const double Valenciav_lU0 = auxevol_gfs[IDX4S(VALENCIAV_LU0GF, i0,i1,i2)];
    const double Valenciav_lU1 = auxevol_gfs[IDX4S(VALENCIAV_LU1GF, i0,i1,i2)];
    const double Valenciav_lU2 = auxevol_gfs[IDX4S(VALENCIAV_LU2GF, i0,i1,i2)];
    const double B_lU0 = auxevol_gfs[IDX4S(B_LU0GF, i0,i1,i2)];
    const double B_lU1 = auxevol_gfs[IDX4S(B_LU1GF, i0,i1,i2)];
    const double B_lU2 = auxevol_gfs[IDX4S(B_LU2GF, i0,i1,i2)];
    REAL A_rhsD0 = 0; REAL A_rhsD1 = 0; REAL A_rhsD2 = 0;
    """
    Memory_Write = """rhs_gfs[IDX4S(AD0GF,i0,i1,i2)] += A_rhsD0;
    rhs_gfs[IDX4S(AD1GF,i0,i1,i2)] += A_rhsD1;
    rhs_gfs[IDX4S(AD2GF,i0,i1,i2)] += A_rhsD2;
    """

    indices = ["i0","i1","i2"]
    indicesp1 = ["i0+1","i1+1","i2+1"]

    for flux_dirn in range(3):
        calculate_E_i_flux(flux_dirn,alpha_face,gamma_faceDD,beta_faceU,\
                              Valenciav_rU,B_rU,Valenciav_lU,B_lU)

        E_field_to_print = [\
                            sp.Rational(1,4)*E_fluxD[(flux_dirn+1)%3],
                            sp.Rational(1,4)*E_fluxD[(flux_dirn+2)%3],
                           ]

        E_field_names = [\
                         "A_rhsD"+str((flux_dirn+1)%3),
                         "A_rhsD"+str((flux_dirn+2)%3),
                        ]

        desc = "Calculate the electric flux on the left face in direction " + str(flux_dirn) + "."
        name = "calculate_E_field_D" + str(flux_dirn) + "_right"
        outCfunction(
            outfile  = os.path.join(out_dir,subdir,name+".h"), desc=desc, name=name,
            params   ="const paramstruct *params,const REAL *auxevol_gfs,REAL *rhs_gfs",
            body     =  Memory_Read \
                       +outputC(E_field_to_print,E_field_names,"returnstring",params="outCverbose=False").replace("IDX4","IDX4S")\
                       +Memory_Write,
            loopopts ="InteriorPoints",
            rel_path_to_Cparams=os.path.join("../"))

        desc = "Calculate the electric flux on the left face in direction " + str(flux_dirn) + "."
        name = "calculate_E_field_D" + str(flux_dirn) + "_left"
        outCfunction(
            outfile  = os.path.join(out_dir,subdir,name+".h"), desc=desc, name=name,
            params   ="const paramstruct *params,const REAL *auxevol_gfs,REAL *rhs_gfs",
            body     =  Memory_Read.replace(indices[flux_dirn],indicesp1[flux_dirn]) \
                       +outputC(E_field_to_print,E_field_names,"returnstring",params="outCverbose=False").replace("IDX4","IDX4S")\
                       +Memory_Write,
            loopopts ="InteriorPoints",
            rel_path_to_Cparams=os.path.join("../"))

```

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

Here, as a code validation check, we verify agreement in the SymPy expressions for the `GiRaFFE` evolution equations and auxiliary quantities we intend to use between
1. this tutorial and
2. the NRPy+ [GiRaFFE_NRPy.Induction_Equation](../../edit/in_progress/GiRaFFE_NRPy/Induction_Equation.py) module.

Below are the gridfunction registrations we will need for testing. We will pass these to the above functions to self-validate the module that corresponds with this tutorial.

```python
all_passed=True
def comp_func(expr1,expr2,basename,prefixname2="C2P_P2C."):
    if str(expr1-expr2)!="0":
        print(basename+" - "+prefixname2+basename+" = "+ str(expr1-expr2))
        all_passed=False

def gfnm(basename,idx1,idx2=None,idx3=None):
    if idx2 is None:
        return basename+"["+str(idx1)+"]"
    if idx3 is None:
        return basename+"["+str(idx1)+"]["+str(idx2)+"]"
    return basename+"["+str(idx1)+"]["+str(idx2)+"]["+str(idx3)+"]"

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

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

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

import GiRaFFE_NRPy.Afield_flux as Af

expr_list = []
exprcheck_list = []
namecheck_list = []

for flux_dirn in range(3):
    calculate_E_i_flux(flux_dirn,alpha_face,gamma_faceDD,beta_faceU,\
                       Valenciav_rU,B_rU,Valenciav_lU,B_lU)
    Af.calculate_E_i_flux(flux_dirn,alpha_face,gamma_faceDD,beta_faceU,\
                          Valenciav_rU,B_rU,Valenciav_lU,B_lU)
    namecheck_list.extend([gfnm("E_fluxD",flux_dirn)])
    exprcheck_list.extend([Af.E_fluxD[flux_dirn]])
    expr_list.extend([E_fluxD[flux_dirn]])

for mom_comp in range(len(expr_list)):
    comp_func(expr_list[mom_comp],exprcheck_list[mom_comp],namecheck_list[mom_comp])

import sys
if all_passed:
    print("ALL TESTS PASSED!")
else:
    print("ERROR: AT LEAST ONE TEST DID NOT PASS")
    sys.exit(1)
```

    ALL TESTS PASSED!

We will also check the output C code to make sure it matches what is produced by the python module.

```python
import difflib
import sys

subdir = os.path.join("RHSs")

out_dir = os.path.join("GiRaFFE_standalone_Ccodes")
cmd.mkdir(out_dir)
cmd.mkdir(os.path.join(out_dir,subdir))
valdir = os.path.join("GiRaFFE_Ccodes_validation")
cmd.mkdir(valdir)
cmd.mkdir(os.path.join(valdir,subdir))

generate_Afield_flux_function_files(out_dir,subdir,alpha_face,gamma_faceDD,beta_faceU,\
                                    Valenciav_rU,B_rU,Valenciav_lU,B_lU,inputs_provided=True)
Af.generate_Afield_flux_function_files(valdir,subdir,alpha_face,gamma_faceDD,beta_faceU,\
                                       Valenciav_rU,B_rU,Valenciav_lU,B_lU,inputs_provided=True)

print("Printing difference between original C code and this code...")
# Open the files to compare
files = ["RHSs/calculate_E_field_D0_right.h",
         "RHSs/calculate_E_field_D0_left.h",
         "RHSs/calculate_E_field_D1_right.h",
         "RHSs/calculate_E_field_D1_left.h",
         "RHSs/calculate_E_field_D2_right.h",
         "RHSs/calculate_E_field_D2_left.h"]

for file in files:
    print("Checking file " + file)
    with open(os.path.join(valdir,file)) as file1, open(os.path.join(out_dir,file)) as file2:
        # Read the lines of each file
        file1_lines = file1.readlines()
        file2_lines = file2.readlines()
        num_diffs = 0
        for line in difflib.unified_diff(file1_lines, file2_lines, fromfile=os.path.join(valdir,file), tofile=os.path.join(out_dir,file)):
            sys.stdout.writelines(line)
            num_diffs = num_diffs + 1
        if num_diffs == 0:
            print("No difference. TEST PASSED!")
        else:
            print("ERROR: Disagreement found with .py file. See differences above.")
```

    Output C function calculate_E_field_D0_right() to file GiRaFFE_standalone_Ccodes\RHSs\calculate_E_field_D0_right.h
    Output C function calculate_E_field_D0_left() to file GiRaFFE_standalone_Ccodes\RHSs\calculate_E_field_D0_left.h
    Output C function calculate_E_field_D1_right() to file GiRaFFE_standalone_Ccodes\RHSs\calculate_E_field_D1_right.h
    Output C function calculate_E_field_D1_left() to file GiRaFFE_standalone_Ccodes\RHSs\calculate_E_field_D1_left.h
    Output C function calculate_E_field_D2_right() to file GiRaFFE_standalone_Ccodes\RHSs\calculate_E_field_D2_right.h
    Output C function calculate_E_field_D2_left() to file GiRaFFE_standalone_Ccodes\RHSs\calculate_E_field_D2_left.h
    Output C function calculate_E_field_D0_right() to file GiRaFFE_Ccodes_validation\RHSs\calculate_E_field_D0_right.h
    Output C function calculate_E_field_D0_left() to file GiRaFFE_Ccodes_validation\RHSs\calculate_E_field_D0_left.h
    Output C function calculate_E_field_D1_right() to file GiRaFFE_Ccodes_validation\RHSs\calculate_E_field_D1_right.h
    Output C function calculate_E_field_D1_left() to file GiRaFFE_Ccodes_validation\RHSs\calculate_E_field_D1_left.h
    Output C function calculate_E_field_D2_right() to file GiRaFFE_Ccodes_validation\RHSs\calculate_E_field_D2_right.h
    Output C function calculate_E_field_D2_left() to file GiRaFFE_Ccodes_validation\RHSs\calculate_E_field_D2_left.h
    Printing difference between original C code and this code...
    Checking file RHSs/calculate_E_field_D0_right.h
    No difference. TEST PASSED!
    Checking file RHSs/calculate_E_field_D0_left.h
    No difference. TEST PASSED!
    Checking file RHSs/calculate_E_field_D1_right.h
    No difference. TEST PASSED!
    Checking file RHSs/calculate_E_field_D1_left.h
    No difference. TEST PASSED!
    Checking file RHSs/calculate_E_field_D2_right.h
    No difference. TEST PASSED!
    Checking file RHSs/calculate_E_field_D2_left.h
    No difference. TEST PASSED!

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

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

```python
import cmdline_helper as cmd    # NRPy+: Multi-platform Python command-line interface
cmd.output_Jupyter_notebook_to_LaTeXed_PDF("Tutorial-GiRaFFE_NRPy-Afield_flux",location_of_template_file=os.path.join(".."))
```

    Notebook output to PDF is only supported on Linux systems, with pdflatex installed.