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.
    #     This enables the rest of the routine to assume
    #     sympyexpr_list is indeed a list.
    if not isinstance(sympyexpr_list, 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 5 for open and close braces
    if outCparams.includebraces == "True":
        indent = "  "
    else:
        indent = ""

    # Step 0.c: FDparams named tuple stores parameters used in the finite-difference codegen
    FDparams.enable_SIMD         = outCparams.enable_SIMD
    FDparams.PRECISION           = par.parval_from_str("PRECISION")
    FDparams.FD_CD_order         = par.parval_from_str("FD_CENTDERIVS_ORDER")
    FDparams.enable_FD_functions = par.parval_from_str("enable_FD_functions")
    FDparams.DIM                 = par.parval_from_str("DIM")
    FDparams.MemAllocStyle       = par.parval_from_str("MemAllocStyle")
    FDparams.upwindcontrolvec    = upwindcontrolvec
    FDparams.fullindent          = indent + outCparams.preindent
    FDparams.outCparams          = params

    # Step 1: Generate from list of SymPy expressions in the form
    #     [lhrh(lhs=var, rhs=expr),lhrh(...),...]
    #     all derivative expressions, which we will process next.
    list_of_deriv_vars = generate_list_of_deriv_vars_from_lhrh_sympyexpr_list(sympyexpr_list, FDparams)

    # Step 2a: Extract from list_of_deriv_vars a list of base gridfunctions
    #         and a list of derivative operators. Usually takes list of SymPy
    #         symbols as input, but could just take strings, as this function
    #         does only string manipulations.
    # Example:
    # >>> extract_from_list_of_deriv_vars__base_gfs_and_deriv_ops_lists(["aDD_dD012","aDD_dKOD012","vetU_dKOD21","hDD_dDD0112"])
    # (['aDD01', 'aDD01', 'vetU2', 'hDD01'], ['dD2', 'dKOD2', 'dKOD1', 'dDD12'])
    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)

    # Step 2b:
    # Next, check each base gridfunction to determine whether
    #     it is indeed registered as a gridfunction.
    #     If not, exit with error.
    for basegf in list_of_base_gridfunction_names_in_derivs:
        is_gf = False
        for gf in gri.glb_gridfcs_list:
            if basegf == str(gf.name):
                is_gf = True
        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!")
            sys.exit(1)

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

    # Step 3:
    # Evaluate the finite difference stencil for each
    #     derivative operator, 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(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])

    # Step 4: Create C code to read gridfunctions from memory
    read_from_memory_Ccode = read_gfs_from_memory(list_of_base_gridfunction_names_in_derivs, fdstencl, sympyexpr_list,
                                                  FDparams)

    # Step 5: construct C code.
    Coutput = ""
    if outCparams.includebraces == "True":
        Coutput = outCparams.preindent + "{\n"
    Coutput = construct_Ccode(sympyexpr_list, list_of_deriv_vars,
                           list_of_base_gridfunction_names_in_derivs, list_of_deriv_operators,
                           fdcoeffs, fdstencl, read_from_memory_Ccode, FDparams, Coutput)
    if outCparams.includebraces == "True":
        Coutput += outCparams.preindent+"}"

    # Step 6: Output the C code in desired format: stdout, string, or file.
    if filename == "stdout":
        print(Coutput)
    elif filename == "returnstring":
        return Coutput
    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 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 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_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.")
            sys.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
        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!")
            sys.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.")
            sys.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")
                    sys.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 ) ) ))
        if 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 ) ) ))
        print("Error: MemAllocStyle = "+par.parval_from_str("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))
                # 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
        return "const "+ par.parval_from_str("PRECISION") + " " + 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])]
        gf_array_name = "in_gfs" # Default array name.
        gfaccess_str = gri.gfaccess(gf_array_name,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])
            sys.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(read_from_memory_Ccode) > 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
        # Prefix chosen CSE variables with "FD", for the finite difference coefficients:
        Coutput += indent_Ccode(outputC(exprs,lhsvarnames,"returnstring",params=params + ",CSE_varprefix=FDPart1,includebraces=False,CSE_preprocess=True,SIMD_find_more_subs=True",
                                        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
            if outCparams.SIMD_enable == "True":
                Coutput += """
const double tmp_upwind_Integer_1 = 1.000000000000000000000000000000000;
const REAL_SIMD_ARRAY upwind_Integer_1 = ConstSIMD(tmp_upwind_Integer_1);
const double tmp_upwind_Integer_0 = 0.000000000000000000000000000000000;
const REAL_SIMD_ARRAY upwind_Integer_0 = ConstSIMD(tmp_upwind_Integer_0);
"""
            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))
        upwind_expr_list, var_list = [], []
        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
                upwind_expr_list.append(upwind_expr)
                var_list.append(out__type_var(str(list_of_deriv_vars[i]),AddPrefix_for_UpDownWindVars=False))
        # 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_list,var_list,
                                                "returnstring",params=params + ",CSE_varprefix=FDPart2,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+",CSE_varprefix=FDPart3,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 + "\"")