Compute spectra for standard and pure B modes#

Introduction#

This tutorial illustrates the spectra computation for standard and pure B modes. We will only use the HEALPIX pixellisation to pass through the different steps of generation.

The HEALPIX survey mask is a disk centered on longitude 30° and latitude 50° with a radius of 25 radians. The nside value is set to 512 for this tutorial to reduce computation time.

Preamble#

Versions used for this tutorial

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

print("     Numpy :", np.__version__)
print("    Healpy :", hp.__version__)
print("Matplotlib :", mpl.__version__)
print("    pixell :", pixell.__version__)
print("      pspy :", pspy.__version__)
     Numpy : 1.26.3
    Healpy : 1.16.6
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 HEALPIX survey parameters namely longitude, latitude and patch size. The nside value is set to 512.

lon, lat = 30, 50
radius = 25
nside = 512

Given the nside value, we can set the \(\ell\)max value

ℓmax = 3 * nside - 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 HEALPIX template as follow

from pspy import so_map

template = so_map.healpix_template(ncomp, nside)

We also define the binary template for the window function pixels

binary = so_map.healpix_template(ncomp=1, nside=nside)
vec = hp.pixelfunc.ang2vec(lon, lat, lonlat=True)
disc = hp.query_disc(nside, vec, radius=radius * np.pi / 180)
binary.data[disc] = 1

Generation of spectra#

Generate window#

We then create an apodisation for the survey mask. We use a C1 apodisation with an apodisation size of 5 degrees

from pspy import so_window

window = so_window.create_apodization(binary, apo_type="C1", apo_radius_degree=5)
hp.mollview(window.data, title=None)
../_images/992e95a0891ddfdd32a371c9f552a89bc463f5faa3e96d864c820d44510f225c.png

We can also have a look to the corresponding spin 1 and spin 2 window functions

niter = 3
w1_plus, w1_minus, w2_plus, w2_minus = so_window.get_spinned_windows(window, lmax=ℓmax, niter=niter)
plt.figure(figsize=(8, 8))
kwargs = {"rot": (lon, lat, 0), "xsize": 3500, "reso": 1, "title": None}
hp.gnomview(w1_plus.data, **kwargs, sub=(2, 2, 1))
hp.gnomview(w1_minus.data, **kwargs, sub=(2, 2, 2))
hp.gnomview(w2_plus.data, **kwargs, sub=(2, 2, 3))
hp.gnomview(w2_minus.data, **kwargs, sub=(2, 2, 4))
../_images/6793b89349b1494964bb23f940e4087246309d6fd5e3403b052126b2afce0eec.png

Binning file#

We create a binning file with the following format : lmin, lmax, lmean

import os

from pspy import pspy_utils

output_dir = "/tmp/tutorial_purebb"
os.makedirs(output_dir, exist_ok=True)

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

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_tuple = (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 the binning file. We do it for both calculations i.e. standard and pure B mode

from pspy import so_mcm

kwargs = dict(win1=window_tuple, binning_file=binning_file, lmax=ℓmax, niter=niter, type="Cl")

print("Computing standard mode coupling matrix")
mbb_inv, Bbl = so_mcm.mcm_and_bbl_spin0and2(**kwargs)

print("Computing pure mode coupling matrix")
mbb_inv_pure, Bbl_pure = so_mcm.mcm_and_bbl_spin0and2(**kwargs, pure=True)
Hide code cell output
Computing standard mode coupling matrix
Computing pure mode coupling matrix

Generation of ΛCDM power spectra#

We first have to compute \(C_\ell\) data using a cosmology code such as CAMB and we need to install it since this is not 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

 = np.arange(2, 10_000)
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([-1], lens_potential_accuracy=1)
results = camb.get_results(pars)
powers = results.get_cmb_power_spectra(pars, CMB_unit="muK")

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

cl_file = os.path.join(output_dir, "cl_camb.dat")
np.savetxt(cl_file, np.hstack([[:, np.newaxis], powers["total"][]]))

Running simulations#

Given the parameters and data above, we will now simulate n_sims simulations to check for mean and variance of BB spectrum. For illustrative purpose, we will only run 10 simulations (~ few minutes) but for reasonable comparisons, you should increase this number to few tens of simulations.

We will do it for both calculations (standard and pure) and finally we will graphically compare results

We first need to specify the order of the spectra to be used by pspy although only BB spectrum will be used

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

and we define a dictionnary of methods regarding the calculation type for B mode spectrum

from pspy import sph_tools

methods = {
    "standard": dict(alm=sph_tools.get_alms, mbb=mbb_inv),
    "pure": dict(alm=sph_tools.get_pure_alms, mbb=mbb_inv_pure),
}
from pspy import so_spectra
from tqdm.auto import tqdm

for _ in tqdm(range(n_sims := 10)):
    cmb = template.synfast(cl_file)
    for k, v in methods.items():
        get_alm = v.get("alm")
        alm = get_alm(cmb, window_tuple, niter, ℓmax)
        , ps = so_spectra.get_spectra(alm, spectra=spectra)
        ℓb, ps_dict = so_spectra.bin_spectra(
            , ps, binning_file, ℓmax, type="Cl", mbb_inv=v.get("mbb"), spectra=spectra
        )
        v.setdefault("bb", []).append(ps_dict["BB"])

Let’s plot the mean results against the theory value for BB spectrum

for k, v in methods.items():
    v["mean"] = np.mean(v.get("bb"), axis=0)
    v["std"] = np.std(v.get("bb"), axis=0)

ℓ_th, ps_theory = pspy_utils.ps_lensed_theory_to_dict(cl_file, output_type="Cl", lmax=ℓmax)
ps_theory_b = so_mcm.apply_Bbl(Bbl, ps_theory, spectra=spectra)
ps_theory_b_pure = so_mcm.apply_Bbl(Bbl_pure, ps_theory, spectra=spectra)

fac = ℓb * (ℓb + 1) / (2 * np.pi)
facth = ℓ_th * (ℓ_th + 1) / (2 * np.pi)

fig = plt.figure(figsize=(7, 6))
grid = plt.GridSpec(4, 1, hspace=0, wspace=0)

main = fig.add_subplot(grid[:3], xticklabels=[], xlim=(0, 2 * nside))
main.plot(ℓ_th[:ℓmax], ps_theory["BB"][:ℓmax] * facth[:ℓmax], color="grey")
main.errorbar(ℓb, ps_theory_b["BB"] * fac, color="tab:red", label="binned theory BB")
main.errorbar(ℓb, ps_theory_b_pure["BB"] * fac, color="tab:blue", label="binned theory BB pure")
main.errorbar(
    ℓb,
    methods.get("standard").get("mean") * fac,
    methods.get("standard").get("std") * fac,
    fmt=".",
    color="tab:red",
    label="mean BB",
)
main.errorbar(
    ℓb,
    methods.get("pure").get("mean") * fac,
    methods.get("pure").get("std") * fac,
    fmt=".",
    color="tab:blue",
    label="mean BB pure",
)
main.set(ylim=(-0.07, 0.17), ylabel=r"$D^{BB}_{\ell}$ [µK²]")
plt.legend(title=r"$n_{\rm sims}=%s$" % n_sims)

ratio = fig.add_subplot(grid[3], xlim=(0, 2 * nside))
ratio.plot(ℓb, methods.get("pure").get("std") / methods.get("standard").get("std"), ".-k")
ratio.set(ylabel=r"$\sigma^{\rm pure}_\ell/ \sigma_\ell$", xlabel=r"$\ell$")
ratio.axhline(1, color="gray", ls="dashed");
../_images/4cf7071d5d14faafbdae8f790d618d046cab4fec7b24b58e2012b6eccd49f748.png