To start working with the gravipy package you must load the package and initialize a pretty-printing mode in Jupyter environment
from gravipy.tensorial import * # import GraviPy package
from sympy import init_printing
import inspect
init_printing()
The next step is to choose coordinates and define a metric tensor of a particular space. Let's take, for example, the Schwarzschild metric - vacuum solution to the Einstein's field equations which describes the gravitational field of a spherical mass distribution.
# define some symbolic variables
t, r, theta, phi, M = symbols('t, r, \\theta, \phi, M')
# create a coordinate four-vector object instantiating
# the Coordinates class
x = Coordinates('\chi', [t, r, theta, phi])
# define a matrix of a metric tensor components
Metric = diag(-(1-2*M/r), 1/(1-2*M/r), r**2, r**2*sin(theta)**2)
# create a metric tensor object instantiating the MetricTensor class
g = MetricTensor('g', x, Metric)
Each component of any tensor object, can be computed by calling the appropriate instance of the GeneralTensor subclass with indices as arguments. The covariant indices take positive integer values (1, 2, ..., dim). The contravariant indices take negative values (-dim, ..., -2, -1).
x(-1)
g(1, 1)
x(1)
Matrix representation of a tensor can be obtained in the following way
x(-All)
g(All, All)
g(All, 4)
The GraviPy package contains a number of the Tensor subclasses that can be used to calculate a tensor components. The Tensor subclasses available in the current version of GraviPy package are
print([cls.__name__ for cls in vars()['Tensor'].__subclasses__()])
The first one is the Christoffel class that represents Christoffel symbols of the first and second kind. (Note that the Christoffel symbols are not tensors) Components of the Christoffel objects are computed from the below formula
Let's create an instance of the Christoffel class for the Schwarzschild metric g and compute some components of the object
Ga = Christoffel('Ga', g)
Ga(1, 2, 1)
Each component of the Tensor object is computed only once due to memoization procedure implemented in the Tensor class. Computed value of a tensor component is stored in components dictionary (attribute of a Tensor instance) and returned by the next call to the instance.
Ga.components
The above dictionary consists of two elements because the symmetry of the Christoffel symbols is implemented in the Christoffel class.
If necessary, you can clear the components dictionary
Ga.components = {}
Ga.components
The Matrix representation of the Christoffel symbols is the following
Ga(All, All, All)
You can get help on any of classes mentioned before by running the command
help(Christoffel)
Try also "Christoffel?" and "Christoffel??"
Ri = Ricci('Ri', g)
Ri(All, All)
Contraction of the Ricci tensor $R = R_{\mu}^{\ \mu} = g^{\mu \nu}R_{\mu \nu}$
Ri.scalar()
Rm = Riemann('Rm', g)
Some nonzero components of the Riemann tensor are
from IPython.display import display, Math
from sympy import latex
for i, j, k, l in list(variations(range(1, 5), 4, True)):
if Rm(i, j, k, l) != 0 and k<l and i<j:
display(Math('R_{'+str(i)+str(j)+str(k)+str(l)+'} = '+ latex(Rm(i, j, k, l))))
You can also display the matrix representation of the tensor
# Rm(All, All, All, All)
Contraction of the Riemann tensor $R_{\mu \nu} = R^{\rho}_{\ \mu \rho \nu} $
ricci = sum([Rm(i, All, k, All)*g(-i, -k)
for i, k in list(variations(range(1, 5), 2, True))],
zeros(4))
ricci.simplify()
ricci
G = Einstein('G', Ri)
G(All, All)
tau = Symbol('\\tau')
w = Geodesic('w', g, tau)
w(All).transpose()
Please note that instantiation of a Geodesic class for the metric $g$ automatically turns on a Parametrization mode for the metric $g$. Then all coordinates are functions of a world line parameter $\tau$
Parametrization.info()
x(-All)
g(All, All)
Parametrization mode can be deactivated by typing
Parametrization.deactivate(x)
Parametrization.info()
x(-All)
g(All, All)
All instances of a GeneralTensor subclasses inherits partialD method which works exactly the same way as SymPy diff method.
T = Tensor('T', 2, g)
T(1, 2)
T.partialD(1, 2, 1, 3) # The first two indices belongs to second rank tensor T
T(1, 2).diff(x(-1), x(-3))
The only difference is that computed value of partialD is saved in "_partial_derivativecomponents" dictionary an then returned by the next call to the partialD method.
T.partial_derivative_components
Covariant derivative components of the tensor T can be computed by the covariantD method from the formula
Let's compute some covariant derivatives of a scalar field C
C = Tensor('C', 0, g)
C()
C.covariantD(1)
C.covariantD(2, 3)
All covariantD components of every Tensor object are also memoized
for k in C.covariant_derivative_components:
display(Math(str(k) + ': '
+ latex(C.covariant_derivative_components[k])))
C.covariantD(1, 2, 3)
Proof that the covariant derivative of the metric tensor $g$ is zero
not any([g.covariantD(i, j, k).simplify()
for i, j, k in list(variations(range(1, 5), 3, True))])
Bianchi identity in the Schwarzschild spacetime
not any([(Rm.covariantD(i, j, k, l, m) + Rm.covariantD(i, j, m, k, l)
+ Rm.covariantD(i, j, l, m, k)).simplify()
for i, j, k, l, m in list(variations(range(1, 5), 5, True))])
To define a new scalar/vector/tensor field in some space you should extend the Tensor class or create an instance of the Tensor class.
Let's create a third-rank tensor field living in the Schwarzshild spacetime as an instance of the Tensor class
S = Tensor('S', 3, g)
Until you define (override) the __compute_covariant_component_ method of the S object, all of $4^3$ components are arbitrary functions of coordinates
S(1, 2, 3)
inspect.getsourcelines(T._compute_covariant_component)
Let's assume that tensor S is the commutator of the covariant derivatives of some arbitrary vector field V and create a new __compute_covariant_component_ method for the object S
V = Tensor('V', 1, g)
V(All)
def S_new_method(idxs): # definition
component = (V.covariantD(idxs[0], idxs[1], idxs[2])
- V.covariantD(idxs[0], idxs[2], idxs[1])).simplify()
S.components.update({idxs: component}) # memoization
return component
S._compute_covariant_component = S_new_method
# _compute_covariant_component method was overriden
S(1, 1, 3)
One can check that the well known formula is correct
zeros = reduce(Matrix.add, [Rm(-i, All, All, All)*V(i)
for i in range(1, 5)]) - S(All, All, All)
zeros.simplify()
zeros
Another way of tensor creation is to make an instance of the Tensor class with components option. Tensor components stored in Matrix object are writen to the components dictionary of the instance by this method.
Z = Tensor('Z', 3, g, components=zeros, components_type=(1, 1, 1))
not any(Z.components.values())
As an example of the Tensor class extension you can get the source code of any of the predefined Tensor subclasses
print([cls.__name__ for cls in vars()['Tensor'].__subclasses__()])
inspect.getsourcelines(Christoffel)