def 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):
    """
    C code is constructed in *up to* 3 parts:
     5.a) Read gridfunctions from memory at needed pts
          for finite differencing; compute finite-differencing
          stencils.
     5.b) Implement upwinding algorithm (if relevant)
     5.c) Evaluate SymPy expressions and write to main
          memory
    """

    # Failed Doctest. However, mathematically equivalent with Sympy 1.3
    # :param sympyexpr_list:
    # :param list_of_deriv_vars:
    # :param list_of_base_gridfunction_names_in_derivs:
    # :param list_of_deriv_operators:
    # :param fdcoeffs:
    # :param fdstencl:
    # :param read_from_memory_Ccode:
    # :param FDparams:
    # :param Coutput: The start of the Coutput string; this function's output will be pasted to a copy of Coutput
    # :return: Returns a C code string
    # >>> 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, construct_Ccode
    # >>> 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.FD_functions_enable=False
    # >>> FDparams.PRECISION="double"
    # >>> FDparams.MemAllocStyle="012"
    # >>> FDparams.upwindcontrolvec=vU
    # >>> FDparams.fullindent=""
    # >>> FDparams.outCparams="outCverbose=False"
    # >>> 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])
    # >>> memread_Ccode = read_gfs_from_memory(list_of_base_gridfunction_names_in_derivs, fdstencl, exprlist, FDparams)
    # >>> print(construct_Ccode(exprlist, list_of_deriv_vars, \
    #           list_of_base_gridfunction_names_in_derivs, list_of_deriv_operators, \
    #           fdcoeffs, fdstencl, memread_Ccode, FDparams, ""))
    # /*
    #  * NRPy+ Finite Difference Code Generation, Step 1 of 3: Read from main memory and compute finite difference stencils:
    #  */
    # 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)];
    # const double FDPart1_Rational_1_2 = 1.0/2.0;
    # const double FDPart1_Integer_2 = 2.0;
    # const double FDPart1_Rational_3_2 = 3.0/2.0;
    # const double hDD_dD011 = FDPart1_Rational_1_2*invdx1*(-hDD01_i0_i1m1_i2 + hDD01_i0_i1p1_i2);
    # const double UpwindAlgInputhDD_ddnD021 = invdx1*(-FDPart1_Integer_2*hDD02_i0_i1m1_i2 + FDPart1_Rational_1_2*hDD02_i0_i1m2_i2 + FDPart1_Rational_3_2*hDD02);
    # const double UpwindAlgInputhDD_dupD021 = invdx1*(FDPart1_Integer_2*hDD02_i0_i1p1_i2 - FDPart1_Rational_1_2*hDD02_i0_i1p2_i2 - FDPart1_Rational_3_2*hDD02);
    # const double UpwindControlVectorU1 = vU1;
    # /*
    #  * NRPy+ Finite Difference Code Generation, Step 2 of 3: Implement upwinding algorithm:
    #  */
    # const double UpWind1 = UPWIND_ALG(UpwindControlVectorU1);
    # const double hDD_dupD021 = UpWind1*(-UpwindAlgInputhDD_ddnD021 + UpwindAlgInputhDD_dupD021) + UpwindAlgInputhDD_ddnD021;
    # /*
    #  * NRPy+ Finite Difference Code Generation, Step 3 of 3: Evaluate SymPy expressions and write to main memory:
    #  */
    # a0 = b*hDD01 + c*hDD_dD011;
    # a1 = c*hDD_dupD021*vU1;
    # <BLANKLINE>

    def indent_Ccode(Ccode):
        Ccodesplit = Ccode.splitlines()
        outstring = ""
        for i in range(len(Ccodesplit)):
            outstring += FDparams.fullindent + Ccodesplit[i] + '\n'
        return outstring

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

    # Step 5.a.ii: Perform arithmetic needed for finite differences
    #              associated with input expressions provided in
    #              sympyexpr_list[].rhs.
    #           Note that FDexprs and FDlhsvarnames contain
    #          A) Finite difference expressions (constructed
    #             in steps above) and associated variable names,
    #             and
    #          B) Input expressions sympyexpr_list[], which
    #             in general depend on finite difference
    #             variables.
    FDexprs = []
    FDlhsvarnames = []
    if not FDparams.FD_functions_enable:
        FDexprs, FDlhsvarnames = \
            construct_FD_exprs_as_SymPy_exprs(list_of_deriv_vars,
                                              list_of_base_gridfunction_names_in_derivs, list_of_deriv_operators,
                                              fdcoeffs, fdstencl)

    # Compute finite differences using function calls (instead of inlined calculations)?
    if FDparams.FD_functions_enable:
        # If so, add FD functions to outputC's outC_function_dict (C function dictionary),
        #   AND return the full set of needed calls to these functions (to funccall_list)
        funccall_list = \
            add_FD_func_to_outC_function_dict(list_of_deriv_vars,
                                              list_of_base_gridfunction_names_in_derivs, list_of_deriv_operators,
                                              fdcoeffs, fdstencl)

    # Step 5.b.i: (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.
    upwind_directions = []
    if FDparams.upwindcontrolvec != "":
        upwind_directions_unsorted_withdups = []
        for deriv_op in list_of_deriv_operators:
            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)
        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)
        #   If upwind control vector is specified,
        #        add upwind control vectors to the
        #        derivative expression list, so its
        #        needed elements are read from memory.
        for dirn in upwind_directions:
            FDexprs.append(FDparams.upwindcontrolvec[dirn])
            FDlhsvarnames.append(
                type__var("UpwindControlVectorU" + str(dirn), FDparams))

    # Step 5.x: 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 FDparams.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
        if FDparams.FD_functions_enable:
            # Compute finite differences using function calls (instead of inlined calculations)
            Coutput += indent_Ccode(read_from_memory_Ccode)
            for funccall in funccall_list:
                Coutput += indent_Ccode(funccall)
            if FDparams.upwindcontrolvec != "":
                # Compute finite differences using inlined calculations
                params = FDparams.outCparams
                # We choose the CSE temporary variable prefix "FDpart1" for the finite difference coefficients:
                params += ",CSE_varprefix=FDPart1,includebraces=False,CSE_preprocess=True,SIMD_find_more_subs=True"
                Coutput += indent_Ccode(
                    outputC(FDexprs,
                            FDlhsvarnames,
                            "returnstring",
                            params=params))

        else:
            # Compute finite differences using inlined calculations
            params = FDparams.outCparams.replace(
                "preindent=1",
                "preindent=0")  # Remove an unnecessary indentation
            # We choose the CSE temporary variable prefix "FDpart1" for the finite difference coefficients:
            params += ",CSE_varprefix=FDPart1,includebraces=False,CSE_preprocess=True,SIMD_find_more_subs=True"
            Coutput += indent_Ccode(
                outputC(FDexprs,
                        FDlhsvarnames,
                        "returnstring",
                        params=params,
                        prestring=read_from_memory_Ccode))

    # Step 5.b.ii: Implement control-vector upwinding algorithm.
    if FDparams.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 FDparams.SIMD_enable == "True":
                for n in ["0", "1"]:
                    Coutput += indent_Ccode(
                        "const double tmp_upwind_Integer_" + n + " = " + n +
                        ".000000000000000000000000000000000;\n")
                    Coutput += indent_Ccode(
                        "const REAL_SIMD_ARRAY upwind_Integer_" + n +
                        " = ConstSIMD(tmp_upwind_Integer_" + n + ");\n")
            for dirn in upwind_directions:
                Coutput += indent_Ccode(
                    type__var("UpWind" + str(dirn), FDparams) +
                    " = UPWIND_ALG(UpwindControlVectorU" + str(dirn) + ");\n")
        upwindU = [sp.sympify(0) for i in range(FDparams.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(list_of_deriv_operators[i]) == 5 and (
                    "dupD" in list_of_deriv_operators[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(
                    list_of_deriv_operators[i][len(list_of_deriv_operators[i])
                                               - 1])
                upwind_expr = upwindU[upwind_dirn] * (var_dupD -
                                                      var_ddnD) + var_ddnD
                upwind_expr_list.append(upwind_expr)
                var_list.append(
                    type__var(str(list_of_deriv_vars[i]),
                              FDparams,
                              AddPrefix_for_UpDownWindVars=False))
        # For convenience, we require 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=FDparams.outCparams +
                    ",CSE_varprefix=FDPart2,includebraces=False"))

    # Step 5.c.i: 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 FDparams.SIMD_enable == "True":
            lhsvarnames.append("const REAL_SIMD_ARRAY __RHS_exp_" + str(i))
        else:
            lhsvarnames.append(sympyexpr_list[i].lhs)

    # Step 5.c.ii: Write output to gridfunctions specified in
    #              sympyexpr_list[].lhs.
    write_to_mem_string = ""
    if FDparams.SIMD_enable == "True":
        for i in range(len(sympyexpr_list)):
            write_to_mem_string += "WriteSIMD(&" + sympyexpr_list[
                i].lhs + ", __RHS_exp_" + str(i) + ");\n"

    # outputC requires as its second argument a list of strings.
    #   Sometimes when the lhs's are simple constants, but the inputs
    #   contain gridfunctions, it is necessary to convert the lhs's
    #   to strings:
    lhsvarnamestrings = []
    for lhs in lhsvarnames:
        lhsvarnamestrings.append(str(lhs))

    Coutput += indent_Ccode(
        outputC(exprs,
                lhsvarnamestrings,
                "returnstring",
                params=FDparams.outCparams +
                ",CSE_varprefix=FDPart3,includebraces=False,preindent=0",
                prestring="",
                poststring=write_to_mem_string))

    return Coutput
def symbolic_parital_derivative():
    # Step 2.a: Read in expressions as a (single) string
    with open(os.path.join(inputdir, 'Hamstring.txt'), 'r') as file:
        expressions_as_lines = file.readlines()

    # Step 2.b: Create and populate the "lr" array, which separates each line into left- and right-hand sides
    #   Each entry is a string of the form lhrh(lhs='',rhs='')
    lr = []

    for i in range(len(expressions_as_lines)):
        # Ignore lines with 2 or fewer characters and those starting with #
        if len(expressions_as_lines[i]
               ) > 2 and expressions_as_lines[i][0] != "#":
            # Split each line by its equals sign
            split_line = expressions_as_lines[i].split("=")
            # Append the line to "lr", removing spaces, "sp." prefixes, and replacing Lambda->Lamb
            #   (Lambda is a protected keyword):
            lr.append(
                lhrh(lhs=split_line[0].replace(" ",
                                               "").replace("Lambda", "Lamb"),
                     rhs=split_line[1].replace(" ",
                                               "").replace("sp.", "").replace(
                                                   "Lambda", "Lamb")))

    # Step 2.c: Separate and sympify right- and left-hand sides into separate arrays
    lhss = []
    rhss = []
    for i in range(len(lr)):
        lhss.append(custom_parse_expr(lr[i].lhs))
        rhss.append(custom_parse_expr(lr[i].rhs))

    # Step 3.a: Create `input_constants` array and populate with SymPy symbols
    m1, m2, tortoise, eta, KK, k0, k1, EMgamma, d1v2, dheffSSv2 = sp.symbols(
        'm1 m2 tortoise eta KK k0 k1 EMgamma d1v2 dheffSSv2', real=True)
    input_constants = [
        m1, m2, tortoise, eta, KK, k0, k1, EMgamma, d1v2, dheffSSv2
    ]

    # Step 3.b: Create `dynamic_variables` array and populate with SymPy symbols
    x, y, z, px, py, pz, s1x, s1y, s1z, s2x, s2y, s2z = sp.symbols(
        'x y z px py pz s1x s1y s1z s2x s2y s2z', real=True)
    dynamic_variables = [x, y, z, px, py, pz, s1x, s1y, s1z, s2x, s2y, s2z]

    # Step 4.a: Prepare array of "free symbols" in the right-hand side expressions
    full_symbol_list_with_dups = []
    for i in range(len(lr)):
        for variable in rhss[i].free_symbols:
            full_symbol_list_with_dups.append(variable)

    # Step 4.b: Remove duplicate free symbols
    full_symbol_list = superfast_uniq(full_symbol_list_with_dups)

    # Step 4.c: Remove input constants from symbol list
    for inputconst in input_constants:
        for symbol in full_symbol_list:
            if str(symbol) == str(inputconst):
                full_symbol_list.remove(symbol)

    # Step 5.a: Convert each left-hand side to function notation
    #   while separating and simplifying left- and right-hand sides
    xx = sp.Symbol('xx')
    func = []
    for i in range(len(lr)):
        func.append(sp.sympify(sp.Function(lr[i].lhs)(xx)))

    # Step 5.b: Mark each free variable as a function with argument xx
    full_function_list = []
    for symb in full_symbol_list:
        func = sp.sympify(sp.Function(str(symb))(xx))
        full_function_list.append(func)
        for i in range(len(rhss)):
            for var in rhss[i].free_symbols:
                if str(var) == str(symb):
                    rhss[i] = rhss[i].subs(var, func)

    # Step 6.a: Use SymPy's diff function to differentiate right-hand sides with respect to xx
    #   and append "prm" notation to left-hand sides
    lhss_deriv = []
    rhss_deriv = []
    for i in range(len(rhss)):
        lhss_deriv.append(custom_parse_expr(str(lhss[i]) + "prm"))
        newrhs = custom_parse_expr(
            str(sp.diff(rhss[i], xx)).replace("(xx)", "").replace(
                ", xx", "prm").replace("Derivative", ""))
        rhss_deriv.append(newrhs)

    # Step 7.b: Call the simplication function and then copy results
    lhss_deriv_simp, rhss_deriv_simp = simplify_deriv(lhss_deriv, rhss_deriv)
    lhss_deriv = lhss_deriv_simp
    rhss_deriv = rhss_deriv_simp

    # Step 8.b: Call the derivative function and populate dictionaries with the result
    lhss_derivative = {}
    rhss_derivative = {}
    for index in range(len(dynamic_variables)):
        lhss_temp, rhss_temp = deriv_onevar(lhss_deriv, rhss_deriv,
                                            dynamic_variables, index)
        lhss_derivative[dynamic_variables[index]] = lhss_temp
        rhss_derivative[dynamic_variables[index]] = rhss_temp

    # Step 9: Output original expression and each partial derivative expression in SymPy snytax
    with open("partial_derivatives.txt", "w") as output:
        for i in range(len(lr)):
            right_side = lr[i].rhs
            right_side_in_sp = right_side.replace("sqrt(", "sp.sqrt(").replace(
                "log(", "sp.log(").replace("pi", "sp.pi").replace(
                    "sign(", "sp.sign(").replace("Abs(", "sp.Abs(").replace(
                        "Rational(", "sp.Rational(")
            output.write(str(lr[i].lhs) + " = " + right_side_in_sp)
        for var in dynamic_variables:
            for i in range(len(lhss_derivative[var])):
                right_side = str(rhss_derivative[var][i])
                right_side_in_sp = right_side.replace(
                    "sqrt(", "sp.sqrt(").replace("log(", "sp.log(").replace(
                        "pi", "sp.pi").replace("sign(", "sp.sign(").replace(
                            "Abs(", "sp.Abs(").replace("Rational(",
                                                       "sp.Rational(").replace(
                                                           "prm",
                                                           "prm_" + str(var))
                output.write(
                    str(lhss_derivative[var][i]).replace(
                        "prm", "prm_" + str(var)) + " = " + right_side_in_sp +
                    "\n")
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
def add_FD_func_to_outC_function_dict(
        list_of_deriv_vars, list_of_base_gridfunction_names_in_derivs,
        list_of_deriv_operators, fdcoeffs, fdstencl):
    # Step 5.a.ii.A: First construct a list of all the unique finite difference functions
    list_of_uniq_deriv_operators = superfast_uniq(list_of_deriv_operators)
    Ctype = "REAL"
    if par.parval_from_str("grid::GridFuncMemAccess") == "ETK":
        Ctype = "CCTK_REAL"
    func_prefix = "order_" + str(FDparams.FD_CD_order) + "_"
    if FDparams.SIMD_enable == "True":
        Ctype = "REAL_SIMD_ARRAY"
        func_prefix = "SIMD_" + func_prefix

    # Stores the needed calls to the functions we're adding to outC_function_dict:
    FDfunccall_list = []
    for op in list_of_uniq_deriv_operators:
        which_op_idx = find_which_op_idx(op, list_of_deriv_operators)

        rhs_expr = sp.sympify(0)
        for j in range(len(fdcoeffs[which_op_idx])):
            var = sp.sympify("f" +
                             varsuffix(fdstencl[which_op_idx][j], FDparams))
            rhs_expr += fdcoeffs[which_op_idx][j] * var

        # Multiply each expression by the appropriate power
        #   of 1/dx[i]
        invdx = []
        used_invdx = [False, False, False, False]
        for d in range(FDparams.DIM):
            invdx.append(sp.sympify("invdx" + str(d)))
        # First-order or Kreiss-Oliger derivatives:
        if ((len(op) == 5 and "dKOD" in op) or (len(op) == 3 and "dD" in op)
                or (len(op) == 5 and ("dupD" in op or "ddnD" in op))):
            dirn = int(op[len(op) - 1])
            rhs_expr *= invdx[dirn]
            used_invdx[dirn] = True
        # Second-order derivs:
        elif len(op) == 5 and "dDD" in op:
            dirn1 = int(op[len(op) - 2])
            dirn2 = int(op[len(op) - 1])
            used_invdx[dirn1] = used_invdx[dirn2] = True
            rhs_expr *= invdx[dirn1] * invdx[dirn2]
        else:
            print("Error: was unable to parse derivative operator: ", op)
            sys.exit(1)

        outfunc_params = ""
        for d in range(FDparams.DIM):
            if used_invdx[d]:
                outfunc_params += "const " + Ctype + " invdx" + str(d) + ","

        for j in range(len(fdcoeffs[which_op_idx])):
            var = sp.sympify("f" +
                             varsuffix(fdstencl[which_op_idx][j], FDparams))
            outfunc_params += "const " + Ctype + " " + str(var)
            if j != len(fdcoeffs[which_op_idx]) - 1:
                outfunc_params += ","

        for i in range(len(list_of_deriv_operators)):
            # print("comparing ",list_of_deriv_operators[i],op)
            if list_of_deriv_operators[i] == op:
                funccall = type__var(
                    list_of_deriv_vars[i],
                    FDparams) + " = " + func_prefix + "f_" + str(op) + "("
                for d in range(FDparams.DIM):
                    if used_invdx[d]:
                        funccall += "invdx" + str(d) + ","
                gfname = list_of_base_gridfunction_names_in_derivs[i]
                for j in range(len(fdcoeffs[which_op_idx])):
                    funccall += gfname + varsuffix(fdstencl[which_op_idx][j],
                                                   FDparams)
                    if j != len(fdcoeffs[which_op_idx]) - 1:
                        funccall += ","
                funccall += ");"
                FDfunccall_list.append(funccall)

        # If the function already exists in the outC_function_dict, then do not add it; move to the next op.
        if func_prefix + "f_" + str(op) not in outC_function_dict:
            p = "preindent=1,SIMD_enable=" + FDparams.SIMD_enable + ",outCverbose=False,CSE_preprocess=True,includebraces=False"
            outFDstr = outputC(rhs_expr, "retval", "returnstring", params=p)
            outFDstr = outFDstr.replace("retval = ", "return ")
            add_to_Cfunction_dict(
                desc=" * (__FD_OPERATOR_FUNC__) Finite difference operator for "
                + str(op).replace("dDD", "second derivative: ").replace(
                    "dD", "first derivative: ").replace(
                        "dKOD", "Kreiss-Oliger derivative: ").replace(
                            "dupD", "upwinded derivative: ").replace(
                                "ddnD", "downwinded derivative: "),
                type="static " + Ctype + " _NOINLINE _UNUSED",
                name=func_prefix + "f_" + str(op),
                opts="DisableCparameters",
                params=outfunc_params,
                preloop="",
                body=outFDstr)
    return FDfunccall_list
Example #5
0
def output_H_and_derivs():
    # Open and read the file of numerical expressions (written in SymPy syntax) computing the SEOBNRv3 Hamiltonian.
    f = open("SEOBNR/Hamstring.txt", 'r')
    Hamstring = str(f.read())
    f.close()

    # Split Hamstring by carriage returns.
    Hamterms = Hamstring.splitlines()

    # Create 'lr' array to store each left-hand side and right-hand side of Hamstring as strings.
    lr = []
    # Loop over each line in Hamstring to separate the left- and right-hand sides.
    for i in range(len(Hamterms)):
        # Ignore lines with 2 or fewer characters and those starting with #
        if len(Hamterms[i]) > 2 and Hamterms[i][0] != "#":
            # Split each line by its equals sign.
            splitHamterms = Hamterms[i].split("=")
            # Append terms to the 'lr' array, removing spaces, "sp." prefixes, and replacing Lambda->Lamb (Lambda is a
            # protected keyword)
            lr.append(lhrh(lhs=splitHamterms[0].replace(" ", "").replace("Lambda", "Lamb"),
                           rhs=splitHamterms[1].replace(" ", "").replace("sp.", "").replace("Lambda", "Lamb")))
    # Declare the symbol 'xx', which we use to denote each left-hand side as a function
    xx = sp.Symbol('xx')
    # Create arrays to store simplified left- and right-hand expressions, as well as left-hand sides designated as
    # functions.
    func = []
    lhss = []
    rhss = []
    # Affix '(xx)' to each left-hand side as a function designation; separate and simplify left- and right-hand sides
    # of the numerical expressions.
    for i in range(len(lr)):
        func.append(sp.sympify(sp.Function(lr[i].lhs)(xx)))
        lhss.append(sp.sympify(lr[i].lhs))
        rhss.append(sp.sympify(lr[i].rhs))
    # Creat array for and generate a list of all the "free symbols" in the right-hand side expressions.
    full_symbol_list_with_dups = []
    for i in range(len(lr)):
        for var in rhss[i].free_symbols:
            full_symbol_list_with_dups.append(var)

    # Remove all duplicated "free symbols" from the right-hand side expressions.
    full_symbol_list = superfast_uniq(full_symbol_list_with_dups)

    # Declare input constants.
    m1, m2, eta, KK, k0, k1, d1v2, dheffSSv2 = sp.symbols("m1 m2 eta KK k0 k1 d1v2 dheffSSv2", real=True)
    tortoise, EMgamma = sp.symbols("tortoise EMgamma", real=True)
    input_constants = [m1, m2, eta, KK, k0, k1, d1v2, dheffSSv2, tortoise, EMgamma]

    # Derivatives of input constants will always be zero, so remove them from the full_symbol_list.
    for inputconst in input_constants:
        for symbol in full_symbol_list:
            if str(symbol) == str(inputconst):
                full_symbol_list.remove(symbol)

    # Add symbols to the function list and replace right-hand side terms with their function equivalent.
    full_function_list = []
    for symb in full_symbol_list:
        func = sp.sympify(sp.Function(str(symb))(xx))
        full_function_list.append(func)
        for i in range(len(rhss)):
            for var in rhss[i].free_symbols:
                if str(var) == str(symb):
                    rhss[i] = rhss[i].subs(var, func)

    # Create left- and right-hand side 'deriv' arrays
    lhss_deriv = []
    rhss_deriv = []
    # Differentiate with respect to xx, remove '(xx)', and replace xx with 'prm' notation.
    for i in range(len(rhss)):
        lhss_deriv.append(sp.sympify(str(lhss[i]) + "prm"))
        newrhs = sp.sympify(
            str(sp.diff(rhss[i], xx)).replace("(xx)", "").replace(", xx", "prm").replace("Derivative", ""))
        rhss_deriv.append(newrhs)
    # Simplify derivative expressions with simplify_deriv()
    lhss_deriv_simp, rhss_deriv_simp = simplify_deriv(lhss_deriv, rhss_deriv)
    lhss_deriv = lhss_deriv_simp
    rhss_deriv = rhss_deriv_simp
    # Generate partial derivatives with respect to each of the twelve input variables
    lhss_deriv_x, rhss_deriv_x = deriv_onevar(lhss_deriv, rhss_deriv, xprm=1, yprm=0, zprm=0, pxprm=0, pyprm=0, pzprm=0,
                                              s1xprm=0, s1yprm=0, s1zprm=0, s2xprm=0, s2yprm=0, s2zprm=0)
    lhss_deriv_y, rhss_deriv_y = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=1, zprm=0, pxprm=0, pyprm=0, pzprm=0,
                                              s1xprm=0, s1yprm=0, s1zprm=0, s2xprm=0, s2yprm=0, s2zprm=0)
    lhss_deriv_z, rhss_deriv_z = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=0, zprm=1, pxprm=0, pyprm=0, pzprm=0,
                                              s1xprm=0, s1yprm=0, s1zprm=0, s2xprm=0, s2yprm=0, s2zprm=0)
    lhss_deriv_px, rhss_deriv_px = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=0, zprm=0, pxprm=1, pyprm=0,
                                                pzprm=0, s1xprm=0, s1yprm=0, s1zprm=0, s2xprm=0, s2yprm=0, s2zprm=0)
    lhss_deriv_py, rhss_deriv_py = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=0, zprm=0, pxprm=0, pyprm=1,
                                                pzprm=0, s1xprm=0, s1yprm=0, s1zprm=0, s2xprm=0, s2yprm=0, s2zprm=0)
    lhss_deriv_pz, rhss_deriv_pz = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=0, zprm=0, pxprm=0, pyprm=0,
                                                pzprm=1, s1xprm=0, s1yprm=0, s1zprm=0, s2xprm=0, s2yprm=0, s2zprm=0)
    lhss_deriv_s1x, rhss_deriv_s1x = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=0, zprm=0, pxprm=0, pyprm=0,
                                                  pzprm=0, s1xprm=1, s1yprm=0, s1zprm=0, s2xprm=0, s2yprm=0, s2zprm=0)
    lhss_deriv_s1y, rhss_deriv_s1y = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=0, zprm=0, pxprm=0, pyprm=0,
                                                  pzprm=0, s1xprm=0, s1yprm=1, s1zprm=0, s2xprm=0, s2yprm=0, s2zprm=0)
    lhss_deriv_s1z, rhss_deriv_s1z = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=0, zprm=0, pxprm=0, pyprm=0,
                                                  pzprm=0, s1xprm=0, s1yprm=0, s1zprm=1, s2xprm=0, s2yprm=0, s2zprm=0)
    lhss_deriv_s2x, rhss_deriv_s2x = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=0, zprm=0, pxprm=0, pyprm=0,
                                                  pzprm=0, s1xprm=0, s1yprm=0, s1zprm=0, s2xprm=1, s2yprm=0, s2zprm=0)
    lhss_deriv_s2y, rhss_deriv_s2y = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=0, zprm=0, pxprm=0, pyprm=0,
                                                  pzprm=0, s1xprm=0, s1yprm=0, s1zprm=0, s2xprm=0, s2yprm=1, s2zprm=0)
    lhss_deriv_s2z, rhss_deriv_s2z = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=0, zprm=0, pxprm=0, pyprm=0,
                                                  pzprm=0, s1xprm=0, s1yprm=0, s1zprm=0, s2xprm=0, s2yprm=0, s2zprm=1)
    # Prepare to output derivative expressions in C syntax
    outstring = "/* SEOBNR Hamiltonian expression: */\n"
    outstringsp = ""
    outsplhs = []
    outsprhs = []
    for i in range(len(lr)):
        outstring += outputC(sp.sympify(lr[i].rhs), lr[i].lhs, "returnstring",
                             "outCverbose=False,includebraces=False,CSE_enable=False")
        outstringsp += lr[i].lhs + " = " + lr[i].rhs + "\n"
        outsplhs.append(sp.sympify(lr[i].lhs))
        outsprhs.append(sp.sympify(lr[i].rhs))
    outstring += "\n\n\n/* SEOBNR \partial_x H expression: */\n"
    for i in range(len(lhss_deriv_x)):
        outstring += outputC(rhss_deriv_x[i], str(lhss_deriv_x[i]), "returnstring",
                             "outCverbose=False,includebraces=False,CSE_enable=False")
        outstringsp += str(lhss_deriv_x[i]) + " = " + str(rhss_deriv_x[i]) + "\n"
        outsplhs.append(lhss_deriv_x[i])
        outsprhs.append(rhss_deriv_x[i])

    with open("SEOBNR_Playground_Pycodes/numpy_expressions.py", "w") as file:
        file.write("""from __future__ import division
import numpy as np
def compute_dHdq(m1, m2, eta, x, y, z, px, py, pz, s1x, s1y, s1z, s2x, s2y, s2z, KK, k0, k1, d1v2, dheffSSv2, tortoise, EMgamma):
""")
        for i in range(len(lr) - 1):
            file.write("    " + lr[i].lhs + " = " + str(lr[i].rhs).replace("Rational(",
                                                                           "np.true_divide(").replace("sqrt(",
                                                                                                      "np.sqrt(").replace(
                "log(", "np.log(").replace("sign(", "np.sign(").replace("Abs(", "np.abs(").replace("pi",
                                                                                                   "np.pi") + "\n")
        file.write("""    return Hreal""")
    # Playground agrees to this point... now need to figure out how to do CSE and output to different files!TylerK
    # Open and write to a file that, when called, will perform CSE on Hreal and all derivative expressions.
    with open("SEOBNR_Playground_Pycodes/sympy_expression.py", "w") as file:
        file.write("""import sympy as sp
from outputC import *
def sympy_cse():
    m1,m2,x,y,z,px,py,pz,s1x,s1y,s1z,s2x,s2y,s2z = sp.symbols("m1 m2 x y z px py pz s1x s1y s1z s2x s2y s2z",real=True)
    eta,KK,k0,k1,d1v2,dheffSSv2 = sp.symbols("eta KK k0 k1 d1v2 dheffSSv2",real=True)
    tortoise,EMgamma = sp.symbols("tortoise EMgamma",real=True)
""")
        # Convert Numpy functions in expressions to SymPy expressions.
        for i in range(len(lr)):
            file.write("    " + lr[i].lhs + " = " + str(lr[i].rhs).replace("Abs(", "sp.Abs(").replace("sqrt(",
                        "sp.sqrt(").replace("log(","sp.log(").replace("sign(", "sp.sign(").replace("Rational(",
                        "sp.Rational(").replace("pi", "sp.pi") + "\n")
        # Call the CSE routine for Hreal
        file.write("""
    CSE_results = sp.cse(Hreal, sp.numbered_symbols("Htmp"), order='canonical')
    with open("SEOBNR_Playground_Pycodes/numpy_expressions.py", "a") as file:
        for commonsubexpression in CSE_results[0]:
            file.write("    "+str(commonsubexpression[0])+" = "+str(commonsubexpression[1]).replace("sqrt(","np.sqrt(").replace("log(","np.log(").replace("sign(","np.sign(").replace("Abs(", "np.abs(").replace("pi", "np.pi")+"\\n")
        for i,result in enumerate(CSE_results[1]):
            file.write("    Hreal = "+str(result).replace("sqrt(","np.sqrt(")+"\\n")
""")
        # Declare all Hamiltonian terms as symbols so they can be used in derivative computations.
        for i in range(len(lr)):
            file.write("    " + lr[i].lhs + " = " + "sp.symbols(\"" + lr[i].lhs + "\")\n")

        # Print all terms for each of the partial derivatives.
        #TylerK: make this into a loop so there's not a buch of repeated code
        for i in range(len(lhss_deriv_x)):
            file.write("    " + str(lhss_deriv_x[i]).replace("prm", "prm_x") + " = " +
                       replace_numpy_funcs(rhss_deriv_x[i]).replace("prm", "prm_x") + "\n")
        for i in range(len(lhss_deriv_y)):
            file.write("    " + str(lhss_deriv_y[i]).replace("prm", "prm_y") + " = " +
                       replace_numpy_funcs(rhss_deriv_y[i]).replace("prm", "prm_y") + "\n")
        for i in range(len(lhss_deriv_z)):
            file.write("    " + str(lhss_deriv_z[i]).replace("prm", "prm_z") + " = " +
                       replace_numpy_funcs(rhss_deriv_z[i]).replace("prm", "prm_z") + "\n")

        for i in range(len(lhss_deriv_px)):
            file.write("    " + str(lhss_deriv_px[i]).replace("prm", "prm_px") + " = " +
                       replace_numpy_funcs(rhss_deriv_px[i]).replace("prm", "prm_px") + "\n")
        for i in range(len(lhss_deriv_py)):
            file.write("    " + str(lhss_deriv_py[i]).replace("prm", "prm_py") + " = " +
                       replace_numpy_funcs(rhss_deriv_py[i]).replace("prm", "prm_py") + "\n")
        for i in range(len(lhss_deriv_pz)):
            file.write("    " + str(lhss_deriv_pz[i]).replace("prm", "prm_pz") + " = " +
                       replace_numpy_funcs(rhss_deriv_pz[i]).replace("prm", "prm_pz") + "\n")

        for i in range(len(lhss_deriv_s1x)):
            file.write("    " + str(lhss_deriv_s1x[i]).replace("prm", "prm_s1x") + " = " +
                       replace_numpy_funcs(rhss_deriv_s1x[i]).replace("prm", "prm_s1x") + "\n")
        for i in range(len(lhss_deriv_s1y)):
            file.write("    " + str(lhss_deriv_s1y[i]).replace("prm", "prm_s1y") + " = " +
                       replace_numpy_funcs(rhss_deriv_s1y[i]).replace("prm", "prm_s1y") + "\n")
        for i in range(len(lhss_deriv_s1z)):
            file.write("    " + str(lhss_deriv_s1z[i]).replace("prm", "prm_s1z") + " = " +
                       replace_numpy_funcs(rhss_deriv_s1z[i]).replace("prm", "prm_s1z") + "\n")

        for i in range(len(lhss_deriv_s2x)):
            file.write("    " + str(lhss_deriv_s2x[i]).replace("prm", "prm_s2x") + " = " +
                       replace_numpy_funcs(rhss_deriv_s2x[i]).replace("prm", "prm_s2x") + "\n")
        for i in range(len(lhss_deriv_s2y)):
            file.write("    " + str(lhss_deriv_s2y[i]).replace("prm", "prm_s2y") + " = " +
                       replace_numpy_funcs(rhss_deriv_s2y[i]).replace("prm", "prm_s2y") + "\n")
        for i in range(len(lhss_deriv_s2z)):
            file.write("    " + str(lhss_deriv_s2z[i]).replace("prm", "prm_s2z") + " = " +
                       replace_numpy_funcs(rhss_deriv_s2z[i]).replace("prm", "prm_s2z") + "\n")
        # Perform CSE
        file.write("""
    output_list = ["Hrealprm_x","Hrealprm_y","Hrealprm_z","Hrealprm_px","Hrealprm_py","Hrealprm_pz",
        "Hrealprm_s1x","Hrealprm_s1y","Hrealprm_s1z","Hrealprm_s2x","Hrealprm_s2y","Hrealprm_s2z"]
    expression_list = [Hrealprm_x,Hrealprm_y,Hrealprm_z,Hrealprm_px,Hrealprm_py,Hrealprm_pz,
        Hrealprm_s1x,Hrealprm_s1y,Hrealprm_s1z,Hrealprm_s2x,Hrealprm_s2y,Hrealprm_s2z]
    CSE_results = sp.cse(expression_list, sp.numbered_symbols("tmp"), order='canonical')
    with open("SEOBNR_Playground_Pycodes/numpy_expressions.py", "a") as file:
        for commonsubexpression in CSE_results[0]:
            file.write("    "+str(commonsubexpression[0])+" = "+str(commonsubexpression[1]).replace("sqrt(","np.sqrt(").replace("log(","np.log(").replace("sign(","np.sign(").replace("Abs(", "np.abs(")+"\\n")
        for i,result in enumerate(CSE_results[1]):
            file.write("    "+str(output_list[i])+" = "+str(result).replace("sqrt(","np.sqrt(").replace("log(","np.log(").replace("sign(","np.sign(").replace("Abs(", "np.abs(")+"\\n")
        for i,result in enumerate(CSE_results[1]):
            if i > 0:
                file.write(","+str(output_list[i]))
            else:
                file.write("    return np.array([Hreal,"+str(output_list[i]))
        file.write("])")
""")
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)
Example #7
0
def single_RK_substep_input_symbolic(commentblock,
                                     RHS_str,
                                     RHS_input_str,
                                     RHS_output_str,
                                     RK_lhss_list,
                                     RK_rhss_list,
                                     post_RHS_list,
                                     post_RHS_output_list,
                                     enable_SIMD=False,
                                     enable_griddata=False,
                                     gf_aliases="",
                                     post_post_RHS_string=""):
    return_str = commentblock + "\n"
    if not isinstance(RK_lhss_list, list):
        RK_lhss_list = [RK_lhss_list]
    if not isinstance(RK_rhss_list, list):
        RK_rhss_list = [RK_rhss_list]

    if not isinstance(post_RHS_list, list):
        post_RHS_list = [post_RHS_list]
    if not isinstance(post_RHS_output_list, list):
        post_RHS_output_list = [post_RHS_output_list]

    indent = ""
    if enable_griddata:
        return_str += "{\n" + indent_Ccode(gf_aliases, "  ")
        indent = "  "
    # Part 1: RHS evaluation:
    return_str += indent_Ccode(str(RHS_str).replace(
        "RK_INPUT_GFS",
        str(RHS_input_str).replace("gfsL", "gfs")).replace(
            "RK_OUTPUT_GFS",
            str(RHS_output_str).replace("gfsL", "gfs")) + "\n",
                               indent=indent)

    # Part 2: RK update
    if enable_SIMD:
        return_str += "#pragma omp parallel for\n"
        return_str += indent + "for(int i=0;i<Nxx_plus_2NGHOSTS0*Nxx_plus_2NGHOSTS1*Nxx_plus_2NGHOSTS2*NUM_EVOL_GFS;i+=SIMD_width) {\n"
    else:
        return_str += indent + "LOOP_ALL_GFS_GPS(i) {\n"
    type = "REAL"
    if enable_SIMD:
        type = "REAL_SIMD_ARRAY"
    RK_lhss_str_list = []
    for i, el in enumerate(RK_lhss_list):
        if enable_SIMD:
            RK_lhss_str_list.append(indent +
                                    "const REAL_SIMD_ARRAY __RHS_exp_" +
                                    str(i))
        else:
            RK_lhss_str_list.append(indent + str(el).replace("gfsL", "gfs[i]"))
    read_list = []
    for el in RK_rhss_list:
        for read in list(sp.ordered(el.free_symbols)):
            read_list.append(read)
    read_list_uniq = superfast_uniq(read_list)
    for el in read_list_uniq:
        if str(el) != "dt":
            if enable_SIMD:
                return_str += indent + "  const " + type + " " + str(
                    el) + " = ReadSIMD(&" + str(el).replace("gfsL",
                                                            "gfs[i]") + ");\n"
            else:
                return_str += indent + "  const " + type + " " + str(
                    el) + " = " + str(el).replace("gfsL", "gfs[i]") + ";\n"
    if enable_SIMD:
        return_str += indent + "  const REAL_SIMD_ARRAY DT = ConstSIMD(dt);\n"
    preindent = "1"
    if enable_griddata:
        preindent = "2"
    kernel = outputC(RK_rhss_list,
                     RK_lhss_str_list,
                     filename="returnstring",
                     params="includebraces=False,preindent=" + preindent +
                     ",outCverbose=False,enable_SIMD=" + str(enable_SIMD))
    if enable_SIMD:
        return_str += kernel.replace("dt", "DT")
        for i, el in enumerate(RK_lhss_list):
            return_str += "  WriteSIMD(&" + str(el).replace(
                "gfsL", "gfs[i]") + ", __RHS_exp_" + str(i) + ");\n"
    else:
        return_str += kernel

    return_str += indent + "}\n"

    # Part 3: Call post-RHS functions
    for post_RHS, post_RHS_output in zip(post_RHS_list, post_RHS_output_list):
        return_str += indent_Ccode(
            post_RHS.replace("RK_OUTPUT_GFS",
                             str(post_RHS_output).replace("gfsL", "gfs")))

    if enable_griddata:
        return_str += "}\n"

    for post_RHS, post_RHS_output in zip(post_RHS_list, post_RHS_output_list):
        return_str += indent_Ccode(
            post_post_RHS_string.replace(
                "RK_OUTPUT_GFS",
                str(post_RHS_output).replace("gfsL", "gfs")), "")

    return return_str
def add_HI_func_to_outC_function_dict(
        list_of_interp_vars, list_of_base_gridfunction_names_in_interps,
        list_of_interp_operators, hicoeffs, histencl):
    # Step 5.a.ii.A: First construct a list of all the unique Hermite interpolator functions
    list_of_uniq_interp_operators = superfast_uniq(list_of_interp_operators)
    c_type = "REAL"
    if par.parval_from_str("grid::GridFuncMemAccess") == "ETK":
        c_type = "CCTK_REAL"
    func_prefix = "order_" + str(HIparams.HI_DM_order) + "_"
    if HIparams.enable_SIMD == "True":
        c_type = "REAL_SIMD_ARRAY"
        func_prefix = "SIMD_" + func_prefix

    # Stores the needed calls to the functions we're adding to outC_function_dict:
    HIfunccall_list = []
    for op in list_of_uniq_interp_operators:
        which_op_idx = find_which_op_idx(op, list_of_interp_operators)

        rhs_expr = sp.sympify(0)
        for j in range(len(hicoeffs[which_op_idx])):
            var = sp.sympify("f" +
                             varsuffix(histencl[which_op_idx][j], HIparams))
            rhs_expr += hicoeffs[which_op_idx][j] * var

        # Multiply each expression by the appropriate power
        #   of 1/dx[i]
        invdx = []
        used_invdx = [False, False, False, False]
        for d in range(HIparams.DIM):
            invdx.append(sp.sympify("invdx" + str(d)))
        # First-order or Kreiss-Oliger interpolators:
        if ((len(op) == 5 and "dKOD" in op) or (len(op) == 3 and "dD" in op)
                or (len(op) == 5 and ("dupD" in op or "ddnD" in op))):
            dirn = int(op[len(op) - 1])
            rhs_expr *= invdx[dirn]
            used_invdx[dirn] = True
        # Second-order interps:
        elif len(op) == 5 and "dDD" in op:
            dirn1 = int(op[len(op) - 2])
            dirn2 = int(op[len(op) - 1])
            used_invdx[dirn1] = used_invdx[dirn2] = True
            rhs_expr *= invdx[dirn1] * invdx[dirn2]
        else:
            print("Error: was unable to parse interpolator operator: ", op)
            sys.exit(1)

        outfunc_params = ""
        for d in range(HIparams.DIM):
            if used_invdx[d]:
                outfunc_params += "const " + c_type + " invdx" + str(d) + ","

        for j in range(len(hicoeffs[which_op_idx])):
            var = sp.sympify("f" +
                             varsuffix(histencl[which_op_idx][j], HIparams))
            outfunc_params += "const " + c_type + " " + str(var)
            if j != len(hicoeffs[which_op_idx]) - 1:
                outfunc_params += ","

        for i in range(len(list_of_interp_operators)):
            # print("comparing ",list_of_interp_operators[i],op)
            if list_of_interp_operators[i] == op:
                funccall = type__var(
                    list_of_interp_vars[i],
                    HIparams) + " = " + func_prefix + "f_" + str(op) + "("
                for d in range(HIparams.DIM):
                    if used_invdx[d]:
                        funccall += "invdx" + str(d) + ","
                gfname = list_of_base_gridfunction_names_in_interps[i]
                for j in range(len(hicoeffs[which_op_idx])):
                    funccall += gfname + varsuffix(histencl[which_op_idx][j],
                                                   HIparams)
                    if j != len(hicoeffs[which_op_idx]) - 1:
                        funccall += ","
                funccall += ");"
                HIfunccall_list.append(funccall)

        # If the function already exists in the outC_function_dict, then do not add it; move to the next op.
        if func_prefix + "f_" + str(op) not in outC_function_dict:
            p = "preindent=1,enable_SIMD=" + HIparams.enable_SIMD + ",outCverbose=False,CSE_preprocess=True,includebraces=False"
            outHIstr = outputC(rhs_expr, "retval", "returnstring", params=p)
            outHIstr = outHIstr.replace("retval = ", "return ")
            add_to_Cfunction_dict(
                desc=
                " * (__HI_OPERATOR_FUNC__) Hermite interpolator operator for "
                + str(op).replace("dDD", "second interpolator: ").replace(
                    "dD", "first interpolator: ").replace(
                        "dKOD", "Kreiss-Oliger interpolator: ").replace(
                            "dupD", "upwinded interpolator: ").replace(
                                "ddnD", "downwinded interpolator: ") +
                " direction. In Cartesian coordinates, directions 0,1,2 correspond to x,y,z directions, respectively.",
                c_type="static " + c_type + " _NOINLINE _UNUSED",
                name=func_prefix + "f_" + str(op),
                enableCparameters=False,
                params=outfunc_params,
                preloop="",
                body=outHIstr)
    return HIfunccall_list
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)
Example #10
0
def output_H_and_derivs():
    # Open and read the file of numerical expressions (written in SymPy syntax) computing the SEOBNRv3 Hamiltonian.
    #f = open("SEOBNR/Hamstring.txt", 'r')
    f = open("SEOBNR/SymPy_Hreal_on_bottom.txt", 'r')
    Hamstring = str(f.read())
    f.close()

    # Split Hamstring by carriage returns.
    Hamterms = Hamstring.splitlines()

    # Create 'lr' array to store each left-hand side and right-hand side of Hamstring as strings.
    lr = []
    # Loop over each line in Hamstring to separate the left- and right-hand sides.
    for i in range(len(Hamterms)):
        # Ignore lines with 2 or fewer characters and those starting with #
        if len(Hamterms[i]) > 2 and Hamterms[i][0] != "#":
            # Split each line by its equals sign.
            splitHamterms = Hamterms[i].split("=")
            # Append terms to the 'lr' array, removing spaces, "sp." prefixes, and replacing Lambda->Lamb (Lambda is a
            # protected keyword)
            lr.append(lhrh(lhs=splitHamterms[0].replace(" ", "").replace("Lambda", "Lamb"),
                           rhs=splitHamterms[1].replace(" ", "").replace("sp.", "").replace("Lambda", "Lamb")))
    # Declare the symbol 'xx', which we use to denote each left-hand side as a function
    xx = sp.Symbol('xx')
    # Create arrays to store simplified left- and right-hand expressions, as well as left-hand sides designated as
    # functions.
    func = []
    lhss = []
    rhss = []
    # Affix '(xx)' to each left-hand side as a function designation; separate and simplify left- and right-hand sides
    # of the numerical expressions.
    for i in range(len(lr)):
        func.append(sp.sympify(sp.Function(lr[i].lhs)(xx)))
        lhss.append(sp.sympify(lr[i].lhs))
        rhss.append(sp.sympify(lr[i].rhs))
    # Creat array for and generate a list of all the "free symbols" in the right-hand side expressions.
    full_symbol_list_with_dups = []
    for i in range(len(lr)):
        for var in rhss[i].free_symbols:
            full_symbol_list_with_dups.append(var)

    # Remove all duplicated "free symbols" from the right-hand side expressions.
    full_symbol_list = superfast_uniq(full_symbol_list_with_dups)

    # Declare input constants.
    m1, m2, eta, KK, k0, k1, dSO, dSS = sp.symbols("m1 m2 eta KK k0 k1 dSO dSS", real=True)
    tortoise, EMgamma = sp.symbols("tortoise EMgamma", real=True)
    input_constants = [m1, m2, eta, KK, k0, k1, dSO, dSS, tortoise, EMgamma]

    # Derivatives of input constants will always be zero, so remove them from the full_symbol_list.
    for inputconst in input_constants:
        for symbol in full_symbol_list:
            if str(symbol) == str(inputconst):
                full_symbol_list.remove(symbol)

    # Add symbols to the function list and replace right-hand side terms with their function equivalent.
    full_function_list = []
    for symb in full_symbol_list:
        func = sp.sympify(sp.Function(str(symb))(xx))
        full_function_list.append(func)
        for i in range(len(rhss)):
            for var in rhss[i].free_symbols:
                if str(var) == str(symb):
                    rhss[i] = rhss[i].subs(var, func)

    # Create left- and right-hand side 'deriv' arrays
    lhss_deriv = []
    rhss_deriv = []
    # Differentiate with respect to xx, remove '(xx)', and replace xx with 'prm' notation.
    for i in range(len(rhss)):
        lhss_deriv.append(sp.sympify(str(lhss[i]) + "prm"))
        newrhs = sp.sympify(
            str(sp.diff(rhss[i], xx)).replace("(xx)", "").replace(", xx", "prm").replace("Derivative", ""))
        rhss_deriv.append(newrhs)
    # Simplify derivative expressions with simplify_deriv()
    lhss_deriv_simp, rhss_deriv_simp = simplify_deriv(lhss_deriv, rhss_deriv)
    lhss_deriv = lhss_deriv_simp
    rhss_deriv = rhss_deriv_simp
    # Generate partial derivatives with respect to each of the twelve input variables
    lhss_deriv_x, rhss_deriv_x = deriv_onevar(lhss_deriv, rhss_deriv, xprm=1, yprm=0, zprm=0, p1prm=0, p2prm=0, p3prm=0,
                                              S1xprm=0, S1yprm=0, S1zprm=0, S2xprm=0, S2yprm=0, S2zprm=0)
    #lhss_deriv_y, rhss_deriv_y = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=1, zprm=0, p1prm=0, p2prm=0, p3prm=0,
                                               #S1xprm=0, S1yprm=0, S1zprm=0, S2xprm=0, S2yprm=0, S2zprm=0)
    #lhss_deriv_z, rhss_deriv_z = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=0, zprm=1, p1prm=0, p2prm=0, p3prm=0,
                                               #S1xprm=0, S1yprm=0, S1zprm=0, S2xprm=0, S2yprm=0, S2zprm=0)
    #lhss_deriv_p1, rhss_deriv_p1 = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=0, zprm=0, p1prm=1, p2prm=0,
                                                #p3prm=0, S1xprm=0, S1yprm=0, S1zprm=0, S2xprm=0, S2yprm=0, S2zprm=0)
    lhss_deriv_p2, rhss_deriv_p2 = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=0, zprm=0, p1prm=0, p2prm=1,
                                                p3prm=0, S1xprm=0, S1yprm=0, S1zprm=0, S2xprm=0, S2yprm=0, S2zprm=0)
    lhss_deriv_p3, rhss_deriv_p3 = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=0, zprm=0, p1prm=0, p2prm=0,
                                                p3prm=1, S1xprm=0, S1yprm=0, S1zprm=0, S2xprm=0, S2yprm=0, S2zprm=0)
    #lhss_deriv_S1x, rhss_deriv_S1x = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=0, zprm=0, p1prm=0, p2prm=0,
                                                  #p3prm=0, S1xprm=1, S1yprm=0, S1zprm=0, S2xprm=0, S2yprm=0, S2zprm=0)
    #lhss_deriv_S1y, rhss_deriv_S1y = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=0, zprm=0, p1prm=0, p2prm=0,
                                                  #p3prm=0, S1xprm=0, S1yprm=1, S1zprm=0, S2xprm=0, S2yprm=0, S2zprm=0)
    #lhss_deriv_S1z, rhss_deriv_S1z = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=0, zprm=0, p1prm=0, p2prm=0,
                                                  #p3prm=0, S1xprm=0, S1yprm=0, S1zprm=1, S2xprm=0, S2yprm=0, S2zprm=0)
    #lhss_deriv_S2x, rhss_deriv_S2x = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=0, zprm=0, p1prm=0, p2prm=0,
                                                  #p3prm=0, S1xprm=0, S1yprm=0, S1zprm=0, S2xprm=1, S2yprm=0, S2zprm=0)
    #lhss_deriv_S2y, rhss_deriv_S2y = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=0, zprm=0, p1prm=0, p2prm=0,
                                                  #p3prm=0, S1xprm=0, S1yprm=0, S1zprm=0, S2xprm=0, S2yprm=1, S2zprm=0)
    #lhss_deriv_S2z, rhss_deriv_S2z = deriv_onevar(lhss_deriv, rhss_deriv, xprm=0, yprm=0, zprm=0, p1prm=0, p2prm=0,
                                                  #p3prm=0, S1xprm=0, S1yprm=0, S1zprm=0, S2xprm=0, S2yprm=0, S2zprm=1)
    # Prepare to output derivative expressions in C syntax
    outstring = "/* SEOBNR Hamiltonian expression: */\n"
    outstringsp = ""
    outsplhs = []
    outsprhs = []
    for i in range(len(lr)):
        outstring += outputC(sp.sympify(lr[i].rhs), lr[i].lhs, "returnstring",
                             "outCverbose=False,includebraces=False,CSE_enable=False")
        outstringsp += lr[i].lhs + " = " + lr[i].rhs + "\n"
        outsplhs.append(sp.sympify(lr[i].lhs))
        outsprhs.append(sp.sympify(lr[i].rhs))

    outstring += "\n\n\n/* SEOBNR \partial_x H expression: */\n"
    for i in range(len(lhss_deriv_x)):
        outstring += outputC(rhss_deriv_x[i], str(lhss_deriv_x[i]), "returnstring",
                             "outCverbose=False,includebraces=False,CSE_enable=False")
        outstringsp += str(lhss_deriv_x[i]) + " = " + str(rhss_deriv_x[i]) + "\n"
        outsplhs.append(lhss_deriv_x[i])
        outsprhs.append(rhss_deriv_x[i])

    outstring += "\n\n\n/* SEOBNR \partial_p2 H expression: */\n"
    for i in range(len(lhss_deriv_p2)):
        outstring += outputC(rhss_deriv_p2[i], str(lhss_deriv_p2[i]), "returnstring",
                             "outCverbose=False,includebraces=False,CSE_enable=False")
        outstringsp += str(lhss_deriv_p2[i]) + " = " + str(rhss_deriv_p2[i]) + "\n"
        outsplhs.append(lhss_deriv_p2[i])
        outsprhs.append(rhss_deriv_p2[i])

    outstring += "\n\n\n/* SEOBNR \partial_p3 H expression: */\n"
    for i in range(len(lhss_deriv_p3)):
        outstring += outputC(rhss_deriv_p3[i], str(lhss_deriv_p3[i]), "returnstring",
                             "outCverbose=False,includebraces=False,CSE_enable=False")
        outstringsp += str(lhss_deriv_p3[i]) + " = " + str(rhss_deriv_p3[i]) + "\n"
        outsplhs.append(lhss_deriv_p3[i])
        outsprhs.append(rhss_deriv_p3[i])

    with open("SEOBNR_Playground_Pycodes/new_dHdx.py", "w") as file:
        file.write("""from __future__ import division
import numpy as np
def new_compute_dHdx(m1, m2, eta, x, y, z, p1, p2, p3, S1x, S1y, S1z, S2x, S2y, S2z, KK, k0, k1, dSO, dSS, tortoise, EMgamma):
""")
        for i in range(len(lr) - 1):
            file.write("    " + lr[i].lhs + " = " + str(lr[i].rhs).replace("Rational(", "np.true_divide(").replace("sqrt(", "np.sqrt(").replace("log(", "np.log(").replace("sign(", "np.sign(").replace("Abs(", "np.abs(").replace("pi", "np.pi") + "\n")
        for i in range(len(lhss_deriv_x)):
            file.write("    " + str(lhss_deriv_x[i]).replace("prm", "prm_x") + " = " + replace_numpy_funcs(rhss_deriv_x[i]).replace("prm", "prm_x").replace("sp.sqrt(","np.sqrt(").replace("sp.log(","np.log(").replace("sp.sign(","np.sign(").replace("sp.Abs(", "np.abs(") + "\n")
        file.write("    return np.array([Hrealprm_x])")

    with open("SEOBNR_Playground_Pycodes/new_dHdp2.py", "w") as file:
        file.write("""from __future__ import division
import numpy as np
def new_compute_dHdp2(m1, m2, eta, x, y, z, p1, p2, p3, S1x, S1y, S1z, S2x, S2y, S2z, KK, k0, k1, dSO, dSS, tortoise, EMgamma):
""")
        for i in range(len(lr) - 1):
            file.write("    " + lr[i].lhs + " = " + str(lr[i].rhs).replace("Rational(", "np.true_divide(").replace("sqrt(", "np.sqrt(").replace("log(", "np.log(").replace("sign(", "np.sign(").replace("Abs(", "np.abs(").replace("pi", "np.pi") + "\n")
        for i in range(len(lhss_deriv_p2)):
            file.write("    " + str(lhss_deriv_p2[i]).replace("prm", "prm_p2") + " = " + replace_numpy_funcs(rhss_deriv_p2[i]).replace("prm", "prm_p2").replace("sp.sqrt(","np.sqrt(").replace("sp.log(","np.log(").replace("sp.sign(","np.sign(").replace("sp.Abs(", "np.abs(") + "\n")
        file.write("    return np.array([Hrealprm_p2])")

    with open("SEOBNR_Playground_Pycodes/new_dHdp3.py", "w") as file:
        file.write("""from __future__ import division
import numpy as np
def new_compute_dHdp3(m1, m2, eta, x, y, z, p1, p2, p3, S1x, S1y, S1z, S2x, S2y, S2z, KK, k0, k1, dSO, dSS, tortoise, EMgamma):
""")
        for i in range(len(lr) - 1):
            file.write("    " + lr[i].lhs + " = " + str(lr[i].rhs).replace("Rational(", "np.true_divide(").replace("sqrt(", "np.sqrt(").replace("log(", "np.log(").replace("sign(", "np.sign(").replace("Abs(", "np.abs(").replace("pi", "np.pi") + "\n")
        for i in range(len(lhss_deriv_p3)):
            file.write("    " + str(lhss_deriv_p3[i]).replace("prm", "prm_p3") + " = " + replace_numpy_funcs(rhss_deriv_p3[i]).replace("prm", "prm_p3").replace("sp.sqrt(","np.sqrt(").replace("sp.log(","np.log(").replace("sp.sign(","np.sign(").replace("sp.Abs(", "np.abs(") + "\n")
        file.write("    return np.array([Hrealprm_p3])")

    # TylerK: now create a text file listing only the terms so we can take a second derivative!

    with open("SEOBNR_Playground_Pycodes/dHdx.txt", "w") as file:
        for i in range(len(lr) - 1):
            file.write(lr[i].lhs + " = " + str(lr[i].rhs) + "\n")
        for i in range(len(lhss_deriv_x)):
            file.write(str(lhss_deriv_x[i]).replace("prm", "prm_x") + " = " + replace_numpy_funcs(rhss_deriv_x[i]).replace("prm", "prm_x") + "\n")
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 + "\"")