Power spectra computation in CAR pixellisation#

Introduction#

This tutorial shows how to compute the power spectra of spin 0 and 2 fields. We will use the CAR pixellisation to pass through the different steps of generation. If you are interested on doing the same thing with HEALPIX pixellisation, you can have a look at this file.

The CAR survey mask is defined with coordinates ra0, ra1, dec0, dec1 and has an angular resolution of 1 arcminute. We simulate 2 splits with 5 µK.arcmin noise and we also include a point source mask with 100 holes of size 10 arcminutes. We appodize the survey mask with an apodisation of 1 degree and the point source mask with an apodisation of 0.3 degree. We finally compute the spectra between the 2 splits of data up to lmax=1000.

Preamble#

Versions used for this tutorial

import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import pixell
import pspy

print("     Numpy :", np.__version__)
print("Matplotlib :", mpl.__version__)
print("    pixell :", pixell.__version__)
print("      pspy :", pspy.__version__)
     Numpy : 1.26.3
Matplotlib : 3.8.2
    pixell : 0.21.1
      pspy : 1.7.0+1.gafa4ce9.dirty

Set Planck colormap as default

pixell.colorize.mpl_setdefault("planck")

Generation of the templates, mask and apodisation type#

We start by specifying the CAR survey parameters, it will go from right ascension ra0 to ra1 and from declination dec0 to dec1 (all in degrees) with a resolution of 1 arcminute.

ra0, ra1, dec0, dec1 = -25, 25, -25, 25
res = 1

For this example, we will make use of 3 components : Temperature (spin 0) and polarisation Q and U (spin 2)

ncomp = 3

Given the parameters, we can generate the CAR template as follow

from pspy import so_map

template_car = so_map.car_template(ncomp, ra0, ra1, dec0, dec1, res)

We also define a binary template for the window function: we set pixels inside the survey at 1 and at the border to be zero

binary_car = so_map.car_template(1, ra0, ra1, dec0, dec1, res)
binary_car.data[:] = 0
binary_car.data[1:-1, 1:-1] = 1

Generation of spectra#

Generation of simulations#

We first have to compute a set of theoretical power spectra \(C_\ell\) using a Boltzmann solver such as CAMB and we need to install it since this is a prerequisite of pspy. We can do it within this notebook by executing the following command

%pip install camb
Requirement already satisfied: camb in /home/garrido/Workdir/cmb/development/pspy/pyenv/lib/python3.11/site-packages (1.5.3)
Requirement already satisfied: scipy>=1.0 in /home/garrido/Workdir/cmb/development/pspy/pyenv/lib/python3.11/site-packages (from camb) (1.11.4)
Requirement already satisfied: sympy>=1.0 in /home/garrido/Workdir/cmb/development/pspy/pyenv/lib/python3.11/site-packages (from camb) (1.12)
Requirement already satisfied: packaging in /home/garrido/Workdir/cmb/development/pspy/pyenv/lib/python3.11/site-packages (from camb) (23.2)
Requirement already satisfied: numpy<1.28.0,>=1.21.6 in /home/garrido/Workdir/cmb/development/pspy/pyenv/lib/python3.11/site-packages (from scipy>=1.0->camb) (1.26.3)
Requirement already satisfied: mpmath>=0.19 in /home/garrido/Workdir/cmb/development/pspy/pyenv/lib/python3.11/site-packages (from sympy>=1.0->camb) (1.3.0)
Note: you may need to restart the kernel to use updated packages.

To make sure everything goes well, we can import CAMB and check its version

import camb

print("CAMB version:", camb.__version__)
CAMB version: 1.5.3

Now that CAMB is properly installed, we will produce the \(C_\ell\)s from \(\ell\)min=2 to \(\ell\)max=104 for the following set of \(\Lambda\)CDM parameters

ℓmin, ℓmax = 2, 10**4
 = np.arange(ℓmin, ℓmax)
cosmo_params = {
    "H0": 67.5,
    "As": 1e-10 * np.exp(3.044),
    "ombh2": 0.02237,
    "omch2": 0.1200,
    "ns": 0.9649,
    "Alens": 1.0,
    "tau": 0.0544,
}
pars = camb.set_params(**cosmo_params)
pars.set_for_lmax(ℓmax, lens_potential_accuracy=1)
results = camb.get_results(pars)
powers = results.get_cmb_power_spectra(pars, CMB_unit="muK")

We finally have to write the \(C_\ell\)s into a file to feed the so_map.synfast function

import os

output_dir = "/tmp/tutorial_spectra_spin0and2"
os.makedirs(output_dir, exist_ok=True)
cl_file = os.path.join(output_dir, "cl_camb.dat")
np.savetxt(cl_file, np.hstack([[:, np.newaxis], powers["total"][ℓmin:ℓmax]]))

Given the CAMB file, we generate a CMB realisation

cmb = template_car.synfast(cl_file)

Then, we make 2 splits out of it, each with 5 µK.arcmin rms in temperature and 5\(\times\surd\)2 µK.arcmin in polarisation

nsplits = 2
splits = [cmb.copy() for i in range(nsplits)]
for i in range(nsplits):
    noise = so_map.white_noise(cmb, rms_uKarcmin_T=5, rms_uKarcmin_pol=np.sqrt(2) * 5)
    splits[i].data += noise.data

We can plot each component T, Q, U for both splits and for the original CMB realisation

fig, axes = plt.subplots(3, 3, figsize=(9, 9), sharex=True, sharey=True)
kwargs = dict(extent=[ra1, ra0, dec0, dec1], origin="lower")
for i, field in enumerate(fields := "TQU"):
    if field == "T":
        vrange = dict(vmin=-300, vmax=300)
    else:
        vrange = dict(vmin=-20, vmax=20)

    axes[0, i].set_title(fields[i])
    axes[0, i].imshow(cmb.data[i], **kwargs, **vrange)
    axes[1, i].imshow(splits[0].data[i], **kwargs, **vrange)
    axes[2, i].imshow(splits[1].data[i], **kwargs, **vrange)

axes[0, 0].set_ylabel("original")
axes[1, 0].set_ylabel("split 0")
axes[2, 0].set_ylabel("split 1")
fig.tight_layout()
../_images/b0ba318bae8ee524904ffadc07c7ec70ec086c37c992c09673a902b54f36615d.png

Generate window#

We then create an apodisation for the survey mask. We use an apodisation routine with an apodisation size of 1 degree

from pspy import so_window

window = so_window.create_apodization(binary_car, apo_type="C1", apo_radius_degree=1)

We also create a point source mask made of 100 holes each with a 10 arcminutes size

mask = so_map.simulate_source_mask(binary_car, n_holes=100, hole_radius_arcmin=10)

and we apodize it

mask = so_window.create_apodization(mask, apo_type="C1", apo_radius_degree=0.3)

The window is given by the product of the survey window and the mask window

window.data *= mask.data
fig, ax = plt.subplots(figsize=(5, 5))
ax.imshow(window.data, **kwargs, vmin=-1, vmax=+1);
../_images/656a8c66d4d4626443f9dd214dcea60a8b29a1e10d5968b23f0780f3b8be79ce.png

Compute mode coupling matrix#

For spin 0 and 2 the window need to be a tuple made of two objects: the window used for spin 0 and the one used for spin 2

window = (window, window)

The windows (for spin0 and spin2) are going to couple mode together, we compute a mode coupling matrix in order to undo this effect given a binning file (format: lmin, lmax, lmean) and a \(\ell\)max value of 1000

from pspy import pspy_utils

binning_file = os.path.join(output_dir, "binning.dat")
pspy_utils.create_binning_file(bin_size=40, n_bins=100, file_name=binning_file)

from pspy import so_mcm

ℓmax = 1000
mbb_inv, Bbl = so_mcm.mcm_and_bbl_spin0and2(window, binning_file, lmax=ℓmax, type="Dl", niter=0)

Compute alms and bin spectra#

from pspy import sph_tools

alms = [sph_tools.get_alms(split, window, niter=0, lmax=ℓmax) for split in splits]

We need to specify the order of the spectra to be used by pspy

spectra = ["TT", "TE", "TB", "ET", "BT", "EE", "EB", "BE", "BB"]

and we finally build a dictionary of cross split spectra

Db_dict = {}
from itertools import combinations_with_replacement as cwr

for (i1, alm1), (i2, alm2) in cwr(enumerate(alms), 2):
    from pspy import so_spectra

    , ps = so_spectra.get_spectra(alm1, alm2, spectra=spectra)
    ℓb, Db = so_spectra.bin_spectra(
        , ps, binning_file, ℓmax, type="Dl", mbb_inv=mbb_inv, spectra=spectra
    )
    Db_dict.update({f"split{i1}xsplit{i2}": Db})

To compare with the input \(C_\ell\), we also compute the binned theory spectra

from pspy import pspy_utils

, ps_theory = pspy_utils.ps_lensed_theory_to_dict(cl_file, "Dl", lmax=ℓmax)
ps_theory_b = so_mcm.apply_Bbl(Bbl, ps_theory, spectra=spectra)

and we finally plot all the results

fig, axes = plt.subplots(3, 3, figsize=(15, 12), sharex=True)
for ax, spec in zip(axes.flatten(), spectra):
    for k, v in Db_dict.items():
        ax.plot(ℓb, v[spec], "-o", label=k)
    ax.plot(ℓb, ps_theory_b[spec], "--", color="tab:red", label="binned theory")
    ax.plot(, ps_theory[spec], color="tab:red", label="theory")
    ax.set_ylabel(r"$D^{%s}_{\ell}$ [μK²]" % spec, fontsize=14)

for ax in axes[-1]:
    ax.set_xlabel(r"$\ell$", fontsize=14)
axes[0, -1].legend(loc="upper left", bbox_to_anchor=(1, 1))
fig.tight_layout()
../_images/220b78ee524f91bc975f794382cfd7c1bafaa65ed753a0c01f18853c9759bcdc.png