#!/usr/bin/env python # coding: utf-8 # **CAMB Python example notebook** # # Run it online yourself in [Binder](https://mybinder.org/v2/gh/cmbant/camb/master?filepath=docs%2FCAMBdemo.ipynb). # In[1]: get_ipython().run_line_magic('matplotlib', 'inline') get_ipython().run_line_magic('config', "InlineBackend.figure_format = 'retina'") import os import numpy as np from matplotlib import pyplot as plt import camb from camb import initialpower, model print("Using CAMB %s installed at %s" % (camb.__version__, os.path.dirname(camb.__file__))) # make sure the version and path is what you expect # In[2]: # Set up a new set of parameters for CAMB # The defaults give one massive neutrino and helium set using BBN consistency pars = camb.set_params( H0=67.5, ombh2=0.022, omch2=0.122, mnu=0.06, omk=0, tau=0.06, As=2e-9, ns=0.965, halofit_version="mead", lmax=3000 ) # In[3]: # calculate results for these parameters results = camb.get_results(pars) # In[4]: # get dictionary of CAMB power spectra powers = results.get_cmb_power_spectra(pars, CMB_unit="muK") for name in powers: print(name) # In[5]: # plot the total lensed CMB power spectra versus unlensed, and fractional difference totCL = powers["total"] unlensedCL = powers["unlensed_scalar"] print(totCL.shape) # Python CL arrays are all zero based (starting at L=0), Note L=0,1 entries will be zero by default. # The different CL are always in the order TT, EE, BB, TE (with BB=0 for unlensed scalar results). ls = np.arange(totCL.shape[0]) fig, ax = plt.subplots(2, 2, figsize=(12, 12)) ax[0, 0].plot(ls, totCL[:, 0], color="k") ax[0, 0].plot(ls, unlensedCL[:, 0], color="C2") ax[0, 0].set_title(r"$TT\, [\mu K^2]$") ax[0, 1].plot(ls[2:], 1 - unlensedCL[2:, 0] / totCL[2:, 0]) ax[0, 1].set_title(r"Fractional TT lensing") ax[1, 0].plot(ls, totCL[:, 1], color="k") ax[1, 0].plot(ls, unlensedCL[:, 1], color="C2") ax[1, 0].set_title(r"$EE\, [\mu K^2]$") ax[1, 1].plot(ls, totCL[:, 3], color="k") ax[1, 1].plot(ls, unlensedCL[:, 3], color="C2") ax[1, 1].set_title(r"$TE\, [\mu K^2]$") for ax in ax.reshape(-1): ax.set_xlim([2, 3000]) ax.set_xlabel(r"$\ell$") # In[6]: # The lensing B modes are non-linear, so need to be calculated carefully if you want them accurate (even at low ell) # Need both high lmax, non-linear lensing and high k # lens_potential_accuracy=1 turns on the latter, and can be increased to check precision pars.set_for_lmax(2500, lens_potential_accuracy=1) results = camb.get_results(pars) lmax2500CL = results.get_lensed_scalar_cls(CMB_unit="muK") ls = np.arange(lmax2500CL.shape[0]) pars.set_for_lmax(4000, lens_potential_accuracy=1) results = camb.get_results(pars) lmax4000CL = results.get_lensed_scalar_cls(CMB_unit="muK") pars.set_for_lmax(4000, lens_potential_accuracy=2) results = camb.get_results(pars) accCL = results.get_lensed_scalar_cls(CMB_unit="muK") pars.set_for_lmax(6000, lens_potential_accuracy=4) results = camb.get_results(pars) refCL = results.get_lensed_scalar_cls(CMB_unit="muK") fig, ax = plt.subplots(1, 2, figsize=(12, 4)) ax[0].plot(ls, totCL[: len(ls), 2], color="C0") ax[0].plot(ls, lmax2500CL[: len(ls), 2], color="C1") ax[0].plot(ls, lmax4000CL[: len(ls), 2], color="C2") ax[0].plot(ls, accCL[: len(ls), 2], color="C3") ax[0].plot(ls, refCL[: len(ls), 2], color="k") ax[0].set_xlim([2, 2500]) ax[0].set_xlabel(r"$\ell$", fontsize=13) ax[0].set_ylabel(r"$\ell(\ell+1)C_\ell^{BB}/2\pi\,[\mu {\rm K}^2]$", fontsize=13) ax[1].plot(ls[2:], totCL[2 : len(ls), 2] / refCL[2 : len(ls), 2] - 1, color="C0") ax[1].plot(ls[2:], lmax2500CL[2 : len(ls), 2] / refCL[2 : len(ls), 2] - 1, color="C1") ax[1].plot(ls[2:], lmax4000CL[2 : len(ls), 2] / refCL[2 : len(ls), 2] - 1, color="C2") ax[1].plot(ls[2:], accCL[2 : len(ls), 2] / refCL[2 : len(ls), 2] - 1, color="C3") ax[1].axhline(0, color="k") ax[1].set_xlim([2, 2500]) ax[1].set_xlabel(r"$\ell$", fontsize=13) ax[1].set_ylabel("fractional error", fontsize=13) ax[1].legend( [ "Default accuracy", "lmax=2500, lens_potential_accuracy=1", "lmax=4000, lens_potential_accuracy=1", "lmax=4000, lens_potential_accuracy=2", ] ); # In[7]: # You can calculate spectra for different primordial power spectra without recalculating everything # for example, let's plot the BB spectra as a function of r pars.set_for_lmax(4000, lens_potential_accuracy=1) pars.WantTensors = True results = camb.get_transfer_functions(pars) lmax = 2000 rs = np.linspace(0, 0.2, 6) for r in rs: inflation_params = initialpower.InitialPowerLaw() inflation_params.set_params(ns=0.96, r=r) results.power_spectra_from_transfer(inflation_params) # warning OK here, not changing scalars cl = results.get_total_cls(lmax, CMB_unit="muK") plt.loglog(np.arange(lmax + 1), cl[:, 2]) plt.xlim([2, lmax]) plt.legend(["$r = %s$" % r for r in rs], loc="lower right") plt.ylabel(r"$\ell(\ell+1)C_\ell^{BB}/ (2\pi \mu{\rm K}^2)$") plt.xlabel(r"$\ell$"); # In[8]: # Now get matter power spectra and sigma8 at redshift 0 and 0.8 # parameters can all be passed as a dict as above, or you can call # separate functions to set up the parameter object pars = camb.set_params(H0=67.5, ombh2=0.022, omch2=0.122, ns=0.965) # Note non-linear corrections couples to smaller scales than you want pars.set_matter_power(redshifts=[0.0, 0.8], kmax=2.0) # Linear spectra pars.NonLinear = model.NonLinear_none results = camb.get_results(pars) kh, z, pk = results.get_matter_power_spectrum(minkh=1e-4, maxkh=1, npoints=200) s8 = np.array(results.get_sigma8()) # Non-Linear spectra (Halofit) pars.NonLinear = model.NonLinear_both results.calc_power_spectra(pars) kh_nonlin, z_nonlin, pk_nonlin = results.get_matter_power_spectrum(minkh=1e-4, maxkh=1, npoints=200) print("sigma 8 values at the two redshifts:", results.get_sigma8()) # In[9]: for i, (redshift, line) in enumerate(zip(z, ["-", "--"])): plt.loglog(kh, pk[i, :], color="k", ls=line) plt.loglog(kh_nonlin, pk_nonlin[i, :], color="r", ls=line) plt.xlabel("k/h Mpc") plt.legend(["linear", "non-linear"], loc="lower left") plt.title("Matter power at z=%s and z= %s" % tuple(z)); # In[10]: # If you want to use sigma8 (redshift zero) as an input parameter, have to scale the input primordial amplitude As: As = 2e-9 # fiducial amplitude guess to start with pars = camb.set_params(H0=67.5, ombh2=0.022, omch2=0.122, ns=0.965, As=As) pars.set_matter_power(redshifts=[0.0], kmax=2.0) results = camb.get_results(pars) s8_fid = results.get_sigma8_0() # now set correct As using As \propto sigma8**2. sigma8 = 0.81 # value we want pars.InitPower.set_params(As=As * sigma8**2 / s8_fid**2, ns=0.965) # check result results = camb.get_results(pars) print(sigma8, results.get_sigma8_0()) # In[11]: # Plot CMB lensing potential power for various values of w at fixed H0 pars = camb.CAMBparams() pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122) pars.InitPower.set_params(As=2e-9, ns=0.965) pars.set_for_lmax(2000, lens_potential_accuracy=1) ws = np.linspace(-1.5, -0.6, 5) for w in ws: pars.set_dark_energy(w=w, wa=0, dark_energy_model="fluid") results = camb.get_results(pars) cl = results.get_lens_potential_cls(lmax=2000) plt.loglog(np.arange(2001), cl[:, 0]) plt.legend([f"$w = {w:.3f}$" for w in ws]) plt.ylabel(r"$[L(L+1)]^2C_L^{\phi\phi}/2\pi$") plt.xlabel("$L$") plt.xlim([2, 2000]) # In[12]: # Same for varying w at fixed thetastar rather than fixed H0 # When using thetastar you must instead call set_cosmology() *after* setting the dark energy parameters # or use camb.set_params to set everything at once as in this example ws = np.linspace(-1.5, -0.6, 5) for w in ws: pars = camb.set_params( w=w, wa=0, dark_energy_model="fluid", thetastar=0.0104, ombh2=0.022, omch2=0.12, As=2e-9, ns=0.96, lmax=2000, lens_potential_accuracy=2, ) results = camb.get_results(pars) cl = results.get_lens_potential_cls(lmax=2000) plt.loglog(np.arange(2001), cl[:, 0]) plt.legend([f"$w = {w:.3f}$" for w in ws]) plt.ylabel(r"$[L(L+1)]^2C_L^{\phi\phi}/2\pi$") plt.xlabel("$L$") plt.xlim([2, 2000]) # --- # You can view the parameters (as used by fortran CAMB internals) by just printing the parameter object. # See the [docs](https://camb.readthedocs.io/en/latest/model.html) for parameter and structure descriptions # In[13]: # parameters can also be read from text .ini files, for example this sets up a best-fit # Planck 2018 LCDM cosmology (base_plikHM_TTTEEE_lowl_lowE_lensing). # [Use planck_2018_acc.ini if you need high-ell and/or accurate BB and CMB lensng spectra at beyond-Planck accuracy] pars = camb.read_ini("https://raw.githubusercontent.com/cmbant/CAMB/master/inifiles/planck_2018.ini") # for a local github installation you can just do # pars=camb.read_ini(os.path.join(camb_path,'inifiles','planck_2018.ini')) # view parameter objects using print(), or use pickle or repr to save and restore print(pars) # In[14]: # The dark energy model can be changed as in the previous example, or by assigning to pars.DarkEnergy. # ** Note that if using thetastar as a parameter, you *must* set dark energy before calling set_cosmology # or use the camb.set_params() function setting everything at once from a dict ** # e.g. use the PPF model from camb.dark_energy import DarkEnergyPPF pars.DarkEnergy = DarkEnergyPPF(w=-1.2, wa=0.2) print("w, wa model parameters:\n\n", pars.DarkEnergy) results = camb.get_background(pars) # or can also use a w(a) numerical function # (note this will slow things down; make your own dark energy class in fortran for best performance) a = np.logspace(-5, 0, 1000) w = -1.2 + 0.2 * (1 - a) pars.DarkEnergy = DarkEnergyPPF() pars.DarkEnergy.set_w_a_table(a, w) print("Table-interpolated parameters (w and wa are set to estimated values at 0):\n\n", pars.DarkEnergy) results2 = camb.get_background(pars) rho, _ = results.get_dark_energy_rho_w(a) rho2, _ = results2.get_dark_energy_rho_w(a) plt.plot(a, rho, color="k") plt.plot(a, rho2, color="r", ls="--") plt.ylabel(r"$\rho/\rho_0$") plt.xlabel("$a$") plt.xlim(0, 1) plt.title("Dark enery density"); # In[15]: # Get various background functions and derived parameters pars = camb.CAMBparams() pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122) results = camb.get_background(pars) print("Derived parameter dictionary: %s" % results.get_derived_params()) # In[16]: z = np.linspace(0, 4, 100) DA = results.angular_diameter_distance(z) plt.plot(z, DA) plt.xlabel("$z$") plt.ylabel(r"$D_A /\rm{Mpc}$") plt.title("Angular diameter distance") plt.ylim([0, 2000]) plt.xlim([0, 4]); # In[17]: print("CosmoMC theta_MC parameter: %s" % results.cosmomc_theta()) # In[18]: # You can also directly access some lower level quantities, for example the CMB transfer functions: pars = camb.CAMBparams() pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122) data = camb.get_transfer_functions(pars) transfer = data.get_cmb_transfer_data() print("Number of sources (T, E, phi..): %s; number of ell: %s; number of k: %s " % tuple(transfer.delta_p_l_k.shape)) # In[19]: # Plot the transfer functions as a function of k for various ell fig, axs = plt.subplots(2, 2, figsize=(12, 8), sharex=True) for ix, ax in zip([3, 20, 40, 60], axs.reshape(-1)): ax.plot(transfer.q, transfer.delta_p_l_k[0, ix, :]) ax.set_title(r"$\ell = %s$" % transfer.L[ix]) if ix > 1: ax.set_xlabel(r"$k \rm{Mpc}$") # In[20]: # Note that internal samplings can be quite sparsely sampled, e.g. look at l=2 transfer function def plot_cmb_transfer_l(trans, ix): _, axs = plt.subplots(1, 2, figsize=(12, 6)) for source_ix, (name, ax) in enumerate(zip(["T", "E"], axs)): ax.semilogx(trans.q, trans.delta_p_l_k[source_ix, ix, :]) ax.set_xlim([1e-5, 0.05]) ax.set_xlabel(r"$k \rm{Mpc}$") ax.set_title(r"%s transfer function for $\ell = %s$" % (name, trans.L[ix])) plot_cmb_transfer_l(transfer, 0) # In[21]: # So if you want to make nice plots, either interpolate or do things at higher than default accuracy pars.set_accuracy(AccuracyBoost=2) # higher accuracy, so higher sampling density data = camb.get_transfer_functions(pars) plot_cmb_transfer_l(data.get_cmb_transfer_data(), 0) pars.set_accuracy(AccuracyBoost=1); # re-set default # In[22]: # Similarly for tensor transfer function # e.g. see where various C_L^BB come from in k by plotting normalized transfer**2 # (C_l is ~ integral over log k P(k) T(k)^2) pars = camb.CAMBparams() pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122) pars.WantScalars = False pars.WantTensors = True pars.set_accuracy(AccuracyBoost=2) data = camb.get_transfer_functions(pars) transfer = data.get_cmb_transfer_data("tensor") print(r"Calculated L: %s" % transfer.L) plt.figure(figsize=(14, 3)) ixs = [13, 19, 21] ls = [transfer.L[i] for i in ixs] cols = ["b", "r", "c"] for ix, col in zip(ixs, cols): k_weight = transfer.delta_p_l_k[2, ix, :] ** 2 k_weight /= np.sum(k_weight) plt.semilogx(transfer.q, k_weight, color=col) plt.xlim([1e-3, 0.1]) plt.legend(ls) plt.xlabel(r"$k \rm{Mpc}$") plt.title(r"Contribution to B from primordial tensor power spectrum for various $\ell$") # compare with k_* = l/chi*, note DAstar is in GPc, so multiply by 1000 to get standard Mpc units used for k derived = data.get_derived_params() for ell, col in zip(ls, cols): plt.axvline(ell / (1000 * derived["DAstar"]), color=col, ls=":", lw=2) # In[23]: # if you want to combine the transfer functions with the primordial power spectra, you can get the latter via k = 10 ** np.linspace(-5, 1, 50) pars.InitPower.set_params(ns=0.96, r=0.2) # this functions imposes inflation consistency relation by default scalar_pk = pars.scalar_power(k) tensor_pk = pars.tensor_power(k) plt.semilogx(k, scalar_pk) plt.semilogx(k, tensor_pk) plt.xlabel(r"$k \rm{Mpc}$") plt.ylabel(r"${\cal P}(k)$") plt.legend(["scalar", "tensor"]); # In[24]: # set_params is a shortcut routine for setting many things at once pars = camb.set_params(H0=67.5, ombh2=0.022, omch2=0.122, As=2e-9, ns=0.95) data = camb.get_background(pars) # In[25]: # There are functions get plot evolution of variables, e.g. for the background as a function of conformal time: # (there is an example changing the reionization history later) eta = 10 ** (np.linspace(1, 4, 300)) back_ev = data.get_background_time_evolution(eta, ["x_e", "visibility"]) fig, axs = plt.subplots(1, 2, figsize=(12, 5)) axs[0].semilogx(eta, back_ev["x_e"]) axs[1].loglog(eta, back_ev["visibility"]) axs[0].set_xlabel(r"$\eta/\rm{Mpc}$") axs[0].set_ylabel("$x_e$") axs[1].set_xlabel(r"$\eta/\rm{Mpc}$") axs[1].set_ylabel("Visibility") fig.suptitle("Ionization history, including both hydrogen and helium recombination and reionization"); # In[26]: # or as a function of redshift z = 10 ** np.linspace(2, 4, 300) back_ev = data.get_background_redshift_evolution(z, ["x_e", "visibility"], format="array") fig, axs = plt.subplots(1, 2, figsize=(12, 5)) for ( i, (ax, label), ) in enumerate(zip(axs, ["$x_e$", "Visibility"])): ax.semilogx(z, back_ev[:, i]) ax.set_xlabel("$z$") ax.set_ylabel(label) ax.set_xlim([500, 1e4]) # In[27]: # ..and perturbation transfer functions, e.g. for k=0.1. # Note that quantities are synchronous gauge unless otherwise specified # Also note v_newtonian_cdm, v_newtonian_baryon include a factor of -k/H, and Weyl a factor of k^2 - # see https://camb.readthedocs.io/en/latest/transfer_variables.html print("Available variables are %s" % camb.model.evolve_names) # In[28]: eta = np.linspace(1, 400, 300) ks = [0.02, 0.1] ev = data.get_time_evolution(ks, eta, ["delta_baryon", "delta_photon"]) _, axs = plt.subplots(1, 2, figsize=(12, 5)) for i, ax in enumerate(axs): ax.plot(eta, ev[i, :, 0]) ax.plot(eta, ev[i, :, 1]) ax.set_title("$k= %s$" % ks[i]) ax.set_xlabel(r"$\eta/\rm{Mpc}$") plt.legend([r"$\Delta_b$", r"$\Delta_\gamma$"], loc="upper left") # In[29]: # or as a function of redshift z = np.linspace(500, 5000, 300) ks = [0.02, 0.1] ev = data.get_redshift_evolution(ks, z, ["delta_baryon", "delta_cdm", "delta_photon"]) _, axs = plt.subplots(1, 2, figsize=(12, 5)) for i, ax in enumerate(axs): ax.plot(z, ev[i, :, 0]) ax.plot(z, ev[i, :, 1]) ax.plot(z, ev[i, :, 2]) ax.set_title(r"$k= %s/\rm{Mpc}$" % ks[i]) ax.set_xlabel("$z$") plt.legend([r"$\Delta_b$", r"$\Delta_c$", r"$\Delta_\gamma$"], loc="upper right"); # In[30]: # Here you can see oscillation of delta_photon, subsequent decay of the potential and change to Mezsaroz growth in delta_cdm eta = 10 ** (np.linspace(0, 3, 500)) def plot_ev(ev, k): plt.figure(figsize=(8, 6)) plt.loglog(eta, ev[:, 0]) plt.loglog(eta, np.abs(ev[:, 1])) plt.loglog(eta, -ev[:, 2] / k**2) # Weyl is k^2*phi plt.title(r"$k= %s/\rm{Mpc}$" % k) plt.xlabel(r"$\eta/\rm{Mpc}$") plt.legend([r"$\Delta_c$", r"$|\Delta_\gamma|$", r"$-(\Phi+\Psi)/2$"], loc="upper left") k = 0.3 plot_ev(data.get_time_evolution(k, eta, ["delta_cdm", "delta_photon", "Weyl"]), k) # In[31]: # Note that time evolution can be visually quite sensitive to accuracy. # By default it is boosted, but you can change this. e.g. plot_ev(data.get_time_evolution(k, eta, ["delta_cdm", "delta_photon", "Weyl"], lAccuracyBoost=1), k) plot_ev(data.get_time_evolution(k, eta, ["delta_cdm", "delta_photon", "Weyl"], lAccuracyBoost=10), k) # In[32]: # It's also possible to plot quantities in other gauges, or arbitrary symbolic expressions, # using camb.symbolic. # For example, this plots the Newtonian gauge density compared to the synchronous gauge one import camb.symbolic as cs Delta_c_N = cs.make_frame_invariant(cs.Delta_c, "Newtonian") ev = data.get_time_evolution(k, eta, ["delta_cdm", Delta_c_N]) plt.figure(figsize=(6, 4)) plt.loglog(eta, ev[:, 0]) plt.loglog(eta, ev[:, 1]) plt.title(r"$k= %s/\rm{Mpc}$" % k) plt.xlabel(r"$\eta/\rm{Mpc}$") plt.legend([r"$\Delta_c^{\rm synchronous}$", r"$\Delta_c^{\rm Newtonian}$"], fontsize=14); # In[33]: # Or see that the Newtonian-gauge CDM peculiar velocity decays roughly propto 1/a on sub-horizon # scales during radiation domination after the potentials have decayed so there is no driving force # (leading to logarithmic Meszaros growth of the CDM density perturbation) k = 4 vc_N = cs.make_frame_invariant(cs.v_c, "Newtonian") ev = data.get_time_evolution(k, eta, [Delta_c_N, vc_N, "a"]) plt.figure(figsize=(6, 4)) plt.loglog(eta, ev[:, 0]) plt.loglog(eta, -ev[:, 1]) eta_ln = 6 * np.sqrt(3) * np.pi / 4 / k horizon_index = np.searchsorted(eta, eta_ln, side="right") plt.loglog(eta, -ev[horizon_index, 1] * ev[horizon_index, 2] / ev[:, 2], ls="--") plt.ylim([1e-4, 5e2]) plt.title(r"$k= %s/\rm{Mpc}$" % k) plt.xlabel(r"$\eta/\rm{Mpc}$") plt.legend( [r"$\Delta_c^{\rm Newtonian}$", r"$-v_c^{\rm Newtonian}$", r"$v_c^{\rm Newtonian}\propto 1/a$"], fontsize=14 ); # For further details of camb.symbolic and examples of what can be done see the [CAMB symbolic ScalEqs notebook](https://camb.readthedocs.io/en/latest/ScalEqs.html) (run now in [Binder](https://mybinder.org/v2/gh/cmbant/camb/master?filepath=docs%2FScalEqs.ipynb)). This also serves as documentation for the scalar equations implemented in CAMB. # In[34]: # For calculating large-scale structure and lensing results yourself, get a power spectrum # interpolation object. In this example we calculate the CMB lensing potential power # spectrum using the Limber approximation, using PK=camb.get_matter_power_interpolator() function. # calling PK(z, k) will then get power spectrum at any k and redshift z in range. nz = 100 # number of steps to use for the radial/redshift integration kmax = 10 # kmax to use # First set up parameters as usual pars = camb.CAMBparams() pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122) pars.InitPower.set_params(ns=0.965) # For Limber result, want integration over \chi (comoving radial distance), from 0 to chi_*. # so get background results to find chistar, set up a range in chi, and calculate corresponding redshifts results = camb.get_background(pars) chistar = results.conformal_time(0) - results.tau_maxvis chis = np.linspace(0, chistar, nz) zs = results.redshift_at_comoving_radial_distance(chis) # Calculate array of delta_chi, and drop first and last points where things go singular dchis = (chis[2:] - chis[:-2]) / 2 chis = chis[1:-1] zs = zs[1:-1] # Get the matter power spectrum interpolation object (based on RectBivariateSpline). # Here for lensing we want the power spectrum of the Weyl potential. PK = camb.get_matter_power_interpolator( pars, nonlinear=True, hubble_units=False, k_hunit=False, kmax=kmax, var1=model.Transfer_Weyl, var2=model.Transfer_Weyl, zmax=zs[-1], ) # Have a look at interpolated power spectrum results for a range of redshifts # Expect linear potentials to decay a bit when Lambda becomes important, and change from non-linear growth plt.figure(figsize=(8, 5)) k = np.exp(np.log(10) * np.linspace(-4, 2, 200)) zplot = [0, 0.5, 1, 4, 20] for z in zplot: plt.loglog(k, PK.P(z, k)) plt.xlim([1e-4, kmax]) plt.xlabel(r"k Mpc") plt.ylabel(r"$P_\Psi\, Mpc^{-3}$") plt.legend(["z=%s" % z for z in zplot]) # Now do integral to get convergence power spectrum, using Limber approximation ($k\approx (l+0.5)/\chi$) # $$ # C_l^\kappa \approx [l(l+1)]^2\int_0^{\chi_*} d\chi \left( \frac{\chi_*-\chi}{\chi^2\chi_*}\right)^2 P_\Psi\left(\frac{l+0.5}{\chi}, z(\chi)\right) # $$ # where $P_\Psi $ is obtained from the interpolator. # In[35]: # Get lensing window function (flat universe) win = ((chistar - chis) / (chis**2 * chistar)) ** 2 # Do integral over chi ls = np.arange(2, 2500 + 1, dtype=np.float64) cl_kappa = np.zeros(ls.shape) w = np.ones(chis.shape) # this is just used to set to zero k values out of range of interpolation for i, ell in enumerate(ls): k = (ell + 0.5) / chis w[:] = 1 w[k < 1e-4] = 0 w[k >= kmax] = 0 cl_kappa[i] = np.dot(dchis, w * PK.P(zs, k, grid=False) * win / k**4) cl_kappa *= (ls * (ls + 1)) ** 2 # In[36]: # Compare with CAMB's calculation: # note that to get CAMB's internal calculation accurate at the 1% level at L~2000, # need lens_potential_accuracy=2. Increase to 4 for accurate match to the Limber calculation here pars.set_for_lmax(2500, lens_potential_accuracy=2) results = camb.get_results(pars) cl_camb = results.get_lens_potential_cls(2500) # cl_camb[:,0] is phi x phi power spectrum (other columns are phi x T and phi x E) # Make plot. Expect difference at very low-L from inaccuracy in Limber approximation, and # very high L from differences in kmax (lens_potential_accuracy is only 2, though good by eye here) cl_limber = 4 * cl_kappa / 2 / np.pi # convert kappa power to [l(l+1)]^2C_phi/2pi (what cl_camb is) plt.loglog(ls, cl_limber, color="b") plt.loglog(np.arange(2, cl_camb[:, 0].size), cl_camb[2:, 0], color="r") plt.xlim([1, 2000]) plt.legend(["Limber", "CAMB hybrid"]) plt.ylabel(r"$[L(L+1)]^2C_L^{\phi}/2\pi$") plt.xlabel("$L$") # In[37]: # The non-linear model can be changed like this: pars.set_matter_power(redshifts=[0.0], kmax=2.0) pars.NonLinearModel.set_params(halofit_version="takahashi") kh_nonlin, _, pk_takahashi = results.get_nonlinear_matter_power_spectrum(params=pars) pars.NonLinearModel.set_params(halofit_version="mead") kh_nonlin, _, pk_mead = results.get_nonlinear_matter_power_spectrum(params=pars) fig, axs = plt.subplots(2, 1, sharex=True, figsize=(8, 8)) axs[0].loglog(kh_nonlin, pk_takahashi[0]) axs[0].loglog(kh_nonlin, pk_mead[0]) axs[1].semilogx(kh_nonlin, pk_mead[0] / pk_takahashi[0] - 1) axs[1].set_xlabel(r"$k/h\, \rm{Mpc}$") axs[1].legend(["Mead/Takahashi-1"], loc="upper left"); # In[38]: # Get angular power spectrum for galaxy number counts and lensing from camb.sources import GaussianSourceWindow, SplinedSourceWindow pars = camb.CAMBparams() pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122) pars.InitPower.set_params(As=2e-9, ns=0.965) pars.set_for_lmax(lmax, lens_potential_accuracy=1) # set Want_CMB to true if you also want CMB spectra or correlations pars.Want_CMB = False # NonLinear_both or NonLinear_lens will use non-linear corrections pars.NonLinear = model.NonLinear_both # Set up W(z) window functions, later labelled W1, W2. Gaussian here. pars.SourceWindows = [ GaussianSourceWindow(redshift=0.17, source_type="counts", bias=1.2, sigma=0.04, dlog10Ndm=-0.2), GaussianSourceWindow(redshift=0.5, source_type="lensing", sigma=0.07), ] results = camb.get_results(pars) cls = results.get_source_cls_dict() # Note that P is CMB lensing, as a deflection angle power (i.e. PxP is [l(l+1)]^2C_l\phi\phi/2\pi) # lensing window functions are for kappa (and counts for the fractional angular number density) ls = np.arange(2, lmax + 1) for spectrum in ["W1xW1", "W2xW2", "W1xW2", "PxW1", "PxW2"]: plt.loglog(ls, cls[spectrum][2 : lmax + 1], label=spectrum) plt.xlabel(r"$\ell$") plt.ylabel(r"$\ell(\ell+1)C_\ell/2\pi$") plt.legend() # In[39]: # You can also use window functions from numerical table of W(z). It must be well enough sampled to interpolate nicely. # e.g. reproduce Gaussian this way.. zs = np.arange(0, 0.5, 0.02) W = np.exp(-((zs - 0.17) ** 2) / 2 / 0.04**2) / np.sqrt(2 * np.pi) / 0.04 pars.SourceWindows = [SplinedSourceWindow(bias=1.2, dlog10Ndm=-0.2, z=zs, W=W)] results = camb.get_results(pars) cls2 = results.get_cmb_unlensed_scalar_array_dict() plt.loglog(ls, cls["W1xW1"][2 : lmax + 1], label=spectrum) plt.loglog(ls, cls2["W1xW1"][2 : lmax + 1], ls="--") plt.xlabel(r"$\ell$") plt.ylabel(r"$\ell(\ell+1)C_\ell/2\pi$") # In[40]: # Sources can include various terms using these options (line_xx refers to 21cm) print(pars.SourceTerms) # In[41]: # Results above include redshift distortions but not magnification bias (counts_lensing). # Try turning off redshift distortions: pars.SourceTerms.counts_redshift = False results = camb.get_results(pars) cls3 = results.get_source_cls_dict() plt.loglog(ls, cls["W1xW1"][2 : lmax + 1]) plt.loglog(ls, cls3["W1xW1"][2 : lmax + 1]) plt.legend(["With redshift distortions", "Without"]) plt.xlabel(r"$\ell$") plt.ylabel(r"$\ell(\ell+1)C_\ell/2\pi$") plt.xlim(2, lmax); # In[42]: # For number counts you can give a redshift-dependent bias (the underlying code supports general b(z,k)) # toy model for single-bin LSST/Vera Rubin [using numbers from 1705.02332] z0 = 0.311 zs = np.arange(0, 10, 0.02) W = np.exp(-zs / z0) * (zs / z0) ** 2 / 2 / z0 bias = 1 + 0.84 * zs pars.SourceWindows = [SplinedSourceWindow(dlog10Ndm=0, z=zs, W=W, bias_z=bias)] lmax = 3000 pars.set_for_lmax(lmax, lens_potential_accuracy=5) results = camb.get_results(pars) # e.g. plot the cross-correlation with CMB lensing cls = results.get_cmb_unlensed_scalar_array_dict() nbar = 40 / (np.pi / 180 / 60) ** 2 # Poission noise ls = np.arange(2, lmax + 1) Dnoise = 1 / nbar * ls * (ls + 1) / 2 / np.pi correlation = cls["PxW1"][2 : lmax + 1] / np.sqrt(cls["PxP"][2 : lmax + 1] * (cls["W1xW1"][2 : lmax + 1] + Dnoise)) plt.plot(np.arange(2, lmax + 1), correlation) plt.xlabel(r"$L$") plt.ylabel("correlation") plt.xlim(2, lmax) plt.title("CMB lensing - LSST correlation (single redshift bin)"); # In[43]: # Let's look at some non-standard primordial power spectrum, e.g. with wavepacket oscillation # Define our custom power spectrum function (here power law with one wavepacket) def PK(k, As, ns, amp, freq, wid, centre, phase): return As * (k / 0.05) ** (ns - 1) * (1 + np.sin(phase + k * freq) * amp * np.exp(-((k - centre) ** 2) / wid**2)) # Check how this looks compared to power law ks = np.linspace(0.02, 1, 1000) pk1 = 2e-9 * (ks / 0.05) ** (0.96 - 1) pk2 = PK(ks, 2e-9, 0.96, 0.0599, 280, 0.08, 0.2, 0) plt.semilogx(ks, pk1) plt.semilogx(ks, pk2) plt.ylabel("$P(k)$") plt.xlabel(r"$k\, {\rm Mpc}$") plt.legend(["Power law", "Custom"]) plt.title("Scalar initial power spectrum"); # In[44]: # Now compute C_l and compare pars = camb.CAMBparams() pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122, tau=0.06) lmax = 2500 pars.set_for_lmax(lmax, lens_potential_accuracy=1) # For comparison, standard power law pars.InitPower.set_params(As=2e-9, ns=0.96) results = camb.get_results(pars) cl_unlensed = results.get_unlensed_scalar_cls(CMB_unit="muK") cl = results.get_lensed_scalar_cls(CMB_unit="muK") # Now get custom spectrum (effective_ns_for_nonlinear is used for halofit if required) pars.set_initial_power_function(PK, args=(2e-9, 0.96, 0.0599, 280, 0.08, 0.2, 0), effective_ns_for_nonlinear=0.96) results2 = camb.get_results(pars) cl2 = results2.get_lensed_scalar_cls(CMB_unit="muK") ls = np.arange(2, lmax) plt.plot(ls, (cl2[2:lmax, 0] - cl[2:lmax, 0])) plt.xlabel(r"$\ell$") plt.ylabel(r"$\ell(\ell+1)\Delta C_\ell/2\pi\, [\mu K^2]$") plt.title(r"$C_\ell$ difference to power law") # In[45]: # Note that if you have sharp features or fine oscillations, you may need # increase accuracy to sample them well. e.g. let's try increasing the frequency # Default accuracy pars.Accuracy.lSampleBoost = 1 pars.Accuracy.IntkAccuracyBoost = 1 pars.Accuracy.SourcekAccuracyBoost = 1 freq = 1000 ks = np.linspace(0.02, 1, 1000) pk1 = 2e-9 * (ks / 0.05) ** (0.96 - 1) pk2 = PK(ks, 2e-9, 0.96, 0.0599, freq, 0.08, 0.2, 0) plt.semilogx(ks, pk1) plt.semilogx(ks, pk2) plt.ylabel("$P(k)$") plt.xlabel(r"$k\, {\rm Mpc}$") plt.title("Scalar power spectrum") plt.figure() pars.set_initial_power_function(PK, args=(2e-9, 0.96, 0.0599, freq, 0.08, 0.2, 0), effective_ns_for_nonlinear=0.96) results2 = camb.get_results(pars) cl_unlensed2 = results2.get_unlensed_scalar_cls(CMB_unit="muK") # need to increase default sampling in ell to see features smaller than peaks reliably pars.Accuracy.lSampleBoost = 2 # may also need to sample k more densely when computing C_l from P(k) pars.Accuracy.IntkAccuracyBoost = 2 results3 = camb.get_results(pars) cl_unlensed3 = results3.get_unlensed_scalar_cls(CMB_unit="muK") cl3 = results3.get_lensed_scalar_cls(CMB_unit="muK") ls = np.arange(2, lmax) plt.plot(ls, (cl_unlensed2[2:lmax, 0] / cl_unlensed[2:lmax, 0] - 1)) plt.plot(ls, (cl_unlensed3[2:lmax, 0] / cl_unlensed[2:lmax, 0] - 1)) plt.xlabel(r"$\ell$") plt.ylabel(r"$\Delta C_\ell/C_\ell$") plt.title(r"Fractional $C_\ell$ difference to power law") plt.legend(["Default accuracy", "Boosted accuracy"]) # In[46]: # Note that lensing washes out small features on small scales plt.plot(ls, (cl_unlensed3[2:lmax, 0] / cl_unlensed[2:lmax, 0] - 1)) plt.plot(ls, (cl3[2:lmax, 0] / cl[2:lmax, 0] - 1)) plt.xlabel(r"$\ell$") plt.ylabel(r"$\Delta C_\ell/C_\ell$") plt.legend(["Unlensed", "Lensed"]) plt.title(r"Fractional $C_\ell$ difference to power law"); # In[47]: # Now look at the (small!) effect of neutrino mass splittings on the matter power spectrum (in linear theory) # The "neutrino_hierarchy" parameter uses a two eigenstate approximation to the full hierarchy (which is very accurate for cosmology) pars = camb.CAMBparams() pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122, mnu=0.11, neutrino_hierarchy="normal") pars.InitPower.set_params(ns=0.965) pars.set_matter_power(redshifts=[0.0], kmax=2.0) results = camb.get_results(pars) kh, z, pk = results.get_matter_power_spectrum(minkh=1e-4, maxkh=2, npoints=200) pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122, mnu=0.11, neutrino_hierarchy="inverted") results = camb.get_results(pars) kh2, z2, pk2 = results.get_matter_power_spectrum(minkh=1e-4, maxkh=2, npoints=200) plt.semilogx(kh, pk2[0, :] / pk[0, :] - 1) plt.ylabel(r"$\Delta$ PK/PK") plt.xlabel(r"$k\, [h \,\rm{Mpc}^{-1}]$") plt.title(r"Normal vs Inverted for $\sum m_\nu=0.11 \rm{eV}$"); # In[48]: # Matter power functions can get other variables than the total density. # For example look at the relative baryon-CDM velocity by using the power spectrum of # model.Transfer_vel_baryon_cdm kmax = 10 k_per_logint = 30 zs = [200, 500, 800, 1090] pars = camb.CAMBparams() pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122) pars.InitPower.set_params(ns=0.965) pars.WantTransfer = True pars.set_matter_power(redshifts=zs, kmax=kmax, k_per_logint=k_per_logint, silent=True) results = camb.get_results(pars) PKint = results.get_matter_power_interpolator( nonlinear=False, hubble_units=False, k_hunit=False, var1=model.Transfer_vel_baryon_cdm, var2=model.Transfer_vel_baryon_cdm, ) # In[49]: # Make plot like Fig 1 of https://arxiv.org/abs/1005.2416 # showing contribution to the CDM-baryon relative velocity variance per log k plt.figure(figsize=(8, 5)) ks = np.logspace(-3, np.log10(kmax), 300) for i, z in enumerate(zs): curlyP = PKint.P(z, ks) * ks**3 / (2 * np.pi**2) plt.semilogx(ks, curlyP) rms = np.sqrt(curlyP[1:-1].dot((ks[2:] - ks[:-2]) / 2 / ks[1:-1])) print("rms velocity at z=%s: %.3g" % (z, rms), "(%.3g km/s)" % (rms * 299792)) plt.xlim([1e-3, kmax]) plt.xlabel("k Mpc", fontsize=16) plt.ylabel(r"$\mathcal{P}_v(k)$", fontsize=16) plt.legend(["$z = %s$" % z for z in zs]); # In[50]: # You can also get the matter transfer functions # These are synchronous gauge and normalized to unit primordial curvature perturbation # The values stored in the array are quantities like Delta_x/k^2, and hence # are nearly independent of k on large scales. # Indices in the transfer_data array are the variable type, the k index, and the redshift index pars = camb.CAMBparams() pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122) pars.InitPower.set_params(ns=0.965) pars.set_matter_power(redshifts=[0], kmax=kmax) results = camb.get_results(pars) trans = results.get_matter_transfer_data() # get kh - the values of k/h at which they are calculated kh = trans.transfer_data[0, :, 0] # transfer functions for different variables, e.g. CDM density and the Weyl potential # CDM perturbations have grown, Weyl is O(1) of primordial value on large scales delta = trans.transfer_data[model.Transfer_cdm - 1, :, 0] W = trans.transfer_data[model.Transfer_Weyl - 1, :, 0] plt.plot(kh, delta) plt.loglog(kh, -W) plt.xlabel(r"$k/h\, [\rm Mpc]^{-1}$", fontsize=16) plt.title("Matter transfer functions") plt.legend([r"$\Delta_c/k^2$", r"Weyl potential $\Psi$"], fontsize=14); # In[51]: # Check we can get the matter power spectrum from the transfer function as expected k = kh * results.Params.h transfer = trans.transfer_data[model.Transfer_tot - 1, :, 0] primordial_PK = results.Params.scalar_power(k) matter_power = primordial_PK * transfer**2 * k**4 / (k**3 / (2 * np.pi**2)) # compare with CAMB's explicit output for the matter power spectrum kh2, zs, PK = results.get_linear_matter_power_spectrum(hubble_units=False) plt.loglog(kh, matter_power) plt.loglog(kh, PK[0, :]) plt.xlabel(r"$k\, [h Mpc^{-1}]$"); # In[52]: # It is also possible to get the real-space linear perturbation variance in spheres, i.e. \sigma_R. # Using R=8 and hubble_units=T would return the standard definition of \sigma_8 from scipy.interpolate import InterpolatedUnivariateSpline from scipy.optimize import brentq pars = camb.CAMBparams() pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122) pars.InitPower.set_params(ns=0.965) pars.set_matter_power(redshifts=[0, 1], kmax=kmax) results = camb.get_results(pars) R, z, sigma_R = results.get_sigmaR(R=np.arange(1, 20, 0.5), hubble_units=False, return_R_z=True) plt.plot(R, sigma_R[1, :], label="z = %s" % z[1]) plt.plot(R, sigma_R[0, :], label="z = %s" % z[0]) plt.ylabel(r"$\sigma_R$", fontsize=14) plt.xlabel(r"$R/{\rm Mpc}$", fontsize=14) plt.legend() # To get the non-linear scale where sigma_R=1, e.g. at z=0 sR = InterpolatedUnivariateSpline(R, sigma_R[-1, :] - 1) R_nonlin = brentq(sR, R[0], R[-1]) print(r"R giving \sigma_R=1 at z=0 is at R=%.2f Mpc (or %.2f Mpc/h)" % (R_nonlin, R_nonlin * pars.h)) # In[53]: # the above is for the default density variable delta_tot, without including # massive neutrinos the result would be very slightly different sigma_R_nonu = results.get_sigmaR(R=np.arange(1, 20, 0.5), var1="delta_nonu", var2="delta_nonu", hubble_units=False) plt.plot(R, sigma_R_nonu[0, :] / sigma_R[0, :] - 1, label="z = %s" % z[0]) plt.plot(R, sigma_R_nonu[1, :] / sigma_R[1, :] - 1, label="z = %s" % z[1]) plt.ylabel(r"$\Delta\sigma_R/\sigma_R$", fontsize=14) plt.xlabel(r"$R/{\rm Mpc}$", fontsize=14) plt.legend(); # In[54]: # results for power spectra using different initial power spectra # can be computed without recomputing the transfer functions pars = camb.set_params( H0=67.5, ombh2=0.022, omch2=0.122, redshifts=[0], kmax=5, As=2e-9, ns=0.96, halofit_version="mead" ) results = camb.get_transfer_functions(pars) kref, _, PKref = results.get_linear_matter_power_spectrum(hubble_units=False, k_hunit=False) kref, _, PKnl = results.get_nonlinear_matter_power_spectrum(hubble_units=False, k_hunit=False) ns_arr = np.arange(0.94, 0.981, 0.01) fig, axs = plt.subplots(1, 2, figsize=(14, 5), sharey=True) for ns in ns_arr: results.Params.InitPower.set_params(As=2e-9, ns=ns) results.calc_power_spectra() k, _, PK = results.get_linear_matter_power_spectrum(hubble_units=False, k_hunit=False) np.testing.assert_allclose(k, kref) axs[0].semilogx(k, PK[0, :] / PKref[0, :] - 1) k, _, PK = results.get_nonlinear_matter_power_spectrum(hubble_units=False, k_hunit=False) axs[1].semilogx(k, PK[0, :] / PKnl[0, :] - 1) for ax, t in zip(axs, ["Linear", "Nonlinear"]): ax.set_title("%s spectrum" % t) ax.set_xlim(1e-3, 5) # ax.set_ylim([-0.1,0.1]) ax.set_xlabel(r"$k\, [Mpc^{-1}]$") plt.legend(["$n_s = %.2f$" % ns for ns in ns_arr], ncol=2, loc="lower left") axs[0].set_ylabel(r"$\Delta P(k)/P(k)$", fontsize=16); # In[55]: # The non-linear model parameters can also be varied without recomputing the transfer functions # eg. look at the effect of the HMcode baryonic feedback parameter results = camb.get_transfer_functions(pars) kref, _, PKnl = results.get_nonlinear_matter_power_spectrum(hubble_units=False, k_hunit=False) feedbacks = np.arange(2, 4.1, 0.5) for baryfeed in feedbacks: results.Params.NonLinearModel.set_params(halofit_version="mead", HMCode_A_baryon=baryfeed) results.calc_power_spectra() k, _, PK = results.get_nonlinear_matter_power_spectrum(hubble_units=False, k_hunit=False) np.testing.assert_allclose(k, kref) plt.semilogx(k, PK[0, :] / PKnl[0, :] - 1) plt.xlim(1e-2, 5) plt.ylabel(r"$\Delta P(k)/P(k)$", fontsize=16) plt.xlabel(r"$k\, [Mpc^{-1}]$") plt.legend([r"$A_{\rm baryon} = %.2f$" % b for b in feedbacks]); # In[56]: # However non-linear lensing or other sources, or non-linearly lensed CMB requires # recalculation from the time transfer functions. This is a bit slower but faster than recomputing everything. # e.g. look at the impact of the baryon feedback parameter on the lensing potential power spectrum pars = camb.set_params(H0=67.5, ombh2=0.022, omch2=0.122, ns=0.965, lens_potential_accuracy=1, lmax=3000) results = camb.get_transfer_functions(pars, only_time_sources=True) results.calc_power_spectra() ref = results.get_lens_potential_cls()[:, 0] feedbacks = np.arange(2, 4.1, 0.5) for baryfeed in feedbacks: results.Params.NonLinearModel.set_params(halofit_version="mead", HMCode_A_baryon=baryfeed) results.calc_power_spectra() CL = results.get_lens_potential_cls()[:, 0] plt.semilogx(np.arange(2, CL.shape[0]), CL[2:] / ref[2:] - 1) plt.legend([r"$A_{\rm baryon} = %.2f$" % b for b in feedbacks]) plt.ylabel(r"$\Delta C_L/C_L$", fontsize=16) plt.xlabel("$L$") plt.title("Impact of baryonic feedback on the lensing potential"); # In[57]: # CAMB has a basic scalar field quintessence dark energy model # EarlyQuintessence is a specific example implementation that implements early dark energy # (axion-like, as arXiv:1908.06995) with potential V(\phi) = m^2f^2 (1 - cos(\phi/f))^2 + \Lambda # AxionEffectiveFluid is an approximate model that does not evolve the quintessence equations # To implement other models you'd need to make your own new subclass. # Note that this is not as well tested as most of the rest of CAMB. # Use n=3 and keep theta_* angular distance parameter fixed to roughly fit CMB data thetastar = 0.01044341764253 n = 3 fig, axs = plt.subplots(3, 1, figsize=(10, 8)) zs = np.logspace(1, 5, 500) pars = camb.set_params(ombh2=0.022, omch2=0.122, thetastar=thetastar) # results = camb.get_results(pars) print("LCDM: h0=", results.Params.H0) cl_LCDM = results.get_lensed_scalar_cls(CMB_unit="muK") axs[1].plot(cl_LCDM[2:, 0]) # Set dark energy fraction 0.1 at z=1e4 pars = camb.set_params( ombh2=0.022, omch2=0.122, w_n=(n - 1.0) / (n + 1.0), theta_i=np.pi / 2, zc=1e4, fde_zc=0.1, dark_energy_model="AxionEffectiveFluid", thetastar=thetastar, ) # results = camb.get_results(pars) print("AxionEffectiveFluid: h0 = ", results.Params.H0) axs[0].semilogx(zs, results.get_Omega("de", z=zs)) cl = results.get_lensed_scalar_cls(CMB_unit="muK") axs[0].set_ylabel(r"$\Omega_{\rm de}$") axs[2].plot(cl[2:, 0] / cl_LCDM[2:, 0] - 1) pars = camb.set_params( ombh2=0.022, omch2=0.122, m=8e-53, f=0.05, n=n, theta_i=3.1, use_zc=True, zc=1e4, fde_zc=0.1, dark_energy_model="EarlyQuintessence", thetastar=thetastar, ) # results = camb.get_results(pars) print("EarlyQuintessence: h0 = ", results.Params.H0) cl = results.get_lensed_scalar_cls(CMB_unit="muK") axs[0].semilogx(zs, results.get_Omega("de", z=zs)) axs[0].set_xlabel(r"$z$") axs[1].plot(cl[2:, 0]) axs[2].plot(cl[2:, 0] / cl_LCDM[2:, 0] - 1) axs[1].set_ylabel(r"$D_l [\mu {\rm K}^2]$") axs[2].set_ylabel(r"$\Delta D_l [\mu {\rm K}^2]$") axs[1].set_xlabel(r"$\ell$") axs[2].set_xlabel(r"$\ell$") plt.tight_layout() print("m = ", results.Params.DarkEnergy.m, "f = ", results.Params.DarkEnergy.f) # In[58]: # You can also calculate CMB correlation functions # (the correlations module also has useful functions for the inverse transform, # CMB lensing and derivatives of the lensed spectra - see docs) from camb import correlations pars = camb.set_params(H0=67.5, ombh2=0.022, omch2=0.122, As=2e-9, ns=0.96) pars.set_for_lmax(4000, lens_potential_accuracy=1) results = camb.get_results(pars) lensed_cl = results.get_lensed_scalar_cls() corrs, xvals, weights = correlations.gauss_legendre_correlation(lensed_cl) r = np.arccos(xvals) * 180 / np.pi # sampled theta values in degrees fig, axs = plt.subplots(2, 2, figsize=(12, 8)) for ix, (cap, ax) in enumerate(zip(["TT", "+", "-", r"\times"], axs.reshape(4))): ax.plot(r, corrs[:, ix]) ax.axhline(0, color="k") ax.set_xlim([0, 10]) ax.set_xlabel(r"$\theta$ [degrees]") ax.set_ylabel(r"$\zeta_{%s}(\theta)$" % (cap), fontsize=14) ax.set_xlim(0, 4) plt.suptitle("Correlation functions"); # In[59]: # For partially-delensed or Alens-scaled spectra, power spectra can be computed using # a custom or scaled lensing potential power spectrum. There's a pure-python # interface in the correlation module, or can use the result object functions # (faster). # Here just plot results for scaled lensing spectrum, can use # get_lensed_cls_with_spectrum to calculate lensed spectrum with specific # lensing power if needed. For BB the scaling is fairly linear, but less so for # other spectra. pars = camb.set_params(H0=67.5, ombh2=0.022, omch2=0.122, As=2e-9, ns=0.96) pars.set_for_lmax(3500, lens_potential_accuracy=1) results = camb.get_results(pars) for Alens in [1, 0.9, 0.5, 0.1]: plt.plot(np.arange(2, 2501), results.get_partially_lensed_cls(Alens, lmax=2500)[2:, 2], label="$A_L = %s$" % Alens) plt.ylabel(r"$C_\ell^{BB}$") plt.xlabel(r"$\ell$") plt.xlim([2, 2500]) plt.legend(); # In[60]: # For CMB lensing reconstruction, the non-perturbative response functions depend on the # `gradient' C_l, which can be calculated in the flat sky approximation using CAMB # On large scales similar to lensed CMB spectrum, but important difference esp for TT on small scales # see arXiv:1906.08760, 1101.2234 c_lensed = results.get_lensed_scalar_cls() c_lens_response = results.get_lensed_gradient_cls() plt.plot(np.arange(2, 3501), c_lens_response[2:3501, 0] / c_lensed[2:3501, 0] - 1) plt.ylabel(r"$\Delta C_l/C_l$") plt.xlabel("$l$") plt.xlim(2, 3500) plt.title(r"Lensing gradient response vs lensed $C_l$"); # CAMB also supports dark-age 21cm (see [astro-ph/0702600](https://arxiv.org/abs/astro-ph/0702600)) # In[61]: # Get 21cm transfer functions, similar to Fig 4 of astro-ph/0702600 pars = camb.set_params( cosmomc_theta=0.0104, ombh2=0.022, omch2=0.12, tau=0.053, As=2.08e-09, ns=0.968, num_massive_neutrinos=1 ) pars.Do21cm = True pars.Evolve_delta_xe = True # ionization fraction perturbations pars.Evolve_baryon_cs = True # accurate baryon perturbations pars.WantCls = False pars.SourceTerms.use_21cm_mK = False # Use dimensionless rather than mK units redshifts = [50] pars.set_matter_power(kmax=1000, redshifts=redshifts) # get transfer functions results = camb.get_results(pars) trans = results.get_matter_transfer_data() # get kh - the values of k/h at which they are calculated k = trans.transfer_data[0, :, 0] * results.Params.h for zix, z in enumerate( redshifts, ): # see https://camb.readthedocs.io/en/latest/transfer_variables.html delta_c = trans.transfer_data[model.Transfer_cdm - 1, :, zix] delta_b = trans.transfer_data[model.Transfer_b - 1, :, zix] T_g = trans.transfer_data[model.Transfer_Tmat - 1, :, zix] mono = trans.transfer_data[model.Transfer_monopole - 1, :, zix] plt.loglog(k, mono * k**2, color="k", ls="-", lw=2) plt.plot(k, delta_b * k**2, color="gray", ls="-") plt.plot(k, T_g * k**2, color="b", ls="--") plt.plot(k, delta_c * k**2, color="C0", ls="-.") plt.xlabel(r"$k\, \rm Mpc$", fontsize=16) plt.title(f"Transfer functions, z={redshifts}") plt.legend([r"21cm monopole", r"$\Delta_b$", r"$\Delta_{T_g}$", r"$\Delta_c$"], fontsize=14) plt.ylabel(r"$T_\chi$") plt.xlim(1e-4, 1e3) plt.ylim(1e-3, 1e4); # In[62]: # Reionization models can be changed using the reionization_model parameter (new in v1.5.1). # The default tanh model is not very physical, there is a sample exponential model included as an alternative # To define a new model inherit from the ReionizationModel classes already defined (in Fortran and Python) # Plot the reionization histories and corresponding EE/BB spectra for various parameters pdf_file = "" pdf = None if pdf_file: from matplotlib.backends.backend_pdf import PdfPages pdf = PdfPages(pdf_file) z = np.logspace(-2, 3, 1000) for exp_pow in [1, 1.2, 1.5, 2]: fig, axs = plt.subplots(1, 2, figsize=(14, 6)) ax = axs[0] for tau, c in zip((0.04, 0.055, 0.08, 0.12), ("k", "C0", "C1", "C2")): As = 1.8e-9 * np.exp(2 * tau) pars2 = camb.set_params( H0=67.5, ombh2=0.022, omch2=0.122, As=As, ns=0.95, r=0.001, reionization_model="ExpReionization", tau=tau, reion_exp_power=exp_pow, **{"Reion.timestep_boost": 1}, ) data2 = camb.get_results(pars2) pars = camb.set_params(H0=67.5, ombh2=0.022, omch2=0.122, As=As, ns=0.95, tau=tau, r=0.001) data = camb.get_results(pars) eta = data.conformal_time(z) eta2 = data2.conformal_time(z) back_ev = data.get_background_time_evolution(eta, ["x_e", "visibility"]) back_ev2 = data2.get_background_time_evolution(eta2, ["x_e", "visibility"]) ax.plot( z, back_ev["x_e"], ls="--", color=c, label=r"Tanh, $\tau =%.3f, z_{0.5} = %.3f$" % (tau, data.Params.Reion.redshift), ) ax.plot( z, back_ev2["x_e"], color=c, label=r"Exp, $\tau =%.3f, z_{0.5} = %.3f$" % (tau, data2.Params.Reion.redshift) ) cl = data.get_total_cls() cl2 = data2.get_total_cls() axs[1].loglog(cl[2:, 1], ls="--", color=c) axs[1].loglog(cl2[2:, 1], ls="-", color=c) cl = data.get_lensed_scalar_cls() cl2 = data2.get_lensed_scalar_cls() axs[1].loglog(cl[2:, 2], ls=":", color=c) axs[1].loglog(cl2[2:, 2], ls="-.", color=c) cl = data.get_tensor_cls() cl2 = data2.get_tensor_cls() axs[1].loglog(cl[2:, 2], ls="--", color=c) axs[1].loglog(cl2[2:, 2], ls="-", color=c) axs[1].set_xlim((2, 250)) axs[1].set_title(r"$EE$/$BB$: $r=0.001$, $A_s e^{-2\tau}=1.8\times 10^{-9}$") axs[1].set_xlabel(r"$\ell$") axs[1].set_ylabel(r"$D_\ell$") ax.set_xlim(0, 30) ax.set_xlabel("$z$") ax.set_ylabel("$x_e$") ax.legend(fontsize=9) ax.axvline(6.1, color="g", ls=":") ax.set_title(r"$x_e(z>z_c) \propto e^{-\lambda (z-z_c)^p}$, $p=%s$" % exp_pow) if pdf: pdf.savefig() # ### Animations # # Going back to standard CMB, here is an example of how to make an animation of the evolution of # the transfer functions. # In[63]: # A potential issue here is that with large dynamic range, you may wish to plot modes where # k*eta << 1 (long way outside horizon). Evolution is not normally calculated in the code a long # way outside the horizon, starting instead with the series solution. So for results which are numerically # unstable, you may need to replace the numerical result with the series result. # Use widget to see animation in notebook rather than just saving (with "pip install ipympl") # %matplotlib widget import camb.symbolic as cs pars = camb.read_ini("https://raw.githubusercontent.com/cmbant/CAMB/master/inifiles/planck_2018.ini") data = camb.get_results(pars) # get ranges to plot, evolving until recombination zstar = data.get_derived_params()["zstar"] etastar = data.conformal_time(zstar) # stop time evolution (eta) at recombination eta = np.logspace(-1.7, np.log10(etastar), 400) # wide range of k k = 10 ** np.linspace(-3, 1, 1200) # get some background quantities z = data.redshift_at_conformal_time(eta) back_ev = data.get_background_time_evolution(eta, ["x_e", "visibility"]) x_e = back_ev["x_e"] vis = back_ev["visibility"] adotoa = data.h_of_z(z) / (1 + z) # comoving Hubble # ratio of matter to radiation (neglecting neutrino mass) rho = data.get_background_densities(1 / (1 + z)) Rmat = (rho["baryon"] + rho["cdm"]) / (rho["photon"] + rho["neutrino"] + rho["nu"]) # some quantities needed for superhorizon series solution -see series solutions in notes at # https://cosmologist.info/notes/CAMB.pdf grhonu = data.grhormass[0] + data.grhornomass Rv = grhonu / (grhonu + data.grhog) omega = (data.grhob + data.grhoc) / np.sqrt(3 * (data.grhog + grhonu)) # initial value of super-horizon Psi for unit curvature Psi_init = -10 / (4 * Rv + 15) # make evolution plot in Newtonian gauge. Use symbolic package to get right gauge-invariant quantities. ev = data.get_time_evolution( k, eta, [ cs.Psi_N, # Newtonian potential \Psi "delta_photon", # sychronous gauge photon perturbation cs.make_frame_invariant(cs.Delta_g, "Newtonian"), # photon perturbation cs.make_frame_invariant(cs.v_b, "Newtonian"), # baryon velocity ], lAccuracyBoost=4, ) Psi = ev[:, :, 0] Delta_g = ev[:, :, 1] Delta_g_N = ev[:, :, 2] v_b_N = ev[:, :, 3] # Now replace Psi results well outside horizon where numerically unstable with series solution # Note Newtonian-gauge Delta_g does not start at zero, so is also unstable for i, etai in enumerate(eta): for j, kj in enumerate(k): if etai * kj < 0.1 and etai * omega < 0.1: Psi[j, i] = Psi_init - 25 / 8 * omega * etai * (8 * Rv - 3) / (4 * Rv + 15) / (2 * Rv + 15) # use Delta_g_N = Delta_g - 4H sigma/k (with adiabatic series result for sigma) Delta_g_N[j, i] = Delta_g[j, i] - 4 * adotoa[i] * ( Psi_init / 2 * etai - 15 * omega * etai**2 / 8 * (4 * Rv - 5) / (4 * Rv + 15) / (2 * Rv + 15) ) # In[64]: # output animation (must have ffmpeg installed) import math from matplotlib import animation def latex_sci_format(num, dec=3): return "{:.{dec}f}\\times 10^{{{}}}".format( num / (10 ** int(math.floor(math.log10(abs(num))))), int(math.floor(math.log10(abs(num)))), dec=dec ) fig, ax = plt.subplots() plot_vars = { r"$\Psi$": Psi, r"$\frac{1}{4}\Delta^{\rm Newt}_\gamma +\Psi$": Delta_g_N / 4 + Psi, r"$\frac{1}{\sqrt{3}}v_b^{\rm Newt}$": v_b_N / np.sqrt(3), } anim_fps = 8 lines = [] for lab in plot_vars.keys(): (line,) = ax.semilogx([], [], label=lab) lines.append(line) ax.axhline(0, color="k") plt.legend(loc="upper left") ax.set_xlim(k[0], k[-1]) ax.set_ylim(-1, 1) ax.set_xlabel(r"$k\, {\rm Mpc}$") # Define the update function def update(i): ax.set_title( r"$z= %s, \eta/{\rm Mpc}= %.2f, x_e = %.2f, \rho_m/\rho_{\rm rad}=%.2f$" % (latex_sci_format(z[i]), eta[i], x_e[i], Rmat[i]) ) for line, var in zip(lines, plot_vars.values()): line.set_data(k, var[:, i]) return lines # Create the animation transfer_anim = animation.FuncAnimation(fig, update, frames=range(len(eta)), blit=True) writer = animation.writers["ffmpeg"](fps=anim_fps, bitrate=-1) # can save like this transfer_anim.save(r"Newtonian_transfer_evolve_monopole_phi_vb.mp4", writer=writer, dpi=240) # See the output mp4 file [here](https://cdn.cosmologist.info/antony/Newtonian_transfer_evolve_monopole_phi_vb.mp4) or play below: # # # In[65]: # or play inline using: # from IPython.display import display, Video # video = transfer_anim.to_html5_video() # display(Video(data=video, embed=True, width = 600)) # In[66]: # Animate the matter transfer and potential evolution up to today # now use log scale and synchronous gauge as more relevant for the late-time matter import camb.symbolic as cs eta = np.logspace(-1.7, np.log10(data.tau0), 500)[:-1] k = 10 ** np.linspace(-3, 2, 2000) z = data.redshift_at_conformal_time(eta) ev = data.get_time_evolution( k, eta, [ cs.Psi_N, "delta_cdm", # sychronous gauge cdm perturbation "delta_photon", "delta_baryon", ], lAccuracyBoost=4, ) Psi = ev[:, :, 0] Delta_c = ev[:, :, 1] Delta_g = ev[:, :, 2] Delta_b = ev[:, :, 3] x_e = data.get_background_time_evolution(eta, ["x_e"])["x_e"] adotoa = data.h_of_z(z) / (1 + z) # comoving Hubble rho = data.get_background_densities(1 / (1 + z)) Rmat = (rho["baryon"] + rho["cdm"]) / (rho["photon"] + rho["neutrino"] + rho["nu"]) zstar = data.get_derived_params()["zstar"] # series for super-horizon Psi grhonu = data.grhormass[0] + data.grhornomass Rv = grhonu / (grhonu + data.grhog) omega = (data.grhob + data.grhoc) / np.sqrt(3 * (data.grhog + grhonu)) # initial value of super-horizon Psi for unit curvature and no isocurvature Psi_init = -10 / (4 * Rv + 15) for i, etai in enumerate(eta): for j, kj in enumerate(k): if etai * kj < 0.1 and etai * omega < 0.1: Psi[j, i] = Psi_init - 25 / 8 * omega * etai * (8 * Rv - 3) / (4 * Rv + 15) / (2 * Rv + 15) # In[67]: fig, ax = plt.subplots() plot_vars = {r"$\Delta_c$": Delta_c, r"$\Delta_b$": Delta_b, r"$\Delta_g$": Delta_g, r"$\Psi$": Psi} anim_fps = 8 lines = [] for lab in plot_vars.keys(): (line,) = ax.loglog([], [], label=lab) lines.append(line) plt.axhline(0, ls="-", color="k") plt.legend(loc="upper left") ax.set_xlim(k[0], k[-1]) ax.set_xlabel(r"$k\, {\rm Mpc}$") ax.set_ylim(1e-4, 2e5) lines.append(ax.loglog([], [], ls="--", color="C1")[0]) lines.append(ax.loglog([], [], ls="--", color="C2")[0]) lines.append(ax.loglog([], [], ls="--", color="C3")[0]) # Define the update function def update(i): ax.set_title( r"$z= %s, \eta/{\rm Mpc}= %.2f, x_e = %.2f, \rho_m/\rho_{\rm rad}=%.2f$" % (latex_sci_format(z[i], 2), eta[i], x_e[i], Rmat[i]), fontsize=10, ) for line, var in zip(lines, plot_vars.values()): line.set_data(k, var[:, i]) lines[-3].set_data(k, -Delta_b[:, i]) lines[-2].set_data(k, -Delta_g[:, i]) lines[-1].set_data(k, -Psi[:, i]) if z[i] < zstar: # hide photons after recombination as irrelevant for matter lines[2].set_data([], []) lines[-2].set_data([], []) return lines # Create the animation transfer_anim = animation.FuncAnimation(fig, update, frames=range(len(eta)), blit=True, repeat=False) writer = animation.writers["ffmpeg"](fps=anim_fps, bitrate=-1) transfer_anim.save(r"matter_transfer_evolve_sync.mp4", writer=writer, dpi=240) # This is the video produced of the synchronous transfer function evolution (dashed lines are negative): # #