# Start-to-Finish Example: `GiRaFFE_NRPy` 1D tests

### Authors: Patrick Nelson & Terrence Pierre Jacques

### Adapted from [Start-to-Finish Example: Head-On Black Hole Collision](../Tutorial-Start_to_Finish-BSSNCurvilinear-Two_BHs_Collide.ipynb)

## This module compiles and runs code tests for all 1D initial data options available in GiRaFFE-NRPy+, evolving one-dimensional GRFFE waves.

### NRPy+ Source Code for this module:

* Main python module for all 1D initial data: [GiRaFFEfood_NRPy/GiRaFFEfood_NRPy_1D_tests.py](../../edit/in_progress/GiRaFFEfood_NRPy/GiRaFFEfood_NRPy_1D_tests.py) __Options:__
    1. [Fast Wave](Tutorial-GiRaFFEfood_NRPy_1D_tests-fast_wave.ipynb)
    1. [Alfven Wave](Tutorial-GiRaFFEfood_NRPy_1D_alfven_wave.ipynb)
    1. [Degenerate Alfven Wave](Tutorial-GiRaFFEfood_NRPy_1D_tests-degen_Alfven_wave.ipynb)
    1. [Three Alfven Waves](Tutorial-GiRaFFEfood_NRPy_1D_tests-three_waves.ipynb)
    1. [FFE Breakdown](Tutorial-GiRaFFEfood_NRPy_1D_tests-FFE_breakdown.ipynb)
* [GiRaFFE_NRPy/GiRaFFE_NRPy_staggered_Afield_flux.py](../../edit/in_progress/GiRaFFE_NRPy/GiRaFFE_NRPy_staggered_Afield_flux.py) [\[**tutorial**\]](Tutorial-GiRaFFE_NRPy_staggered-Afield_flux.ipynb) Generates the expressions to find the flux term of the induction equation.
* [GiRaFFE_NRPy/GiRaFFE_NRPy_staggered_A2B.py](../../edit/in_progress/GiRaFFE_NRPy/GiRaFFE_NRPy_staggered_A2B.py) [\[**tutorial**\]](Tutorial-GiRaFFE_NRPy_staggered-A2B.ipynb) Generates the driver to compute the magnetic field from the vector potential/
* [GiRaFFE_NRPy/GiRaFFE_NRPy_BCs.py](../../edit/in_progress/GiRaFFE_NRPy/GiRaFFE_NRPy_BCs.py) [\[**tutorial**\]](Tutorial-GiRaFFE_NRPy-BCs.ipynb) Generates the code to apply boundary conditions to the vector potential, scalar potential, and three-velocity.
* [GiRaFFE_NRPy/GiRaFFE_NRPy_C2P_P2C.py](../../edit/in_progress/GiRaFFE_NRPy/GiRaFFE_NRPy_C2P_P2C.py) [\[**tutorial**\]](Tutorial-GiRaFFE_NRPy-C2P_P2C.ipynb) Generates the conservative-to-primitive and primitive-to-conservative solvers.
* [GiRaFFE_NRPy/GiRaFFE_NRPy_Metric_Face_Values.py](../../edit/in_progress/GiRaFFE_NRPy/GiRaFFE_NRPy_Metric_Face_Values.py) [\[**tutorial**\]](Tutorial-GiRaFFE_NRPy-Metric_Face_Values.ipynb) Generates code to interpolate metric gridfunctions to cell faces.
* [GiRaFFE_NRPy/GiRaFFE_NRPy_PPM.py](../../edit/in_progress/GiRaFFE_NRPy/GiRaFFE_NRPy_PPM.py) [\[**tutorial**\]](Tutorial-GiRaFFE_NRPy-PPM.ipynb) Genearates code to reconstruct primitive variables on cell faces.
* [GiRaFFE_NRPy/GiRaFFE_NRPy_staggered_Source_Terms.py](../../edit/in_progress/GiRaFFE_NRPy/GiRaFFE_NRPy_staggered_Source_Terms.py) [\[**tutorial**\]](Tutorial-GiRaFFE_NRPy_staggered-Source_Terms.ipynb) Generates the expressions to find the flux term of the Poynting flux evolution equation.
* [GiRaFFE_NRPy/Stilde_flux.py](../../edit/in_progress/GiRaFFE_NRPy/Stilde_flux.py) [\[**tutorial**\]](Tutorial-GiRaFFE_NRPy-Stilde_flux.ipynb) Generates the expressions to find the flux term of the Poynting flux evolution equation.
* [../GRFFE/equations.py](../../edit/GRFFE/equations.py) [\[**tutorial**\]](../Tutorial-GRFFE_Equations-Cartesian.ipynb) Generates code necessary to compute the source terms.
* [../GRHD/equations.py](../../edit/GRHD/equations.py) [\[**tutorial**\]](../Tutorial-GRHD_Equations-Cartesian.ipynb) Generates code necessary to compute the source terms.

Here we use NRPy+ to generate the C source code necessary to set up initial data for an Alfvén wave (see [the original GiRaFFE paper](https://arxiv.org/pdf/1704.00599.pdf)). Then we use it to generate the RHS expressions for [Method of Lines](https://reference.wolfram.com/language/tutorial/NDSolveMethodOfLines.html) time integration based on the [explicit Runge-Kutta fourth-order scheme](https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods) (RK4).

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

This notebook is organized as follows

1. [Step 1](#initializenrpy): Set core NRPy+ parameters for numerical grids
1. [Step 2](#grffe): Output C code for GRFFE evolution
    1. [Step 2.a](#mol): Output macros for Method of Lines timestepping
1. [Step 3](#gf_id): Import `GiRaFFEfood_NRPy` initial data modules
1. [Step 4](#cparams): Output C codes needed for declaring and setting Cparameters; also set `free_parameters.h`
1. [Step 5](#mainc): `GiRaFFE_NRPy_standalone.c`: The Main C Code
1. [Step 6](#compileexec): Compile and execute C codes
1. [Step 7](#plots): Data Visualization
1. [Step 8](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file

# Step 1: Set up core functions and parameters for solving  GRFFE equations \[Back to [top](#toc)\]
$$\label{setup}$$

```python
import shutil, os, sys           # Standard Python modules for multiplatform OS-level functions
# First, we'll add the parent directory to the list of directories Python will check for modules.
nrpy_dir_path = os.path.join("..")
if nrpy_dir_path not in sys.path:
    sys.path.append(nrpy_dir_path)

# Step P1: Import needed NRPy+ core modules:
from outputC import outCfunction, lhrh # NRPy+: Core C code output module
import sympy as sp               # SymPy: The Python computer algebra package upon which NRPy+ depends
import finite_difference as fin  # NRPy+: Finite difference C code generation module
import NRPy_param_funcs as par   # NRPy+: Parameter interface
import grid as gri               # NRPy+: Functions having to do with numerical grids
import 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

# Step P2: Create C code output directory:
Ccodesdir = os.path.join("GiRaFFE_staggered_1D_Tests_standalone_Ccodes/")
# First remove C code output directory if it exists
# Courtesy https://stackoverflow.com/questions/303200/how-do-i-remove-delete-a-folder-that-is-not-empty
# !rm -r ScalarWaveCurvilinear_Playground_Ccodes
shutil.rmtree(Ccodesdir, ignore_errors=True)
# Then create a fresh directory
cmd.mkdir(Ccodesdir)

# Step P3: Create executable output directory:
outdir = os.path.join(Ccodesdir,"output/")
cmd.mkdir(outdir)

# Step P5: Set timestepping algorithm (we adopt the Method of Lines)
REAL      = "double"    # Best to use double here.
default_CFL_FACTOR= 0.5 # (GETS OVERWRITTEN WHEN EXECUTED.) In pure axisymmetry (symmetry_axes = 2 below) 1.0 works fine. Otherwise 0.5 or lower.

# Step P6: Set the finite differencing order to 2.
par.set_parval_from_str("finite_difference::FD_CENTDERIVS_ORDER",4)

thismodule = "Start_to_Finish-GiRaFFE_NRPy-1D_tests"
TINYDOUBLE = par.Cparameters("REAL", thismodule, "TINYDOUBLE", 1e-100)

import GiRaFFE_NRPy.GiRaFFE_NRPy_Main_Driver_staggered as md
# par.set_paramsvals_value("GiRaFFE_NRPy.GiRaFFE_NRPy_C2P_P2C::enforce_speed_limit_StildeD = False")
par.set_paramsvals_value("GiRaFFE_NRPy.GiRaFFE_NRPy_C2P_P2C::enforce_current_sheet_prescription = False")
```

# Step 2: Output C code for GRFFE evolution \[Back to [top](#toc)\]
$$\label{grffe}$$

We will first write the C codes needed for GRFFE evolution. We have already written a module to generate all these codes and call the functions in the appropriate order, so we will import that here. We will take the slightly unusual step of doing this before we generate the initial data functions because the main driver module will register all the gridfunctions we need. It will also generate functions that, in addition to their normal spot in the MoL timestepping, will need to be called during the initial data step to make sure all the variables are appropriately filled in.

All of this is handled with a single call to `GiRaFFE_NRPy_Main_Driver_generate_all()`, which will register gridfunctions, write all the C code kernels, and write the C code functions to call those.

```python
md.GiRaFFE_NRPy_Main_Driver_generate_all(Ccodesdir)
```

    Output C function calculate_StildeD0_source_term() to file GiRaFFE_staggered_1D_Tests_standalone_Ccodes/RHSs/calculate_StildeD0_source_term.h
    Output C function calculate_StildeD1_source_term() to file GiRaFFE_staggered_1D_Tests_standalone_Ccodes/RHSs/calculate_StildeD1_source_term.h
    Output C function calculate_StildeD2_source_term() to file GiRaFFE_staggered_1D_Tests_standalone_Ccodes/RHSs/calculate_StildeD2_source_term.h
    Output C function calculate_Stilde_rhsD() to file GiRaFFE_staggered_1D_Tests_standalone_Ccodes/RHSs/calculate_Stilde_rhsD.h
    Output C function GiRaFFE_NRPy_cons_to_prims() to file GiRaFFE_staggered_1D_Tests_standalone_Ccodes/C2P/GiRaFFE_NRPy_cons_to_prims.h
    Output C function GiRaFFE_NRPy_prims_to_cons() to file GiRaFFE_staggered_1D_Tests_standalone_Ccodes/C2P/GiRaFFE_NRPy_prims_to_cons.h

## Step 2.a: Output macros for Method of Lines timestepping \[Back to [top](#toc)\]
$$\label{mol}$$

Now, we generate the code to implement the method of lines using the fourth-order Runge-Kutta algorithm.

```python
RK_method = "RK4"

# Step 3: Generate Runge-Kutta-based (RK-based) timestepping code.
#       As described above the Table of Contents, this is a 3-step process:
#       3.A: Evaluate RHSs (RHS_string)
#       3.B: Apply boundary conditions (post_RHS_string, pt 1)
import MoLtimestepping.C_Code_Generation as MoL
from MoLtimestepping.RK_Butcher_Table_Dictionary import Butcher_dict
RK_order  = Butcher_dict[RK_method][1]
cmd.mkdir(os.path.join(Ccodesdir,"MoLtimestepping/"))
MoL.MoL_C_Code_Generation(RK_method,
    RHS_string      = """
GiRaFFE_NRPy_RHSs(&params,auxevol_gfs,RK_INPUT_GFS,RK_OUTPUT_GFS);""",
    post_RHS_string = """
GiRaFFE_NRPy_post_step(&params,xx,auxevol_gfs,RK_OUTPUT_GFS,n+1);\n""",
    outdir = os.path.join(Ccodesdir,"MoLtimestepping/"))
```

# Step 3: Import `GiRaFFEfood_NRPy` initial data modules \[Back to [top](#toc)\]
$$\label{gf_id}$$

With the preliminaries out of the way, we will write the C functions to set up initial data. There are two categories of initial data that must be set: the spacetime metric variables, and the GRFFE plasma variables. We will set up the spacetime first, namely the Minkowski spacetime.

```python
gammaDD = ixp.zerorank2(DIM=3)
for i in range(3):
    for j in range(3):
        if i==j:
            gammaDD[i][j] = sp.sympify(1) # else: leave as zero
betaU = ixp.zerorank1() # All should be 0
alpha = sp.sympify(1)

# Description and options for this initial data
desc = "Generate a flat spacetime metric."
loopopts_id ="AllPoints" # we don't need to read coordinates for flat spacetime.

# For testing: Also set inverse metric:
gammaUU, unused_gammaDET = ixp.symm_matrix_inverter3x3(gammaDD)

name = "set_initial_spacetime_metric_data"
values_to_print = [
                   lhrh(lhs=gri.gfaccess("auxevol_gfs","gammaDD00"),rhs=gammaDD[0][0]),
                   lhrh(lhs=gri.gfaccess("auxevol_gfs","gammaDD01"),rhs=gammaDD[0][1]),
                   lhrh(lhs=gri.gfaccess("auxevol_gfs","gammaDD02"),rhs=gammaDD[0][2]),
                   lhrh(lhs=gri.gfaccess("auxevol_gfs","gammaDD11"),rhs=gammaDD[1][1]),
                   lhrh(lhs=gri.gfaccess("auxevol_gfs","gammaDD12"),rhs=gammaDD[1][2]),
                   lhrh(lhs=gri.gfaccess("auxevol_gfs","gammaDD22"),rhs=gammaDD[2][2]),
                   lhrh(lhs=gri.gfaccess("auxevol_gfs","betaU0"),rhs=betaU[0]),
                   lhrh(lhs=gri.gfaccess("auxevol_gfs","betaU1"),rhs=betaU[1]),
                   lhrh(lhs=gri.gfaccess("auxevol_gfs","betaU2"),rhs=betaU[2]),
                   lhrh(lhs=gri.gfaccess("auxevol_gfs","alpha"),rhs=alpha)
                  ]

outCfunction(
    outfile  = os.path.join(Ccodesdir,name+".h"), desc=desc, name=name,
    params   ="const paramstruct *params,REAL *xx[3],REAL *auxevol_gfs",
    body     = fin.FD_outputC("returnstring",values_to_print,params="outCverbose=False"),
    loopopts = loopopts_id)

```

    Output C function set_initial_spacetime_metric_data() to file GiRaFFE_staggered_1D_Tests_standalone_Ccodes/set_initial_spacetime_metric_data.h

Now, we will write out the initials data function for the GRFFE variables.

```python
initial_data_dir = os.path.join(Ccodesdir,"InitialData/")
cmd.mkdir(initial_data_dir)

ID_opts = ["AlfvenWave", "ThreeAlfvenWaves", "DegenAlfvenWave", "FastWave", "FFEBD"]

import GiRaFFEfood_NRPy.GiRaFFEfood_NRPy as gid
for initial_data in ID_opts:
    if initial_data=="AlfvenWave":
        gid.GiRaFFEfood_NRPy_generate_initial_data(ID_type = initial_data, stagger_enable = True)
        desc = "Generate Alfven wave 1D initial data for GiRaFFEfood_NRPy."
    elif initial_data=="ThreeAlfvenWaves":
        gid.GiRaFFEfood_NRPy_generate_initial_data(ID_type = "ThreeWaves", stagger_enable = True)
        desc = "Generate three Alfven wave 1D initial data for GiRaFFEfood_NRPy."
    elif initial_data=="DegenAlfvenWave":
        gid.GiRaFFEfood_NRPy_generate_initial_data(ID_type = initial_data, stagger_enable = True)
        desc = "Generate degenerate Alfven wave 1D initial data for GiRaFFEfood_NRPy."
    elif initial_data=="FastWave":
        gid.GiRaFFEfood_NRPy_generate_initial_data(ID_type = initial_data, stagger_enable = True)
        desc = "Generate fast wave 1D initial data for GiRaFFEfood_NRPy."
    elif initial_data=="FFEBD":
        gid.GiRaFFEfood_NRPy_generate_initial_data(ID_type = "FFE_Breakdown", stagger_enable = True)
        desc = "Generate FFE breakdown 1D initial data for GiRaFFEfood_NRPy."

    name = initial_data

    values_to_print = [
                       lhrh(lhs=gri.gfaccess("out_gfs","AD0"),rhs=gid.AD[0]),
                       lhrh(lhs=gri.gfaccess("out_gfs","AD1"),rhs=gid.AD[1]),
                       lhrh(lhs=gri.gfaccess("out_gfs","AD2"),rhs=gid.AD[2]),
                       lhrh(lhs=gri.gfaccess("auxevol_gfs","ValenciavU0"),rhs=gid.ValenciavU[0]),
                       lhrh(lhs=gri.gfaccess("auxevol_gfs","ValenciavU1"),rhs=gid.ValenciavU[1]),
                       lhrh(lhs=gri.gfaccess("auxevol_gfs","ValenciavU2"),rhs=gid.ValenciavU[2]),
#                        lhrh(lhs=gri.gfaccess("auxevol_gfs","BU0"),rhs=gid.BU[0]),
#                        lhrh(lhs=gri.gfaccess("auxevol_gfs","BU1"),rhs=gid.BU[1]),
#                        lhrh(lhs=gri.gfaccess("auxevol_gfs","BU2"),rhs=gid.BU[2]),
                       lhrh(lhs=gri.gfaccess("out_gfs","psi6Phi"),rhs=sp.sympify(0))
                      ]

    outCfunction(
        outfile  = os.path.join(initial_data_dir,name+".c"), desc=desc, name=name,
        params   ="const paramstruct *params, REAL *xx[3], REAL *auxevol_gfs, REAL *out_gfs",
        body     = fin.FD_outputC("returnstring",values_to_print,params="outCverbose=False"),
        rel_path_to_Cparams='../',
        loopopts ="AllPoints,Read_xxs")

inital_data_body = """

const char *option1 = "AlfvenWave";
const char *option2 = "ThreeAlfvenWaves";
const char *option3 = "DegenAlfvenWave";
const char *option4 = "FastWave";
const char *option5 = "FFEBD";

if (strcmp(initial_data_option, option1) == 0) {
    AlfvenWave(params, xx, auxevol_gfs, out_gfs);
}

else if (strcmp(initial_data_option, option2) == 0) {
    ThreeAlfvenWaves(params, xx, auxevol_gfs, out_gfs);
}

else if (strcmp(initial_data_option, option3) == 0) {
    DegenAlfvenWave(params, xx, auxevol_gfs, out_gfs);
}

else if (strcmp(initial_data_option, option4) == 0) {
    FastWave(params, xx, auxevol_gfs, out_gfs);
}

else if (strcmp(initial_data_option, option5) == 0) {
    FFEBD(params, xx, auxevol_gfs, out_gfs);
}

else {
    printf("ERROR: Invalid choice of initial data.");
    exit(1);
}

"""
name = "initial_data"
desc = "Main initial data function."
includes = ["AlfvenWave.c", "ThreeAlfvenWaves.c", "DegenAlfvenWave.c", "FastWave.c", "FFEBD.c"]
outCfunction(
    outfile  = os.path.join(initial_data_dir,name+".h"), desc=desc, name=name,
    params   ="const char *initial_data_option, const paramstruct *restrict params,REAL *xx[3],REAL *restrict auxevol_gfs,REAL *restrict out_gfs",
    body     = inital_data_body,
    includes = includes,
    prefunc="#include <string.h>",
    rel_path_to_Cparams='../',
    loopopts ="")

```

    Output C function AlfvenWave() to file GiRaFFE_staggered_1D_Tests_standalone_Ccodes/InitialData/AlfvenWave.c
    Output C function ThreeAlfvenWaves() to file GiRaFFE_staggered_1D_Tests_standalone_Ccodes/InitialData/ThreeAlfvenWaves.c
    Output C function DegenAlfvenWave() to file GiRaFFE_staggered_1D_Tests_standalone_Ccodes/InitialData/DegenAlfvenWave.c
    Output C function FastWave() to file GiRaFFE_staggered_1D_Tests_standalone_Ccodes/InitialData/FastWave.c
    Output C function FFEBD() to file GiRaFFE_staggered_1D_Tests_standalone_Ccodes/InitialData/FFEBD.c
    Output C function initial_data() to file GiRaFFE_staggered_1D_Tests_standalone_Ccodes/InitialData/initial_data.h

# Step 4: Output C codes needed for declaring and setting Cparameters; also set `free_parameters.h` \[Back to [top](#toc)\]
$$\label{cparams}$$

Based on declared NRPy+ Cparameters, first we generate `declare_Cparameters_struct.h`, `set_Cparameters_default.h`, and `set_Cparameters[-SIMD].h`.

Then we output `free_parameters.h`, which sets initial data parameters, as well as grid domain & reference metric parameters, applying `domain_size` and `sinh_width`/`SymTP_bScale` (if applicable) as set above

```python
# Step 3.e: Output C codes needed for declaring and setting Cparameters; also set free_parameters.h
# Step 3.e.i: Generate declare_Cparameters_struct.h, set_Cparameters_default.h, and set_Cparameters[-SIMD].h
par.generate_Cparameters_Ccodes(os.path.join(Ccodesdir))

# Step 3.e.ii: Set free_parameters.h
with open(os.path.join(Ccodesdir,"free_parameters.h"),"w") as file:
    file.write("""// Override parameter defaults with values based on command line arguments and NGHOSTS.
params.Nxx0 = atoi(argv[1]);
params.Nxx1 = atoi(argv[2]);
params.Nxx2 = atoi(argv[3]);
params.Nxx_plus_2NGHOSTS0 = params.Nxx0 + 2*NGHOSTS;
params.Nxx_plus_2NGHOSTS1 = params.Nxx1 + 2*NGHOSTS;
params.Nxx_plus_2NGHOSTS2 = params.Nxx2 + 2*NGHOSTS;
// Step 0d: Set up space and time coordinates
// Step 0d.i: Declare \Delta x^i=dxx{0,1,2} and invdxx{0,1,2}, as well as xxmin[3] and xxmax[3]:
const REAL xxmin[3] = {-1.3255,-0.085,-0.085};
const REAL xxmax[3] = { 1.6745, 0.115, 0.115};

params.dxx0 = (xxmax[0] - xxmin[0]) / ((REAL)params.Nxx0+1);
params.dxx1 = (xxmax[1] - xxmin[1]) / ((REAL)params.Nxx1+1);
params.dxx2 = (xxmax[2] - xxmin[2]) / ((REAL)params.Nxx2+1);
printf("dxx0,dxx1,dxx2 = %.5e,%.5e,%.5e\\n",params.dxx0,params.dxx1,params.dxx2);
params.invdx0 = 1.0 / params.dxx0;
params.invdx1 = 1.0 / params.dxx1;
params.invdx2 = 1.0 / params.dxx2;

const int poison_grids = 0;
// Standard GRFFE parameters:
params.GAMMA_SPEED_LIMIT = 2000.0;
params.diss_strength = 0.1;
""")

```

# Step 4: Set up boundary condition functions for chosen singular, curvilinear coordinate system \[Back to [top](#toc)\]
$$\label{bc_functs}$$

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

...But, for the moment, we're actually just using this because it writes the file `gridfunction_defines.h`.

```python
import CurviBoundaryConditions.CurviBoundaryConditions as cbcs
cbcs.Set_up_CurviBoundaryConditions(os.path.join(Ccodesdir,"boundary_conditions/"),Cparamspath=os.path.join("../"),enable_copy_of_static_Ccodes=False)
```

    Wrote to file "GiRaFFE_staggered_1D_Tests_standalone_Ccodes/boundary_conditions/parity_conditions_symbolic_dot_products.h"
    Evolved parity: ( AD0:1, AD1:2, AD2:3, StildeD0:1, StildeD1:2, StildeD2:3,
        psi6Phi:0 )

    AuxEvol parity: ( BU0:1, BU1:2, BU2:3, B_lU0:1, B_lU1:2, B_lU2:3, B_rU0:1,
        B_rU1:2, B_rU2:3, BstaggerU0:1, BstaggerU1:2, BstaggerU2:3,
        Bstagger_lU0:1, Bstagger_lU1:2, Bstagger_lU2:3, Bstagger_rU0:1,
        Bstagger_rU1:2, Bstagger_rU2:3, Stilde_flux_HLLED0:1,
        Stilde_flux_HLLED1:2, Stilde_flux_HLLED2:3, ValenciavU0:1,
        ValenciavU1:2, ValenciavU2:3, Valenciav_lU0:1, Valenciav_lU1:2,
        Valenciav_lU2:3, Valenciav_llU0:1, Valenciav_llU1:2, Valenciav_llU2:3,
        Valenciav_lrU0:1, Valenciav_lrU1:2, Valenciav_lrU2:3, Valenciav_rU0:1,
        Valenciav_rU1:2, Valenciav_rU2:3, Valenciav_rlU0:1, Valenciav_rlU1:2,
        Valenciav_rlU2:3, Valenciav_rrU0:1, Valenciav_rrU1:2, Valenciav_rrU2:3,
        alpha:0, alpha_face:0, betaU0:1, betaU1:2, betaU2:3, beta_faceU0:1,
        beta_faceU1:2, beta_faceU2:3, cmax_x:0, cmax_y:0, cmax_z:0, cmin_x:0,
        cmin_y:0, cmin_z:0, gammaDD00:4, gammaDD01:5, gammaDD02:6, gammaDD11:7,
        gammaDD12:8, gammaDD22:9, gamma_faceDD00:4, gamma_faceDD01:5,
        gamma_faceDD02:6, gamma_faceDD11:7, gamma_faceDD12:8, gamma_faceDD22:9,
        psi6_temp:0, psi6center:0 )
    Wrote to file "GiRaFFE_staggered_1D_Tests_standalone_Ccodes/boundary_conditions/EigenCoord_Cart_to_xx.h"

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

```python
# Part P0: Define REAL, set the number of ghost cells NGHOSTS (from NRPy+'s FD_CENTDERIVS_ORDER),
#          and set the CFL_FACTOR (which can be overwritten at the command line)

with open(os.path.join(Ccodesdir,"GiRaFFE_NRPy_REAL__NGHOSTS__CFL_FACTOR.h"), "w") as file:
    file.write("""
// Part P0.a: Set the number of ghost cells, from NRPy+'s FD_CENTDERIVS_ORDER
#define NGHOSTS """+str(3)+"""
#define NGHOSTS_A2B """+str(2)+"""
// Part P0.b: Set the numerical precision (REAL) to double, ensuring all floating point
//            numbers are stored to at least ~16 significant digits
#define REAL """+REAL+"""
// Part P0.c: Set the CFL Factor. Can be overwritten at command line.
REAL CFL_FACTOR = """+str(default_CFL_FACTOR)+";")
```

```python
%%writefile $Ccodesdir/GiRaFFE_NRPy_standalone.c
// Step P0: Define REAL and NGHOSTS; and declare CFL_FACTOR. This header is generated in NRPy+.
#include "GiRaFFE_NRPy_REAL__NGHOSTS__CFL_FACTOR.h"

#include "declare_Cparameters_struct.h"

const int NSKIP_1D_OUTPUT = 1;

// Step P1: Import needed header files
#include "stdio.h"
#include "stdlib.h"
#include "math.h"
#include "time.h"
#include "stdint.h" // Needed for Windows GCC 6.x compatibility
#ifndef M_PI
#define M_PI 3.141592653589793238462643383279502884L
#endif
#ifndef M_SQRT1_2
#define M_SQRT1_2 0.707106781186547524400844362104849039L
#endif

// Step P2: Declare the IDX4S(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_2NGHOSTS0 elements in memory. Similarly, consecutive values of
//           "k" are separated by Nxx_plus_2NGHOSTS0*Nxx_plus_2NGHOSTS1 in memory, etc.
#define IDX4S(g,i,j,k) \
( (i) + Nxx_plus_2NGHOSTS0 * ( (j) + Nxx_plus_2NGHOSTS1 * ( (k) + Nxx_plus_2NGHOSTS2 * (g) ) ) )
#define IDX4ptS(g,idx) ( (idx) + (Nxx_plus_2NGHOSTS0*Nxx_plus_2NGHOSTS1*Nxx_plus_2NGHOSTS2) * (g) )
#define IDX3S(i,j,k) ( (i) + Nxx_plus_2NGHOSTS0 * ( (j) + Nxx_plus_2NGHOSTS1 * ( (k) ) ) )
#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++)
#define LOOP_ALL_GFS_GPS(ii) _Pragma("omp parallel for") \
  for(int (ii)=0;(ii)<Nxx_plus_2NGHOSTS_tot*NUM_EVOL_GFS;(ii)++)

// Step P3: Set gridfunction macros
#include "boundary_conditions/gridfunction_defines.h"

// Step P4: Include the RHS, BC, and primitive recovery functions
#include "GiRaFFE_NRPy_Main_Driver.h"

// Step P5: Include the initial data functions
#include "set_initial_spacetime_metric_data.h"
#include "InitialData/initial_data.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[]) {
    paramstruct params;
#include "set_Cparameters_default.h"
    // Step 0a: Read command-line input, error out if nonconformant
    if(argc != 5 || atoi(argv[1]) < NGHOSTS || atoi(argv[2]) < NGHOSTS || atoi(argv[3]) < NGHOSTS) {
      printf("Error: Expected three command-line arguments: ./GiRaFFE_NRPy_standalone [Nx] [Ny] [Nz],\n");
      printf("where Nx is the number of grid points in the x direction, and so forth.\n");
      printf("Nx,Ny,Nz MUST BE larger than NGHOSTS (= %d)\n",NGHOSTS);
      exit(1);
    }
    // Step 0c: Set free parameters, overwriting Cparameters defaults
    //          by hand or with command-line input, as desired.
#include "free_parameters.h"
#include "set_Cparameters-nopointer.h"

    //          ... and then set up the numerical grid structure in time:
    const REAL t_final = 2.0;
    const REAL CFL_FACTOR = 0.5; // Set the CFL Factor

    // Step 0c: Allocate memory for gridfunctions
    const int Nxx_plus_2NGHOSTS_tot = Nxx_plus_2NGHOSTS0*Nxx_plus_2NGHOSTS1*Nxx_plus_2NGHOSTS2;
    // Step 0k: Allocate memory for gridfunctions
#include "MoLtimestepping/RK_Allocate_Memory.h"
    REAL *restrict auxevol_gfs = (REAL *)malloc(sizeof(REAL) * NUM_AUXEVOL_GFS * Nxx_plus_2NGHOSTS_tot);
    REAL *evol_gfs_exact = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot);
    REAL *auxevol_gfs_exact  = (REAL *)malloc(sizeof(REAL) * NUM_AUXEVOL_GFS * Nxx_plus_2NGHOSTS_tot);

    // For debugging, it can be useful to set everything to NaN initially.
    if(poison_grids) {
        for(int ii=0;ii<NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot;ii++) {
            y_n_gfs[ii] = 1.0/0.0;
            y_nplus1_running_total_gfs[ii] = 1.0/0.0;
            //k_odd_gfs[ii] = 1.0/0.0;
            //k_even_gfs[ii] = 1.0/0.0;
            diagnostic_output_gfs[ii] = 1.0/0.0;
            evol_gfs_exact[ii] = 1.0/0.0;
        }
        for(int ii=0;ii<NUM_AUXEVOL_GFS * Nxx_plus_2NGHOSTS_tot;ii++) {
            auxevol_gfs[ii] = 1.0/0.0;
            auxevol_gfs_exact[ii] = 1.0/0.0;
        }
    }

    // Step 0d: Set up coordinates: Set dx, and then dt based on dx_min and CFL condition
    // This is probably already defined above, but just in case...
    #ifndef MIN
    #define MIN(A, B) ( ((A) < (B)) ? (A) : (B) )
    #endif
    REAL dt = CFL_FACTOR * MIN(dxx0,MIN(dxx1,dxx2)); // CFL condition
    int Nt = (int)(t_final / dt + 0.5); // The number of points in time.
                                        //Add 0.5 to account for C rounding down integers.

    // Step 0e: Set up cell-centered Cartesian coordinate grids
    REAL *xx[3];
    xx[0] = (REAL *)malloc(sizeof(REAL)*Nxx_plus_2NGHOSTS0);
    xx[1] = (REAL *)malloc(sizeof(REAL)*Nxx_plus_2NGHOSTS1);
    xx[2] = (REAL *)malloc(sizeof(REAL)*Nxx_plus_2NGHOSTS2);
    for(int j=0;j<Nxx_plus_2NGHOSTS0;j++) xx[0][j] = xxmin[0] + (j-NGHOSTS+1)*dxx0;
    for(int j=0;j<Nxx_plus_2NGHOSTS1;j++) xx[1][j] = xxmin[1] + (j-NGHOSTS+1)*dxx1;
    for(int j=0;j<Nxx_plus_2NGHOSTS2;j++) xx[2][j] = xxmin[2] + (j-NGHOSTS+1)*dxx2;

    // Step 1: Set up initial data to be exact solution at time=0:
    REAL time = 0.0;
    set_initial_spacetime_metric_data(&params, xx, auxevol_gfs);
    const char *initial_data_option = argv[4];
    initial_data(initial_data_option, &params, xx, auxevol_gfs, y_n_gfs);

    // Fill in  the remaining quantities
    GiRaFFE_compute_B_and_Bstagger_from_A(&params,
                                          auxevol_gfs+Nxx_plus_2NGHOSTS_tot*GAMMADD00GF,
                                          auxevol_gfs+Nxx_plus_2NGHOSTS_tot*GAMMADD01GF,
                                          auxevol_gfs+Nxx_plus_2NGHOSTS_tot*GAMMADD02GF,
                                          auxevol_gfs+Nxx_plus_2NGHOSTS_tot*GAMMADD11GF,
                                          auxevol_gfs+Nxx_plus_2NGHOSTS_tot*GAMMADD12GF,
                                          auxevol_gfs+Nxx_plus_2NGHOSTS_tot*GAMMADD22GF,
                                          auxevol_gfs+Nxx_plus_2NGHOSTS_tot*PSI6_TEMPGF, /* Temporary storage,overwritten */
                                          y_n_gfs+Nxx_plus_2NGHOSTS_tot*AD0GF,
                                          y_n_gfs+Nxx_plus_2NGHOSTS_tot*AD1GF,
                                          y_n_gfs+Nxx_plus_2NGHOSTS_tot*AD2GF,
                                          auxevol_gfs+Nxx_plus_2NGHOSTS_tot*BU0GF,
                                          auxevol_gfs+Nxx_plus_2NGHOSTS_tot*BU1GF,
                                          auxevol_gfs+Nxx_plus_2NGHOSTS_tot*BU2GF,
                                          auxevol_gfs+Nxx_plus_2NGHOSTS_tot*BSTAGGERU0GF,
                                          auxevol_gfs+Nxx_plus_2NGHOSTS_tot*BSTAGGERU1GF,
                                          auxevol_gfs+Nxx_plus_2NGHOSTS_tot*BSTAGGERU2GF);
    //override_BU_with_old_GiRaFFE(&params,auxevol_gfs,0);
    GiRaFFE_NRPy_prims_to_cons(&params,auxevol_gfs,y_n_gfs);
    // Extra stack, useful for debugging:
    GiRaFFE_NRPy_cons_to_prims(&params,xx,auxevol_gfs,y_n_gfs);

    for(int n=0;n<=Nt;n++) { // Main loop to progress forward in time.
    //for(int n=0;n<=1;n++) { // Main loop to progress forward in time.
        // Step 1a: Set current time to correct value & compute exact solution
        time = ((REAL)n)*dt;

        /* Step 2: Validation: Output relative error between numerical and exact solution, */
        //if(time == 0.0 || time == 0.5 || time == 1.0 || time == 2.0 || time == 0.02 || time == 0.56) {
        if(1) {
            // Step 2c: Output relative error between exact & numerical at center of grid.
            const int i0mid=Nxx_plus_2NGHOSTS0/2;
            const int i1mid=Nxx_plus_2NGHOSTS1/2;
            const int i2mid=Nxx_plus_2NGHOSTS2/2;
            char filename[100];
            sprintf(filename,"out%d__%s-%08d.txt", Nxx0, initial_data_option, n);
            FILE *out2D = fopen(filename, "w");
            for(int i0=0;i0<Nxx_plus_2NGHOSTS0;i0++) {
                const int idx = IDX3S(i0,i1mid,i2mid);
                fprintf(out2D,"%.16e %.16e %.16e %.16e %.16e %.16e %.16e %.16e %.16e %.16e %.16e %.16e %.16e %.16e %.16e\n",
                        xx[0][i0],
                        auxevol_gfs[IDX4ptS(BU0GF,idx)],auxevol_gfs[IDX4ptS(BU1GF,idx)],auxevol_gfs[IDX4ptS(BU2GF,idx)],
                        y_n_gfs[IDX4ptS(AD0GF,idx)],y_n_gfs[IDX4ptS(AD1GF,idx)],y_n_gfs[IDX4ptS(AD2GF,idx)],
                        y_n_gfs[IDX4ptS(STILDED0GF,idx)],y_n_gfs[IDX4ptS(STILDED1GF,idx)],y_n_gfs[IDX4ptS(STILDED2GF,idx)],
                        auxevol_gfs[IDX4ptS(VALENCIAVU0GF,idx)],auxevol_gfs[IDX4ptS(VALENCIAVU1GF,idx)],auxevol_gfs[IDX4ptS(VALENCIAVU2GF,idx)],
                        y_n_gfs[IDX4ptS(PSI6PHIGF,idx)], time);
            }
            fclose(out2D);
            // For convergence testing, we'll shift the grid x -> x-1 and output initial data again, giving the exact solution.
            LOOP_REGION(0,Nxx_plus_2NGHOSTS0,0,1,0,1) {
                xx[0][i0] += -mu_AW*time;
                //xx[0][i0] += -time;
            }
            set_initial_spacetime_metric_data(&params,xx,auxevol_gfs_exact);
            initial_data(initial_data_option, &params,xx,auxevol_gfs_exact,evol_gfs_exact);
            // Fill in  the remaining quantities
            GiRaFFE_compute_B_and_Bstagger_from_A(&params,
                                                  auxevol_gfs_exact+Nxx_plus_2NGHOSTS_tot*GAMMADD00GF,
                                                  auxevol_gfs_exact+Nxx_plus_2NGHOSTS_tot*GAMMADD01GF,
                                                  auxevol_gfs_exact+Nxx_plus_2NGHOSTS_tot*GAMMADD02GF,
                                                  auxevol_gfs_exact+Nxx_plus_2NGHOSTS_tot*GAMMADD11GF,
                                                  auxevol_gfs_exact+Nxx_plus_2NGHOSTS_tot*GAMMADD12GF,
                                                  auxevol_gfs_exact+Nxx_plus_2NGHOSTS_tot*GAMMADD22GF,
                                                  auxevol_gfs_exact+Nxx_plus_2NGHOSTS_tot*PSI6_TEMPGF, /* Temporary storage,overwritten */
                                                  evol_gfs_exact+Nxx_plus_2NGHOSTS_tot*AD0GF,
                                                  evol_gfs_exact+Nxx_plus_2NGHOSTS_tot*AD1GF,
                                                  evol_gfs_exact+Nxx_plus_2NGHOSTS_tot*AD2GF,
                                                  auxevol_gfs_exact+Nxx_plus_2NGHOSTS_tot*BU0GF,
                                                  auxevol_gfs_exact+Nxx_plus_2NGHOSTS_tot*BU1GF,
                                                  auxevol_gfs_exact+Nxx_plus_2NGHOSTS_tot*BU2GF,
                                                  auxevol_gfs_exact+Nxx_plus_2NGHOSTS_tot*BSTAGGERU0GF,
                                                  auxevol_gfs_exact+Nxx_plus_2NGHOSTS_tot*BSTAGGERU1GF,
                                                  auxevol_gfs_exact+Nxx_plus_2NGHOSTS_tot*BSTAGGERU2GF);
            GiRaFFE_NRPy_prims_to_cons(&params,auxevol_gfs_exact,evol_gfs_exact);
            // And now, we'll set the grid back to rights.
            LOOP_REGION(0,Nxx_plus_2NGHOSTS0,0,1,0,1) {
                xx[0][i0] -= -mu_AW*time;
                //xx[0][i0] -= -time;
            }
            sprintf(filename,"out%d-%08d_exact.txt",Nxx0,n);
            FILE *out2D_exact = fopen(filename, "w");
            for(int i0=0;i0<Nxx_plus_2NGHOSTS0;i0++) {
                const int idx = IDX3S(i0,i1mid,i2mid);
                fprintf(out2D_exact,"%.16e %.16e %.16e %.16e %.16e %.16e %.16e %.16e %.16e %.16e %.16e %.16e %.16e %.16e\n",
                        xx[0][i0],
                        auxevol_gfs_exact[IDX4ptS(BU0GF,idx)],auxevol_gfs_exact[IDX4ptS(BU1GF,idx)],auxevol_gfs_exact[IDX4ptS(BU2GF,idx)],
                        evol_gfs_exact[IDX4ptS(AD0GF,idx)],evol_gfs_exact[IDX4ptS(AD1GF,idx)],evol_gfs_exact[IDX4ptS(AD2GF,idx)],
                        evol_gfs_exact[IDX4ptS(STILDED0GF,idx)],evol_gfs_exact[IDX4ptS(STILDED1GF,idx)],evol_gfs_exact[IDX4ptS(STILDED2GF,idx)],
                        auxevol_gfs_exact[IDX4ptS(VALENCIAVU0GF,idx)],auxevol_gfs_exact[IDX4ptS(VALENCIAVU1GF,idx)],auxevol_gfs_exact[IDX4ptS(VALENCIAVU2GF,idx)],
                        evol_gfs_exact[IDX4ptS(PSI6PHIGF,idx)]);
            }
            fclose(out2D_exact);

        }

        // Step 3: Evolve scalar wave initial data forward in time using Method of Lines with RK4 algorithm,
        //         applying quadratic extrapolation outer boundary conditions.
        // Step 3.b: Step forward one timestep (t -> t+dt) in time using
        //           chosen RK-like MoL timestepping algorithm
#include "MoLtimestepping/RK_MoL.h"
    } // End main loop to progress forward in time.

    // Step 4: Free all allocated memory
#include "MoLtimestepping/RK_Free_Memory.h"
    free(auxevol_gfs);
    free(auxevol_gfs_exact);
    free(evol_gfs_exact);
    for(int i=0;i<3;i++) free(xx[i]);
    return 0;
}
```

    Writing GiRaFFE_staggered_1D_Tests_standalone_Ccodes//GiRaFFE_NRPy_standalone.c

# Step 6: Compile generated C codes & perform GRFFE simulations \[Back to [top](#toc)\]
$$\label{compileexec}$$

To aid in the cross-platform-compatible (with Windows, MacOS, & Linux) compilation and execution, we make use of `cmdline_helper` [(**Tutorial**)](Tutorial-cmdline_helper.ipynb).

```python
cmd.C_compile(os.path.join(Ccodesdir,"GiRaFFE_NRPy_standalone.c"),
              os.path.join(Ccodesdir,"output","GiRaFFE_NRPy_standalone"),compile_mode="optimized")

# Change to output directory
os.chdir(outdir)
# Clean up existing output files
cmd.delete_existing_files("out*.txt")
cmd.delete_existing_files("out*.png")

# ID options are: "AlfvenWave", "ThreeAlfvenWaves", "DegenAlfvenWave", "FastWave", "FFEBD"
# for opt in ID_opts:
opt = "AlfvenWave"
cmd.Execute("GiRaFFE_NRPy_standalone", "299 4 4 "+opt, "out_298"+opt+".txt")
#     cmd.Execute("GiRaFFE_NRPy_standalone", "1280 9 9 "+opt, "out_1280"+opt+".txt")
#     cmd.Execute("GiRaFFE_NRPy_standalone", "1280 32 32 "+opt, "out_"+opt+".txt")

# cmd.Execute("GiRaFFE_NRPy_standalone", "149 9 9 AlfvenWave","out149.txt")
# Return to root directory
os.chdir(os.path.join("../../"))
```

    Compiling executable...
    (EXEC): Executing `gcc -std=gnu99 -Ofast -fopenmp -march=native -funroll-loops GiRaFFE_staggered_1D_Tests_standalone_Ccodes/GiRaFFE_NRPy_standalone.c -o GiRaFFE_staggered_1D_Tests_standalone_Ccodes/output/GiRaFFE_NRPy_standalone -lm`...
    (BENCH): Finished executing in 3.2181918621063232 seconds.
    Finished compilation.
    (EXEC): Executing `taskset -c 0,1,2,3 ./GiRaFFE_NRPy_standalone 299 4 4 AlfvenWave`...
    (BENCH): Finished executing in 18.254799842834473 seconds.

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

Now we plot the data and recreate figure 1 from the [GiRaFFE paper](https://arxiv.org/pdf/1704.00599.pdf). We reconstruct the electric field via

$$
E_i = -\epsilon_{ijk}v^j B^k
$$

the `calc_E` function below. We also calculate the FFE condition $B^2 - E^2$ below using the `calc_Bsquared_minus_Esquared` function.

```python
eDDD = ixp.LeviCivitaSymbol_dim3_rank3()
def calc_E(data):
    VU0 = data[:, 10]
    VU1 = data[:, 11]
    VU2 = data[:, 12]
    BU0 = data[:, 1]
    BU1 = data[:, 2]
    BU2 = data[:, 3]

    VU = [VU0, VU1, VU2]
    BU = [BU0, BU1, BU2]
    ED = np.zeros((VU0.size, 3))
    for i in range(3):
        for j in range(3):
            for k in range(3):
                ED[:,i] = ED[:,i] - eDDD[i][j][k]*VU[j]*BU[k]
    return ED

def calc_Bsquared_minus_Esquared(data):
    EU = calc_E(data)
    BU0 = data[:, 1]
    BU1 = data[:, 2]
    BU2 = data[:, 3]
    return (BU0**2 + BU1**2 + BU2**2) - (EU[:,0]**2 + EU[:,1]**2 + EU[:,2]**2)

```

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

#          0   1     2     3     4     5     6     7          8          9          10            11            12             13
labels = ["x","BU0","BU1","BU2","AD0","AD1","AD2","StildeD0","StildeD1","StildeD2","ValenciavU0","ValenciavU1","ValenciavU2", "psi6Phi"]
fig = plt.figure(figsize=(6, 15))

# spec = mpl.gridspec.GridSpec(ncols=6, nrows=2,wspace=0.65, hspace=0.4) # 6 columns evenly divides both 2 & 3
# ax1 = fig.add_subplot(spec[0,0:2]) # row 0 with axes spanning 2 cols on evens
# ax2 = fig.add_subplot(spec[0,2:4])
# ax3 = fig.add_subplot(spec[0,4:])
# ax4 = fig.add_subplot(spec[1,1:3]) # row 0 with axes spanning 2 cols on odds
# ax5 = fig.add_subplot(spec[1,3:5])

gs = gridspec.GridSpec(nrows=5, ncols=1, hspace=0.5)
ax1 = fig.add_subplot(gs[0, 0])
ax2 = fig.add_subplot(gs[1, 0])
ax3 = fig.add_subplot(gs[2, 0])
ax4 = fig.add_subplot(gs[3, 0])
ax5 = fig.add_subplot(gs[4, 0])

Data_num_Fast_A = np.loadtxt(os.path.join(Ccodesdir,"output","out299__FastWave-00000000.txt"))
Data_num_Fast_B = np.loadtxt(os.path.join(Ccodesdir,"output","out299__FastWave-00000100.txt"))

E_Fast_A = calc_E(Data_num_Fast_A)
E_Fast_B = calc_E(Data_num_Fast_B)

ax1.scatter(Data_num_Fast_A[:,0], np.abs(E_Fast_A[:,2]), s=1,label = 't = 0')
ax1.plot(Data_num_Fast_B[:,0], np.abs(E_Fast_B[:,2]), 'k-', label = 't = 0.5')
ax1.set_xlim(-0.5, 1.5)
ax1.set_ylim(0.6)
ax1.text(0.95, 0.01, 'Fast Wave',
        verticalalignment='bottom', horizontalalignment='right',
        transform=ax1.transAxes,
        color='black', fontsize=14)
ax1.set_xlabel('x')
ax1.set_ylabel(r'$|E^z|$')
ax1.legend()

Data_num_Alf_A = np.loadtxt(os.path.join(Ccodesdir,"output","out299__AlfvenWave-00000000.txt"))
Data_num_Alf_B = np.loadtxt(os.path.join(Ccodesdir,"output","out299__AlfvenWave-00000400.txt"))

ax2.scatter(Data_num_Alf_A[:,0], Data_num_Alf_A[:,3], s=1, label = 't = 0')
ax2.plot(Data_num_Alf_B[:,0], Data_num_Alf_B[:,3], 'k-', label = 't = 2.0')
ax2.set_xlim(-1.5, 1.5)
ax2.set_ylim(1.1)
ax2.text(0.95, 0.01, 'Alfven Wave',
        verticalalignment='bottom', horizontalalignment='right',
        transform=ax2.transAxes,
        color='black', fontsize=14)
ax2.set_xlabel('x')
ax2.set_ylabel(r'$B^z$')
ax2.legend(loc='center right')

Data_num_DegenAlf_A = np.loadtxt(os.path.join(Ccodesdir,"output","out299__DegenAlfvenWave-00000000.txt"))
Data_num_DegenAlf_B = np.loadtxt(os.path.join(Ccodesdir,"output","out299__DegenAlfvenWave-00000200.txt"))

E_DegenAlf_A = calc_E(Data_num_DegenAlf_A)
E_DegenAlf_B = calc_E(Data_num_DegenAlf_B)

ax3.scatter(Data_num_DegenAlf_A[:,0], E_DegenAlf_A[:,1], s=1, label = 't = 0')
ax3.plot(Data_num_DegenAlf_B[:,0], E_DegenAlf_B[:,1], 'k-', label = 't = 1.0')
ax3.set_xlim(-1.5, 1.5)
ax3.set_ylim(-1.35)
ax3.text(0.95, 0.01, 'Deg. Alfven Wave',
        verticalalignment='bottom', horizontalalignment='right',
        transform=ax3.transAxes,
        color='black', fontsize=14)
ax3.set_xlabel('x')
ax3.set_ylabel(r'$E^y$')
ax3.legend()

# Data_num_ThreeAlf_A = np.loadtxt(os.path.join(Ccodesdir,"output","out149__ThreeAlfvenWaves-00000000.txt"))
Data_num_ThreeAlf_B = np.loadtxt(os.path.join(Ccodesdir,"output","out299__ThreeAlfvenWaves-00000112.txt"))

# ax2.plot(Data_num_ThreeAlf_A[:,0], Data_num_ThreeAlf_A[:,2], 'k-')
ax4.scatter(Data_num_ThreeAlf_B[:,0], Data_num_ThreeAlf_B[:,2], s=1, label = 't = 0.56')
ax4.set_xlim(-1.0, 1.0)
# ax4.set_ylim()
ax4.text(0.95, 0.01, 'Three Waves',
        verticalalignment='bottom', horizontalalignment='right',
        transform=ax4.transAxes,
        color='black', fontsize=14)
ax4.set_xlabel('x')
ax4.set_ylabel(r'$B^y$')
ax4.legend(loc='center')

Data_num_FFEBD_A = np.loadtxt(os.path.join(Ccodesdir,"output","out299__FFEBD-00000000.txt"))
Data_num_FFEBD_B = np.loadtxt(os.path.join(Ccodesdir,"output","out299__FFEBD-00000004.txt"))

B2mE2_A = calc_Bsquared_minus_Esquared(Data_num_FFEBD_A)
B2mE2_B = calc_Bsquared_minus_Esquared(Data_num_FFEBD_B)

ax5.scatter(Data_num_FFEBD_A[:,0], B2mE2_A, s=1, label = 't = 0')
ax5.plot(Data_num_FFEBD_B[:,0], B2mE2_B, 'k-', label = 't = 0.02')
ax5.set_xlim(-0.4, 0.6)
ax5.text(0.95, 0.01, 'FFE Breakdown',
        verticalalignment='bottom', horizontalalignment='right',
        transform=ax5.transAxes,
        color='black', fontsize=14)
ax5.set_xlabel('x')
ax5.set_ylabel(r'$B^2 - E^2$')
ax5.legend()

plt.savefig(os.path.join(Ccodesdir,"output","NRPy-GiRaFFE"), dpi=800, bbox_inches="tight")
plt.close(fig)

```

```python
img1 = plt.imread(os.path.join(Ccodesdir,"output","NRPy-GiRaFFE.png"))
img2 =  plt.imread(os.path.join("GiRaFFE_NRPy/example_par_files/figure1_GiRaFFE_paper.png"))

NUM_ROWS = 1
IMGs_IN_ROW = 2
f, ax = plt.subplots(NUM_ROWS, IMGs_IN_ROW, figsize=(28,18))
plt.subplots_adjust(wspace=0.05)
plt.axis('off')

ax[0].imshow(img1)
ax[1].imshow(img2)

ax[0].set_title('image 1')
ax[1].set_title('image 2')

# title = 'side by side view of images'
# f.suptitle(title, fontsize=16)
plt.tight_layout()
# plt.xticks([])
# plt.yticks([])
plt.show()
```

```python
# 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

cmd.delete_existing_files(os.path.join(Ccodesdir,'output',"out299*.png"))
globby = glob.glob(os.path.join(Ccodesdir,'output','out299-*.txt'))
file_list_exact = []
for x in sorted(globby):
    file_list_exact.append(x)
globby = glob.glob(os.path.join(Ccodesdir,'output','out299__AlfvenWave*.txt'))
file_list_approx = []
for x in sorted(globby):
    file_list_approx.append(x)

number_of_files = int(len(file_list_exact))

for timestep in range(number_of_files):
    fig = plt.figure()
    numer_filename = file_list_approx[timestep]
    exact_filename = file_list_exact[timestep]
    Numer = np.loadtxt(numer_filename)
    Exact = np.loadtxt(exact_filename)

    plt.title("Alfvén Wave")
    plt.xlabel("x")
    plt.ylabel("$B^z$")
#     plt.xlim(-0.5,0.5)
#     plt.ylim(1.0,1.7)

    plt.plot(Numer[:,0],Numer[:,3],'.',label="Numerical")
    plt.plot(Exact[:,0],Exact[:,3],label="Exact")
    plt.legend()
    savefig(numer_filename+".png",dpi=150)
    plt.close(fig)
    sys.stdout.write("%c[2K" % 27)
    sys.stdout.write("Processing file "+numer_filename+"\r")
    sys.stdout.flush()
```

    Processing file GiRaFFE_staggered_1D_Tests_standalone_Ccodes/output/out299__AlfvenWave-00000400.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
# !rm -f GiRaFFE_NRPy-1D_tests.mp4
cmd.delete_existing_files("GiRaFFE_NRPy-1D_tests.mp4")

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

myimages = []
from math import floor
for i in range(floor(number_of_files/6)):
    img = mgimg.imread(file_list_approx[6*i]+".png")
    imgplot = plt.imshow(img)
    myimages.append([imgplot])

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

```python
%%HTML

```

# Step 8: 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-Start_to_Finish-GiRaFFE_NRPy-1D_tests-staggered.pdf](Tutorial-Start_to_Finish-GiRaFFE_NRPy-1D_tests-staggered.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-Start_to_Finish-GiRaFFE_NRPy-1D_tests-staggered",location_of_template_file=os.path.join(".."))
```


# Numerical Solution of the Schrödinger Equation Using a Finite Difference, Finite Element and Neural Network Approach

Author: Ante Lojic Kapetanovic

Course: Modern Physics [FEMT08](https://nastava.fesb.unist.hr/nastava/predmeti/11624) taught by professor Ivica Puljak

Date: 2020, 26 Mar

## Content

* [Content](#Content)

* [Theory behind...](#Theory-behind...)
    * [1 Introduction to Quantum Mechanics](#1-Introduction-to-Quantum-Mechanics)

    * [2 Schrödinger Equation](#2-Schrödinger-Equation)

    * [3 The Use of Wave Equation](#3-The-Use-of-Wave-Equation)

* [Numerical solution of the Schrödinger equation](#Numerical-solution-of-the-Schrödinger-equation)
    * [1 Finite difference method](#1-Finite-difference-method)

    * [2 Finite element method](#2-Finite-element-method)

    * [3 Artifical neural network method](#3-Artifical-neural-network-method)
* [Results, discussion and conclusion](#Results,-discussion-and-conclussion)

## Theory behind...

### 1 Introduction to Quantum Mechanics

One of the greatest modern-science successes surely has to be credited to sir **Isac Newton** for his 1687. work titled *Philosophiæ Naturalis Principia Mathematica*. Immense knowledge of physics and mechanics accumulated over the centuries was beautifully summed up in three Newton laws. Those laws served as the foundation to many discoveries that will occur for few centuries to come.
Also, great contribution in electricity and magnetism came from **Oersted**, **Faraday**, **Ampere**, **Henry** and **Ohm**. But not until **James Clerk Maxwell** created theoretical synthesis of electricity and magnetism in his 1867. work titled *Treatise on Electricity and Magnetism*, the electromagnetism is understood as an indivisible phenomenon.
The progress in 19th century was so huge that, to some, it seemed impossible there is anything fundamentally new to discover.

However, the idea of (electromagnetic) waves propagating through vacuum was a mistery; also the black body radiation and the photoelectric effect were imposible to explain using the classical mechanics and Maxwell's classical electromagnetism.

**Max Planck** had made the great first step into the creation of quantum hypothesis in 1900. Planck introduced the idea of discretized quantum oscilators that are able to radiate energy at certain discrete levels in order to explain black body radiation. Radiated energy was described using the frequency at which the source oscilates, multiplied by the extremely small constant, later known as the Planck constant $h$, mathematically formulated in the following expression:

$$
\begin{equation}
    B(\nu, T) = \frac{ 2 h \nu^3}{c^2} \frac{1}{e^\frac{h\nu}{k_B T} - 1}
\end{equation}
$$

where $\nu$ is the frequency at which the source oscilates, T is the temperature, $k_B$ is the Boltzmann constant and $h$ is the Plank constant. $B(\nu, T)$ is the intensity of black body radiation, which can also be expressed using wavelength, $\lambda$, instead of frequency, $\nu$:

$$
\begin{equation}
    B(\lambda, T) =\frac{2hc^2}{\lambda^5}\frac{1}{ e^{\frac{hc}{\lambda k_B T}} - 1}
\end{equation}
$$

Code for visual comparisson between the Planck's interpretation and the classical Rayleigh-Jeans interpretation of black body radiation is in the `black_body_radiation.py` script.

```python
%matplotlib inline
%run src/scripts/black_body_radiation.py
```

Young physicist **Albert Einstein** in 1905 using the Planck's hypothesis was able to explain the photoelectric effect.
With 1913 **Bohr** atom model it was obvious that the quantum hypothesis reflects low level law of the nature and the modern physics was conceived.

Even though the quantum hypothesis was able to explain some of the problems classical mechanics and physics in general had failed to explain, a lot of mysteries were yet to be unraveled. The wave-particle duality of nature by **Louis de Broglie** captured through the expression:

$$
\begin{equation}
\lambda = \frac{h}{p}
\end{equation}
$$

where $\lambda$ is the wavelength, $h$ is the Planck constant and $p$ is the momentum of a particle, was the first step into a more general but also more complex way to explain the quantum hypothesis.

The rather unusual De Broigle's hypothesis, presented in his 1924 PhD thesis, was soon after experimentally proved by **Davisson** and **Germer**. The conducted double-slit experiment is a demonstration that light and electrons can display characteristics of both classically defined waves and particles.

```python
# use `inline` for inline plot or `notebook` for interactive plot w/ sliders
%matplotlib inline
%run src/scripts/double_slit_exp.py
# a - single slit width
# λ - wavelength
# L - screen distance
# d - distance between slits
```

In 1926, assuming the wave-particle duality of nature, **Erwin Schrödinger** formulates *the wave equation*. The wave equation describes the strenth of the field produced by the particle wave and has its solution as a function of both time and position. This particle wave is also known as the matter wave and is in the perfect accordance to the Davisson-Germer experiment.
In the experiment, if the electrons were generated one by one, at first, the locations would seem to hit the sensor screen randomly. However, if the experiment is allowed to run for a very long time, a pattern shown in the previous figure will emerge. It is impossible to predict where each individual electron would strike the sensor screen, but using the Schrödinger equation, one can predict the probability of an electron striking a certain point of the sensor screen. This probability is proportional to the square of the magnitude of the wave function for the observed electron at that point, and is known as the Born rule, which will be described in more details in the following section. It was formulated by German physicist **Max Born**, soon after Schrödinger formulated his equation.

Davisson-Germer experiment also showed that if the observer wants to locate the position of an electron at every point, the sensor screen will not detect wave characteristics of a generated electron, rather the pattern of sensed electrons on the screen will be as if the elelctron has particle characteristics only. This occurance is called the wave function collapse and, more generally, it occurs when a wave function, naturally in a superposition of several eigenstates reduces to a single eigenstate due to the interaction with the external world. This interaction is called an *observation*. The problem of the wave function collapse is called the *measurement problem*. The inability to observe such a collapse directly has given rise to different interpretations of quantum mechanics and poses a key set of questions that each interpretation must answer. The oldest and most widely accepted interpretation is already mentioned Born rule, statistical interpretation which gave rise to the Copenhagen interpretation. Controversial Copenhagen philospohy, whose founding fathers are Neils Bohr and **Werner Heisenberg**, states that physical systems generally do not have definite properties prior to being measured, and quantum mechanics can only predict the probability distribution of a given measurement's possible results. Another opposing philosophy, supported by Einstein and Schrödinger, advocated the idea of hidden states and incomplete realization of quantum mechanics. From this, famous Schrödinger cat thought experiment emerged, illustrating the incompleteness of the Copenhagen interpretation. Great explanation of the Schrödinger cat thought experiment and intro to many-world interpretation by Sean Carroll is given [here](https://www.youtube.com/watch?v=kTXTPe3wahc).

To take a few steps back, this study is based on, however flawed, still very powerful Copenhagen interpretation, without which most of the technology of the second half of the 20th century would not have been successfully realized. This probabilistic philosophy is based on the Heisenberg uncertainty principle. The uncertainty, in its most basic form, states that product of the position uncertainty and the momentum uncertainty of the observed electron at the same time is greater or equal to the reduced Planck constant $h$:

$$
\begin{equation}
\Delta p \cdot \Delta x \geqslant \hbar
\end{equation}
$$

where $\hbar=\frac{h}{2\pi}$.

In order to determine precise position of the electron, one has to observe the electron using some electromagnetic-based detection mechanism. A photon with a low wavelength $\lambda_p$, emitted out of the detection mechanism, carries large momentum $p_p$ and the collision with an electron will deliver significant kinetic energy to an electron. Thus, an electron position at the certain time-point will be known with high probability but the initial momentum of an electron is changed and there is no way to know the exact value of the momentum.
The principle is also applicable to all canonically conjugate variables (two variables, once multiplied, give the product of the physical dimension of the reduced Planck constant, e.q. time and energy):

$$
\begin{equation}
\Delta E \cdot \Delta t \geqslant \hbar
\end{equation}
$$

The uncertainty principle is an inherent property of the nature and comes from the fact that the matter acts both as waves and as particles. The uncertainty principle does not depend on the precision or tuning of a measuring device or a system.

### 2 Schrödinger Equation

Assuming low radiation frequencies, where corpuscular nature of light is hardly detected, Maxwell's equations, in their differential form, are defined as follows:

$$
\begin{align}
\nabla \times \mathbf{E} &= - \frac{\partial \mathbf{B}}{\partial t} \\
\nabla \times \mathbf{B} &= \mu_0 ( \mathbf{J} + \epsilon_0\frac{\partial \mathbf{E}}{\partial t} ) \\
\nabla \cdot \mathbf{E} &= \frac{\rho}{\epsilon_0} \\
\nabla \cdot \mathbf{B} &= 0
\end{align}
$$

where:
* $\mathbf{E}$ is electric vector field;
* $\mathbf{B}$ is magnetic pseudovector field;
* $\mathbf{J}$ is electric current density;
* $\epsilon_0$ is the permittivity of free space and
* $\mu_0$ is the permeability of free space.

The electromagnetic wave radiation velocity is defined with the following expression:

$$
\begin{equation}
c = \lambda \cdot \nu
\end{equation}
$$

where $\lambda$ is the wavelenght and $\nu$ is the frequency.
Equation (10) can be rewritten as:

$$
\begin{equation}
\frac{\nu^2}{c^2} - \frac{1}{\nu^2} = 0
\end{equation}
$$

for later mathematical convenience.

The value of the electric field of the plane electromagnetic wave expanding in $x$ direction is defined as follows:

$$
\begin{equation}
E = E_0 \sin 2\pi \big(\nu t - \frac{x}{\lambda}\big)
\end{equation}
$$

with the associated second partial derivatives:

$$
\begin{equation}
\frac{\partial^2 E}{\partial t^2} = -4\pi^2 \nu^2E; \qquad \frac{\partial^2 E}{\partial x^2} = -\frac{4\pi^2}{\lambda^2} E
\end{equation}
$$

Equation (11), once multiplied by $4\pi^2\mathbf{E}$ and rearanged considering the form of second partial derivatives defined in (13) generates the following expression:

$$
\begin{equation}
\frac{\partial^2 \mathbf{E}}{\partial x^2} - \frac{1}{c^2}\frac{\partial^2 \mathbf{E}}{\partial t^2} = 0
\end{equation}
$$

For a free-particle, i.e. an electron, moving rectilinearly, the matter wave takes the form of the plane wave. The relativistic relation between the energy and the momentum of a free-particle is defiend using the expression:

$$
\begin{equation}
\frac{E^2}{c^2} = p^2 + m_0^2 c^2
\end{equation}
$$

where $E$ is the relativistic energy of a free-particle, $p$ is the relativistic momentum and $m_o$ is the rest mass.

Assuming the wave-particle duality for a free-particle where $E=h\nu$ and $p=\frac{h}{\nu}$, the relativistic relation between the energy and the momentum of a free-particle becomes:

$$
\begin{equation}
\frac{h^2 \nu^2}{c^2} = \frac{h^2}{\lambda^2} + \frac{h^2 \nu_0^2}{c^2} \Rightarrow \frac{\nu^2}{c^2} = \frac{1}{\lambda^2} + \frac{\nu_0^2}{c^2}
\end{equation}
$$

If we assume that the rest mass is 0, (16) becomes (11).

The matter wave of an observed free particle is defined as:

$$
\begin{equation}
\Phi = A \cdot e^{-2\pi i(\nu t - x/\lambda)}
\end{equation}
$$

with the associated second partial derivatives:

$$
\begin{equation}
\frac{\partial^2 \Phi}{\partial t^2} = -4\pi^2 \nu^2\Phi; \qquad \frac{\partial^2 \Phi}{\partial x^2} = -\frac{4\pi^2}{\lambda^2} \Phi
\end{equation}
$$

Equation (16), once multiplied by $\Phi$ and rearanged considering the form of second partial derivatives defined in (18) generates the following expression:

$$
\begin{equation}
\frac{\partial^2 \mathbf{\Phi}}{\partial x^2} - \frac{1}{c^2}\frac{\partial^2 \mathbf{\Phi}}{\partial t^2} = \frac{4\pi^2\mu_0^2}{c^2}\mathbf{\Phi}
\end{equation}
$$

or, generalized in the 3-D space:

$$
\begin{equation}
\frac{\partial^2 \mathbf{\Phi}}{\partial x^2} + \frac{\partial^2 \mathbf{\Phi}}{\partial y^2} + \frac{\partial^2 \mathbf{\Phi}}{\partial z^2} - \frac{1}{c^2}\frac{\partial^2 \mathbf{\Phi}}{\partial t^2} = \frac{4\pi^2\mu_0^2}{c^2}\mathbf{\Phi}
\end{equation}
$$

$$\Downarrow$$

$$
\begin{equation}
\Delta\mathbf{\Phi} - \frac{1}{c^2}\frac{\partial^2 \mathbf{\Phi}}{\partial t^2} = \frac{4\pi^2\mu_0^2}{c^2}\mathbf{\Phi}
\end{equation}
$$

thus forming the relativistic version of the matter wave, in the literature often referenced as the **Klein-Gordon** equation.

In practice, the difference between $\nu$ and $\nu_0$ is negligible and the non-relativistic version of the Klein-Gordon equation is of greater importance. From here, the famous time-dependant Schrödinger equation for a free-particle arises and is written as:

$$
\begin{equation}
-\frac{\hbar^2}{2m_0} \Delta \mathbf{\Phi} = i \hbar \frac{\partial \mathbf{\Phi}}{\partial t}
\end{equation}
$$

where $m_0$ is the rest mass of a free particle.

Separating the spatial, $\psi(x, y, z)$, and temporal variables of the function $\Phi$, time-independent Schrödinger equation is written as:

$$
\begin{equation}
-\frac{\hbar^2}{2m} \Delta \psi = E \psi
\end{equation}
$$

where $E$ is the non-relativistic free-particle energy and the mass of a free particle is the same as the rest mass of a free particle. The non-relativistic energy is defined as:

$$
\begin{equation}
E = \frac{p^2}{2m} + U
\end{equation}
$$

where $U$ is the potential energy. Equation (24) must always be satisfied in order to obey the conservation of energy law.

A solution of Schrödinger equation is a wave function, $\psi$, a quantity that described the displacement of the matter wave produced by particles, i.e. electrons. In Copenhagen interpretation, as mentioned in previous section, the square of the absolute value of the wave function results in the probability of finding the particle at given location.

#### 1-D time-independent Schrödinger equation

1-D time-independent Schrödinger equation for a single non-relativistic particle is obtained from the time-dependant equation by the separation of variables and is written using the following expression:

$$
\begin{equation}
\Big(-\frac{\hbar^2}{2m} \cdot \frac{d^2 \psi(x)}{dx^2} + U(x)\Big) \psi(x) = E \psi(x)
\end{equation}
$$

where, in the language of linear algebra, this equation is the eigenvalue equation. The wave function is an eigenfunction, $\psi(x)$ of the Hamiltonian operator, $\hat H$, with corresponding eigenvalue $E$:

$$
\begin{equation}
\hat H \psi(x) = \Big(-\frac{\hbar^2}{2m} \cdot \frac{d^2}{dx^2} + U(x)\Big) \psi(x)
\end{equation}
$$

The form of the Hamiltonian operator comes from classical mechanics, where the Hamiltonian function is the sum of the kinetic and potential energies.

The probability density function of a particle over the x-axis is written using the following expression:

$$
\begin{equation}
\mbox{pdf} = \lvert \psi(x) \lvert^2
\end{equation}
$$

and it can be shown, that the probability density function in space does not depend on time. From time-dependent Schrödinger equation (22), by letting the $\Phi(x,t) = \psi(x) \cdot \Phi(t)$ and applying the separation of variables, we obtain the following expression for temporal variable:

$$
\begin{equation}
i\hbar \cdot \frac{1}{\Phi(t)} \cdot \frac{\partial \Phi(t)}{\partial t} = E
\end{equation}
$$

The solution of (28) is obtained by integrating both sides:

$$
\begin{equation}
\Phi(t) = e^{-\frac{iEt}{\hbar}}
\end{equation}
$$

Finally the 1-D time-dependant wave function is written as:

$$
\begin{equation}
\Phi(x,t) = \psi(x) e^{-\frac{iEt}{\hbar}}
\end{equation}
$$

Following the Copenhagen interpretation, the square of the 1-D time-dependant wave function resolves the probability density of a single particle to be found at the certain location on the $x$-axis at a specific time $t$. This results in the following:

$$
\begin{equation}
\lvert \Phi(x,t) \lvert^2 = \lvert \psi(x) \lvert^2 \cdot \lvert e^{-\frac{iEt}{\hbar}} \lvert^2 = \lvert \psi(x) \lvert^2
\end{equation}
$$

From this, it is obvious that the probability density function in space does not depend on time, thus the rest of the seminar will be concentrated on the non-linear Schrödinger equation for non-relativitic particles.

The probability of finding the observed particle anywhere in the domain $x \subseteq \Omega$ is 1 since the particle has to be somewhere. The actual position is not known, rather one can describe the uncertainty of finding the particle for position in $[x, x+\mbox{d}x]$. The sum of probabilities over the entire solution domain should be equal to one:

$$
\begin{equation}
\int_{x \subseteq \Omega} \lvert \psi(x) \lvert^2 \mbox{d}x = 1
\end{equation}
$$

or, more explicitly:

$$
\begin{equation}
\int_{x \subseteq \Omega} \psi(x) \Psi^{*}(x) \mbox{d}x = 1
\end{equation}
$$

#### Wave function constraints

In order to represent a physically observable system in a measurable and meaningful fashion, the wave function must satisfy set of constraints:

1. the wave function must be a solution to Schrödinger equation;
2. the wave function and the first derivative of the wave function, $\frac{d \Psi(x)}{dx}$ must be continuous
3. the wave function must be normalizable - the 1-D wave function approaches to zero as $x$ approaches to infinity;

### 3 The Use of Wave Equation

#### Electron in Infinite Potential Well

Consider an electron of mass $m$ confined to 1-D rigid box of width L.
The potential of the infinite potential well is defined as follows:

$$
\begin{align}
    U(x) &= 0, \qquad &0 < x < L\\
    U(x) &= +\infty, \qquad &x \leq 0, x \geq L
\end{align}
$$

The electron can move along $x$-axis and the collisions with the walls are perfectly elastic. The described situation is known as an inifinitely deep square well potential.
In order to determine the motion of the observed electron as it travels along the $x$-axis, we must determine the wave function using Schrödinger equation.

Boundary conditions are pre-defined in (34) and (35). For $x=0$ and $x=L$, the potential energy is defined as $U(0) = U(L) = +\infty$, thus the wave function at boundaries is 0, $\psi(0) = \psi(L) = 0$, otherwise the product of $U(x) \cdot \psi(x)$ in Schrödinger equation would be phisically infeasible.

Inside the potential well, the potential energy is equal to 0, which means that the Schrödinger equation becomes:

$$
\begin{equation}
    E \cdot \psi(x) = - \frac{\hbar^2}{2m} \cdot \frac{d^2 \psi(x)}{dx^2}
\end{equation}
$$

and the wave function is written as:

$$
\begin{equation}
    \psi(x) = A \sin(kx) + B \cos(kx)
\end{equation}
$$

In order to discover the unknown parameters $A$ and $B$, the boundary condtions have to be taken into consideration.

If $x=0$,

$\Psi(x=0) = 0 \qquad \Rightarrow \qquad \psi(0) = A\sin(0) + B\cos(0) = 0$,

and

$$
\begin{equation}
    \boxed{B=0}
\end{equation}
$$

---

Since $B=0$ the wave function takes the following form of $\psi(x) = A\sin(kx)$.

---

If $x=L$,

$\psi(x=L) = 0 \qquad \Rightarrow \qquad \psi(L) = A\sin(kL) = 0$.

Since the electron is assumed to exists somewhere in the solution domain $ x \in (0, L)$, the term $\sin(kL)$ has to be equal to 0. This is true for $kL = 0, \pi, 2\pi...$ or, more generally, $kL=n\pi$ where $n=1,2,3,...$.

From here, $k$ can be written as:

$$
\begin{equation}
    k = \frac{n\pi}{L}
\end{equation}
$$

and, since the $k$ is known to be:

$$
\begin{equation}
    k = \sqrt{\frac{2mE}{\hbar^2}}
\end{equation}
$$

Combining the expressions (39) and (40), the following expression occurs:

$$
\begin{equation}
    \frac{n\pi}{L} = \sqrt{\frac{2mE}{\hbar^2}}
\end{equation}
$$

From (41), the non-relativistic energy of an electron is defined as:

$$
\begin{equation}
    E = \frac{n^{2}h^{2}}{8 m L^2}
\end{equation}
$$

where, $n=1,2,3,...$ and $\hbar=h/2\pi$.

Energy discretion is a direct consequence of boundary conditions and the initial assumption that the potential energy of an electron within the area is equal to zero.

The positive integer $n$ is a principal quantum number. Quantum numbers describe values of conserved quantities in the dynamics of a quantum system, in this case the quantum system in an electron trapped inside of a infinite potential well. The quantum number $n$ represent principal quantum numbers, which give acceptable solutions to the Schrödinger equation. The standard model of a quantum system is described using four quantum numbers: principal quantum number $n$, azimuthal quantum number $l$, magentic quantum number $m_l$ and spin quantum number $m_s$. The first three quantum numbers are derived from Schrödinger equation, while the fourth comes from applying special relativity. The principal quantum number describes the electron shell - energy level of an electron, and it ranges from 1 to the shell containing the outermost electron of the specific atom, thus is of the greatest importance for the general solution of time-independent Schrödinger equation. Since the principal quantum number $n$ is natural number, the energy level of an electron is a discrete variable.

The zero point energy represents the ground state of an electron or the least amount of energy that electron can contain, where $n=1$.

A wave function has to obey [the set of constraints](#Wave-function-constraints) defined in the previous section:

$$
\begin{equation}
    \int_{-\infty}^{+\infty}\lvert \psi \lvert^2 dx = \int_{0}^{L} A^2 \sin^2 \big(\frac{n \pi}{L} \cdot x \big)dx = 1
\end{equation}
$$

From (43):

$$
\begin{equation}
    \frac{A^2 L}{2} = 1 \qquad \Rightarrow \qquad \boxed{A=\sqrt{\frac{2}{L}}}
\end{equation}
$$

---

Finally, the wave function describing the motion of the observed electron with potential energy $U(x)=0$ and with mass $m$ moving along the the $x$-axis trapped inside the infinite potential well, considering (38) and (44), is given using the following expression:

$$
\begin{equation}
    \psi_n(x) = \sqrt{\frac{2}{L}}\sin\big( \frac{n \pi}{L} \cdot x \big)
\end{equation}
$$

where $n$ is the principal quantum number and $L$ is the width of the rigid box. The term $\sqrt{\frac{2}{L}}$ represents the amplitude of the wave function.

In order to obtain the probability density of the electron in three different energy states for $n=1, 2 \mbox{ and } 3$, trapped inside the infinite potential well of width $L=1 \mbox{ nm}$, the square of the wave function has to be observed:

```python
%matplotlib inline
%run src/scripts/infinite_potential_well.py
```

The highest point of each probability density function plotted in previous figure represents the most likely placement of an electron at any given moment in time. Boundary conditions are defined *a priori* and are integrated in the analytical solution given in (45), thus the probability of an electron placement at locations $x=\{0, L\}$ is 0.
It is also important to notice that the area under each of the probability density curves shown above, is equal to 1. In order to be measurable and physically meaningful, a wave function must be normalized, mathematically formulated in (43). The total probability of finding the electron inside the $x \in \langle 0, L \rangle$ region is obtained by integrating the probability density function over $x$ and is always equal to 1. If the $L$ increases, the probability density peak will decrease and the probability density function curve will, consequently, be flatten.

#### Electron in Finite Potential Well

Consider an electron of mass $m$ confined to 1-D finite potential well of width L and potential barriers at energy level of $U_0$.
The potential of the finite potential well is defined as follows:

$$
\begin{align}
    U(x) &= 0, &\qquad \lvert x \lvert > L/2\\
    U(x) &= U_0, &\qquad \lvert x \lvert \leq L
\end{align}
$$

In order to determine the motion of the observed electron as it travels along the $x$-axis, we must determine the wave function using Schrödinger equation for three different regions.

##### Region II: $x \in \langle -L/2, L/2 \rangle$
If particle is in region II, then the $U(x)=0$ due to (46). The Schrödinger equation is defined as follows:

$$
\begin{equation}
    E \cdot \psi (x) = -\frac{\hbar^2}{2m} \cdot \frac{d^2 \psi(x)}{dx^2}
\end{equation}
$$

From (48) the wave function is defined as:

$$
\begin{equation}
    \psi(x) = A \sin(kx) + B \sin(kx)
\end{equation}
$$

where $k = \frac{n\pi}{L}$.

##### Region I and III: $x \in \langle-\infty, -L/2] \cup [L/2, +\infty \rangle$
If particle is in region I or III, then the $U(x) = U_0$ due to (47). The Schrödinger equation is defined as follows:

$$
\begin{equation}
    E \cdot \psi(x) = U_0 \cdot \psi(x) -\frac{\hbar^2}{2m} \cdot \frac{d^2 \psi(x)}{dx^2}
\end{equation}
$$

After rearanging (50), the Schrödinger equation becomes:

$$
\begin{equation}
    \frac{d^2 \psi(x)}{dx^2} - \Big ( \frac{2m}{\hbar}(E - U_0) \Big ) \psi(x) = 0
\end{equation}
$$

If the term $\frac{2m}{\hbar}(E - U_0)$ is written as $\alpha^2$, the equation takes simplified form of:

$$
\begin{equation}
    \frac{d^2 \psi(x)}{dx^2} - \alpha^2 \psi(x) = 0
\end{equation}
$$

and the solution of the equation is:

$$
\begin{equation}
    \psi(x) = C \mbox{e}^{\alpha x} + D \mbox{e}^{-\alpha x}
\end{equation}
$$

For the region I, if $x \rightarrow -\infty$, the term $\mbox{e}^{-\alpha x} \rightarrow \infty$ and $\psi(x) \rightarrow \infty$, which is physically infeasible. In order to avoid this, the term $D$ is set to be 0 for region I. The wave function for region I is defined as:

$$
\begin{equation}
    \psi(x) = C \mbox{e}^{\alpha x}
\end{equation}
$$

the wave function of the region III is defined analogously as:

$$
\begin{equation}
    \psi(x) = D \mbox{e}^{-\alpha x}
\end{equation}
$$

The wave function must obey [the set of constraints](#Wave-function-constraints) defined in the previous section, that is, the wave function must be continuous throughout $x$-axis in order to be physically feasible. The total wave function composed of wave functions for three different regions defined in (49), (54) and (55) must satisfy the following conditions on boundaries to achieve the continuity:

$$
\begin{align}
    \psi_1(x) = \psi_2(x), \quad \frac{\psi_1(x)}{dx}=\frac{\psi_2(x)}{dx} \qquad &\mbox{for } x=-\frac{L}{2};\\
    \psi_2(x) = \psi_3(x), \quad \frac{\psi_2(x)}{dx}=\frac{\psi_3(x)}{dx} \qquad &\mbox{for } x=\frac{L}{2}.
\end{align}
$$

where $\psi_1(x)$, $\psi_2(x)$ and $\psi_3(x)$ are wave functions for regions I, II and III, respectively.

Even though, the analytical solution for previously described situation exists, it is not going to be solved in this notebook, rather the high level intuition of the problem will be presented. According to classical mechanics, if $U_0 > E$, an electron cannot be found in the regions I and III. By solving the Schrödinger through the framework of quantum mechanics, it is shown that there is some probability of an electron to penetrate the potential barriers and to be found outside of the region II, even if the potential energy $U_0$ is greater than the total energy of an electron. The possibility of positioning an electron outside the potential well makes the energy conservation law momentarily invalid. This, however, can be explained via the Heisenberg uncertainty principle defined in (5). The uncertainty principle enables an electron to exist outside of the potential well for the amount of time $\Delta t$ during which a certain amount of energy uncertainty also co-exists.

```python
%matplotlib inline
%run -i src/scripts/finite_potential_well.py
```

##### Quantum Tunneling

As shown in the previous section, an electron is able to penetrate potential barrier and enter a region, which is not explainable through the lense of classical mechanics. According to quantum mechanics, there is some non-zero probability that the particle will end up passing through a potential barrier even though the potential energy level of the barrier is higher than the total non-relativistic energy of an electron. The wave function of an electron, once it enters the potential region, will start to decay exponentially. Before the amplitude of the wave function drops to zero, the wave function resurfaces and continues to propagate with a smaller amplitude on the other side of the barrier. Since there is existing wave function on the other side of the barrier, one can reason that electron is able to penetrate the barrier even though its kinetic energy is less than the potential energy of a barrier. This phenomenon is known as quantum tunneling. From here, two important coefficients can be calculated:

**Transmission coefficient** ($T$) - the fraction of the transmitted wave function of an electron

$$
\begin{equation}
    T  \approx \mbox{e}^{-2\alpha L}
\end{equation}
$$

where L is the width of a potential barrier and $\alpha$ is formulated as follows:

$$
\begin{equation}
    \alpha = \frac{\sqrt{2m(U_0 - E}}{\hbar}
\end{equation}
$$

where $m$ is the mass of an electron, $U_0$ is the poteential energy level of a barrier, $E$ is the kinetic energy of an electron and $\hbar$ is the reduced Planck constant.

**Reflection coefficient** ($R$) - fraction of the reflected wave function of an electron:

$$
\begin{equation}
    R = 1 - T
\end{equation}
$$

The total sum of the transmission and reflection coefficient is always 1.

The following cell is the output of a Python solver for the 1-D Schrödinger equation beautfully explained through [this blog post](http://jakevdp.github.io/blog/2012/09/05/quantum-python/). The code is minimally modified to satisfy the current version of Python and some dependencies. All rights go to [Jake Vanderplas](http://vanderplas.com/).

```python
# use `inline` for inline plot or `notebook` for interactive plot
%matplotlib notebook
%run -i src/scripts/quantum_tunneling.py
```

    <IPython.core.display.Javascript object>

## Numerical solution of the Schrödinger equation

### 1 Finite difference method

The wavefunction, $\psi(x)$, over $x$-axis is defined as eigenvector of the Hamiltonian operator as follows:

$$
\begin{equation}
    \hat{H} \psi = E \psi
\end{equation}
$$

where $\hat{H}$ is the Hamiltonian operator, E is eigenvalues (energies) of an electron with the wave function $\psi$. This problem should be solved as eigenproblem, otherwise the solution to $\psi$ will be trivial.

Hamiltonian operator is then defiend as $N \times N$ matrix, where $N$ is the chosen number of grid points.

```python
%matplotlib inline
%run src/fdm_infinite_potential_well.py -n 50
```

### 2 Finite element method

Applying the weighted residual approach to Schrödinger equation (25) with $U(x)=0$ the following term is obtained:

$$
\begin{equation}
    -\int_{0}^{L} \frac{\hbar^2}{2m}\frac{d^2\psi}{dx^2}W_{j} = \int_{0}^{L} E \psi W_{j} dx
\end{equation}
$$

where $W_{j}$ are test functions for $j=1, 2, 3, ..., n$, where $n$ stands for the number of approximated solutions. From here, weak formulation is utilized as follows:

$$
\begin{equation}
    \int_{0}^{L} \frac{d\psi}{dx}\frac{dW_{j}}{dx} dx = \frac{2m}{\hbar^2}E \int_{0}^{L} \psi W_{j} dx + \frac{2m}{\hbar^2} \frac{d\psi}{dx} W_j \Big \lvert_{0}^L
\end{equation}
$$

The approximate solution is formed as the linear combination of coefficients $\alpha_i$ and shape functions $N_i$. In the Galerkin-Bubnov framework test functions are the same as shape functions, $W_j = N_j$ and the vectorized Schrödinger equation is defined as follows:

$$
\begin{equation}
    [A]\{\alpha\} = [B]\{\alpha\}[E]
\end{equation}
$$

where:
* A is the left-hand-side matrix, $n \times n$ shaped, defined as:

$$
\begin{equation}
    A_{ji} = \int_{x_1}^{x_2} \frac{dN_i(x)}{dx}\frac{dN_{j}}{dx} dx
\end{equation}
$$

* B is the right-hand-side vector, $n \times n$ shaped, defined as:

$$
\begin{equation}
    B_{ji} = \frac{2m}{\hbar^2}E \int_{x_1}^{x_2} N_i(x) N_{j}(x) dx
\end{equation}
$$

* E is a diagonal matrix representing eigenvalues for different princple quantum numbers, $1$ to $N$.

```python
%matplotlib inline
%run src/fem_infinite_potential_well.py -n 49
```

### 3 Artifical neural network method

[The technique proposed by Lagaris et al.](https://pubmed.ncbi.nlm.nih.gov/18255782/) is extended and adjusted to modern machine learning practices in order to acquire the wave function in the particle-in-a-box quantum model. Firstly, the wave function is approximated using the following expression:

$$
\begin{equation}
	\hat \psi(x) = B(x) NN(x; \theta)
\end{equation}
$$

where $B(x)$ is any function that satisfies boundary conditions, in this case $B(x) = x(1-x)$, and $NN(x; \theta)$ is the neural network that takes positional coordinate $x$ as the input argument and is parameterized using the weights and biases, $\theta$. The collocation method is applied in order to discretize the solution domain, $\Omega$, into the set of points for training, $x_i \in \Omega$ for $i \in [1, N]$, where $N$ is the total number of collocation points. The problem is transformed to the optimization problem with the following loss function:

$$
\begin{equation}
	J(x, \theta) = \frac{\sum_i [H \hat \psi(x_i; \theta) - E \hat \psi(x_i; \theta)]^2}{\int_\Omega \lvert \hat \psi \rvert^2 dx} + \lvert \psi(0) - \hat \psi(0) \rvert^2 + \lvert \psi(L) - \hat \psi(L) \rvert^2
\end{equation}
$$

where

$$
\begin{equation}
	E = \frac{\int_\Omega \hat \psi^* H \hat \psi dx}{\int_\Omega \lvert \hat \psi \rvert^2 dx}
\end{equation}
$$

Just like in the original work, the neural network is the single hidden layer multi-layer perceptron (MLP) with 40 hidden units total. Every unit is activated using hyperbolic tangent, *tanh*, activation function. Instead of manually applying symbolic derivatives, the automatic differentiation procedure is applied. Once the derivative of the loss function with respect to the network parameters has been acquired, the minimization can be performed. The Broyden-Fletcher-Goldfarb-Shanno (BFGS) algorithm, an iterative method for solving unconstrained nonlinear optimization problems, is applied.

## Results, discussion and conclussion

```python
%matplotlib inline
%run src/compare.py -n 10
```

```python
%matplotlib inline
%run -i src/compare.py -n 50
```

```python
%matplotlib inline
%run -i src/compare.py -n 100
```