General Relativity in Python
I have covered several different software packages for doing scientific computation in Linux Journal, but I haven't spent as much time describing available libraries and the kind of work that can be done with those libraries. So in this article, I take a look at SymPy, the Python module that allows you to do symbolic mathematics, and specifically at a utility named GraviPy that is built upon SymPy. With this utility, you can do all sorts of computations that you'll want to do if you're researching general relativity.
You can install both modules using pip with the following command:
sudo pip install sympy
sudo pip install gravipy
Then, you can import everything from both modules with the Python statements:
from sympy import *
from gravipy import *
This way, you will have access to all of the functionality from these modules within the namespace of your own Python code.
When you do work in
general relativity, you often need to look at very complicated
equations. The GraviPy module includes a function, called
init_printing()
,
that sets up everything you need for pretty-printed equations. You
probably will want to call this near the top of your code so that your worksheet
is more human-readable.
Let's start with the SymPy Python module. SymPy is designed to give you the ability to do symbolic mathematical computations. With it, you can do things like solve algebraic expressions, rearrange and simplify equations, and even perform symbolic derivatives and integrals. I've looked at SymPy in a previous issue of LJ, so here, I just focus on some of the core parts as a reminder.
The first necessary step is to
define the symbolic variables that you are going to use in your
calculations. Using the symbols
command, you can define them with:
x,y,z = symbols('x y z')
This way, you define the variables that define the three space dimensions in Cartesian coordinates.
If you want to have all four space-time coordinates defined in spherical coordinates, you can use this:
t, r, theta, phi = symbols('t, r, \\theta, \phi')
Remember that you are feeding a string into the symbols function. This means you need to escape any special characters to get the results you are expecting.
These commands will give you the symbolic
variables that you can use in expressions. But, general relativity has a
special class of variables, called coordinates, that are used to define the
space-time itself. There is a class, called
Coordinates
, that helps define
this. Using the spherical coordinates above, you can create the coordinates
with the statement:
x = Coordinates('\chi', [t, r, theta, phi])
The four space-time coordinates are stored in the object
x
. You can
access the individual coordinates by using an index. In general relativity,
there are two different ways of indexing variables: covariant and
contravariant indexes. To look at the element values for the covariant
version, you need to use positive index values. Negative index values will
give you the contravariant versions. As an example, if you wanted to
get the time variable, you would use the following:
x(-1)
Right away, you should notice that this implies that GraviPy uses 1-based indexing rather than 0-based indexing.
Now that you have a set of variables to define the space-time coordinates to be used, let's move on to actual general relativity.
The core part of general relativity is the metric. The metric defines how space-time is shaped. This space-time shape is what defines the gravitational force. The usual phrase that explains what is happening is that matter tells space-time how to bend, and space-time tells matter how to move. To define the metric within GraviPy, you need to start by creating a metric tensor object. You can do so with this statement:
Metric = diag(-(1-2*M/r), 1/(1-2*M/r), r**2, r**2*sin(theta)**2)
In this example, you'll notice a new variable, named M
, which
represents the mass of the matter that is creating this space-time
distortion. If it is an item that will remain static, you don't need
to do anything extra. But, there is no reason that it should remain static.
If it is something that can change, you need to use the
symbols
command to define it. In its most general form, the metric tensor is a
4-by-4 matrix of elements. The above example is of a
diagonal metric where only the non-zero elements are along the diagonal.
Once you have this tensor object, you can define the metric based on it
with the statement:
g = MetricTensor('g', x, Metric)
This example gives the function the metric definition and the coordinate system to be used.
To access the various elements, you can use indices that follow the same format as above. For example, you could use the following:
g(1, 1) -> 2M/r-1
For both vectors and tensors, you can use a special index called
All
that will give you every possible value for the index in question. For
example, you can get the entire list of coordinates with this:
x(-All) -> [t r theta phi]
You can get all of the elements of the metric tensor with the statement:
g(All, All)
Now that you have a set of coordinates and a metric tensor, there are
a number of standard tensors that need to be calculated to help work out
what the geometry of space-time actually looks like and how things like
light beams travel through this geometry. The GraviPy module includes
the tensor classes Christoffel
,
Riemann
, Ricci
,
Einstein
and
Geodesic
. The Christoffel
,
Riemann
and Ricci
tensors
depend only on
the metric. Therefore, they all have very similar forms to create new
instances and get results out of them. For example, you can define the
Christoffel
tensor values with this statement:
Ga = Christoffel('Ga', g)
You can get individual elements with indices, just like with the metric tensor. But, some of these tensors can have a number of uninteresting zero entries. So, you can get the non-zero values with the statement:
Ga.components
This returns a dictionary where the keys are the coordinate sets for where this particular value is located, as well as the actual non-zero value at the point.
The Einstein
tensor is the one used in the actual
Einstein equations for space-time, and they are a little different. In
order to calculate them, you first need to calculate the
Ricci
tensor
with this statement:
Ri = Ricci('Ri', g)
Once you have this tensor, you can calculate the
Einstein
tensor with this:
G = Einstein('G', Ri)
Before I leave off, I should look at two techniques that are absolutely necessary to doing general relativity. The first is index contraction. This is where you end up summing values over two of the indices in a tensor. In Python, you can do this by explicitly summing with:
Rm = Riemann('Rm', g)
ricci = sum([Rm(i, All, k, All)*g(-i, -k) for i, k in
↪list(variations(range(1, 5), 2, True))], zeros(4))
These two lines are equivalent to the above single creation of the
Ricci
tensor. In many cases, complicated calculations are not simplified
automatically.
This means that you need to do this simplification explicitly
with the command:
ricci.simplify()
The other important technique is to calculate geodesics, which are equations that define how light beams travel in this space-time. You need to create a new variable to handle the world line parameter for these equations:
tau = symbols('\\tau')
Now, you can calculate the geodesics with this:
w = Geodesic('w', g, tau)
Again, you can use 1-based indices to access the various non-trivial equations for your space-time of interest.
With SymPy, you can do all sorts of symbolic calculations normally reserved for programs like Maple, Maxima or Mathematica. Building on these capabilities, GraviPy lets you play with space-time and do calculations to determine curvature and gravitational effects. There is not a great deal of information available on-line explaining what GraviPy can do. Your best option is to download the source files, which include a tutorial iPython notebook, even if you install GraviPy through pip. Now you can do your gravitational research all from the comfort of Python. If you are a Python fan, this module will let you do interesting research work in your favourite language.