import pandas as pd
import healpy as hp
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inlineIn this tutorial we will load CMB power spectra generated by the cosmological parameters measured by Planck, we will create a realization of the Spherical Harmonics coefficients
# filter out all warnings
import warnings
warnings.filterwarnings("ignore")print(plt.style.available)plt.style.use('seaborn-talk')First we go to the Planck data release page at IPAC, I prefer plain web pages instead of the funky javascript interface of the Planck Legacy Archive. The spectra are relegated to the Ancillary data section:
https://irsa.ipac.caltech.edu/data/Planck/release_3/ancillary-data/
We can choose one of the available spectra, I would like the spectrum from theory, BaseSpectra, and download it locally:
!wget https://irsa.ipac.caltech.edu/data/Planck/release_3/ancillary-data/cosmoparams/COM_PowerSpect_CMB-base-plikHM-TTTEEE-lowl-lowE-lensing-minimum-theory_R3.01.txtNow, I couldn’t find any documentation about the format of those spectra, there are no units and we don’t know if those spectra are just the CAMB software, anyway I prefer to cross check with some public plot.
pandas has trouble with the # at the beginning of the title line, the easiest is to edit that out with a file editor.
!head -3 COM_PowerSpect_CMB-base-plikHM-TTTEEE-lowl-lowE-lensing-minimum-theory_R3.01.txtYou should not have # at the beginning of the first line
input_cl = pd.read_csv("COM_PowerSpect_CMB-base-plikHM-TTTEEE-lowl-lowE-lensing-minimum-theory_R3.01.txt",
delim_whitespace=True, index_col=0)input_cl.head()input_cl.plot(logx=True, logy=True, grid=True);We can compare this to one of the plots from the Planck wiki:

input_cl.TT.plot(logx=False, logy=False, grid=True)
plt.ylabel("$\dfrac{\ell(\ell+1)}{2\pi} C_\ell~[\mu K^2]$")
plt.xlabel("$\ell$")
plt.xlim([50, 2500]);Very good, they agree, therefore the data are in $ C_$, let’s transform to
input_cl.head()len(input_cl)lmax = input_cl.index[-1]
print(lmax)cl = input_cl.divide(input_cl.index * (input_cl.index+1) / (np.pi*2), axis="index")cl /= 1e12cl.head()healpy tools expect an array starting from zero, so let’s extend that.
cl = cl.reindex(np.arange(0, lmax+1))cl.head()cl = cl.fillna(0)cl.head()Generate a CMB map realization
The power spectrum gives us the distribution of power with the angular scale, if we want to create a map we could use hp.synfast, however the realization will be different each time that we run synfast, we could always use the same seed to make it reproducible. However, in case we want to have the same realization of a CMB map at different resolutions (hp.alm2map.
We also want to make sure that the
seed = 583
np.random.seed(seed)alm = hp.synalm((cl.TT, cl.EE, cl.BB, cl.TE), lmax=lmax, new=True)high_nside = 1024
cmb_map = hp.alm2map(alm, nside=high_nside, lmax=lmax)We can double check that we got the order of magnitude correct, for example we can compare with one of the published Planck maps, the scale is
hp.mollview(cmb_map[0], min=-300*1e-6, max=300*1e-6, unit="K", title="CMB Temperature")Polarization is generally an order of magnitude lower:
for m in cmb_map[1:]:
hp.mollview(m, min=-30*1e-6, max=30*1e-6, unit="K", title="CMB Polarization")Double check the spectra of the maps
As a final check, we can compare the spectra of the output maps to the input spectra.
cl_from_map = hp.anafast(cmb_map, lmax=lmax, use_pixel_weights=True) * 1e12 # in muK^2
ell = np.arange(cl_from_map.shape[1])
cl_from_map *= ell * (ell+1) / (2*np.pi)np.median(cl_from_map, axis=1)plt.plot(cl_from_map[0], label="map TT")
input_cl.TT.plot(logx=False, logy=False, grid=True)
plt.legend();input_cl.plot(logx=True, logy=True, grid=True)
plt.plot(cl_from_map[0], label="map TT")
plt.plot(cl_from_map[1], label="map EE")
plt.plot(cl_from_map[2], label="map BB")
plt.plot(cl_from_map[3], label="map TE")
#plt.plot(cl_from_map[4], label="map EB")
#plt.plot(cl_from_map[5], label="map TB")
plt.ylabel("$\dfrac{\ell(\ell+1)}{2\pi} C_\ell~[\mu K^2]$")
plt.xlabel("$\ell$")
plt.legend();The scale is fine, we notice below that going through the discretization step of mapping the Spherical Harmonics into a map in pixel space and then trying to estimate the power spectrum again from that introduces some noise, on top of that, there is the issue of “Cosmic variance”, i.e. the
plt.plot(cl_from_map[0][:-2], label="map TT")
input_cl.TT.plot(logx=False, logy=False, grid=True)
plt.ylabel("$\dfrac{\ell(\ell+1)}{2\pi} C_\ell~[\mu K^2]$")
plt.xlabel("$\ell$")
plt.legend();plt.plot(cl_from_map[1][:-2], label="map EE")
input_cl.EE.plot(logx=False, logy=False, grid=True)
plt.ylabel("$\dfrac{\ell(\ell+1)}{2\pi} C_\ell~[\mu K^2]$")
plt.xlabel("$\ell$")
plt.legend();plt.plot(cl_from_map[2], label="map BB")
input_cl.BB.plot(logx=False, logy=False, grid=True)
plt.ylabel("$\dfrac{\ell(\ell+1)}{2\pi} C_\ell~[\mu K^2]$")
plt.xlabel("$\ell$")
plt.legend();Save the to disk
We can save in a standard FITS format
hp.write_alm(f"Planck_bestfit_alm_seed_{seed}.fits", alm, overwrite=True)!ls *alm*fitsCreate a lower resolution map from the same
The
low_nside = 32
low_nside_lmax = 3*low_nside - 1
low_nside_cmb_map = hp.alm2map(alm, nside=low_nside)cl_low_nside = hp.anafast(low_nside_cmb_map, use_pixel_weights=True, lmax=low_nside_lmax) * 1e12
ell_low_nside = np.arange(cl_low_nside.shape[1])
cl_low_nside *= ell_low_nside * (ell_low_nside+1) / (2*np.pi)plt.plot(cl_from_map[0], label="map TT")
plt.plot(cl_low_nside[0], label="low nside map TT")
input_cl.TT.plot(logx=False, logy=False, grid=True)
plt.axhline(low_nside_cmb_map.std()**2)
plt.ylabel("$\dfrac{\ell(\ell+1)}{2\pi} C_\ell~[\mu K^2]$")
plt.xlabel("$\ell$")
plt.legend();We notice that the spectra is dominated by noise. The issue arises from the fact that Spherical Harmonics transforms are well defined only for band-limited signals, that means that at high
Unfortunately the lmax argument of hp.map2alm doesn’t help because it is designed to specify the
We can fix the issue by zeroing the hp.almxfl, it automatically assumes that if a input
clip = np.ones(low_nside_lmax+1)
alm_clipped = np.array([hp.almxfl(each, clip) for each in alm])low_nside_cmb_map_clipped = hp.alm2map(alm_clipped, nside=low_nside)cl_low_nside_clipped = hp.anafast(low_nside_cmb_map_clipped, use_pixel_weights=True, lmax=low_nside_lmax) * 1e12
cl_low_nside_clipped *= ell_low_nside * (ell_low_nside+1) / (2*np.pi)plt.plot(cl_from_map[0], label="map TT", alpha=.5)
plt.plot(cl_low_nside_clipped[0], label="low nside map TT clipped", alpha=.8)
input_cl.TT.plot(logx=False, logy=False, grid=True)
plt.axvline(2*low_nside, color="black", linestyle="--", label="ell=2*N_side")
plt.xlim([0, 800])
plt.ylabel("$\dfrac{\ell(\ell+1)}{2\pi} C_\ell~[\mu K^2]$")
plt.xlabel("$\ell$")
plt.legend();Thanks to this the spectra is accurate at least up to
Finally we can check by eye that the large scale features of the map (focus on a hot or cold spot) are the same in the 2 maps
hp.mollview(cmb_map[0], title="CMB T map at $N_{side}$= " + str(high_nside))
hp.mollview(low_nside_cmb_map_clipped[0], title="CMB T map at $N_{side}$= " + str(low_nside))hp.write_map(f"map_nside_{low_nside}_from_Planck_bestfit_alm_seed_{seed}_K_CMB.fits", low_nside_cmb_map_clipped, overwrite=True)!ls *.fitsClip to a lower
It was easy to use hp.almxfl to zero the coefficient above a threshold, however, the array remains very large, especially inconvenient if you need to save it to disk.
We want instead to actually clip the coefficients using the hp.Alm.getidx function which gives us the indices of a specific
lclip = low_nside_lmax%%time
clipping_indices = []
for m in range(lclip+1):
clipping_indices.append(hp.Alm.getidx(lmax, np.arange(m, lclip+1), m))
print(clipping_indices[:2])
clipping_indices = np.concatenate(clipping_indices)alm_really_clipped = [each[clipping_indices] for each in alm]low_nside_cmb_map_really_clipped = hp.alm2map(alm_clipped, nside=low_nside)hp.mollview(low_nside_cmb_map_clipped[0], title="zeroed - CMB T map at $N_{side}$= " + str(low_nside))
hp.mollview(low_nside_cmb_map_really_clipped[0], title="clipped - CMB T map at $N_{side}$= " + str(low_nside))(low_nside_cmb_map_clipped[0] - low_nside_cmb_map_really_clipped[0]).sum()hp.write_alm(f"Planck_bestfit_alm_seed_{seed}_lmax_{lclip}_K_CMB.fits", alm_really_clipped, overwrite=True)!ls *.fits