{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"\n",
"\n",
"# Finite-Difference Playground: Using NRPy+-Generated C Codes in a Larger Project\n",
"\n",
"## Author: Zach Etienne\n",
"### Formatting improvements courtesy Brandon Clark\n",
"\n",
"## Introduction: \n",
"To illustrate how NRPy+-based codes can be used, we write a C code that makes use of the NRPy+-generated C code from the [previous module](Tutorial-Finite_Difference_Derivatives.ipynb). This is a rather silly example, as the C code generated by NRPy+ could be easily generated by hand. However, as we will see in later modules, NRPy+'s true strengths lie in its automatic handling of far more complex and generic expressions, in higher dimensions. For the time being, bear with NRPy+; its true powers will become clear soon!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"\n",
"# Table of Contents\n",
"$$\\label{toc}$$\n",
"\n",
"This notebook is organized as follows\n",
"\n",
"1. [Step 1](#outputc): Output the C file `finite_diff_tutorial-second_deriv.h`\n",
"1. [Step 2](#fdplayground): Finite-Difference Playground: A Complete C Code for Analyzing Finite-Difference Expressions Output by NRPy+\n",
"1. [Step 3](#exercise): Exercises\n",
"1. [Step 4](#latex_pdf_output): Output this notebook to $\\LaTeX$-formatted PDF file"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"\n",
"# Step 1: Output the C file `finite_diff_tutorial-second_deriv.h` \\[Back to [top](#toc)\\]\n",
"$$\\label{outputc}$$\n",
"\n",
"We start with the NRPy+ code from the [previous module](Tutorial-Finite_Difference_Derivatives.ipynb), and output it to the C file `finite_diff_tutorial-second_deriv.h`."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"execution": {
"iopub.execute_input": "2021-03-07T17:32:17.521891Z",
"iopub.status.busy": "2021-03-07T17:32:17.520599Z",
"iopub.status.idle": "2021-03-07T17:32:17.976712Z",
"shell.execute_reply": "2021-03-07T17:32:17.975986Z"
},
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{\n",
" /*\n",
" * NRPy+ Finite Difference Code Generation, Step 1 of 2: Read from main memory and compute finite difference stencils:\n",
" */\n",
" /*\n",
" * Original SymPy expression:\n",
" * \"const double phi_dDD00 = invdx0**2*(-5*phi/2 + 4*phi_i0m1/3 - phi_i0m2/12 + 4*phi_i0p1/3 - phi_i0p2/12)\"\n",
" */\n",
" const double phi_i0m2 = aux_gfs[IDX2S(PHIGF, i0-2)];\n",
" const double phi_i0m1 = aux_gfs[IDX2S(PHIGF, i0-1)];\n",
" const double phi = aux_gfs[IDX2S(PHIGF, i0)];\n",
" const double phi_i0p1 = aux_gfs[IDX2S(PHIGF, i0+1)];\n",
" const double phi_i0p2 = aux_gfs[IDX2S(PHIGF, i0+2)];\n",
" const double FDPart1_Rational_5_2 = 5.0/2.0;\n",
" const double FDPart1_Rational_1_12 = 1.0/12.0;\n",
" const double FDPart1_Rational_4_3 = 4.0/3.0;\n",
" const double phi_dDD00 = ((invdx0)*(invdx0))*(FDPart1_Rational_1_12*(-phi_i0m2 - phi_i0p2) + FDPart1_Rational_4_3*(phi_i0m1 + phi_i0p1) - FDPart1_Rational_5_2*phi);\n",
" /*\n",
" * NRPy+ Finite Difference Code Generation, Step 2 of 2: Evaluate SymPy expressions and write to main memory:\n",
" */\n",
" /*\n",
" * Original SymPy expression:\n",
" * \"aux_gfs[IDX2S(OUTPUTGF, i0)] = phi_dDD00\"\n",
" */\n",
" aux_gfs[IDX2S(OUTPUTGF, i0)] = phi_dDD00;\n",
"}\n",
"Wrote to file \"finite_diff_tutorial-second_deriv.h\"\n"
]
}
],
"source": [
"# Step P1: Import needed NRPy+ core modules:\n",
"from outputC import lhrh # NRPy+: Core C code output module\n",
"import NRPy_param_funcs as par # NRPy+: Parameter interface\n",
"import finite_difference as fin # NRPy+: Finite difference C code generation module\n",
"import grid as gri # NRPy+: Functions having to do with numerical grids\n",
"import indexedexp as ixp # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support\n",
"\n",
"# Set the spatial dimension to 1\n",
"par.set_paramsvals_value(\"grid::DIM = 1\")\n",
"\n",
"# Register the input gridfunction \"phi\" and the gridfunction to which data are output, \"output\":\n",
"phi, output = gri.register_gridfunctions(\"AUX\",[\"phi\",\"output\"])\n",
"\n",
"# Declare phi_dDD as a rank-2 indexed expression: phi_dDD[i][j] = \\partial_i \\partial_j phi\n",
"phi_dDD = ixp.declarerank2(\"phi_dDD\",\"nosym\")\n",
"\n",
"# Set output to \\partial_0^2 phi\n",
"output = phi_dDD[0][0]\n",
"\n",
"# Output to the screen the core C code for evaluating the finite difference derivative\n",
"fin.FD_outputC(\"stdout\",lhrh(lhs=gri.gfaccess(\"out_gf\",\"output\"),rhs=output))\n",
"\n",
"# Now, output the above C code to a file named \"finite_diff_tutorial-second_deriv.h\".\n",
"fin.FD_outputC(\"finite_diff_tutorial-second_deriv.h\",lhrh(lhs=gri.gfaccess(\"aux_gfs\",\"output\"),rhs=output))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"\n",
"# Step 2: Finite-Difference Playground: A Complete C Code for Analyzing Finite-Difference Expressions Output by NRPy+ \\[Back to [top](#toc)\\]\n",
"$$\\label{fdplayground}$$\n",
"\n",
"NRPy+ is designed to generate C code \"kernels\" at the heart of more advanced projects. As an example of its utility, let's now write a simple C code that imports the above file `finite_diff_tutorial-second_deriv.h` to evaluate the finite-difference second derivative of\n",
"\n",
"$$f(x) = \\sin(x)$$\n",
"\n",
"at fourth-order accuracy. Let's call the finite-difference second derivative of $f$ evaluated at a point $x$ $f''(x)_{\\rm FD}$. A fourth-order-accurate $f''(x)_{\\rm FD}$ will, in the truncation-error-dominated regime, satisfy the equation\n",
"\n",
"$$f''(x)_{\\rm FD} = f''(x)_{\\rm exact} + \\mathcal{O}(\\Delta x^4).$$\n",
"\n",
"Therefore, the [relative error](https://en.wikipedia.org/wiki/Approximation_error) between the finite-difference derivative and the exact value should be given to good approximation by\n",
"\n",
"$$E_{\\rm Rel} = \\left| \\frac{f''(x)_{\\rm FD} - f''(x)_{\\rm exact}}{f''(x)_{\\rm exact}}\\right| \\propto \\Delta x^4,$$\n",
"\n",
"so that (taking the logarithm of both sides of the equation):\n",
"\n",
"$$\\log_{10} E_{\\rm Rel} = 4 \\log_{10} (\\Delta x) + \\log_{10} (k),$$\n",
"\n",
"where $k$ is the proportionality constant, divided by $f''(x)_{\\rm exact}$.\n",
"\n",
"Let's confirm this is true using our finite-difference playground code, which imports the NRPy+-generated C code generated above for evaluating $f''(x)_{\\rm FD}$ at fourth-order accuracy, and outputs $\\log_{10} (\\Delta x)$ and $\\log_{10} E_{\\rm Rel}$ in a range of $\\Delta x$ that is truncation-error dominated."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"execution": {
"iopub.execute_input": "2021-03-07T17:32:17.983163Z",
"iopub.status.busy": "2021-03-07T17:32:17.982187Z",
"iopub.status.idle": "2021-03-07T17:32:17.986485Z",
"shell.execute_reply": "2021-03-07T17:32:17.985840Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Overwriting finite_difference_playground.c\n"
]
}
],
"source": [
"%%writefile finite_difference_playground.c\n",
"\n",
"// Part P1: Import needed header files\n",
"#include \"stdio.h\" // Provides printf()\n",
"#include \"stdlib.h\" // Provides malloc() and free()\n",
"#include \"math.h\" // Provides sin()\n",
"\n",
"// Part P2: Declare the IDX2S(gf,i) macro, which enables us to store 2-dimensions of\n",
"// data in a 1D array. In this case, consecutive values of \"i\"\n",
"// (\"gf\" held to a fixed value) are consecutive in memory, where\n",
"// consecutive values of \"gf\" (fixing \"i\") are separated by N elements in\n",
"// memory.\n",
"#define IDX2S(gf, i) ( (i) + Npts_in_stencil * (gf) )\n",
"\n",
"// Part P3: Set PHIGF and OUTPUTGF macros\n",
"#define PHIGF 0\n",
"#define OUTPUTGF 1\n",
"\n",
"// Part P4: Import code generated by NRPy+ to compute f''(x)\n",
"// as a finite difference derivative.\n",
"void f_dDD_FD(double *in_gfs,double *aux_gfs,const int i0,const int Npts_in_stencil,const double invdx0) {\n",
"#include \"finite_diff_tutorial-second_deriv.h\"\n",
"}\n",
"\n",
"// Part P5: Define the function we wish to differentiate, as well as its exact second derivative:\n",
"double f(const double x) { return sin(x); } // f(x)\n",
"double f_dDD_exact(const double x) { return -sin(x); } // f''(x)\n",
"\n",
"// Part P6: Define x_i = (x_0 + i*Delta_x)\n",
"double x_i(const double x_0,const int i,const double Delta_x) {\n",
" return (x_0 + (double)i*Delta_x);\n",
"}\n",
"\n",
"\n",
"// main() function\n",
"int main(int argc,char *argv[]) {\n",
" // Step 0: Read command-line arguments (TODO)\n",
"\n",
" // Step 1: Set some needed constants\n",
" const int Npts_in_stencil = 5; // Equal to the finite difference order, plus one. '+str(par.parval_from_str(\"finite_difference::FD_CENTDERIVS_ORDER\"))+'+1;\n",
" const double PI = 3.14159265358979323846264338327950288; // The scale over which the sine function varies.\n",
" const double x_eval = PI/4.0; // x_0 = desired x at which we wish to compute f(x)\n",
"\n",
" // Step 2: Evaluate f''(x_eval) using the exact expression:\n",
" double EX = f_dDD_exact(x_eval);\n",
"\n",
" // Step 3: Allocate space for two gridfunctions\n",
" double *in_gfs = (double *)malloc(sizeof(double)*Npts_in_stencil*2);\n",
"\n",
" // Step 4: Loop over grid spacings\n",
" for(double Delta_x = 1e-3*(2*PI);Delta_x<=1.5e-1*(2*PI);Delta_x*=1.1) {\n",
"\n",
" // Step 4a: x_eval is the center point of the finite differencing stencil,\n",
" // thus x_0 = x_eval - 2*dx for fourth-order-accurate first & second finite difference derivs,\n",
" // and x_0 = x_eval - 3*dx for sixth-order-accurate first & second finite difference derivs, etc.\n",
" // In general, for the integer Npts_in_stencil, we have\n",
" // x_0 = x_eval - (double)(Npts_in_stencil/2)*Delta_x,\n",
" // where we rely upon integer arithmetic (which always rounds down) to ensure\n",
" // Npts_in_stencil/2 = 5/2 = 2 for fourth-order-accurate first & second finite difference derivs:\n",
" const double x_0 = x_eval - (double)(Npts_in_stencil/2)*Delta_x;\n",
"\n",
" // Step 4b: Set \\phi=PHIGF to be f(x) as defined in the\n",
" // f(const double x) function above, where x_i = stencil_start_x + i*Delta_x:\n",
" for(int ii=0;ii]"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
"image/png": "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\n",
"text/plain": [
"