{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<script async src=\"https://www.googletagmanager.com/gtag/js?id=UA-59152712-8\"></script>\n",
    "<script>\n",
    "  window.dataLayer = window.dataLayer || [];\n",
    "  function gtag(){dataLayer.push(arguments);}\n",
    "  gtag('js', new Date());\n",
    "\n",
    "  gtag('config', 'UA-59152712-8');\n",
    "</script>\n",
    "\n",
    "# Start-to-Finish Example: Head-On Black Hole Collision\n",
    "\n",
    "## Author: Zach Etienne\n",
    "### Formatting improvements courtesy Brandon Clark\n",
    "\n",
    "## This module implements a basic numerical relativity code to merge two black holes in *spherical coordinates*. The method consists of generating the necessary C source code for initial data, progressing it with a Runge-Kutta fourth-order scheme, and visualizing the black hole evolution by plotting the conformal factor. The validation confirms numerical error convergence to zero at higher resolutions.\n",
    "\n",
    "### Here we place the black holes initially on the $z$-axis, so the entire simulation is axisymmetric about the $\\phi$-axis. Not sampling in the $\\phi$ direction greatly speeds up the simulation.\n",
    "\n",
    "**Notebook Status:** <font color = green><b> Validated </b></font>\n",
    "\n",
    "**Validation Notes:** This module has been validated to exhibit convergence to zero of the Hamiltonian constraint violation at the expected order to the exact solution *after a short numerical evolution of the initial data* (see [plots at bottom](#convergence)), and all quantities have been validated against the [original SENR code](https://bitbucket.org/zach_etienne/nrpy).\n",
    "\n",
    "### NRPy+ Source Code & Tutorials for this module: \n",
    "* [BSSN/BrillLindquist.py](../edit/BSSN/BrillLindquist.py); [\\[**tutorial**\\]](Tutorial-ADM_Initial_Data-Brill-Lindquist.ipynb): Brill-Lindquist initial data; sets all ADM variables in Cartesian basis: \n",
    "* [BSSN/ADM_Initial_Data_Reader__BSSN_Converter.py](../edit/BSSN/ADM_Initial_Data_Reader__BSSN_Converter.py); [\\[**tutorial**\\]](Tutorial-ADM_Initial_Data_Reader__BSSN_Converter.ipynb): Registers the C function for our \"universal\" initial data reader/converter initial_data_reader__convert_ADM_Cartesian_to_BSSN().\n",
    "* [MoLtimestepping/MoL.py](../edit/MoLtimestepping/MoL.py); ([**NRPy+ Tutorial documentation**](Tutorial-Method_of_Lines-C_Code_Generation.ipynb)): Registers C functions for Method of Lines time integration, to push our initial data forward in time.\n",
    "* [CurviBoundaryConditions/CurviBoundaryConditions.py](../edit/CurviBoundaryConditions/CurviBoundaryConditions.py); [\\[**tutorial**\\]](Tutorial-Start_to_Finish-Curvilinear_BCs.ipynb): Registers the C function for our \"universal\" initial data reader/converter initial_data_reader__convert_ADM_Cartesian_to_BSSN(). This applies boundary conditions to BSSN quantities (including $\\lambda^i$, which is computed via finite difference derivatives and thus only defined in grid interior)\n",
    "* [BSSN/BSSN_Ccodegen_library.py](../edit/BSSN/BSSN_Ccodegen_library.py); [\\[**tutorial**\\]](Tutorial-BSSN_time_evolution-C_codegen_library.ipynb): Implements a number of helper functions for generating C codes from symbolic expressions generated in the following modules/tutorials:\n",
    "    * [BSSN/BSSN_constraints.py](../edit/BSSN/BSSN_constraints.py); [\\[**tutorial**\\]](Tutorial-BSSN_constraints.ipynb): Hamiltonian constraint in BSSN curvilinear basis/coordinates\n",
    "    * [BSSN/BSSN_RHSs.py](../edit/BSSN/BSSN_RHSs.py); [\\[**tutorial**\\]](Tutorial-BSSN_time_evolution-BSSN_RHSs.ipynb): Generates the right-hand sides for the BSSN evolution equations in singular, curvilinear coordinates\n",
    "    * [BSSN/BSSN_gauge_RHSs.py](../edit/BSSN/BSSN_gauge_RHSs.py); [\\[**tutorial**\\]](Tutorial-BSSN_time_evolution-BSSN_gauge_RHSs.ipynb): Generates the right-hand sides for the BSSN gauge evolution equations in singular, curvilinear coordinates\n",
    "\n",
    "## Introduction:\n",
    "Here we use NRPy+ to generate the C source code necessary to set up initial data for two black holes (Brill-Lindquist, [Brill & Lindquist, Phys. Rev. 131, 471, 1963](https://journals.aps.org/pr/abstract/10.1103/PhysRev.131.471); see also Eq. 1 of [Brandt & Brügmann, arXiv:gr-qc/9711015v1](https://arxiv.org/pdf/gr-qc/9711015v1.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 an [explicit Runge-Kutta fourth-order scheme](https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods) (RK4 is chosen below, but multiple options exist). \n",
    "\n",
    "The entire algorithm is outlined as follows, with links to the relevant NRPy+ tutorial notebooks listed at each step:\n",
    "\n",
    "1. Allocate memory for gridfunctions, including temporary storage for the Method of Lines time integration\n",
    "    * [**NRPy+ tutorial on Method of Lines algorithm**](Tutorial-Method_of_Lines-C_Code_Generation.ipynb).\n",
    "1. Set gridfunction values to initial data \n",
    "    * [**NRPy+ tutorial on Brill-Lindquist initial data**](Tutorial-ADM_Initial_Data-Brill-Lindquist.ipynb)\n",
    "    * [**NRPy+ tutorial on validating Brill-Lindquist initial data**](Tutorial-Start_to_Finish-BSSNCurvilinear-Exact_Initial_Data.ipynb).\n",
    "1. Next, integrate the initial data forward in time using the Method of Lines coupled to a Runge-Kutta explicit timestepping algorithm:\n",
    "    1. At the start of each iteration in time, output the Hamiltonian constraint violation \n",
    "        * [**NRPy+ tutorial on BSSN constraints**](Tutorial-BSSN_constraints.ipynb).\n",
    "    1. At each RK time substep, do the following:\n",
    "        1. Evaluate BSSN RHS expressions \n",
    "            * [**NRPy+ tutorial on BSSN right-hand sides**](Tutorial-BSSN_time_evolution-BSSN_RHSs.ipynb) ([**BSSN Introduction Notebook**](Tutorial-BSSN_formulation.ipynb))\n",
    "            * [**NRPy+ tutorial on BSSN gauge condition right-hand sides**](Tutorial-BSSN_time_evolution-BSSN_gauge_RHSs.ipynb) \n",
    "        1. Apply singular, curvilinear coordinate boundary conditions [*a la* the SENR/NRPy+ paper](https://arxiv.org/abs/1712.07658)\n",
    "            * [**NRPy+ tutorial on setting up singular, curvilinear boundary conditions**](Tutorial-Start_to_Finish-Curvilinear_BCs.ipynb)\n",
    "        1. Enforce constraint on conformal 3-metric: $\\det{\\bar{\\gamma}_{ij}}=\\det{\\hat{\\gamma}_{ij}}$ \n",
    "            * [**NRPy+ tutorial on enforcing $\\det{\\bar{\\gamma}_{ij}}=\\det{\\hat{\\gamma}_{ij}}$ constraint**](Tutorial-BSSN_enforcing_determinant_gammabar_equals_gammahat_constraint.ipynb)\n",
    "1. Repeat the above steps at two numerical resolutions to confirm convergence to zero."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='toc'></a>\n",
    "\n",
    "# Table of Contents\n",
    "$$\\label{toc}$$\n",
    "\n",
    "This notebook is organized as follows\n",
    "\n",
    "1. [Step 1](#initializenrpy): Set core NRPy+ parameters for numerical grids and reference metric\n",
    "1. [Step 2](#ccodegen): Generate C code kernels for BSSN expressions, in parallel if possible\n",
    "    1. [Step 2.a](#rfm_ccodegen): Generate C code kernels for reference metric\n",
    "1. [Step 3](#cparams_rfm_and_domainsize): Set `free_parameters.h`; also output C codes needed for declaring and setting Cparameters\n",
    "1. [Step 4](#bc_functs): Set up boundary condition functions for chosen singular, curvilinear coordinate system\n",
    "1. [Step 5](#mainc): `BrillLindquist_Playground`: The C code `main()` function\n",
    "1. [Step 6](#compileexec): Compile generated C codes & perform the black hole collision calculation\n",
    "1. [Step 7](#visualize): Visualize the output!\n",
    "    1. [Step 7.a](#installdownload): Install `scipy` and download `ffmpeg` if they are not yet installed/downloaded\n",
    "    1. [Step 7.b](#genimages): Generate images for visualization animation\n",
    "    1. [Step 7.c](#genvideo): Generate visualization animation\n",
    "1. [Step 8](#convergence): Plot the numerical error at the end of the simulation, and confirm that it converges to zero with increasing numerical resolution (sampling)\n",
    "1. [Step 9](#latex_pdf_output): Output this notebook to $\\LaTeX$-formatted PDF file"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='initializenrpy'></a>\n",
    "\n",
    "# Step 1: Set core NRPy+ parameters for numerical grids and reference metric \\[Back to [top](#toc)\\]\n",
    "$$\\label{initializenrpy}$$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2021-09-23T22:08:25.778737Z",
     "iopub.status.busy": "2021-09-23T22:08:25.774943Z",
     "iopub.status.idle": "2021-09-23T22:08:26.199113Z",
     "shell.execute_reply": "2021-09-23T22:08:26.198629Z"
    }
   },
   "outputs": [
    {
     "ename": "ModuleNotFoundError",
     "evalue": "No module named 'sympy'",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mModuleNotFoundError\u001b[0m                       Traceback (most recent call last)",
      "Cell \u001b[0;32mIn[1], line 2\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[38;5;66;03m# Step P1: Import needed NRPy+ core modules:\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01moutputC\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m add_to_Cfunction_dict, outC_function_master_list  \u001b[38;5;66;03m# NRPy+: Core C code output module\u001b[39;00m\n\u001b[1;32m      3\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mfinite_difference\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mfin\u001b[39;00m  \u001b[38;5;66;03m# NRPy+: Finite difference C code generation module\u001b[39;00m\n\u001b[1;32m      4\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mNRPy_param_funcs\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mpar\u001b[39;00m   \u001b[38;5;66;03m# NRPy+: Parameter interface\u001b[39;00m\n",
      "File \u001b[0;32m~/Documents/VS_Code/Etienne/forked/nrpytutorial/outputC.py:18\u001b[0m\n\u001b[1;32m     12\u001b[0m __all__ \u001b[38;5;241m=\u001b[39m [\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mlhrh\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124moutCparams\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mnrpyAbs\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124msuperfast_uniq\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mcheck_if_string__error_if_not\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m     13\u001b[0m            \u001b[38;5;124m'\u001b[39m\u001b[38;5;124moutputC\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mparse_outCparams_string\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m     14\u001b[0m            \u001b[38;5;124m'\u001b[39m\u001b[38;5;124moutC_NRPy_basic_defines_h_dict\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m     15\u001b[0m            \u001b[38;5;124m'\u001b[39m\u001b[38;5;124moutC_function_prototype_dict\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124moutC_function_dict\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mCfunction\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124madd_to_Cfunction_dict\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124moutCfunction\u001b[39m\u001b[38;5;124m'\u001b[39m]\n\u001b[1;32m     17\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mloop\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mlp\u001b[39;00m                             \u001b[38;5;66;03m# NRPy+: C code loop interface\u001b[39;00m\n\u001b[0;32m---> 18\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mNRPy_param_funcs\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mpar\u001b[39;00m                \u001b[38;5;66;03m# NRPy+: parameter interface\u001b[39;00m\n\u001b[1;32m     19\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mSIMD\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m expr_convert_to_SIMD_intrins \u001b[38;5;66;03m# NRPy+: SymPy expression => SIMD intrinsics interface\u001b[39;00m\n\u001b[1;32m     20\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mcse_helpers\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m cse_preprocess,cse_postprocess  \u001b[38;5;66;03m# NRPy+: CSE preprocessing and postprocessing\u001b[39;00m\n",
      "File \u001b[0;32m~/Documents/VS_Code/Etienne/forked/nrpytutorial/NRPy_param_funcs.py:10\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[38;5;66;03m# As documented in the NRPy+ tutorial module\u001b[39;00m\n\u001b[1;32m      2\u001b[0m \u001b[38;5;66;03m#   Tutorial-Coutput__Parameter_Interface.ipynb\u001b[39;00m\n\u001b[1;32m      3\u001b[0m \u001b[38;5;66;03m#   this core NRPy+ module is used for\u001b[39;00m\n\u001b[0;32m   (...)\u001b[0m\n\u001b[1;32m      7\u001b[0m \u001b[38;5;66;03m# Author: Zachariah B. Etienne\u001b[39;00m\n\u001b[1;32m      8\u001b[0m \u001b[38;5;66;03m#         zachetie **at** gmail **dot* com\u001b[39;00m\n\u001b[0;32m---> 10\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01msympy\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01msp\u001b[39;00m                   \u001b[38;5;66;03m# Import SymPy\u001b[39;00m\n\u001b[1;32m     11\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01msys\u001b[39;00m                           \u001b[38;5;66;03m# Standard Python: OS-independent system functions\u001b[39;00m\n\u001b[1;32m     12\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mcollections\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m namedtuple   \u001b[38;5;66;03m# Standard Python: Enable namedtuple data type\u001b[39;00m\n",
      "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'sympy'"
     ]
    }
   ],
   "source": [
    "# Step P1: Import needed NRPy+ core modules:\n",
    "from outputC import add_to_Cfunction_dict, outC_function_master_list  # NRPy+: Core C code output module\n",
    "import finite_difference as fin  # NRPy+: Finite difference C code generation module\n",
    "import NRPy_param_funcs as par   # NRPy+: Parameter interface\n",
    "import grid as gri               # NRPy+: Functions having to do with numerical grids\n",
    "import reference_metric as rfm   # NRPy+: Reference metric support\n",
    "import cmdline_helper as cmd     # NRPy+: Multi-platform Python command-line interface\n",
    "from pickling import unpickle_NRPy_env  # NRPy+: Pickle/unpickle NRPy+ environment, for parallel codegen\n",
    "import shutil, os, sys           # Standard Python modules for multiplatform OS-level functions, benchmarking\n",
    "\n",
    "# Step P2: Create C code output directory:\n",
    "Ccodesrootdir = os.path.join(\"BSSN_Two_BHs_Collide_Ccodes\")\n",
    "# First remove C code output directory if it exists\n",
    "# Courtesy https://stackoverflow.com/questions/303200/how-do-i-remove-delete-a-folder-that-is-not-empty\n",
    "shutil.rmtree(Ccodesrootdir, ignore_errors=True)\n",
    "# Then create a fresh directory\n",
    "cmd.mkdir(Ccodesrootdir)\n",
    "\n",
    "# Step P3: Create executable output directory:\n",
    "outdir = os.path.join(Ccodesrootdir, \"output\")\n",
    "cmd.mkdir(outdir)\n",
    "\n",
    "# Step 1.a: Enable SIMD-optimized code?\n",
    "#           I.e., generate BSSN and Ricci C code kernels using SIMD-vectorized\n",
    "#           compiler intrinsics, which *greatly improve the code's performance*,\n",
    "#           though at the expense of making the C-code kernels less\n",
    "#           human-readable.\n",
    "#           * Important note in case you wish to modify the BSSN/Ricci kernels\n",
    "#             here by adding expressions containing transcendental functions\n",
    "#             (e.g., certain scalar fields):\n",
    "#           Note that SIMD-based transcendental function intrinsics are not\n",
    "#           supported by the default installation of gcc or clang (you will\n",
    "#           need to use e.g., the SLEEF library from sleef.org, for this\n",
    "#           purpose). The Intel compiler suite does support these intrinsics\n",
    "#           however without the need for external libraries.\n",
    "enable_SIMD = True\n",
    "\n",
    "# Step 1.b: Enable reference metric precomputation.\n",
    "enable_rfm_precompute = True\n",
    "\n",
    "if enable_SIMD and not enable_rfm_precompute:\n",
    "    print(\"ERROR: SIMD does not currently handle transcendental functions,\\n\")\n",
    "    print(\"       like those found in rfmstruct (rfm_precompute).\\n\")\n",
    "    print(\"       Therefore, enable_SIMD==True and enable_rfm_precompute==False\\n\")\n",
    "    print(\"       is not supported.\\n\")\n",
    "    sys.exit(1)\n",
    "\n",
    "# Step 1.c: Enable \"FD functions\". In other words, all finite-difference stencils\n",
    "#         will be output as inlined static functions. This is essential for\n",
    "#         compiling highly complex FD kernels with using certain versions of GCC;\n",
    "#         GCC 10-ish will choke on BSSN FD kernels at high FD order, sometimes\n",
    "#         taking *hours* to compile. Unaffected GCC versions compile these kernels\n",
    "#         in seconds. FD functions do not slow the code performance, but do add\n",
    "#         another header file to the C source tree.\n",
    "# With gcc 7.5.0, enable_FD_functions=True decreases performance by 10%\n",
    "enable_FD_functions = True\n",
    "\n",
    "# Step 2: Set some core parameters, including CoordSystem MoL timestepping algorithm,\n",
    "#                                 FD order, floating point precision, and CFL factor:\n",
    "# Choices are: Spherical, SinhSpherical, SinhSphericalv2, Cylindrical, SinhCylindrical,\n",
    "#              SymTP, SinhSymTP\n",
    "CoordSystem     = \"Spherical\"\n",
    "par.set_parval_from_str(\"reference_metric::CoordSystem\", CoordSystem)\n",
    "rfm.reference_metric()\n",
    "\n",
    "# Step 2.a: Set outer boundary condition\n",
    "outer_bc_type = \"RADIATION_OUTER_BCS\"  # can be EXTRAPOLATION_OUTER_BCS or RADIATION_OUTER_BCS\n",
    "radiation_BC_FD_order = 2\n",
    "\n",
    "# Step 2.b: Set defaults for Coordinate system parameters.\n",
    "#           These are perhaps the most commonly adjusted parameters,\n",
    "#           so we enable modifications at this high level.\n",
    "\n",
    "# domain_size sets the default value for:\n",
    "#   * Spherical's params.RMAX\n",
    "#   * SinhSpherical*'s params.AMAX\n",
    "#   * Cartesians*'s -params.{x,y,z}min & .{x,y,z}max\n",
    "#   * Cylindrical's -params.ZMIN & .{Z,RHO}MAX\n",
    "#   * SinhCylindrical's params.AMPL{RHO,Z}\n",
    "#   * *SymTP's params.AMAX\n",
    "domain_size     = 7.5 # Needed for all coordinate systems.\n",
    "\n",
    "# sinh_width sets the default value for:\n",
    "#   * SinhSpherical's params.SINHW\n",
    "#   * SinhCylindrical's params.SINHW{RHO,Z}\n",
    "#   * SinhSymTP's params.SINHWAA\n",
    "sinh_width      = 0.4 # If Sinh* coordinates chosen\n",
    "\n",
    "# sinhv2_const_dr sets the default value for:\n",
    "#   * SinhSphericalv2's params.const_dr\n",
    "#   * SinhCylindricalv2's params.const_d{rho,z}\n",
    "sinhv2_const_dr = 0.05 # If Sinh*v2 coordinates chosen\n",
    "\n",
    "# SymTP_bScale sets the default value for:\n",
    "#   * SinhSymTP's params.bScale\n",
    "SymTP_bScale    = 0.5 # If SymTP chosen\n",
    "\n",
    "# Step 2.c: Set the order of spatial and temporal derivatives;\n",
    "#           the core data type, and the CFL factor.\n",
    "# RK_method choices include: Euler, \"RK2 Heun\", \"RK2 MP\", \"RK2 Ralston\", RK3, \"RK3 Heun\", \"RK3 Ralston\",\n",
    "#              SSPRK3, RK4, DP5, DP5alt, CK5, DP6, L6, DP8\n",
    "RK_method = \"RK4\"\n",
    "FD_order  = 4            # Finite difference order: even numbers only, starting with 2. 12 is generally unstable\n",
    "REAL      = \"double\"     # Best to use double here.\n",
    "default_CFL_FACTOR= 0.5  # (GETS OVERWRITTEN IF SPECIFIED AT COMMAND LINE.)\n",
    "                         # In pure axisymmetry (symmetry_axes = 2 below) 1.0 works fine. Otherwise 0.5 or lower."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2021-09-23T22:08:26.203895Z",
     "iopub.status.busy": "2021-09-23T22:08:26.203369Z",
     "iopub.status.idle": "2021-09-23T22:08:26.205606Z",
     "shell.execute_reply": "2021-09-23T22:08:26.205163Z"
    }
   },
   "outputs": [],
   "source": [
    "# Step 5: Set the finite differencing order to FD_order (set above).\n",
    "par.set_parval_from_str(\"finite_difference::FD_CENTDERIVS_ORDER\", FD_order)\n",
    "\n",
    "# Directory for reference_metric precomputation header files:\n",
    "rfm_precompute_Ccode_outdir = os.path.join(Ccodesrootdir, \"rfm_files/\")\n",
    "if enable_rfm_precompute:\n",
    "    cmd.mkdir(os.path.join(Ccodesrootdir, \"rfm_files/\"))\n",
    "    par.set_parval_from_str(\"reference_metric::rfm_precompute_Ccode_outdir\", rfm_precompute_Ccode_outdir)\n",
    "\n",
    "# Step 6: Copy SIMD/SIMD_intrinsics.h to $Ccodesrootdir/SIMD/SIMD_intrinsics.h\n",
    "if enable_SIMD:\n",
    "    cmd.mkdir(os.path.join(Ccodesrootdir,\"SIMD\"))\n",
    "    shutil.copy(os.path.join(\"SIMD/\")+\"SIMD_intrinsics.h\",os.path.join(Ccodesrootdir,\"SIMD/\"))\n",
    "\n",
    "# Step 7: Set finite_difference::enable_FD_functions appropriately. Defaults to False\n",
    "if enable_FD_functions:\n",
    "    par.set_parval_from_str(\"finite_difference::enable_FD_functions\", enable_FD_functions)\n",
    "\n",
    "# Step 8: If enable_SIMD, then copy SIMD/SIMD_intrinsics.h to $Ccodesrootdir/SIMD/SIMD_intrinsics.h\n",
    "cmd.mkdir(os.path.join(Ccodesrootdir,\"SIMD\"))\n",
    "if enable_SIMD:\n",
    "    shutil.copy(os.path.join(\"SIMD\", \"SIMD_intrinsics.h\"), os.path.join(Ccodesrootdir, \"SIMD\"))\n",
    "\n",
    "# Step 9: Set the direction=2 (phi) axis to be the symmetry axis; i.e.,\n",
    "#         axis \"2\", corresponding to the i2 direction.\n",
    "#      This sets all spatial derivatives in the phi direction to zero.\n",
    "par.set_parval_from_str(\"indexedexp::symmetry_axes\",\"2\")\n",
    "OMP_pragma_on = \"i1\"  # structure OpenMP loops to parallelize, not over i2 (phi direction), but i1 (theta direction)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Step 10: Generate Runge-Kutta-based (RK-based) timestepping code.\n",
    "#       As described above the Table of Contents, this is a 3-step process:\n",
    "#       10.A: Evaluate RHSs (RHS_string)\n",
    "#       10.B: Apply boundary conditions (post_RHS_string, pt 1)\n",
    "#       10.C: Enforce det(gammabar) = det(gammahat) constraint (post_RHS_string, pt 2)\n",
    "import MoLtimestepping.MoL as MoL\n",
    "# from MoLtimestepping.RK_Butcher_Table_Dictionary import Butcher_dict\n",
    "# RK_order  = Butcher_dict[RK_method][1]\n",
    "RHS_string      = \"\"\"\n",
    "Ricci_eval(params, rfmstruct, RK_INPUT_GFS, auxevol_gfs);\n",
    "rhs_eval(  params, rfmstruct, auxevol_gfs, RK_INPUT_GFS, RK_OUTPUT_GFS);\"\"\"\n",
    "RHS_string += \"\"\"\n",
    "if(params->outer_bc_type == RADIATION_OUTER_BCS)\n",
    "  apply_bcs_outerradiation_and_inner(params, bcstruct, griddata->xx,\n",
    "                                     gridfunctions_wavespeed,gridfunctions_f_infinity,\n",
    "                                     RK_INPUT_GFS, RK_OUTPUT_GFS);\"\"\"\n",
    "\n",
    "# Extrapolation BCs are applied to the evolved gridfunctions themselves after the MoL update\n",
    "post_RHS_string = \"\"\"if(params->outer_bc_type == EXTRAPOLATION_OUTER_BCS)\n",
    "  apply_bcs_outerextrap_and_inner(params, bcstruct, RK_OUTPUT_GFS);\n",
    "\"\"\"\n",
    "post_RHS_string += \"enforce_detgammahat_constraint(params, rfmstruct, RK_OUTPUT_GFS);\\n\"\n",
    "\n",
    "if not enable_rfm_precompute:\n",
    "    RHS_string = RHS_string.replace(\"rfmstruct\", \"xx\")\n",
    "    post_RHS_string = post_RHS_string.replace(\"rfmstruct\", \"xx\")\n",
    "\n",
    "MoL.register_C_functions_and_NRPy_basic_defines(RK_method,\n",
    "        RHS_string=RHS_string, post_RHS_string=post_RHS_string,\n",
    "        enable_rfm=enable_rfm_precompute, enable_curviBCs=True, enable_SIMD=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='ccodegen'></a>\n",
    "\n",
    "# Step 2: Generate C code kernels for BSSN expressions, in parallel if possible \\[Back to [top](#toc)\\]\n",
    "$$\\label{ccodegen}$$\n",
    "\n",
    "In the following code cell, we create a list of Python functions, which each registers a single C code function in `outputC`'s `outC_function_dict` dictionary. These Python functions are defined in \n",
    "\n",
    "1. [`BSSN.Initial_Data_Reader__BSSN_Converter`](../edit/BSSN.Initial_Data_Reader__BSSN_Converter.py); [\\[**tutorial**\\]](Tutorial-ADM_Initial_Data_Reader__BSSN_Converter.ipynb), which registers the C function for our \"universal\" initial data reader/converter `initial_data_reader__convert_ADM_Cartesian_to_BSSN()`.\n",
    "1. the [`BSSN.BSSN_Ccodegen_library`](../edit/BSSN/BSSN_Ccodegen_library.py) NRPy+ module [\\[**tutorial**\\]](Tutorial-BSSN_time_evolution-C_codegen_library.ipynb), which contains Python functions for generating C code from symbolic expressions constructed within the following NRPy+ modules/tutorials:\n",
    "    1. [BSSN/BSSN_constraints.py](../edit/BSSN/BSSN_constraints.py); [\\[**tutorial**\\]](Tutorial-BSSN_constraints.ipynb): Hamiltonian constraint in BSSN curvilinear basis/coordinates\n",
    "    1. [BSSN/BSSN_RHSs.py](../edit/BSSN/BSSN_RHSs.py); [\\[**tutorial**\\]](Tutorial-BSSN_time_evolution-BSSN_RHSs.ipynb): Generates the right-hand sides for the BSSN evolution equations in singular, curvilinear coordinates\n",
    "    1. [BSSN/BSSN_gauge_RHSs.py](../edit/BSSN/BSSN_gauge_RHSs.py); [\\[**tutorial**\\]](Tutorial-BSSN_time_evolution-BSSN_gauge_RHSs.ipynb): Generates the right-hand sides for the BSSN gauge evolution equations in singular, curvilinear coordinates\n",
    "    1. [BSSN/Enforce_Detgammahat_Constraint.py](../edit/BSSN/Enforce_Detgammahat_Constraint.py); [**tutorial**](Tutorial-BSSN_enforcing_determinant_gammabar_equals_gammahat_constraint.ipynb): Generates symbolic expressions for enforcing the $\\det{\\bar{\\gamma}}=\\det{\\hat{\\gamma}}$ constraint\n",
    "\n",
    "\n",
    "Next, from within a `multiprocessing` environment, we then call all the Python C-code generation functions in this list in parallel (if `multiprocessing` is supported). This is quite useful, as these functions take several seconds to complete.\n",
    "\n",
    "Within each `multiprocessing` process, the current NRPy+ environment is cloned, and a new function is registered to the `outC_function_dict` dictionary. Thus when each process completes, it contains a unique NRPy+ environment, with only its function registered. We address this by saving each process' NRPy+ environment and sending it back in a common binary format known as a `pickle`, using NRPy+'s [`pickling`](../edit/pickling.py) module. The environments are combined in an unpickling such that all functions exist within the same `outC_function_dict` dictionary.\n",
    "\n",
    "To make the current environment fully consistent, we call `reference_metric.py` to register all its associated C functions (stored in globals) and contributions to `NRPy_basic_defines.h`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# First set up the symbolic expressions for Brill-Lindquist initial data\n",
    "import BSSN.BrillLindquist as BLID\n",
    "BLID.BrillLindquist()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next we register the `ID_function()` needed for setting ADM spacetime quantities within our \"universal\" initial data reader/converter, as described in [this tutorial](Tutorial-ADM_Initial_Data_Reader__BSSN_Converter.ipynb). Appropriately, we refer to this `ID_function()` by the name `BrillLindquist()`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import BSSN.ADM_Initial_Data_Reader__BSSN_Converter as IDread\n",
    "\n",
    "IDread.add_to_Cfunction_dict_exact_ADM_ID_function(\"BrillLindquist\",  \"Cartesian\",\n",
    "                                                   BLID.alpha, BLID.betaU, BLID.BU, BLID.gammaDD, BLID.KDD)\n",
    "IDread.register_NRPy_basic_defines(include_T4UU=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2021-09-23T22:08:26.232538Z",
     "iopub.status.busy": "2021-09-23T22:08:26.231967Z",
     "iopub.status.idle": "2021-09-23T22:08:41.510761Z",
     "shell.execute_reply": "2021-09-23T22:08:41.510225Z"
    }
   },
   "outputs": [],
   "source": [
    "# Step 2: Generate C code kernels for BSSN expressions, in parallel if possible;\n",
    "import BSSN.BSSN_Ccodegen_library as BCL\n",
    "# Step 2.a: Create a list of functions we wish to evaluate in parallel (if possible)\n",
    "# Create lists for all BSSN functions\n",
    "codegen_funcs = [IDread.add_to_Cfunction_dict_initial_data_reader__convert_ADM_Sph_or_Cart_to_BSSN]\n",
    "codegen_funcs.append(BCL.add_rhs_eval_to_Cfunction_dict)\n",
    "codegen_funcs.append(BCL.add_Ricci_eval_to_Cfunction_dict)\n",
    "codegen_funcs.append(BCL.add_BSSN_constraints_to_Cfunction_dict)\n",
    "codegen_funcs.append(BCL.add_enforce_detgammahat_constraint_to_Cfunction_dict)\n",
    "\n",
    "# Step 2.b: Define master functions for parallelization.\n",
    "#           Note that lambdifying this doesn't work in Python 3\n",
    "def master_func(arg):\n",
    "    if codegen_funcs[arg] == IDread.add_to_Cfunction_dict_initial_data_reader__convert_ADM_Sph_or_Cart_to_BSSN:\n",
    "        ret = codegen_funcs[arg](addl_includes=None, rel_path_to_Cparams=os.path.join(\".\"),\n",
    "                                 input_Coord=\"Cartesian\", include_T4UU=False)\n",
    "    else:\n",
    "        if enable_rfm_precompute:\n",
    "            # We use rfm_precompute for all BSSN functions:\n",
    "            par.set_parval_from_str(\"reference_metric::enable_rfm_precompute\", \"True\")\n",
    "            rfm.reference_metric()\n",
    "\n",
    "        if codegen_funcs[arg].__name__ == \"add_BSSN_constraints_to_Cfunction_dict\":\n",
    "            ret = codegen_funcs[arg](includes=[\"NRPy_basic_defines.h\"],\n",
    "                                     rel_path_to_Cparams=os.path.join(\".\"), output_H_only=True,\n",
    "                                     enable_rfm_precompute=enable_rfm_precompute, enable_SIMD=enable_SIMD,\n",
    "                                     OMP_pragma_on=OMP_pragma_on)\n",
    "        elif codegen_funcs[arg].__name__ == \"add_rhs_eval_to_Cfunction_dict\" or \\\n",
    "            codegen_funcs[arg].__name__ == \"add_Ricci_eval_to_Cfunction_dict\":\n",
    "            ret = codegen_funcs[arg](includes=[\"NRPy_basic_defines.h\"],\n",
    "                                     rel_path_to_Cparams=os.path.join(\".\"),\n",
    "                                     enable_rfm_precompute=enable_rfm_precompute, enable_SIMD=enable_SIMD,\n",
    "                                     OMP_pragma_on=OMP_pragma_on)\n",
    "        elif codegen_funcs[arg].__name__ == \"add_enforce_detgammahat_constraint_to_Cfunction_dict\":\n",
    "            ret = codegen_funcs[arg](includes=[\"NRPy_basic_defines.h\"],\n",
    "                                     rel_path_to_Cparams=os.path.join(\".\"),\n",
    "                                     enable_rfm_precompute=enable_rfm_precompute, OMP_pragma_on=OMP_pragma_on)\n",
    "        else:\n",
    "            print(\"ERROR: DID NOT RECOGNIZE FUNCTION \" + codegen_funcs[arg].__name__ + \"\\n\")\n",
    "            sys.exit(1)\n",
    "        if enable_rfm_precompute:\n",
    "            par.set_parval_from_str(\"reference_metric::enable_rfm_precompute\", \"False\")\n",
    "            rfm.ref_metric__hatted_quantities()\n",
    "    return ret\n",
    "\n",
    "\n",
    "NRPyEnvVars = []\n",
    "raised_exception = False\n",
    "try:\n",
    "    if os.name == 'nt':\n",
    "        # It's a mess to get working in Windows, so we don't bother. :/\n",
    "        #  https://medium.com/@grvsinghal/speed-up-your-python-code-using-multiprocessing-on-windows-and-jupyter-or-ipython-2714b49d6fac\n",
    "        raise Exception(\"Parallel codegen currently not available in certain environments, e.g., Windows\")\n",
    "\n",
    "    # Step 2.d: Import the multiprocessing module.\n",
    "    import multiprocessing\n",
    "\n",
    "    # Step 2.e: Evaluate list of functions in parallel if possible;\n",
    "    #           otherwise fallback to serial evaluation:\n",
    "    pool = multiprocessing.Pool()\n",
    "    NRPyEnvVars.append(pool.map(master_func, range(len(codegen_funcs))))\n",
    "    pool.terminate()\n",
    "    pool.join()\n",
    "except:\n",
    "    print(\"FAILED PARALLEL CODEGEN!\")\n",
    "    NRPyEnvVars = [] # Reset, as pickling/unpickling unnecessary for serial codegen (see next line)\n",
    "\n",
    "    # Steps 2.d-e, alternate: As fallback, evaluate functions in serial.\n",
    "    #       This will happen on Android and Windows systems\n",
    "    for i, func in enumerate(codegen_funcs):\n",
    "        master_func(i)\n",
    "    raised_exception = True\n",
    "\n",
    "outCfunc_master_list = outC_function_master_list\n",
    "if not raised_exception:\n",
    "    outCfunc_master_list = unpickle_NRPy_env(NRPyEnvVars)\n",
    "    for el in outCfunc_master_list:\n",
    "        if el not in outC_function_master_list:  # in case there are duplicate funcs, which can happen\n",
    "            #                                      if finite_difference_functions = True\n",
    "            outC_function_master_list += [el]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='rfm_ccodegen'></a>\n",
    "\n",
    "## Step 2.a: Generate C code kernels for reference metric \\[Back to [top](#toc)\\]\n",
    "$$\\label{rfm_ccodegen}$$\n",
    "\n",
    "In the [reference_metric](../edit/reference_metric.py) NRPy+ module, `register_C_functions_and_NRPy_basic_defines()` registers the following C functions to `outC_Cfunction_dict`:\n",
    "\n",
    "1. `find_timestep()`: Finds the minimum spacing between adjacent gridpoints on our numerical grid $\\min(ds_i)$, and sets the timestep according to the [CFL](https://en.wikipedia.org/w/index.php?title=Courant%E2%80%93Friedrichs%E2%80%93Lewy_condition&oldid=806430673) condition: $\\Delta t \\le \\frac{\\min(ds_i)}{c}$, where $c$ is the wavespeed, and $ds_i = h_i \\Delta x^i$ is the proper distance between neighboring gridpoints in the $i$th direction (in 3D, there are 3 directions), $h_i$ is the $i$th reference metric scale factor, and $\\Delta x^i$ is the uniform grid spacing in the $i$th direction.\n",
    "1. `xx_to_Cart()`: Input = uniformly sampled coordinate xx0,xx1,xx2 (e.g., r,theta,phi in Spherical coordinates). Output = Cartesian coordinate (x,y,z).\n",
    "1. `set_Nxx_dxx_invdx_params__and__xx()`: Sets `Nxx{0,1,2}`, `Nxx_plus_2NGHOSTS{0,1,2}`, `dxx{0,1,2}`, and `invdx{0,1,2}`; and defines `xx[3][]`.\n",
    "1. `Cart_to_xx_and_nearest_i0i1i2()`: Input = Cartesian coordinate (x,y,z). Output = uniformly sampled coordinate xx0,xx1,xx2 (e.g., r,theta,phi in Spherical coordinates), as well as corresponding grid index `i0,i1,i2`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2021-09-23T22:08:41.574056Z",
     "iopub.status.busy": "2021-09-23T22:08:41.548577Z",
     "iopub.status.idle": "2021-09-23T22:08:41.938615Z",
     "shell.execute_reply": "2021-09-23T22:08:41.938185Z"
    }
   },
   "outputs": [],
   "source": [
    "# Generate & register C function set_Nxx_dxx_invdx_params__and__xx()\n",
    "# Generate & register C function xx_to_Cart() for\n",
    "#               (the mapping from xx->Cartesian) for the chosen\n",
    "#               CoordSystem:\n",
    "# Generate & register the find_timestep() function\n",
    "\n",
    "# Sets reference_metric globals: NRPy_basic_defines_str, rfm_struct__malloc, rfm_struct__define, rfm_struct__freemem\n",
    "if enable_rfm_precompute:\n",
    "    par.set_parval_from_str(\"reference_metric::rfm_precompute_Ccode_outdir\", rfm_precompute_Ccode_outdir)\n",
    "    par.set_parval_from_str(\"reference_metric::enable_rfm_precompute\", \"True\")\n",
    "    par.set_parval_from_str(\"reference_metric::rfm_precompute_to_Cfunctions_and_NRPy_basic_defines\", \"True\")\n",
    "    rfm.reference_metric()\n",
    "\n",
    "rfm.register_C_functions(enable_rfm_precompute=enable_rfm_precompute, use_unit_wavespeed_for_find_timestep=True)\n",
    "rfm.register_NRPy_basic_defines(enable_rfm_precompute=enable_rfm_precompute)\n",
    "\n",
    "if enable_rfm_precompute:\n",
    "    par.set_parval_from_str(\"reference_metric::enable_rfm_precompute\", \"False\")\n",
    "    rfm.ref_metric__hatted_quantities()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='cparams_rfm_and_domainsize'></a>\n",
    "\n",
    "# Step 3: Set `free_parameters.h`; also output C codes needed for declaring and setting Cparameters  \\[Back to [top](#toc)\\]\n",
    "$$\\label{cparams_rfm_and_domainsize}$$\n",
    "\n",
    "First 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."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2021-09-23T22:08:41.942239Z",
     "iopub.status.busy": "2021-09-23T22:08:41.941715Z",
     "iopub.status.idle": "2021-09-23T22:08:41.943573Z",
     "shell.execute_reply": "2021-09-23T22:08:41.943985Z"
    }
   },
   "outputs": [],
   "source": [
    "# Step 3.e.i: Set free_parameters.h\n",
    "outstr = r\"\"\"// Set free-parameter values.\n",
    "\n",
    "// Outer boundary condition choice:\n",
    "params.outer_bc_type = \"\"\"+outer_bc_type+r\"\"\";\n",
    "\n",
    "// Set the default CFL Factor. Can be overwritten at command line.\n",
    "REAL CFL_FACTOR = \"\"\"+str(default_CFL_FACTOR)+r\"\"\";\n",
    "\n",
    "// Set free-parameter values for BSSN evolution:\n",
    "params.eta = 1.0;\n",
    "\n",
    "// Set free parameters for the (Brill-Lindquist) initial data\n",
    "params.BH1_posn_x = 0.0; params.BH1_posn_y = 0.0; params.BH1_posn_z =+0.5;\n",
    "params.BH2_posn_x = 0.0; params.BH2_posn_y = 0.0; params.BH2_posn_z =-0.5;\n",
    "params.BH1_mass = 0.5; params.BH2_mass = 0.5;\n",
    "\"\"\"\n",
    "\n",
    "# Append to $Ccodesrootdir/free_parameters.h reference metric parameters based on generic\n",
    "#    domain_size,sinh_width,sinhv2_const_dr,SymTP_bScale,\n",
    "#    parameters set above.\n",
    "outstr += rfm.out_default_free_parameters_for_rfm(\"returnstring\",\n",
    "                                                  domain_size,sinh_width,sinhv2_const_dr,SymTP_bScale)\n",
    "with open(os.path.join(Ccodesrootdir,\"free_parameters.h\"),\"w\") as file:\n",
    "    file.write(outstr.replace(\"params.\", \"griddata.params.\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='bc_functs'></a>\n",
    "\n",
    "# Step 4: Set up boundary condition functions for chosen singular, curvilinear coordinate system \\[Back to [top](#toc)\\]\n",
    "$$\\label{bc_functs}$$\n",
    "\n",
    "Next apply singular, curvilinear coordinate boundary conditions [as documented in the corresponding NRPy+ tutorial notebook](Tutorial-Start_to_Finish-Curvilinear_BCs.ipynb)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2021-09-23T22:08:41.952190Z",
     "iopub.status.busy": "2021-09-23T22:08:41.951777Z",
     "iopub.status.idle": "2021-09-23T22:08:42.181582Z",
     "shell.execute_reply": "2021-09-23T22:08:42.181905Z"
    }
   },
   "outputs": [],
   "source": [
    "import CurviBoundaryConditions.CurviBoundaryConditions as CBC\n",
    "CBC.CurviBoundaryConditions_register_NRPy_basic_defines()\n",
    "CBC.CurviBoundaryConditions_register_C_functions(radiation_BC_FD_order=radiation_BC_FD_order)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='mainc'></a>\n",
    "\n",
    "# Step 5: The C code `main()` function for `BrillLindquist_Playground` \\[Back to [top](#toc)\\]\n",
    "$$\\label{mainc}$$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2021-09-23T22:08:42.191346Z",
     "iopub.status.busy": "2021-09-23T22:08:42.183557Z",
     "iopub.status.idle": "2021-09-23T22:08:42.193048Z",
     "shell.execute_reply": "2021-09-23T22:08:42.192711Z"
    }
   },
   "outputs": [],
   "source": [
    "def add_to_Cfunction_dict_main__BrillLindquist_Playground():\n",
    "    includes = [\"NRPy_basic_defines.h\", \"NRPy_function_prototypes.h\", \"time.h\"]\n",
    "    desc = \"\"\"// main() function:\n",
    "// Step 0: Read command-line input, set up grid structure, allocate memory for gridfunctions, set up coordinates\n",
    "// Step 1: Set up initial data to an exact solution\n",
    "// Step 2: Start the timer, for keeping track of how fast the simulation is progressing.\n",
    "// Step 3: Integrate the initial data forward in time using the chosen RK-like Method of\n",
    "//         Lines timestepping algorithm, and output periodic simulation diagnostics\n",
    "// Step 3.a: Output 2D data file periodically, for visualization\n",
    "// Step 3.b: Step forward one timestep (t -> t+dt) in time using\n",
    "//           chosen RK-like MoL timestepping algorithm\n",
    "// Step 3.c: If t=t_final, output conformal factor & Hamiltonian\n",
    "//           constraint violation to 2D data file\n",
    "// Step 3.d: Progress indicator printing to stderr\n",
    "// Step 4: Free all allocated memory\n",
    "\"\"\"\n",
    "    c_type = \"int\"\n",
    "    name = \"main\"\n",
    "    params = \"int argc, const char *argv[]\"\n",
    "    body = r\"\"\"  griddata_struct griddata;\n",
    "  set_Cparameters_to_default(&griddata.params);\n",
    "\n",
    "  // Step 0.a: Set free parameters, overwriting Cparameters defaults\n",
    "  //          by hand or with command-line input, as desired.\n",
    "#include \"free_parameters.h\"\n",
    "\n",
    "  // Step 0.b: Read command-line input, error out if nonconformant\n",
    "  if((argc != 4 && argc != 5) || atoi(argv[1]) < NGHOSTS || atoi(argv[2]) < NGHOSTS || atoi(argv[3]) < 2 /* FIXME; allow for axisymmetric sims */) {\n",
    "    fprintf(stderr,\"Error: Expected three command-line arguments: ./BrillLindquist_Playground Nx0 Nx1 Nx2,\\n\");\n",
    "    fprintf(stderr,\"where Nx[0,1,2] is the number of grid points in the 0, 1, and 2 directions.\\n\");\n",
    "    fprintf(stderr,\"Nx[] MUST BE larger than NGHOSTS (= %d)\\n\",NGHOSTS);\n",
    "    exit(1);\n",
    "  }\n",
    "  if(argc == 5) {\n",
    "    CFL_FACTOR = strtod(argv[4],NULL);\n",
    "    if(CFL_FACTOR > 0.5 && atoi(argv[3])!=2) {\n",
    "      fprintf(stderr,\"WARNING: CFL_FACTOR was set to %e, which is > 0.5.\\n\",CFL_FACTOR);\n",
    "      fprintf(stderr,\"         This will generally only be stable if the simulation is purely axisymmetric\\n\");\n",
    "      fprintf(stderr,\"         However, Nx2 was set to %d>2, which implies a non-axisymmetric simulation\\n\",atoi(argv[3]));\n",
    "    }\n",
    "  }\n",
    "  // Step 0.c: Set up numerical grid structure, first in space...\n",
    "  const int Nxx[3] = { atoi(argv[1]), atoi(argv[2]), atoi(argv[3]) };\n",
    "  if(Nxx[0]%2 != 0 || Nxx[1]%2 != 0 || Nxx[2]%2 != 0) {\n",
    "    fprintf(stderr,\"Error: Cannot guarantee a proper cell-centered grid if number of grid cells not set to even number.\\n\");\n",
    "    fprintf(stderr,\"       For example, in case of angular directions, proper symmetry zones will not exist.\\n\");\n",
    "    exit(1);\n",
    "  }\n",
    "\n",
    "  // Step 0.d: Uniform coordinate grids are stored to *xx[3]\n",
    "  // Step 0.d.i: Set bcstruct\n",
    "  {\n",
    "    int EigenCoord;\n",
    "    EigenCoord = 1;\n",
    "    // Step 0.d.ii: Call set_Nxx_dxx_invdx_params__and__xx(), which sets\n",
    "    //             params Nxx,Nxx_plus_2NGHOSTS,dxx,invdx, and xx[] for the\n",
    "    //             chosen Eigen-CoordSystem.\n",
    "    set_Nxx_dxx_invdx_params__and__xx(EigenCoord, Nxx, &griddata.params, griddata.xx);\n",
    "    // Step 0.e: Find ghostzone mappings; set up bcstruct\n",
    "    bcstruct_set_up(&griddata.params, griddata.xx, &griddata.bcstruct);\n",
    "    // Step 0.e.i: Free allocated space for xx[][] array\n",
    "    for(int i=0;i<3;i++) free(griddata.xx[i]);\n",
    "\n",
    "    // Step 0.f: Call set_Nxx_dxx_invdx_params__and__xx(), which sets\n",
    "    //          params Nxx,Nxx_plus_2NGHOSTS,dxx,invdx, and xx[] for the\n",
    "    //          chosen (non-Eigen) CoordSystem.\n",
    "    EigenCoord = 0;\n",
    "    set_Nxx_dxx_invdx_params__and__xx(EigenCoord, Nxx, &griddata.params, griddata.xx);\n",
    "  }\n",
    "\n",
    "  // Step 0.g: Time coordinate parameters\n",
    "  griddata.params.t_final =  domain_size; /* Final time is set so that at t=t_final,\n",
    "                                           * data at the origin have not been corrupted\n",
    "                                           * by the approximate outer boundary condition */\n",
    "\n",
    "  // Step 0.h: Set timestep based on smallest proper distance between gridpoints and CFL factor\n",
    "  griddata.params.dt = find_timestep(&griddata.params, griddata.xx, CFL_FACTOR);\n",
    "  //fprintf(stderr,\"# Timestep set to = %e\\n\",(double)dt);\n",
    "  // Ntot = N_outputs * output_every_N -> output_every_N = Ntot/N_outputs; set N_outputs=800\n",
    "  int output_every_N = (int)((griddata.params.t_final/griddata.params.dt + 0.5) / 800.0);\n",
    "  if(output_every_N == 0) output_every_N = 1;\n",
    "\n",
    "  // Step 0.i: Error out if the number of auxiliary gridfunctions outnumber evolved gridfunctions.\n",
    "  //              This is a limitation of the RK method. You are always welcome to declare & allocate\n",
    "  //              additional gridfunctions by hand.\n",
    "  if(NUM_AUX_GFS > NUM_EVOL_GFS) {\n",
    "    fprintf(stderr,\"Error: NUM_AUX_GFS > NUM_EVOL_GFS. Either reduce the number of auxiliary gridfunctions,\\n\");\n",
    "    fprintf(stderr,\"       or allocate (malloc) by hand storage for *diagnostic_output_gfs. \\n\");\n",
    "    exit(1);\n",
    "  }\n",
    "\n",
    "  // Step 0.j: Declare struct for gridfunctions and allocate memory for y_n_gfs gridfunctions\n",
    "  MoL_malloc_y_n_gfs(&griddata.params, &griddata.gridfuncs);\n",
    "\"\"\"\n",
    "    if enable_rfm_precompute:\n",
    "        body += \"\"\"\n",
    "  // Step 0.k: Set up precomputed reference metric arrays\n",
    "  // Step 0.k.i: Allocate space for precomputed reference metric arrays.\n",
    "  rfm_precompute_rfmstruct_malloc(&griddata.params, &griddata.rfmstruct);\n",
    "\n",
    "  // Step 0.k.ii: Define precomputed reference metric arrays.\n",
    "  rfm_precompute_rfmstruct_define(&griddata.params, griddata.xx, &griddata.rfmstruct);\"\"\"\n",
    "    body += r\"\"\"\n",
    "  // Step 0.l: Set up initial data to an exact solution (Brill-Lindquist)\n",
    "  ID_persist_struct ID_persist;\n",
    "  initial_data_reader__convert_ADM_Cartesian_to_BSSN(&griddata, &ID_persist, BrillLindquist);\n",
    "\n",
    "  // Step 0.m: Allocate memory for non-y_n_gfs gridfunctions\n",
    "  MoL_malloc_non_y_n_gfs(&griddata.params, &griddata.gridfuncs);\n",
    "\n",
    "  // Step 0.n: Apply boundary conditions, as the BSSN\n",
    "  //           quantity lambda^i, defined using finite-\n",
    "  //           difference derivatives, is undefined in\n",
    "  //           ghost zones.\n",
    "  apply_bcs_outerextrap_and_inner(&griddata.params, &griddata.bcstruct, griddata.gridfuncs.y_n_gfs);\n",
    "  enforce_detgammahat_constraint(&griddata.params, &griddata.rfmstruct, griddata.gridfuncs.y_n_gfs);\n",
    "\n",
    "  // Step 1: Start the timer, for keeping track of how fast the simulation is progressing.\n",
    "#ifdef __linux__ // Use high-precision timer in Linux.\n",
    "  struct timespec start, end;\n",
    "  clock_gettime(CLOCK_REALTIME, &start);\n",
    "#else     // Resort to low-resolution, standards-compliant timer in non-Linux OSs\n",
    "  // http://www.cplusplus.com/reference/ctime/time/\n",
    "  time_t start_timer,end_timer;\n",
    "  time(&start_timer); // Resolution of one second...\n",
    "#endif\n",
    "\n",
    "  const int Nxx_plus_2NGHOSTS0 = griddata.params.Nxx_plus_2NGHOSTS0;\n",
    "  const int Nxx_plus_2NGHOSTS1 = griddata.params.Nxx_plus_2NGHOSTS1;\n",
    "  const int Nxx_plus_2NGHOSTS2 = griddata.params.Nxx_plus_2NGHOSTS2;\n",
    "\n",
    "  // Step 3: Integrate the initial data forward in time using the chosen RK-like Method of\n",
    "  //         Lines timestepping algorithm, and output periodic simulation diagnostics\n",
    "  while(griddata.params.time < griddata.params.t_final)\n",
    "    { // Main loop to progress forward in time.\n",
    "\n",
    "    // Step 3.a: Output 2D data file periodically, for visualization\n",
    "    if(griddata.params.n%100 == 0) {\n",
    "      // Evaluate BSSN constraints (currently only Hamiltonian constraint violation computed).\n",
    "      Ricci_eval(&griddata.params, &griddata.rfmstruct, griddata.gridfuncs.y_n_gfs, griddata.gridfuncs.auxevol_gfs);  // <- depends on Ricci.\n",
    "      BSSN_constraints(&griddata.params, &griddata.rfmstruct, griddata.gridfuncs.y_n_gfs, griddata.gridfuncs.auxevol_gfs, griddata.gridfuncs.diagnostic_output_gfs);\n",
    "\n",
    "      char filename[100];  sprintf(filename,\"out%d-%08d.txt\",Nxx[0],griddata.params.n);\n",
    "      FILE *out2D = fopen(filename, \"w\");\n",
    "      LOOP_REGION(NGHOSTS,Nxx_plus_2NGHOSTS0-NGHOSTS,\n",
    "                  NGHOSTS,Nxx_plus_2NGHOSTS1-NGHOSTS,\n",
    "                  NGHOSTS,Nxx_plus_2NGHOSTS2-NGHOSTS) {\n",
    "        const int idx = IDX3S(i0,i1,i2);\n",
    "        REAL xCart[3];  xx_to_Cart(&griddata.params,griddata.xx,i0,i1,i2, xCart);\n",
    "        fprintf(out2D,\"%e %e %e %e\\n\",\n",
    "                xCart[1],xCart[2],\n",
    "                griddata.gridfuncs.y_n_gfs[IDX4ptS(CFGF,idx)],log10(fabs(griddata.gridfuncs.diagnostic_output_gfs[IDX4ptS(HGF,idx)])));\n",
    "      }\n",
    "      fclose(out2D);\n",
    "    }\n",
    "\n",
    "    // Step 3.b: Step forward one timestep (t -> t+dt) in time using\n",
    "    //           chosen RK-like MoL timestepping algorithm\n",
    "    MoL_step_forward_in_time(&griddata);\n",
    "\n",
    "    // Step 3.c: If t=t_final, output conformal factor & Hamiltonian\n",
    "    //           constraint violation to 2D data file\n",
    "    if(griddata.params.time > (griddata.params.t_final - griddata.params.dt)) {\n",
    "      // Evaluate BSSN constraints (currently only Hamiltonian constraint violation computed).\n",
    "      Ricci_eval(&griddata.params, &griddata.rfmstruct, griddata.gridfuncs.y_n_gfs, griddata.gridfuncs.auxevol_gfs);  // <- depends on Ricci.\n",
    "      BSSN_constraints(&griddata.params, &griddata.rfmstruct, griddata.gridfuncs.y_n_gfs, griddata.gridfuncs.auxevol_gfs, griddata.gridfuncs.diagnostic_output_gfs);\n",
    "\n",
    "      char filename[100];  sprintf(filename,\"out%d.txt\",Nxx[0]);\n",
    "      FILE *out2D = fopen(filename, \"w\");\n",
    "      LOOP_REGION(NGHOSTS,Nxx_plus_2NGHOSTS0-NGHOSTS,\n",
    "                  NGHOSTS,Nxx_plus_2NGHOSTS1-NGHOSTS,\n",
    "                  NGHOSTS,Nxx_plus_2NGHOSTS2-NGHOSTS) {\n",
    "        int idx = IDX3S(i0,i1,i2);\n",
    "        REAL xCart[3];  xx_to_Cart(&griddata.params,griddata.xx,i0,i1,i2, xCart);\n",
    "        fprintf(out2D,\"%e %e %e %e\\n\",\n",
    "                xCart[1],xCart[2],\n",
    "                griddata.gridfuncs.y_n_gfs[IDX4ptS(CFGF,idx)],log10(fabs(griddata.gridfuncs.diagnostic_output_gfs[IDX4ptS(HGF,idx)])));\n",
    "      }\n",
    "      fclose(out2D);\n",
    "    }\n",
    "    // Step 3.d: Progress indicator printing to stderr.\n",
    "    //      Note that we're at the end of an iteration, so n=0\n",
    "    //      indicates we are actually at the start of iteration 1.\n",
    "    if((griddata.params.n + 1) % 10 == 0 ||\n",
    "       (griddata.params.time > griddata.params.t_final + griddata.params.dt) ) {\n",
    "      // Step 3.d.i: Measure average time per iteration\n",
    "#ifdef __linux__ // Use high-precision timer in Linux.\n",
    "      clock_gettime(CLOCK_REALTIME, &end);\n",
    "      const long long unsigned int time_in_ns = 1000000000L * (end.tv_sec - start.tv_sec) + end.tv_nsec - start.tv_nsec;\n",
    "#else     // Resort to low-resolution, standards-compliant timer in non-Linux OSs\n",
    "      time(&end_timer); // Resolution of one second...\n",
    "      REAL time_in_ns = difftime(end_timer,start_timer)*1.0e9+0.5; // Round up to avoid divide-by-zero.\n",
    "#endif\n",
    "      const REAL s_per_iteration_avg = ((REAL)time_in_ns / (REAL)(griddata.params.n + 1)) / 1.0e9;\n",
    "      //  We are at the n+1st iteration now  ---------------------v        -------^\n",
    "      const int iterations_remaining = (int)(griddata.params.t_final/griddata.params.dt + 0.5) - (griddata.params.n + 1);\n",
    "      const REAL seconds_remaining = (int)(s_per_iteration_avg * (REAL)iterations_remaining);\n",
    "      const int time_remaining__hrs  = (int)(seconds_remaining / 3600.0);\n",
    "      const int time_remaining__mins = (int)(seconds_remaining / 60.0) % 60;\n",
    "      const int time_remaining__secs = (int)(seconds_remaining) - time_remaining__mins*60 - time_remaining__hrs*3600;\n",
    "      //const REAL num_RHS_pt_evals = (REAL)(Nxx[0]*Nxx[1]*Nxx[2]) * 4.0 * (REAL)(n+1); // 4 RHS evals per gridpoint for RK4\n",
    "      //const REAL RHS_pt_evals_per_sec = num_RHS_pt_evals / ((REAL)time_in_ns / 1.0e9);\n",
    "\n",
    "      // Step 3.d.ii: Output simulation progress to stderr\n",
    "      fprintf(stderr,\"%c[2K\", 27); // Clear the line\n",
    "      fprintf(stderr,\"It: %d t=%.2f dt=%.2e | log10H: %.1f | %.1f%%; ETA %dh%dm%ds | t/h %.2f\\r\",\n",
    "              (griddata.params.n), (double)((griddata.params.n)*griddata.params.dt), (double)griddata.params.dt,\n",
    "              (double)log10(fabs(griddata.gridfuncs.diagnostic_output_gfs[IDX4ptS(HGF,(int)(Nxx_plus_2NGHOSTS0*Nxx_plus_2NGHOSTS1*Nxx_plus_2NGHOSTS2*0.5))])),\n",
    "              (double)(100.0*(REAL)(griddata.params.n)/(REAL)(int)((griddata.params.t_final/griddata.params.dt + 0.5))),\n",
    "              time_remaining__hrs, time_remaining__mins, time_remaining__secs,\n",
    "              (double)(griddata.params.dt * 3600.0 / s_per_iteration_avg));\n",
    "      fflush(stderr); // Flush the stderr buffer\n",
    "    } // End progress indicator if(griddata.params.n % 10 == 0)\n",
    "  } // End main loop to progress forward in time.\n",
    "  fprintf(stderr,\"\\n\"); // Clear the final line of output from progress indicator.\n",
    "\n",
    "  // Step 4: Free all allocated memory\n",
    "\"\"\"\n",
    "    if enable_rfm_precompute:\n",
    "        body += \"  rfm_precompute_rfmstruct_freemem(&griddata.params, &griddata.rfmstruct);\\n\"\n",
    "    body += r\"\"\"\n",
    "  free(griddata.bcstruct.inner_bc_array);\n",
    "  for(int ng=0;ng<NGHOSTS*3;ng++) free(griddata.bcstruct.pure_outer_bc_array[ng]);\n",
    "  MoL_free_memory_y_n_gfs(&griddata.params, &griddata.gridfuncs);\n",
    "  MoL_free_memory_non_y_n_gfs(&griddata.params, &griddata.gridfuncs);\n",
    "  for(int i=0;i<3;i++) free(griddata.xx[i]);\n",
    "\n",
    "  return 0;\n",
    "\"\"\"\n",
    "    # As rfmstruct stores functions of xx, when rfm_precompute is disabled,\n",
    "    #   we always pass xx to a function instead of &rfmstruct.\n",
    "    if not enable_rfm_precompute:\n",
    "        body = body.replace(\"&rfmstruct\", \"xx\")\n",
    "    add_to_Cfunction_dict(\n",
    "        includes=includes,\n",
    "        desc=desc,\n",
    "        c_type=c_type, name=name, params=params,\n",
    "        body=body,\n",
    "        rel_path_to_Cparams=os.path.join(\".\"), enableCparameters=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='compileexec'></a>\n",
    "\n",
    "# Step 6: Compile generated C codes & perform the black hole collision calculation \\[Back to [top](#toc)\\]\n",
    "$$\\label{compileexec}$$\n",
    "\n",
    "First we register remaining C functions and contributions to `NRPy_basic_defines.h`, then we output `NRPy_basic_defines.h` and `NRPy_function_prototypes.h`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2021-09-23T22:08:42.198101Z",
     "iopub.status.busy": "2021-09-23T22:08:42.197578Z",
     "iopub.status.idle": "2021-09-23T22:08:42.199609Z",
     "shell.execute_reply": "2021-09-23T22:08:42.200020Z"
    }
   },
   "outputs": [],
   "source": [
    "add_to_Cfunction_dict_main__BrillLindquist_Playground()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2021-09-23T22:08:42.198101Z",
     "iopub.status.busy": "2021-09-23T22:08:42.197578Z",
     "iopub.status.idle": "2021-09-23T22:08:42.199609Z",
     "shell.execute_reply": "2021-09-23T22:08:42.200020Z"
    }
   },
   "outputs": [],
   "source": [
    "import outputC as outC\n",
    "outC.outputC_register_C_functions_and_NRPy_basic_defines()  # #define M_PI, etc.\n",
    "# Declare paramstruct, register set_Cparameters_to_default(),\n",
    "#   and output declare_Cparameters_struct.h and set_Cparameters[].h:\n",
    "outC.NRPy_param_funcs_register_C_functions_and_NRPy_basic_defines(os.path.join(Ccodesrootdir))\n",
    "par.register_NRPy_basic_defines()  # add `paramstruct params` to griddata struct.\n",
    "\n",
    "gri.register_C_functions_and_NRPy_basic_defines()  # #define IDX3S(), etc.\n",
    "fin.register_C_functions_and_NRPy_basic_defines(NGHOSTS_account_for_onezone_upwind=True,\n",
    "                                                enable_SIMD=enable_SIMD)  # #define NGHOSTS, and UPWIND() macro if SIMD disabled\n",
    "\n",
    "# Output functions for computing all finite-difference stencils.\n",
    "#   Must be called after defining all functions depending on FD stencils.\n",
    "if enable_FD_functions:\n",
    "    fin.output_finite_difference_functions_h(path=Ccodesrootdir)\n",
    "\n",
    "# Call this last: Set up NRPy_basic_defines.h and NRPy_function_prototypes.h.\n",
    "outC.construct_NRPy_basic_defines_h(Ccodesrootdir, enable_SIMD=enable_SIMD)\n",
    "outC.construct_NRPy_function_prototypes_h(Ccodesrootdir)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Finally, we output all the C codes in `outC_function_dict` to files in the directory `Ccodesrootdir`, generate a `Makefile`, and compile the project using a parallel `make` command. If the `make` command fails, a backup serial compilation script is run.\n",
    "\n",
    "To aid in the cross-platform-compatible (with Windows, MacOS, & Linux) compilation and execution, we make use of `cmdline_helper` [(**Tutorial**)](Tutorial-cmdline_helper.ipynb)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2021-09-23T22:08:42.205118Z",
     "iopub.status.busy": "2021-09-23T22:08:42.204597Z",
     "iopub.status.idle": "2021-09-23T22:08:48.673486Z",
     "shell.execute_reply": "2021-09-23T22:08:48.673915Z"
    }
   },
   "outputs": [],
   "source": [
    "import cmdline_helper as cmd\n",
    "cmd.new_C_compile(Ccodesrootdir, os.path.join(\"output\", \"BrillLindquist_Playground\"),\n",
    "                  uses_free_parameters_h=True, compiler_opt_option=\"fast\") # fastdebug or debug also supported\n",
    "\n",
    "# Change to output directory\n",
    "os.chdir(os.path.join(Ccodesrootdir, \"output\"))\n",
    "# Clean up existing output files\n",
    "cmd.delete_existing_files(\"out*.txt\")\n",
    "cmd.delete_existing_files(\"out*.png\")\n",
    "# Run executable with CFL_FACTOR = 1.0, which is allowed since\n",
    "#   simulation is axisymmetric and all phi derivs are set to zero.\n",
    "CFL_FACTOR=1.0\n",
    "cmd.Execute(\"BrillLindquist_Playground\", \"72 12 2 \"+str(CFL_FACTOR))\n",
    "cmd.Execute(\"BrillLindquist_Playground\", \"96 16 2 \"+str(CFL_FACTOR))\n",
    "os.chdir(os.path.join(\"..\", \"..\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='visualize'></a>\n",
    "\n",
    "# Step 7: Visualize the output! \\[Back to [top](#toc)\\]\n",
    "$$\\label{visualize}$$ \n",
    "\n",
    "In this section we will generate a movie, plotting the conformal factor of these initial data on a 2D grid, such that darker colors imply stronger gravitational fields. Hence, we see the two black holes initially centered at $z/M=\\pm 0.5$, where $M$ is an arbitrary mass scale (conventionally the [ADM mass](https://en.wikipedia.org/w/index.php?title=ADM_formalism&oldid=846335453) is chosen), and our formulation of Einstein's equations adopt $G=c=1$ [geometrized units](https://en.wikipedia.org/w/index.php?title=Geometrized_unit_system&oldid=861682626)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='installdownload'></a>\n",
    "\n",
    "## Step 7.a: Install `scipy` and download `ffmpeg` if they are not yet installed/downloaded \\[Back to [top](#toc)\\]\n",
    "$$\\label{installdownload}$$ \n",
    "\n",
    "Note that if you are not running this within `mybinder`, but on a Windows system, `ffmpeg` must be installed using a separate package (on [this site](http://ffmpeg.org/)), or if running Jupyter within Anaconda, use the command: `conda install -c conda-forge ffmpeg`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2021-09-23T22:08:48.686286Z",
     "iopub.status.busy": "2021-09-23T22:08:48.685784Z",
     "iopub.status.idle": "2021-09-23T22:08:49.316627Z",
     "shell.execute_reply": "2021-09-23T22:08:49.316145Z"
    }
   },
   "outputs": [],
   "source": [
    "!pip install scipy > /dev/null\n",
    "\n",
    "check_for_ffmpeg = !which ffmpeg >/dev/null && echo $?\n",
    "if check_for_ffmpeg != ['0']:\n",
    "    print(\"Couldn't find ffmpeg, so I'll download it.\")\n",
    "    # Courtesy https://johnvansickle.com/ffmpeg/\n",
    "    !wget https://etienneresearch.com/ffmpeg-static-amd64-johnvansickle.tar.xz\n",
    "    !tar Jxf ffmpeg-static-amd64-johnvansickle.tar.xz\n",
    "    print(\"Copying ffmpeg to ~/.local/bin/. Assumes ~/.local/bin is in the PATH.\")\n",
    "    !mkdir ~/.local/bin/\n",
    "    !cp ffmpeg-static-amd64-johnvansickle/ffmpeg ~/.local/bin/\n",
    "    print(\"If this doesn't work, then install ffmpeg yourself. It should work fine on mybinder.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='genimages'></a>\n",
    "\n",
    "## Step 7.b: Generate images for visualization animation \\[Back to [top](#toc)\\]\n",
    "$$\\label{genimages}$$ \n",
    "\n",
    "Here we loop through the data files output by the executable compiled and run in [the previous step](#mainc), generating a [png](https://en.wikipedia.org/wiki/Portable_Network_Graphics) image for each data file.\n",
    "\n",
    "**Special thanks to Terrence Pierre Jacques. His work with the first versions of these scripts greatly contributed to the scripts as they exist below.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2021-09-23T22:08:49.326547Z",
     "iopub.status.busy": "2021-09-23T22:08:49.324255Z",
     "iopub.status.idle": "2021-09-23T22:08:52.146563Z",
     "shell.execute_reply": "2021-09-23T22:08:52.147000Z"
    }
   },
   "outputs": [],
   "source": [
    "## VISUALIZATION ANIMATION, PART 1: Generate PNGs, one per frame of movie ##\n",
    "\n",
    "import diagnostics_generic.process_2D_data as plot2D\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "import glob\n",
    "\n",
    "globby = glob.glob(os.path.join(outdir,'out96-00*.txt'))\n",
    "file_list = []\n",
    "for x in sorted(globby):\n",
    "    file_list.append(x)\n",
    "\n",
    "xy_extent=1.4\n",
    "\n",
    "for filename in file_list:\n",
    "    output_grid_x, output_grid_y, output_grid_data = \\\n",
    "        plot2D.generate_uniform_2D_grid(filename, 0,1,2, [-xy_extent,xy_extent], [-xy_extent,xy_extent])\n",
    "\n",
    "    fig = plt.figure()\n",
    "    colorbar_description = \"Conformal factor $W$\"\n",
    "    plt.title(\"Black Hole Head-on Collision (conf factor $W$)\")\n",
    "    plt.xlabel(r\"$y/M$\")\n",
    "    plt.ylabel(r\"$z/M$\")\n",
    "\n",
    "    im = plt.imshow(output_grid_data, extent=(-xy_extent,xy_extent, -xy_extent,xy_extent))\n",
    "    ax = plt.colorbar()\n",
    "    ax.set_label(colorbar_description)\n",
    "    plt.savefig(os.path.join(filename+\".png\"),dpi=150)\n",
    "    plt.close(fig)\n",
    "    sys.stdout.write(\"%c[2K\" % 27)\n",
    "    sys.stdout.write(\"Processing file \"+filename+\"\\r\")\n",
    "    sys.stdout.flush()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='genvideo'></a>\n",
    "\n",
    "## Step 7.c: Generate visualization animation \\[Back to [top](#toc)\\]\n",
    "$$\\label{genvideo}$$ \n",
    "\n",
    "In the following step, [ffmpeg](http://ffmpeg.org) is used to generate an [mp4](https://en.wikipedia.org/wiki/MPEG-4) video file, which can be played directly from this Jupyter notebook."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2021-09-23T22:08:52.931586Z",
     "iopub.status.busy": "2021-09-23T22:08:52.931027Z",
     "iopub.status.idle": "2021-09-23T22:08:52.933692Z",
     "shell.execute_reply": "2021-09-23T22:08:52.933338Z"
    },
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "## VISUALIZATION ANIMATION, PART 2: Combine PNGs to generate movie ##\n",
    "from matplotlib import animation\n",
    "from IPython.display import HTML\n",
    "import matplotlib.image as mgimg\n",
    "\n",
    "# https://stackoverflow.com/questions/14908576/how-to-remove-frame-from-matplotlib-pyplot-figure-vs-matplotlib-figure-frame\n",
    "# https://stackoverflow.com/questions/23176161/animating-pngs-in-matplotlib-using-artistanimation\n",
    "\n",
    "fig = plt.figure(frameon=False)\n",
    "ax = fig.add_axes([0, 0, 1, 1])\n",
    "ax.axis('off')\n",
    "\n",
    "myimages = []\n",
    "\n",
    "for i in range(len(file_list)):\n",
    "    img = mgimg.imread(file_list[i]+\".png\")\n",
    "    imgplot = plt.imshow(img)\n",
    "    myimages.append([imgplot])\n",
    "\n",
    "ani = animation.ArtistAnimation(fig, myimages, interval=100,  repeat_delay=1000)\n",
    "plt.close()\n",
    "ani.save(os.path.join(outdir,'BH_Head-on_Collision.mp4'), fps=5,dpi=150)\n",
    "\n",
    "## VISUALIZATION ANIMATION, PART 3: Display movie as embedded HTML5 (see next cell) ##\n",
    "\n",
    "# https://stackoverflow.com/questions/18019477/how-can-i-play-a-local-video-in-my-ipython-notebook\n",
    "\n",
    "# Embed video based on suggestion:\n",
    "#  https://stackoverflow.com/questions/39900173/jupyter-notebook-html-cell-magic-with-python-variable\n",
    "HTML(\"\"\"\n",
    "<video width=\"480\" height=\"360\" controls>\n",
    "  <source src=\\\"\"\"\"+os.path.join(outdir,\"BH_Head-on_Collision.mp4\")+\"\"\"\\\" type=\"video/mp4\">\n",
    "</video>\n",
    "\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='convergence'></a>\n",
    "\n",
    "# Step 8: Plot the numerical error at the end of the simulation, and confirm that it converges to zero with increasing numerical resolution (sampling) \\[Back to [top](#toc)\\]\n",
    "$$\\label{convergence}$$\n",
    "\n",
    "First, we plot the log10-absolute value Hamiltonian constraint violation on the $x$-$z$ plane, near the black hole (at $x=y=z=0$). Notice the constraint violation is largest inside the horizon, as expected."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2021-09-23T22:08:52.956531Z",
     "iopub.status.busy": "2021-09-23T22:08:52.956111Z",
     "iopub.status.idle": "2021-09-23T22:08:53.140079Z",
     "shell.execute_reply": "2021-09-23T22:08:53.139637Z"
    }
   },
   "outputs": [],
   "source": [
    "xy_extent=2.5\n",
    "output_grid_x, output_grid_y, output_grid_H_data = \\\n",
    "    plot2D.generate_uniform_2D_grid(os.path.join(outdir,'out96.txt'),\n",
    "                                    0,1,3, [-xy_extent,xy_extent], [-xy_extent,xy_extent])\n",
    "\n",
    "plt.clf()\n",
    "plt.title(r\"$96\\times 16$ Numerical Error: $\\log_{10}$|Ham|\")\n",
    "plt.xlabel(r\"$y/M$\")\n",
    "plt.ylabel(r\"$z/M$\")\n",
    "\n",
    "fig96 = plt.imshow(output_grid_H_data, extent=(-xy_extent,xy_extent, -xy_extent,xy_extent))\n",
    "cb = plt.colorbar(fig96)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next, we check that indeed the numerical errors converge to zero as expected, using the fact that the Hamiltonian constraint violation should converge to zero with increasing resolution. See [the Scalar Wave Curvilinear tutorial notebook](Tutorial-Start_to_Finish-ScalarWaveCurvilinear.ipynb) for more documentation on measuring numerical convergence."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2021-09-23T22:08:53.145680Z",
     "iopub.status.busy": "2021-09-23T22:08:53.145274Z",
     "iopub.status.idle": "2021-09-23T22:08:53.262908Z",
     "shell.execute_reply": "2021-09-23T22:08:53.262372Z"
    }
   },
   "outputs": [],
   "source": [
    "# Plot settings\n",
    "x_extent=3.0              # plot from -x_extent to +x_extent\n",
    "sample_numpts_x = 100     # number of points to plot\n",
    "interp_method = \"linear\"  # Could be linear (recommended), nearest (don't use; gridpoints are off-axis), or cubic\n",
    "\n",
    "output_grid_x, output_grid_data72 = \\\n",
    "    plot2D.extract_1D_slice_from_2D_data(os.path.join(outdir,'out72.txt'), 0.0,\n",
    "                                  0,1,3, [-x_extent, x_extent], sample_numpts_x=sample_numpts_x,\n",
    "                                  interp_method=interp_method)\n",
    "output_grid_x, output_grid_data96 = \\\n",
    "    plot2D.extract_1D_slice_from_2D_data(os.path.join(outdir,'out96.txt'), 0.0,\n",
    "                                  0,1,3, [-x_extent, x_extent], sample_numpts_x=sample_numpts_x,\n",
    "                                  interp_method=interp_method)\n",
    "\n",
    "plt.clf()\n",
    "fig, ax = plt.subplots()\n",
    "plt.title(r\"$4^{\\rm th}$-order Convergence, at $t/M$=7.5 (post-merger; horiz at $y/M\\sim\\pm 1$)\")\n",
    "plt.xlabel(r\"$y/M$\")\n",
    "plt.ylabel(r\"$\\log_{10}$(Ham. Constraint Violation)\")\n",
    "\n",
    "import numpy as np\n",
    "ax.plot(output_grid_x, output_grid_data96,                         'k-',  label='Nr=96')\n",
    "ax.plot(output_grid_x, output_grid_data72 + 4*np.log10(72.0/96.0), 'k--', label='Nr=72, mult by (72/96)^4')\n",
    "ax.set_ylim([-8.5,0.5])\n",
    "\n",
    "legend = ax.legend(loc='lower right', shadow=True, fontsize='x-large')\n",
    "legend.get_frame().set_facecolor('C1')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='latex_pdf_output'></a>\n",
    "\n",
    "# Step 9: Output this notebook to $\\LaTeX$-formatted PDF file \\[Back to [top](#toc)\\]\n",
    "$$\\label{latex_pdf_output}$$\n",
    "\n",
    "The following code cell converts this Jupyter notebook into a proper, clickable $\\LaTeX$-formatted PDF file. After the cell is successfully run, the generated PDF may be found in the root NRPy+ tutorial directory, with filename\n",
    "[Tutorial-Start_to_Finish-BSSNCurvilinear-Two_BHs_Collide.pdf](Tutorial-Start_to_Finish-BSSNCurvilinear-Two_BHs_Collide.pdf) (Note that clicking on this link may not work; you may need to open the PDF file through another means.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2021-09-23T22:08:53.266009Z",
     "iopub.status.busy": "2021-09-23T22:08:53.265372Z",
     "iopub.status.idle": "2021-09-23T22:08:56.325817Z",
     "shell.execute_reply": "2021-09-23T22:08:56.325358Z"
    }
   },
   "outputs": [],
   "source": [
    "import cmdline_helper as cmd    # NRPy+: Multi-platform Python command-line interface\n",
    "cmd.output_Jupyter_notebook_to_LaTeXed_PDF(\"Tutorial-Start_to_Finish-BSSNCurvilinear-Two_BHs_Collide\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}