#!/usr/bin/env python # coding: utf-8 # <script async src="https://www.googletagmanager.com/gtag/js?id=UA-59152712-8"></script> # <script> # window.dataLayer = window.dataLayer || []; # function gtag(){dataLayer.push(arguments);} # gtag('js', new Date()); # # gtag('config', 'UA-59152712-8'); # </script> # # # Generating C code for the right-hand sides of Maxwell's equations, in ***curvilinear*** coordinates, using a reference metric formalism # # ## Author: Ian Ruchlin # ### Formatting improvements courtesy Brandon Clark # # [comment]: <> (Abstract: TODO) # # ### The following formulations of Maxwell's equations, called System I and System II, are described in [Illustrating Stability Properties of Numerical Relativity in Electrodynamics](https://arxiv.org/abs/gr-qc/0201051) by Knapp et al. # # **Notebook Status:** <font color='red'><b> In progress </b></font> # # **Validation Notes:** This module has not yet undergone validation testing. Do ***not*** use it until after appropriate validation testing has been performed. # # ## Introduction: # [Maxwell's equations](https://en.wikipedia.org/wiki/Maxwell%27s_equations) are subject to the Gauss' law constraint # $$\mathcal{C} \equiv \hat{D}_{i} E^{i} - 4 \pi \rho = 0 \; ,$$ # where $E^{i}$ is the electric vector field, $\hat{D}_{i}$ is the [covariant derivative](https://en.wikipedia.org/wiki/Covariant_derivative) associated with the reference metric $\hat{\gamma}_{i j}$ (which is taken to represent flat space), and $\rho$ is the electric charge density. We use $\mathcal{C}$ as a measure of numerical error. Maxwell's equations are also required to satisfy $\hat{D}_{i} B^{i} = 0$, where $B^{i}$ is the magnetic vector field. The magnetic constraint implies that the magnetic field can be expressed as # $$B_{i} = \epsilon_{i j k} \hat{D}^{j} A^{k} \; ,$$ # where $\epsilon_{i j k}$ is the totally antisymmetric [Levi-Civita tensor](https://en.wikipedia.org/wiki/Levi-Civita_symbol) and $A^{i}$ is the vector potential field. Together with the scalar potential $\psi$, the electric field can be expressed in terms of the potential fields as # $$E_{i} = -\hat{D}_{i} \psi - \partial_{t} A_{i} \; .$$ # For now, we work in vacuum, where the electric charge density and the electric current density vector both vanish ($\rho = 0$ and $j_{i} = 0$). # # In addition to the Gauss constraints, the electric and magnetic fields obey two independent [electromagnetic invariants](https://en.wikipedia.org/wiki/Classification_of_electromagnetic_fields#Invariants) # \begin{align} # \mathcal{P} &\equiv B_{i} B^{i} - E_{i} E^{i} \; , \\ # \mathcal{Q} &\equiv E_{i} B^{i} \; . # \end{align} # In vacuum, these satisfy $\mathcal{P} = \mathcal{Q} = 0$. # <a id='toc'></a> # # # Table of Contents: # $$\label{toc}$$ # # This notebook is organized as follows # # 1. [Step 1](#sys1): System I # 1. [Step 2](#sys2): System II # 1. [Step 3](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file # <a id='sys1'></a> # # # Step 1: System I \[Back to [top](#toc)\] # $$\label{sys1}$$ # # In terms of the above definitions, the evolution Maxwell's equations take the form # \begin{align} # \partial_{t} A_{i} &= -E_{i} - \hat{D}_{i} \psi \; , \\ # \partial_{t} E_{i} &= -\hat{D}_{j} \hat{D}^{j} A_{i} + \hat{D}_{i} \hat{D}_{j} A^{j}\; , \\ # \partial_{t} \psi &= -\hat{D}_{i} A^{i} \; . # \end{align} # Note that this coupled system contains mixed second derivatives in the second term on the right hand side of the $E^{i}$ evolution equation. We will revisit this fact when building System II. # # It can be shown that the Gauss constraint satisfies the evolution equation # $$\partial_{t} \mathcal{C} = 0 \; .$$ # This implies that any constraint violating numerical error remains fixed in place during the evolution. This becomes problematic when the violations grow large and spoil the physics of the simulation. # In[1]: import NRPy_param_funcs as par # NRPy+: parameter interface import indexedexp as ixp # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support import grid as gri # NRPy+: Functions having to do with numerical grids import finite_difference as fin # NRPy+: Finite difference C code generation module import reference_metric as rfm # NRPy+: Reference metric support from outputC import lhrh # NRPy+: Core C code output module par.set_parval_from_str("reference_metric::CoordSystem", "Spherical") par.set_parval_from_str("grid::DIM", 3) rfm.reference_metric() # The name of this module ("maxwell") is given by __name__: thismodule = __name__ # Step 0: Read the spatial dimension parameter as DIM. 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") # 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_dHatD = ixp.zerorank2() for i in range(DIM): for j in range(DIM): AD_dHatD[i][j] = AD_dD[i][j] for k in range(DIM): AD_dHatD[i][j] -= rfm.GammahatUDD[k][i][j] * AD[k] # Second covariant derivative # D_{k} D_{j} A_{i} = \partial_{k} D_{j} A_{i} - \Gamma^{l}_{jk} D_{l} A_{i} # - \Gamma^{l}_{ik} D_{j} A_{l} # = A_{i,jk} # - \Gamma^{l}_{ij,k} A_{l} # - \Gamma^{l}_{ij} A_{l,k} # - \Gamma^{l}_{jk} A_{i;\hat{l}} # - \Gamma^{l}_{ik} A_{l;\hat{j}} AD_dHatDD = ixp.zerorank3() for i in range(DIM): for j in range(DIM): for k in range(DIM): AD_dHatDD[i][j][k] = AD_dDD[i][j][k] for l in range(DIM): AD_dHatDD[i][j][k] += - rfm.GammahatUDDdD[l][i][j][k] * AD[l] \ - rfm.GammahatUDD[l][i][j] * AD_dD[l][k] \ - rfm.GammahatUDD[l][j][k] * AD_dHatD[i][l] \ - rfm.GammahatUDD[l][i][k] * AD_dHatD[l][j] # Covariant divergence # D_{i} A^{i} = ghat^{ij} D_{j} A_{i} DivA = 0 # Gradient of covariant divergence # DivA_dD_{i} = ghat^{jk} A_{k;\hat{j}\hat{i}} DivA_dD = ixp.zerorank1() # Covariant Laplacian # LapAD_{i} = ghat^{jk} A_{i;\hat{j}\hat{k}} LapAD = ixp.zerorank1() for i in range(DIM): for j in range(DIM): DivA += rfm.ghatUU[i][j] * AD_dHatD[i][j] for k in range(DIM): DivA_dD[i] += rfm.ghatUU[j][k] * AD_dHatDD[k][j][i] LapAD[i] += rfm.ghatUU[j][k] * AD_dHatDD[i][j][k] # Step 5: Define right-hand sides for the evolution. AD_rhs = ixp.zerorank1() ED_rhs = ixp.zerorank1() for i in range(DIM): AD_rhs[i] = -ED[i] - psi_dD[i] ED_rhs[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=AD_rhs[i])) lhrh_list.append(lhrh(lhs=gri.gfaccess("rhs_gfs", "ED" + str(i)), rhs=ED_rhs[i])) lhrh_list.append(lhrh(lhs=gri.gfaccess("rhs_gfs", "psi"), rhs=psi_rhs)) fin.FD_outputC("stdout", lhrh_list) # <a id='sys2'></a> # # # Step 2: System II \[Back to [top](#toc)\] # $$\label{sys2}$$ # # Define the auxiliary variable # $$\Gamma \equiv \hat{D}_{i} A^{i} \; .$$ # Substituting this into Maxwell's equations yields the system # \begin{align} # \partial_{t} A_{i} &= -E_{i} - \hat{D}_{i} \psi \; , \\ # \partial_{t} E_{i} &= -\hat{D}_{j} \hat{D}^{j} A_{i} + \hat{D}_{i} \Gamma \; , \\ # \partial_{t} \psi &= -\Gamma \; , \\ # \partial_{t} \Gamma &= -\hat{D}_{i} \hat{D}^{i} \psi \; . # \end{align} # # # # It can be shown that the Gauss constraint now satisfies the wave equation # $$\partial_{t}^{2} \mathcal{C} = \hat{D}_{i} \hat{D}^{i} \mathcal{C} \; .$$ # Thus, any constraint violation introduced by numerical error propagates away at the speed of light. This property increases the stability of of the simulation, compared to System I above. A similar trick is used in the [BSSN formulation](Tutorial-BSSNCurvilinear.ipynb) of Einstein's equations. # In[2]: # 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_dHatDD = ixp.zerorank2() for i in range(DIM): for j in range(DIM): psi_dHatDD[i][j] = psi_dDD[i][j] for k in range(DIM): psi_dHatDD[i][j] += - rfm.GammahatUDD[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 += rfm.ghatUU[i][j] * psi_dHatDD[i][j] # Step 9: Define right-hand sides for the evolution. AD_rhs = ixp.zerorank1() ED_rhs = ixp.zerorank1() for i in range(DIM): AD_rhs[i] = -ED[i] - psi_dD[i] ED_rhs[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=AD_rhs[i])) lhrh_list.append(lhrh(lhs=gri.gfaccess("rhs_gfs", "ED" + str(i)), rhs=ED_rhs[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) # <a id='latex_pdf_output'></a> # # # Step 3: Output this notebook to $\LaTeX$-formatted PDF file \[Back to [top](#toc)\] # $$\label{latex_pdf_output}$$ # # The following code cell converts this Jupyter notebook into a proper, clickable $\LaTeX$-formatted PDF file. After the cell is successfully run, the generated PDF may be found in the root NRPy+ tutorial directory, with filename # [Tutorial-MaxwellCurvilinear.pdf](Tutorial-MaxwellCurvilinear.pdf) (Note that clicking on this link may not work; you may need to open the PDF file through another means.) # In[3]: import cmdline_helper as cmd # NRPy+: Multi-platform Python command-line interface cmd.output_Jupyter_notebook_to_LaTeXed_PDF("Tutorial-MaxwellCurvilinear")