Notebook Status: Self-Validated
Validation Notes: This tutorial notebook has been confirmed to be self-consistent with its corresponding NRPy+ module, as documented below. Additional validation tests may have been performed, but are as yet, undocumented. (TODO)
As is standard in NRPy+,
As a corollary, any expressions involving mixed Greek and Latin indices will need to offset one set of indices by one: A Latin index in a four-vector will be incremented and a Greek index in a three-vector will be decremented (however, the latter case does not occur in this tutorial notebook).
Each family of quantities is constructed within a given function (boldfaced below). This notebook is organized as follows
declare_BSSN_gridfunctions_if_not_declared_already()
: Declare all of the core BSSN variables {hij,aij,cf,K,λi,α,Vi,Bi} and register them as gridfunctionsBSSN_basic_tensors()
: Define all basic conformal BSSN tensors {ˉγij,ˉAij,ˉΛi,βi,Bi} in terms of BSSN gridfunctionsgammabar__inverse_and_derivs()
: ˉγij, and spatial derivatives of ˉγij including ˉΓijk
detgammabar_and_derivs()
: detˉγij and its derivativesAbarUU_AbarUD_trAbar()
: Quantities related to conformal traceless extrinsic curvature ˉAij: ˉAij, ˉAij, and ˉAkkRicciBar__gammabarDD_dHatD__DGammaUDD__DGammaU()
: The conformal ("barred") Ricci tensor ˉRij and associated quantities
betaU_derivs()
: Unrescaled shift vector βi and spatial derivatives βi,j and βi,jkphi_and_derivs()
: Standard BSSN conformal factor ϕ, and its derivatives ϕ,i, ϕ,ij, ˉDjϕ, and ˉDjˉDkϕ
BSSN.BSSN_quantities
NRPy+ module# Step 1: Import all needed modules from NRPy+:
import NRPy_param_funcs as par
import sympy as sp
import indexedexp as ixp
import grid as gri
import reference_metric as rfm
import sys
# Step 1.a: Set the coordinate system for the numerical grid
par.set_parval_from_str("reference_metric::CoordSystem","Spherical")
# Step 1.b: Given the chosen coordinate system, set up
# corresponding reference metric and needed
# reference metric quantities
# The following function call sets up the reference metric
# and related quantities, including rescaling matrices ReDD,
# ReU, and hatted quantities.
rfm.reference_metric()
# Step 1.c: Set spatial dimension (must be 3 for BSSN, as BSSN is
# a 3+1-dimensional decomposition of the general
# relativistic field equations)
DIM = 3
par.set_parval_from_str("grid::DIM",DIM)
# Step 1.d: Declare/initialize parameters for this module
thismodule = "BSSN_quantities"
par.initialize_param(par.glb_param("char", thismodule, "EvolvedConformalFactor_cf", "W"))
par.initialize_param(par.glb_param("bool", thismodule, "detgbarOverdetghat_equals_one", "True"))
# Step 2: Register all needed BSSN gridfunctions.
# Step 2.a: Register indexed quantities, using ixp.register_... functions
hDD = ixp.register_gridfunctions_for_single_rank2("EVOL", "hDD", "sym01")
aDD = ixp.register_gridfunctions_for_single_rank2("EVOL", "aDD", "sym01")
lambdaU = ixp.register_gridfunctions_for_single_rank1("EVOL", "lambdaU")
vetU = ixp.register_gridfunctions_for_single_rank1("EVOL", "vetU")
betU = ixp.register_gridfunctions_for_single_rank1("EVOL", "betU")
# Step 2.b: Register scalar quantities, using gri.register_gridfunctions()
trK, cf, alpha = gri.register_gridfunctions("EVOL",["trK", "cf", "alpha"])
While the covariant form of the BSSN evolution equations are properly covariant (with the potential exception of the shift evolution equation, since the shift is a freely specifiable gauge quantity), components of the rank-1 and rank-2 tensors εij, ˉAij, and ˉΛi will drop to zero (destroying information) or diverge (to ∞) at coordinate singularities.
The good news is, this singular behavior is well-understood in terms of the scale factors of the reference metric, enabling us to define rescaled version of these quantities that are well behaved (so that, e.g., they can be finite differenced).
For example, given a smooth vector in a 3D Cartesian basis ˉΛi, all components ˉΛx, ˉΛy, and ˉΛz will be smooth (by assumption). When changing the basis to spherical coordinates (applying the appropriate Jacobian matrix transformation), we will find that since ϕ=arctan(y/x), ˉΛϕ is given by
ˉΛϕ=∂ϕ∂xˉΛx+∂ϕ∂yˉΛy+∂ϕ∂zˉΛz=−yx2+y2ˉΛx+xx2+y2ˉΛy=−y(rsinθ)2ˉΛx+x(rsinθ)2ˉΛy.Thus ˉΛϕ diverges at all points where rsinθ=0 (or equivalently where x=y=0; i.e., the z-axis) due to the 1(rsinθ)2 that appear in the Jacobian transformation.
This divergence might pose no problem on cell-centered grids that avoid rsinθ=0, except that the BSSN equations require that first and second derivatives of these quantities be taken. Usual strategies for numerical approximation of these derivatives (e.g., finite difference methods) will "see" these divergences and errors generally will not drop to zero with increased numerical sampling of the functions at points near where the functions diverge.
However, notice that if we define λϕ such that
ˉΛϕ=1rsinθλϕ,then λϕ will be smooth as well.
Avoiding such singularities can be generalized to other coordinate systems, so long as λi is defined as:
ˉΛi=λiscalefactor[i],where scalefactor[i] is the ith scale factor in the given coordinate system. In an identical fashion, we define the smooth versions of βi and Bi to be Vi and Bi, respectively. We refer to Vi and Bi as vet[i] and bet[i] respectively in the code after the Hebrew letters that bear some resemblance.
Similarly, we define the smooth versions of ˉAij and εij (aij and hij, respectively) via
ˉAij=scalefactor[i] scalefactor[j] aijεij=scalefactor[i] scalefactor[j] hij,where in this case we multiply due to the fact that these tensors are purely covariant (as opposed to contravariant). To slightly simplify the notation, in NRPy+ we define the rescaling matrices ReU[i]
and ReDD[i][j]
, such that
Thus, for example, ˉAij and ˉΛi can be expressed as the Hadamard product of matrices :
ˉAij=ReDD∘a=ReDD[i][j]aijˉΛi=ReU∘λ=ReU[i]λi,where no sums are implied by the repeated indices.
Further, since the scale factors are time-independent,
∂tˉAij=ReDD[i][j] ∂taij∂tˉγij=∂t(εij+ˆγij)=∂tεij=scalefactor[i] scalefactor[j] ∂thij.Thus instead of taking space or time derivatives of BSSN quantities
{ˉγij,ˉAij,ϕ,K,ˉΛi,α,βi,Bi},across coordinate singularities, we instead factor out the singular scale factors according to this prescription so that space or time derivatives of BSSN quantities are written in terms of finite-difference derivatives of the rescaled variables
{hij,aij,cf,K,λi,α,Vi,Bi},and exact expressions for (spatial) derivatives of scale factors. Note that cf
is the chosen conformal factor (supported choices for cf
are discussed in Step 6.a).
As an example, let's evaluate ˉΛi,j according to this prescription:
ˉΛi,j=−λi(ReU[i])2 ∂j(ReU[i])+∂jλiReU[i]=−λi(ReU[i])2 ReUdD[i][j]+∂jλiReU[i].Here, the derivative ReUdD[i][j]
is computed symbolically and exactly using SymPy, and the derivative ∂jλi represents a derivative of a smooth quantity (so long as ˉΛi is smooth in the Cartesian basis).
BSSN_basic_tensors()
: Define all basic conformal BSSN tensors {ˉγij,ˉAij,ˉΛi,βi,Bi} in terms of BSSN gridfunctions [Back to top]¶The BSSN_vars__tensors()
function defines the tensorial BSSN quantities {ˉγij,ˉAij,ˉΛi,βi,Bi}, in terms of the rescaled "base" tensorial quantities {hij,aij,λi,Vi,Bi}, respectively:
Rescaling vectors and tensors are built upon the scale factors for the chosen (in a general, singular) coordinate system, which is defined in NRPy+'s reference_metric.py (Tutorial), and the rescaled variables are defined in the stub function BSSN/BSSN_rescaled_vars.py.
Here we implement BSSN_vars__tensors()
:
# Step 3.a: Define all basic conformal BSSN tensors in terms of BSSN gridfunctions
# Step 3.a.i: gammabarDD and AbarDD:
gammabarDD = ixp.zerorank2()
AbarDD = ixp.zerorank2()
for i in range(DIM):
for j in range(DIM):
# gammabar_{ij} = h_{ij}*ReDD[i][j] + gammahat_{ij}
gammabarDD[i][j] = hDD[i][j]*rfm.ReDD[i][j] + rfm.ghatDD[i][j]
# Abar_{ij} = a_{ij}*ReDD[i][j]
AbarDD[i][j] = aDD[i][j]*rfm.ReDD[i][j]
# Step 3.a.ii: LambdabarU, betaU, and BU:
LambdabarU = ixp.zerorank1()
betaU = ixp.zerorank1()
BU = ixp.zerorank1()
for i in range(DIM):
LambdabarU[i] = lambdaU[i]*rfm.ReU[i]
betaU[i] = vetU[i] *rfm.ReU[i]
BU[i] = betU[i] *rfm.ReU[i]
# Step 4.a: Inverse conformal 3-metric gammabarUU:
# Step 4.a.i: gammabarUU:
gammabarUU, dummydet = ixp.symm_matrix_inverter3x3(gammabarDD)
In the BSSN-in-curvilinear coordinates formulation, all quantities must be defined in terms of rescaled quantities hij and their derivatives (evaluated using finite differences), as well as reference-metric quantities and their derivatives (evaluated exactly using SymPy).
For example, ˉγij,k is given by: ˉγij,k=∂kˉγij=∂k(ˆγij+εij)=∂k(ˆγij+hijReDD[i][j])=ˆγij,k+hij,kReDD[i][j]+hijReDDdD[i][j][k],
ReDDdD[i][j][k]
is computed within rfm.reference_metric()
.
# Step 4.b.i gammabarDDdD[i][j][k]
# = \hat{\gamma}_{ij,k} + h_{ij,k} \text{ReDD[i][j]} + h_{ij} \text{ReDDdD[i][j][k]}.
gammabarDD_dD = ixp.zerorank3()
hDD_dD = ixp.declarerank3("hDD_dD","sym01")
hDD_dupD = ixp.declarerank3("hDD_dupD","sym01")
gammabarDD_dupD = ixp.zerorank3()
for i in range(DIM):
for j in range(DIM):
for k in range(DIM):
gammabarDD_dD[i][j][k] = rfm.ghatDDdD[i][j][k] + \
hDD_dD[i][j][k]*rfm.ReDD[i][j] + hDD[i][j]*rfm.ReDDdD[i][j][k]
# Compute associated upwinded derivative, needed for the \bar{\gamma}_{ij} RHS
gammabarDD_dupD[i][j][k] = rfm.ghatDDdD[i][j][k] + \
hDD_dupD[i][j][k]*rfm.ReDD[i][j] + hDD[i][j]*rfm.ReDDdD[i][j][k]
By extension, the second derivative ˉγij,kl is given by ˉγij,kl=∂l(ˆγij,k+hij,kReDD[i][j]+hijReDDdD[i][j][k])=ˆγij,kl+hij,klReDD[i][j]+hij,kReDDdD[i][j][l]+hij,lReDDdD[i][j][k]+hijReDDdDD[i][j][k][l]
# Step 4.b.ii: Compute gammabarDD_dDD in terms of the rescaled BSSN quantity hDD
# and its derivatives, as well as the reference metric and rescaling
# matrix, and its derivatives (expression given below):
hDD_dDD = ixp.declarerank4("hDD_dDD","sym01_sym23")
gammabarDD_dDD = ixp.zerorank4()
for i in range(DIM):
for j in range(DIM):
for k in range(DIM):
for l in range(DIM):
# gammabar_{ij,kl} = gammahat_{ij,kl}
# + h_{ij,kl} ReDD[i][j]
# + h_{ij,k} ReDDdD[i][j][l] + h_{ij,l} ReDDdD[i][j][k]
# + h_{ij} ReDDdDD[i][j][k][l]
gammabarDD_dDD[i][j][k][l] = rfm.ghatDDdDD[i][j][k][l]
gammabarDD_dDD[i][j][k][l] += hDD_dDD[i][j][k][l]*rfm.ReDD[i][j]
gammabarDD_dDD[i][j][k][l] += hDD_dD[i][j][k]*rfm.ReDDdD[i][j][l] + \
hDD_dD[i][j][l]*rfm.ReDDdD[i][j][k]
gammabarDD_dDD[i][j][k][l] += hDD[i][j]*rfm.ReDDdDD[i][j][k][l]
Finally, we compute the Christoffel symbol associated with the barred 3-metric: ˉΓikl: ˉΓikl=12ˉγim(ˉγmk,l+ˉγml,k−ˉγkl,m)
# Step 4.b.iii: Define barred Christoffel symbol \bar{\Gamma}^{i}_{kl} = GammabarUDD[i][k][l] (see expression below)
GammabarUDD = ixp.zerorank3()
for i in range(DIM):
for k in range(DIM):
for l in range(DIM):
for m in range(DIM):
# Gammabar^i_{kl} = 1/2 * gammabar^{im} ( gammabar_{mk,l} + gammabar_{ml,k} - gammabar_{kl,m}):
GammabarUDD[i][k][l] += sp.Rational(1,2)*gammabarUU[i][m]* \
(gammabarDD_dD[m][k][l] + gammabarDD_dD[m][l][k] - gammabarDD_dD[k][l][m])
detgammabar_and_derivs()
: detˉγij and its derivatives [Back to top]¶As described just before Section III of Baumgarte et al (2012), we are free to choose detˉγij, which should remain fixed in time.
As in Baumgarte et al (2012) generally we make the choice detˉγij=detˆγij, but this need not be the case; we could choose to set detˉγij to another expression.
In case we do not choose to set detˉγij/detˆγij=1, below we begin the implementation of a gridfunction, detgbarOverdetghat
, which defines an alternative expression in its place.
detˉγij/detˆγij=detgbarOverdetghat
≠1 is not yet implemented. However, we can define detgammabar
and its derivatives in terms of a generic detgbarOverdetghat
and detˆγij and their derivatives:
https://en.wikipedia.org/wiki/Determinant#Properties_of_the_determinant
# Step 5: det(gammabarDD) and its derivatives
detgbarOverdetghat = sp.sympify(1)
detgbarOverdetghat_dD = ixp.zerorank1()
detgbarOverdetghat_dDD = ixp.zerorank2()
if par.parval_from_str(thismodule+"::detgbarOverdetghat_equals_one") == "False":
print("Error: detgbarOverdetghat_equals_one=\"False\" is not fully implemented yet.")
sys.exit(1)
## Approach for implementing detgbarOverdetghat_equals_one=False:
# detgbarOverdetghat = gri.register_gridfunctions("AUX", ["detgbarOverdetghat"])
# detgbarOverdetghatInitial = gri.register_gridfunctions("AUX", ["detgbarOverdetghatInitial"])
# detgbarOverdetghat_dD = ixp.declarerank1("detgbarOverdetghat_dD")
# detgbarOverdetghat_dDD = ixp.declarerank2("detgbarOverdetghat_dDD", "sym01")
# Step 5.b: Define detgammabar, detgammabar_dD, and detgammabar_dDD (needed for
# \partial_t \bar{\Lambda}^i below)detgammabar = detgbarOverdetghat * rfm.detgammahat
detgammabar = detgbarOverdetghat * rfm.detgammahat
detgammabar_dD = ixp.zerorank1()
for i in range(DIM):
detgammabar_dD[i] = detgbarOverdetghat_dD[i] * rfm.detgammahat + detgbarOverdetghat * rfm.detgammahatdD[i]
detgammabar_dDD = ixp.zerorank2()
for i in range(DIM):
for j in range(DIM):
detgammabar_dDD[i][j] = detgbarOverdetghat_dDD[i][j] * rfm.detgammahat + \
detgbarOverdetghat_dD[i] * rfm.detgammahatdD[j] + \
detgbarOverdetghat_dD[j] * rfm.detgammahatdD[i] + \
detgbarOverdetghat * rfm.detgammahatdDD[i][j]
AbarUU_AbarUD_trAbar_AbarDD_dD()
: Quantities related to conformal traceless extrinsic curvature ˉAij: ˉAij, ˉAij, and ˉAkk [Back to top]¶ˉAij is given by application of the raising operators (a.k.a., the inverse 3-metric) ˉγjk on both of the covariant ("down") components: ˉAij=ˉγikˉγjlˉAkl.
ˉAij is given by a single application of the raising operator (a.k.a., the inverse 3-metric) ˉγik on ˉAkj: ˉAij=ˉγikˉAkj.
The trace of ˉAij, ˉAkk, is given by a contraction with the barred 3-metric: Tr(ˉAij)=ˉAkk=ˉγkjˉAjk.
Note that while ˉAij is defined as the traceless conformal extrinsic curvature, it may acquire a nonzero trace (assuming the initial data impose tracelessness) due to numerical error. Tr(ˉAij) is included in the BSSN equations to drive Tr(ˉAij) to zero.
In terms of rescaled BSSN quantities, ˉAij is given by ˉAij=ReDD[i][j]aij,
# Step 6: Quantities related to conformal traceless extrinsic curvature
# Step 6.a.i: Compute Abar^{ij} in terms of Abar_{ij} and gammabar^{ij}
AbarUU = ixp.zerorank2()
for i in range(DIM):
for j in range(DIM):
for k in range(DIM):
for l in range(DIM):
# Abar^{ij} = gammabar^{ik} gammabar^{jl} Abar_{kl}
AbarUU[i][j] += gammabarUU[i][k]*gammabarUU[j][l]*AbarDD[k][l]
# Step 6.a.ii: Compute Abar^i_j in terms of Abar_{ij} and gammabar^{ij}
AbarUD = ixp.zerorank2()
for i in range(DIM):
for j in range(DIM):
for k in range(DIM):
# Abar^i_j = gammabar^{ik} Abar_{kj}
AbarUD[i][j] += gammabarUU[i][k]*AbarDD[k][j]
# Step 6.a.iii: Compute Abar^k_k = trace of Abar:
trAbar = sp.sympify(0)
for k in range(DIM):
for j in range(DIM):
# Abar^k_k = gammabar^{kj} Abar_{jk}
trAbar += gammabarUU[k][j]*AbarDD[j][k]
# Step 6.a.iv: Compute Abar_{ij,k}
AbarDD_dD = ixp.zerorank3()
AbarDD_dupD = ixp.zerorank3()
aDD_dD = ixp.declarerank3("aDD_dD" ,"sym01")
aDD_dupD = ixp.declarerank3("aDD_dupD","sym01")
for i in range(DIM):
for j in range(DIM):
for k in range(DIM):
AbarDD_dupD[i][j][k] = rfm.ReDDdD[i][j][k]*aDD[i][j] + rfm.ReDD[i][j]*aDD_dupD[i][j][k]
AbarDD_dD[i][j][k] = rfm.ReDDdD[i][j][k]*aDD[i][j] + rfm.ReDD[i][j]*aDD_dD[ i][j][k]
RicciBar__gammabarDD_dHatD__DGammaUDD__DGammaU()
: The conformal ("barred") Ricci tensor ˉRij and associated quantities [Back to top]¶Let's compute perhaps the most complicated expression in the BSSN evolution equations, the conformal Ricci tensor:
ˉRij=−12ˉγklˆDkˆDlˉγij+ˉγk(iˆDj)ˉΛk+ΔkΔ(ij)k+ˉγkl(2Δmk(iΔj)ml+ΔmikΔmjl).Let's tackle the ˆDkˆDlˉγij term first:
First note that the covariant derivative of a metric with respect to itself is zero ˆDlˆγij=0,
Next, the covariant derivative of a tensor is given by (from the wikipedia article on covariant differentiation): (∇ecT)a1…arb1…bs=∂∂xcTa1…arb1…bs+Γa1dcTda2…arb1…bs+⋯+ΓardcTa1…ar−1db1…bs−Γdb1cTa1…ardb2…bs−⋯−ΓdbscTa1…arb1…bs−1d.
Therefore, ˆDlˉγij=ˆDlεij=εij,l−ˆΓmilεmj−ˆΓmjlεim.
Since the covariant first derivative is a tensor, the covariant second derivative is given by (same as Eq. 27 in Baumgarte et al (2012))
ˆDkˆDlˉγij=ˆDkˆDlεij=∂kˆDlεij−ˆΓmlk(ˆDmεij)−ˆΓmik(ˆDlεmj)−ˆΓmjk(ˆDlεim),where the first term is the partial derivative of the expression already derived for ˆDlεij:
∂kˆDlεij=∂k(εij,l−ˆΓmilεmj−ˆΓmjlεim)=εij,lk−ˆΓmil,kεmj−ˆΓmilεmj,k−ˆΓmjl,kεim−ˆΓmjlεim,k.In terms of the evolved quantity hij, the derivatives of εij are given by: εij,k=∂k(hijReDD[i][j])=hij,kReDD[i][j]+hijReDDdD[i][j][k],
# Step 7: Conformal Ricci tensor, part 1: The \hat{D}_{k} \hat{D}_{l} \bar{\gamma}_{i j} term
# Step 7.a.i: Define \varepsilon_{ij} = epsDD[i][j]
epsDD = ixp.zerorank3()
for i in range(DIM):
for j in range(DIM):
epsDD[i][j] = hDD[i][j]*rfm.ReDD[i][j]
# Step 7.a.ii: Define epsDD_dD[i][j][k]
hDD_dD = ixp.declarerank3("hDD_dD","sym01")
epsDD_dD = ixp.zerorank3()
for i in range(DIM):
for j in range(DIM):
for k in range(DIM):
epsDD_dD[i][j][k] = hDD_dD[i][j][k]*rfm.ReDD[i][j] + hDD[i][j]*rfm.ReDDdD[i][j][k]
# Step 7.a.iii: Define epsDD_dDD[i][j][k][l]
hDD_dDD = ixp.declarerank4("hDD_dDD","sym01_sym23")
epsDD_dDD = ixp.zerorank4()
for i in range(DIM):
for j in range(DIM):
for k in range(DIM):
for l in range(DIM):
epsDD_dDD[i][j][k][l] = hDD_dDD[i][j][k][l]*rfm.ReDD[i][j] + \
hDD_dD[i][j][k]*rfm.ReDDdD[i][j][l] + \
hDD_dD[i][j][l]*rfm.ReDDdD[i][j][k] + \
hDD[i][j]*rfm.ReDDdDD[i][j][k][l]
We next compute three quantities derived above:
gammabarDD_DhatD[i][j][l]
= ˆDlˉγij=ˆDlεij=εij,l−ˆΓmilεmj−ˆΓmjlεim,gammabarDD_DhatD\_dD[i][j][l][k]
= ∂kˆDlˉγij=∂kˆDlεij=εij,lk−ˆΓmil,kεmj−ˆΓmilεmj,k−ˆΓmjl,kεim−ˆΓmjlεim,k, andgammabarDD_DhatDD[i][j][l][k]
= ˆDkˆDlˉγij=∂kˆDlεij−ˆΓmlk(ˆDmεij)−ˆΓmik(ˆDlεmj)−ˆΓmjk(ˆDlεim).# Step 7.a.iv: DhatgammabarDDdD[i][j][l] = \bar{\gamma}_{ij;\hat{l}}
# \bar{\gamma}_{ij;\hat{l}} = \varepsilon_{i j,l}
# - \hat{\Gamma}^m_{i l} \varepsilon_{m j}
# - \hat{\Gamma}^m_{j l} \varepsilon_{i m}
gammabarDD_dHatD = ixp.zerorank3()
for i in range(DIM):
for j in range(DIM):
for l in range(DIM):
gammabarDD_dHatD[i][j][l] = epsDD_dD[i][j][l]
for m in range(DIM):
gammabarDD_dHatD[i][j][l] += - rfm.GammahatUDD[m][i][l]*epsDD[m][j] \
- rfm.GammahatUDD[m][j][l]*epsDD[i][m]
# Step 7.a.v: \bar{\gamma}_{ij;\hat{l},k} = DhatgammabarDD_dHatD_dD[i][j][l][k]:
# \bar{\gamma}_{ij;\hat{l},k} = \varepsilon_{ij,lk}
# - \hat{\Gamma}^m_{i l,k} \varepsilon_{m j}
# - \hat{\Gamma}^m_{i l} \varepsilon_{m j,k}
# - \hat{\Gamma}^m_{j l,k} \varepsilon_{i m}
# - \hat{\Gamma}^m_{j l} \varepsilon_{i m,k}
gammabarDD_dHatD_dD = ixp.zerorank4()
for i in range(DIM):
for j in range(DIM):
for l in range(DIM):
for k in range(DIM):
gammabarDD_dHatD_dD[i][j][l][k] = epsDD_dDD[i][j][l][k]
for m in range(DIM):
gammabarDD_dHatD_dD[i][j][l][k] += -rfm.GammahatUDDdD[m][i][l][k]*epsDD[m][j] \
-rfm.GammahatUDD[m][i][l]*epsDD_dD[m][j][k] \
-rfm.GammahatUDDdD[m][j][l][k]*epsDD[i][m] \
-rfm.GammahatUDD[m][j][l]*epsDD_dD[i][m][k]
# Step 7.a.vi: \bar{\gamma}_{ij;\hat{l}\hat{k}} = DhatgammabarDD_dHatDD[i][j][l][k]
# \bar{\gamma}_{ij;\hat{l}\hat{k}} = \partial_k \hat{D}_{l} \varepsilon_{i j}
# - \hat{\Gamma}^m_{lk} \left(\hat{D}_{m} \varepsilon_{i j}\right)
# - \hat{\Gamma}^m_{ik} \left(\hat{D}_{l} \varepsilon_{m j}\right)
# - \hat{\Gamma}^m_{jk} \left(\hat{D}_{l} \varepsilon_{i m}\right)
gammabarDD_dHatDD = ixp.zerorank4()
for i in range(DIM):
for j in range(DIM):
for l in range(DIM):
for k in range(DIM):
gammabarDD_dHatDD[i][j][l][k] = gammabarDD_dHatD_dD[i][j][l][k]
for m in range(DIM):
gammabarDD_dHatDD[i][j][l][k] += - rfm.GammahatUDD[m][l][k]*gammabarDD_dHatD[i][j][m] \
- rfm.GammahatUDD[m][i][k]*gammabarDD_dHatD[m][j][l] \
- rfm.GammahatUDD[m][j][k]*gammabarDD_dHatD[i][m][l]
By definition, the index symmetrization operation is given by: ˉγk(iˆDj)ˉΛk=12(ˉγkiˆDjˉΛk+ˉγkjˆDiˉΛk),
and ˉγij is trivially computed (=εij+ˆγij) so the only nontrival part to computing this term is in evaluating ˆDjˉΛk.
The covariant derivative is with respect to the hatted metric (i.e. the reference metric), so ˆDjˉΛk=∂jˉΛk+ˆΓkmjˉΛm,
Then the expression for ˆDjˉΛk becomes ˆDjˉΛk=λk,jReU[k]+λkReUdD[k][j]+ˆΓkmjλmReU[m],
# Step 7.b: Second term of RhatDD: compute \hat{D}_{j} \bar{\Lambda}^{k} = LambarU_dHatD[k][j]
lambdaU_dD = ixp.declarerank2("lambdaU_dD","nosym")
LambarU_dHatD = ixp.zerorank2()
for j in range(DIM):
for k in range(DIM):
LambarU_dHatD[k][j] = lambdaU_dD[k][j]*rfm.ReU[k] + lambdaU[k]*rfm.ReUdD[k][j]
for m in range(DIM):
LambarU_dHatD[k][j] += rfm.GammahatUDD[k][m][j]*lambdaU[m]*rfm.ReU[m]
Our goal here is to compute the quantities appearing as the final terms of the conformal Ricci tensor: ΔkΔ(ij)k+ˉγkl(2Δmk(iΔj)ml+ΔmikΔmjl).
DGammaUDD[k][i][j]
=Δkij is simply the difference in Christoffel symbols: Δkij=ˉΓijk−ˆΓijk, andDGammaU[k]
=Δk is the contraction: ˉγijΔkijAdding these expressions to Ricci is straightforward, since ˉΓijk and ˉγij were defined above in Step 4, and ˆΓijk was computed within NRPy+'s reference_metric()
function:
# Step 7.c: Conformal Ricci tensor, part 3: The \Delta^{k} \Delta_{(i j) k}
# + \bar{\gamma}^{k l}*(2 \Delta_{k(i}^{m} \Delta_{j) m l}
# + \Delta_{i k}^{m} \Delta_{m j l}) terms
# Step 7.c.i: Define \Delta^i_{jk} = \bar{\Gamma}^i_{jk} - \hat{\Gamma}^i_{jk} = DGammaUDD[i][j][k]
DGammaUDD = ixp.zerorank3()
for i in range(DIM):
for j in range(DIM):
for k in range(DIM):
DGammaUDD[i][j][k] = GammabarUDD[i][j][k] - rfm.GammahatUDD[i][j][k]
# Step 7.c.ii: Define \Delta^i = \bar{\gamma}^{jk} \Delta^i_{jk}
DGammaU = ixp.zerorank1()
for i in range(DIM):
for j in range(DIM):
for k in range(DIM):
DGammaU[i] += gammabarUU[j][k] * DGammaUDD[i][j][k]
Next we define Δijk=ˉγimΔmjk:
# Step 7.c.iii: Define \Delta_{ijk} = \bar{\gamma}_{im} \Delta^m_{jk}
DGammaDDD = ixp.zerorank3()
for i in range(DIM):
for j in range(DIM):
for k in range(DIM):
for m in range(DIM):
DGammaDDD[i][j][k] += gammabarDD[i][m] * DGammaUDD[m][j][k]
# Step 7.d: Summing the terms and defining \bar{R}_{ij}
# Step 7.d.i: Add the first term to RbarDD:
# Rbar_{ij} += - \frac{1}{2} \bar{\gamma}^{k l} \hat{D}_{k} \hat{D}_{l} \bar{\gamma}_{i j}
RbarDD = ixp.zerorank2()
RbarDDpiece = ixp.zerorank2()
for i in range(DIM):
for j in range(DIM):
for k in range(DIM):
for l in range(DIM):
RbarDD[i][j] += -sp.Rational(1,2) * gammabarUU[k][l]*gammabarDD_dHatDD[i][j][l][k]
RbarDDpiece[i][j] += -sp.Rational(1,2) * gammabarUU[k][l]*gammabarDD_dHatDD[i][j][l][k]
# Step 7.d.ii: Add the second term to RbarDD:
# Rbar_{ij} += (1/2) * (gammabar_{ki} Lambar^k_{;\hat{j}} + gammabar_{kj} Lambar^k_{;\hat{i}})
for i in range(DIM):
for j in range(DIM):
for k in range(DIM):
RbarDD[i][j] += sp.Rational(1,2) * (gammabarDD[k][i]*LambarU_dHatD[k][j] + \
gammabarDD[k][j]*LambarU_dHatD[k][i])
# Step 7.d.iii: Add the remaining term to RbarDD:
# Rbar_{ij} += \Delta^{k} \Delta_{(i j) k} = 1/2 \Delta^{k} (\Delta_{i j k} + \Delta_{j i k})
for i in range(DIM):
for j in range(DIM):
for k in range(DIM):
RbarDD[i][j] += sp.Rational(1,2) * DGammaU[k] * (DGammaDDD[i][j][k] + DGammaDDD[j][i][k])
# Step 7.d.iv: Add the final term to RbarDD:
# Rbar_{ij} += \bar{\gamma}^{k l} (\Delta^{m}_{k i} \Delta_{j m l}
# + \Delta^{m}_{k j} \Delta_{i m l}
# + \Delta^{m}_{i k} \Delta_{m j l})
for i in range(DIM):
for j in range(DIM):
for k in range(DIM):
for l in range(DIM):
for m in range(DIM):
RbarDD[i][j] += gammabarUU[k][l] * (DGammaUDD[m][k][i]*DGammaDDD[j][m][l] +
DGammaUDD[m][k][j]*DGammaDDD[i][m][l] +
DGammaUDD[m][i][k]*DGammaDDD[m][j][l])
betaU_derivs()
: The unrescaled shift vector βi spatial derivatives: βi,j & βi,jk, written in terms of the rescaled shift vector Vi [Back to top]¶This step, which documents the function betaUbar_and_derivs()
inside the BSSN.BSSN_unrescaled_and_barred_vars module, defines three quantities:
betaU_dD[i][j]
=βi,j=(Vi∘ReU[i]),j=Vi,j∘ReU[i]+Vi∘ReUdD[i][j]betaU_dupD[i][j]
: the same as above, except using upwinded finite-difference derivatives to compute Vi,j instead of centered finite-difference derivatives.betaU_dDD[i][j][k]
=βi,jk=Vi,jk∘ReU[i]+Vi,j∘ReUdD[i][k]+Vi,k∘ReUdD[i][j]+Vi∘ReUdDD[i][j][k]# Step 8: The unrescaled shift vector betaU spatial derivatives:
# betaUdD & betaUdDD, written in terms of the
# rescaled shift vector vetU
vetU_dD = ixp.declarerank2("vetU_dD","nosym")
vetU_dupD = ixp.declarerank2("vetU_dupD","nosym") # Needed for upwinded \beta^i_{,j}
vetU_dDD = ixp.declarerank3("vetU_dDD","sym12") # Needed for \beta^i_{,j}
betaU_dD = ixp.zerorank2()
betaU_dupD = ixp.zerorank2() # Needed for, e.g., \beta^i RHS
betaU_dDD = ixp.zerorank3() # Needed for, e.g., \bar{\Lambda}^i RHS
for i in range(DIM):
for j in range(DIM):
betaU_dD[i][j] = vetU_dD[i][j]*rfm.ReU[i] + vetU[i]*rfm.ReUdD[i][j]
betaU_dupD[i][j] = vetU_dupD[i][j]*rfm.ReU[i] + vetU[i]*rfm.ReUdD[i][j] # Needed for \beta^i RHS
for k in range(DIM):
# Needed for, e.g., \bar{\Lambda}^i RHS:
betaU_dDD[i][j][k] = vetU_dDD[i][j][k]*rfm.ReU[i] + vetU_dD[i][j]*rfm.ReUdD[i][k] + \
vetU_dD[i][k]*rfm.ReUdD[i][j] + vetU[i]*rfm.ReUdDD[i][j][k]
When solving the BSSN time evolution equations across the coordinate singularity (i.e., the "puncture") inside puncture black holes for example, the standard conformal factor ϕ becomes very sharp, whereas χ=e−4ϕ is far smoother (see, e.g., Campanelli, Lousto, Marronetti, and Zlochower (2006) for additional discussion). Thus if we choose to rewrite derivatives of ϕ in the BSSN equations in terms of finite-difference derivatives cf
=χ, numerical errors will be far smaller near the puncture.
The BSSN modules in NRPy+ support three options for the conformal factor variable cf
:
cf
=ϕ,cf
=χ=e−4ϕ, andcf
=W=e−2ϕ.The BSSN equations are written in terms of ϕ (actually only e−4ϕ appears) and derivatives of ϕ, we now define e−4ϕ and derivatives of ϕ in terms of the chosen cf
.
First, we define the base variables needed within the BSSN equations:
# Step 9: Standard BSSN conformal factor phi,
# and its partial and covariant derivatives,
# all in terms of BSSN gridfunctions like cf
# Step 9.a.i: Define partial derivatives of \phi in terms of evolved quantity "cf":
cf_dD = ixp.declarerank1("cf_dD")
cf_dupD = ixp.declarerank1("cf_dupD") # Needed for \partial_t \phi next.
cf_dDD = ixp.declarerank2("cf_dDD","sym01")
phi_dD = ixp.zerorank1()
phi_dupD = ixp.zerorank1()
phi_dDD = ixp.zerorank2()
exp_m4phi = sp.sympify(0)
Then we define ϕ,i, ϕ,ij, and e−4ϕ for each of the choices of cf
.
For cf
=ϕ, this is trivial:
# Step 9.a.ii: Assuming cf=phi, define exp_m4phi, phi_dD,
# phi_dupD (upwind finite-difference version of phi_dD), and phi_DD
if par.parval_from_str(thismodule+"::EvolvedConformalFactor_cf") == "phi":
for i in range(DIM):
phi_dD[i] = cf_dD[i]
phi_dupD[i] = cf_dupD[i]
for j in range(DIM):
phi_dDD[i][j] = cf_dDD[i][j]
exp_m4phi = sp.exp(-4*cf)
For cf
=W=e−2ϕ, we have
*Exercise to student: Prove the above relations*
# Step 9.a.iii: Assuming cf=W=e^{-2 phi}, define exp_m4phi, phi_dD,
# phi_dupD (upwind finite-difference version of phi_dD), and phi_DD
if par.parval_from_str(thismodule+"::EvolvedConformalFactor_cf") == "W":
# \partial_i W = \partial_i (e^{-2 phi}) = -2 e^{-2 phi} \partial_i phi
# -> \partial_i phi = -\partial_i cf / (2 cf)
for i in range(DIM):
phi_dD[i] = - cf_dD[i] / (2*cf)
phi_dupD[i] = - cf_dupD[i] / (2*cf)
for j in range(DIM):
# \partial_j \partial_i phi = - \partial_j [\partial_i cf / (2 cf)]
# = - cf_{,ij} / (2 cf) + \partial_i cf \partial_j cf / (2 cf^2)
phi_dDD[i][j] = (- cf_dDD[i][j] + cf_dD[i]*cf_dD[j] / cf) / (2*cf)
exp_m4phi = cf*cf
For cf
=χ=e−4ϕ, we have
*Exercise to student: Prove the above relations*
# Step 9.a.iv: Assuming cf=chi=e^{-4 phi}, define exp_m4phi, phi_dD,
# phi_dupD (upwind finite-difference version of phi_dD), and phi_DD
if par.parval_from_str(thismodule+"::EvolvedConformalFactor_cf") == "chi":
# \partial_i chi = \partial_i (e^{-4 phi}) = -4 e^{-4 phi} \partial_i phi
# -> \partial_i phi = -\partial_i cf / (4 cf)
for i in range(DIM):
phi_dD[i] = - cf_dD[i] / (4*cf)
phi_dupD[i] = - cf_dupD[i] / (4*cf)
for j in range(DIM):
# \partial_j \partial_i phi = - \partial_j [\partial_i cf / (4 cf)]
# = - cf_{,ij} / (4 cf) + \partial_i cf \partial_j cf / (4 cf^2)
phi_dDD[i][j] = (- cf_dDD[i][j] + cf_dD[i]*cf_dD[j] / cf) / (4*cf)
exp_m4phi = cf
# Step 9.a.v: Error out if unsupported EvolvedConformalFactor_cf choice is made:
cf_choice = par.parval_from_str(thismodule+"::EvolvedConformalFactor_cf")
if cf_choice not in ('phi', 'W', 'chi'):
print("Error: EvolvedConformalFactor_cf == "+par.parval_from_str(thismodule+"::EvolvedConformalFactor_cf")+" unsupported!")
sys.exit(1)
# Step 9.b: Define phi_dBarD = phi_dD (since phi is a scalar) and phi_dBarDD (covariant derivative)
# \bar{D}_i \bar{D}_j \phi = \phi_{;\bar{i}\bar{j}} = \bar{D}_i \phi_{,j}
# = \phi_{,ij} - \bar{\Gamma}^k_{ij} \phi_{,k}
phi_dBarD = phi_dD
phi_dBarDD = ixp.zerorank2()
for i in range(DIM):
for j in range(DIM):
phi_dBarDD[i][j] = phi_dDD[i][j]
for k in range(DIM):
phi_dBarDD[i][j] += - GammabarUDD[k][i][j]*phi_dD[k]
BSSN.BSSN_quantities
NRPy+ module [Back to top]¶As a code validation check, we verify agreement in the SymPy expressions for the RHSs of the BSSN equations between
By default, we analyze the RHSs in Spherical coordinates, though other coordinate systems may be chosen.
def comp_func(expr1,expr2,basename,prefixname2="Bq."):
if str(expr1-expr2)!="0":
print(basename+" - "+prefixname2+basename+" = "+ str(expr1-expr2))
return 1
return 0
def gfnm(basename,idx1,idx2=None,idx3=None):
if idx2 is None:
return basename+"["+str(idx1)+"]"
if idx3 is None:
return basename+"["+str(idx1)+"]["+str(idx2)+"]"
return basename+"["+str(idx1)+"]["+str(idx2)+"]["+str(idx3)+"]"
expr_list = []
exprcheck_list = []
namecheck_list = []
# Step 3:
import BSSN.BSSN_quantities as Bq
Bq.BSSN_basic_tensors()
for i in range(DIM):
namecheck_list.extend([gfnm("LambdabarU",i),gfnm("betaU",i),gfnm("BU",i)])
exprcheck_list.extend([Bq.LambdabarU[i],Bq.betaU[i],Bq.BU[i]])
expr_list.extend([LambdabarU[i],betaU[i],BU[i]])
for j in range(DIM):
namecheck_list.extend([gfnm("gammabarDD",i,j),gfnm("AbarDD",i,j)])
exprcheck_list.extend([Bq.gammabarDD[i][j],Bq.AbarDD[i][j]])
expr_list.extend([gammabarDD[i][j],AbarDD[i][j]])
# Step 4:
Bq.gammabar__inverse_and_derivs()
for i in range(DIM):
for j in range(DIM):
namecheck_list.extend([gfnm("gammabarUU",i,j)])
exprcheck_list.extend([Bq.gammabarUU[i][j]])
expr_list.extend([gammabarUU[i][j]])
for k in range(DIM):
namecheck_list.extend([gfnm("gammabarDD_dD",i,j,k),
gfnm("gammabarDD_dupD",i,j,k),
gfnm("GammabarUDD",i,j,k)])
exprcheck_list.extend([Bq.gammabarDD_dD[i][j][k],Bq.gammabarDD_dupD[i][j][k],Bq.GammabarUDD[i][j][k]])
expr_list.extend( [gammabarDD_dD[i][j][k],gammabarDD_dupD[i][j][k],GammabarUDD[i][j][k]])
# Step 5:
Bq.detgammabar_and_derivs()
namecheck_list.extend(["detgammabar"])
exprcheck_list.extend([Bq.detgammabar])
expr_list.extend([detgammabar])
for i in range(DIM):
namecheck_list.extend([gfnm("detgammabar_dD",i)])
exprcheck_list.extend([Bq.detgammabar_dD[i]])
expr_list.extend([detgammabar_dD[i]])
for j in range(DIM):
namecheck_list.extend([gfnm("detgammabar_dDD",i,j)])
exprcheck_list.extend([Bq.detgammabar_dDD[i][j]])
expr_list.extend([detgammabar_dDD[i][j]])
# Step 6:
Bq.AbarUU_AbarUD_trAbar_AbarDD_dD()
namecheck_list.extend(["trAbar"])
exprcheck_list.extend([Bq.trAbar])
expr_list.extend([trAbar])
for i in range(DIM):
for j in range(DIM):
namecheck_list.extend([gfnm("AbarUU",i,j),gfnm("AbarUD",i,j)])
exprcheck_list.extend([Bq.AbarUU[i][j],Bq.AbarUD[i][j]])
expr_list.extend([AbarUU[i][j],AbarUD[i][j]])
for k in range(DIM):
namecheck_list.extend([gfnm("AbarDD_dD",i,j,k)])
exprcheck_list.extend([Bq.AbarDD_dD[i][j][k]])
expr_list.extend([AbarDD_dD[i][j][k]])
# Step 7:
Bq.RicciBar__gammabarDD_dHatD__DGammaUDD__DGammaU()
for i in range(DIM):
namecheck_list.extend([gfnm("DGammaU",i)])
exprcheck_list.extend([Bq.DGammaU[i]])
expr_list.extend([DGammaU[i]])
for j in range(DIM):
namecheck_list.extend([gfnm("RbarDD",i,j)])
exprcheck_list.extend([Bq.RbarDD[i][j]])
expr_list.extend([RbarDD[i][j]])
for k in range(DIM):
namecheck_list.extend([gfnm("DGammaUDD",i,j,k),gfnm("gammabarDD_dHatD",i,j,k)])
exprcheck_list.extend([Bq.DGammaUDD[i][j][k],Bq.gammabarDD_dHatD[i][j][k]])
expr_list.extend([DGammaUDD[i][j][k],gammabarDD_dHatD[i][j][k]])
# Step 8:
Bq.betaU_derivs()
for i in range(DIM):
for j in range(DIM):
namecheck_list.extend([gfnm("betaU_dD",i,j),gfnm("betaU_dupD",i,j)])
exprcheck_list.extend([Bq.betaU_dD[i][j],Bq.betaU_dupD[i][j]])
expr_list.extend([betaU_dD[i][j],betaU_dupD[i][j]])
for k in range(DIM):
namecheck_list.extend([gfnm("betaU_dDD",i,j,k)])
exprcheck_list.extend([Bq.betaU_dDD[i][j][k]])
expr_list.extend([betaU_dDD[i][j][k]])
# Step 9:
Bq.phi_and_derivs()
#phi_dD,phi_dupD,phi_dDD,exp_m4phi,phi_dBarD,phi_dBarDD
namecheck_list.extend(["exp_m4phi"])
exprcheck_list.extend([Bq.exp_m4phi])
expr_list.extend([exp_m4phi])
for i in range(DIM):
namecheck_list.extend([gfnm("phi_dD",i),gfnm("phi_dupD",i),gfnm("phi_dBarD",i)])
exprcheck_list.extend([Bq.phi_dD[i],Bq.phi_dupD[i],Bq.phi_dBarD[i]])
expr_list.extend( [phi_dD[i],phi_dupD[i],phi_dBarD[i]])
for j in range(DIM):
namecheck_list.extend([gfnm("phi_dDD",i,j),gfnm("phi_dBarDD",i,j)])
exprcheck_list.extend([Bq.phi_dDD[i][j],Bq.phi_dBarDD[i][j]])
expr_list.extend([phi_dDD[i][j],phi_dBarDD[i][j]])
num_failures = 0
for i in range(len(expr_list)):
num_failures += comp_func(expr_list[i],exprcheck_list[i],namecheck_list[i])
if num_failures == 0:
print("ALL TESTS PASSED!")
else:
print(str(num_failures) + " TESTS FAILED")
sys.exit(1)
ALL TESTS PASSED!
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-BSSN_quantities.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-BSSN_quantities")
Created Tutorial-BSSN_quantities.tex, and compiled LaTeX file to PDF file Tutorial-BSSN_quantities.pdf