import pandas as pd
import healpy as hp
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
In 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
"ignore") warnings.filterwarnings(
print(plt.style.available)
'seaborn-talk') plt.style.use(
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.txt
Now, 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.txt
You should not have #
at the beginning of the first line
= pd.read_csv("COM_PowerSpect_CMB-base-plikHM-TTTEEE-lowl-lowE-lensing-minimum-theory_R3.01.txt",
input_cl =True, index_col=0) delim_whitespace
input_cl.head()
=True, logy=True, grid=True); input_cl.plot(logx
We can compare this to one of the plots from the Planck wiki:
=False, logy=False, grid=True)
input_cl.TT.plot(logx"$\dfrac{\ell(\ell+1)}{2\pi} C_\ell~[\mu K^2]$")
plt.ylabel("$\ell$")
plt.xlabel(
50, 2500]); plt.xlim([
Very good, they agree, therefore the data are in $ C_$, let’s transform to
input_cl.head()
len(input_cl)
= input_cl.index[-1]
lmax print(lmax)
= input_cl.divide(input_cl.index * (input_cl.index+1) / (np.pi*2), axis="index") cl
/= 1e12 cl
cl.head()
healpy
tools expect an array starting from zero, so let’s extend that.
= cl.reindex(np.arange(0, lmax+1)) cl
cl.head()
= cl.fillna(0) cl
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
= 583
seed np.random.seed(seed)
= hp.synalm((cl.TT, cl.EE, cl.BB, cl.TE), lmax=lmax, new=True) alm
= 1024
high_nside = hp.alm2map(alm, nside=high_nside, lmax=lmax) cmb_map
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
0], min=-300*1e-6, max=300*1e-6, unit="K", title="CMB Temperature") hp.mollview(cmb_map[
Polarization is generally an order of magnitude lower:
for m in cmb_map[1:]:
min=-30*1e-6, max=30*1e-6, unit="K", title="CMB Polarization") hp.mollview(m,
Double check the spectra of the maps
As a final check, we can compare the spectra of the output maps to the input spectra.
= hp.anafast(cmb_map, lmax=lmax, use_pixel_weights=True) * 1e12 # in muK^2
cl_from_map = np.arange(cl_from_map.shape[1])
ell *= ell * (ell+1) / (2*np.pi) cl_from_map
=1) np.median(cl_from_map, axis
0], label="map TT")
plt.plot(cl_from_map[=False, logy=False, grid=True)
input_cl.TT.plot(logx; plt.legend()
=True, logy=True, grid=True)
input_cl.plot(logx0], 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[#plt.plot(cl_from_map[4], label="map EB")
#plt.plot(cl_from_map[5], label="map TB")
"$\dfrac{\ell(\ell+1)}{2\pi} C_\ell~[\mu K^2]$")
plt.ylabel("$\ell$")
plt.xlabel(; 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
0][:-2], label="map TT")
plt.plot(cl_from_map[=False, logy=False, grid=True)
input_cl.TT.plot(logx"$\dfrac{\ell(\ell+1)}{2\pi} C_\ell~[\mu K^2]$")
plt.ylabel("$\ell$")
plt.xlabel(; plt.legend()
1][:-2], label="map EE")
plt.plot(cl_from_map[=False, logy=False, grid=True)
input_cl.EE.plot(logx"$\dfrac{\ell(\ell+1)}{2\pi} C_\ell~[\mu K^2]$")
plt.ylabel("$\ell$")
plt.xlabel(; plt.legend()
2], label="map BB")
plt.plot(cl_from_map[=False, logy=False, grid=True)
input_cl.BB.plot(logx"$\dfrac{\ell(\ell+1)}{2\pi} C_\ell~[\mu K^2]$")
plt.ylabel("$\ell$")
plt.xlabel(; plt.legend()
Save the to disk
We can save in a standard FITS format
f"Planck_bestfit_alm_seed_{seed}.fits", alm, overwrite=True) hp.write_alm(
!ls *alm*fits
Create a lower resolution map from the same
The
= 32
low_nside = 3*low_nside - 1
low_nside_lmax = hp.alm2map(alm, nside=low_nside) low_nside_cmb_map
= hp.anafast(low_nside_cmb_map, use_pixel_weights=True, lmax=low_nside_lmax) * 1e12
cl_low_nside = np.arange(cl_low_nside.shape[1])
ell_low_nside *= ell_low_nside * (ell_low_nside+1) / (2*np.pi) cl_low_nside
0], label="map TT")
plt.plot(cl_from_map[0], label="low nside map TT")
plt.plot(cl_low_nside[=False, logy=False, grid=True)
input_cl.TT.plot(logx**2)
plt.axhline(low_nside_cmb_map.std()"$\dfrac{\ell(\ell+1)}{2\pi} C_\ell~[\mu K^2]$")
plt.ylabel("$\ell$")
plt.xlabel(; 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
= np.ones(low_nside_lmax+1)
clip = np.array([hp.almxfl(each, clip) for each in alm]) alm_clipped
= hp.alm2map(alm_clipped, nside=low_nside) low_nside_cmb_map_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) cl_low_nside_clipped
0], label="map TT", alpha=.5)
plt.plot(cl_from_map[0], label="low nside map TT clipped", alpha=.8)
plt.plot(cl_low_nside_clipped[=False, logy=False, grid=True)
input_cl.TT.plot(logx2*low_nside, color="black", linestyle="--", label="ell=2*N_side")
plt.axvline(0, 800])
plt.xlim(["$\dfrac{\ell(\ell+1)}{2\pi} C_\ell~[\mu K^2]$")
plt.ylabel("$\ell$")
plt.xlabel(; 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
0], title="CMB T map at $N_{side}$= " + str(high_nside))
hp.mollview(cmb_map[0], title="CMB T map at $N_{side}$= " + str(low_nside)) hp.mollview(low_nside_cmb_map_clipped[
f"map_nside_{low_nside}_from_Planck_bestfit_alm_seed_{seed}_K_CMB.fits", low_nside_cmb_map_clipped, overwrite=True) hp.write_map(
!ls *.fits
Clip 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
= low_nside_lmax lclip
%%time
= []
clipping_indices for m in range(lclip+1):
+1), m))
clipping_indices.append(hp.Alm.getidx(lmax, np.arange(m, lclipprint(clipping_indices[:2])
= np.concatenate(clipping_indices) clipping_indices
= [each[clipping_indices] for each in alm] alm_really_clipped
= hp.alm2map(alm_clipped, nside=low_nside) low_nside_cmb_map_really_clipped
0], title="zeroed - CMB T map at $N_{side}$= " + str(low_nside))
hp.mollview(low_nside_cmb_map_clipped[0], title="clipped - CMB T map at $N_{side}$= " + str(low_nside)) hp.mollview(low_nside_cmb_map_really_clipped[
0] - low_nside_cmb_map_really_clipped[0]).sum() (low_nside_cmb_map_clipped[
f"Planck_bestfit_alm_seed_{seed}_lmax_{lclip}_K_CMB.fits", alm_really_clipped, overwrite=True) hp.write_alm(
!ls *.fits