The expanding background
Cosmology is built on a single observational fact – the universe is homogeneous and isotropic on large scales – and on a theory that says how spacetime responds to its energy content – general relativity. From these two ingredients we will derive the Friedmann equation, the equation that controls the expansion rate of a smooth, expanding universe. We will derive it twice, first by a Newtonian argument and then properly from the Einstein equations, code both up, and use the result to plot the expansion history of our universe. Every later chapter sits on top of what we build here.
The FRW metric
The assumption of homogeneity and isotropy uniquely fixes the spacetime metric, up to an overall time-dependent scale factor and a curvature parameter . The result is the Friedmann-Robertson-Walker (FRW) metric. In the flat case (), which is what Planck observations select to high accuracy, the line element in cosmic time is
The coordinates are comoving: a galaxy that has no peculiar motion stays at fixed as the universe expands. The physical distance between two such galaxies is times their coordinate separation. The function – the scale factor – is the only dynamical degree of freedom of the background.
The components of the metric tensor are
The only non-zero partial derivatives are for , where dot denotes .
Friedmann from Newtonian energy conservation
Before going through the general-relativistic derivation, it is illuminating to see how far a purely Newtonian argument gets us. Imagine a homogeneous, isotropic dust universe – non-relativistic matter only, with density and no pressure. Consider a thin spherical shell of comoving radius . Its physical radius at time is . By Newton's shell theorem, the gravitational force on a test particle of mass sitting on the shell depends only on the matter inside the shell:
Energy conservation for this test particle reads
Dividing by and rearranging,
The first term sources expansion from the gravitational energy of the matter; the second term, which depends on the binding energy of the test particle, is the analogue of the curvature term in the relativistic Friedmann equation. For a flat universe we take , giving
This is correct in form but the argument has two flaws. First, it picks a centre (the centre of the sphere), in apparent violation of homogeneity. Second, here is rest-mass density only: photons, neutrinos, dark energy, anything with a non-trivial pressure or relativistic energy, cannot be included. Both issues are fixed once we re-derive the equation from general relativity. The end result – equation Eq. – will turn out to be correct, but with reinterpreted as the total energy density of everything that gravitates.
Christoffel symbols of the FRW metric
In general relativity, the analogue of “acceleration due to a force” is the geodesic equation, which involves the Christoffel symbols of the metric. The Christoffel symbols are not tensors; they are coordinate-dependent objects built from first derivatives of the metric. Their definition is
The combination of partial derivatives on the right is symmetric in , so . Geometrically, measures how a basis vector changes when you parallel-transport it along .
For our FRW metric, only is non-zero. We work through the possibilities case by case.
Case .
The inverse metric is diagonal, so the sum over in Eq. reduces to :
has only the component, which is the constant , so . Similarly . We are left with
For (spatial), , and for any other combination it vanishes. So
Case (spatial).
Now restricts (no sum) and gives a factor :
for all components (the metric depends only on ). The first two terms are non-zero only if a spatial index of is being differentiated by , i.e. when one of equals and the other equals :
By symmetry ; all other vanish. Putting both cases together:
The Ricci tensor
The Riemann curvature tensor is built from the Christoffels and their derivatives:
The Ricci tensor is its contraction on the first and third indices.
We need only the components and . Let us compute first.
.
We use Eq.: for all , so the first and third terms vanish. The second term is
The fourth term picks up only , contributions:
Combining,
.
By isotropy, must be proportional to , so it is enough to compute :
Term by term:
: only is non-zero, and .
: each depends only on , so its derivative is zero.
: only with is non-zero (); we then need . Product .
: the non-zero contributions are (twice, since the index pair can be swapped, giving as well). Total .
Putting it together,
By isotropy,
Ricci scalar.
The Einstein equation
Einstein's field equation in its most familiar form is
The stress-energy tensor of a perfect fluid – a fluid characterised entirely by its rest-frame energy density and isotropic pressure – is
with the fluid four-velocity. In the rest frame (comoving with the cosmic fluid), , , and we get
The component.
Compute :
Setting this equal to ,
This is the Friedmann equation. It has the same form as the Newtonian Eq., but now is the total energy density of every species that gravitates – matter, radiation, dark energy.
The component.
A parallel calculation gives
(no sum on ), and equating to produces the acceleration equation:
Pressure gravitates. A fluid with gives : accelerated expansion. This is what dark energy does.
Continuity and the equation of state
The Bianchi identity combined with the Einstein equation gives . For our perfect fluid, the component evaluates to
This is just energy conservation: dilutes as from volume expansion, plus an extra term for fluids with non-zero pressure. Combined with a constant equation of state , integration is immediate:
The three cases relevant for us:
(matter): . Standard mass-density dilution.
(radiation): . The extra factor of is the redshift of photon energies.
(cosmological constant): const.
Putting the pieces together: in CDM
The total energy density today divides cleanly into five species: photons (), neutrinos (), cold dark matter (), baryons (), and the cosmological constant (). Each scales with at a rate fixed by its equation of state:
Substituting into the Friedmann equation gives
The photon and neutrino contributions are evaluated from first principles. The Stefan-Boltzmann law gives
for the photons. Each of the three families of nearly-massless neutrinos contributes the same expression with two modifications: a factor from Fermi-Dirac vs Bose-Einstein statistics, and a factor from the temperature ratio established at annihilation. With standing in for the effective number of neutrino species,
For cold dark matter and baryons we are given and , so
with . Flatness fixes the last piece:
Matter-radiation equality.
Setting ,
Matter- equality lies at , i.e. .
The code: physical constants
Now we start coding. The first chapter-1 cell defines the physical constants we need. In our unit system, only the conversion factors carry dimensions.
# Physical constants used throughout the tutorial.
c_km_s = 2.99792458e5 # speed of light (km/s)
k_B = 1.380649e-23 # Boltzmann constant (J/K)
h_P = 6.62607015e-34 # Planck constant (J s)
m_e = 9.1093837015e-31 # electron mass (kg)
m_H = 1.673575e-27 # hydrogen atom mass (kg)
m_He4 = 6.646479073e-27 # He-4 atom mass (kg)
not4 = m_He4 / m_H # He/H mass ratio (~3.97, not exactly 4)
sigma_T = 6.6524587321e-29 # Thomson cross section (m^2)
G = 6.67430e-11 # gravitational constant (m^3/kg/s^2)
Mpc_in_m = 3.0856775814913673e22 # 1 Mpc in metres
sigma_SB = 5.670374419e-8 # Stefan-Boltzmann constant (W/m^2/K^4)
These are SI values. Only c_km_s, G, sigma_SB, sigma_T, m_H, and Mpc_in_m get used in chapter 1; the rest are needed for recombination in chapter 2.
The code: init_background
This function converts the Planck-2018 parameters in the params dictionary into the internal densities used by the Friedmann evaluator. We adopt the CAMB unit convention: instead of storing , we store
This combination is convenient because it absorbs the awkward factor of the Friedmann equation, and because it has natural units of inverse-length-squared once is in force. In our dictionary the keys will be grhog (), grhornomass (), grhoc (), grhob (), and grhov (). The Friedmann equation becomes
Pulling out a common factor gives the helper quantity
in which the Friedmann equation reads simply , and the conformal-time integrand becomes .
init_background
def init_background(params):
"""Convert cosmological parameters to internal `grho_i` densities,
where grho_i = 8*pi*G*rho_{i,0} in Mpc^-2 (CAMB convention).
"""
h = params['h']
H0 = 100 * h / c_km_s # H_0 in 1/Mpc (c = 1)
grhocrit_h2 = 3 * (100.0 / c_km_s)**2 # 3*(H_100/c)^2 in 1/Mpc^2
# Photon energy density from the blackbody formula
T_cmb = params['T_cmb']
rho_gamma = 4 * sigma_SB / (c_km_s * 1e3)**3 * T_cmb**4 # kg/m^3
H100_SI = 100 * 1e3 / Mpc_in_m # 1/s
rho_crit_100 = 3 * H100_SI**2 / (8 * np.pi * G) # kg/m^3
omega_gamma = rho_gamma / rho_crit_100
# Internal densities: 8*pi*G*rho_{i,0} in Mpc^-2
grhog = grhocrit_h2 * omega_gamma # photons (a^-4)
grhornomass = grhog * 7/8 * (4/11)**(4/3) * params['N_eff'] # massless nu (a^-4)
grhoc = grhocrit_h2 * params['omega_c_h2'] # CDM (a^-3)
grhob = grhocrit_h2 * params['omega_b_h2'] # baryons (a^-3)
# Cosmological constant: Omega_L = 1 - Omega_m - Omega_r (flat universe)
omega_m = (params['omega_c_h2'] + params['omega_b_h2']) / h**2
omega_r = omega_gamma * (1 + 7/8 * (4/11)**(4/3) * params['N_eff']) / h**2
grhov = grhocrit_h2 * (1 - omega_m - omega_r) * h**2 # Lambda (const)
# Two thermodynamic quantities used much later (chapter 2): the
# Thomson opacity prefactor, and the He/H number-fraction f_He.
rho_b_SI = params['omega_b_h2'] * rho_crit_100
n_H_Mpc = (1 - params['Y_He']) * rho_b_SI / m_H * Mpc_in_m**3
akthom = (sigma_T / Mpc_in_m**2) * n_H_Mpc
f_He = params['Y_He'] / (not4 * (1 - params['Y_He']))
return {
'H0': H0, 'h': h,
'grhog': grhog, 'grhornomass': grhornomass,
'grhoc': grhoc, 'grhob': grhob, 'grhov': grhov,
'akthom': akthom, 'f_He': f_He,
'Y_He': params['Y_He'], 'T_cmb': T_cmb,
}
Reading the code line by line.
H0 = 100*h/c_km_s: converted to natural units by dividing out .grhocrit_h2: . The units check: is in km/s/Mpc, in km/s, so is in .rho_gamma = 4*sigma_SB/(c_km_s*1e3)**3 * T_cmb**4: the standard from blackbody radiation, written as in SI units. The factor(c_km_s*1e3)**3converts from km/s to m/s and cubes it.grhog,grhornomass: the photon contribution times gives the total radiation density; separating out the photon and neutrino pieces lets the perturbation code in chapter 3 evolve them separately.grhoc,grhob: direct conversion of via .grhov: closure condition multiplied back by to get .akthom,f_He: thermodynamic constants we will need in chapter 2; harmless to compute here.
The code: and
The Friedmann equation in our units becomes three short helper functions.
def total_density_a4(a, bg):
"""Returns 8*pi*G*rho_tot*a^4 -- the polynomial in eq. (above) that
sits inside the square root of the Friedmann equation."""
return (bg['grhog'] + bg['grhornomass']
+ (bg['grhoc'] + bg['grhob']) * a
+ bg['grhov'] * a**4)
def dtau_da(a, bg):
"""d(eta)/da = 1/(a^2 H) = sqrt(3 / total_density_a4), in Mpc."""
return np.sqrt(3.0 / total_density_a4(a, bg))
def hubble(a, bg):
"""Hubble parameter H(a) = sqrt(total_density_a4 / 3) / a^2, in 1/Mpc (c=1)."""
return np.sqrt(total_density_a4(a, bg) / 3.0) / a**2
The function total_density_a4(a, bg) implements Eq.: it returns the polynomial . The radiation contribution is constant in inside this function because we pulled out an overall ; the matter term scales linearly with because we pulled out the same from an ; and the term picks up . hubble(a, bg) then returns directly via Eq., and dtau_da(a, bg) returns the integrand we need for the conformal-time integral.
The code: conformal time and sound horizon
The conformal time – the maximum comoving distance light can have travelled since the Big Bang – is
The comoving distance from us to the surface of last scattering is then , with and . We also need the comoving sound horizon, , with the sound speed of the baryon-photon plasma and . This will appear constantly in later chapters as the scale that sets the CMB acoustic peak positions.
def conformal_time(a, bg):
"""eta(a) = int_0^a da'/(a'^2 H), in Mpc."""
a = np.atleast_1d(a)
out = np.array([
integrate.quad(dtau_da, 0, ai, args=(bg,), limit=100, epsrel=1e-8)[0]
for ai in a])
return out.squeeze()
def sound_horizon(a, bg):
"""Comoving sound horizon r_s(a) = int_0^a c_s da'/(a'^2 H), in Mpc."""
def integrand(ap):
R = 0.75 * bg['grhob'] * ap / bg['grhog']
return 1.0 / np.sqrt(total_density_a4(ap, bg) * (1.0 + R))
return integrate.quad(integrand, 0, a)[0]
Both use scipy.integrate.quad, an adaptive Gauss-Kronrod quadrature, with relative tolerance . The integrals are well-behaved at the lower limit because is finite there (the denominator approaches the constant as ).
The code: driver
A small convenience function that assembles the background dictionary and computes the most-needed derived quantities.
build_background
def build_background(params):
"""Top-level: prepare the background for downstream use."""
bg = init_background(params)
bg['tau0'] = float(conformal_time(1.0, bg))
a_eq = (bg['grhog'] + bg['grhornomass']) / (bg['grhoc'] + bg['grhob'])
bg['a_eq'] = a_eq
bg['z_eq'] = 1.0 / a_eq - 1.0
bg['tau_eq'] = float(conformal_time(a_eq, bg))
return bg
The formula for comes from setting . In our convention,
Run the driver and inspect the numbers:
bg = build_background(params)
print(f"H_0 = {bg['H0']:.4e} 1/Mpc = {bg['H0']*c_km_s:.2f} km/s/Mpc")
print(f"tau_0 = {bg['tau0']:.1f} Mpc")
print(f"z_eq = {bg['z_eq']:.0f}")
print(f"tau_eq = {bg['tau_eq']:.2f} Mpc")
The output should read:
H_0 = 2.2469e-04 1/Mpc = 67.36 km/s/Mpc
tau_0 = 14174.6 Mpc
z_eq = 3403
tau_eq = 112.82 Mpc
A comoving age of is the radius of the observable universe; the angular distance to the CMB is a touch smaller because we observe it from rather than . The fact that – much less than – says that matter-radiation equality happened very early; almost the entire history of the universe has been matter- and dark-energy-dominated.
Plot:
We now have everything we need to plot the expansion history. The plot script below uses only the functions just defined.
a_grid = np.geomspace(1e-7, 1.0, 400)
z_grid = 1.0 / a_grid - 1.0
H_grid = np.array([hubble(ai, bg) for ai in a_grid])
# Where does matter equal Lambda?
rho_m = (bg['grhoc'] + bg['grhob']) / a_grid**3
rho_L = bg['grhov'] * np.ones_like(a_grid)
a_lambda = a_grid[np.argmin(np.abs(rho_m - rho_L))]
z_lambda = 1.0 / a_lambda - 1.0
fig, ax = plt.subplots()
sel = (z_grid > 1e-3) & (z_grid < 1e5)
ax.loglog(z_grid[sel], H_grid[sel] / bg['H0'], 'k-', lw=1.6)
ax.axvline(bg['z_eq'], color='C3', ls=':', alpha=0.7)
ax.text(bg['z_eq']*1.3, 2, f"$z_\\mathrm{{eq}} = {bg['z_eq']:.0f}$", color='C3')
ax.axvline(z_lambda, color='C2', ls=':', alpha=0.7)
ax.text(z_lambda*1.3, 2, f"$z_\\Lambda = {z_lambda:.2f}$", color='C2')
ax.set_xlabel('Redshift $z$')
ax.set_ylabel('$H(z) / H_0$')
ax.set_title('Hubble expansion history')
ax.grid(True, which='both', alpha=0.3)
plt.show()
The curve has three regimes. At , is nearly flat – dark-energy domination. Between and matter dominates and . Above radiation dominates and . The change of slope at is subtle on a log-log plot but real – it controls the relative heights of the CMB acoustic peaks (chapter 5).
Plot: the composition of the universe
The density fractions make the era transitions visually explicit.
rho_r = (bg['grhog'] + bg['grhornomass']) / a_grid**4
rho_m = (bg['grhoc'] + bg['grhob']) / a_grid**3
rho_L = bg['grhov'] * np.ones_like(a_grid)
rho_tot = rho_r + rho_m + rho_L
fig, ax = plt.subplots()
ax.loglog(a_grid, rho_m/rho_tot, label='Matter', color='C0', lw=1.6)
ax.loglog(a_grid, rho_r/rho_tot, label='Radiation', color='C3', lw=1.6)
ax.loglog(a_grid, rho_L/rho_tot, label=r'$\Lambda$', color='C2', lw=1.6)
ax.axvline(bg['a_eq'], color='gray', ls=':', alpha=0.5)
ax.axvline(a_lambda, color='gray', ls=':', alpha=0.5)
ax.text(bg['a_eq']*1.3, 1.2e-5, r'$a_\mathrm{eq}$', color='gray')
ax.text(a_lambda*1.3, 1.2e-5, r'$a_\Lambda$', color='gray')
ax.set_xlabel('Scale factor $a$')
ax.set_ylabel(r'$\Omega_i(a)$')
ax.set_title('Composition of the universe')
ax.set_ylim(1e-6, 2); ax.set_xlim(1e-7, 1)
ax.legend(loc='lower right'); ax.grid(True, which='both', alpha=0.3)
plt.show()
The radiation (red) and matter (blue) curves cross at , corresponding to . The matter and curves cross at , corresponding to . Reading off : , , .
Looking ahead
The bg dictionary now contains everything chapter 2 needs to compute the recombination history: , the densities of each species, the Thomson opacity prefactor akthom, the He/H number fraction f_He, and the helper functions total_density_a4, dtau_da, hubble, conformal_time, sound_horizon. Keep your notebook open. We will pick up directly from bg.