# 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


# Introduction to PyTorch
In this tutorial we will take a first look at the PyTorch library as well as some extensions. The official documents are much more detailed, so it is always a good idea to go through them:
* [Official tutorials](https://pytorch.org/tutorials/)
* [PyTorch Documentation](https://pytorch.org/docs/stable/index.html)
* [Examples](https://github.com/pytorch/examples)
* [PyTorch Lightning Documentation](https://pytorch-lightning.readthedocs.io/en/latest/)

Make sure you have a recent version of PyTorch installed. Installation instructions can be found [here](https://pytorch.org/get-started/locally/). Additionally, you need the following packages:
* `torchvision`
* `tqdm`
* `scikit-learn`
* `pytorch-lightning`
* `tensorboard`

If you use [Google Colab](https://colab.research.google.com/) (recommended), you should already have all packages except `pytorch-lightning`. You can install missing packages like this:

```python
!pip install pytorch-lightning
```

If you use Jupyter Lab, make sure to activate extensions as described [here](https://github.com/tqdm/tqdm/issues/394#issuecomment-384743637), otherwise you won't be able to see progress bars.

This tutorial is made with PyTorch version 1.4.0. Below you can check the versions of your installed packages:

```python
import torch
import torchvision
import tqdm
import sklearn
import pytorch_lightning
import tensorboard

for m in [torch, torchvision, tqdm, sklearn, pytorch_lightning, tensorboard]:
    print('{}: v{}'.format(m.__name__, m.__version__))
```

## PyTorch Basics
The first part of the tutoral covers the basics of PyTorch without the Lightning extension.

### Example: Digit Recognition
We will start by creating a feed-forward network for hand-written digit recognition using the [MNIST dataset](https://en.wikipedia.org/wiki/MNIST_database). It contains images, where each image shows one hand-written digit. The task is to classify each image as one of ten classes, one for each digit. This dataset is included in `torchvision`. In order to use this dataset with PyTorch, the images have to be converted to Tensors, which we can achieve easily by using `torchvision.transforms.ToTensor`. Note that in many cases it is also useful to normalize the data (from $[0, 255]$ to $[0, 1]$). We skip this step for simplicity here.

```python
from torchvision.datasets import MNIST
from torchvision.transforms import ToTensor

mnist_train = MNIST(root='data', download=True, train=True, transform=ToTensor())
mnist_test = MNIST(root='data', download=True, train=False, transform=ToTensor())
```

The data is already split in training and test sets. We can inspect the lengths of the datasets to find out the number of images. In this case we have $60000$ training examples and $10000$ testing examples. The images are $28 \times 28$ pixels, the labels are integers in $[0, 9]$.

```python
print('{} training images'.format(len(mnist_train)))
print('{} test images'.format(len(mnist_test)))

image, label = mnist_train[0]
print('image shape: {}'.format(image.shape))
print('label: {}'.format(label))
```

#### Model Definition
There are multiple ways of defining models in PyTorch. For simple problems, it is possible to define a model as a list of sequential layers. We start with a simple feed-forward network for this task. Using `torch.nn.Sequential`, it is as simple as combining a list of layers:

```python
import torch.nn as nn

model = nn.Sequential(
    nn.Flatten(),
    nn.Linear(in_features=784, out_features=512),
    nn.ReLU(),
    nn.Dropout(p=0.2),
    nn.Linear(in_features=512, out_features=10))
```

Alternatively, it is possible to define more complex models using a functional API. Each custom module should subclass `torch.nn.Module` and implement the `forward` method. The following model definition is equivalent to the first one:

```python
class MNISTModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.flatten = nn.Flatten()
        self.linear_1 = nn.Linear(in_features=784, out_features=512)
        self.relu = nn.ReLU()
        self.dropout = nn.Dropout(p=0.2)
        self.linear_2 = nn.Linear(in_features=512, out_features=10)

    def forward(self, inputs):
        x = self.flatten(inputs)
        x = self.linear_1(x)
        x = self.relu(x)
        x = self.dropout(x)
        x = self.linear_2(x)
        return x

model = MNISTModel()
```

In detail, our new model has the following layers:
1. The first layer flattens the input. Right now our images have a shape of $(28, 28)$. Since our model consists of regular fully-connected layers, we need to convert them to vectors. This can be done using a `torch.nn.Flatten` layer, which is our model's input layer.
2. The second layer is a dense (fully connected) hidden layer with $512$ neurons. The number of input features is the number of pixels per image. We use ReLU activation.
3. The third layer adds some dropout regularization.
4. The fourth layer is another dense layer with $10$ neurons, where each neuron corresponds to one class.

Now that our model is complete, we need a loss function and an optimizer to train it. We choose cross entropy loss. Note that `torch.nn.CrossEntropyLoss` does more than just computing the loss:
1. It converts the output of our model to a probability distribution using the _LogSoftmax_. Since we have $10$ classes, we need $10$ output neurons, where each neuron outputs the probability that the input image belongs to its class. Recall the definition of the Softmax function:
\begin{equation}
	softmax(y_i) = \frac{e^{y_i}}{\sum_{j=1}^{m} e^{y_j}}
\end{equation}
The Softmax function produces a probability distribution in the output layer, where we can then select the class with the highest probability.
2. It handles our labels correctly, even though they are simply class indices ($0$ to $9$) and not probability distributions.

As our optimizer we choose `torch.optim.Adam`. It is possible to optimize all model parameters or just a subset of them.

```python
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters())
```

#### Training the Model
We are now ready to train our model. PyTorch offers utility classes that can (and should) be used for training. The class `torch.utils.data.DataLoader` provides useful features such as shuffling, batching, parallel processing and more. It expects a `torch.utils.data.Dataset`, which `torchvision.datasets.MNIST` is a subclass of, hence we can directly use it:

```python
from torch.utils.data import DataLoader

train_dl = DataLoader(mnist_train, batch_size=32, shuffle=True)
test_dl = DataLoader(mnist_test, batch_size=32)
```

To train a single epoch, we iterate over the DataLoader. For each input batch, we first apply the model (forward pass), then calculate the loss and call the optimizer (backward pass).

```python
from tqdm.notebook import tqdm

def train_epoch(model, train_dl, loss_fn, optimizer):
    epoch_loss = 0
    for inputs, labels in tqdm(train_dl):
        outputs = model(inputs)
        loss = loss_fn(outputs, labels)
        epoch_loss += loss.item()

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    return epoch_loss / len(train_dl)
```

We can now train our model for a couple of epochs. Calling the `train` function ensures that the model is in training mode (more on that later).

```python
model.train()
for epoch in range(5):
    loss = train_epoch(model, train_dl, loss_fn, optimizer)
    print('epoch {} -- training loss: {}'.format(epoch + 1, loss))
```

#### Testing the Model
Now that our model is trained, we can use it to make some predictions. Let's take the first item in the test set and predict its class. Note that our model expects batches as inputs, i.e. the shape should be $(b, 28, 28)$, where $b$ is the batch size (this can be variable). Even if we only want to input a single example, the shape must be $(1, 28, 28)$.

```python
inputs, label = mnist_test[0]
print('input shape: {}'.format(inputs.shape))
print('real label: {}'.format(label))
```

Whenever we intend to use the model without training it, we do not need the gradients to be computed during the forward pass. To prevent this, we can use `torch.no_grad`. Before we use the model for predictions, we switch to evaluation mode by calling the `eval` function to disable dropout.

```python
model.eval()
with torch.no_grad():
    predictions = model(inputs)

print('predictions: {}'.format(predictions))
print('class with highest score: {}'.format(torch.argmax(predictions)))
```

Our model assigned the image to the correct class.

Using the same technique, we can now evaluate our model by computing some metrics on the test set. We convert the labels and outputs to numpy arrays using `torch.Tensor.numpy` and use Scikit-learn to compute the standard classification metrics.

```python
from sklearn.metrics import classification_report

def evaluate(model, test_dl):
    true_labels = []
    predicted_labels = []
    for inputs, labels in tqdm(test_dl):
        outputs = model(inputs)
        predictions = torch.argmax(outputs, -1)

        true_labels.extend(labels.numpy())
        predicted_labels.extend(predictions.numpy())
    return true_labels, predicted_labels

with torch.no_grad():
    true_labels, predicted_labels = evaluate(model, test_dl)
print(classification_report(true_labels, predicted_labels))
```

Congratulations, your model has an accuracy of $98\%$! You can now save it using `torch.save`:

```python
torch.save(model.state_dict(), 'mnist_model.pt')
```

Loading it back is just as easy:

```python
state_dict = torch.load('mnist_model.pt')
model.load_state_dict(state_dict)
```

Note that this only saves the model weights, but not the structure. You can also save the whole model instead of just the state dict.

## PyTorch Lightning
[PyTorch Lightning](https://github.com/PyTorchLightning/pytorch-lightning) is a PyTorch wrapper that implements many useful functions and saves us from writing a lot of code. We will introduce it (somewhat briefly) in this section.

### Lightning Modules
In order to use Lightning to train your model, it has to be defined as a subclass of `pytorch_lightning.LightningModule`. This class extends the standard `torch.nn.Module`, adding methods for datasets, optimizers, loss functions and more. Let's redefine our MNIST model using Lightning:

```python
from pytorch_lightning import LightningModule
from sklearn.metrics import accuracy_score

class MNISTLightningModel(LightningModule):
    def __init__(self):
        super().__init__()
        self.flatten = nn.Flatten()
        self.linear_1 = nn.Linear(in_features=784, out_features=512)
        self.relu = nn.ReLU()
        self.dropout = nn.Dropout(p=0.2)
        self.linear_2 = nn.Linear(in_features=512, out_features=10)

        self.loss_fn = nn.CrossEntropyLoss()

    def forward(self, inputs):
        x = self.flatten(inputs)
        x = self.linear_1(x)
        x = self.relu(x)
        x = self.dropout(x)
        x = self.linear_2(x)
        return x

    def prepare_data(self):
        self.mnist_train = MNIST(root='data', download=True, train=True, transform=ToTensor())
        self.mnist_test = MNIST(root='data', download=True, train=False, transform=ToTensor())

    def training_step(self, batch, batch_idx):
        inputs, labels = batch
        outputs = self(inputs)
        return {'loss': self.loss_fn(outputs, labels)}

    def training_epoch_end(self, results):
        avg_loss = torch.stack([step['loss'] for step in results]).mean()
        return {'log': {'train_loss': avg_loss}}

    def test_step(self, batch, batch_idx):
        inputs, labels = batch
        outputs = self(inputs)
        predictions = torch.argmax(outputs, -1)
        return {'test_labels': labels, 'test_predictions': predictions}

    def test_epoch_end(self, results):
        true_labels = []
        predicted_labels = []
        for step in results:
            true_labels.extend(step['test_labels'].numpy())
            predicted_labels.extend(step['test_predictions'].numpy())

        # all logs need to be tensors, not numbers
        acc = torch.as_tensor(accuracy_score(true_labels, predicted_labels))
        return {'log': {'test_accuracy': acc}}

    def configure_optimizers(self):
        return torch.optim.Adam(self.parameters())

    def train_dataloader(self):
        return DataLoader(self.mnist_train, batch_size=32, shuffle=True)

    def test_dataloader(self):
        return DataLoader(self.mnist_test, batch_size=32)
```

The Lightning model above contains everything our previous model contained and is defined by a single well-organized class.  The `prepare_data` method gets called once before the training starts and takes care of dataset related tasks like downloads. The training is handled by the `training_step` method, which returns the loss for a given batch. `training_epoch_end` is called after one epoch and averages the training loss. Similarly, `test_step` applies the model to a batch of testing data and returns the labels and predictions, while `test_epoch_end` finally aggregates the results and returns the accuracy score for the whole test set.

Training and testing the model now becomes trivial:

```python
from pytorch_lightning import Trainer

model = MNISTLightningModel()
trainer = Trainer(max_epochs=5)
trainer.fit(model)
trainer.test()
```

### TensorBoard
You might have noticed that our Lightning model did not output any numbers like training loss. We did, however, return logging dictionaries in our `training_epoch_end` and `test_epoch_end` methods that contain the values we would like to monitor during and after training. The logs are saved in a new folder (`lightning_logs` by default). We can use _TensorBoard_ to inspect and visualize these logs. TensorBoard a very useful tool used to monitor the training process and inspect the model. It allows you to plot losses, metrics and other parameters like the learning rate in real time during the training and has many other cool features. You can launch TensorBoard either via the terminal or directly in the notebook:

```python
%load_ext tensorboard
%tensorboard --logdir lightning_logs
```

Above you should be able to see your training loss now. If you ran the training multiple times, you can enable and disable each run individually on the left. You can customize the values to be logged here to your liking by returning more values in the logging dictionaries within your Lightning model. For example, it is possible to log the training loss after each batch in `training_step` and omit the `training_epoch_end` function altogether. This can be useful if your dataset is very large and does not require even one full epoch of training.

Note that, if you do not see the test results in TensorBoard, it might be related to [this bug](https://github.com/PyTorchLightning/pytorch-lightning/issues/1447), which should be fixed in the next version of Lightning.

### Validation
So far we have only trained our model for a fixed number of epochs on our training data. However, this approach is very prone to overfitting. It is usually best to keep a smaller number of examples as a validation set (or dev set) and calculate the loss on this set after every $n$ epochs (or batches). As soon as we see the validation loss increase, we should stop the training to avoid overfitting.

In order to use early stopping with our dataset, we need to create a validation set first by taking a small number of instances from the training set. For simplicity, we create the split in the `prepare_data` method here; however, in practice it might be better to have a fixed split that never changes. Additionally, we implement the methods `validation_step`, which computes the loss on a batch of validation data, and `validation_epoch_end`, which averages the loss over the whole validation set.

```python
from torch.utils.data import random_split

class MNISTLightningModelWithVal(MNISTLightningModel):
    def prepare_data(self):
        mnist_train = MNIST(root='data', download=True, train=True, transform=ToTensor())
        self.mnist_train, self.mnist_val = random_split(mnist_train, [55000, 5000])
        self.mnist_test = MNIST(root='data', download=True, train=False, transform=ToTensor())

    def validation_step(self, batch, batch_idx):
        inputs, labels = batch
        outputs = self(inputs)
        return {'loss': self.loss_fn(outputs, labels)}

    def validation_epoch_end(self, results):
        avg_loss = torch.stack([step['loss'] for step in results]).mean()
        return {'log': {'val_loss': avg_loss}}

    def train_dataloader(self):
        return DataLoader(self.mnist_train, batch_size=32, shuffle=True)

    def val_dataloader(self):
        return DataLoader(self.mnist_val, batch_size=32)

    def test_dataloader(self):
        return DataLoader(self.mnist_test, batch_size=32)
```

After implementing the validation methods, you can use parameters such as `check_val_every_n_epoch` or `val_check_interval` to control the frequency of validation during training. It is also possible to perform validation more than once per epoch by setting `val_check_interval` to a value between $0$ and $1$.

```python
model = MNISTLightningModelWithVal()
trainer = Trainer(max_epochs=5, check_val_every_n_epoch=1)
trainer.fit(model)
```

### Callbacks
Callback functions can be used to execute any arbitrary code during training or testing of your Lightning model. A callback class implements methods like
* `on_batch_end`
* `on_batch_start`
* `on_epoch_end`
* `on_epoch_start`

and so on. The complete list can be found [here](https://pytorch-lightning.readthedocs.io/en/latest/callbacks.html). These methods are then called at the appropriate times during the training/testing process. You can create your own callback classes by subclassing `pytorch_lightning.callbacks.base.Callback`. In this section we will look at some existing callbacks.

#### Early Stopping
In the previous section we implemented validation. Since the validation loss tells us when the model starts overfitting, it makes sense to stop the training process after reaching this point to avoid unnecessary computations. This technique is called _early stopping_. Lightning provides a pre-implemented callback `pytorch_lightning.callbacks.EarlyStopping` that monitors the validation loss and stops the training once it starts increasing:

```python
from pytorch_lightning.callbacks import EarlyStopping

cb_early_stop = EarlyStopping(monitor='val_loss', patience=3)
```

Note that name of value we monitor (`val_loss`) needs to be identical to the name we assigned in our logs earlier. We can also set the `patience` parameter as the maximum number of iterations where an increase is tolerated before stopping. Let's train the same model again with early stopping. During the training, you can scroll up and monitor the logs in real time using TensorBoard.

```python
model = MNISTLightningModelWithVal()
trainer = Trainer(check_val_every_n_epoch=1, early_stop_callback=cb_early_stop)
trainer.fit(model)
```

#### Checkpointing
Model checkpointing is an important technique when you train models over a longer period of time. You want to save your trained weights after every couple of epochs (or batches) for two reasons:
1. If the process is killed unexpectedly, you don't lose all of your progress.
2. If your model starts to overfit, you can use the last 'good' checkpoint.

Checkpointing is implemented as a callback function, `pytorch_lightning.callbacks.ModelCheckpoint`.

```python
import os
from pytorch_lightning.callbacks import ModelCheckpoint

os.makedirs('ckpt', exist_ok=True)
ckpt_path = os.path.join('ckpt', '{epoch}-{val_loss:.2f}')
cb_checkpoint = ModelCheckpoint(filepath=ckpt_path, period=1, save_top_k=3, monitor='val_loss')
```

We configured the checkpointing callback above to save the checkpoints in a directory called `ckpt`. A checkpoint will be saved after every epoch, as controlled by `period`. Only the best $3$ checkpoints will be kept, based on the validation loss. The file name is important here, as it determines whether or not checkpoints will be overwritten. Placeholders in the file name will be replaced by the corresponding values.

```python
model = MNISTLightningModelWithVal()
trainer = Trainer(check_val_every_n_epoch=1, early_stop_callback=cb_early_stop, checkpoint_callback=cb_checkpoint)
trainer.fit(model)
```

## Further Reading
In this section we describe some more advanced topics which are not fully covered in this tutorial but well worth checking out.

### Training on GPUs
So far we have only used a small dataset which can be trained on CPUs in reasonable time. However, once you start working with large models or data, this becomes unfeasible. GPUs (and TPUs) are optimized for matrix (or tensor) multiplications and are much better suited for training deep models. You have multiple options to use GPUs:

* If you have your own NVIDIA GPU, install [CUDA](https://developer.nvidia.com/cuda-downloads) and [cuDNN](https://developer.nvidia.com/cudnn) to use it (note that Deep Learning libraries tend to require specific versions of these).
* If you are using Google Colab, you can activate GPU (or TPU) acceleration for free in the menu under `Runtime`, selecting `Change runtime type`. Restart the runtime afterwards.

Start by checking your CUDA installation:

```python
print(torch.cuda.is_available())
```

#### PyTorch
In order to train on a GPU, you have to transfer both your parameters (the model) and your data to the GPU before (or during) training. Note that this is a small model, so the speed-up is not very big.

```python
model = MNISTModel()
model.to('cuda:0')
optimizer = torch.optim.Adam(model.parameters())

def train_epoch_cuda(model, train_dl, loss_fn, optimizer):
    epoch_loss = 0
    for inputs, labels in tqdm(train_dl):
        outputs = model(inputs.to('cuda:0'))
        loss = loss_fn(outputs, labels.to('cuda:0'))
        epoch_loss += loss.item()

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    return epoch_loss / len(train_dl)

model.train()
for epoch in range(5):
    loss = train_epoch_cuda(model, train_dl, loss_fn, optimizer)
    print('epoch {} -- training loss: {}'.format(epoch + 1, loss))
```

#### Lightning
Lightning makes training on a GPU very easy. All we have to do is set the `gpus` parameter:

```python
model = MNISTLightningModel()
trainer = Trainer(gpus=1, max_epochs=5)
trainer.fit(model)
```

Lightning also supports training on TPUs (available through Google Colab) out of the box. Check out the official documentation [here](https://pytorch-lightning.readthedocs.io/en/latest/tpu.html).

If you have multiple GPUs, you can use PyTorch's `DataParallel` or `DistributedDataParallel` mode. Check out the official guides for [PyTorch](https://pytorch.org/tutorials/beginner/blitz/data_parallel_tutorial.html) and [Lightning](https://pytorch-lightning.readthedocs.io/en/latest/multi_gpu.html).

### Custom Datasets
Once you start using your own data for training it is very likely you have to create custom dataset classes. Refer to the [relevant PyTorch documentation](https://pytorch.org/docs/stable/data.html) on how to do this. In essence, you have to extend `torch.utils.data.Dataset` and implement the `__getitem__` and `__len__` methods. Depending on your use case, it might be required to implement a _collate_ function, which forms batches from your data instances.

For very large datasets that do not fit in the main memory, it might be worth checking out `h5py` ([HDF5 for Python](http://docs.h5py.org/en/stable/)). It is a data type that allows non-sequential access directly from the disk.


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

```