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

### Author: Patrick Nelson

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

## This module implements a basic GRFFE code to evolve one-dimensional GRFFE waves.

### NRPy+ Source Code for this module:
* [GiRaFFEfood_NRPy/GiRaFFEfood_NRPy_Exact_Wald.py](../../edit/in_progress/GiRaFFEfood_NRPy/GiRaFFEfood_NRPy_Exact_Wald.py) [\[**tutorial**\]](Tutorial-GiRaFFEfood_NRPy_Exact_Wald.ipynb) Generates Exact Wald initial data
* [GiRaFFEfood_NRPy/GiRaFFEfood_NRPy_Aligned_Rotator.py](../../edit/in_progress/GiRaFFEfood_NRPy/GiRaFFEfood_NRPy_Aligned_Rotator.py) [\[**tutorial**\]](Tutorial-GiRaFFEfood_NRPy_Aligned_Rotator.ipynb) Generates Aligned Rotator initial data
* [GiRaFFEfood_NRPy/GiRaFFEfood_NRPy_1D_tests.py](../../edit/in_progress/GiRaFFEfood_NRPy/GiRaFFEfood_NRPy_1D_tests.py) [\[**tutorial**\]](Tutorial-GiRaFFEfood_NRPy_1D_tests.ipynb) Generates Alfvén Wave initial data.
* [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 Exact Wald (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

# 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, add_to_Cfunction_dict # 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_new_way_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",2)

# Step P7: Enable SIMD-optimized code?
#          I.e., generate BSSN and Ricci C code kernels using SIMD-vectorized
#          compiler intrinsics, which *greatly improve the code's performance*,
#          though at the expense of making the C-code kernels less
#          human-readable.
#          * Important note in case you wish to modify the BSSN/Ricci kernels
#            here by adding expressions containing transcendental functions
#            (e.g., certain scalar fields):
#          Note that SIMD-based transcendental function intrinsics are not
#          supported by the default installation of gcc or clang (you will
#          need to use e.g., the SLEEF library from sleef.org, for this
#          purpose). The Intel compiler suite does support these intrinsics
#          however without the need for external libraries.
enable_SIMD = False

# Step 1.b: Enable reference metric precomputation.
enable_rfm_precompute = False

if enable_SIMD and not enable_rfm_precompute:
    print("ERROR: SIMD does not currently handle transcendental functions,\n")
    print("       like those found in rfmstruct (rfm_precompute).\n")
    print("       Therefore, enable_SIMD==True and enable_rfm_precompute==False\n")
    print("       is not supported.\n")
    sys.exit(1)

# Step 1.c: Enable "FD functions". In other words, all finite-difference stencils
#         will be output as inlined static functions. This is essential for
#         compiling highly complex FD kernels with using certain versions of GCC;
#         GCC 10-ish will choke on BSSN FD kernels at high FD order, sometimes
#         taking *hours* to compile. Unaffected GCC versions compile these kernels
#         in seconds. FD functions do not slow the code performance, but do add
#         another header file to the C source tree.
# With gcc 7.5.0, enable_FD_functions=True decreases performance by 10%
enable_FD_functions = False

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

import GiRaFFE_NRPy.GiRaFFE_NRPy_Main_Driver_staggered_new_way 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.

## 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 = "Euler"

# 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.

```python
# There are several initial data routines we need to test. We'll control which one we use with a string option
initial_data = "ExactWald" # Valid options: "ExactWald", "AlignedRotator"
spacetime = "ShiftedKerrSchild" # Valid options: "ShiftedKerrSchild", "flat"

if spacetime == "ShiftedKerrSchild":
    import BSSN.ShiftedKerrSchild as sks
    sks.ShiftedKerrSchild(True)
    import reference_metric as rfm
    par.set_parval_from_str("reference_metric::CoordSystem","Cartesian")
    rfm.reference_metric()
    # Use the Jacobian matrix to transform the vectors to Cartesian coordinates.
    par.set_parval_from_str("reference_metric::CoordSystem","Spherical")
    rfm.reference_metric()
    Jac_dUCart_dDrfmUD,Jac_dUrfm_dDCartUD = rfm.compute_Jacobian_and_inverseJacobian_tofrom_Cartesian()
    # Transform the coordinates of the Jacobian matrix from spherical to Cartesian:
    par.set_parval_from_str("reference_metric::CoordSystem","Cartesian")
    rfm.reference_metric()
    tmpa,tmpb,tmpc = sp.symbols("tmpa,tmpb,tmpc")
    for i in range(3):
        for j in range(3):
            Jac_dUCart_dDrfmUD[i][j] = Jac_dUCart_dDrfmUD[i][j].subs([(rfm.xx[0],tmpa),(rfm.xx[1],tmpb),(rfm.xx[2],tmpc)])
            Jac_dUCart_dDrfmUD[i][j] = Jac_dUCart_dDrfmUD[i][j].subs([(tmpa,rfm.xxSph[0]),(tmpb,rfm.xxSph[1]),(tmpc,rfm.xxSph[2])])
            Jac_dUrfm_dDCartUD[i][j] = Jac_dUrfm_dDCartUD[i][j].subs([(rfm.xx[0],tmpa),(rfm.xx[1],tmpb),(rfm.xx[2],tmpc)])
            Jac_dUrfm_dDCartUD[i][j] = Jac_dUrfm_dDCartUD[i][j].subs([(tmpa,rfm.xxSph[0]),(tmpb,rfm.xxSph[1]),(tmpc,rfm.xxSph[2])])

    gammaSphDD = ixp.zerorank2()
    for i in range(3):
        for j in range(3):
            gammaSphDD[i][j] += sks.gammaSphDD[i][j].subs(sks.r,rfm.xxSph[0]).subs(sks.th,rfm.xxSph[1])

    betaSphU = ixp.zerorank1()
    for i in range(3):
        betaSphU[i] += sks.betaSphU[i].subs(sks.r,rfm.xxSph[0]).subs(sks.th,rfm.xxSph[1])
    alpha = sks.alphaSph.subs(sks.r,rfm.xxSph[0]).subs(sks.th,rfm.xxSph[1])

    gammaDD = rfm.basis_transform_tensorDD_from_rfmbasis_to_Cartesian(Jac_dUrfm_dDCartUD, gammaSphDD)

    unused_gammaUU,gammaDET = ixp.symm_matrix_inverter3x3(gammaDD)
    sqrtgammaDET = sp.sqrt(gammaDET)

    betaU = rfm.basis_transform_vectorD_from_rfmbasis_to_Cartesian(Jac_dUrfm_dDCartUD, betaSphU)

    loopopts_id ="AllPoints,Read_xxs"

elif spacetime == "flat":
    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.

desc = "Generate a spinning black hole with Shifted Kerr Schild metric in a Cartesian basis."
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_new_way_standalone_Ccodes/set_initial_spacetime_metric_data.h

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

```python
import GiRaFFEfood_NRPy.GiRaFFEfood_NRPy as gid
if initial_data=="ExactWald":
    gid.GiRaFFEfood_NRPy_generate_initial_data(ID_type = initial_data, stagger_enable = True,M=sks.M,KerrSchild_radial_shift=sks.r0,gammaDD=gammaDD,sqrtgammaDET=sqrtgammaDET)
    desc = "Generate exact Wald initial test data for GiRaFFEfood_NRPy."
elif initial_data=="SplitMonopole":
    gid.GiRaFFEfood_NRPy_generate_initial_data(ID_type = initial_data, stagger_enable = True,M=sks.M,a=sks.a,KerrSchild_radial_shift=sks.r0,alpha=alpha,betaU=betaSphU,gammaDD=gammaDD,sqrtgammaDET=sqrtgammaSphDET)
    desc = "Generate Split Monopole initial test data for GiRaFFEfood_NRPy."
else:
    print("Unsupported Initial Data string "+initial_data+"! Supported ID: ExactWald, or SplitMonopole")

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(Ccodesdir,name+".h"), 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"),
    loopopts ="AllPoints,Read_xxs")
```

    Output C function initial_data() to file GiRaFFE_staggered_new_way_standalone_Ccodes/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.5,-1.5,-1.5};
const REAL xxmax[3] = { 1.5, 1.5, 1.5};

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 = %.15e,%.15e,%.15e\\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 = 1;
// Standard GRFFE parameters:
params.GAMMA_SPEED_LIMIT = 2000.0;
params.diss_strength = 4.0;
""")
if initial_data=="ExactWald":
    with open(os.path.join(Ccodesdir,"free_parameters.h"),"a") as file:
        file.write("""params.r0 = 0.4;
params.a = 0.0;
""")

```

# 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(Ccodesdir,enable_copy_of_static_Ccodes=False)
```

    Wrote to file "GiRaFFE_staggered_new_way_standalone_Ccodes/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_new_way_standalone_Ccodes/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
#include "GiRaFFE_NRPy_REAL__NGHOSTS__CFL_FACTOR.h"
#include "declare_Cparameters_struct.h"
def add_to_Cfunction_dict_main__GiRaFFE_NRPy_3D_tests_staggered():
    includes = ["NRPy_basic_defines.h", "GiRaFFE_main_defines.h", "NRPy_function_prototypes.h", "time.h", "set_initial_spacetime_metric_data.h", "initial_data.h"]
    desc = """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
"""
    prefunc = """const int NSKIP_1D_OUTPUT = 1;
const REAL outer_integration_radius = 1.5; const REAL inner_integration_radius = 0.6;
#define LOOP_ALL_GFS_GPS(ii) _Pragma("omp parallel for") \
  for(int (ii)=0;(ii)<Nxx_plus_2NGHOSTS_tot*NUM_EVOL_GFS;(ii)++)
"""
    c_type = "int"
    name = "main"
    params = "int argc, const char *argv[]"
    body = """
    paramstruct params;
#include "set_Cparameters_default.h"
    // Step 0a: Read command-line input, error out if nonconformant
    if(argc != 4 || 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);
    initial_data(&params,xx,auxevol_gfs,y_n_gfs);

    // Code to perturb the initial data:
    /*
    for(int ii=0;ii<NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot;ii++) {
        y_n_gfs[ii] += 1.0e-15;
        y_nplus1_running_total_gfs[ii] += 1.0e-15;
        //k_odd_gfs[ii] = 1.0/0.0;
        //k_even_gfs[ii] = 1.0/0.0;
        diagnostic_output_gfs[ii] += 1.0e-15;
        evol_gfs_exact[ii] += 1.0e-15;
    }
    for(int ii=0;ii<NUM_AUXEVOL_GFS * Nxx_plus_2NGHOSTS_tot;ii++) {
        auxevol_gfs[ii] += 1.0e-15;
        auxevol_gfs_exact[ii] += 1.0e-15;
    }
    */

    // 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);
    GiRaFFE_NRPy_prims_to_cons(&params,auxevol_gfs,y_n_gfs);
    GiRaFFE_NRPy_cons_to_prims(&params,xx,auxevol_gfs,y_n_gfs);
    GiRaFFE_NRPy_prims_to_cons(&params,auxevol_gfs,y_n_gfs);
    GiRaFFE_NRPy_cons_to_prims(&params,xx,auxevol_gfs,y_n_gfs);
    GiRaFFE_NRPy_prims_to_cons(&params,auxevol_gfs,y_n_gfs);
    //GiRaFFE_NRPy_cons_to_prims(&params,xx,auxevol_gfs,y_n_gfs);

    // Fill in the exact gridfunctions the same way as above: In Exact Wald, initial data IS exact solution!
    for(int ii=0;ii<NUM_EVOL_GFS * Nxx_plus_2NGHOSTS_tot;ii++) {
        evol_gfs_exact[ii] = y_n_gfs[ii];
    }
    for(int ii=0;ii<NUM_AUXEVOL_GFS * Nxx_plus_2NGHOSTS_tot;ii++) {
        auxevol_gfs_exact[ii] = auxevol_gfs[ii];
    }

    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((n)%NSKIP_1D_OUTPUT ==0) {
            // Step 2c: Output quantities along x axis.
            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-%08d.txt",Nxx0,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\\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(ALPHAGF,idx)]*auxevol_gfs[IDX4ptS(VALENCIAVU0GF,idx)]-auxevol_gfs[IDX4ptS(BETAU0GF,idx)],
                        auxevol_gfs[IDX4ptS(ALPHAGF,idx)]*auxevol_gfs[IDX4ptS(VALENCIAVU1GF,idx)]-auxevol_gfs[IDX4ptS(BETAU1GF,idx)],
                        auxevol_gfs[IDX4ptS(ALPHAGF,idx)]*auxevol_gfs[IDX4ptS(VALENCIAVU2GF,idx)]-auxevol_gfs[IDX4ptS(BETAU2GF,idx)],
                        y_n_gfs[IDX4ptS(PSI6PHIGF,idx)]);
            }
            fclose(out2D);

            // Compute & output L2 norm. For each quantity Q we care about:
            // \\sqrt{\\frac{1}{N} \\sum_{ijk} \\left( Q_{\\rm approx} - Q_{\\rm exact} \\right)^2}
            int num_quantities_integrated = 13;
            REAL integrated_quantities[num_quantities_integrated];
            for(int ii=0; ii<num_quantities_integrated; ii++) {
                integrated_quantities[ii] = 0.0;
            }
            sprintf(filename,"out%d-L2.txt",Nxx0);
            FILE *outL2 = fopen(filename, "a");
            int num_points = 0;
#ifndef SQR
#define SQR(x) ((x) * (x))
#endif /*SQR*/
            for(int i2=0;i2<Nxx_plus_2NGHOSTS2;i2++) {
                REAL xx2 = xx[2][i2];
                for(int i1=0;i1<Nxx_plus_2NGHOSTS1;i1++) {
                    REAL xx1 = xx[1][i1];
                    for(int i0=0;i0<Nxx_plus_2NGHOSTS0;i0++) {
                        REAL xx0 = xx[0][i0];
                        // Make sure we're inside a thick spherical shell:
                        if(SQR(xx0)+SQR(xx1)+SQR(xx2) < SQR(outer_integration_radius) &&
                           SQR(xx0)+SQR(xx1)+SQR(xx2) > SQR(inner_integration_radius)) {
                            const int idx = IDX3S(i0,i1,i2);
                            // Compute the sum of the squares of the difference between numerical and exact:
                            integrated_quantities[0]  += SQR(auxevol_gfs[IDX4ptS(BU0GF,idx)]         - auxevol_gfs_exact[IDX4ptS(BU0GF,idx)]);
                            integrated_quantities[1]  += SQR(auxevol_gfs[IDX4ptS(BU1GF,idx)]         - auxevol_gfs_exact[IDX4ptS(BU1GF,idx)]);
                            integrated_quantities[2]  += SQR(auxevol_gfs[IDX4ptS(BU2GF,idx)]         - auxevol_gfs_exact[IDX4ptS(BU2GF,idx)]);
                            integrated_quantities[3]  += SQR(y_n_gfs[IDX4ptS(AD0GF,idx)]             - evol_gfs_exact[IDX4ptS(AD0GF,idx)]);
                            integrated_quantities[4]  += SQR(y_n_gfs[IDX4ptS(AD1GF,idx)]             - evol_gfs_exact[IDX4ptS(AD1GF,idx)]);
                            integrated_quantities[5]  += SQR(y_n_gfs[IDX4ptS(AD2GF,idx)]             - evol_gfs_exact[IDX4ptS(AD2GF,idx)]);
                            integrated_quantities[6]  += SQR(y_n_gfs[IDX4ptS(STILDED0GF,idx)]        - evol_gfs_exact[IDX4ptS(STILDED0GF,idx)]);
                            integrated_quantities[7]  += SQR(y_n_gfs[IDX4ptS(STILDED1GF,idx)]        - evol_gfs_exact[IDX4ptS(STILDED1GF,idx)]);
                            integrated_quantities[8]  += SQR(y_n_gfs[IDX4ptS(STILDED2GF,idx)]        - evol_gfs_exact[IDX4ptS(STILDED2GF,idx)]);
                            integrated_quantities[9]  += SQR(auxevol_gfs[IDX4ptS(VALENCIAVU0GF,idx)] - auxevol_gfs_exact[IDX4ptS(VALENCIAVU0GF,idx)]);
                            integrated_quantities[10] += SQR(auxevol_gfs[IDX4ptS(VALENCIAVU1GF,idx)] - auxevol_gfs_exact[IDX4ptS(VALENCIAVU1GF,idx)]);
                            integrated_quantities[11] += SQR(auxevol_gfs[IDX4ptS(VALENCIAVU2GF,idx)] - auxevol_gfs_exact[IDX4ptS(VALENCIAVU2GF,idx)]);
                            integrated_quantities[12] += SQR(y_n_gfs[IDX4ptS(PSI6PHIGF,idx)]         - evol_gfs_exact[IDX4ptS(PSI6PHIGF,idx)]);
                            num_points++;
                        }
                    }
                }
            }
            // Divide by N, take the square root:
            for(int ii=0; ii<num_quantities_integrated; ii++) {
                integrated_quantities[ii] = sqrt(integrated_quantities[ii]/num_points);
            }
            fprintf(outL2,"%d %.16e %.16e %.16e %.16e %.16e %.16e %.16e %.16e %.16e %.16e %.16e %.16e %.16e\\n",
                    n,
                    integrated_quantities[0],integrated_quantities[1],integrated_quantities[2],
                    integrated_quantities[3],integrated_quantities[4],integrated_quantities[5],
                    integrated_quantities[6],integrated_quantities[7],integrated_quantities[8],
                    integrated_quantities[9],integrated_quantities[10],integrated_quantities[11],
                    integrated_quantities[12]);
            fclose(outL2);
        }

        // 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;
"""
    add_to_Cfunction_dict(
        includes=includes,
        desc=desc,
        c_type=c_type, name=name, params=params,
        prefunc = prefunc, body=body,
        rel_path_to_Cparams=os.path.join("."), enableCparameters=False)
```

```python
import GiRaFFE_NRPy.GiRaFFE_NRPy_staggered_Source_Terms as stgsrc
stgsrc.add_to_Cfunction_dict__GiRaFFE_NRPy_staggered_Source_Terms()

md.add_to_Cfunction_dict__cons_to_prims(md.StildeD,md.BU,md.gammaDD,md.betaU,md.alpha,includes=["NRPy_basic_defines.h","GiRaFFE_basic_defines.h"])
md.add_to_Cfunction_dict__prims_to_cons(md.gammaDD,md.betaU,md.alpha,md.ValenciavU,md.BU,md.sqrt4pi,includes=["NRPy_basic_defines.h","GiRaFFE_basic_defines.h"])

import GiRaFFE_NRPy.GiRaFFE_NRPy_Source_Terms as source
source.add_to_Cfunction_dict__functions_for_StildeD_source_term(md.outCparams,md.gammaDD,md.betaU,md.alpha,
                                                                md.ValenciavU,md.BU,md.sqrt4pi,includes=["NRPy_basic_defines.h","GiRaFFE_basic_defines.h"])
import GiRaFFE_NRPy.Stilde_flux as Sf
Sf.add_to_Cfunction_dict__Stilde_flux(includes=["NRPy_basic_defines.h","GiRaFFE_basic_defines.h"], inputs_provided = True, alpha_face=md.alpha_face, gamma_faceDD=md.gamma_faceDD,
                                      beta_faceU=md.beta_faceU, Valenciav_rU=md.Valenciav_rU, B_rU=md.B_rU,
                                      Valenciav_lU=md.Valenciav_lU, B_lU=md.B_lU, sqrt4pi=md.sqrt4pi,write_cmax_cmin=True)

import GiRaFFE_NRPy.GiRaFFE_NRPy_staggered_Afield_flux as Af
Af.add_to_Cfunction_dict__GiRaFFE_NRPy_staggered_Afield_flux()

import GiRaFFE_NRPy.GiRaFFE_NRPy_Metric_Face_Values as FCVAL
FCVAL.add_to_Cfunction_dict__GiRaFFE_NRPy_FCVAL(includes=["NRPy_basic_defines.h","GiRaFFE_basic_defines.h"])

import GiRaFFE_NRPy.GiRaFFE_NRPy_PPM as PPM
PPM.add_to_Cfunction_dict__GiRaFFE_NRPy_PPM(Ccodesdir)

import GiRaFFE_NRPy.GiRaFFE_NRPy_staggered_A2B as A2B
A2B.add_to_Cfunction_dict__GiRaFFE_NRPy_staggered_A2B()

import GiRaFFE_NRPy.GiRaFFE_NRPy_BCs as BC
BC.add_to_Cfunction_dict__GiRaFFE_NRPy_BCs()

md.add_to_Cfunction_dict__driver_function()
add_to_Cfunction_dict_main__GiRaFFE_NRPy_3D_tests_staggered()
```

Now, we will register the remaining C functions and contributions to `NRPy_basic_defines.h`, then we output `NRPy_basic_defines.h` and `NRPy_function_prototypes.h`.

```python
import outputC as outC
outC.outputC_register_C_functions_and_NRPy_basic_defines()  # #define M_PI,  etc.
# Declare paramstruct, register set_Cparameters_to_default(),
#   and output declare_Cparameters_struct.h and set_Cparameters[].h:
outC.NRPy_param_funcs_register_C_functions_and_NRPy_basic_defines(os.path.join(Ccodesdir))

gri.register_C_functions_and_NRPy_basic_defines(enable_griddata_struct=False, enable_bcstruct_in_griddata_struct=False,
                                                enable_rfmstruct=False,
                                                enable_MoL_gfs_struct=False,
                                                extras_in_griddata_struct=None)  # #define IDX3S(),  etc.
fin.register_C_functions_and_NRPy_basic_defines(NGHOSTS_account_for_onezone_upwind=True,
                                                enable_SIMD=enable_SIMD)  # #define NGHOSTS, and UPWIND() macro if SIMD disabled

# Output functions for computing all finite-difference stencils.
#   Must be called after defining all functions depending on FD stencils.
if enable_FD_functions:
    fin.output_finite_difference_functions_h(path=Ccodesdir)

# Call this last: Set up NRPy_basic_defines.h and NRPy_function_prototypes.h.
outC.construct_NRPy_basic_defines_h(Ccodesdir, enable_SIMD=enable_SIMD)

with open(os.path.join(Ccodesdir,"GiRaFFE_basic_defines.h"),"w") as file:
    file.write("""#define NGHOSTS_A2B """+str(2)+"\n"+"""extern int kronecker_delta[4][3];

extern int MAXFACE;
extern int NUL;
extern int MINFACE;

// Some additional constants needed for PPM:
extern int VX,VY,VZ,
  BX_CENTER,BY_CENTER,BZ_CENTER,BX_STAGGER,BY_STAGGER,BZ_STAGGER,
  VXR,VYR,VZR,VXL,VYL,VZL;  //<-- Be _sure_ to define MAXNUMVARS appropriately!
extern int NUM_RECONSTRUCT_GFS;

// Structure to track ghostzones for PPM:
typedef struct __gf_and_gz_struct__ {
  REAL *gf;
  int gz_lo[4],gz_hi[4];
} gf_and_gz_struct;
""")

with open(os.path.join(Ccodesdir,"GiRaFFE_main_defines.h"),"w") as file:
    file.write("""#define NGHOSTS_A2B """+str(2)+"\n"+PPM.kronecker_code+"""const int MAXFACE = -1;
const int NUL     = +0;
const int MINFACE = +1;

// Some additional constants needed for PPM:
const int VX=0,VY=1,VZ=2,
  BX_CENTER=3,BY_CENTER=4,BZ_CENTER=5,BX_STAGGER=6,BY_STAGGER=7,BZ_STAGGER=8,
  VXR=9,VYR=10,VZR=11,VXL=12,VYL=13,VZL=14;  //<-- Be _sure_ to define MAXNUMVARS appropriately!
const int NUM_RECONSTRUCT_GFS = 15;

// Structure to track ghostzones for PPM:
typedef struct __gf_and_gz_struct__ {
  REAL *gf;
  int gz_lo[4],gz_hi[4];
} gf_and_gz_struct;
""")

outC.construct_NRPy_function_prototypes_h(Ccodesdir)
```

```python
cmd.new_C_compile(Ccodesdir, os.path.join("output", "GiRaFFE_NRPy_standalone"),
                  uses_free_parameters_h=True, compiler_opt_option="fast") # fastdebug or debug also supported

# !gcc -g -O2 -fopenmp GiRaFFE_standalone_Ccodes/GiRaFFE_NRPy_standalone.c -o GiRaFFE_NRPy_standalone -lm

# Change to output directory
os.chdir(outdir)
# Clean up existing output files
cmd.delete_existing_files("out*.txt")
cmd.delete_existing_files("out*.png")
# cmd.Execute(os.path.join(Ccodesdir,"output","GiRaFFE_NRPy_standalone"), "640 16 16", os.path.join(outdir,"out640.txt"))
cmd.Execute("GiRaFFE_NRPy_standalone", "64 64 64","out64.txt")
# cmd.Execute("GiRaFFE_NRPy_standalone", "239 15 15","out239.txt")
# !OMP_NUM_THREADS=1 valgrind --track-origins=yes -v ./GiRaFFE_NRPy_standalone 1280 32 32
# Return to root directory
os.chdir(os.path.join("../../"))
```

    (EXEC): Executing `make -j10`...
    (BENCH): Finished executing in 0.8128435611724854 seconds.
    Finished compilation.
    (EXEC): Executing `taskset -c 0,1,2,3 ./GiRaFFE_NRPy_standalone 64 64 64`...
    (BENCH): Finished executing in 18.257782697677612 seconds.

Now, we will load the data generated by the simulation and plot it in order to test for convergence.

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

#          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"]
Data_numer = np.loadtxt(os.path.join(Ccodesdir,"output","out64-00000010.txt"))
# Data_num_2 = np.loadtxt(os.path.join(Ccodesdir,"output","out239-00000080.txt"))
# Data_old = np.loadtxt("/home/penelson/OldCactus/Cactus/exe/ABE-GiRaFFEfood_1D_AlfvenWave/giraffe-grmhd_primitives_bi.x.asc")
# Data_o_2 = np.loadtxt("/home/penelson/OldCactus/Cactus/exe/ABE-GiRaFFEfood_1D_AlfvenWave_2/giraffe-grmhd_primitives_bi.x.asc")
# Data_numer = Data_old[5000:5125,11:15] # The column range is chosen for compatibility with the plotting script.
# Data_num_2 = Data_o_2[19600:19845,11:15] # The column range is chosen for compatibility with the plotting script.
Data_exact = np.loadtxt(os.path.join(Ccodesdir,"output","out64-00000010.txt"))
# Data_exa_2 = np.loadtxt(os.path.join(Ccodesdir,"output","out239-00000080_exact.txt"))

predicted_order = 2.0
column = 3
plt.figure()
# # plt.plot(Data_exact[2:-2,0],np.log2(np.absolute((Data_numer[2:-2,column]-Data_exact[2:-2,column])/\
# #                                     (Data_num_2[2:-2:2,column]-Data_exa_2[2:-2:2,column]))),'.')
plt.plot(Data_exact[:,0],Data_exact[:,column])
plt.plot(Data_exact[:,0],Data_numer[:,column],'.')
# plt.xlim(-0.0,1.0)
# # plt.ylim(-1.0,5.0)
# # plt.ylim(-0.0005,0.0005)
# plt.xlabel("x")
# plt.ylabel("BU2")
plt.show()

# Plotting scripts for comparison with original GiRaFFE:
# old_files = ["",
#              "giraffe-grmhd_primitives_bi.x.asc","giraffe-grmhd_primitives_bi.x.asc","giraffe-grmhd_primitives_bi.x.asc",
#              "giraffe-em_ax.x.asc","giraffe-em_ay.x.asc","giraffe-em_az.x.asc",
#              "giraffe-grmhd_conservatives.x.asc","giraffe-grmhd_conservatives.x.asc","giraffe-grmhd_conservatives.x.asc",
#              "giraffe-grmhd_primitives_allbutbi.x.asc","giraffe-grmhd_primitives_allbutbi.x.asc","giraffe-grmhd_primitives_allbutbi.x.asc",
#              "giraffe-em_psi6phi.x.asc"]
# column = 6
# column_old = [0,12,13,14,12,12,12,12,13,14,12,13,14,12]
# old_path = "/home/penelson/Cactus/exe/ABE-GiRaFFEfood_ExactWald_newcode_agreement"
# perturb_path = "/home/penelson/Cactus/exe/ABE-GiRaFFEfood_ExactWald_newcode_agreement_pert"
# new_path = os.path.join(Ccodesdir,"output")
# data_old = np.loadtxt(os.path.join(old_path,old_files[column]))
# data_per = np.loadtxt(os.path.join(perturb_path,old_files[column]))
# n=80
# data_old = data_old[n*70:n*70+70,:]# Select only the nth timestep
# data_per = data_per[n*70:n*70+70,:]# Select only the nth timestep
# data_new = np.loadtxt(os.path.join(new_path,"out64-00000080.txt"))

# deltaA_old = data_old[125:250,:] - data_old[0:125,:]
# data_new_t0 = np.loadtxt(os.path.join(new_path,"out119-00000000.txt"))
# deltaA_new = data_new[:,:] - data_new_t0[:,:]

# plt.figure()
# plt.semilogy(data_new[:,0],(data_new[:,column]-data_old[:,column_old[column]])/(data_new[:,column]+data_old[:,column_old[column]]),label="Old vs. New")

# plt.plot(data_new[4:22,0],data_new[4:22,column]-data_old[4:22,column_old[column]])
# plt.plot(data_new[49:-4,0],data_new[49:-4,column]-data_old[49:-4,column_old[column]])

# plt.plot(data_new[:,0],data_old[:,column_old[column]])
# plt.plot(data_new[:,0],data_new[:,column],'.')

# plt.plot(data_new[:22,0],data_old[:22,column_old[column]])
# plt.plot(data_new[49:,0],data_old[49:,column_old[column]])
# plt.plot(data_new[:22,0],data_new[:22,column],'.')
# plt.plot(data_new[49:,0],data_new[49:,column],'.')

# plt.plot(data_new[3:-3,0],deltaA_new[3:-3,column],'.')
# plt.plot(data_new[3:-3,0],deltaA_old[3:-3,column_old[column]]-deltaA_new[3:-3,column])
# plt.xlabel(labels[0])
# plt.ylabel(labels[column])
# plt.show()

# plt.figure()
# plt.semilogy(data_new[:,0],(data_old[:,column_old[column]]-data_per[:,column_old[column]])/(data_old[:,column_old[column]]+data_per[:,column_old[column]]),label="Old vs. Perturbed")
# plt.xlabel(labels[0])
# plt.ylabel("E_rel in "+labels[column])
# plt.legend()
# plt.show()
```

Here, we will use the output L2 norm data and find its convergence to determine how well our code converges.

By doing so, we show that the truncation error dominates and converges to zero at the expected rate. In the code above, we output the L2 norm within a thick spherical shell; for some quantity $Q$, this is given as
$$
| Q_{\rm approx} - Q_{\rm exact}| = \sqrt{\frac{1}{N} \sum_{ijk} \left( Q_{\rm approx} - Q_{\rm exact} \right)^2}
$$

Here, we import that data and calculate the convergence in the usual way,
$$
k = \log_2 \left( \frac{F - F_1}{F - F_2} \right),
$$
where $k$ is the convergence order, $F$ is the exact solution, $F_1$ is the approximate solution on the coarser grid with resolution $\Delta x$, and $F_2$ is the approximate solution on the finer grid with resolution $\Delta x/2$. Note that for the exact solution, the L2 norm is $0$.

```python
# l2_lo = np.loadtxt(os.path.join(new_path,"out64-L2.txt"))
# l2_hi = np.loadtxt(os.path.join(new_path,"out128-L2.txt"))
# #          0   1     2     3     4     5     6     7          8          9          10            11            12             13
# labels = ["n","BU0","BU1","BU2","AD0","AD1","AD2","StildeD0","StildeD1","StildeD2","ValenciavU0","ValenciavU1","ValenciavU2", "psi6Phi"]

# def conv_order(low,high):
#     order = np.log2(low/high)
#     return order

# for column in range(1,len(labels)):
#     print(labels[column]+": "+str(conv_order(l2_lo[1,column],l2_hi[2,column])))
```

This code will create an animation of the wave over time.

```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("out64-00*.png")
globby = glob.glob(os.path.join(Ccodesdir,'output','out64-00*.txt'))
file_list = []
for x in sorted(globby):
    file_list.append(x)

number_of_files = int(len(file_list))

for timestep in range(number_of_files):
    fig = plt.figure()
    numer_filename = file_list[timestep]
    exact_filename = os.path.join(Ccodesdir,'output','out64-00000000.txt')
    Numer = np.loadtxt(numer_filename)
    Exact = np.loadtxt(exact_filename)

    plt.title("Exact Wald")
    plt.xlabel("x")
    plt.ylabel("BU2")
#     plt.xlim(-0.5,0.5)
#     plt.ylim(1.0,1.7)

    plt.plot(Numer[3:22,0],Numer[3:22,3],'.',label="Numerical")
    plt.plot(Numer[49:-3,0],Numer[49:-3,3],'.',label="Numerical")
    plt.plot(Exact[3:22,0],Exact[3:22,3],label="Exact")
    plt.plot(Exact[49:-3,0],Exact[49:-3,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_new_way_standalone_Ccodes/output/out64-00000087.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-3D_tests.mp4")

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

myimages = []

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

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

```python
%%HTML

```

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

    Created Tutorial-GiRaFFE_NRPy_Main_Driver.tex, and compiled LaTeX file to
        PDF file Tutorial-GiRaFFE_NRPy_Main_Driver.pdf


```python
# 그래프, 수학 기능 추가
# Add graph and math features
import pylab as py
import numpy as np
import numpy.linalg as nl
# 기호 연산 기능 추가
# Add symbolic operation capability
import sympy as sy

```

# 파이썬에서의 선형대수 : 표준 기능
Linear Algebra in Python: Standard Library

(Karlijn Willems, SciPy Cheat Sheet: Linear Algebra in Python, DataCamp, 2017/02/07, https://www.datacamp.com/community/blog/python-scipy-cheat-sheet)

(Wikipedia contributors. Linear algebra. Wikipedia, The Free Encyclopedia. August 2, 2018, 17:17 UTC. Available at: https://en.wikipedia.org/w/index.php?title=Linear_algebra&oldid=853134963. Accessed August 11, 2018. )

**선형 대수**란 간단히 말하면 벡터와 행렬에 관한 수학의 한 분야이다.

In short, **Linear Algebra** is an area of mathematics about vectors and matrices.

파이썬 프로그래밍 언어의 기본 기능만으로도 선형 대수 문제를 해결하는 것이 가능은 하나, 보다 효율을 높이기 위해, 1990년대 이후, 여러 개발자들의 공헌으로 [**사이파이** 계열 확장 모듈](https://www.scipy.org/stackspec.html)을 개발하였다.

We can solve linear algebra with default features of python. However, to make it more efficient, since 1990's, a group of community developers contributed in developing [**SciPy** stack](https://www.scipy.org/stackspec.html).

여기서는 다른 모듈을 사용하지 않고 파이썬 자체의 문법만 사용할 것이다.

Here we would use the Python grammar itself without using other modules.

# 벡터
Vector

벡터는 크기만이 아니라 방향도 가지는 양이다.

Vectors is a quantity having both its magnitude and its direction.

벡터에 관해 아래 비디오 링크를 참고할 수 있다. (5:48)

Regarding the vector, you can check the following link. (5:48)

[](https://www.youtube.com/watch?v=br7tS1t2SFE)

예를 들어 2차원 벡터를 생각해 보자.

For example, let's think about two-dimensional vectors (2D vectors).

## 벡터 정의 : 표준 기능
Definition of vectors : Standard Library

리스트 list 또는 튜플 tuple 을 이용하여 다음과 같이 벡터를 정의할 수 있다.

We can define the vectors using the list or tuple as follows.

```python
# ref : https://www.youtube.com/watch?v=8QihetGj3pg
a = [6, -2] # list
b = (-4, 4) # tuple

```

```python
a

```

```python
b

```

위 2차원 벡터를 한번 그려 보자

Let's plot the 2D vectors above.

```python
def draw_2dvec(x, y, x0=0, y0=0, color='k', name=None):
    py.quiver(x0, y0, x, y, color=color, angles='xy', scale_units='xy', scale=1)
    if name is not None:
        if not name.startswith('$'):
            vec_str = '$\\vec{%s}$' % name
        else:
            vec_str = name
        py.text(0.5 * x + x0, 0.5 * y + y0, vec_str)

```

```python
draw_2dvec(a[0], a[1], name='a')
draw_2dvec(b[0], b[1], name='b')

py.axis('equal')
py.xlim((-8, 8))
py.ylim((-8, 8))
py.grid(True)

```

## 벡터 합 : 표준 기능
Sum of two vectors : Standard Library

두 벡터를 더해 보자.
Let's add two vectors.

```python
def add_two_vectors(a, b):

    result = []

    for ai, bi in zip(a, b):
        print(f"zip : ai={ai}, bi={bi}")
        result.append(ai + bi)

    return result

```

```python
a_plus_b = add_two_vectors(a, b)

```

```python
a_plus_b

```

이 벡터의 합을 그려보자.

Let's draw this sum of vectors.

```python
draw_2dvec(a[0], a[1], name='a')
draw_2dvec(b[0], b[1], name='b')
draw_2dvec(a_plus_b[0], a_plus_b[1], name='$\\vec{a}+\\vec{b}$')

py.axis('equal')
py.xlim((-8, 8))
py.ylim((-8, 8))
py.grid(True)

```

어떻게 해서 벡터의 합은 이렇게 된 것일까? $\vec{b}$ 벡터의 시작점을 $\vec{a}$ 벡터의 끝점으로 옮겨 보자.

How come this vector sum came up like this?  Let's move the starting point of $\vec{b}$ to the starting point of $\vec{a}$.

```python
draw_2dvec(a[0], a[1], name='a')
draw_2dvec(b[0], b[1], a[0], a[1], color=(0.5, 0.5, 0.5), name='b')
draw_2dvec(b[0], b[1], name='b')
draw_2dvec(a_plus_b[0], a_plus_b[1], name='$\\vec{a}+\\vec{b}$')

py.axis('equal')
py.xlim((-8, 8))
py.ylim((-8, 8))
py.grid(True)

```

여기서 $\vec{a}$, $\vec{b}$ 그리고 $\vec{a} + \vec{b}$ 가 삼각형을 이룬다는 것을 알 수 있다.

Here, you can see that $\vec{a}$, $\vec{b}$, and $\vec{a} + \vec{b}$ form a triangle.

$\vec{b}$의 시작점을 $\vec{a}$의 끝점으로 옮긴 결과, 회색 벡터의 끝점이 $\vec{a} + \vec{b}$ 의 끝점과 같다.

As the result of moving the start point of $\vec{b}$ to the end point of $\vec{a}$, the end points of the gray vector and $\vec{a} + \vec{b}$ are identicial.

### 교환법칙 : 표준 기능
Commutative Law : Standard Library

벡터의 합의 순서를 바꾸어 보자.
Let's change the order of addition.

```python
b_plus_a = add_two_vectors(b, a)

```

```python
b_plus_a

```

이는 $\vec{a}+\vec{b}$와 같다.
This is the same as $\vec{a}+\vec{b}$.

이번에도 시각화 해 보자.
Let's visualize again.

```python
draw_2dvec(a[0], a[1], name='a')
draw_2dvec(b[0], b[1], name='b')
draw_2dvec(a[0], a[1], b[0], b[1], color=(0.75, 0.75, 0.75), name='a')
draw_2dvec(b_plus_a[0], b_plus_a[1], name='$\\vec{b}+\\vec{a}$')

py.axis('equal')
py.xlim((-8, 8))
py.ylim((-8, 8))
py.grid(True)

```

비슷하게, $\vec{a}$, $\vec{b}$ 그리고 $\vec{b} + \vec{a}$ 가 삼각형을 이룬다는 것을 알 수 있다.

Similarly, you can see that $\vec{a}$, $\vec{b}$, and $\vec{b} + \vec{a}$ form a triangle.

이번에는 $\vec{a}$의 시작점을 $\vec{b}$의 끝점으로 옮겨 보았다. 덧셈의 순서와는 상관 없이, 회색 벡터의 끝점이 $\vec{b} + \vec{a}$ 의 끝점과 일치하는 것을 확인할 수 있다.

This time, we moved the start point of $\vec{a}$ to the end point of $\vec{b}$. We can confirm that regardless of the order of addition, the end points of the gray vector and $\vec{a} + \vec{b}$ are identicial.

두 방식을 모두 표시해 보자.
Let's indicate both ways.

```python
draw_2dvec(a[0], a[1], name='a')
draw_2dvec(b[0], b[1], name='b')
draw_2dvec(a[0], a[1], b[0], b[1], color=(0.75, 0.75, 0.75), name='a')
draw_2dvec(b[0], b[1], a[0], a[1], color=(0.5, 0.5, 0.5), name='b')
draw_2dvec(b_plus_a[0], b_plus_a[1], name='$\\vec{a}+\\vec{b}$')

py.axis('equal')
py.xlim((-8, 8))
py.ylim((-8, 8))
py.grid(True)

```

벡터 합은 $\vec{a}$와 $\vec{b}$가 이루는 평행 사변형의 한 대각선임을 알 수 있다.

We can see that the vector sum is one of diagonals of the parallogram of $\vec{a}$'s and $\vec{b}$'s.

## 스칼라와 벡터의 곱 : 표준 기능
Product of a Scalar and a Vector : Standard Library

벡터에 어떤 스칼라 값을 곱해 보자.
Let's multiply a scalar value to a vector.

```python
def scalar_mul(a, x_vector):
    result = [0.0] * len(x_vector)

    for k in range(len(x_vector)):
        result[k] = a * x_vector[k]

    return result

```

```python
x = (2, 1)
alpha = 3
alpha_x = scalar_mul(alpha, x)

```

```python
alpha_x

```

그림으로 표시해 보자.
Let's draw.

```python
draw_2dvec(alpha_x[0], alpha_x[1], name='$\\alpha\\vec{x}$', color=(0.5, 0.5, 0.5))
draw_2dvec(x[0], x[1], name='x')

py.axis('equal')
py.xlim((-6, 6))
py.ylim((-6, 6))
py.grid(True)

```

방향은 바뀌지 않고 크기만 달라지는 것을 알 수 있다.

The direction does not change but the magnitude changes.

스칼라 값이 음인 경우는 어떠할까?
What if scalar value is negative?

```python
x = (2, 1)
beta = -1
beta_x = scalar_mul(beta, x)

```

```python
beta_x

```

```python
draw_2dvec(alpha_x[0], alpha_x[1], name='$\\alpha\\vec{x}$', color=(0.5, 0.5, 0.5))
draw_2dvec(beta_x[0], beta_x[1], name='$\\beta\\vec{x}$', color=py.ones((1, 3)) * 0.25)
draw_2dvec(x[0], x[1], name='x')

py.axis('equal')
py.xlim((-6, 6))
py.ylim((-6, 6))
py.grid(True)

```

음의 스칼라를 곱하면 방향이 반대로 된다는 것을 알 수 있다.

In case of the negative scalar, the direction becomes the opposite.

## 벡터의 차 : 표준 기능
Difference of two vectors : Standard Library

어떤 벡터 $\vec{b}$를 다른 벡터 $\vec{a}$에서 빼는 셈에 대해 생각해 보자.

Let's think about subtracting a vector $\vec{b}$ from another vector $\vec{a}$.

```python
a_minus_b = add_two_vectors(a, scalar_mul(-1, b))

```

```python
a_minus_b

```

그림으로 표시보자.

Let's visualize.

```python
draw_2dvec(a[0], a[1], name='a')
draw_2dvec(b[0], b[1], name='b')
draw_2dvec(a_minus_b[0], a_minus_b[1], name='$\\vec{a}-\\vec{b}$')

py.axis('equal')
py.xlim((-10, 10))
py.ylim((-10, 10))
py.grid(True)

```

이번에는 어떻게 해서 벡터의 차가 이렇게 된 것인지 알아 보자. $\vec{b}$ 벡터에 -1을 곱해서 시작점을 $\vec{a}$ 벡터의 끝점으로 옮겨 보자.

Let's figure out the vector subtraction.  Let's multiply by -1 to $\vec{b}$ and move the starting point to the end point of $\vec{a}$.

```python
draw_2dvec(a[0], a[1], name='a')
draw_2dvec(-b[0], -b[1], a[0], a[1], color=(0.5, 0.5, 0.5), name='$-\\vec{b}$')
draw_2dvec(b[0], b[1], name='b')
draw_2dvec(a_minus_b[0], a_minus_b[1], name='$\\vec{a}-\\vec{b}$')

py.axis('equal')
py.xlim((-10, 10))
py.ylim((-10, 10))
py.grid(True)

```

벡터 뺄셈은 부호를 바꾸어 더하는 것과 같다.
Subtracting a vector is equivalent to changing the sign of the vector and adding it.

 이번에는 $\vec{a}-\vec{b}$ 의 시작점을 $\vec{b}$의 끝점으로 옮겨 보자.

 This time, let's move the start point of $\vec{a}-\vec{b}$ to the end point of $\vec{b}$.

```python
draw_2dvec(a[0], a[1], name='a')
draw_2dvec(b[0], b[1], name='b')
draw_2dvec(a[0], a[1], b[0], b[1], color=(0.75, 0.75, 0.75), name='a')
draw_2dvec(b[0], b[1], a[0], a[1], color=(0.5, 0.5, 0.5), name='b')
draw_2dvec(a_minus_b[0], a_minus_b[1], b[0], b[1], name='$\\vec{a}-\\vec{b}$')

py.axis('equal')
py.xlim((-8, 8))
py.ylim((-8, 8))
py.grid(True)

```

이는 다음을 뜻한다.

This means the following.

$$
\vec{b}+\left(\vec{a}-\vec{b}\right)=\vec{a}
$$

또한, $\vec{a}-\vec{b}$ 도 $\vec{a}$와 $\vec{b}$가 이루는 평행 사변형의 다른 대각선임을 알 수 있다.

We can also see that the vector subtraction is the other diagonal of the parallogram of $\vec{a}$'s and $\vec{b}$'s.

## 연습 문제
Exercise

* 임의의 두 2차원 벡터를 파이썬 튜플 tuple 로 정의하시오
Define two 2-dimensional vectors as python tuples.

```python

```

* 위에서 보인 예와 같이 두 벡터를 그려 보시오
Plot these two vectors as above

```python

```

* 두 벡터의 합과 두 벡터를 함께 그려 보시오
Plot the sum of two vectors with the two vectors

```python

```

* 두 벡터와 두 벡터의 합, 두 벡터의 차를 함께 그려 보시오
Plot the two vectors, the sum vector and the difference vector

```python

```

## 튜플 `tuple` 과 리스트 `list`
`tuple` and `list`

리스트 `list` 와 튜플 `tuple` 모두 여러 항목을 저장할 수 있다.
Both `list` and `tuple` can store multiple items.

또한 각 항목을 순서로 구분한다.
Also, distinguish each item with the order.

리스트는 항목을 변경 추가 삭제할 수 있으나 (mutable) 튜플은 한번 만들어지면 변경할 수 없다 (immutable).

We may change, add, or remove items in lists (mutable) but we cannot with tuples (immutable).

예를 들어 다음 함수를 생각해 보자.
Let's check following function.

```python
def multipy_scalar(a, x_vector):
    y_vector = x_vector

    for k in range(len(x_vector)):
        y_vector[k] = a * x_vector[k]

    return y_vector

```

```python
x_list = [1, 2, 3]
a_sample = 0.1
y_sample = multipy_scalar(a_sample, x_list)

```

```python
y_sample

```

```python
x_list

```

튜플 tuple 의 경우, 항목을 바꿀 수 없으므로 아래 예에서 예외 Exception 가 발생한다.

Because a tuple does not allow chaning one of its items, following example would raise an Exception.

```python
try:
    x_tuple = (4, 5, 6)
    b_sample = 0.01
    y_sample = multipy_scalar(b_sample, x_tuple)
except TypeError as e:
    print(e)
    print('cannot change an item in tuple')

```

## `zip`()

기본 내장 함수인 `zip()` 은 여러 모음에서 한 항목씩 꺼내어 만든 튜플 `tuple` 을 *생성*한다.

Built-in function `zip()` generates `tuple`s of items from multiple collections.

```python
a_str = 'abc'
b_list = [0, 1, 2]
c_dict = {'x':'xylophone', 'y':'yield'}

for a_b_c in zip(a_str, b_list, c_dict):
    print(a_b_c)

```

한 모음이라도 항목이 다 떨어지면 중단한다.

The iteration stops when the smallest collection exhausts.

### 리스트 컴프리헨션
List comprehension

다음 결과를 비교하시오.

Compare the results of following cells.

```python
s_for = []

for i in range(10):
    s_for.append(2 * i + 1)

print(s_for)

```

```python
s_list_comprehension = [2 * j + 1 for j in range(10)]

print(s_list_comprehension)

```

```python
s_map = list(map(lambda k: 2 * k + 1, range(10)))

print(s_map)

```

`set()` 이나 `dict()`에 대해서도 적용 가능하다.

It is applicable to `set()` or `dict()`.

```python
input_str = 'set comprehension'

```

```python
set_for = set()

for c in input_str:
    set_for.add(c*2)

print(set_for)

```

```python
set_comprehension = {2 * j for j in input_str}

print(set_comprehension)
print(set_for == set_comprehension)

```

```python
input_str = 'dictionary comprehension'

```

```python
dict_for = {}

for c in input_str:
    # ASCII code for c = ord(c)
    dict_for[c] = ord(c)

print(dict_for)

```

```python
dict_comprehension = {c:ord(c) for c in input_str}

print(dict_comprehension)

```

## `quiver()`

이 함수는 다수의 화살표를 동시에 생성한다.

This function would generate a group of arrows at the same time.

아래 셀의 결과를 비교하시오.
Compare the result of the following cells.

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

```

```python
x_deg = np.linspace(0, 360)
y = np.zeros_like(x_deg)
u = np.zeros_like(x_deg)
v = np.cos(np.deg2rad(x_deg))

plt.quiver(x_deg, y, u, v)
plt.show()

```

```python
x_deg = np.linspace(0, 360)
y = np.zeros_like(x_deg)
u = np.zeros_like(x_deg)
v = np.cos(np.deg2rad(x_deg))

plt.plot(x_deg, v)
plt.quiver(x_deg, y, u, v)

plt.show()

```

## 두 벡터의 합을 구하는 다른 방법
Other ways to add two vectors

리스트 항목 순서
Indices of lists

```python
def add_two_vectors_index(a, b):

    result = [0.0] * len(a)

    for index in range(len(a)):
        print(f"index : a[{index}]={a[index]}, b[{index}]={b[index]}")
        result[index] = a[index] + b[index]

    return result

```

`zip()`

```python
def add_two_vectors_zip(a, b):

    result = []

    for ai, bi in zip(a, b):
        print(f"zip : ai={ai}, bi={bi}")
        result.append(ai + bi)

    return result

```

`map()`

```python
def add_two_vectors_map(a, b):
    return list(
        map(
            lambda ai_bi: sum(ai_bi),
            zip(a, b)
        )
    )

```

리스트 줄여쓰기
List comprehension

```python
def add_two_vectors_list_comprehension(a, b):
    return [(ai + bi) for ai, bi in zip(a, b)]

```

결과는 모두 같다.
All three methods give same results.

```python
x = [1, 2, 3]
y = [0.1, 0.2, 0.3]

print('result =', add_two_vectors(x, y))
print('result (index) =', add_two_vectors_index(x, y))
print('result (zip) =', add_two_vectors_zip(x, y))
print('result (map) =', add_two_vectors_map(x, y))
print('result (list comprehension) =', add_two_vectors_list_comprehension(x, y))

```

## `assert`

`assert` 명령은 프로그램이 예상했던 대로 작동하는지 확인할 수 있는 방법이다.

`assert` is one of ways to check if program is working as expected.

```
assert <logic>, <message>
```

위 셀에서 `<logic>` 위치의 논리식이 거짓이면 `AssertionError` 예외를 발생시킨다.

In the cell above, when the logical expression of `<logic>` is False, it would raise an exception of `AssertionError`.

### 벡터 합
Vector addition

벡터 덧셈의 경우, 두 벡터의 크기가 같은지 확인할 필요가 있다.

In case of vector addition, it is necessary to check if the lengths of two vectors are the same.

```python
def add_two_vectors(a, b):

    assert len(a) == len(b), f"len(a) = {len(a)}, len(b) = {len(b)}"

    result = []

    for ai, bi in zip(a, b):
        result.append(ai + bi)

    return result

```

두 벡터의 길이가 다르면 `AssertionError` 를 발생 시킬 것이다.

If lengths of two vectors are different, it would raise an `AssertionError`.

## 시험
Tests

```python
import random

test_alpha_scalar = random.random()*2 - 1

test_x_vector = [random.random()*2 - 1, random.random()*2 - 1]

expected = [(test_x_vector[0] * test_alpha_scalar), (test_x_vector[1] * test_alpha_scalar)]

result = scalar_mul(test_alpha_scalar, test_x_vector)

assert expected == result, (
    f"\nscalar = {test_alpha_scalar}\n"
    f"input vector = {test_x_vector}\n"
    f"expected = {expected}\n"
    f"result = {result}\n"
)

```

## Final Bell
마지막 종

```python
# stackoverfow.com/a/24634221
import os
os.system("printf '\a'");

```

```python

```


# Tutorial 2: Facial recognition using modern convnets
**Week 2, Day 2: Modern Convnets**

**By Neuromatch Academy**

__Content creators:__ Laura Pede, Richard Vogg, Marissa Weis, Timo Lüddecke, Alexander Ecker (based on an initial version by Ben Heil)

__Content reviewers:__ Arush Tagade, Polina Turishcheva, Yu-Fang Yang, Bettina Hein, Melvin Selim Atay

__Content editors:__ Roberto Guidotti, Spiros Chavlis

__Production editors:__ Anoop Kulkarni, Roberto Guidotti, Cary Murray, Spiros Chavlis

**Our 2021 Sponsors, including Presenting Sponsor Facebook Reality Labs**

---
# Tutorial Objectives

In this tutorial you will learn about:

1. An application of modern CNNs in facial recognition.
2. Ethical aspects of facial recognition.

```python
# @title Tutorial slides

# @markdown These are the slides for the videos in this tutorial
from IPython.display import IFrame
IFrame(src=f"https://mfr.ca-1.osf.io/render?url=https://osf.io/4r2dp/?direct%26mode=render%26action=download%26mode=render", width=854, height=480)
```

---
# Setup

```python
# @title Install dependencies
# @markdown Install facenet - a model used to do facial recognition
!pip install facenet-pytorch --quiet
!pip install pillow --quiet
```

```python
# Imports
import glob
import torch

import numpy as np
import sklearn.decomposition
import matplotlib.pyplot as plt

from PIL import Image

from torchvision import transforms
from torchvision.utils import make_grid
from torchvision.datasets import ImageFolder

from facenet_pytorch import MTCNN, InceptionResnetV1
```

```python
# @title Set random seed

# @markdown Executing `set_seed(seed=seed)` you are setting the seed

# for DL its critical to set the random seed so that students can have a
# baseline to compare their results to expected results.
# Read more here: https://pytorch.org/docs/stable/notes/randomness.html

# Call `set_seed` function in the exercises to ensure reproducibility.
import random
import torch

def set_seed(seed=None, seed_torch=True):
  if seed is None:
    seed = np.random.choice(2 ** 32)
  random.seed(seed)
  np.random.seed(seed)
  if seed_torch:
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    torch.cuda.manual_seed(seed)
    torch.backends.cudnn.benchmark = False
    torch.backends.cudnn.deterministic = True

  print(f'Random seed {seed} has been set.')

# In case that `DataLoader` is used
def seed_worker(worker_id):
  worker_seed = torch.initial_seed() % 2**32
  np.random.seed(worker_seed)
  random.seed(worker_seed)
```

```python
# @title Set device (GPU or CPU). Execute `set_device()`
# especially if torch modules used.

# inform the user if the notebook uses GPU or CPU.

def set_device():
  device = "cuda" if torch.cuda.is_available() else "cpu"
  if device != "cuda":
    print("WARNING: For this notebook to perform best, "
        "if possible, in the menu under `Runtime` -> "
        "`Change runtime type.`  select `GPU` ")
  else:
    print("GPU is enabled in this notebook.")

  return device
```

```python
SEED = 2021
set_seed(seed=SEED)
DEVICE = set_device()
```

---
# Section 1: Face Recognition

## Section 1.1: Download and prepare the data

```python
# @title Download Data

!rm -rf cis_522_data/
!git clone --quiet https://github.com/ben-heil/cis_522_data.git
!tar -xzf cis_522_data/archive.tar.gz
!tar -xzf cis_522_data/faces.tar.gz
```

```python
# @title Video 1: Face Recognition using CNNs
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
      self.id=id
      src = "https://player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
      super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id=f"BV17B4y1K7WV", width=854, height=480, fs=1)
  print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id=f"jJqEv8hpRa4", width=854, height=480, fs=1, rel=0)
  print("Video available at https://youtube.com/watch?v=" + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

One application of large CNNs is **facial recognition**. The problem formulation in facial recognition is a little different from the image classification we've seen so far. In facial recognition, we don't want to have a fixed number of individuals that the model can learn. If that were the case then to learn a new person it would be necessary to modify the output portion of the architecture and retrain to account for the new person.

Instead, we train a model to learn an **embedding** where images from the same individual are close to each other in an embedded space, and images corresponding to different people are far apart. When the model is trained, it takes as input an image and outputs an embedding vector corresponding to the image.

To achieve this, facial recognitions typically use a **triplet loss** that compares two images from the same individual (i.e., "anchor" and "positive" images) and a negative image from a different individual (i.e., "negative" image). The loss requires the distance between the anchor and negative points to be greater than a margin $\alpha$ + the distance between the anchor and positive points.

## Section 1.2: View and transform the data

A well-trained facial recognition system should be able to map different images of the same individual relatively close together. We will load 15 images of three individuals (maybe you know them - then you can see that your brain is quite well in facial recognition).

After viewing the images, we will transform them: MTCNN ([github repo](https://github.com/ipazc/mtcnn)) detects the face and crops the image around the face. Then we stack all the images together in a tensor.

```python
# @title Display Images
# @markdown Here are the source images of Bruce Lee, Neil Patrick Harris, and Pam Grier
train_transform = transforms.Compose((transforms.Resize((256, 256)),
                                      transforms.ToTensor()))

face_dataset = ImageFolder('faces', transform=train_transform)

image_count = len(face_dataset)

face_loader = torch.utils.data.DataLoader(face_dataset,
                                          batch_size=45,
                                          shuffle=False)

dataiter = iter(face_loader)
images, labels = dataiter.next()

# show images
plt.figure(figsize=(15, 15))
plt.imshow(make_grid(images, nrow=15).permute(1, 2, 0))
plt.axis('off')
plt.show()
```

```python
# @title Image Preprocessing Function
def process_images(image_dir: str, size=256):
  """
  This function returns two tensors for the given image dir: one usable for inputting into the
  facenet model, and one that is [0,1] scaled for visualizing

  Parameters:
    image_dir: The glob corresponding to images in a directory

  Returns:
    model_tensor: A image_count x channels x height x width tensor scaled to between -1 and 1,
                  with the faces detected and cropped to the center using mtcnn
    display_tensor: A transformed version of the model tensor scaled to between 0 and 1
  """
  mtcnn = MTCNN(image_size=size, margin=32)
  images = []
  for img_path in glob.glob(image_dir):
    img = Image.open(img_path)
    # Normalize and crop image
    img_cropped = mtcnn(img)
    images.append(img_cropped)

  model_tensor = torch.stack(images)
  display_tensor = model_tensor / (model_tensor.max() * 2)
  display_tensor += .5

  return model_tensor, display_tensor
```

Now that we have our images loaded, we need to preprocess them. To make the images easier for the network to learn, we crop them to include just faces.

```python
bruce_tensor, bruce_display = process_images('faces/bruce/*.jpg')
neil_tensor, neil_display = process_images('faces/neil/*.jpg')
pam_tensor, pam_display = process_images('faces/pam/*.jpg')

tensor_to_display = torch.cat((bruce_display, neil_display, pam_display))

plt.figure(figsize=(15, 15))
plt.imshow(make_grid(tensor_to_display, nrow=15).permute(1, 2, 0))
plt.axis('off')
plt.show()
```

## Section 1.3: Embedding with a pretrained network

We load a pretrained facial recognition model called [FaceNet](https://github.com/timesler/facenet-pytorch). It was trained on the [VGGFace2](https://github.com/ox-vgg/vgg_face2) dataset which contains 3.31 million images of 9131 individuals.

We use the pretrained model to calculate embeddings for all of our input images.

```python
resnet = InceptionResnetV1(pretrained='vggface2').eval().to(DEVICE)
```

```python
# Calculate embedding
resnet.classify = False
bruce_embeddings = resnet(bruce_tensor.to(DEVICE))
neil_embeddings = resnet(neil_tensor.to(DEVICE))
pam_embeddings = resnet(pam_tensor.to(DEVICE))
```

### Think! 1.3: Embedding vectors

We want to understand what happens when the model receives an image and returns the corresponding embedding vector.

- What are the height, width and number of channels of one input image?
- What are the dimensions of one stack of images (e.g. bruce_tensor)?
- What are the dimensions of the corresponding embedding (e.g. bruce_embeddings)?
- What would be the dimensions of the embedding of one input image?

**Hints:**
- You can double click on a variable name and hover over it to see the dimensions of tensors.
- You do not have to answer the questions in the order they are asked.

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content-dl/tree/main//tutorials/W2D2_ModernConvnets/solutions/W2D2_Tutorial2_Solution_bbe072ff.py)

We cannot show 512-dimensional vectors visually, but using **Principal Component Analysis (PCA)** we can project the 512 dimensions onto a 2-dimensional space while preserving the maximum amount of data variation possible. This is just a visual aid for us to understand the concept. Note that if you would like to do any calculation, like distances between two images, this would be done with the whole 512-dimensional embedding vectors.

```python
embedding_tensor = torch.cat((bruce_embeddings,
                              neil_embeddings,
                              pam_embeddings)).to(device='cpu')

pca = sklearn.decomposition.PCA(n_components=2)
pca_tensor = pca.fit_transform(embedding_tensor.detach().cpu().numpy())
```

```python
num = 15
categs = 3
colors = ['blue', 'orange', 'magenta']
labels = ['Bruce Lee', 'Neil Patrick Harris', 'Pam Grier']
markers = ['o', 'x', 's']
plt.figure(figsize=(8, 8))
for i in range(categs):
   plt.scatter(pca_tensor[i*num:(i+1)*num, 0],
               pca_tensor[i*num:(i+1)*num, 1],
               c=colors[i],
               marker=markers[i], label=labels[i])
plt.legend()
plt.title('PCA Representation of the Image Embeddings')
plt.xlabel('PC 1')
plt.ylabel('PC 2')
plt.show()
```

Great! The images corresponding to each individual are separated from each other in the embedding space!

If Neil Patrick Harris wants to unlock his phone with facial recognition, the phone takes the image from the camera, calculates the embedding and checks if it is close to the registered embeddings corresponding to Neil Patrick Harris.

---
# Section 2: Ethics – bias/discrimination due to pre-training datasets
Popular facial recognition datasets like VGGFace2 and CASIA-WebFace consist primarily of caucasian faces.
As a result, even state of the art facial recognition models [substantially underperform](https://openaccess.thecvf.com/content_ICCV_2019/papers/Wang_Racial_Faces_in_the_Wild_Reducing_Racial_Bias_by_Information_ICCV_2019_paper.pdf) when attempting to recognize faces of other races.

Given the implications that poor model performance can have in fields like security and criminal justice, it's very important to be aware of these limitations if you're going to be building facial recognition systems.

In this example we will work with a small subset from the [UTKFace](https://susanqq.github.io/UTKFace/) dataset with 49 pictures of black women and 49 picture of white women. We will use the same pretrained model as in Section 8 of Tutorial 1, see and discuss the consequences of the model being trained on an imbalanced dataset.

```python
# @title Video 2: Ethical aspects
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
      self.id=id
      src = "https://player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
      super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id=f"BV1Jo4y1Q7K3", width=854, height=480, fs=1)
  print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id=f"vYilJV3PqUM", width=854, height=480, fs=1, rel=0)
  print("Video available at https://youtube.com/watch?v=" + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

## Section 2.1: Download the Data

```python
# @title Run this cell to get the data and free ram by deleting old variables

!rm -rf cis_522_data/

if 'embedding_tesnor' in locals():
  del embedding_tensor
if 'pca_tesnor' in locals():
  del pca_tensor
if 'tensor_to_display' in locals():
  del tensor_to_display
if 'bruce_embeddings' in locals():
  del bruce_embeddings
if 'neil_embeddings' in locals():
  del neil_embeddings
if 'pam_embeddings' in locals():
  del pam_embeddings

# remove directories, if previously run.
!rm -rf __MACOSX/
!rm -rf face_sample/
!rm -rf face_sample2/
!git clone --quiet https://github.com/richardvogg/face_sample.git
!unzip -q face_sample/face_sample2.zip
```

## Section 2.2: Load, view and transform the data

```python
black_female_tensor, black_female_display = process_images('face_sample2/??_1_1_*.jpg', size=150)
white_female_tensor, white_female_display = process_images('face_sample2/??_1_0_*.jpg', size=150)
```

We can check the dimensions of these tensors and see that for each group we have images of size 150x150 and three channels (RGB) of 49 individuals.

**Note:** Originally, the size of images was 200x200, but due to RAM resources, we have reduce it. You can change it back, i.e., `size=200`.

```python
print(white_female_tensor.shape)
print(black_female_tensor.shape)
```

```python
# @title Visualize some example faces
tensor_to_display = torch.cat((white_female_display[:15],
                               black_female_display[:15]))

plt.figure(figsize=(12, 12))
plt.imshow(make_grid(tensor_to_display, nrow = 15).permute(1, 2, 0))
plt.axis('off')
plt.show()
```

## Section 2.3: Calculate embeddings

We use the same pretrained facial recognition network as in section 8 to calculate embeddings. If you have memory issues running this part, go to Edit > Notebook settings and check if GPU is selected as Hardware accelerator. If this does not help you can restart the notebook, go to Runtime -> Restart runtime.

```python
resnet.classify = False
black_female_embeddings = resnet(black_female_tensor.to(DEVICE))
white_female_embeddings = resnet(white_female_tensor.to(DEVICE))
```

We will use the embeddings to show that the model was trained on an imbalanced dataset. For this, we are going to calculate a distance matrix of all combinations of images, like in this small example with $n=3$ (in our case $n=98$).

Calculate the distance between each pair of image embeddings in our tensor and visualize all the distances. Remember that two embeddings are vectors and the distance between two vectors is the Euclidean distance.

```python
# @title Function to calculate pairwise distances
# @markdown `torch.cdist` is used
def calculate_pairwise_distances(embedding_tensor):
  """
  This function calculates the distance between each pair of image embeddings
  in a tensor using the `torch.cdist`.

  Parameters:
    embedding_tensor : torch.Tensor
      A num_images x embedding_dimension tensor

  Returns:
    distances : torch.Tensor
      A num_images x num_images tensor containing the pairwise distances between
      each to image embedding
  """

  distances = torch.cdist(embedding_tensor, embedding_tensor)

  return distances
```

```python
# @title Visualize the distances

embedding_tensor = torch.cat((black_female_embeddings,
                              white_female_embeddings)).to(device='cpu')

distances = calculate_pairwise_distances(embedding_tensor)

plt.figure(figsize=(8, 8))
plt.imshow(distances.detach().cpu().numpy())
plt.annotate('Black female', (2, -0.5), fontsize=20, va='bottom')
plt.annotate('White female', (52, -0.5), fontsize=20, va='bottom')
plt.annotate('Black female', (-0.5, 45), fontsize=20, rotation=90, ha='right')
plt.annotate('White female', (-0.5, 90), fontsize=20, rotation=90, ha='right')
cbar = plt.colorbar()
cbar.set_label('Distance', fontsize=16)
plt.axis('off')
plt.show()
```

## Exercise 2.1

What do you observe? The faces of which group are more similar to each other for the Face Detection algorithm?

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content-dl/tree/main//tutorials/W2D2_ModernConvnets/solutions/W2D2_Tutorial2_Solution_866d0881.py)

## Exercise 2.2
- What does it mean in real life applications that the distance is smaller between the embeddings of one group?
- Can you come up with example situations/applications where this has a negative impact?
- What could you do to avoid these problems?

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content-dl/tree/main//tutorials/W2D2_ModernConvnets/solutions/W2D2_Tutorial2_Solution_b92b2f40.py)

Lastly, to show the importance of the dataset which you use to pretrain your model, look at how much space white men and women take in different embeddings. FairFace is a dataset which is specifically created with completely balanced classes. The blue dots in all visualizations are white male and white female.

Adopted from [Kärkkäinen and Joo, 2019, arXiv](https://arxiv.org/abs/1908.04913)

---
# Summary

In this tutorial we have learned how to apply a modern convnet in real application such as facial recognition. However, as the state-of-the-art tools for facial recognition are trained mostly with caucasian faces, they fail or they perform much worst when they have to deal with faces from other races.

```python
# @title Video 3: Summary and Outlook
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
      self.id=id
      src = "https://player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
      super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id=f"BV1So4y1D7Ev", width=854, height=480, fs=1)
  print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id=f"sjj0-7i6XfE", width=854, height=480, fs=1, rel=0)
  print("Video available at https://youtube.com/watch?v=" + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

---
# Bonus : Within Sum of Squares

We can try to put this observation in numbers. For this we work with the embeddings.
We want to calculate the centroid of each group, which is the average of the 49 embeddings of the group. As each embedding vector has a dimension of 512, the centroid will also have this dimension.

Now we can calculate how far away the observations $x$ of each group $S_i$ are from the centroid $\mu_i$. This concept is known as Within Sum of Squares (WSS) from cluster analysis.

\begin{equation}
\text{WSS} = \sum_{x\in S_i} ||x - \mu_i||^2
\end{equation}

where $|| \cdot ||$ is the Euclidean norm.

The Within Sum of Squares (WSS) is a number which measures this variability of a group in the embedding space. If all embeddings of one group were very close to each other, the WSS would be very small. In our case we see that the WSS for the black females is much smaller than for the white females. This means that it is much harder for the model to distinguish two black females than to distinguish two white females. The WSS complements the observation from the distance matrix, where we observed overall smaller pairwise distances between black females.

```python
# @title Function to calculate WSS

def wss(group):
  """
  This function returns the sum of squared distances of the N vectors of a
  group tensor (N x K) to its centroid (1 x K).

  Args:
    group: A image_count x embedding_size tensor

  Returns:
    sum_sq: A 1x1 tensor with the sum of squared distances.

  Hints:
    - to calculate the centroid, torch.mean() will be of use.
    - We need the mean of the N=49 observations. If our input tensor is of size
      N x K, we expect the centroid to be of dimensions 1 x K.
      Use the axis argument within torch.mean
    """

  centroid = torch.mean(group, axis=0)
  distance = torch.linalg.norm(group - centroid.view(1, -1), axis=1)
  sum_sq = torch.sum(distance**2)
  return sum_sq
```

```python
# @markdown Let's calculate the WSS for the two groups of our example.

print(f"Black female embedding WSS: {np.round(wss(black_female_embeddings).item(), 2)}")
print(f"White female embedding WSS: {np.round(wss(white_female_embeddings).item(), 2)}")
```


# Molecular Dynamics: Lab 1

In part based on [Fortran code from Furio Ercolessi](http://www.fisica.uniud.it/~ercolessi/md/f90/).

```python
from IPython.core.display import HTML
css_file = 'https://raw.githubusercontent.com/ngcm/training-public/master/ipython_notebook_styles/ngcmstyle.css'
HTML(url=css_file)
```

<link href='http://fonts.googleapis.com/css?family=Open+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Arvo:400,700,400italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=PT+Mono' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Shadows+Into+Light' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Nixie+One' rel='stylesheet' type='text/css'>
<style>

@font-face {
    font-family: "Computer Modern";
    src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
}

#notebook_panel { /* main background */
    background: rgb(245,245,245);
}

div.cell { /* set cell width */
    width: 1000px;
}

div #notebook { /* centre the content */
    background: #fff; /* white background for content */
    width: 1200px;
    margin: auto;
    padding-left: 0em;
}

#notebook li { /* More space between bullet points */
margin-top:0.8em;
}

/* draw border around running cells */
div.cell.border-box-sizing.code_cell.running {
    border: 1px solid #111;
}

/* Put a solid color box around each cell and its output, visually linking them*/
div.cell.code_cell {
    background-color: rgb(256,256,256);
    border-radius: 0px;
    padding: 0.5em;
    margin-left:1em;
    margin-top: 1em;
}

div.text_cell_render{
    font-family: 'Open Sans' sans-serif;
    line-height: 140%;
    font-size: 125%;
    font-weight: 400;
    width:900px;
    margin-left:auto;
    margin-right:auto;
}

/* Formatting for header cells */
.text_cell_render h1 {
    font-family: 'Arvo', serif;
    font-style:regular;
    font-weight: 400;
    font-size: 45pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.5em;
    margin-top: 0.5em;
    display: block;
}
.text_cell_render h2 {
    font-family: 'Arvo', serif;
    font-weight: 400;
    font-size: 30pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.1em;
    margin-top: 0.3em;
    display: block;
}

.text_cell_render h3 {
    font-family: 'Arvo', serif;
    margin-top:16px;
	font-size: 22pt;
    font-weight: 600;
    margin-bottom: 3px;
    font-style: regular;
    color: rgb(102,102,0);
}

.text_cell_render h4 {    /*Use this for captions*/
    font-family: 'Arvo', serif;
    font-size: 14pt;
    text-align: center;
    margin-top: 0em;
    margin-bottom: 2em;
    font-style: regular;
}

.text_cell_render h5 {  /*Use this for small titles*/
    font-family: 'Arvo', sans-serif;
    font-weight: 400;
    font-size: 16pt;
    color: rgb(163,0,0);
    font-style: italic;
    margin-bottom: .1em;
    margin-top: 0.8em;
    display: block;
}

.text_cell_render h6 { /*use this for copyright note*/
    font-family: 'PT Mono', sans-serif;
    font-weight: 300;
    font-size: 9pt;
    line-height: 100%;
    color: grey;
    margin-bottom: 1px;
    margin-top: 1px;
}

.CodeMirror{
        font-family: "PT Mono";
        font-size: 90%;
}

</style>

## Basics of Molecular Dynamics

```python
%matplotlib inline
import numpy
from matplotlib import pyplot
from mpl_toolkits.mplot3d.axes3d import Axes3D
from matplotlib import rcParams
rcParams['font.family'] = 'serif'
rcParams['font.size'] = 16
rcParams['figure.figsize'] = (12,6)
```

The basic idea of molecular dynamics is to follow the location of the particles in time using Newton's laws. Each particle has a location $\vec{X}_i$ which obeys

$$
\begin{equation}
  m_i \frac{d^2}{d t^2} \vec{X}_i = m_i \vec{A}_i = -\nabla V \left( \vec{X}_1, \dots, \vec{X}_N \right).
\end{equation}
$$

Most of the time we will work in computational coordinates, rescaled by a reference length $L$, so that the particles obey

$$
\begin{equation}
  \frac{d^2}{d t^2} \vec{x}_i = \vec{a}_i = -\nabla V \left( \vec{x}_1, \dots, \vec{x}_N \right).
\end{equation}
$$

This gives us a computational domain, once translated to the centre-of-mass, of $\vec{x} \in [-0.5, 0.5]^3$.

The simplest possibility is that the interaction potential $V$ is the sum of pairwise interactions,

$$
\begin{equation}
  V \left( \vec{x}_1, \dots, \vec{x}_N \right) = \sum_i \sum_{j>i} \phi \left( L \left| \vec{x}_i - \vec{x}_j \right| \right).
\end{equation}
$$

The most commonly used pairwise interaction potential is the *Lennard-Jones* potential

$$
\begin{equation}
  \phi(r) = 4 \varepsilon \left[ \left( \frac{\sigma}{r} \right)^{12} - \left( \frac{\sigma}{r} \right)^{6} \right].
\end{equation}
$$

To stop this potential extending for infinite distance a cutoff is used; $\phi(r) \to \phi(r) - \phi(R_c)$, where $R_c$ is a constant (here $2.5$ when $\sigma = 1 = \varepsilon = M_i$).

As, in this case,

$$
\begin{equation}
  \frac{d}{dr} \phi(r) = 24 \left[ 2 \left( \frac{1}{r} \right)^{14} - \left( \frac{1}{r} \right)^{8} \right],
\end{equation}
$$

we have

$$
\begin{equation}
  \vec{a}_i = -\left. \nabla V \right|_{i} = \sum_{j>i}  (\vec{x}_i - \vec{x}_j) \, \frac{d}{dr} \phi(r) - \sum_{j<i}  (\vec{x}_j - \vec{x}_i) \, \frac{d}{dr} \phi(r).
\end{equation}
$$

Note that Newton's third law means that the force on particle $i$ from particle $j$ must be exactly opposite to the force on particle $j$ from particle $i$. So the algorithm can be implemented by setting $\vec{a}_i = 0$ and, for each particle $i$, and for all $j > i$, setting *both*

1. $\vec{a}_i = \vec{a}_i + (\vec{x}_i - \vec{x}_j) \, \frac{d}{dr} \phi(r)$,
2. $\vec{a}_j = \vec{a}_j - (\vec{x}_i - \vec{x}_j) \, \frac{d}{dr} \phi(r)$.

Implement an algorithm to calculate the acceleration given the positions.

```python

```

### Time evolution

The problem with time integrating is that most numerical algorithms do not conserve the total energy of the system, which can be be crucial. Certain algorithms, such as the [velocity Verlet](http://en.wikipedia.org/wiki/Verlet_integration#Velocity_Verlet) algorithm, do conserve total energy.

#### Velocity Verlet

Given initial positions $\vec{x}$ and velocities $\vec{v}$ of each particle, the algorithm can be written as

1. $x^{n+1} = \vec{x}^n + \Delta t \, \vec{v}^n + \tfrac{1}{2} \Delta t^2 \, \vec{a}^n$
2. $\vec{v}^{*} = \vec{v}^n + \tfrac{1}{2} \Delta t \, \vec{a}^n$;
3. Derive $\vec{a}^{n+1}$ from the interaction potential using $\vec{x}^{n+1}$;
4. $\vec{v}^{n+1} = \vec{v}^{*} + \tfrac{1}{2} \Delta t \vec{a}^{n+1}$.

Implement this algorithm.

```python

```

### Periodic boundaries

If we just evolved the particles, they could fly off to infinity. Instead it is typical to work inside a box with periodic boundaries, so that there is an "infinite lattice" of particles.

Implement a function that resets the position of particles that leave the given domain; the velocities and particles should remain unchanged.

```python

```

### What to compute

The fundamental quantity is the location of the particles. However, the thing to analyze will be average properties of the particles; their mean temperature, pressure and energies.

Implement a function, given the particle locations $\vec{x}$ to compute the temperature, where

$$
\begin{align}
%  E_{\text{potential},\ i} &= \sum_{j /ne i} \phi \left( L \left| \vec{x}_i - \vec{x}_j \right| \right), \\
  E_{\text{kinetic},\ i} &= \tfrac{1}{2} L^2 \left| \vec{v}_i \right|^2, \\
  T & = \frac{2}{3 N} \sum_{i=1}^N E_{\text{kinetic},\ i}.
\end{align}
$$

```python

```

### Run in a box

Fix the domain to be $\vec{x} \in [0, 6.1984]^3$. Fix the timestep to be $0.005$ and take 100 timesteps, given the initial data prescribed in `input.dat` (see `input.dat` on Blackboard).

```python

```

Then plot some basic quantities, such as

1. The temperature as a function of time;
2. The trajectory of two particles from the middle of the domain (eg particles 100 and 101) as a function of time (using `plot3D`);
3. The trajectories of all the particles.

```python

```

FEEG6016 Simulation and Modelling/03-Molecular-Dynamics-Lab-1.ipynb