Covariant Derivatives
In this notebook, we show how to differentiate scalar, vector, 1-form, and arbitrary tensor fields using the unique Levi-Civita connection of a Riemannian manifold. A brief introduction to Riemannian manifolds is given here. The resulting differential operators are denoted as covariant derivatives, such as the covariant divergence and covariant curl. They simplify to the usual derivatives in the Euclidean setting. In the presence of curvature, the coordinate expressions involve additional terms using the Christoffel symbols of the Levi-Civita connection.
First, we define several fields and discuss their inner products. Then, the covariant derivatives are defined and explained in coordinates. We will see that integration by parts formulae also hold in the general Riemannian setting, as do (some) differential identities known from the Euclidean case.
[1]:
from ngsolve import *
from ngsolve.fem import Einsum, LeviCivitaSymbol
from ngsolve.webgui import Draw
from netgen.occ import *
import ngsdiffgeo as dg
Let \((M,g)\) be a Riemannian manifold with metric tensor \(g\) on a 2D manifold \(M\). Charts induce local coordinates \(x^i\) in the local basis \(\partial_i\) with their corresponding dual 1-forms \(dx^i\). We define the volume forms in the local coordinates on the elements and edges, \begin{align*} \omega = \sqrt{\det(g_{ij})}\,dx^1\wedge\dots\wedge dx^N,\qquad \omega_{\partial M} = \star n^\flat, \end{align*} where \(n^\flat\) is the 1-form associated with the \(g\)-normalized normal vector \(n\) and \(\star\) the Hodge-star operator.
[2]:
metric = dg.CigarSoliton().metric
christoffel1 = dg.CigarSoliton().chr1
christoffel2 = dg.CigarSoliton().chr2
mesh = Mesh(unit_square.GenerateMesh(maxh=0.1))
mf = dg.RiemannianManifold(metric)
omega_T = dg.ScalarField(mf.VolumeForm(VOL))
omega_S = dg.ScalarField(mf.VolumeForm(BND))
Scalar, vector, 1-form, and tensor fields
We start by defining some scalar, vector, 1-form, and tensor fields
\begin{align*} &f,g,h\in C^\infty(M),\qquad X = X^i\,\partial_i\in\mathfrak{X}(M),\qquad \alpha = \alpha_i\,dx^i\in \Lambda^1(M),\\ & A= A^{ij}\partial_i\otimes \partial_j\in\mathcal{T}^0_2(M),\quad B = \alpha\otimes X = \alpha_iX^j\,dx^i\otimes \partial_j\in\mathcal{T}^1_1(M). \end{align*} We use a string to indicate whether the indices of a general tensor field are covariant or contravariant, where 1 denotes covariant indices and 0 denotes contravariant indices.
[3]:
f = dg.ScalarField(CF(x**2 * y - 0.1 * y * x))
g = dg.ScalarField(CF(x * y**2 + 0.1 * y * x - 0.73 * x))
h = dg.TensorProduct(f, g)
Z = dg.OneForm(dg.GradCF(f, 2))
X = dg.VectorField(CF((10 * x * y**3 - x**2, y**4 * x - y)))
Y = dg.VectorField(CF((y**2 * x - 0.1 * y * x**2, 10 * x * y**3 + x**2 - y)))
alpha = dg.OneForm(CF((sin(x * y), cos(x) * y**2)))
beta = dg.OneForm(
CF((0.2 * y * x**2 + 0.37 * x**3, 2 * x**2 * y**2 + 0.1 * x**2 - 1.34 * y))
)
A = dg.TensorField(
CF((10 * x * y**3 - x**2, y**4 * x - y, sin(x * y), cos(x) * y**2), dims=(2, 2)),
"00",
)
B = dg.TensorProduct(alpha, X)
C = dg.TensorProduct(alpha, beta)
Inner product
The \(g\)-inner product of scalar \(f\), \(g\), vector \(X\), \(Y\), and 1-form \(\alpha\), \(\beta\) fields is given by
\begin{align*} g(f,g) = f\,g,\qquad g(X,Y)= g_{ij}X^iY^j,\qquad g(\alpha,\beta) = g^{ij}\alpha_i\beta_j, \qquad g(\alpha,X) = \alpha_i\beta^i, \end{align*} where we used the Einstein summation convention of repeated indices, e.g.
\begin{align*} g_{ij}X^iY^j:=\sum_{i,j}g_{ij}X^iY^j. \end{align*}
For two tensor fields of the form \(A=X_1 \otimes \dots \otimes X_k \otimes \alpha_1\otimes\dots\otimes \alpha_l\) and \(B=Y_1\otimes\dots\otimes Y_m\otimes \beta_1\otimes\dots\otimes \beta_n\) of the same rank \(r=k+l=m+n\) the definition of the inner product of vector and 1-form fields directly extends to \begin{align*} g(A,B) := g(X_1,Y_1)\cdots g(\alpha_l,\beta_n). \end{align*}
The InnerProduct method of the Riemannian manifold class recognizes the type of tensor fields and employs the correct inner product.
[4]:
print(f"{Integrate(mf.InnerProduct(f, g),mesh):.6f} = {Integrate(f * g, mesh):.6f}")
print(
f"{Integrate(mf.InnerProduct(X, Y),mesh):.6f} = {Integrate(metric*X*Y, mesh):.6f}"
)
print(
f"{Integrate(mf.InnerProduct(alpha, Y),mesh):.6f} = {Integrate(alpha*Y, mesh):.6f}"
)
print(
f"{Integrate(mf.InnerProduct(alpha, beta),mesh):.6f} = {Integrate(Inv(metric)*alpha*beta, mesh):.6f}"
)
print(
f"{Integrate(mf.InnerProduct(A, A),mesh):.6f} = {Integrate(InnerProduct(metric*A*metric,A), mesh):.6f}"
)
print(
f"{Integrate(mf.InnerProduct(A, B),mesh):.6f} = {Integrate(InnerProduct(metric*A,B), mesh):.6f}"
)
-0.017694 = -0.017694
-0.019774 = -0.019774
0.575764 = 0.575764
-0.203729 = -0.203729
0.838426 = 0.838426
1.010107 = 1.010107
Covariant differential operators
Covariant derivative
Covariant derivatives enable the differentiation of tensor fields on Riemannian manifolds. It takes a \((k,l)\)-tensor field \(\mathcal{T}^k_l(M)\) acting on \(k\) vector fields and \(l\) covectors (1-forms) and returns a \((k+1,l)\)-tensor \(\mathcal{T}^{k+1}l(M)\), i.e. for \(A\in \mathcal{T}^k_l(M)\) and \(X,Y_1,\dots, Y_k\in \mathfrak{X}(M)\), \(\alpha_1,\dots, \alpha_l\) yields \(\nabla A\in \mathcal{T}^{k+1}l(M)\) with
\begin{align*} &(\nabla A)(X,Y_1,\dots, Y_k,\alpha_1,\dots, \alpha_l)= (\nabla_XA)(Y_1,\dots. Y_k,\alpha_1,\dots, \alpha_l)\\ & = X(A(Y_1,\dots, Y_k,\alpha_1,\dots, \alpha_l)) - A(\nabla_XY_1,\dots Y_k,\alpha_1,\dots \alpha_l) -\cdots - A(Y_1,\dots, Y_k,\alpha_1,\dots, \nabla_X\alpha_l), \end{align*}
where the last equality is the Leibnitz rule, which can be used to define the covariant derivative for arbitrary tensors from the differentiation of vector and 1-form fields.
In the flat case, these operators simplify to their Euclidean counterparts. Therefore, in coordinates, they are expressed by standard derivatives with additional terms involving the metric and its derivatives. Particularly the Christoffel symbols of first \(\Gamma_{ijk}\) and second \(\Gamma_{ij}^k\) kind,
\begin{align*} \Gamma_{ijk}=\frac{1}{2}\left(\partial_i g_{jk}+\partial_jg_{ik}-\partial_kg_{ij}\right),\qquad \Gamma_{ij}^k = g^{kl}\Gamma_{ijl} =\frac{1}{2}g^{kl}\left(\partial_i g_{jl}+\partial_jg_{il}-\partial_lg_{ij}\right). \end{align*}
Further, the covariant derivative is compatible with the metric \(g\), and thus its inverse and determinant (volume forms), by construction (the unique Levi-Civita connection). That means
\begin{align*} \nabla g = 0,\quad \nabla g^{-1} = 0,\quad \nabla \omega=0. \end{align*} However, expressing the volume form in the local basis \(\omega=\sqrt{\det (g_{ij})}\,dx^1\wedge\dots\wedge dx^N\) the covariant derivative of the determinant is in general not zero \begin{align*} \nabla \sqrt{\det g}\neq 0. \end{align*}
For a scalar field \(s\in C^\infty(M)\), the covariant derivative coincides with the exterior derivative (and Lie derivative), yielding the 1-form \(\nabla s= ds\in \Lambda^1(M)\). The covariant gradient is obtained by the musical \(\sharp\) operator giving a vector field \begin{align*} \nabla s = d s = \partial_i s\,dx^i,\qquad \mathrm{grad}(s) = (\nabla s)^\sharp = g^{ij}\partial_j s\,\partial_i. \end{align*} Thus, \(\nabla_X s = (ds)(X) = X(s) = X^i\partial_is\) for a vector field \(X= X^i\partial_i\in \mathfrak{X}(M)\).
For vector field \(v\in \mathfrak{X}(M)\) and a 1-form \(\alpha\in \Lambda^1(M)\) the covariant derivative is given in coordinates by \begin{align*} &\nabla v = (\partial_i v^j + \Gamma^j_{ik}v^k)dx^i\otimes \partial_j,\qquad \nabla\alpha = (\partial_i \alpha_j - \Gamma_{ij}^k\alpha_k)dx^i\otimes dx^j. \end{align*} Note the different signs in front of the Christoffel symbols, whether they are contracted with a vector field or 1-form. Using the Leibnitz rule above, the covariant derivative of an arbitrary order tensor \(A\in \mathcal{T}^k_l(M)\) is now easily given \begin{align*} &\nabla A = \left(\partial_i A_{r_1\dots r_k}^{s_1\dots s_l} -\sum_{j=1}^k\Gamma^{t}_{ir_j}A^{s_1\dots s_l}_{r_1\dots t\dots r_k}+ \sum_{j=1}^l\Gamma^{s_j}_{it}A^{s_1\dots t\dots s_l}_{r_1\dots r_k}\right)dx^i\otimes dx^{r_1}\otimes\dots \otimes dx^{r_k}\otimes \partial_{s_1}\otimes\dots\otimes \partial_{s_l}. \end{align*}
The covariant derivative method of the Riemannian manifold class takes the tensor’s signature into account. Further, a so-called GradCF (gradient coefficient function) is used to compute the derivative of an arbitrary tensor numerically. It differentiates classical CoefficientFunctions and GridFunctions. Differentiation of trial and test functions is WIP.
[5]:
# scalar field
print(
f"{Integrate(mf.CovDeriv(f), mesh).Norm():.6f} = {Integrate(CF((f.Diff(x),f.Diff(y))), mesh).Norm():.6f}"
)
# vector field
cov_grad_X = CF((X.Diff(x), X.Diff(y)), dims=(2, 2)) + Einsum(
"ikj,k->ij", christoffel2, X
)
print(
f"{Integrate(mf.CovDeriv(X), mesh).Norm():.6f} = {Integrate(cov_grad_X, mesh).Norm():.6f}"
)
# 1-form
cov_grad_alpha = CF((alpha.Diff(x), alpha.Diff(y)), dims=(2, 2)) - Einsum(
"ijk,k->ij", christoffel2, alpha
)
print(
f"{Integrate(mf.CovDeriv(alpha), mesh).Norm():.6f} = {Integrate(cov_grad_alpha, mesh).Norm():.6f}"
)
# second order tensor field
cov_grad_B = (
CF((B.Diff(x), B.Diff(y)), dims=(2, 2, 2))
- Einsum("ijl,lk->ijk", christoffel2, B)
+ Einsum("ilk,jl->ijk", christoffel2, B)
)
print(
f"{Integrate(mf.CovDeriv(B), mesh).Norm():.6f} = {Integrate(cov_grad_B, mesh).Norm():.6f}"
)
# covariant derivative of metric is zero (Levi-Civita connection is metric-compatible)
print(f"{Integrate(mf.CovDeriv(mf.G),mesh).Norm():.6f} = 0")
print(f"{Integrate(mf.CovDeriv(mf.G_inv),mesh).Norm():.6f} = 0")
# covariant derivative of volume form expressed as determinant in local coordinates
# is in general not zero
print(f"{Integrate(mf.CovDeriv(omega_T),mesh).Norm():.6f} != 0")
0.531769 = 0.531769
4.814188 = 4.814188
1.144794 = 1.144794
4.772021 = 4.772021
0.000000 = 0
0.000000 = 0
0.495241 != 0
Covariant divergence
The covariant divergence of an arbitrary tensor (of order at least 1) is defined as first taking the covariant derivative and then contracting the first two indices, i.e. taking the trace \begin{align*} \mathrm{div} A = \mathrm{tr}_{12}(\nabla A), \end{align*} where the contraction is to be understood with respect to the metric \(g\). For example, for a 1-form \(\alpha\) and a vector field \(X\) \begin{align*} \mathrm{div}\alpha = g^{ij}(\nabla\alpha)_{ij},\qquad \mathrm{div}X=(\nabla X)^i_{i}. \end{align*} The coordinate expression, therefore, follows directly from the covariant derivative. Again, the covariant divergence of (functions of) the metric vanishes.
[6]:
# vector field
print(f"{Integrate(mf.CovDiv(X),mesh):.6f} = {Integrate(Trace(cov_grad_X),mesh):.6f}")
# 1-form
print(
f"{Integrate(mf.CovDiv(alpha),mesh):.6f} = {Integrate(Trace(Inv(metric)*cov_grad_alpha),mesh):.6f}"
)
# second order tensor field
cov_div_B = Einsum(
"ij,ijk->k",
Inv(metric),
CF((B.Diff(x), B.Diff(y)), dims=(2, 2, 2))
- Einsum("ijl,lk->ijk", christoffel2, B)
+ Einsum("ilk,jl->ijk", christoffel2, B),
)
print(
f"{Integrate(mf.CovDiv(B),mesh).Norm():.6f} = {Integrate(cov_div_B,mesh).Norm():.6f}"
)
# covariant divergence of (inverse) metric is zero
print(f"{Integrate(mf.CovDiv(mf.G),mesh).Norm():.6f} = 0")
print(f"{Integrate(mf.CovDiv(mf.G_inv),mesh).Norm():.6f} = 0")
0.788776 = 0.788776
2.326922 = 2.326922
9.871358 = 9.871358
0.000000 = 0
0.000000 = 0
Analogously to the Euclidean case, there holds the following integration by parts formula for a scalar field \(f \in C^{\infty}(M)\) and a vector field \(X\in\mathfrak{X}(M)\)
\begin{align*} \int_{M} g(\nabla f,X)\,\omega = -\int_{M} f\,\mathrm{div}(X)\,\omega + \int_{\partial M}f\, g(X,n)\,\omega_{\partial M}, \end{align*}
where \(n\) denotes the outer \(g\)-normal vector. That means, \(g(n,n)=1\). Further, for a second order tensor \(A\), we have
\begin{align*} \int_{M} g(\nabla X,A)\,\omega = -\int_{M} g(X,\mathrm{div}(A))\,\omega + \int_{\partial M} g(n\otimes X,A)\,\omega_{\partial M}, \end{align*} where \(g(n\otimes X,A) = g(X, n \llcorner A)\) can be written by contracting \(A\) with \(n\) in its first argument. Note that in the first integration by parts formula \(X\) can be replaced by a 1-form \(\alpha\in \Lambda^1(M)\). Also, \(X\) can be replaced by \(\alpha\) in the second formula, and the second-order tensor \(A\) can have any signature.
More generally, let \(A\in \mathcal{T}^k_l(M)\) and \(B\in\mathcal{T}^r_s(M)\) with \(k+l+1 = r+s\). Then there holds \begin{align*} \int_{M} g(\nabla A,B)\,\omega = -\int_{M} g(A,\mathrm{div}(B))\,\omega + \int_{\partial M} g(n\otimes A,B)\,\omega_{\partial M}. \end{align*}
In coordinates, using that in 2D with \(\hat{t}\) and \(\hat{n}\) the Euclidean tangential and normal vectors \begin{align*} n^i = \frac{\sqrt{\det g}}{\sqrt{g_{\hat{t}\hat{t}}}}g^{ij}\hat{n}_j,\qquad \omega = \sqrt{\det(g)},\qquad \omega_{\partial M} = \sqrt{g_{\hat{t}\hat{t}}} \end{align*} there holds for a scalar \(f\), vector field \(X\) and \((2,0)\)-tensor \(A\)
\begin{align*} \int_M X^i\partial_i f\,\sqrt{\det g}\,dx &= -\int_Mf\,(\partial_iX^i+\Gamma_{ik}^iX^k)\,\sqrt{\det g}\,dx \\ &\quad+ \int_{\partial M} f\,g_{ij}X^i n^j\, \sqrt{g_{tt}}\,ds,\\ \int_M(\partial_iX^j+\Gamma_{ik}^jX^k)g^{ik}A_{kj}\,\sqrt{\det g}\,dx &= -\int_MX^j(\partial_iA_{ij}-\Gamma_{ii}^kA_{kj}-\Gamma_{ij}^kA_{ik})\,\sqrt{\det g}\,dx\\ &\quad +\int_{\partial M}A_{ij}n^i X^j\,\sqrt{g_{tt}}\,ds. \end{align*}
From the above expressions, it is clear that coordinate-based formulas are very error-prone.
[7]:
# scalar with vector field
print(
f"{Integrate(
mf.InnerProduct(mf.CovDeriv(f), X) * omega_T * dx, mesh
):.6f} = {Integrate(
-mf.InnerProduct(mf.CovDiv(X), f) * omega_T * dx
+ f
* mf.InnerProduct(X, mf.normal)
* omega_S
* ds,
mesh,
):.6f}"
)
# vector field with second order tensor field
print(
f"{Integrate(mf.InnerProduct(mf.CovDeriv(X), A) * omega_T * dx, mesh):.6f} = {Integrate(
-mf.InnerProduct(mf.CovDiv(A), X) * omega_T * dx
+ mf.InnerProduct(dg.TensorProduct(mf.normal, X), A)
* omega_S
* ds,
mesh,
):.6f}"
)
0.381579 = 0.381579
1.246865 = 1.246865
Covariant curl and rot in 2D
In two dimensions, the covariant rot and curl operators are defined for scalar fields \(f\), 1-forms \(\alpha\), vector fields \(X\), and \((2,0)\)-tensors \(\sigma\) by \begin{align*} \mathrm{rot}(f)= \varepsilon^{ij}\partial_jf\,\partial_i,\qquad \mathrm{rot}(X) = \varepsilon^{jq}\left(\partial_qX^i+\Gamma^i_{qk}X^k\right)\,\partial_i\otimes \partial_j \end{align*} and \begin{align*} \mathrm{curl}(\alpha)= \varepsilon^{ij}\partial_i\alpha_j,\qquad \mathrm{curl}(\sigma)= \varepsilon^{jk}(\partial_j\sigma_{ik}-\Gamma_{ji}^m\sigma_{mk})\,dx^i. \end{align*} The incompatibility operator of a \((2,0)\)-tensor is defined as two consecutive curl operators \begin{align*} \mathrm{inc}(A) = \mathrm{curl}(\mathrm{curl}(A)),\qquad \mathrm{inc}(A)=\varepsilon^{qi}\varepsilon^{jk}\left(\partial_j\partial_qA_{ik}-\partial_q(\Gamma_{ji}^mA_{mk})-\Gamma_{lq}^l(\partial_jA_{ik}-\Gamma_{ij}^mA_{mk})\right). \end{align*} Above, the covariant Levi-Civita symbols are given by \(\varepsilon^{ij}=1/\sqrt{\det g}\,\hat{\varepsilon}^{ij}\) and \(\varepsilon_{ij}\sqrt{\det g}\,\hat{\varepsilon}_{ij}\), where \(\hat{\varepsilon}_{ij}\) and \(\hat{\varepsilon}_{ij}\) are the standard permuting symbols being 1 or -1 if \((i,j)\) is an even or odd permutation of \((1,2)\) and 0 else. Further, note that the covariant Levi-Civita symbols are not tensors but tensor densities (they get multiplied/divided by the volume form).
We can also apply the rot operator twice in a row \begin{align*} \mathrm{rot}(\mathrm{rot}(f)) = \varepsilon^{jq}\left( \varepsilon^{il}\partial_q\partial_lf-\Gamma_{lq}^q\varepsilon^{il}\partial_lf +\varepsilon^{kl}\Gamma_{qk}^i\partial_lf\right)\,\partial_i\otimes\partial_j. \end{align*}
There holds analogously to the Euclidean case \begin{align*} \mathrm{div}(\mathrm{rot}(f))=0,\qquad \mathrm{curl}(\nabla f) = 0,\qquad \mathrm{curl}(\mathrm{rot}(f)) = -\Delta f, \end{align*} however, these identities are not valid for tensor fields, e.g. \begin{align*} \mathrm{div}(\mathrm{rot}(X))\neq 0,\qquad \mathrm{curl}(\nabla \alpha) \neq 0! \end{align*}
[8]:
# rot
# scalar field
print(
f"{Integrate(mf.CovRot(f), mesh).Norm():.6f}\
= {Integrate(1/sqrt(Det(metric))*CF((f.Diff(y),-f.Diff(x))), mesh).Norm():.6f}"
)
# vector field
cov_rot_X = Einsum(
"jq,qi->ij",
1 / sqrt(Det(metric)) * LeviCivitaSymbol(2),
CF((X.Diff(x), X.Diff(y)), dims=(2, 2)) + Einsum("ikj,k->ij", christoffel2, X),
)
print(
f"{Integrate(mf.CovRot(X), mesh).Norm():.6f} = {Integrate(cov_rot_X, mesh).Norm():.6f}"
)
# 1-form
cov_rot_alpha = Einsum(
"jq,qi->ij",
1 / sqrt(Det(metric)) * LeviCivitaSymbol(2),
dg.GradCF(mf.G_inv * alpha, 2)
+ Einsum("ikj,k->ij", christoffel2, mf.G_inv * alpha),
)
print(
f"{Integrate(mf.CovRot(alpha), mesh).Norm():.6f} = {Integrate(cov_rot_alpha, mesh).Norm():.6f}"
)
# curl
# 1-form
cov_curl_alpha = Einsum(
"ij,ij->",
1 / sqrt(Det(metric)) * LeviCivitaSymbol(2),
CF((alpha.Diff(x), alpha.Diff(y)), dims=(2, 2)),
)
print(
f"{Integrate(mf.CovCurl(alpha), mesh):.6f} = {Integrate(cov_curl_alpha, mesh):.6f}"
)
# vector field
cov_curl_X = Einsum(
"ij,ij->",
1 / sqrt(Det(metric)) * LeviCivitaSymbol(2),
dg.GradCF(metric * X, 2),
)
print(f"{Integrate(mf.CovCurl(X), mesh):.6f} = {Integrate(cov_curl_X, mesh):.6f}")
# (2,0)-tensor field (general 2-order fields not implemented yet)
cov_curl_C = Einsum(
"jk,jik->i",
1 / sqrt(Det(metric)) * LeviCivitaSymbol(2),
dg.GradCF(C, 2) - Einsum("jim,mk->jik", christoffel2, C),
)
print(
f"{Integrate(mf.CovCurl(C), mesh).Norm():.6f} = {Integrate(cov_curl_C, mesh).Norm():.6f}"
)
1.063312 = 1.063312
10.041697 = 10.041697
3.895205 = 3.895205
-1.145168 = -1.145168
-3.832837 = -3.832837
0.503877 = 0.503877
[9]:
print(f"div(rot(f)) = {Integrate(mf.CovDiv(mf.CovRot(f)), mesh):.6f}")
print(f"curl(nabla(f)) = {Integrate(mf.CovCurl(mf.CovDeriv(f)), mesh):.6f}")
print(f"curl(rot(f)) = {Integrate(mf.CovCurl(mf.CovRot(f)), mesh):.6f}")
print(f"-Delta f = {-Integrate(mf.Trace(mf.CovHesse(f)), mesh):.6f}")
# invoking tensor fields
print(
f"curl(nabla(alpha)) = {Integrate(mf.CovCurl(mf.CovDeriv(alpha)), mesh).Norm():.6f}"
)
print(f"div(rot(X)) = {Integrate(mf.CovDiv(mf.CovRot(X)), mesh).Norm():.6f}")
div(rot(f)) = 0.000000
curl(nabla(f)) = -0.000000
curl(rot(f)) = -1.833333
-Delta f = -1.833333
curl(nabla(alpha)) = 2.824292
div(rot(X)) = 18.249655
With \(t\) denoting the \(g\)-tangential vector, in coordinates \(t= \frac{1}{\sqrt{g_{\hat{t}\hat{t}}}}\hat{t}^i\,\partial_i\), there holds the following integration by parts formulae \begin{align*} \int_M\mathrm{curl}(X)\,f\,\omega &= \int_M g(\mathrm{rot}(f),X)\,\omega + \int_{\partial M} g(X,t)\,f\,\omega_{\partial M},\\ \int_Mg(\mathrm{curl}(A),X)\,\omega &= \int_M g(A,\mathrm{rot}(X))\,\omega + \int_{\partial M}g(A,X\otimes t)\,\omega_{\partial M},\\ \int_M \mathrm{inc}(A)\,f\,\omega&=\int_{M}g(\mathrm{curl}(A),\mathrm{rot}(f))\,\omega +\int_{\partial M}g(\mathrm{curl}(A),t)\,f\,\omega_{\partial M}\\ &= \int_Mg(A,\mathrm{rot}(\mathrm{rot}(f)))\,\omega + \int_{\partial M}\left( g(A,\mathrm{rot}(f)\otimes t)+ g(\mathrm{curl}(A),t)\,f\right)\omega_{\partial M}. \end{align*}
[10]:
# curl-rot integration-by-parts between vector and scalar field
print(
f"{Integrate(
mf.InnerProduct(mf.CovCurl(X), f) * omega_T * dx, mesh
):.6f} = {Integrate(
mf.InnerProduct(X,mf.CovRot(f)) * omega_T * dx
+ f
* mf.InnerProduct(X, mf.tangent)
* omega_S
* ds,
mesh,
):.6f}"
)
# curl-rot integration-by-parts between (2,0)-tensor and vector field
print(
f"{Integrate(
mf.InnerProduct(mf.CovCurl(C), X) * omega_T * dx, mesh
):.6f} = {Integrate(
mf.InnerProduct(C, mf.CovRot(X)) * omega_T * dx
+ mf.InnerProduct(C, dg.TensorProduct(X,mf.tangent))
* omega_S
* ds,
mesh,
):.6f}"
)
# inc - rot/rot integration-by-parts between (2,0)-tensor and scalar field
print(
f"{Integrate(
mf.InnerProduct(mf.CovInc(C, True), f) * omega_T * dx, mesh
):.6f} = {Integrate(
mf.InnerProduct(mf.CovCurl(C),mf.CovRot(f)) * omega_T * dx
+ f
* mf.InnerProduct(mf.CovCurl(C), mf.tangent)
* omega_S
* dx(element_boundary=True),
mesh,
):.6f} = {Integrate(
mf.InnerProduct(C,mf.CovRot(mf.CovRot(f))) * omega_T * dx
+ (mf.InnerProduct(C, dg.TensorProduct(mf.CovRot(f),mf.tangent)) + f
* mf.InnerProduct(mf.CovCurl(C), mf.tangent))
* omega_S
* dx(element_boundary=True),
mesh,
):.6f}"
)
-0.562212 = -0.562212
0.610140 = 0.610140
-1.290332 = -1.290332 = -1.290332
With our definition that the normal vector \(n\) points outward elements, there holds the following relation with the tangent vector \(t\) \begin{align*} g(\mathrm{rot}(f),n) = g(\nabla f,t). \end{align*}
[11]:
print(
f"{Integrate(mf.InnerProduct(mf.CovRot(f),mf.normal) * dx(element_boundary=True), mesh):.6f}\
= {Integrate(mf.InnerProduct(mf.CovDeriv(f),mf.tangent) * dx(element_boundary=True), mesh):.6f}"
)
-0.053979 = -0.053979
Covariant curl in 3D
Next, we consider 3-dimensional Riemannian manifolds. First, define a metric and some fields.
[12]:
metric = dg.WarpedProduct().metric
mesh = Mesh(unit_cube.GenerateMesh(maxh=0.1))
mf = dg.RiemannianManifold(metric)
omega_T = mf.VolumeForm(VOL)
omega_S = mf.VolumeForm(BND)
X = dg.VectorField(CF((10 * x * y**3 - x**2 * z, y**4 * x - y * z, z * x * y)))
Y = dg.VectorField(
CF(
(
y**2 * x * z - 0.1 * y * x**2,
10 * x * y**3 + x**2 - y * z,
sin(z) + cos(x * y),
)
)
)
alpha = dg.OneForm(CF((sin(x * y), cos(x) * y**2, 2 * x * y * z)))
beta = dg.OneForm(
CF(
(
0.2 * y * x**2 + 0.37 * x**3 * z,
2 * x**2 * y**2 + 0.1 * x**2 - 1.34 * y * z,
cos(x * y) + 0.1 * z,
)
)
)
B = dg.TensorProduct(alpha, X)
The Covariant curl is defined with the covariant Levi-Civita symbols \(\varepsilon^{ijk}=1/\sqrt{\det g}\,\hat{\varepsilon}^{ijk}\) and \(\varepsilon_{ijk}=\sqrt{\det g}\,\hat{\varepsilon}_{ijk}\) for vector fields \(X\in\mathfrak{X}(M)\) and 1-forms \(\alpha\in\Lambda^1(M)\) \begin{align*} \mathrm{curl}(X) = \frac{1}{\sqrt{\det g}}\varepsilon^{ijk}\partial{j}X_k\,\partial_i,\qquad \mathrm{curl}(\alpha) = \frac{1}{\sqrt{\det g}}\varepsilon^{ijk}\partial_{j}\alpha_k\,\partial_i, \end{align*} where we lowered the index for \(X\).
There holds the integration by parts formula \begin{align*} \int_M g(\mathrm{curl}(X),\alpha)\,\omega = \int_M g(X,\mathrm{curl}(\alpha))\,\omega - \int_{\partial M}g(X\times n,\alpha)\,\omega_{\partial M}, \end{align*} where \(n\) is again the \(g\)-normal vector and the cross product is defined by \(X\times Y = \omega(X,Y,\cdot)^\sharp\), or in coordinates \begin{align*} X\times Y = \frac{1}{\sqrt{\det g}}\varepsilon^{ijk}X_jY_k\,\partial_i. \end{align*} We can also use 1-forms and mix vector fields with 1-forms in the above formula.
[13]:
# two vector fields
print(
f"{Integrate(mf.InnerProduct(mf.CovCurl(X), Y) * omega_T * dx(bonus_intorder=1), mesh):.6f} = {Integrate(
mf.InnerProduct(X, mf.CovCurl(Y)) * omega_T * dx(bonus_intorder=1)
- mf.InnerProduct(mf.Cross(X, mf.normal), Y)
* omega_S
* ds(bonus_intorder=1),
mesh,
):.6f}"
)
# vector field with 1-form
print(
f"{Integrate(mf.InnerProduct(mf.CovCurl(X), alpha) * omega_T * dx, mesh):.6f} = {Integrate(
mf.InnerProduct(X, mf.CovCurl(alpha)) * omega_T * dx
- mf.InnerProduct(mf.Cross(X, mf.normal), alpha)
* omega_S
* ds,
mesh,
):.6f}"
)
# 1-form fields
print(
f"{Integrate(mf.InnerProduct(mf.CovCurl(alpha), beta) * omega_T * dx, mesh):.6f} = {Integrate(
mf.InnerProduct(alpha, mf.CovCurl(beta)) * omega_T * dx
- mf.InnerProduct(mf.Cross(alpha, mf.normal), beta)
* omega_S
* ds,
mesh,
):.6f}"
)
78.498239 = 78.498239
-5.844097 = -5.844097
-0.400652 = -0.400652
Note, that we used the bonus-intorder flag to increase the order of numerical integration to integrate the terms sufficiently accurate.
Hessian and Laplace-Beltrami
For a scalar field \(f\in C^{\infty}(M)\) the Riemannian Hessian is defined by \(\nabla^2 f = \nabla df\in \mathcal{T}^2_0(M)\). In coordinates
\begin{align*} \mathrm{Hess}f = \left(\partial^2_{i,j}f-\Gamma^k_{ij}\partial_k f\right)\,dx^i\otimes dx^j. \end{align*} The Hessian is symmetric by the symmetry of Christoffel symbols \(\Gamma_{ij}^k=\Gamma_{ji}^k\).
The Laplace-Beltrami operator of a scalar field \(f\in C^{\infty}(M)\) is defined in the usual way \(\Delta f = \mathrm{div}(\nabla f)\), in coordinates \begin{align*} \Delta f = \mathrm{tr}(\nabla^2 f) = \partial^2_{i,i}f-\Gamma^k_{ii}\partial_k f = \frac{1}{\sqrt{\det g}}\mathrm{div}(\sqrt{\det g} (\nabla f)^\sharp). \end{align*}
There hold the following integration by parts formulae \begin{align*} &\int_M g(\mathrm{div}A,\nabla f)\,\omega = - \int_Mg(A,\mathrm{Hess}f)\,\omega + \int_{\partial M} g(A, n\otimes \nabla f)\omega_{\partial M},\\ &\int_M \Delta f\,g\,\omega = -\int_{M} g(\nabla f,\nabla g)\,\omega + \int_{\partial M}(\nabla f)(n)\,g\,\omega_{\partial M}. \end{align*} Note that \((\nabla f)(n) = g(\nabla f, n)\).
[14]:
with TaskManager():
print(
f"{Integrate(mf.InnerProduct(mf.CovDiv(B), mf.CovDeriv(f)) * omega_T * dx, mesh):.6f} = {Integrate(
-mf.InnerProduct(B, mf.CovHesse(f)) * omega_T * dx
+mf.InnerProduct(B, dg.TensorProduct(mf.normal,mf.CovDeriv(f)))
* omega_S
* dx(element_boundary=True),
mesh,
):.6f}"
)
17.015834 = 17.015834
Note that we used dx(element-boundary=True) instead of ds above for the right-hand side. Otherwise, the full covariant derivative would not be available.
[15]:
print(
f"{Integrate(mf.InnerProduct(mf.Trace(mf.CovHesse(f)), g) * omega_T * dx, mesh):.6f} = {Integrate(
-mf.InnerProduct(mf.CovDeriv(f), mf.CovDeriv(g)) * omega_T * dx
+mf.InnerProduct(mf.CovDeriv(f), mf.normal)*g
* omega_S
* dx(element_boundary=True),
mesh,
):.6f}"
)
-0.081667 = -0.081667
Fancy identities
2D
For a \((2,0)\)-tensor \(\sigma\in \mathcal{T}^{2}_0(M)\) there holds \begin{align*} \mathrm{inc}(\sigma) = -\mathrm{div}\mathrm{div}(\mathbb{S}\sigma) = -\mathrm{div}\mathrm{div}(\sigma) + \Delta \mathrm{Tr}(\sigma), \end{align*} where \(\mathbb{S}\sigma = \sigma-\mathrm{Tr}(\sigma)\,g\).
[16]:
metric = dg.CigarSoliton().metric
mesh = Mesh(unit_square.GenerateMesh(maxh=0.1))
mf = dg.RiemannianManifold(metric)
alpha = dg.OneForm(CF((sin(x * y), cos(x) * y**2)))
beta = dg.OneForm(
CF((0.2 * y * x**2 + 0.37 * x**3, 2 * x**2 * y**2 + 0.1 * x**2 - 1.34 * y))
)
C = dg.TensorProduct(alpha, beta)
print(
f"{Integrate(mf.CovInc(C, True),mesh):.6f}\
= {Integrate(-mf.CovDiv(mf.CovDiv(dg.TensorField(C-mf.Trace(C)*mf.G, "11"))),mesh):.6f}\
= {Integrate(-mf.CovDiv(mf.CovDiv(C))+mf.Trace(mf.CovHesse((mf.Trace(C)))),mesh):.6f}"
)
-4.573159 = -4.573159 = -4.573159
[ ]: