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. 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!
finite_diff_tutorial-second_deriv.h
[Back to top]¶We start with the NRPy+ code from the previous module, and output it to the C file finite_diff_tutorial-second_deriv.h
.
# Step P1: Import needed NRPy+ core modules:
from outputC import lhrh # NRPy+: Core C code output module
import NRPy_param_funcs as par # NRPy+: Parameter interface
import finite_difference as fin # NRPy+: Finite difference C code generation module
import grid as gri # NRPy+: Functions having to do with numerical grids
import indexedexp as ixp # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support
# Set the spatial dimension to 1
par.set_paramsvals_value("grid::DIM = 1")
# Register the input gridfunction "phi" and the gridfunction to which data are output, "output":
phi, output = gri.register_gridfunctions("AUX",["phi","output"])
# Declare phi_dDD as a rank-2 indexed expression: phi_dDD[i][j] = \partial_i \partial_j phi
phi_dDD = ixp.declarerank2("phi_dDD","nosym")
# Set output to \partial_0^2 phi
output = phi_dDD[0][0]
# Output to the screen the core C code for evaluating the finite difference derivative
fin.FD_outputC("stdout",lhrh(lhs=gri.gfaccess("out_gf","output"),rhs=output))
# Now, output the above C code to a file named "finite_diff_tutorial-second_deriv.h".
fin.FD_outputC("finite_diff_tutorial-second_deriv.h",lhrh(lhs=gri.gfaccess("aux_gfs","output"),rhs=output))
{ /* * NRPy+ Finite Difference Code Generation, Step 1 of 2: Read from main memory and compute finite difference stencils: */ /* * Original SymPy expression: * "const double phi_dDD00 = invdx0**2*(-5*phi/2 + 4*phi_i0m1/3 - phi_i0m2/12 + 4*phi_i0p1/3 - phi_i0p2/12)" */ const double phi_i0m2 = aux_gfs[IDX2S(PHIGF, i0-2)]; const double phi_i0m1 = aux_gfs[IDX2S(PHIGF, i0-1)]; const double phi = aux_gfs[IDX2S(PHIGF, i0)]; const double phi_i0p1 = aux_gfs[IDX2S(PHIGF, i0+1)]; const double phi_i0p2 = aux_gfs[IDX2S(PHIGF, i0+2)]; const double FDPart1_Rational_5_2 = 5.0/2.0; const double FDPart1_Rational_1_12 = 1.0/12.0; const double FDPart1_Rational_4_3 = 4.0/3.0; 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); /* * NRPy+ Finite Difference Code Generation, Step 2 of 2: Evaluate SymPy expressions and write to main memory: */ /* * Original SymPy expression: * "aux_gfs[IDX2S(OUTPUTGF, i0)] = phi_dDD00" */ aux_gfs[IDX2S(OUTPUTGF, i0)] = phi_dDD00; } Wrote to file "finite_diff_tutorial-second_deriv.h"
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
at fourth-order accuracy. Let's call the finite-difference second derivative of f evaluated at a point x f″(x)FD. A fourth-order-accurate f″(x)FD will, in the truncation-error-dominated regime, satisfy the equation
f″(x)FD=f″(x)exact+O(Δx4).Therefore, the relative error between the finite-difference derivative and the exact value should be given to good approximation by
ERel=|f″(x)FD−f″(x)exactf″(x)exact|∝Δx4,so that (taking the logarithm of both sides of the equation):
log10ERel=4log10(Δx)+log10(k),where k is the proportionality constant, divided by f″(x)exact.
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)FD at fourth-order accuracy, and outputs log10(Δx) and log10ERel in a range of Δx that is truncation-error dominated.
%%writefile finite_difference_playground.c
// Part P1: Import needed header files
#include "stdio.h" // Provides printf()
#include "stdlib.h" // Provides malloc() and free()
#include "math.h" // Provides sin()
// Part P2: Declare the IDX2S(gf,i) macro, which enables us to store 2-dimensions of
// data in a 1D array. In this case, consecutive values of "i"
// ("gf" held to a fixed value) are consecutive in memory, where
// consecutive values of "gf" (fixing "i") are separated by N elements in
// memory.
#define IDX2S(gf, i) ( (i) + Npts_in_stencil * (gf) )
// Part P3: Set PHIGF and OUTPUTGF macros
#define PHIGF 0
#define OUTPUTGF 1
// Part P4: Import code generated by NRPy+ to compute f''(x)
// as a finite difference derivative.
void f_dDD_FD(double *in_gfs,double *aux_gfs,const int i0,const int Npts_in_stencil,const double invdx0) {
#include "finite_diff_tutorial-second_deriv.h"
}
// Part P5: Define the function we wish to differentiate, as well as its exact second derivative:
double f(const double x) { return sin(x); } // f(x)
double f_dDD_exact(const double x) { return -sin(x); } // f''(x)
// Part P6: Define x_i = (x_0 + i*Delta_x)
double x_i(const double x_0,const int i,const double Delta_x) {
return (x_0 + (double)i*Delta_x);
}
// main() function
int main(int argc,char *argv[]) {
// Step 0: Read command-line arguments (TODO)
// Step 1: Set some needed constants
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;
const double PI = 3.14159265358979323846264338327950288; // The scale over which the sine function varies.
const double x_eval = PI/4.0; // x_0 = desired x at which we wish to compute f(x)
// Step 2: Evaluate f''(x_eval) using the exact expression:
double EX = f_dDD_exact(x_eval);
// Step 3: Allocate space for two gridfunctions
double *in_gfs = (double *)malloc(sizeof(double)*Npts_in_stencil*2);
// Step 4: Loop over grid spacings
for(double Delta_x = 1e-3*(2*PI);Delta_x<=1.5e-1*(2*PI);Delta_x*=1.1) {
// Step 4a: x_eval is the center point of the finite differencing stencil,
// thus x_0 = x_eval - 2*dx for fourth-order-accurate first & second finite difference derivs,
// and x_0 = x_eval - 3*dx for sixth-order-accurate first & second finite difference derivs, etc.
// In general, for the integer Npts_in_stencil, we have
// x_0 = x_eval - (double)(Npts_in_stencil/2)*Delta_x,
// where we rely upon integer arithmetic (which always rounds down) to ensure
// Npts_in_stencil/2 = 5/2 = 2 for fourth-order-accurate first & second finite difference derivs:
const double x_0 = x_eval - (double)(Npts_in_stencil/2)*Delta_x;
// Step 4b: Set \phi=PHIGF to be f(x) as defined in the
// f(const double x) function above, where x_i = stencil_start_x + i*Delta_x:
for(int ii=0;ii<Npts_in_stencil;ii++) {
in_gfs[IDX2S(PHIGF, ii)] = f(x_i(x_0,ii,Delta_x));
}
// Step 4c: Set invdx0, which is needed by the NRPy+-generated "finite_diff_tutorial-second_deriv.h"
const double invdx0 = 1.0/Delta_x;
// Step 4d: Evaluate the finite-difference second derivative of f(x):
const int i0 = Npts_in_stencil/2; // The derivative is evaluated at the center of the stencil.
f_dDD_FD(in_gfs,in_gfs,i0,Npts_in_stencil,invdx0);
double FD = in_gfs[IDX2S(OUTPUTGF,i0)];
// Step 4e: Print log_10(\Delta x) and log_10([relative error])
printf("%e\t%.15e\n",log10(Delta_x),log10(fabs((EX-FD)/(EX))));
}
// Step 5: Free the allocated memory for the gridfunctions.
free(in_gfs);
return 0;
}
Overwriting finite_difference_playground.c
Next we compile and run the C code.
import cmdline_helper as cmd
cmd.C_compile("finite_difference_playground.c", "fdp")
cmd.delete_existing_files("data.txt")
cmd.Execute("fdp", "", "data.txt")
Compiling executable... (EXEC): Executing `gcc -std=gnu99 -Ofast -fopenmp -march=native -funroll-loops finite_difference_playground.c -o fdp -lm`... (BENCH): Finished executing in 0.20644569396972656 seconds. Finished compilation. (EXEC): Executing `taskset -c 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 ./fdp `... (BENCH): Finished executing in 0.20549631118774414 seconds.
Finally, let's plot log10ERel as a function of log10(Δx). Again, the expression at fourth-order accuracy should obey
log10ERel=4log10(Δx)+log10(k).Defining ˆx=log10(Δx) and y(ˆx)=log10ERel, we can write the above equation in the more suggestive form:
y(ˆx)=4ˆx+log10(k),so y(ˆx)=log10ERel(log10(Δx)) should be a line with positive slope of 4.
%matplotlib inline
import matplotlib.pyplot as plt
# from https://stackoverflow.com/questions/52386747/matplotlib-check-for-empty-plot
import numpy
x, y = numpy.loadtxt(fname='data.txt', delimiter='\t', unpack=True)
fig, ax = plt.subplots()
ax.set_xlabel('log10(Delta_x)')
ax.set_ylabel('log10([Relative Error])')
ax.plot(x, y, color='b')
[<matplotlib.lines.Line2D at 0x7fe648756e30>]
A quick glance at the above plot indicates that between log10(Δx)≈−2.0 and log10(Δx)≈−1.0, the logarithmic relative error log10ERel increases by about 4, indicating a positive slope of approximately 4. Thus we have confirmed fourth-order convergence.
polyfit()
function to evaluate the least-squares slope of the above line.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-Start_to_Finish-Finite_Difference_Playground.pdf. (Note that clicking on this link may not work; you may need to open the PDF file through another means.)
import cmdline_helper as cmd # NRPy+: Multi-platform Python command-line interface
cmd.output_Jupyter_notebook_to_LaTeXed_PDF("Tutorial-Start_to_Finish-Finite_Difference_Playground")
Created Tutorial-Start_to_Finish-Finite_Difference_Playground.tex, and compiled LaTeX file to PDF file Tutorial-Start_to_Finish- Finite_Difference_Playground.pdf