Colossus tutorial: Cosmology Basics

Welcome to the Colossus cosmology tutorial.

In [1]:
from __future__ import print_function 
import matplotlib.pyplot as plt
import copy
%matplotlib inline

Setting, changing, and adjusting cosmologies

Let's begin by importing the Colossus cosmology module:

In [2]:
from colossus.cosmology import cosmology

If this call results in an error, please see the chapter Installation in the documentation. Let's set a cosmology, e.g. the most recent Planck cosmology. This function returns a cosmology object:

In [3]:
cosmo = cosmology.setCosmology('planck18')

Colossus comes with more than 20 hard-coded cosmologies:

In [4]:
for k in cosmology.cosmologies:
    print(k)
planck18-only
planck18
planck15-only
planck15
planck13-only
planck13
WMAP9-only
WMAP9-ML
WMAP9
WMAP7-only
WMAP7-ML
WMAP7
WMAP5-only
WMAP5-ML
WMAP5
WMAP3-ML
WMAP3
WMAP1-ML
WMAP1
illustris
bolshoi
multidark-planck
millennium
EdS
powerlaw

If we want to retrieve details about a given cosmology, we can just print the cosmology object:

In [5]:
print(cosmo)
Cosmology "planck18" 
    flat = True, Om0 = 0.3111, Ode0 = 0.6888, Ob0 = 0.0490, H0 = 67.66, sigma8 = 0.8102, ns = 0.9665
    de_model = lambda, relspecies = True, Tcmb0 = 2.7255, Neff = 3.0460, powerlaw = False

In Colossus, the cosmology is set globally. This means that we can obtain the current cosmology object from anywhere in the code:

In [6]:
cosmo = cosmology.getCurrent()
print(cosmo.name)
planck18

However, cosmologies are objects, meaning that we can keep multiple cosmologies around. This can be useful when switching back and forth between different cosmologies, for example:

In [7]:
cosmo2 = cosmology.setCosmology('WMAP9')
cosmology.setCurrent(cosmo2)
print(cosmology.getCurrent().name)
cosmology.setCurrent(cosmo)
print(cosmology.getCurrent().name)
WMAP9
planck18

Of course, you can also set a user-defined cosmology. In this case, arguments to the constructor of the Cosmology object are passed as a dictionary. Note that there are many more possible arguments than shown here, as described in the documentation.

In [8]:
my_cosmo = {'flat': True, 'H0': 72.0, 'Om0': 0.25, 'Ob0': 0.043, 'sigma8': 0.8, 'ns': 0.97}
cosmo = cosmology.setCosmology('my_cosmo', **my_cosmo)
print(cosmo)
Cosmology "my_cosmo" 
    flat = True, Om0 = 0.2500, Ode0 = 0.7499, Ob0 = 0.0430, H0 = 72.00, sigma8 = 0.8000, ns = 0.9700
    de_model = lambda, relspecies = True, Tcmb0 = 2.7255, Neff = 3.0460, powerlaw = False

By default, relativistic species (neutrinos and photons) are included, meaning that they contribute to the energy density of the universe and are subtracted from the remaining components. Thus, when setting the flat cosmology above with $\Omega_{\rm m,0}=0.25$ and relspecies=True, the density of dark energy is not 0.75 but rather 0.75 minus the density of relativistic species. In practice, the relativistic contribution is small except at very high redshifts and makes a negligible contribution to most calculations.

It is possible to change parameters of a cosmology after it has been created, but we need to alert Colossus of the change because it needs to clear the cache:

In [9]:
cosmo.Om0 = 0.27
cosmo.checkForChangedCosmology()
print(cosmo)
Cosmology: Detected change in cosmological parameters.
Cosmology "my_cosmo" 
    flat = True, Om0 = 0.2700, Ode0 = 0.7299, Ob0 = 0.0430, H0 = 72.00, sigma8 = 0.8000, ns = 0.9700
    de_model = lambda, relspecies = True, Tcmb0 = 2.7255, Neff = 3.0460, powerlaw = False

All the default parameter sets above represent flat $\Lambda CDM$ cosmologies, i.e. model dark energy as a cosmological constant and contain no curvature. To add curvature, the default for flatness must be overwritten, and the dark energy content of the universe must be set explicitly (for flat cosmologies, it is computed from the matter and relativistic contributions). We can either create an updated parameter dictionary and pass it as params, or we can additionally use keyword arguments that overwrite the content of params:

In [10]:
cosmo = cosmology.setCosmology('planck_curv', params = cosmology.cosmologies['planck18'], flat = False, Ode0 = 0.75)
print(cosmo.Ok0)
-0.06119138647952183

Multiple models for the dark energy equation of state parameter $w(z)$ are implemented, namely a cosmological constant ($w = -1$), a constant $w$, a linearly varying $w(z) = w_0 + w_a (1 - a)$, and arbitrary user-supplied functions for $w(z)$. To set, for example, a linearly varying EOS, we change the de_model parameter. We can evaluate $w(z)$ using the wz() function:

In [11]:
cosmo = cosmology.setCosmology('planck_w0wa', params = cosmology.cosmologies['planck18'], 
                               de_model = 'w0wa', w0 = -0.8, wa = 0.1)
print(cosmo.wz(0.5))
-0.7666666666666667

We can implement more exotic models by supplying an arbitrary function for $w(z)$:

In [12]:
def wz_func(z):
    return -1.0 + 0.1 * z

cosmo = cosmology.setCosmology('planck_wz', params = cosmology.cosmologies['planck18'], 
                               de_model = 'user', wz_function = wz_func)
print(cosmo.wz(0.5))
-0.95

Note that for the cases of the $w_0$-$w_a$ and user-defined cosmologies, the range of future redshifts is restricted to $z=-0.9$ by default because the dark energy component's exponential growth can otherwise lead to overflow errors.

Standard calculations: Densities, distances, times

Let's perform some simple calculations such as cosmological densities, distances, and times. As before, we begin by setting a particular cosmology:

In [13]:
cosmo = cosmology.setCosmology('planck18')

Using the object returned by this function (or cosmology.getCurrent()), we can evaluate standard cosmological quantities. For example, let's print the age of the universe today. The cosmology functions always take redshift rather than scale factor as an argument:

In [14]:
print(cosmo.age(0.0))
13.786206419159683

Note that the returned age is in Gyr. The colossus cosmology functions adhere to the following unit system:

  • Length: comoving Mpc/h
  • Mass: Msun
  • Wavenumber: comoving h/Mpc
  • Time: Gyr
  • Density: physical Msun * h^2 / kpc^3

Virtually all Colossus functions accept numpy arrays as input and return arrays of the same dimensions:

In [15]:
import numpy as np
z = np.array([0.0, 1.0, 2.0, 3.0])
cosmo.angularDiameterDistance(z)
Out[15]:
array([-1.76263959e-15,  1.14871120e+03,  1.19712269e+03,  1.10009255e+03])

The colossus cosmology module works to pretty extreme redshifts, namely between $z = -0.995$ and $200$ (though some functions may have stricter limits, for example $z>=0$). For example, let's plot the relative density contribution of the various components in the Planck cosmology:

In [16]:
zp1 = 10**np.arange(-1.0, 2.0, 0.05)
z = zp1 - 1.0

O_b = cosmo.Ob(z)
O_dm = cosmo.Om(z) - O_b
O_de = cosmo.Ode(z)
O_gamma = cosmo.Ogamma(z)
O_nu = cosmo.Onu(z)

plt.figure()
plt.loglog()
plt.xlabel('z+1')
plt.ylabel('Fraction of total density')
plt.plot(zp1, O_dm, '-', label = 'Dark matter')
plt.plot(zp1, O_b, '-', label = 'Baryons')
plt.plot(zp1, O_de, '-', label = 'Dark Energy')
plt.plot(zp1, O_gamma, '-', label = 'Photos')
plt.plot(zp1, O_nu, '-', label = 'Neutrinos')
plt.legend(loc = 4);

There is more to the functions mentioned above: they can also give us their own inverse, e.g. $z(t)$ instead of $t(z)$:

In [17]:
z = np.array([0.0, 1.0, 2.0, 3.0])
t = cosmo.age(z)
z2 = cosmo.age(t, inverse = True)
print(z2)
[-4.41276515e-05  9.99787190e-01  1.99991520e+00  3.00002259e+00]

The slight errors in the recovered redshifts give a hint as to how colossus works internally: the majority of functions rely on interpolation for performance. The interpolating splines can be inverted numerically. If, for some reason, you really need the exact calculation, you can set interpolation = False in the constructor, or at a later time.

Once again using interpolating splines, we can also evaluate the derivatives of many functions, e.g. $dt/dz$ or $d^2t/dz^2$:

In [18]:
cosmo.age(z, derivative = 1)
Out[18]:
array([-14.45267824,  -4.05174963,  -1.59720974,  -0.79551907])
In [19]:
cosmo.age(z, derivative = 2)
Out[19]:
array([21.19751241,  4.41082623,  1.26979433,  0.48567331])

We can even combine the inverse and derivative arguments to evaluate the derivative of the inverse, e.g. $dz/dt$:

In [20]:
t = cosmo.age(z)
cosmo.age(t, inverse = True, derivative = 1)
Out[20]:
array([-0.06920297, -0.24626517, -0.62714306, -1.2568361 ])

A word of warning: while the documentation quotes a particular accuracy for each function, the accuracy of the derivatives and inverses is not guaranteed. There are many more functions related to densities, distances, and times. Please consult the documentation of the cosmology module for an exhaustive list.