#!/usr/bin/env python
# coding: utf-8
#
#
#
# # NRPy+ 10-Minute Overview
#
# ## Author: Zach Etienne
#
# ## This notebook presents NRPy+, a Python framework that converts intricate SymPy expressions into optimized C/C++ code for use in numerical relativity. It emphasizes the computation of 3-Christoffel symbols $\Gamma^{i}_{jk}$ and numerical derivative designations within expressions.
# [comment]: <> (added this abstract in case. omit of it's unnecessary.)
#
#
# # Table of Contents
# $$\label{toc}$$
#
# This notebook is organized as follows
#
# 1. [Part 1](#why): Why NRPy+?
# 1. [Part 2](#christoffel_symbols): Constructing 3-Christoffels $\Gamma^i_{jk}$ as symbolic expressions in terms of 3-metric $\gamma_{ij}$ and derivatives
# 1. [Part 3](#outputc): `outputC()` example: Output $\Gamma^i_{jk}$ expressions as optimized C code, assuming derivatives already specified
# 1. [Part 4](#fd_outputc): `FD_outputC()` example: Specify numerical derivatives within $\Gamma^i_{jk}$ expressions as finite differences
# 1. [Part 5](#what_next): What next? Navigating the NRPy+ tutorial
# 1. [Part 6](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF
#
#
# # Part 1: Why NRPy+? \[Back to [top](#toc)\]
# $$\label{why}$$
#
# A core problem faced by numerical relativity is not that the techniques we use are particularly complex, but that *the equations numerical relativists solve are very complex*.
#
# [Einstein notation](https://en.wikipedia.org/wiki/Einstein_notation) certainly makes the complexity more manageable, and is the most common way to perform tensorial mathematics in numerical relativity.
#
# NRPy+ is built upon the idea that equations written in Einstein notation *are a form of code*, and when this code is expressed in NRPy+'s native (Python) language
#
# * rank-0 tensors (scalars) are *variables*
# * rank-1 tensors are *lists*
# * rank-2 tensors are *lists of lists*
# * ... and so forth.
#
# Further, implied tensor summations are simply *loops*.
#
# NRPy+ combines the above ideas with the incredibly powerful [SymPy](https://www.sympy.org/) computer algebra package (think: Mathematica for Python, but fully free & open source), with a custom-built code generation infrastructure for converting complex SymPy expressions directly into highly-optimized C/C++ code.
#
# Importantly, NRPy+ builds on the idea of *learning by example*. The core NRPy+ repository contains more than 100 pedagogical and well-formatted Jupyter notebook tutorials, covering topics of core relevance to the field of numerical relativity. About 25 of these tutorials generate *complete C codes* capable of e.g., evolving the scalar wave equation, Maxwell's equations, and Einstein's equations of general relativity -- all in a variety of coordinate systems. All ~100 Jupyter notebooks are linked to from [the main NRPy+ tutorial table of contents.](NRPyPlus_Tutorial.ipynb)
#
# This 10-minute overview however is designed to introduce only the very basic features of NRPy+, with a core focus on the idea that *NRPy+ can be used to benefit any numerical relativity code*.
#
#
# # Part 2: Constructing 3-Christoffels $\Gamma^i_{jk}$ as symbolic expressions in terms of 3-metric $\gamma_{ij}$ and derivatives \[Back to [top](#toc)\]
# $$\label{christoffel_symbols}$$
#
# **Problem statement**: Given a three-metric $\gamma_{ij}$, construct all 18 independent Christoffel symbols $\Gamma^i_{jk}$, which involves first derivatives of the metric. Assume that $\gamma_{ij}$ *and its derivatives* are given numerically, requiring the derivatives to be defined as symbols.
#
# In NRPy+ we adopt a rigid syntax for tensors and indexed expressions involving Python lists, so that for example
#
# * $\gamma_{ij}=$ `gammaDD[i][j]`
# * $\gamma_{ij,k}=$ `gammaDD_dD[i][j][k]`
#
# Christoffel symbols (of the first kind) are defined as ([source](https://en.wikipedia.org/wiki/Christoffel_symbols#Christoffel_symbols_of_the_first_kind)):
#
# \begin{align}
# \Gamma_{ij}^k &= \frac{1}{2} \gamma^{kl}\left(\gamma_{jl,i} + \gamma_{il,j} - \gamma_{ij,l}\right)\\
# \end{align}
#
# So first we'll define $\gamma_{ij}$ and its inverse using NRPy+ functions
# In[1]:
import sympy as sp # SymPy: The Python computer algebra package upon which NRPy+ depends
import indexedexp as ixp # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support
# gammaDD is a rank-2 indexed expression symmetric in indexes 0 and 1
gammaDD = ixp.declarerank2("gammaDD", symmetry="sym01", DIM=3)
# gammaUU is the inverse of gammaDD, and
# gammaDET (unused) is the determinant of gammaDD
gammaUU, gammaDET = ixp.symm_matrix_inverter3x3(gammaDD) # inverts a 3x3 symmetric matrix
# Below we interact a bit with these generated expressions, confirming they
#
# *Exercise to students*: Verify that $\gamma_{ij} \gamma^{ij} = 3$ using the provided data structures & Python loops. (*Hint 1*: Scroll down a couple of cells to see how the Christoffel symbols are implemented. *Hint 2*: You will need to use SymPy's `simplify()` function to simplify the expression obtained.)
# In[2]:
print("Check that gammaDD[0][1] = gammaDD[1][0]:")
print("gammaDD[0][1] = ", gammaDD[0][1])
print("gammaDD[1][0] = ", gammaDD[1][0])
print("\nOutput gammaUU[1][0] = ", gammaUU[1][0])
print("\nCheck that gammaUU[1][0] - gammaUU[0][1] = 0:")
print("gammaUU[1][0] - gammaUU[0][1] = ", gammaUU[1][0] - gammaUU[0][1])
# Define Christoffel symbols in terms of the inverse metric and metric first derivatives:
# $$
# \Gamma_{ij}^k = \frac{1}{2} \gamma^{kl}\left(\gamma_{jl,i} + \gamma_{il,j} - \gamma_{ij,l}\right)
# $$
# In[3]:
# First define symbolic expressions for metric derivatives
gammaDD_dD = ixp.declarerank3("gammaDD_dD", symmetry="sym01", DIM=3)
# Initialize GammaUDD (3-Christoffel) to zero
GammaUDD = ixp.zerorank3(DIM=3)
for i in range(3):
for j in range(3):
for k in range(3):
for l in range(3):
GammaUDD[k][i][j] += sp.Rational(1, 2) * gammaUU[k][l] * (
gammaDD_dD[j][l][i] + gammaDD_dD[i][l][j] - gammaDD_dD[i][j][l])
# Now let's confirm that $\Gamma^{k}_{ij} = \Gamma^k_{ji}$:
# In[4]:
for i in range(3):
for j in range(3):
for k in range(3):
print(GammaUDD[i][j][k] - GammaUDD[i][k][j])
#
#
# # Part 3: `outputC()` example: Output $\Gamma^i_{jk}$ expressions as optimized C code, assuming derivatives already specified \[Back to [top](#toc)\]
# $$\label{outputc}$$
#
# At the core of NRPy+ is the ability to convert SymPy expressions to highly optimized C code.
#
# **Problem statement**: Output all 18 unique Christoffel symbols with 3 different levels of optimization supported by NRPy+'s core C output routine `outputC()`.
#
# First we store all 18 unique Christoffel symbols, as well as their desired variable names in C, to Python lists.
# In[5]:
symbols_list = []
varname_list = []
for i in range(3):
for j in range(3):
for k in range(j, 3):
symbols_list += [GammaUDD[i][j][k]]
varname_list += ["ChristoffelUDD" + str(i) + str(j) + str(k)]
# Next we input these lists into NRPy+'s C/C++ code generation function `outputC()`, at three different levels of optimization.
#
# **Optimization Level 0 (don't ever do it this way)**: Compute each Christoffel symbol independently.
# In[6]:
import outputC as outC # NRPy+: Core C code output module
outC.outputC(symbols_list, varname_list, filename="stdout", params="CSE_enable=False,outCverbose=False")
# Notice in the above code that many expressions are recomputed time and time again. This is *incredibly inefficient*, and generally compilers won't optimize this properly. So in optimization level 1, we use common subexpression elimination.
#
# **Optimization Level 1**: Use [common subexpression elimination (CSE)](https://en.wikipedia.org/wiki/Common_subexpression_elimination) to group common subexpressions
# In[7]:
outC.outputC(symbols_list, varname_list, filename="stdout", params="CSE_enable=True,outCverbose=False")
# **Optimization Level 2**: Use CSE and take advantage of [single instruction, multiple data (SIMD) macros](https://en.wikipedia.org/wiki/Single_instruction,_multiple_data). NRPy+ translates these macros into assembler-level-optimized code for (x86_64) CPUs. Use case: looping over data on a numerical grid; can evaluate expressions at up to 8 gridpoints simultaneously *per CPU core*. Note that `FusedMulSubSIMD(a,b,c)` and `FusedMulAddSIMD(a,b,c)` perform *two* floating-point operations per cycle (on modern CPUs), whereas for other arithmetic operations at most one FP operation can be performed per cycle.
# In[8]:
outC.outputC(symbols_list, varname_list, filename="stdout", params="CSE_enable=True,enable_SIMD=True,outCverbose=False")
#
#
# # Part 4: `FD_outputC()` example: Specify numerical derivatives within $\Gamma^i_{jk}$ expressions as finite differences \[Back to [top](#toc)\]
# $$\label{fd_outputc}$$
#
# To emphasize the infrastructure-agnostic nature of NRPy+, in the above C codes we assumed that the derivatives were already computed (e.g., using finite-difference derivatives, discontinuous Galerkin methods, pseudospectral methods, finite-element methods, etc.)
#
# In this part, we demonstrate NRPy+'s `FD_outputC()` code, which basically prepends the above C/C++ codes with the code needed for computing arbitrary-order finite-difference derivatives.
#
# To do this, NRPy+ makes the standard assumption that the underlying grid is *uniform* and that derivatives are taken with respect to functions stored at each point on the numerical grid. Appropriately, we call such functions "gridfunctions".
#
# In the following, we assume that the input, $\gamma_{ij}$, is stored at each gridpoint. Also, we wish to store each independent component of $\Gamma^i_{jk}$ at each gridpoint as output. The below code cell also introduces the NRPy+ parameter interface (accessed via `NRPy_param_funcs.py`) to set the finite-differencing order to 6.
#
# We'll use Optimization Level 1, to make the code easier to read; change `enable_SIMD=False` in the below `FD_outputC()` function call to `enable_SIMD=True` to see the Optimization Level 2 version.
# In[9]:
import grid as gri # NRPy+: Functions having to do with numerical grids
import finite_difference as fin # NRPy+: Finite difference C code generation module
import NRPy_param_funcs as par # NRPy+: parameter interface
# First specify the finite-differencing order to be 6th order (all even orders > 0 supported!)
par.set_parval_from_str("finite_difference::FD_CENTDERIVS_ORDER", 6)
# Next register gridfunctions for Christoffel symbols
gri.register_gridfunctions("AUX", varname_list, rank=3, is_indexed=True, DIM=3)
# Then register gamma_{ij} as gridfunctions
ixp.register_gridfunctions_for_single_rank2("EVOL", "gammaDD", "sym01", DIM=3)
# Finally output the C code using FD_outputC()
# Step 1: FD_outputC() requires that left-hand side and right-hand side of each
# expression be specified in a named-tuple "lhrh" defined in outputC.py
outputC_lhrh = []
for i, varname in enumerate(varname_list):
outputC_lhrh += [outC.lhrh(lhs=gri.gfaccess("out_gf", varname), rhs=symbols_list[i])]
# Step 2: call FD_outputC.
fin.FD_outputC("stdout", outputC_lhrh, params="CSE_enable=True,enable_SIMD=False,outCverbose=False")
#
#
# # Part 5: What next? Navigating the NRPy+ tutorial \[Back to [top](#toc)\]
# $$\label{what_next}$$
#
# As mentioned previously, NRPy+ is meant to be "learn by example". To that end, there are more than 100 fully documented Jupyter notebooks covering a wide variety of topics relevant to numerical relativity research and optimized algorithms for solving PDEs numerically.
#
# So the answer to the question "*What next?*" is, naturally, "*Whatever you like!*" To continue the journey, check out [**the main NRPy+ tutorial table of contents**.](NRPyPlus_Tutorial.ipynb).
#
#
# # Part 6: Output this notebook to $\LaTeX$-formatted PDF file \[Back to [top](#toc)\]
# $$\label{latex_pdf_output}$$
#
# The following code cell converts this Jupyter notebook into a proper, clickable $\LaTeX$-formatted PDF file. After the cell is successfully run, the generated PDF may be found in the root NRPy+ tutorial directory, with filename
# [Tutorial-NRPyPlus_10_Minute_Overview.pdf](Tutorial-NRPyPlus_10_Minute_Overview.pdf). (Note that clicking on this link may not work; you may need to open the PDF file through another means.)
# In[10]:
import cmdline_helper as cmd # NRPy+: Multi-platform Python command-line interface
cmd.output_Jupyter_notebook_to_LaTeXed_PDF("Tutorial-NRPyPlus_10_Minute_Overview")