Notebook Status: Self-Validated
Validation Notes: This tutorial notebook has been confirmed to be self-consistent with its corresponding NRPy+ module, as documented below, here and here. Additional validation tests may have been performed, but are as yet, undocumented. (TODO)
This tutorial will draw on previous work done by Ian Ruchlin on Maxwell's equations in Curvilinear Coordinates, which itself drew on the two formulations described in Illustrating Stability Properties of Numerical Relativity in Electrodynamics. This will be done to aid construction of an Einstein Toolkit thorn, which will itself be built in the Tutorial-ETK_thorn-Maxwell tutorial notebook.
The construction of our equations here will be nearly identical; however, by assuming Cartesian coordinates, we are able to make several simplifications and eliminate the need for reference_metric.py as a dependency.
We will begin with their System I. While the Curvilinear version of this code assumed flat spacetime, we will be constructing the equations in a general spacetime. This allows us to simply replace the reference metric "hatted" quantities with their general counterparts.
This notebook is organized as follows
Maxwell.MaxwellCartesian_Evol
NRPy+ Module (System I)Maxwell.MaxwellCartesian_Evol
NRPy+ Module (System II)Maxwell.MaxwellCartesian_ID
NRPy+ Module#Step P1: Import needed Python modules
import NRPy_param_funcs as par # NRPy+: Parameter interface
import sympy as sp # SymPy: The Python computer algebra package upon which NRPy+ depends
import indexedexp as ixp # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support
import finite_difference as fin # NRPy+: Finite difference C code generation module
import grid as gri # NRPy+: Functions having to do with numerical grids
#Step P2: Name this Python module
thismodule = "MaxwellCartesian"
#Step 0: Set the spatial dimension parameter to 3.
par.set_parval_from_str("grid::DIM", 3)
DIM = par.parval_from_str("grid::DIM")
# Step 1: Set the finite differencing order to 4.
par.set_parval_from_str("finite_difference::FD_CENTDERIVS_ORDER", 4)
# Step 2: Register gridfunctions that are needed as input.
psi = gri.register_gridfunctions("EVOL", ["psi"])
# Step 3a: Declare the rank-1 indexed expressions E_{i}, A_{i},
# and \partial_{i} \psi. Derivative variables like these
# must have an underscore in them, so the finite
# difference module can parse the variable name properly.
ED = ixp.register_gridfunctions_for_single_rank1("EVOL", "ED")
AD = ixp.register_gridfunctions_for_single_rank1("EVOL", "AD")
psi_dD = ixp.declarerank1("psi_dD")
x,y,z = gri.register_gridfunctions("AUX",["x","y","z"])
## Step 3b: Declare the conformal metric tensor and its first
# derivative. These are needed to find the Christoffel
# symbols, which we need for covariant derivatives.
gammaDD = ixp.register_gridfunctions_for_single_rank2("AUX","gammaDD", "sym01") # The AUX or EVOL designation is *not*
# used in diagnostic modules.
gammaDD_dD = ixp.declarerank3("gammaDD_dD","sym01")
gammaDD_dDD = ixp.declarerank4("gammaDD_dDD","sym01_sym23")
gammaUU, detgamma = ixp.symm_matrix_inverter3x3(gammaDD)
gammaUU_dD = ixp.declarerank3("gammaDD_dD","sym01")
# Define the Christoffel symbols
GammaUDD = ixp.zerorank3(DIM)
for i in range(DIM):
for k in range(DIM):
for l in range(DIM):
for m in range(DIM):
GammaUDD[i][k][l] += (sp.Rational(1,2))*gammaUU[i][m]*\
(gammaDD_dD[m][k][l] + gammaDD_dD[m][l][k] - gammaDD_dD[k][l][m])
# Step 3b: Declare the rank-2 indexed expression \partial_{j} A_{i},
# which is not symmetric in its indices.
# Derivative variables like these must have an underscore
# in them, so the finite difference module can parse the
# variable name properly.
AD_dD = ixp.declarerank2("AD_dD", "nosym")
# Step 3c: Declare the rank-3 indexed expression \partial_{jk} A_{i},
# which is symmetric in the two {jk} indices.
AD_dDD = ixp.declarerank3("AD_dDD", "sym12")
# Step 4: Calculate first and second covariant derivatives, and the
# necessary contractions.
# First covariant derivative
# D_{j} A_{i} = A_{i,j} - \Gamma^{k}_{ij} A_{k}
AD_dcovD = ixp.zerorank2()
for i in range(DIM):
for j in range(DIM):
AD_dcovD[i][j] = AD_dD[i][j]
for k in range(DIM):
AD_dcovD[i][j] -= GammaUDD[k][i][j] * AD[k]
One particular difficulty we will encounter here is taking the covariant Laplacian of a vector. This will here take the form of DjDjAi. We will start with the outer derivative after lowering the index on the second operator. So, we see that DjDjAi=Dj(γjkDkAi)=(DkAi)Djγjk+γjkDjDkAi=γjk[∂j(DkAi)−ΓlijDkAl−ΓljkDlAi],
So, the Laplacian becomes DjDjAi=γjk[Ai,jk−Al(γli,kj+γkl,ij−γik,lj)⏟Term 1+Γlik(γlmAm,j+Amγlm,j)⏟Term 2−(ΓlijAl,k+ΓljkAi,l)⏟Term 3+(ΓmijΓlmkAl+ΓmjkΓlimAl)⏟Term 4];
# First, we must construct the lowered Christoffel symbols:
# \Gamma_{ijk} = \gamma_{il} \Gamma^l_{jk}
# And raise the index on A:
# A^j = \gamma^{ij} A_i
GammaDDD = ixp.zerorank3()
AU = ixp.zerorank1()
for i in range(DIM):
for j in range(DIM):
AU[j] += gammaUU[i][j] * AD[i]
for k in range(DIM):
for l in range(DIM):
GammaDDD[i][j][k] += gammaDD[i][l] * GammaUDD[l][j][k]
# Covariant second derivative (the bracketed terms):
# D_j D^j A_i = \gamma^{jk} [A_{i,jk} - A^l (\gamma_{li,kj} + \gamma_{kl,ij} - \gamma_{ik,lj})
# + \Gamma_{lik} (\gamma^{lm} A_{m,j} + A_m \gamma^{lm}{}_{,j})
# - (\Gamma^l_{ij} A_{l,k} + \Gamma^l_{jk} A_{i,l})
# + (\Gamma^m_{ij} \Gamma^l_{mk} A_l + \Gamma ^m_{jk} \Gamma^l_{im} A_l)
AD_dcovDD = ixp.zerorank3()
for i in range(DIM):
for j in range(DIM):
for k in range(DIM):
AD_dcovDD[i][j][k] = AD_dDD[i][j][k]
for l in range(DIM):
# Terms 1 and 3
AD_dcovDD[i][j][k] -= AU[l] * (gammaDD_dDD[l][i][k][j] + gammaDD_dDD[k][l][i][j] - \
gammaDD_dDD[i][k][l][j]) \
+ GammaUDD[l][i][j] * AD_dD[l][k] + GammaUDD[l][j][k] * AD_dD[i][l]
for m in range(DIM):
# Terms 2 and 4
AD_dcovDD[i][j][k] += GammaDDD[l][i][k] * (gammaUU[l][m] * AD_dD[m][j] + AD[m] * gammaUU_dD[l][m][j]) \
+ GammaUDD[m][i][j] * GammaUDD[l][m][k] * AD[l] \
+ GammaUDD[m][j][k] * GammaUDD[l][i][m] * AD[l]
# Covariant divergence
# D_{i} A^{i} = \gamma^{ij} D_{j} A_{i}
DivA = 0
# Gradient of covariant divergence
# DivA_dD_{i} = \gamma^{jk} A_{k;\hat{j}\hat{i}}
DivA_dD = ixp.zerorank1()
# Covariant Laplacian
# LapAD_{i} = \gamma^{jk} A_{i;\hat{j}\hat{k}}
LapAD = ixp.zerorank1()
for i in range(DIM):
for j in range(DIM):
DivA += gammaUU[i][j] * AD_dcovD[i][j]
for k in range(DIM):
DivA_dD[i] += gammaUU[j][k] * AD_dcovDD[k][j][i]
LapAD[i] += gammaUU[j][k] * AD_dcovDD[i][j][k]
# Step 5: Define right-hand sides for the evolution.
ArhsD = ixp.zerorank1()
ErhsD = ixp.zerorank1()
for i in range(DIM):
ArhsD[i] = -ED[i] - psi_dD[i]
ErhsD[i] = -LapAD[i] + DivA_dD[i]
psi_rhs = -DivA
# Step 6: Generate C code for System I Maxwell's evolution equations,
# print output to the screen (standard out, or stdout).
#lhrh_list = []
#for i in range(DIM):
# lhrh_list.append(lhrh(lhs=gri.gfaccess("rhs_gfs", "AD" + str(i)), rhs=ArhsD[i]))
# lhrh_list.append(lhrh(lhs=gri.gfaccess("rhs_gfs", "ED" + str(i)), rhs=ErhsD[i]))
#lhrh_list.append(lhrh(lhs=gri.gfaccess("rhs_gfs", "psi"), rhs=psi_rhs))
#fin.FD_outputC("stdout", lhrh_list)
To evaluate our results, we will need to measure how much our simulation differs from what should be physically possible. We will do this with the constraint equation DiEi=4πρe; specifically, we will measure the constraint violation C=DiEi−4πρe=Di(γijEj)−4πρe=γijDiEj+EjDiγij−4πρe=γijDiEj,
ED_dD = ixp.declarerank2("ED_dD","nosym")
Cviolation = gri.register_gridfunctions("AUX", ["Cviolation"])
Cviolation = sp.sympify(0)
for i in range(DIM):
for j in range(DIM):
Cviolation += gammaUU[i][j] * ED_dD[j][i]
for b in range(DIM):
Cviolation -= gammaUU[i][j] * GammaUDD[b][i][j] * ED[b]
Maxwell.MaxwellCartesian_Evol
NRPy+ Module (System I) [Back to top]¶Here, as a code validation check, we verify agreement in the SymPy expressions for the RHSs of Maxwell's equations (in System I) between
# Reset the list of gridfunctions, as registering a gridfunction
# twice will spawn an error.
gri.glb_gridfcs_list = []
# Step 18: Call the MaxwellCartesian_Evol() function from within the
# Maxwell/MaxwellCartesian_Evol.py module,
# which should do exactly the same as in Steps 1-16 above.
import Maxwell.MaxwellCartesian_Evol as mwevol
par.set_parval_from_str("System_to_use","System_I")
mwevol.MaxwellCartesian_Evol()
print("Consistency check between MaxwellCartesian tutorial and NRPy+ module: ALL SHOULD BE ZERO.")
print("psi_rhs - mwevol.psi_rhs = " + str(psi_rhs - mwevol.psi_rhs))
for i in range(DIM):
print("ArhsD["+str(i)+"] - mwevol.ArhsD["+str(i)+"] = " + str(ArhsD[i] - mwevol.ArhsD[i]))
print("ErhsD["+str(i)+"] - mwevol.ErhsD["+str(i)+"] = " + str(ErhsD[i] - mwevol.ErhsD[i]))
print("Cviolation - mwevol.Cviolation = " + str(Cviolation - mwevol.Cviolation))
Warning: System I is less stable! Consistency check between MaxwellCartesian tutorial and NRPy+ module: ALL SHOULD BE ZERO. psi_rhs - mwevol.psi_rhs = 0 ArhsD[0] - mwevol.ArhsD[0] = 0 ErhsD[0] - mwevol.ErhsD[0] = 0 ArhsD[1] - mwevol.ArhsD[1] = 0 ErhsD[1] - mwevol.ErhsD[1] = 0 ArhsD[2] - mwevol.ArhsD[2] = 0 ErhsD[2] - mwevol.ErhsD[2] = 0 Cviolation - mwevol.Cviolation = 0
We will now build the equations for System II.
# We inherit here all of the definitions from System I, above
# Step 7a: Register the scalar auxiliary variable \Gamma
Gamma = gri.register_gridfunctions("EVOL", ["Gamma"])
# Step 7b: Declare the ordinary gradient \partial_{i} \Gamma
Gamma_dD = ixp.declarerank1("Gamma_dD")
# Step 8a: Construct the second covariant derivative of the scalar \psi
# \psi_{;\hat{i}\hat{j}} = \psi_{,i;\hat{j}}
# = \psi_{,ij} - \Gamma^{k}_{ij} \psi_{,k}
psi_dDD = ixp.declarerank2("psi_dDD", "sym01")
psi_dcovDD = ixp.zerorank2()
for i in range(DIM):
for j in range(DIM):
psi_dcovDD[i][j] = psi_dDD[i][j]
for k in range(DIM):
psi_dcovDD[i][j] += - GammaUDD[k][i][j] * psi_dD[k]
# Step 8b: Construct the covariant Laplacian of \psi
# Lappsi = ghat^{ij} D_{j} D_{i} \psi
Lappsi = 0
for i in range(DIM):
for j in range(DIM):
Lappsi += gammaUU[i][j] * psi_dcovDD[i][j]
# Step 9: Define right-hand sides for the evolution.
ArhsD = ixp.zerorank1()
ErhsD = ixp.zerorank1()
for i in range(DIM):
ArhsD[i] = -ED[i] - psi_dD[i]
ErhsD[i] = -LapAD[i] + Gamma_dD[i]
psi_rhs = -Gamma
Gamma_rhs = -Lappsi
# Step 10: Generate C code for System II Maxwell's evolution equations,
# print output to the screen (standard out, or stdout).
#lhrh_list = []
#for i in range(DIM):
# lhrh_list.append(lhrh(lhs=gri.gfaccess("rhs_gfs", "AD" + str(i)), rhs=ArhsD[i]))
# lhrh_list.append(lhrh(lhs=gri.gfaccess("rhs_gfs", "ED" + str(i)), rhs=ErhsD[i]))
#lhrh_list.append(lhrh(lhs=gri.gfaccess("rhs_gfs", "psi"), rhs=psi_rhs))
#lhrh_list.append(lhrh(lhs=gri.gfaccess("rhs_gfs", "Gamma"), rhs=Gamma_rhs))
#fin.FD_outputC("stdout", lhrh_list)
Maxwell.MaxwellCartesian_Evol
NRPy+ Module (System II) [Back to top]¶Here, as a code validation check, we verify agreement in the SymPy expressions for the RHSs of Maxwell's equations (in System II) between
# Reset the list of gridfunctions, as registering a gridfunction
# twice will spawn an error.
gri.glb_gridfcs_list = []
# Step 18: Call the MaxwellCartesian_Evol() function from within the
# Maxwell/MaxwellCartesian_Evol.py module,
# which should do exactly the same as in Steps 1-16 above.
par.set_parval_from_str("System_to_use","System_II")
mwevol.MaxwellCartesian_Evol()
print("Consistency check between MaxwellCartesian tutorial and NRPy+ module: ALL SHOULD BE ZERO.")
print("psi_rhs - mwevol.psi_rhs = " + str(psi_rhs - mwevol.psi_rhs))
print("Gamma_rhs - mwevol.Gamma_rhs = " + str(Gamma_rhs - mwevol.Gamma_rhs))
for i in range(DIM):
print("ArhsD["+str(i)+"] - mwevol.ArhsD["+str(i)+"] = " + str(ArhsD[i] - mwevol.ArhsD[i]))
print("ErhsD["+str(i)+"] - mwevol.ErhsD["+str(i)+"] = " + str(ErhsD[i] - mwevol.ErhsD[i]))
print("Cviolation - mwevol.Cviolation = " + str(Cviolation - mwevol.Cviolation))
Consistency check between MaxwellCartesian tutorial and NRPy+ module: ALL SHOULD BE ZERO. psi_rhs - mwevol.psi_rhs = 0 Gamma_rhs - mwevol.Gamma_rhs = 0 ArhsD[0] - mwevol.ArhsD[0] = 0 ErhsD[0] - mwevol.ErhsD[0] = 0 ArhsD[1] - mwevol.ArhsD[1] = 0 ErhsD[1] - mwevol.ErhsD[1] = 0 ArhsD[2] - mwevol.ArhsD[2] = 0 ErhsD[2] - mwevol.ErhsD[2] = 0 Cviolation - mwevol.Cviolation = 0
Now that we have evolution equations in place, we must construct the initial data that will be evolved by the solver of our choice. We will start from the analytic solution to this system of equations, given in Illustrating Stability Properties of Numerical Relativity in Electrodynamics as Aˆϕ=Asinθ(e−λv2−e−λu2r2−2λve−λv2−ue−λu2r),
For system II, we will also need to set initial data for Γ. Since Γ=−∂tψ and we have chosen ψ(t=0)=0, Γ(t=0)=0.
# Step 1: Declare free parameters intrinsic to these initial data
amp,lam = par.Cparameters("REAL",thismodule,
["amp","lam"],
[1.0,1.0]) # __name__ = "MaxwellCartesian_ID", this module's name
# Step 2: Set the initial data
AidD = ixp.zerorank1()
EidD = ixp.zerorank1()
EidU = ixp.zerorank1()
# Set the coordinate transformations:
radial = sp.sqrt(x*x + y*y + z*z)
polar = sp.atan2(sp.sqrt(x*x + y*y),z)
EU_phi = 8*amp*radial*sp.sin(polar)*lam*lam*sp.exp(-lam*radial*radial)
EidU[0] = -(y * EU_phi)/sp.sqrt(x*x + y*y)
EidU[1] = (x * EU_phi)/sp.sqrt(x*x + y*y)
# The z component (2)is zero.
for i in range(DIM):
for j in range(DIM):
EidD[i] += gammaDD[i][j] * EidU[j]
psi_ID = sp.sympify(0)
Gamma_ID = sp.sympify(0)
Maxwell.MaxwellCartesian_ID
NRPy+ Module [Back to top]¶Here, as a code validation check, we verify agreement in the SymPy expressions for the initial data we intend to use between
Since the initial data is identical between the two systems for Ei, Ai, and ψ, so checking system I should be redundant; we will do it anyways, to be sure.
# Reset the list of gridfunctions, as registering a gridfunction
# twice will spawn an error.
gri.glb_gridfcs_list = []
par.set_parval_from_str("System_to_use","System_I")
import Maxwell.MaxwellCartesian_ID as mwid
mwid.MaxwellCartesian_ID()
print("System I consistency check between MaxwellCartesian tutorial and NRPy+ module;\n ALL SHOULD BE ZERO:")
print("psi_ID - mwid.psi_ID = " + str(psi_ID - mwid.psi_ID))
for i in range(DIM):
print("AidD["+str(i)+"] - mwid.AidD["+str(i)+"] = " + str(AidD[i] - mwid.AidD[i]))
print("EidD["+str(i)+"] - mwid.EidD["+str(i)+"] = " + str(EidD[i] - mwid.EidD[i]))
System I consistency check between MaxwellCartesian tutorial and NRPy+ module; ALL SHOULD BE ZERO: psi_ID - mwid.psi_ID = 0 AidD[0] - mwid.AidD[0] = 0 EidD[0] - mwid.EidD[0] = 0 AidD[1] - mwid.AidD[1] = 0 EidD[1] - mwid.EidD[1] = 0 AidD[2] - mwid.AidD[2] = 0 EidD[2] - mwid.EidD[2] = 0
Finally, we will repeat the check with system II initial data.
# Reset the list of gridfunctions, as registering a gridfunction
# twice will spawn an error.
gri.glb_gridfcs_list = []
par.set_parval_from_str("System_to_use","System_II")
mwid.MaxwellCartesian_ID()
print("System II consistency check between MaxwellCartesian tutorial and NRPy+ module;\n ALL SHOULD BE ZERO:")
print("psi_ID - mwid.psi_ID = " + str(psi_ID - mwid.psi_ID))
print("Gamma_ID - mwid.Gamma_ID = " + str(Gamma_ID - mwid.Gamma_ID))
for i in range(DIM):
print("AidD["+str(i)+"] - mwid.AidD["+str(i)+"] = " + str(AidD[i] - mwid.AidD[i]))
print("EidD["+str(i)+"] - mwid.EidD["+str(i)+"] = " + str(EidD[i] - mwid.EidD[i]))
System II consistency check between MaxwellCartesian tutorial and NRPy+ module; ALL SHOULD BE ZERO: psi_ID - mwid.psi_ID = 0 Gamma_ID - mwid.Gamma_ID = 0 AidD[0] - mwid.AidD[0] = 0 EidD[0] - mwid.EidD[0] = 0 AidD[1] - mwid.AidD[1] = 0 EidD[1] - mwid.EidD[1] = 0 AidD[2] - mwid.AidD[2] = 0 EidD[2] - mwid.EidD[2] = 0
The following code cell converts this Jupyter notebook into a proper, clickable LATEX-formatted PDF file. After the cell is successfully run, the generated PDF may be found in the root NRPy+ tutorial directory, with filename Tutorial-MaxwellCartesian.pdf (Note that clicking on this link may not work; you may need to open the PDF file through another means.)
import cmdline_helper as cmd # NRPy+: Multi-platform Python command-line interface
cmd.output_Jupyter_notebook_to_LaTeXed_PDF("Tutorial-MaxwellCartesian")
Created Tutorial-MaxwellCartesian.tex, and compiled LaTeX file to PDF file Tutorial-MaxwellCartesian.pdf