#!/usr/bin/env python # coding: utf-8 # # # # # Finite-Difference Playground: Using NRPy+-Generated C Codes in a Larger Project # # ## Author: Zach Etienne # ### Formatting improvements courtesy Brandon Clark # # ## This notebook demonstrates the use of NRPy+ generated C codes in finite-difference derivatives computation and verifies fourth-order convergence. It invites further exploration of higher-order accuracies and highlights the advantages of NRPy+'s handling of complex higher-dimensional expressions. # # ## Introduction: # 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! # # # # Table of Contents # $$\label{toc}$$ # # This notebook is organized as follows # # 1. [Step 1](#outputc): Register the C function `finite_diff_tutorial__second_deriv()` # 1. [Step 2](#fdplayground): Finite-Difference Playground: A Complete C Code for Analyzing Finite-Difference Expressions Output by NRPy+ # 1. [Step 3](#exercise): Exercises # 1. [Step 4](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file # # # # Step 1: Register the C function `finite_diff_tutorial__second_deriv()` \[Back to [top](#toc)\] # $$\label{outputc}$$ # # We start with the NRPy+ code from the [previous module](Tutorial-Finite_Difference_Derivatives.ipynb): # In[1]: # Step P1: Import needed NRPy+ core modules: import outputC as outC # 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 import cmdline_helper as cmd # NRPy+: Multi-platform Python command-line interface import os, shutil # Standard Python: multiplatform OS funcs # 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] # Now for the C code generation. First create the output directory for C codes. # In[2]: # Step P2: Create C code output directory: Ccodesrootdir = os.path.join("FiniteDifferencePlayground_Ccodes/") # P2.a: First remove C code output directory if it exists # Courtesy https://stackoverflow.com/questions/303200/how-do-i-remove-delete-a-folder-that-is-not-empty shutil.rmtree(Ccodesrootdir, ignore_errors=True) # P2.b: Then create a fresh directory # Then create a fresh directory cmd.mkdir(Ccodesrootdir) # Then declare C macros that will appear within `NRPy_basic_defines.h`, `#include`d in most C files within NRPy+ generated C codes. # In[3]: outC.outC_NRPy_basic_defines_h_dict["FiniteDifferencePlayground"] = r""" // 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) ) // Declare PHIGF and OUTPUTGF, used for IDX2S's gf input. #define PHIGF 0 #define OUTPUTGF 1 """ # Next, register the C function `finite_diff_tutorial__second_deriv()`, which contains the core C-code kernel for evaluating the finite-difference derivative. # # Registering C functions with NRPy+ in this way encourages developers to abide by NRPy+ standards, in which # # * core functions are located within files of the same name # * code comments describing the function are well-formatted and appear at the top of the function # * function prototypes are automatically generated from information input into `add_to_Cfunction_dict()` # * generating a fully functional `Makefile` from lists of C functions is automated and provided by `outputC`'s `construct_Makefile_from_outC_function_dict()`. # # Further, it is conventional in NRPy+ to register C functions within a Python function, called at the bottom of a notebook/workflow. Turns out C-code generation of very complex expressions can take some time, and doing it via function calls enables us to parallelize the process (via Python's [multiprocessing](https://docs.python.org/3/library/multiprocessing.html) module), resulting in much faster turnaround times. # # Doing it this way also enables functions to be automatically generated with multiple options, with each function specializing in a given option. For example, one could imagine outputting multiple such functions for different finite difference orders. When compared to C++ template metaprogramming ("[Difficulty level: Hard](https://www.geeksforgeeks.org/template-metaprogramming-in-c/)"), it is far simpler to gain familiarity with this more explicit approach. # In[4]: def add_to_Cfunction_dict_finite_diff_tutorial__second_deriv(): # includes field: list of files to #include at the top. NRPy_basic_defines.h defines C macros used here # for accessing gridfunction data. includes = ["stdio.h", "NRPy_basic_defines.h"] # Add stdio.h in case we'd like to use printf() for debugging. prefunc = "" # would contain other functions that are *only* called by this function, declared static. desc = r"""Evaluate phi''(x0) the second derivative of phi, a function sampled on a uniform grid. The derivative is evaluated using a standard 4th-order-accurate finite-difference approach. See Tutorial-Finite_Difference_Derivatives.ipynb in https://github.com/zachetienne/nrpytutorial for more details. """ c_type = "void" name = "finite_diff_tutorial__second_deriv" params = "const double *in_gfs, const double invdx0, const int i0, const int Npts_in_stencil, double *aux_gfs" body = fin.FD_outputC("returnstring", outC.lhrh(lhs=gri.gfaccess("aux_gfs", "output"), rhs=output), params="preindent=1,includebraces=False") + "\n" outC.add_to_Cfunction_dict( includes=includes, prefunc=prefunc, desc=desc, c_type=c_type, name=name, params=params, body=body, enableCparameters=False) # Let's take a look at NRPy+'s generated version of this function. All we need to do is call the above function `add_to_Cfunction_dict_finite_diff_tutorial__second_deriv()` to register the function, and then it can be accessed directly from `outputC`'s `outC_function_master_list` list. # In[5]: add_to_Cfunction_dict_finite_diff_tutorial__second_deriv() for item in outC.outC_function_master_list: print(outC.outC_function_dict[item.name]) # # # # Step 2: Finite-Difference Playground: A Complete C Code for Analyzing Finite-Difference Expressions Output by NRPy+ \[Back to [top](#toc)\] # $$\label{fdplayground}$$ # # 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 # # $$f(x) = \sin(x)$$ # # 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 # # $$f''(x)_{\rm FD} = f''(x)_{\rm exact} + \mathcal{O}(\Delta x^4).$$ # # 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 # # $$E_{\rm Rel} = \left| \frac{f''(x)_{\rm FD} - f''(x)_{\rm exact}}{f''(x)_{\rm exact}}\right| \propto \Delta x^4,$$ # # so that (taking the logarithm of both sides of the equation): # # $$\log_{10} E_{\rm Rel} = 4 \log_{10} (\Delta x) + \log_{10} (k),$$ # # where $k$ is the proportionality constant, divided by $f''(x)_{\rm 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)_{\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. # In[6]: def add_to_Cfunction_dict_main__finite_difference_playground(): includes = ["stdio.h", "stdlib.h", "math.h", "NRPy_basic_defines.h", "NRPy_function_prototypes.h"] prefunc = r""" // 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) // Define the uniform grid coordinate: // 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); } """ desc = """// main() function: // Step 0: Read command-line input, set up grid structure, allocate memory for gridfunctions, set up coordinates // Step 1: Write test data to gridfunctions // Step 2: Overwrite all data in ghost zones with NaNs // Step 3: Apply curvilinear boundary conditions // Step 4: Print gridfunction data after curvilinear boundary conditions have been applied // Step 5: Free all allocated memory """ c_type = "int" name = "main" params = "int argc, const char *argv[]" body = r""" // 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 gridfunction input and output. double *restrict gfs = (double *restrict)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 # # # Step 3: Exercises \[Back to [top](#toc)\] # $$\label{exercise}$$ # # 1. Use NumPy's [`polyfit()`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.polyfit.html) function to evaluate the least-squares slope of the above line. # 2. Explore $\log_{10}(\Delta x)$ outside the above (truncation-error-dominated) range. What other errors dominate outside the truncation-error-dominated regime? # 3. Adjust the above NRPy+ and C codes to support 6th-order-accurate finite differencing. What should the slope of the resulting plot of $\log_{10} E_{\rm Rel}$ versus $\log_{10}(\Delta x)$ be? Explain why this case does not provide as clean a slope as the 4th-order case. # # # # Step 4: 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-Start_to_Finish-Finite_Difference_Playground.pdf](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.) # In[10]: 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")