def read_gfs_from_memory(list_of_base_gridfunction_names_in_derivs, fdstencl,
                         sympyexpr_list, FDparams):
    # with open(list_of_base_gridfunction_names_in_derivs[0]+".txt","w") as file:
    #     file.write(str(list_of_base_gridfunction_names_in_derivs))
    #     file.write(str(fdstencl))
    #     file.write(str(sympyexpr_list))
    #     file.write(str(FDparams))
    """

    :param list_of_base_gridfunction_names_in_derivs:
    :param fdstencl:
    :param sympyexpr_list:
    :param FDparams:
    :return:
    >>> from outputC import lhrh
    >>> import indexedexp as ixp
    >>> import NRPy_param_funcs as par
    >>> from finite_difference_helpers import generate_list_of_deriv_vars_from_lhrh_sympyexpr_list,FDparams
    >>> from finite_difference_helpers import extract_from_list_of_deriv_vars__base_gfs_and_deriv_ops_lists
    >>> from finite_difference_helpers import read_gfs_from_memory
    >>> from finite_difference import compute_fdcoeffs_fdstencl
    >>> import grid as gri
    >>> gri.glb_gridfcs_list = []
    >>> hDD      = ixp.register_gridfunctions_for_single_rank2("EVOL","hDD","sym01")
    >>> hDD_dD   = ixp.declarerank3("hDD_dD","sym01")
    >>> hDD_dupD = ixp.declarerank3("hDD_dupD","sym01")
    >>> vU       = ixp.register_gridfunctions_for_single_rank1("EVOL","vU")
    >>> a0,a1,b,c = par.Cparameters("REAL",__name__,["a0","a1","b","c"],1)
    >>> par.set_parval_from_str("finite_difference::FD_CENTDERIVS_ORDER",2)
    >>> FDparams.DIM=3
    >>> FDparams.SIMD_enable="False"
    >>> FDparams.PRECISION="double"
    >>> FDparams.MemAllocStyle="012"
    >>> FDparams.upwindcontrolvec=vU
    >>> exprlist = [lhrh(lhs=a0,rhs=b*hDD[1][0] + c*hDD_dD[0][1][1]), \
                    lhrh(lhs=a1,rhs=c*hDD_dupD[0][2][1]*vU[1])]
    >>> list_of_deriv_vars = generate_list_of_deriv_vars_from_lhrh_sympyexpr_list(exprlist,FDparams)
    >>> list_of_base_gridfunction_names_in_derivs, list_of_deriv_operators = extract_from_list_of_deriv_vars__base_gfs_and_deriv_ops_lists(list_of_deriv_vars)
    >>> fdcoeffs = [[] for i in range(len(list_of_deriv_operators))]
    >>> fdstencl = [[[] for i in range(4)] for j in range(len(list_of_deriv_operators))]
    >>> for i in range(len(list_of_deriv_operators)): fdcoeffs[i], fdstencl[i] = compute_fdcoeffs_fdstencl(list_of_deriv_operators[i])
    >>> print(read_gfs_from_memory(list_of_base_gridfunction_names_in_derivs, fdstencl, exprlist, FDparams))
    const double hDD01_i0_i1m1_i2 = in_gfs[IDX4(HDD01GF, i0,i1-1,i2)];
    const double hDD01 = in_gfs[IDX4(HDD01GF, i0,i1,i2)];
    const double hDD01_i0_i1p1_i2 = in_gfs[IDX4(HDD01GF, i0,i1+1,i2)];
    const double hDD02_i0_i1m2_i2 = in_gfs[IDX4(HDD02GF, i0,i1-2,i2)];
    const double hDD02_i0_i1m1_i2 = in_gfs[IDX4(HDD02GF, i0,i1-1,i2)];
    const double hDD02 = in_gfs[IDX4(HDD02GF, i0,i1,i2)];
    const double hDD02_i0_i1p1_i2 = in_gfs[IDX4(HDD02GF, i0,i1+1,i2)];
    const double hDD02_i0_i1p2_i2 = in_gfs[IDX4(HDD02GF, i0,i1+2,i2)];
    const double vU1 = in_gfs[IDX4(VU1GF, i0,i1,i2)];
    <BLANKLINE>
    """

    # Step 4a: Compile list of points to read from memory
    #          for each gridfunction i, based on list
    #          provided in fdstencil[i][].
    list_of_points_read_from_memory_with_duplicates = [
        [] for i in range(len(gri.glb_gridfcs_list))
    ]
    for j in range(len(list_of_base_gridfunction_names_in_derivs)):
        derivgfname = list_of_base_gridfunction_names_in_derivs[j]
        # Next find the corresponding gridfunction index:
        for i in range(len(gri.glb_gridfcs_list)):
            gfname = gri.glb_gridfcs_list[i].name
            # If the gridfunction for the derivative matches, then
            #    add to the list of points read from memory:
            if derivgfname == gfname:
                for k in range(len(fdstencl[j])):
                    list_of_points_read_from_memory_with_duplicates[i].append(
                        str(fdstencl[j][k][0]) + "," + str(fdstencl[j][k][1]) +
                        "," + str(fdstencl[j][k][2]) + "," +
                        str(fdstencl[j][k][3]))

    # Step 4b: "Zeroth derivative" case:
    #     If gridfunction appears in expression not
    #     as derivative (i.e., by itself), it must
    #     be read from memory as well.
    for expr in range(len(sympyexpr_list)):
        for var in sympyexpr_list[expr].rhs.free_symbols:
            vartype = gri.variable_type(var)
            if vartype == "gridfunction":
                for i in range(len(gri.glb_gridfcs_list)):
                    gfname = gri.glb_gridfcs_list[i].name
                    if gfname == str(var):
                        list_of_points_read_from_memory_with_duplicates[
                            i].append("0,0,0,0")

    # Step 4c: Remove duplicates when reading from memory;
    #     do not needlessly read the same variable
    #     from memory twice.
    list_of_points_read_from_memory = [
        [] for i in range(len(gri.glb_gridfcs_list))
    ]
    for i in range(len(gri.glb_gridfcs_list)):
        list_of_points_read_from_memory[i] = superfast_uniq(
            list_of_points_read_from_memory_with_duplicates[i])

    # Step 4d: Minimize cache misses:
    #      Sort the list of points read from
    #      main memory by how they are stored
    #      in memory.

    # Step 4d.i: Define a function that maps a gridpoint
    #     index (i,j,k,l) to a unique memory "address",
    #     which will correspond to the correct ordering
    #     of actual memory addresses.
    #
    #     Input: a list of 4 indices, e.g., (i,j,k,l)
    #            corresponding to a gridpoint's *spatial*
    #            index in memory (thus we support up to
    #            4D in space). If spatial dimension is
    #            less than 4D, then just set latter
    #            index/indices to zero. E.g., for 2D
    #            spatial indexing, set (i,j,0,0).
    #     Output: a single number, which when sorted
    #            will yield a unique "address" in memory
    #            such that consecutive addresses are
    #            consecutive in memory.
    def unique_idx(idx4, FDparams):
        # os and sz are set *just for the purposes of ensuring indices are ordered in memory*
        #    Do not modify the values of os and sz.
        os = 50  # offset
        sz = 100  # assumed size in each direction
        if FDparams.MemAllocStyle == "210":
            return str(
                int(idx4[0]) + os + sz * ((int(idx4[1]) + os) + sz *
                                          ((int(idx4[2]) + os) + sz *
                                           (int(idx4[3]) + os))))
        if FDparams.MemAllocStyle == "012":
            return str(
                int(idx4[3]) + os + sz * ((int(idx4[2]) + os) + sz *
                                          ((int(idx4[1]) + os) + sz *
                                           (int(idx4[0]) + os))))
        print("Error: MemAllocStyle = " + FDparams.MemAllocStyle +
              " unsupported.")
        sys.exit(1)

    # Step 4d.ii: For each gridfunction and
    #      point read from memory, call unique_idx,
    #      then sort according to memory "address"
    # Input: list_of_points_read_from_memory[gridfunction][point],
    #        gri.glb_gridfcs_list[gridfunction]
    # Output: 1) A list of points to be read from
    #            memory, sorted according to memory
    #            "address":
    #            sorted_list_of_points_read_from_memory[gridfunction][point]
    #        2) A list containing the gridfunction
    #           read at each point, with the number
    #           of elements corresponding exactly
    #           to the total number of points read
    #           from memory for all gridfunctions:
    #           read_from_memory_gf[]
    read_from_memory_gf = []
    sorted_list_of_points_read_from_memory = [
        [] for i in range(len(gri.glb_gridfcs_list))
    ]
    for gfidx in range(len(gri.glb_gridfcs_list)):
        # Continue only if reading at least one point of gfidx from memory.
        #     The sorting algorithm at the end of this code block is not
        #     well-defined (will throw an error) if no points of gfidx are
        #     read from memory.
        if len(list_of_points_read_from_memory[gfidx]) > 0:
            read_from_memory_index = []
            for idx in list_of_points_read_from_memory[gfidx]:
                read_from_memory_gf.append(gri.glb_gridfcs_list[gfidx])
                idxsplit = idx.split(',')
                idx4 = [
                    int(idxsplit[0]),
                    int(idxsplit[1]),
                    int(idxsplit[2]),
                    int(idxsplit[3])
                ]
                read_from_memory_index.append(unique_idx(idx4, FDparams))
                # https://stackoverflow.com/questions/13668393/python-sorting-two-lists
                _unused_list, sorted_list_of_points_read_from_memory[gfidx] = \
                    [list(x) for x in zip(*sorted(zip(read_from_memory_index, list_of_points_read_from_memory[gfidx]),
                                                  key=itemgetter(0)))]
    # Step 4e: Create the full C code string
    #      for reading from memory:

    read_from_memory_Ccode = ""
    count = 0
    for gfidx in range(len(gri.glb_gridfcs_list)):
        for pt in range(len(sorted_list_of_points_read_from_memory[gfidx])):
            read_from_memory_Ccode += read_from_memory_Ccode_onept(
                read_from_memory_gf[count].name,
                sorted_list_of_points_read_from_memory[gfidx][pt], FDparams)
            count += 1
    return read_from_memory_Ccode
Esempio n. 2
0
def FD_outputC(filename, sympyexpr_list, params="", upwindcontrolvec=""):
    outCparams = parse_outCparams_string(params)

    # Step 0.a:
    # In case sympyexpr_list is a single sympy expression,
    #     convert it to a list with just one element:
    if type(sympyexpr_list) is not list:
        sympyexpr_list = [sympyexpr_list]

    # Step 0.b:
    # finite_difference.py takes control over outCparams.includebraces here,
    #     which is necessary because outputC() is called twice:
    #     first for the reads from main memory and finite difference
    #     stencil expressions, and second for the SymPy expressions and
    #     writes to main memory.
    # If outCparams.includebraces==True, then it will close off the braces
    #     after the finite difference stencil expressions and start new ones
    #     for the SymPy expressions and writes to main memory, resulting
    #     in a non-functioning C code.
    # To get around this issue, we create braces around the entire
    #     string of C output from this function, only if
    #     outCparams.includebraces==True.
    # See Step 6 for corresponding end brace.
    if outCparams.includebraces == "True":
        Coutput = outCparams.preindent + "{\n"
        indent = "   "
    else:
        Coutput = ""
        indent = ""

    # Step 1a:
    # Create a list of free symbols in the sympy expr list
    #     that are registered neither as gridfunctions nor
    #     as C parameters. These *must* be derivatives,
    #     so we call the list "list_of_deriv_vars"
    list_of_deriv_vars_with_duplicates = []
    for expr in sympyexpr_list:
        for var in expr.rhs.free_symbols:
            vartype = gri.variable_type(var)
            if vartype == "other":
                # vartype=="other" should ONLY refer to derivatives, so
                #    if "_dD" or variants do not appear in a variable classified
                #    neither as a gridfunction nor a Cparameter, then error out.
                if ("_dD"   in str(var)) or \
                   ("_dKOD" in str(var)) or \
                   ("_dupD" in str(var)) or \
                   ("_ddnD" in str(var)):
                    pass
                else:
                    print("Error: Unregistered variable \"" + str(var) +
                          "\" in SymPy expression")
                    print(
                        "All variables in SymPy expressions passed to FD_outputC() must be registered"
                    )
                    print(
                        "in NRPy+ as either a gridfunction or Cparameter, by calling"
                    )
                    print(
                        str(var) +
                        " = register_gridfunctions...() (in ixp/grid) if \"" +
                        str(var) + "\" is a gridfunction, or")
                    print(
                        str(var) +
                        " = Cparameters() (in par) otherwise (e.g., if it is a free parameter set at C runtime)."
                    )
                    exit(1)
                list_of_deriv_vars_with_duplicates.append(var)
#            elif vartype == "gridfunction":
#                list_of_deriv_vars_with_duplicates.append(var)
    list_of_deriv_vars = superfast_uniq(list_of_deriv_vars_with_duplicates)

    # Upwinding with respect to a control vector: algorithm description.
    #   To enable, set the FD_outputC()'s fourth function argument to the
    #   desired control vector. In BSSN, the betaU vector controls the upwinding.
    #   See https://arxiv.org/pdf/gr-qc/0206072.pdf for motivation and
    #   https://arxiv.org/pdf/gr-qc/0109032.pdf for implementation details,
    #   at second order. Note that the BSSN shift vector behaves like a *negative*
    #   velocity. See http://www.damtp.cam.ac.uk/user/naweb/ii/advection/advection.php
    #   for a very basic example motivating this choice.

    # Step 1b: For each variable with suffix _dupD, append to
    #          the list_of_deriv_vars the corresponding _ddnD.
    #          Both are required for control-vector upwinding. See
    #          the above print() block for further documentation
    #          on upwinding--both motivation and implementation
    #          details.
    if upwindcontrolvec != "":
        for var in list_of_deriv_vars:
            if "_dupD" in str(var):
                list_of_deriv_vars.append(
                    sp.sympify(str(var).replace("_dupD", "_ddnD")))

    # Finally, sort the list_of_deriv_vars. This ensures
    #     consistency in the C code output, and might even be
    #     tuned to reduce cache misses.
    #     Thanks to Aaron Meurer for this nice one-liner!
    list_of_deriv_vars = sorted(list_of_deriv_vars, key=sp.default_sort_key)

    # Step 2:
    # Process list_of_deriv_vars into a list of base gridfunctions
    #    and a list of derivative operators.
    # Step 2a:
    # First determine the base gridfunction name from
    #     "list_of_deriv_vars"
    deriv__base_gridfunction_name = []
    deriv__operator = []
    for var in list_of_deriv_vars:
        # Step 2a.1: Check that the number of juxtaposed integers
        #            at the end of a variable name matches the
        #            number of U's + D's in the variable name:
        varstr = str(var)
        num_UDs = 0
        for i in range(len(varstr)):
            if varstr[i] == 'D' or varstr[i] == 'U':
                num_UDs += 1
        num_digits = 0
        i = len(varstr) - 1
        while varstr[i].isdigit():
            num_digits += 1
            i -= 1
        if num_UDs != num_digits:
            print("Error: " + varstr + " has " + str(num_UDs) +
                  " U's and D's, but ")
            print(
                str(num_digits) + " integers at the end. These must be equal.")
            print("Please rename your gridfunction.")
            exit(1)
        # Step 2a.2: Based on the variable name, find the rank of
        #            the underlying gridfunction of which we're
        #            trying to take the derivative.
        rank = 0  # rank = "number of juxtaposed U's and D's before the underscore in a derivative expression"
        underscore_position = -1
        for i in range(len(varstr) - 1, -1, -1):
            if underscore_position > 0 and (varstr[i] == "U"
                                            or varstr[i] == "D"):
                rank += 1
            if varstr[i] == "_":
                underscore_position = i

        # Step 2a.3: Based on the variable name, find the order
        #            of the derivative we're trying to take.
        deriv_order = 0  # deriv_order = "number of D's after the underscore in a derivative expression"
        for i in range(underscore_position + 1, len(varstr)):
            if (varstr[i] == "D"):
                deriv_order += 1

        # Step 2a.4: Based on derivative order and rank,
        #            store the base gridfunction name in
        #            deriv__base_gridfunction_name[]
        deriv__base_gridfunction_name.append(
            varstr[0:underscore_position] +
            varstr[len(varstr) - deriv_order - rank:len(varstr) - deriv_order])
        deriv__operator.append(varstr[underscore_position + 1:len(varstr) -
                                      deriv_order - rank] +
                               varstr[len(varstr) - deriv_order:len(varstr)])

    # Step 2b:
    # Then check each base gridfunction to determine whether
    #     it is indeed registered as a gridfunction.
    #     If not, exit with error.
    for basegf in deriv__base_gridfunction_name:
        is_gf = False
        for gf in gri.glb_gridfcs_list:
            if basegf == str(gf.name):
                is_gf = True
                pass
        if not is_gf:
            print("Error: Attempting to take the derivative of " + basegf +
                  ", which is not a registered gridfunction.")
            print(
                "       Make sure your gridfunction name does not have any underscores in it!"
            )
            exit(1)

    # Step 2c:
    # Check each derivative operator to make sure it is
    #     supported. If not, error out.
    for i in range(len(deriv__operator)):
        found_derivID = False
        for derivID in ["dD", "dupD", "ddnD", "dKOD"]:
            if derivID in deriv__operator[i]:
                found_derivID = True
        if not found_derivID:
            print("Error: Valid derivative operator in " + deriv__operator[i] +
                  " not found.")
            exit(1)

    # Step 2d (Upwinded derivatives algorithm, part 1):
    # If an upwinding control vector is specified, determine
    #    which of the elements of the vector will be required.
    #    This ensures that those elements are read from memory.
    # For example, if a symmetry axis is specified,
    #     upwind derivatives with respect to only
    #     two of the three dimensions are used. Here
    #     we find all directions used for upwinding.
    if upwindcontrolvec != "":
        upwind_directions_unsorted_withdups = []
        for deriv_op in deriv__operator:
            if "dupD" in deriv_op:
                if deriv_op[len(deriv_op) - 1].isdigit():
                    dirn = int(deriv_op[len(deriv_op) - 1])
                    upwind_directions_unsorted_withdups.append(dirn)
                else:
                    print("Error: Derivative operator " + deriv_op +
                          " does not contain a direction")
                    exit(1)
        upwind_directions = []
        if len(upwind_directions_unsorted_withdups) > 0:
            upwind_directions = superfast_uniq(
                upwind_directions_unsorted_withdups)
            upwind_directions = sorted(upwind_directions,
                                       key=sp.default_sort_key)

    # Step 3:
    # Evaluate the finite difference stencil for each
    #     derivative operator,
    # TODO: being careful not to needlessly recompute.
    # Note: Each finite difference stencil consists
    #     of two parts:
    #     1) The coefficient, and
    #     2) The index corresponding to the coefficient.
    #     The former is stored as a rational number, and
    #     the latter as a simple string, such that e.g.,
    #     in 3D, the empty string corresponds to (i,j,k),
    #     the string "ip1" corresponds to (i+1,j,k),
    #     the string "ip1kp1" corresponds to (i+1,j,k+1),
    #     etc.
    fdcoeffs = [[] for i in range(len(deriv__operator))]
    fdstencl = [[[] for i in range(4)] for j in range(len(deriv__operator))]
    for i in range(len(deriv__operator)):
        fdcoeffs[i], fdstencl[i] = compute_fdcoeffs_fdstencl(
            deriv__operator[i])

    # Step 4:
    # Create C code to read gridfunctions from memory
    # Step 4a: Compile list of points to read from memory
    #          for each gridfunction i, based on list
    #          provided in fdstencil[i][].
    list_of_points_read_from_memory_with_duplicates = [
        [] for i in range(len(gri.glb_gridfcs_list))
    ]
    for j in range(len(deriv__base_gridfunction_name)):
        derivgfname = deriv__base_gridfunction_name[j]
        # Next find the corresponding gridfunction index:
        for i in range(len(gri.glb_gridfcs_list)):
            gfname = gri.glb_gridfcs_list[i].name
            # If the gridfunction for the derivative matches, then
            #    add to the list of points read from memory:
            if derivgfname == gfname:
                for k in range(len(fdstencl[j])):
                    list_of_points_read_from_memory_with_duplicates[i].append(str(fdstencl[j][k][0]) + "," + \
                                                                              str(fdstencl[j][k][1]) + "," + \
                                                                              str(fdstencl[j][k][2]) + "," + \
                                                                              str(fdstencl[j][k][3]))

    # Step 4b: "Zeroth derivative" case:
    #     If gridfunction appears in expression not
    #     as derivative (i.e., by itself), it must
    #     be read from memory as well.
    for expr in range(len(sympyexpr_list)):
        for var in sympyexpr_list[expr].rhs.free_symbols:
            vartype = gri.variable_type(var)
            if vartype == "gridfunction":
                for i in range(len(gri.glb_gridfcs_list)):
                    gfname = gri.glb_gridfcs_list[i].name
                    if gfname == str(var):
                        list_of_points_read_from_memory_with_duplicates[
                            i].append("0,0,0,0")

    # Step 4c: Remove duplicates when reading from memory;
    #     do not needlessly read the same variable
    #     from memory twice.
    list_of_points_read_from_memory = [
        [] for i in range(len(gri.glb_gridfcs_list))
    ]
    for i in range(len(gri.glb_gridfcs_list)):
        list_of_points_read_from_memory[i] = superfast_uniq(
            list_of_points_read_from_memory_with_duplicates[i])

    # Step 4d: Minimize cache misses:
    #      Sort the list of points read from
    #      main memory by how they are stored
    #      in memory.

    # Step 4d.i: Define a function that maps a gridpoint
    #     index (i,j,k,l) to a unique memory "address",
    #     which will correspond to the correct ordering
    #     of actual memory addresses.
    #
    #     Input: a list of 4 indices, e.g., (i,j,k,l)
    #            corresponding to a gridpoint's *spatial*
    #            index in memory (thus we support up to
    #            4D in space). If spatial dimension is
    #            less than 4D, then just set latter
    #            index/indices to zero. E.g., for 2D
    #            spatial indexing, set (i,j,0,0).
    #     Output: a single number, which when sorted
    #            will yield a unique "address" in memory
    #            such that consecutive addresses are
    #            consecutive in memory.
    def unique_idx(idx4):
        # os and sz are set *just for the purposes of ensuring indices are ordered in memory*
        #    Do not modify the values of os and sz.
        os = 50  # offset
        sz = 100  # assumed size in each direction
        if par.parval_from_str("MemAllocStyle") == "210":
            return str(
                int(idx4[0]) + os + sz * ((int(idx4[1]) + os) + sz *
                                          ((int(idx4[2]) + os) + sz *
                                           (int(idx4[3]) + os))))
        elif par.parval_from_str("MemAllocStyle") == "012":
            return str(
                int(idx4[3]) + os + sz * ((int(idx4[2]) + os) + sz *
                                          ((int(idx4[1]) + os) + sz *
                                           (int(idx4[0]) + os))))
        else:
            print("Error: MemAllocStyle = " +
                  par.parval_from_str("MemAllocStyle") + " unsupported.")
            exit(1)

    # Step 4d.ii: For each gridfunction and
    #      point read from memory, call unique_idx,
    #      then sort according to memory "address"
    # Input: list_of_points_read_from_memory[gridfunction][point],
    #        gri.glb_gridfcs_list[gridfunction]
    # Output: 1) A list of points to be read from
    #            memory, sorted according to memory
    #            "address":
    #            sorted_list_of_points_read_from_memory[gridfunction][point]
    #        2) A list containing the gridfunction
    #           read at each point, with the number
    #           of elements corresponding exactly
    #           to the total number of points read
    #           from memory for all gridfunctions:
    #           read_from_memory_gf[]
    read_from_memory_gf = []
    sorted_list_of_points_read_from_memory = [
        [] for i in range(len(gri.glb_gridfcs_list))
    ]
    for gfidx in range(len(gri.glb_gridfcs_list)):
        # Continue only if reading at least one point of gfidx from memory.
        #     The sorting algorithm at the end of this code block is not
        #     well-defined (will throw an error) if no points of gfidx are
        #     read from memory.
        if len(list_of_points_read_from_memory[gfidx]) > 0:
            read_from_memory_index = []
            for idx in list_of_points_read_from_memory[gfidx]:
                read_from_memory_gf.append(gri.glb_gridfcs_list[gfidx])
                idxsplit = idx.split(',')
                idx4 = [
                    int(idxsplit[0]),
                    int(idxsplit[1]),
                    int(idxsplit[2]),
                    int(idxsplit[3])
                ]
                read_from_memory_index.append(unique_idx(idx4))
                # https://stackoverflow.com/questions/13668393/python-sorting-two-lists
                UNUSEDlist, sorted_list_of_points_read_from_memory[gfidx] = \
                    [list(x) for x in zip(*sorted(zip(read_from_memory_index, list_of_points_read_from_memory[gfidx]),
                                                  key=itemgetter(0)))]
    # Step 4e: Create the full C code string
    #      for reading from memory:

    # if DIM==4:
    #     input: [i,j,k,l]
    #     output: "i0+i,i1+j,i2+k,i3+l"
    # if DIM==3:
    #     input: [i,j,k,l]
    #     output: "i0+i,i1+j,i2+k"
    # etc.
    def ijkl_string(idx4):
        DIM = par.parval_from_str("DIM")
        retstring = ""
        for i in range(DIM):
            if i > 0:
                # Add a comma
                retstring += ","
            retstring += "i" + str(i) + "+" + str(idx4[i])
        return retstring.replace("+-", "-").replace("+0", "")

    def out__type_var(in_var, AddPrefix_for_UpDownWindVars=True):
        varname = str(in_var)
        # Disable prefixing upwinded and downwinded variables
        # if the upwind control vector algorithm is disabled.
        if upwindcontrolvec == "":
            AddPrefix_for_UpDownWindVars = False
        if AddPrefix_for_UpDownWindVars:
            if "_dupD" in varname:  # Variables suffixed with "_dupD" are set
                #                    to be the "pure" upwinded derivative,
                #                    before the upwinding algorithm has been
                #                    applied. However, when they are used
                #                    in the RHS expressions, it is assumed
                #                    that the up. algorithm has been applied.
                #                    To ensure consistency we rename all
                #                    _dupD suffixed variables as
                #                    _dupDPUREUPWIND, and use them as input
                #                    into the upwinding algorithm. The output
                #                    will be the original _dupD variable.
                varname = "UpwindAlgInput" + varname
            if "_ddnD" in varname:  # For consistency with _dupD
                varname = "UpwindAlgInput" + varname
        if outCparams.SIMD_enable == "True":
            return "const REAL_SIMD_ARRAY " + varname
        else:
            TYPE = par.parval_from_str("PRECISION")
            return "const " + TYPE + " " + varname

    def varsuffix(idx4):
        if idx4 == [0, 0, 0, 0]:
            return ""
        return "_" + ijkl_string(idx4).replace(",", "_").replace(
            "+", "p").replace("-", "m")

    def read_from_memory_Ccode_onept(gfname, idx):
        idxsplit = idx.split(',')
        idx4 = [
            int(idxsplit[0]),
            int(idxsplit[1]),
            int(idxsplit[2]),
            int(idxsplit[3])
        ]
        #gfaccess_str = gri.gfaccess("in_gfs",gfname.upper()+"GF",ijkl_string(idx4))
        gfaccess_str = gri.gfaccess("in_gfs", gfname, ijkl_string(idx4))
        if outCparams.SIMD_enable == "True":
            retstring = out__type_var(gfname) + varsuffix(
                idx4) + " = ReadSIMD(&" + gfaccess_str + ");"
        else:
            retstring = out__type_var(gfname) + varsuffix(
                idx4) + " = " + gfaccess_str + ";"
        return retstring + "\n"

    read_from_memory_Ccode = ""
    count = 0
    for gfidx in range(len(gri.glb_gridfcs_list)):
        for pt in range(len(sorted_list_of_points_read_from_memory[gfidx])):
            read_from_memory_Ccode += read_from_memory_Ccode_onept(
                read_from_memory_gf[count].name,
                sorted_list_of_points_read_from_memory[gfidx][pt])
            count += 1

    # Step 5: Output C code. C code consists of three parts
    #         a) Read gridfunctions from memory at needed pts.
    #         b) Perform arithmetic needed for input expressions
    #            provided in sympyexpr_list[].rhs and associated
    #            finite differences.
    #         c) Write output to gridfunctions specified in
    #            sympyexpr_list[].lhs.
    def indent_Ccode(Ccode):
        Ccodesplit = Ccode.splitlines()
        outstring = ""
        for i in range(len(Ccodesplit)):
            outstring += outCparams.preindent + indent + Ccodesplit[i] + '\n'
        return outstring

    # Step 5a: Read gridfunctions from memory at needed pts.
    # *** No need to do anything here; already set in
    #     string "read_from_memory_Ccode". ***

    # FIXME: Update these code comments:
    # Step 5b: Perform arithmetic needed for finite differences
    #          associated with input expressions provided in
    #          sympyexpr_list[].rhs.
    #          Note that exprs and lhsvarnames contain
    #          i)  finite difference expressions (constructed
    #              in steps above) and associated variable names,
    #              and
    #          ii) Input expressions sympyexpr_list[], which
    #              in general depend on finite difference
    #              variables.
    exprs = []
    lhsvarnames = []
    # Step 5b.i: Output finite difference expressions to
    #            Coutput string
    for i in range(len(list_of_deriv_vars)):
        exprs.append(sp.sympify(
            0))  # Append a new element to the list of derivative expressions.
        lhsvarnames.append(out__type_var(list_of_deriv_vars[i]))
        var = deriv__base_gridfunction_name[i]
        for j in range(len(fdcoeffs[i])):
            varname = str(var) + varsuffix(fdstencl[i][j])
            exprs[i] += fdcoeffs[i][j] * sp.sympify(varname)

        # Multiply each expression by the appropriate power
        #   of 1/dx[i]
        invdx = []
        for d in range(par.parval_from_str("DIM")):
            invdx.append(sp.sympify("invdx" + str(d)))
        # First-order or Kreiss-Oliger derivatives:
        if (len(deriv__operator[i]) == 5 and "dKOD" in deriv__operator[i]) or \
           (len(deriv__operator[i]) == 3 and "dD" in deriv__operator[i]) or \
           (len(deriv__operator[i]) == 5 and ("dupD" in deriv__operator[i] or "ddnD" in deriv__operator[i])):
            dirn = int(deriv__operator[i][len(deriv__operator[i]) - 1])
            exprs[i] *= invdx[dirn]
        # Second-order derivs:
        elif len(deriv__operator[i]) == 5 and "dDD" in deriv__operator[i]:
            dirn1 = int(deriv__operator[i][len(deriv__operator[i]) - 2])
            dirn2 = int(deriv__operator[i][len(deriv__operator[i]) - 1])
            exprs[i] *= invdx[dirn1] * invdx[dirn2]
        else:
            print("Error: was unable to parse derivative operator: ",
                  deriv__operator[i])
            exit(1)
    # Step 5b.ii: If upwind control vector is specified,
    #             add upwind control vectors to the
    #             derivative expression list, so its
    #             needed elements are read from memory.
    if upwindcontrolvec != "":
        for i in range(len(upwind_directions)):
            exprs.append(upwindcontrolvec[upwind_directions[i]])
            lhsvarnames.append(
                out__type_var("UpwindControlVectorU" +
                              str(upwind_directions[i])))

    # Step 5b.iii: Output useful code comment regarding
    #              which step we are on. *At most* this
    #              is a 3-step process:
    #           1. Read from memory & compute FD stencils,
    #           2. Perform upwinding, and
    #           3. Evaluate remaining expressions+write
    #              results to main memory.
    NRPy_FD_StepNumber = 1
    NRPy_FD__Number_of_Steps = 1
    if len(list_of_deriv_vars) > 0:
        NRPy_FD__Number_of_Steps += 1
    if upwindcontrolvec != "" and len(upwind_directions) > 0:
        NRPy_FD__Number_of_Steps += 1

    if len(read_from_memory_Ccode) > 0:
        Coutput += indent_Ccode(
            "/* \n * NRPy+ Finite Difference Code Generation, Step " +
            str(NRPy_FD_StepNumber) + " of " + str(NRPy_FD__Number_of_Steps) +
            ": Read from main memory and compute finite difference stencils:\n */\n"
        )
        NRPy_FD_StepNumber = NRPy_FD_StepNumber + 1
        default_CSE_varprefix = outCparams.CSE_varprefix
        # Prefix chosen CSE variables with "FD", for the finite difference coefficients:
        Coutput += indent_Ccode(
            outputC(exprs,
                    lhsvarnames,
                    "returnstring",
                    params=params + ",CSE_varprefix=" + default_CSE_varprefix +
                    "FD,includebraces=False",
                    prestring=read_from_memory_Ccode))

    # Step 5b.iv: Implement control-vector upwinding algorithm.
    if upwindcontrolvec != "":
        if len(upwind_directions) > 0:
            Coutput += indent_Ccode(
                "/* \n * NRPy+ Finite Difference Code Generation, Step " +
                str(NRPy_FD_StepNumber) + " of " +
                str(NRPy_FD__Number_of_Steps) +
                ": Implement upwinding algorithm:\n */\n")
            NRPy_FD_StepNumber = NRPy_FD_StepNumber + 1
            for dirn in upwind_directions:
                Coutput += indent_Ccode(
                    out__type_var("UpWind" + str(dirn)) +
                    " = UPWIND_ALG(UpwindControlVectorU" + str(dirn) + ");\n")
        upwindU = [sp.sympify(0) for i in range(par.parval_from_str("DIM"))]
        for dirn in upwind_directions:
            upwindU[dirn] = sp.sympify("UpWind" + str(dirn))
        for i in range(len(list_of_deriv_vars)):
            if len(deriv__operator[i]) == 5 and ("dupD" in deriv__operator[i]):
                var_dupD = sp.sympify("UpwindAlgInput" +
                                      str(list_of_deriv_vars[i]))
                var_ddnD = sp.sympify(
                    "UpwindAlgInput" +
                    str(list_of_deriv_vars[i]).replace("_dupD", "_ddnD"))
                upwind_dirn = int(deriv__operator[i][len(deriv__operator[i]) -
                                                     1])
                upwind_expr = upwindU[upwind_dirn] * (var_dupD -
                                                      var_ddnD) + var_ddnD
                # For convenience, we require out__type_var() above to
                # prefix up/downwinded variables with "UpwindAlgInput".
                # Here we do not wish to have this prefix.
                Coutput += indent_Ccode(
                    outputC(upwind_expr,
                            out__type_var(str(list_of_deriv_vars[i]),
                                          AddPrefix_for_UpDownWindVars=False),
                            "returnstring",
                            params=params + ",includebraces=False"))

    # Step 5b.v: Add input RHS & LHS expressions from
    #             sympyexpr_list[]
    Coutput += indent_Ccode(
        "/* \n * NRPy+ Finite Difference Code Generation, Step " +
        str(NRPy_FD_StepNumber) + " of " + str(NRPy_FD__Number_of_Steps) +
        ": Evaluate SymPy expressions and write to main memory:\n */\n")
    exprs = []
    lhsvarnames = []
    for i in range(len(sympyexpr_list)):
        exprs.append(sympyexpr_list[i].rhs)
        if outCparams.SIMD_enable == "True":
            lhsvarnames.append("const REAL_SIMD_ARRAY __RHS_exp_" + str(i))
        else:
            lhsvarnames.append(sympyexpr_list[i].lhs)

    # Step 5c: Write output to gridfunctions specified in
    #          sympyexpr_list[].lhs.
    write_to_mem_string = ""
    if outCparams.SIMD_enable == "True":
        for i in range(len(sympyexpr_list)):
            write_to_mem_string += "WriteSIMD(&" + sympyexpr_list[
                i].lhs + ", __RHS_exp_" + str(i) + ");\n"
    Coutput += indent_Ccode(
        outputC(exprs,
                lhsvarnames,
                "returnstring",
                params=params + ",includebraces=False,preindent=0",
                prestring="",
                poststring=write_to_mem_string))

    # Step 6: Add consistent indentation to the output end brace.
    #         See Step 0.b for corresponding start brace.
    if outCparams.includebraces == "True":
        Coutput += outCparams.preindent + "}\n"

    # Step 7: Output the C code in desired format: stdout, string, or file.
    if filename == "stdout":
        print(Coutput)
    elif filename == "returnstring":
        return Coutput + '\n'
    else:
        # Output to the file specified by outCfilename
        with open(filename, outCparams.outCfileaccess) as file:
            file.write(Coutput)
        successstr = ""
        if outCparams.outCfileaccess == "a":
            successstr = "Appended "
        elif outCparams.outCfileaccess == "w":
            successstr = "Wrote "
        print(successstr + "to file \"" + filename + "\"")
def generate_list_of_deriv_vars_from_lhrh_sympyexpr_list(
        sympyexpr_list, FDparams):
    """
    Generate from list of SymPy expressions in the form
    [lhrh(lhs=var, rhs=expr),lhrh(...),...]
    all derivative expressions.
    :param sympyexpr_list <- list of SymPy expressions in the form [lhrh(lhs=var, rhs=expr),lhrh(...),...]:
    :return list of derivative variables; creating _ddnD in case upwinding is enabled with control vector:
    >>> from outputC import lhrh
    >>> import indexedexp as ixp
    >>> import grid as gri
    >>> import NRPy_param_funcs as par
    >>> from finite_difference_helpers import generate_list_of_deriv_vars_from_lhrh_sympyexpr_list,FDparams
    >>> aDD     = ixp.register_gridfunctions_for_single_rank2("EVOL","aDD","sym01")
    >>> aDD_dDD = ixp.declarerank4("aDD_dDD","sym01_sym23")
    >>> aDD_dupD = ixp.declarerank3("aDD_dupD","sym01")
    >>> betaU   = ixp.register_gridfunctions_for_single_rank1("EVOL","betaU")
    >>> a0,a1,b,c = par.Cparameters("REAL",__name__,["a0","a1","b","c"],1)
    >>> FDparams.upwindcontrolvec=betaU
    >>> exprlist = [lhrh(lhs=a0,rhs=b*aDD[1][0] + b*aDD_dDD[2][1][2][1] + c*aDD_dDD[0][1][1][0]), \
                    lhrh(lhs=a1,rhs=aDD_dDD[1][0][0][1] + c*aDD_dupD[0][2][1]*betaU[1])]
    >>> generate_list_of_deriv_vars_from_lhrh_sympyexpr_list(exprlist,FDparams)
    [aDD_dDD0101, aDD_dDD1212, aDD_ddnD021, aDD_dupD021]
    """
    # Step 1a:
    # Create a list of free symbols in the sympy expr list
    #     that are registered neither as gridfunctions nor
    #     as C parameters. These *must* be derivatives,
    #     so we call the list "list_of_deriv_vars"
    list_of_deriv_vars_with_duplicates = []
    for expr in sympyexpr_list:
        for var in expr.rhs.free_symbols:
            vartype = gri.variable_type(var)
            if vartype == "other":
                # vartype=="other" should ONLY refer to derivatives, so
                #    if "_dD" or variants do not appear in a variable classified
                #    neither as a gridfunction nor a Cparameter, then error out.
                if ("_dD"   in str(var)) or \
                   ("_dKOD" in str(var)) or \
                   ("_dupD" in str(var)) or \
                   ("_ddnD" in str(var)):
                    list_of_deriv_vars_with_duplicates.append(var)
                else:
                    print("Error: Unregistered variable \"" + str(var) +
                          "\" in SymPy expression for " + expr.lhs)
                    print(
                        "All variables in SymPy expressions passed to FD_outputC() must be registered"
                    )
                    print(
                        "in NRPy+ as either a gridfunction or Cparameter, by calling"
                    )
                    print(
                        str(var) +
                        " = register_gridfunctions...() (in ixp/grid) if \"" +
                        str(var) + "\" is a gridfunction, or")
                    print(
                        str(var) +
                        " = Cparameters() (in par) otherwise (e.g., if it is a free parameter set at C runtime)."
                    )
                    sys.exit(1)
    list_of_deriv_vars = superfast_uniq(list_of_deriv_vars_with_duplicates)

    # Upwinding with respect to a control vector: algorithm description.
    #   To enable, set the FD_outputC()'s fourth function argument to the
    #   desired control vector. In BSSN, the betaU vector controls the upwinding.
    #   See https://arxiv.org/pdf/gr-qc/0206072.pdf for motivation and
    #   https://arxiv.org/pdf/gr-qc/0109032.pdf for implementation details,
    #   at second order. Note that the BSSN shift vector behaves like a *negative*
    #   velocity. See http://www.damtp.cam.ac.uk/user/naweb/ii/advection/advection.php
    #   for a very basic example motivating this choice.

    # Step 1b: For each variable with suffix _dupD, append to
    #          the list_of_deriv_vars the corresponding _ddnD.
    #          Both are required for control-vector upwinding. See
    #          the above print() block for further documentation
    #          on upwinding--both motivation and implementation
    #          details.
    if FDparams.upwindcontrolvec != "":
        for var in list_of_deriv_vars:
            if "_dupD" in str(var):
                list_of_deriv_vars.append(
                    sp.sympify(str(var).replace("_dupD", "_ddnD")))

    # Finally, sort the list_of_deriv_vars. This ensures
    #     consistency in the C code output, and might even be
    #     tuned to reduce cache misses.
    #     Thanks to Aaron Meurer for this nice one-liner!
    return sorted(list_of_deriv_vars, key=sp.default_sort_key)
def generate_list_of_interp_vars_from_lhrh_sympyexpr_list(
        sympyexpr_list, HIparams):
    """
    Generate from list of SymPy expressions in the form
    [lhrh(lhs=var, rhs=expr),lhrh(...),...]
    all interpolator expressions.
    :param sympyexpr_list <- list of SymPy expressions in the form [lhrh(lhs=var, rhs=expr),lhrh(...),...]:
    :return list of interpolator variables; creating _ddnD in case upwinding is enabled with control vector:
    >>> from outputC import lhrh
    >>> import indexedexp as ixp
    >>> import grid as gri
    >>> import NRPy_param_funcs as par
    >>> from hermite_interpolator_helpers import generate_list_of_interp_vars_from_lhrh_sympyexpr_list,HIparams
    >>> aDD     = ixp.register_gridfunctions_for_single_rank2("EVOL","aDD","sym01")
    >>> aDD_dDD = ixp.declarerank4("aDD_dDD","sym01_sym23")
    >>> aDD_dupD = ixp.declarerank3("aDD_dupD","sym01")
    >>> betaU   = ixp.register_gridfunctions_for_single_rank1("EVOL","betaU")
    >>> a0,a1,b,c = par.Cparameters("REAL",__name__,["a0","a1","b","c"],1)
    >>> HIparams.upwindcontrolvec=betaU
    >>> exprlist = [lhrh(lhs=a0,rhs=b*aDD[1][0] + b*aDD_dDD[2][1][2][1] + c*aDD_dDD[0][1][1][0]), \
                    lhrh(lhs=a1,rhs=aDD_dDD[1][0][0][1] + c*aDD_dupD[0][2][1]*betaU[1])]
    >>> generate_list_of_interp_vars_from_lhrh_sympyexpr_list(exprlist,HIparams)
    [aDD_dDD0101, aDD_dDD1212, aDD_ddnD021, aDD_dupD021]
    """
    # Step 1a:
    # Create a list of free symbols in the sympy expr list
    #     that are registered neither as gridfunctions nor
    #     as C parameters. These *must* be interpolators,
    #     so we call the list "list_of_interp_vars"
    list_of_interp_vars_with_duplicates = []
    for expr in sympyexpr_list:
        for var in expr.rhs.free_symbols:
            vartype = gri.variable_type(var)
            if vartype == "other":
                # vartype=="other" should ONLY refer to interpolators, so
                #    if "_dD" or variants do not appear in a variable classified
                #    neither as a gridfunction nor a Cparameter, then error out.
                if ("_dD"   in str(var)) or \
                   ("_dKOD" in str(var)) or \
                   ("_dupD" in str(var)) or \
                   ("_ddnD" in str(var)):
                    list_of_interp_vars_with_duplicates.append(var)
                else:
                    print("Error: Unregistered variable \"" + str(var) +
                          "\" in SymPy expression for " + expr.lhs)
                    print(
                        "All variables in SymPy expressions passed to HI_outputC() must be registered"
                    )
                    print(
                        "in NRPy+ as either a gridfunction or Cparameter, by calling"
                    )
                    print(
                        str(var) +
                        " = register_gridfunctions...() (in ixp/grid) if \"" +
                        str(var) + "\" is a gridfunction, or")
                    print(
                        str(var) +
                        " = Cparameters() (in par) otherwise (e.g., if it is a free parameter set at C runtime)."
                    )
                    sys.exit(1)
    list_of_interp_vars = superfast_uniq(list_of_interp_vars_with_duplicates)

    # Step 1b: For each variable with suffix _dupD, append to
    #          the list_of_interp_vars the corresponding _ddnD.
    if HIparams.upwindcontrolvec != "":
        for var in list_of_interp_vars:
            if "_dupD" in str(var):
                list_of_interp_vars.append(
                    sp.sympify(str(var).replace("_dupD", "_ddnD")))

    # Finally, sort the list_of_interp_vars. This ensures
    #     consistency in the C code output, and might even be
    #     tuned to reduce cache misses.
    #     Thanks to Aaron Meurer for this nice one-liner!
    return sorted(list_of_interp_vars, key=sp.default_sort_key)