diff --git a/.flake8 b/.flake8 index 87a91cca8..a271a0658 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,8 @@ [flake8] ignore = E741,E226,W503,W504 -exclude = +per-file-ignores = + */__init__.py:F401,F403,F405 +exclude = build/, .eggs/, benchmarks/data, diff --git a/CMakeLists.txt b/CMakeLists.txt index b7e37bdd9..866e4f47b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ option(FORCE_OPENMP "Forcibly use OpenMP " NO) set(CCL_SRC src/ccl_background.c src/ccl_core.c src/ccl_error.c src/ccl_bbks.c src/ccl_f1d.c src/ccl_f2d.c src/ccl_f3d.c - src/ccl_power.c src/ccl_bcm.c + src/ccl_power.c src/ccl_eh.c src/ccl_musigma.c src/ccl_utils.c src/ccl_cls.c src/ccl_massfunc.c src/ccl_neutrinos.c diff --git a/README.md b/README.md index 388e8287b..bacfa8d18 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,10 @@ STYLE CONVENTION USED **`type`** or **`structure`** --> # CCL -[![Build Status](https://travis-ci.org/LSSTDESC/CCL.svg?branch=master)](https://travis-ci.org/LSSTDESC/CCL) [![Coverage Status](https://coveralls.io/repos/github/LSSTDESC/CCL/badge.svg?branch=master)](https://coveralls.io/github/LSSTDESC/CCL?branch=master) [![Documentation Status](https://readthedocs.org/projects/ccl/badge/?version=latest)](https://ccl.readthedocs.io/en/latest/?badge=latest) +![Build](https://github.com/LSSTDESC/CCL/workflows/continuous-integration/badge.svg?branch=master)  +[![Coverage](https://coveralls.io/repos/github/LSSTDESC/CCL/badge.svg?branch=master)](https://coveralls.io/github/LSSTDESC/CCL?branch=master)  +[![Documentation](https://readthedocs.org/projects/ccl/badge/?version=latest)](https://ccl.readthedocs.io/en/latest/)  +[![DOI](https://img.shields.io/badge/DOI-10.3847%2F1538--4365%2Fab1658-B31B1B.svg)](https://iopscience.iop.org/article/10.3847/1538-4365/ab1658) The Core Cosmology Library (CCL) is a standardized library of routines to calculate basic observables used in cosmology. It will be the standard analysis package used by the @@ -72,7 +75,7 @@ cosmo = ccl.Cosmology( transfer_function='bbks') # Define a simple binned galaxy number density curve as a function of redshift -z_n = np.linspace(0., 1., 200) +z_n = np.linspace(0., 1., 500) n = np.ones(z_n.shape) # Create objects to represent tracers of the weak lensing signal with this diff --git a/benchmarks/ccl_test_angpow.c b/benchmarks/ccl_test_angpow.c index be7dd7941..bbbb70742 100644 --- a/benchmarks/ccl_test_angpow.c +++ b/benchmarks/ccl_test_angpow.c @@ -4,11 +4,11 @@ #include #define NZ 1024 -#define Z0_GC 1.0 +#define Z0_GC 1.0 #define SZ_GC 0.02 #define NL 499 -#define CLS_PRECISION 3E-3 +#define CLS_PRECISION 3E-3 CTEST_DATA(angpow) { double Omega_c; @@ -30,7 +30,7 @@ CTEST_DATA(angpow) { -// Set up the cosmological parameters to be used +// Set up the cosmological parameters to be used CTEST_SETUP(angpow){ data->Omega_c = 0.25; data->Omega_b = 0.05; @@ -55,7 +55,7 @@ static void test_angpow_precision(struct angpow_data * data) { // Status flag int status =0; - + // Initialize cosmological parameters ccl_configuration ccl_config=default_config; ccl_config.transfer_function_method=ccl_boltzmann_class; @@ -78,7 +78,7 @@ static void test_angpow_precision(struct angpow_data * data) nz_arr_gc[i]=exp(-0.5*pow((z_arr_gc[i]-Z0_GC)/SZ_GC,2)); bz_arr[i]=1; } - + // Galaxy clustering tracer CCL_ClTracer *ct_gc_A=ccl_cl_tracer_number_counts(ccl_cosmo,1,1,0, NZ,z_arr_gc,nz_arr_gc, @@ -88,7 +88,7 @@ static void test_angpow_precision(struct angpow_data * data) NZ,z_arr_gc,nz_arr_gc, NZ,z_arr_gc,bz_arr, -1,NULL,NULL, &status); - + int *ells=malloc(NL*sizeof(int)); double *cells_gg_angpow=malloc(NL*sizeof(double)); for(int ii=0;iiOmega_c,data->Omega_b, 0.0, 3.046, &mnu, 0, -1.0, 0.0, data->h, data->A_s,data->n_s, - -1, -1, -1, 0., 0., 1.0, 1.0, 0.0, + -1, -1, -1, 0., 0., 1.0, 1.0, 0.0, -1, NULL, NULL, &status); params.T_CMB=2.7; params.Omega_k=0; diff --git a/benchmarks/test_bcm.py b/benchmarks/test_bcm.py index a4acbabaf..524719616 100644 --- a/benchmarks/test_bcm.py +++ b/benchmarks/test_bcm.py @@ -6,22 +6,6 @@ def test_bcm(): cosmo = ccl.Cosmology( - Omega_c=0.25, - Omega_b=0.05, - h=0.7, - A_s=2.2e-9, - n_s=0.96, - Neff=3.046, - m_nu_type='normal', - m_nu=0.0, - Omega_g=0, - Omega_k=0, - w0=-1, - wa=0, - bcm_log10Mc=14.0, - baryons_power_spectrum='bcm') - - cosmo_nobar = ccl.Cosmology( Omega_c=0.25, Omega_b=0.05, h=0.7, @@ -41,12 +25,14 @@ def test_bcm(): k = data[:, 0] * cosmo['h'] a = 1 - fbcm = ccl.bcm_model_fka(cosmo, k, a) + bar = ccl.BaryonsSchneider15(log10Mc=14.) + fbcm = bar.boost_factor(cosmo, k, a) err = np.abs(data[:, 1]/data_nobar[:, 1]/fbcm - 1) assert np.allclose(err, 0, atol=BCM_TOLERANCE, rtol=0) - ratio = ( - ccl.nonlin_matter_power(cosmo, k, a) / - ccl.nonlin_matter_power(cosmo_nobar, k, a)) + cosmo.compute_nonlin_power() + pk_nobar = cosmo.get_nonlin_power() + pk_wbar = bar.include_baryonic_effects(cosmo, pk_nobar) + ratio = pk_wbar.eval(k, a)/pk_nobar.eval(k, a) err = np.abs(data[:, 1]/data_nobar[:, 1]/ratio - 1) assert np.allclose(err, 0, atol=BCM_TOLERANCE, rtol=0) diff --git a/benchmarks/test_cls.py b/benchmarks/test_cells.py similarity index 92% rename from benchmarks/test_cls.py rename to benchmarks/test_cells.py index 27606f3d3..4a496302f 100644 --- a/benchmarks/test_cls.py +++ b/benchmarks/test_cells.py @@ -68,21 +68,21 @@ def set_up(request): # Initialize tracers trc = {} - trc['g1'] = ccl.NumberCountsTracer(cosmo, False, - (z1, pz1), - (z2, bz)) - trc['g2'] = ccl.NumberCountsTracer(cosmo, False, - (z2, pz2), - (z2, bz)) - trc['l1'] = ccl.WeakLensingTracer(cosmo, (z1, pz1)) - trc['l2'] = ccl.WeakLensingTracer(cosmo, (z2, pz2)) - trc['i1'] = ccl.WeakLensingTracer(cosmo, (z1, pz1), + trc['g1'] = ccl.NumberCountsTracer(cosmo, has_rsd=False, + dndz=(z1, pz1), + bias=(z2, bz)) + trc['g2'] = ccl.NumberCountsTracer(cosmo, has_rsd=False, + dndz=(z2, pz2), + bias=(z2, bz)) + trc['l1'] = ccl.WeakLensingTracer(cosmo, dndz=(z1, pz1)) + trc['l2'] = ccl.WeakLensingTracer(cosmo, dndz=(z2, pz2)) + trc['i1'] = ccl.WeakLensingTracer(cosmo, dndz=(z1, pz1), has_shear=False, ia_bias=(z1, a1)) - trc['i2'] = ccl.WeakLensingTracer(cosmo, (z2, pz2), + trc['i2'] = ccl.WeakLensingTracer(cosmo, dndz=(z2, pz2), has_shear=False, ia_bias=(z2, a2)) - trc['ct'] = ccl.CMBLensingTracer(cosmo, 1100.) + trc['ct'] = ccl.CMBLensingTracer(cosmo, z_source=1100.) # Read benchmarks def read_bm(fname): @@ -193,8 +193,8 @@ def read_bm(fname): ('i2', 'i2', 'ii_22', # IA2-IA2 'll_22', 'll_22', 'll_22', 'll_22', 'fl_ll')]) -def test_cls(set_up, t1, t2, bm, - a1b1, a1b2, a2b1, a2b2, fl): +def test_cells(set_up, t1, t2, bm, + a1b1, a1b2, a2b1, a2b2, fl): cosmo, trcs, lfc, bmk = set_up cl = ccl.angular_cl(cosmo, trcs[t1], trcs[t2], lfc['ells'], limber_integration_method='qag_quad') * lfc[fl] @@ -213,8 +213,8 @@ def test_cls(set_up, t1, t2, bm, ('l2', 'l2', 'll_22', # WL2-WL2 'll_22', 'll_22', 'll_22', 'll_22', 'fl_ll')]) -def test_cls_spline(set_up, t1, t2, bm, - a1b1, a1b2, a2b1, a2b2, fl): +def test_cells_spline(set_up, t1, t2, bm, + a1b1, a1b2, a2b1, a2b2, fl): cosmo, trcs, lfc, bmk = set_up cl = ccl.angular_cl(cosmo, trcs[t1], trcs[t2], lfc['ells'], limber_integration_method='spline') * lfc[fl] diff --git a/benchmarks/test_cib.py b/benchmarks/test_cib.py index 3f56edea4..847ae3a7e 100644 --- a/benchmarks/test_cib.py +++ b/benchmarks/test_cib.py @@ -45,4 +45,4 @@ def test_cibcl(): dl = cl*ls*(ls+1)/(2*np.pi) # Compare - assert np.all(np.fabs(dl/(bm[31]+bm[32])-1) < 1E-2) + assert np.all(np.fabs(dl/(bm[31]+bm[32])-1) < 1.2E-2) diff --git a/benchmarks/test_concentration_Ishiyama21.py b/benchmarks/test_concentration_Ishiyama21.py index fdbbb1172..99b6a7b22 100644 --- a/benchmarks/test_concentration_Ishiyama21.py +++ b/benchmarks/test_concentration_Ishiyama21.py @@ -58,5 +58,5 @@ def test_concentration_Ishiyama21(pars): for i, zz in enumerate(Z): dat = data[:, i+1] - mod = cm.get_concentration(COSMO, M_use/H100, 1/(1+zz)) + mod = cm(COSMO, M_use/H100, 1/(1+zz)) assert np.allclose(mod, dat, rtol=UCHUU_DATA_SCATTER) diff --git a/benchmarks/test_correlation.py b/benchmarks/test_correlation.py index 27a988f44..0a321fe27 100644 --- a/benchmarks/test_correlation.py +++ b/benchmarks/test_correlation.py @@ -71,21 +71,21 @@ def set_up(request): # Initialize tracers trc = {} - trc['g1'] = ccl.NumberCountsTracer(cosmo, False, - (z1, pz1), - (z2, bz)) - trc['g2'] = ccl.NumberCountsTracer(cosmo, False, - (z2, pz2), - (z2, bz)) - trc['l1'] = ccl.WeakLensingTracer(cosmo, (z1, pz1)) - trc['l2'] = ccl.WeakLensingTracer(cosmo, (z2, pz2)) - trc['i1'] = ccl.WeakLensingTracer(cosmo, (z1, pz1), + trc['g1'] = ccl.NumberCountsTracer(cosmo, has_rsd=False, + dndz=(z1, pz1), + bias=(z2, bz)) + trc['g2'] = ccl.NumberCountsTracer(cosmo, has_rsd=False, + dndz=(z2, pz2), + bias=(z2, bz)) + trc['l1'] = ccl.WeakLensingTracer(cosmo, dndz=(z1, pz1)) + trc['l2'] = ccl.WeakLensingTracer(cosmo, dndz=(z2, pz2)) + trc['i1'] = ccl.WeakLensingTracer(cosmo, dndz=(z1, pz1), has_shear=False, ia_bias=(z1, a1)) - trc['i2'] = ccl.WeakLensingTracer(cosmo, (z2, pz2), + trc['i2'] = ccl.WeakLensingTracer(cosmo, dndz=(z2, pz2), has_shear=False, ia_bias=(z2, a2)) - trc['ct'] = ccl.CMBLensingTracer(cosmo, 1100.) + trc['ct'] = ccl.CMBLensingTracer(cosmo, z_source=1100.) # Read benchmarks def read_bm(fname): diff --git a/benchmarks/test_correlation_MG.py b/benchmarks/test_correlation_MG.py index 34ba6d5b8..aaa267700 100644 --- a/benchmarks/test_correlation_MG.py +++ b/benchmarks/test_correlation_MG.py @@ -48,10 +48,12 @@ def set_up(request): # Initialize tracers trc = {} - trc['g1'] = ccl.NumberCountsTracer(cosmo, False, (z1, pz1), (z1, bz1)) - trc['g2'] = ccl.NumberCountsTracer(cosmo, False, (z2, pz2), (z2, bz2)) - trc['l1'] = ccl.WeakLensingTracer(cosmo, (z1, pz1)) - trc['l2'] = ccl.WeakLensingTracer(cosmo, (z2, pz2)) + trc['g1'] = ccl.NumberCountsTracer(cosmo, has_rsd=False, + dndz=(z1, pz1), bias=(z1, bz1)) + trc['g2'] = ccl.NumberCountsTracer(cosmo, has_rsd=False, + dndz=(z2, pz2), bias=(z2, bz2)) + trc['l1'] = ccl.WeakLensingTracer(cosmo, dndz=(z1, pz1)) + trc['l2'] = ccl.WeakLensingTracer(cosmo, dndz=(z2, pz2)) # Read benchmarks bms = {} diff --git a/benchmarks/test_correlation_MG2.py b/benchmarks/test_correlation_MG2.py index 44f0bdf88..43ba0f9da 100644 --- a/benchmarks/test_correlation_MG2.py +++ b/benchmarks/test_correlation_MG2.py @@ -48,10 +48,12 @@ def set_up(request): # Initialize tracers trc = {} - trc['g1'] = ccl.NumberCountsTracer(cosmo, False, (z1, pz1), (z1, bz1)) - trc['g2'] = ccl.NumberCountsTracer(cosmo, False, (z2, pz2), (z2, bz2)) - trc['l1'] = ccl.WeakLensingTracer(cosmo, (z1, pz1)) - trc['l2'] = ccl.WeakLensingTracer(cosmo, (z2, pz2)) + trc['g1'] = ccl.NumberCountsTracer(cosmo, has_rsd=False, + dndz=(z1, pz1), bias=(z1, bz1)) + trc['g2'] = ccl.NumberCountsTracer(cosmo, has_rsd=False, + dndz=(z2, pz2), bias=(z2, bz2)) + trc['l1'] = ccl.WeakLensingTracer(cosmo, dndz=(z1, pz1)) + trc['l2'] = ccl.WeakLensingTracer(cosmo, dndz=(z2, pz2)) # Read benchmarks bms = {} diff --git a/benchmarks/test_correlation_MG3_SD.py b/benchmarks/test_correlation_MG3_SD.py index 178c7e21b..caef3585a 100644 --- a/benchmarks/test_correlation_MG3_SD.py +++ b/benchmarks/test_correlation_MG3_SD.py @@ -50,10 +50,12 @@ def set_up(request): # Initialize tracers trc = {} - trc['g1'] = ccl.NumberCountsTracer(cosmo, False, (z1, pz1), (z1, bz1)) - trc['g2'] = ccl.NumberCountsTracer(cosmo, False, (z2, pz2), (z2, bz2)) - trc['l1'] = ccl.WeakLensingTracer(cosmo, (z1, pz1)) - trc['l2'] = ccl.WeakLensingTracer(cosmo, (z2, pz2)) + trc['g1'] = ccl.NumberCountsTracer(cosmo, has_rsd=False, + dndz=(z1, pz1), bias=(z1, bz1)) + trc['g2'] = ccl.NumberCountsTracer(cosmo, has_rsd=False, + dndz=(z2, pz2), bias=(z2, bz2)) + trc['l1'] = ccl.WeakLensingTracer(cosmo, dndz=(z1, pz1)) + trc['l2'] = ccl.WeakLensingTracer(cosmo, dndz=(z2, pz2)) # Read benchmarks bms = {} diff --git a/benchmarks/test_covariances.py b/benchmarks/test_covariances.py index 8b9cdf69f..6c0763113 100644 --- a/benchmarks/test_covariances.py +++ b/benchmarks/test_covariances.py @@ -42,17 +42,17 @@ def test_ssc_WL(): z, nofz = np.loadtxt(os.path.join(data_dir, "ssc_WL_nofz.txt"), unpack=True) - WL_tracer = ccl.WeakLensingTracer(cosmo, (z, nofz)) + WL_tracer = ccl.WeakLensingTracer(cosmo, dndz=(z, nofz)) ell = np.loadtxt(os.path.join(data_dir, "ssc_WL_ell.txt")) fsky = 0.05 - sigma2_B = ccl.sigma2_B_disc(cosmo, a=a, fsky=fsky) + sigma2_B = ccl.sigma2_B_disc(cosmo, a_arr=a, fsky=fsky) cov_ssc = ccl.covariances.angular_cl_cov_SSC(cosmo, - cltracer1=WL_tracer, - cltracer2=WL_tracer, - ell=ell, tkka=tk3D, + tracer1=WL_tracer, + tracer2=WL_tracer, + ell=ell, t_of_kk_a=tk3D, sigma2_B=(a, sigma2_B), fsky=None) var_ssc_ccl = np.diag(cov_ssc) diff --git a/benchmarks/test_distances.py b/benchmarks/test_distances.py index 428b54dc0..f321c54cb 100644 --- a/benchmarks/test_distances.py +++ b/benchmarks/test_distances.py @@ -46,7 +46,7 @@ def Neff_from_N_ur_N_ncdm(N_ur, N_ncdm): """Calculate N_eff from the number of relativistic and massive neutrinos.""" - Neff = N_ur + N_ncdm * ccl.physical_constants.TNCDM**4 / (4./11.)**(4./3.) + Neff = N_ur + N_ncdm * ccl.core._Defaults.T_ncdm**4 / (4./11.)**(4./3.) return Neff diff --git a/benchmarks/test_emu.py b/benchmarks/test_emu.py index 8aa2a1e26..487e65366 100644 --- a/benchmarks/test_emu.py +++ b/benchmarks/test_emu.py @@ -14,8 +14,8 @@ def test_emu_nu(model): cosmos = np.loadtxt("./benchmarks/data/emu_nu_cosmologies.txt") - mnu = ccl.nu_masses( - cosmos[model, 7] * cosmos[model, 2]**2, 'equal', T_CMB=2.725) + mnu = ccl.nu_masses(Omega_nu_h2=cosmos[model, 7]*cosmos[model, 2]**2, + mass_split='equal') cosmo = ccl.Cosmology( Omega_c=cosmos[model, 0], @@ -79,8 +79,8 @@ def test_emu(model): def test_emu_lin(model): cosmos = np.loadtxt("./benchmarks/data/emu_input_cosmologies.txt") - mnu = ccl.nu_masses( - cosmos[model, 7] * cosmos[model, 2]**2, 'equal', T_CMB=2.725) + mnu = ccl.nu_masses(Omega_nu_h2=cosmos[model, 7]*cosmos[model, 2]**2, + mass_split='equal') cosmo = ccl.Cosmology( Omega_c=cosmos[model, 0], diff --git a/benchmarks/test_haloprofile.py b/benchmarks/test_haloprofile.py index d981927d9..b52fe91ef 100644 --- a/benchmarks/test_haloprofile.py +++ b/benchmarks/test_haloprofile.py @@ -58,8 +58,7 @@ def test_profile_Einasto(): mdef = ccl.halos.MassDef(mDelta, 'matter') c = ccl.halos.ConcentrationConstant(c=concentration, mdef=mdef) - mdef = ccl.halos.MassDef(mDelta, 'matter', - c_m_relation=c) + mdef = ccl.halos.MassDef(mDelta, 'matter', concentration=c) p = ccl.halos.HaloProfileEinasto(c, truncated=False) prof = p.real(COSMO, r, halomass, a, mdef) @@ -118,7 +117,7 @@ def test_haloprofile(model): projected_analytic=True) prof = p.projected(COSMO, r, halomass, a, mdef) elif model == 'einasto': - mdef = ccl.halos.MassDef(halomassdef, 'matter', c_m_relation=c) + mdef = ccl.halos.MassDef(halomassdef, 'matter', concentration=c) p = ccl.halos.HaloProfileEinasto(c, truncated=False) prof = p.real(COSMO, r, halomass, a, mdef) elif model == 'hernquist': diff --git a/benchmarks/test_hbf.py b/benchmarks/test_hbf.py index acc29857d..8b6890b07 100644 --- a/benchmarks/test_hbf.py +++ b/benchmarks/test_hbf.py @@ -21,7 +21,7 @@ def test_hbf_tinker10(): m = d_hbf[0] for iz, z in enumerate(zs): hb_d = d_hbf[iz+1] - hb_h = mf.get_halo_bias(cosmo, m, 1. / (1 + z)) + hb_h = mf(cosmo, m, 1. / (1 + z)) assert np.all(np.fabs(hb_h / hb_d - 1) < 1E-3) @@ -32,7 +32,7 @@ def test_hbf_sheth01(): m = d_hbf[0] for iz, z in enumerate(zs): hb_d = d_hbf[iz+1] - hb_h = mf.get_halo_bias(cosmo, m, 1. / (1 + z)) + hb_h = mf(cosmo, m, 1. / (1 + z)) assert np.all(np.fabs(hb_h / hb_d - 1) < 1E-3) @@ -43,5 +43,5 @@ def test_hbf_bhattacharya11(): m = d_hbf[0] for iz, z in enumerate(zs): hb_d = d_hbf[iz+1] - hb_h = mf.get_halo_bias(cosmo, m, 1. / (1 + z)) + hb_h = mf(cosmo, m, 1. / (1 + z)) assert np.all(np.fabs(hb_h / hb_d - 1) < 1E-3) diff --git a/benchmarks/test_hmf.py b/benchmarks/test_hmf.py index 4af293c8f..b77bc2c31 100644 --- a/benchmarks/test_hmf.py +++ b/benchmarks/test_hmf.py @@ -22,7 +22,7 @@ def test_hmf_despali16(): m = d_hmf[0] for iz, z in enumerate(zs): nm_d = d_hmf[iz+1] - nm_h = mf.get_mass_function(cosmo, m, 1. / (1 + z)) + nm_h = mf(cosmo, m, 1. / (1 + z)) assert np.all(np.fabs(nm_h / nm_d - 1) < 0.01) @@ -34,7 +34,7 @@ def test_hmf_bocquet16(): m = d_hmf[0] for iz, z in enumerate(zs): nm_d = d_hmf[iz+1] - nm_h = mf.get_mass_function(cosmo, m, 1. / (1 + z)) + nm_h = mf(cosmo, m, 1. / (1 + z)) assert np.all(np.fabs(nm_h / nm_d - 1) < 0.01) @@ -45,7 +45,7 @@ def test_hmf_watson13(): m = d_hmf[0] for iz, z in enumerate(zs): nm_d = d_hmf[iz+1] - nm_h = mf.get_mass_function(cosmo, m, 1. / (1 + z)) + nm_h = mf(cosmo, m, 1. / (1 + z)) assert np.all(np.fabs(nm_h / nm_d - 1) < 0.01) @@ -56,7 +56,7 @@ def test_hmf_tinker08(): m = d_hmf[0] for iz, z in enumerate(zs): nm_d = d_hmf[iz+1] - nm_h = mf.get_mass_function(cosmo, m, 1. / (1 + z)) + nm_h = mf(cosmo, m, 1. / (1 + z)) assert np.all(np.fabs(nm_h / nm_d - 1) < 0.01) @@ -67,7 +67,7 @@ def test_hmf_press74(): m = d_hmf[0] for iz, z in enumerate(zs): nm_d = d_hmf[iz+1] - nm_h = mf.get_mass_function(cosmo, m, 1. / (1 + z)) + nm_h = mf(cosmo, m, 1. / (1 + z)) assert np.all(np.fabs(nm_h / nm_d - 1) < 0.01) @@ -78,7 +78,7 @@ def test_hmf_angulo12(): m = d_hmf[0] for iz, z in enumerate(zs): nm_d = d_hmf[iz+1] - nm_h = mf.get_mass_function(cosmo, m, 1. / (1 + z)) + nm_h = mf(cosmo, m, 1. / (1 + z)) assert np.all(np.fabs(nm_h / nm_d - 1) < 0.01) @@ -89,7 +89,7 @@ def test_hmf_sheth99(): m = d_hmf[0] for iz, z in enumerate(zs): nm_d = d_hmf[iz+1] - nm_h = mf.get_mass_function(cosmo, m, 1. / (1 + z)) + nm_h = mf(cosmo, m, 1. / (1 + z)) assert np.all(np.fabs(nm_h / nm_d - 1) < 0.01) @@ -100,5 +100,5 @@ def test_hmf_jenkins01(): m = d_hmf[0] for iz, z in enumerate(zs): nm_d = d_hmf[iz+1] - nm_h = mf.get_mass_function(cosmo, m, 1. / (1 + z)) + nm_h = mf(cosmo, m, 1. / (1 + z)) assert np.all(np.fabs(nm_h / nm_d - 1) < 0.01) diff --git a/benchmarks/test_hod.py b/benchmarks/test_hod.py index 13f8ef7a3..80f65ab13 100644 --- a/benchmarks/test_hod.py +++ b/benchmarks/test_hod.py @@ -63,14 +63,12 @@ def _nz_2mrs(z): bmax_0=rmax) prf2pt = ccl.halos.Profile2ptHOD() # P(k) - k_arr = np.geomspace(1E-4, 1E2, 512) - a_arr = np.linspace(0.8, 1, 32) + a_arr, lk_arr, _ = cosmo.get_linear_power().get_spline_arrays() pk_hod = ccl.halos.halomod_Pk2D(cosmo, hmc, prf, prof_2pt=prf2pt, - normprof1=True, lk_arr=np.log(k_arr), - a_arr=a_arr) + normprof1=True, lk_arr=lk_arr, a_arr=a_arr) # C_ell - tr = ccl.NumberCountsTracer(cosmo, False, (z_arr, dndz), - (z_arr, np.ones(len(dndz)))) + tr = ccl.NumberCountsTracer(cosmo, has_rsd=False, dndz=(z_arr, dndz), + bias=(z_arr, np.ones(len(dndz)))) cl_hod = ccl.angular_cl(cosmo, tr, tr, l_bm, p_of_k_a=pk_hod) assert np.all(np.fabs(cl_hod/cl_bm-1) < 0.005) diff --git a/benchmarks/test_massdef.py b/benchmarks/test_massdef.py index f4b1c7581..cfa605811 100644 --- a/benchmarks/test_massdef.py +++ b/benchmarks/test_massdef.py @@ -41,28 +41,28 @@ def test_mdef_get_radius(): def test_mdef_get_mass(): Ms_h = hmd_200m.get_mass(cosmo, Rs_200m, 1.) - assert np.all(np.fabs(Ms_h/Ms-1) < 1E-6) + assert np.all(np.fabs(Ms_h/Ms-1) < 5E-6) Ms_h = hmd_500c.get_mass(cosmo, Rs_500c, 1.) - assert np.all(np.fabs(Ms_h/Ms-1) < 1E-6) + assert np.all(np.fabs(Ms_h/Ms-1) < 5E-6) def test_mdef_concentration(): # Duffy 200 matter - cs_200m_dh = hmd_200m._get_concentration(cosmo, Ms, 1.) + cs_200m_dh = hmd_200m.concentration(cosmo, Ms, 1.) # Bhattacharya 200 matter - cs_200m_bh = hmd_200m_b._get_concentration(cosmo, Ms, 1.) + cs_200m_bh = hmd_200m_b.concentration(cosmo, Ms, 1.) # Duffy 200 critical - cs_200c_dh = hmd_200c._get_concentration(cosmo, Ms, 1.) + cs_200c_dh = hmd_200c.concentration(cosmo, Ms, 1.) # Bhattacharya 200 critical - cs_200c_bh = hmd_200c_b._get_concentration(cosmo, Ms, 1.) + cs_200c_bh = hmd_200c_b.concentration(cosmo, Ms, 1.) # Klypin virial - cs_vir_kh = hmd_vir._get_concentration(cosmo, Ms, 1.) + cs_vir_kh = hmd_vir.concentration(cosmo, Ms, 1.) # Bhattacharya virial - cs_vir_bh = hmd_vir_b._get_concentration(cosmo, Ms, 1.) + cs_vir_bh = hmd_vir_b.concentration(cosmo, Ms, 1.) # Prada 200 critical - cs_200c_ph = hmd_200c_p._get_concentration(cosmo, Ms, 1.) + cs_200c_ph = hmd_200c_p.concentration(cosmo, Ms, 1.) # Diemer 200 critical - cs_200c_dih = hmd_200c_di._get_concentration(cosmo, Ms, 1.) + cs_200c_dih = hmd_200c_di.concentration(cosmo, Ms, 1.) assert np.all(np.fabs(cs_200m_dh/cs_200m_d-1) < 1E-6) assert np.all(np.fabs(cs_200c_dh/cs_200c_d-1) < 1E-6) assert np.all(np.fabs(cs_200m_bh/cs_200m_b-1) < 3E-3) diff --git a/benchmarks/test_power_nu.py b/benchmarks/test_power_nu.py index 8bbb544b8..1da055e9b 100644 --- a/benchmarks/test_power_nu.py +++ b/benchmarks/test_power_nu.py @@ -4,6 +4,10 @@ import pyccl as ccl +# NOTE: We now check up to kmax=23 +# because CLASS v3 has a slight mismatch +# (6e-3) at higher k wavenumbers. +KMAX = 23 POWER_NU_TOL = 1.0E-3 @@ -43,12 +47,12 @@ def test_power_nu(model): warnings.simplefilter("ignore") pk_lin_ccl = ccl.linear_matter_power(cosmo, k_lin, a) - err = np.abs(pk_lin_ccl/pk_lin - 1) - assert np.allclose(err, 0, rtol=0, atol=POWER_NU_TOL) + assert np.allclose(pk_lin_ccl[k_lin < KMAX], + pk_lin[k_lin < KMAX], + rtol=POWER_NU_TOL) data_nl = np.loadtxt("./benchmarks/data/model%d_pk_nl_nu.txt" % (model+1)) k_nl = data_nl[:, 0] * cosmo['h'] pk_nl = data_nl[:, 1] / (cosmo['h']**3) pk_nl_ccl = ccl.nonlin_matter_power(cosmo, k_nl, a) - err = np.abs(pk_nl_ccl/pk_nl - 1) - assert np.allclose(err, 0, rtol=0, atol=POWER_NU_TOL) + assert np.allclose(pk_nl_ccl, pk_nl, rtol=POWER_NU_TOL) diff --git a/benchmarks/test_timing.py b/benchmarks/test_timing.py index d35689b88..a9f5cef27 100644 --- a/benchmarks/test_timing.py +++ b/benchmarks/test_timing.py @@ -22,11 +22,10 @@ def test_timing(): cosmo.compute_nonlin_power() start = time.time() - t_g = [ccl.NumberCountsTracer(cosmo, True, - (z_g, ng), + t_g = [ccl.NumberCountsTracer(cosmo, has_rsd=True, dndz=(z_g, ng), bias=(z_g, np.full(len(z_g), b))) for ng, b in zip(dNdz_g, b_g)] - t_s = [ccl.WeakLensingTracer(cosmo, (z_s, ns)) for ns in dNdz_s] + t_s = [ccl.WeakLensingTracer(cosmo, dndz=(z_s, ns)) for ns in dNdz_s] t_all = t_g + t_s cls = np.zeros([nx, nl]) diff --git a/benchmarks/test_tracers.py b/benchmarks/test_tracers.py index 1bb466ea4..41b06fdcf 100644 --- a/benchmarks/test_tracers.py +++ b/benchmarks/test_tracers.py @@ -20,12 +20,12 @@ def get_prediction(ells, chi_i, chi_f, alpha, beta, gamma, @pytest.fixture(scope='module') def set_up(): - ccl.physical_constants.T_CMB = 2.7 + ccl.physical_constants.unfreeze() ccl.gsl_params.INTEGRATION_LIMBER_EPSREL = 1E-4 ccl.gsl_params.INTEGRATION_EPSREL = 1E-4 cosmo = ccl.Cosmology(Omega_c=0.30, Omega_b=0.00, Omega_g=0, Omega_k=0, h=0.7, sigma8=0.8, n_s=0.96, Neff=0, m_nu=0.0, - w0=-1, wa=0, transfer_function='bbks', + w0=-1, wa=0, T_CMB=2.7, transfer_function='bbks', mass_function='tinker', matter_power_spectrum='linear') diff --git a/include/ccl.h b/include/ccl.h index 58a156d5a..1908d59fe 100644 --- a/include/ccl.h +++ b/include/ccl.h @@ -36,7 +36,6 @@ #include "ccl_correlation.h" #include "ccl_massfunc.h" #include "ccl_neutrinos.h" -#include "ccl_bcm.h" #include "ccl_bbks.h" #include "ccl_eh.h" #include "ccl_halofit.h" diff --git a/include/ccl_bcm.h b/include/ccl_bcm.h deleted file mode 100644 index 33bd3f8c8..000000000 --- a/include/ccl_bcm.h +++ /dev/null @@ -1,24 +0,0 @@ -/** @file */ -#ifndef __CCL_BCM_H_INCLUDED__ -#define __CCL_BCM_H_INCLUDED__ - -CCL_BEGIN_DECLS - -/** - * Correction for the impact of baryonic physics on the matter power spectrum. - * Returns f(k,a) [dimensionless] for given cosmology, using the method specified for the baryonic transfer function. - * f(k,a) is the fractional change in the nonlinear matter power spectrum from the Baryon Correction Model (BCM) of Schenider & Teyssier (2015). The parameters of the model are passed as part of the cosmology class. - * @param cosmo Cosmology parameters and configurations, including baryonic parameters. - * @param k Fourier mode, in [1/Mpc] units - * @param a scale factor, normalized to 1 for today - * @param status Status flag. 0 if there are no errors, nonzero otherwise. - * For specific cases see documentation for ccl_error.c - * @return f(k,a). - */ -double ccl_bcm_model_fka(ccl_cosmology * cosmo, double k, double a, int *status); - -void ccl_bcm_correct(ccl_cosmology *cosmo, ccl_f2d_t *psp, int *status); - -CCL_END_DECLS - -#endif diff --git a/include/ccl_core.h b/include/ccl_core.h index 7a09d65b5..2d1009ffa 100644 --- a/include/ccl_core.h +++ b/include/ccl_core.h @@ -228,6 +228,7 @@ typedef struct ccl_parameters { double sum_nu_masses; // sum of the neutrino masses. double Omega_nu_mass; // Omega_nu for MASSIVE neutrinos double Omega_nu_rel; // Omega_nu for MASSLESS neutrinos + double T_ncdm; // Non-CDM temperature in units of photon temperature. // Primordial power spectra double A_s; @@ -323,8 +324,12 @@ void ccl_cosmology_set_status_message(ccl_cosmology * cosmo, const char * status * @param w0 Dark energy EoS parameter * @param wa Dark energy EoS parameter * @param h Hubble constant in units of 100 km/s/Mpc - * @param norm_pk the normalization of the power spectrum, either A_s or sigma8 + * @param A_s amplitude of primordial scalar perturbations + * @param sigma8 variance of matter density fluctuations at 8 Mpc/h * @param n_s the power-law index of the power spectrum + * @param T_CMB CMB temperature + * @param Omega_g radiation density parameter + * @param T_ncdm the non-CDM temperature in units of photon temperature * @param bcm_log10Mc log10 cluster mass, one of the parameters of the BCM model * @param bcm_etab ejection radius parameter, one of the parameters of the BCM model * @param bcm_ks wavenumber for the stellar profile, one of the parameters of the BCM model @@ -337,8 +342,9 @@ void ccl_cosmology_set_status_message(ccl_cosmology * cosmo, const char * status */ ccl_parameters ccl_parameters_create(double Omega_c, double Omega_b, double Omega_k, double Neff, double* mnu, int n_mnu, - double w0, double wa, double h, double norm_pk, - double n_s, double bcm_log10Mc, double bcm_etab, double bcm_ks, + double w0, double wa, double h, double A_s, double sigma8, + double n_s, double T_CMB, double Omega_g, double T_ncdm, + double bcm_log10Mc, double bcm_etab, double bcm_ks, double mu_0, double sigma_0, double c1_mg, double c2_mg, double lambda_mg, int nz_mgrowth, double *zarr_mgrowth, double *dfarr_mgrowth, int *status); @@ -358,13 +364,6 @@ void ccl_parameters_free(ccl_parameters * params); */ void ccl_cosmology_free(ccl_cosmology * cosmo); -int ccl_get_pk_spline_na(ccl_cosmology *cosmo); -int ccl_get_pk_spline_nk(ccl_cosmology *cosmo); -void ccl_get_pk_spline_a_array(ccl_cosmology *cosmo,int ndout,double* doutput,int *status); -void ccl_get_pk_spline_lk_array(ccl_cosmology *cosmo,int ndout,double* doutput,int *status); -void ccl_get_pk_spline_a_array_from_params(ccl_spline_params *spline_params, int ndout, double *doutput, int *status); -void ccl_get_pk_spline_lk_array_from_params(ccl_spline_params *spline_params, int ndout, double *doutput, int *status); - CCL_END_DECLS #endif diff --git a/include/ccl_neutrinos.h b/include/ccl_neutrinos.h index a1594a9d5..e18f49988 100644 --- a/include/ccl_neutrinos.h +++ b/include/ccl_neutrinos.h @@ -38,23 +38,23 @@ typedef enum ccl_neutrino_mass_splits{ * @param Neff The effective number of species with neutrino mass mnu. * @param mnu Pointer to array containing neutrino mass (can be 0). * @param T_CMB Temperature of the CMB + * @param T_ncdm Non-CDM temperature in units of photon temperature. * @param status Status flag. 0 if there are no errors, nonzero otherwise. * For specific cases see documentation for ccl_error.c * @return OmNuh2 Fractional energy density of neutrions with mass mnu, multiplied by h squared. */ -double ccl_Omeganuh2(double a, int N_nu_mass, double* mnu, double T_CMB, int * status); +double ccl_Omeganuh2(double a, int N_nu_mass, double* mnu, double T_CMB, double T_ncdm, int * status); /** * Returns mass of one neutrino species at a scale factor a. * @param a Scale factor * @param Neff The effective number of species with neutrino mass mnu. * @param OmNuh2 Fractional energy density of neutrions with mass mnu, multiplied by h squared. (can be 0). - * @param T_CMB Temperature of the CMB * @param status Status flag. 0 if there are no errors, nonzero otherwise. * For specific cases see documentation for ccl_error.c * @return Mnu Neutrino mass [eV]. */ -double* ccl_nu_masses(double OmNuh2, ccl_neutrino_mass_splits mass_split, double T_CMB, int * status); +double* ccl_nu_masses(double OmNuh2, ccl_neutrino_mass_splits mass_split, int * status); CCL_END_DECLS #endif diff --git a/pyccl/__init__.py b/pyccl/__init__.py index 0103fce39..8d672446e 100644 --- a/pyccl/__init__.py +++ b/pyccl/__init__.py @@ -24,15 +24,7 @@ from . import ccllib as lib # Hashing, Caching, CCL base, Mutation locks -from .base import ( - CCLObject, - CCLHalosObject, - Caching, - cache, - hash_, - UnlockInstance, - unlock_instance, -) +from .base import * # Errors from .errors import ( @@ -41,14 +33,6 @@ CCLDeprecationWarning, ) -# Constants and accuracy parameters -from .parameters import ( - CCLParameters, - gsl_params, - spline_params, - physical_constants, -) - # Core data structures from .core import ( Cosmology, @@ -90,17 +74,7 @@ from .tk3d import Tk3D # Power spectrum calculations, sigma8 and kNL -from .power import ( - linear_power, - nonlin_power, - linear_matter_power, - nonlin_matter_power, - sigmaR, - sigmaV, - sigma8, - sigmaM, - kNL, -) +from .power import * # Baryons & Neutrinos from .bcm import ( @@ -108,15 +82,13 @@ bcm_correct_pk2d, ) -from .neutrinos import ( - Omeganuh2, - nu_masses, -) +from .neutrinos import * # Cells & Tracers -from .cls import angular_cl +from .cells import angular_cl from .tracers import ( Tracer, + NzTracer, NumberCountsTracer, WeakLensingTracer, CMBLensingTracer, @@ -149,6 +121,21 @@ from .pyutils import debug_mode, resample_array # Deprecated & Renamed modules +import warnings as _warnings +_warnings.warn( + "The default CMB temperature (T_CMB) will change in CCLv3.0.0, " + "from 2.725 to 2.7255 (Kelvin).", CCLDeprecationWarning) + +def __getattr__(name): + rename = {"cls": "cells"} + if name in rename: + from .errors import CCLDeprecationWarning + _warnings.warn(f"Module {name} has been renamed to {rename[name]}.", + CCLDeprecationWarning) + name = rename[name] + return eval(name) + raise AttributeError(f"No module named {name}.") + from .halomodel import ( halomodel_matter_power, halo_concentration, @@ -169,11 +156,14 @@ nfw_profile_2d, ) +from .baryons import ( + Baryons, + BaryonsSchneider15 +) + __all__ = ( - 'lib', 'Caching', 'cache', 'hash_', 'CCLObject', 'CCLHalosObject', - 'UnlockInstance', 'unlock_instance', - 'CCLParameters', 'physical_constants', 'gsl_params', 'spline_params', + 'lib', 'CCLError', 'CCLWarning', 'CCLDeprecationWarning', 'Cosmology', 'CosmologyVanillaLCDM', 'CosmologyCalculator', 'growth_factor', 'growth_factor_unnorm', 'growth_rate', @@ -182,14 +172,10 @@ 'h_over_h0', 'scale_factor_of_chi', 'omega_x', 'rho_x', 'sigma_critical', 'get_camb_pk_lin', 'get_isitgr_pk_lin', 'get_class_pk_lin', 'Pk2D', 'parse_pk2d', 'Tk3D', - 'linear_power', 'nonlin_power', - 'linear_matter_power', 'nonlin_matter_power', - 'sigmaR', 'sigmaV', 'sigma8', 'sigmaM', 'kNL', 'bcm_model_fka', 'bcm_correct_pk2d', - 'Omeganuh2', 'nu_masses', 'angular_cl', 'Tracer', 'NumberCountsTracer', 'WeakLensingTracer', 'CMBLensingTracer', - 'tSZTracer', 'CIBTracer', 'ISWTracer', + 'tSZTracer', 'CIBTracer', 'ISWTracer', 'NzTracer', 'get_density_kernel', 'get_kappa_kernel', 'get_lensing_kernel', 'correlation', 'correlation_3d', 'correlation_multipole', 'correlation_3dRsd', 'correlation_3dRsd_avgmu', 'correlation_pi_sigma', @@ -200,4 +186,5 @@ 'onehalo_matter_power', 'twohalo_matter_power', 'massfunc', 'halo_bias', 'massfunc_m2r', 'nfw_profile_3d', 'einasto_profile_3d', 'hernquist_profile_3d', 'nfw_profile_2d', + 'Baryons', 'BaryonsSchneider15', ) diff --git a/pyccl/background.py b/pyccl/background.py index 5e613e638..1c8dd3151 100644 --- a/pyccl/background.py +++ b/pyccl/background.py @@ -13,10 +13,12 @@ These strings define the `species` inputs to the functions below. """ import numpy as np +from scipy.interpolate import Akima1DInterpolator as interp from . import ccllib as lib -from .pyutils import _vectorize_fn, _vectorize_fn3 -from .pyutils import _vectorize_fn4, _vectorize_fn5 -from .parameters import physical_constants +from .pyutils import (_vectorize_fn, _vectorize_fn3, + _vectorize_fn4, _vectorize_fn5, check, loglin_spacing) +from .base.parameters import physical_constants as const +from .base import warn_api species_types = { 'critical': lib.species_crit_label, @@ -29,6 +31,29 @@ } +def compute_distances(cosmo): + """Compute the distance splines.""" + if cosmo.has_distances: + return + status = 0 + status = lib.cosmology_compute_distances(cosmo.cosmo, status) + check(status, cosmo) + + # lookback time + spl = cosmo.cosmo.spline_params # Replace for CCLv3. + a = loglin_spacing(spl.A_SPLINE_MINLOG, spl.A_SPLINE_MIN, spl.A_SPLINE_MAX, + spl.A_SPLINE_NLOG, spl.A_SPLINE_NA) + t_H = const.MPC_TO_METER / 1e14 / const.YEAR / cosmo["h"] + hoh0 = cosmo.h_over_h0(a) + integral = interp(a, 1/(a*hoh0)).antiderivative() + a_eval = np.r_[1.0, a] # make a single call to the spline + vals = integral(a_eval) + t_arr = t_H * (vals[0] - vals[1:]) + + cosmo.data.lookback = interp(a, t_arr) + cosmo.data.age0 = cosmo.data.lookback(0, extrapolate=True)[()] + + def growth_factor(cosmo, a): """Growth factor. @@ -248,7 +273,8 @@ def omega_x(cosmo, a, species): lib.omega_x_vec, cosmo, a, species_types[species]) -def rho_x(cosmo, a, species, is_comoving=False): +@warn_api +def rho_x(cosmo, a, species, *, is_comoving=False): """Physical or comoving density as a function of scale factor. Args: @@ -280,7 +306,8 @@ def rho_x(cosmo, a, species, is_comoving=False): species_types[species], int(is_comoving)) -def sigma_critical(cosmo, a_lens, a_source): +@warn_api +def sigma_critical(cosmo, *, a_lens, a_source): """Returns the critical surface mass density. .. math:: @@ -303,12 +330,143 @@ def sigma_critical(cosmo, a_lens, a_source): Ds = angular_diameter_distance(cosmo, a_source, a2=None) Dl = angular_diameter_distance(cosmo, a_lens, a2=None) Dls = angular_diameter_distance(cosmo, a_lens, a_source) - A = ( - physical_constants.CLIGHT**2 - * physical_constants.MPC_TO_METER - / (4.0 * np.pi * physical_constants.GNEWT - * physical_constants.SOLAR_MASS) - ) + A = (const.CLIGHT**2 * const.MPC_TO_METER + / (4.0 * np.pi * const.GNEWT * const.SOLAR_MASS)) Sigma_crit = A * Ds / (Dl * Dls) return Sigma_crit + + +def hubble_distance(cosmo, a): + r"""Hubble distance in :math:`\rm Mpc`. + + .. math:: + + D_{\rm H} = \frac{cz}{H_0} + + Arguments + --------- + cosmo : :class:`~pyccl.core.Cosmology` + Cosmological parameters. + a : float or (na,) array_like + Scale factor(s) normalized to 1 today. + + Returns + ------- + D_H : float or (na,) ``numpy.ndarray`` + Hubble distance. + """ + return (1/a - 1) * const.CLIGHT_HMPC / cosmo["h"] + + +def comoving_volume_element(cosmo, a): + r"""Comoving volume element in :math:`\rm Mpc^3 \, sr^{-1}`. + + .. math:: + + \frac{\mathrm{d}V}{\mathrm{d}a \, \mathrm{d} \Omega} + + Arguments + --------- + cosmo : :class:`~pyccl.core.Cosmology` + Cosmological parameters. + a : float or (na,) array_like + Scale factor(s) normalized to 1 today. + + Returns + ------- + dV : float or (na,) ``numpy.ndarray`` + Comoving volume per unit scale factor per unit solid angle. + + See Also + -------- + comoving_volume : integral of the comoving volume element + """ + Dm = comoving_angular_distance(cosmo, a) + Ez = h_over_h0(cosmo, a) + Dh = const.CLIGHT_HMPC / cosmo["h"] + return Dh * Dm**2 / (Ez * a**2) + + +def comoving_volume(cosmo, a, *, solid_angle=4*np.pi): + r"""Comoving volume, in :math:`\rm Mpc^3`. + + .. math:: + + V_{\rm C} = \int_{\Omega} \mathrm{{d}}\Omega \int_z \mathrm{d}z + D_{\rm H} \frac{(1+z)^2 D_{\mathrm{A}}^2}{E(z)} + + Arguments + --------- + cosmo : :class:`~pyccl.core.Cosmology` + Cosmological parameters. + a : float or (na,) array_like + Scale factor(s) normalized to 1 today. + solid_angle : float + Solid angle subtended in the sky for which + the comoving volume is calculated. + + Returns + ------- + V_C : float or (na,) ndarray + Comoving volume at ``a``. + + See Also + -------- + comoving_volume_element : comoving volume element + """ + Omk = cosmo["Omega_k"] + Dm = comoving_angular_distance(cosmo, a) + if Omk == 0: + return solid_angle/3 * Dm**3 + + Dh = hubble_distance(cosmo, a) + sqrt = np.sqrt(np.abs(Omk)) + DmDh = Dm / Dh + arcsinn = np.arcsin if Omk < 0 else np.arcsinh + return ((solid_angle * Dh**3 / (2 * Omk)) + * (DmDh * np.sqrt(1 + Omk * DmDh**2) + - arcsinn(sqrt * DmDh)/sqrt)) + + +def lookback_time(cosmo, a): + r"""Difference of the age of the Universe between some scale factor + and today, in :math:`\rm Gyr`. + + Arguments + --------- + cosmo : :class:`~pyccl.core.Cosmology` + Cosmological parameters. + a : float or (na,) array_like + Scale factor(s) normalized to 1 today. + + Returns + ------- + t_L : float or (na,) ndarray + Lookback time at ``a``. ``nan`` if ``a`` is out of bounds of the spline + parametets stored in ``cosmo``. + """ + cosmo.compute_distances() + out = cosmo.data.lookback(a) + return out[()] + + +def age_of_universe(cosmo, a): + r"""Age of the Universe at some scale factor, in :math:`\rm Gyr`. + + Arguments + --------- + cosmo : :class:`~pyccl.core.Cosmology` + Cosmological parameters. + a : float or (na,) array_like + Scale factor(s) normalized to 1 today. + + Returns + ------- + t_age : float or (na,) ndarray + Age of the Universe at ``a``. ``nan`` if ``a`` is out of bounds of the + spline parametets stored in ``cosmo``. + """ + cosmo.compute_distances() + out = cosmo.data.age0 - cosmo.lookback_time(a) + return out[()] diff --git a/pyccl/baryons/__init__.py b/pyccl/baryons/__init__.py new file mode 100644 index 000000000..f9abcc890 --- /dev/null +++ b/pyccl/baryons/__init__.py @@ -0,0 +1,8 @@ +from .baryons_base import Baryons +from .schneider15 import BaryonsSchneider15 + + +__all__ = ( + "Baryons", + "BaryonsSchneider15" +) diff --git a/pyccl/baryons/baryons_base.py b/pyccl/baryons/baryons_base.py new file mode 100644 index 000000000..8f6fa8b00 --- /dev/null +++ b/pyccl/baryons/baryons_base.py @@ -0,0 +1,38 @@ +from ..base import CCLAutoRepr, CCLNamedClass +from abc import abstractmethod + + +__all__ = ("Baryons",) + + +class Baryons(CCLAutoRepr, CCLNamedClass): + """`BaryonicEffect` obects are used to include the imprint of baryons + on the non-linear matter power spectrum. Their main ingredient is a + method `include_baryonic_effects` that takes in a `ccl.Pk2D` and + returns another `ccl.Pk2D` object that now accounts for baryonic + effects. + """ + + @abstractmethod + def _include_baryonic_effects(self, cosmo, pk): + """Apply baryonic effects to a given power spectrum. + + Args: + cosmo (:class:`~pyccl.core.Cosmology`): Cosmological parameters. + pk (:class:`~pyccl.pk2d.Pk2D`): power spectrum. + + Returns: + :obj:`~pyccl.pk2d.Pk2D` object. + """ + + def include_baryonic_effects(self, cosmo, pk): + """Apply baryonic effects to a given power spectrum. + + Args: + cosmo (:class:`~pyccl.core.Cosmology`): Cosmological parameters. + pk (:class:`~pyccl.pk2d.Pk2D`): power spectrum. + + Returns: + :obj:`~pyccl.pk2d.Pk2D` object or `None`. + """ + return self._include_baryonic_effects(cosmo, pk) diff --git a/pyccl/baryons/schneider15.py b/pyccl/baryons/schneider15.py new file mode 100644 index 000000000..ec5e0a8de --- /dev/null +++ b/pyccl/baryons/schneider15.py @@ -0,0 +1,98 @@ +from .baryons_base import Baryons +from ..pk2d import Pk2D +import numpy as np + + +__all__ = ("BaryonsSchneider15",) + + +class BaryonsSchneider15(Baryons): + """The BCM model boost factor for baryons. + + .. note:: BCM stands for the "baryonic correction model" of Schneider & + Teyssier (2015; https://arxiv.org/abs/1510.06034). See the + `DESC Note `_ + for details. + + .. note:: The boost factor is applied multiplicatively so that + :math:`P_{\\rm corrected}(k, a) = P(k, a)\\, f_{\\rm bcm}(k, a)`. + + Args: + log10Mc (:obj:`float`): logarithmic mass scale of hot + gas suppression. Defaults to log10(1.2E14). + eta_b (:obj:`float`): ratio of escape to ejection radii (see + Teyssier et al. 2015). Defaults to 0.5. + k_s (:obj:`float`): Characteristic scale (wavenumber) of + the stellar component. Defaults to 55.0. + """ + name = 'Schneider15' + __repr_attrs__ = ("log10Mc", "eta_b", "k_s") + + def __init__(self, log10Mc=np.log10(1.2E14), eta_b=0.5, k_s=55.0): + self.log10Mc = log10Mc + self.eta_b = eta_b + self.k_s = k_s + + def boost_factor(self, cosmo, k, a): + """The BCM model boost factor for baryons. + + Args: + cosmo (:class:`~pyccl.core.Cosmology`): Cosmological parameters. + k (float or array_like): Wavenumber; Mpc^-1. + a (float or array_like): Scale factor. + + Returns: + float or array_like: Correction factor to apply to + the power spectrum. + """ + a_use, k_use = map(np.atleast_1d, [a, k]) + a_use, k_use = a_use[:, None], k_use[None, :] + + z = 1/a_use - 1 + kh = k_use / cosmo['h'] + b0 = 0.105*self.log10Mc - 1.27 + bfunc = b0 / (1. + (z/2.3)**2.5) + kg = 0.7 * (1-bfunc)**4 * self.eta_b**(-1.6) + gf = bfunc / (1 + (kh/kg)**3) + 1. - bfunc + scomp = 1 + (kh / self.k_s)**2 + fka = gf * scomp + + if np.ndim(k) == 0: + fka = np.squeeze(fka, axis=-1) + if np.ndim(a) == 0: + fka = np.squeeze(fka, axis=0) + return fka + + def update_parameters(self, log10Mc=None, eta_b=None, k_s=None): + """Update BCM parameters. + + Args: + log10Mc (:obj:`float`): logarithmic mass scale of hot + gas suppression. Defaults to 14.08. + eta_b (:obj:`float`): ratio of escape to ejection radii (see + Teyssier et al. 2015). Defaults to 0.5. + k_s (:obj:`float`): Characteristic scale (wavenumber) of + the stellar component. Defaults to 55.0. + """ + if log10Mc is not None: + self.log10Mc = log10Mc + if eta_b is not None: + self.eta_b = eta_b + if k_s is not None: + self.k_s = k_s + + def _include_baryonic_effects(self, cosmo, pk): + # Applies boost factor + a_arr, lk_arr, pk_arr = pk.get_spline_arrays() + k_arr = np.exp(lk_arr) + fka = self.boost_factor(cosmo, k_arr, a_arr) + pk_arr *= fka + + if pk.psp.is_log: + np.log(pk_arr, out=pk_arr) # in-place log + + return Pk2D(a_arr=a_arr, lk_arr=lk_arr, pk_arr=pk_arr, + is_logp=pk.psp.is_log, + extrap_order_lok=pk.extrap_order_lok, + extrap_order_hik=pk.extrap_order_hik) diff --git a/pyccl/base.py b/pyccl/base.py deleted file mode 100644 index 5725cb0ab..000000000 --- a/pyccl/base.py +++ /dev/null @@ -1,627 +0,0 @@ -import sys -import functools -from collections import OrderedDict -import numpy as np -from inspect import signature -from _thread import RLock -from abc import ABC - - -def _to_hashable(obj): - """Make unhashable objects hashable in a consistent manner.""" - - if isinstance(obj, (int, float, str)): - # Strings and Numbers are hashed directly. - return obj - - elif hasattr(obj, "__iter__"): - # Encapsulate all the iterables to quickly discard as needed. - - if isinstance(obj, np.ndarray): - # Numpy arrays: Convert the data buffer to a byte string. - return obj.tobytes() - - elif isinstance(obj, dict): - # Dictionaries: Build a tuple from key-value pairs, - # where all values are converted to hashables. - out = {key: _to_hashable(value) for key, value in obj.items()} - # Sort unordered dictionaries for hash consistency. - if isinstance(obj, OrderedDict): - return tuple(out.items()) - return tuple(sorted(out.items())) - - else: - # Iterables: Build a tuple from values converted to hashables. - out = [_to_hashable(item) for item in obj] - return tuple(out) - - elif hasattr(obj, "__hash__"): - # Hashables: Just return the object. - return obj - - # NotImplemented: Can't hash safely, so raise TypeError. - raise TypeError(f"Hashing for {type(obj)} not implemented.") - - -def hash_(obj): - """Generic hash method, which changes between processes.""" - digest = hash(repr(_to_hashable(obj))) + sys.maxsize + 1 - return digest - - -class _CachingMeta(type): - """Implement ``property`` to a ``classmethod`` for ``Caching``.""" - # NOTE: Only in 3.8 < py < 3.11 can `classmethod` wrap `property`. - # https://docs.python.org/3.11/library/functions.html#classmethod - @property - def maxsize(cls): - return cls._maxsize - - @maxsize.setter - def maxsize(cls, value): - if value < 0: - raise ValueError( - "`maxsize` should be larger than zero. " - "To disable caching, use `Caching.disable()`.") - cls._maxsize = value - for func in cls._cached_functions: - func.cache_info.maxsize = value - - @property - def policy(cls): - return cls._policy - - @policy.setter - def policy(cls, value): - if value not in cls._policies: - raise ValueError("Cache retention policy not recognized.") - if value == "lfu" != cls._policy: - # Reset counter if we change policy to lfu - # otherwise new objects are prone to being discarded immediately. - # Now, the counter is not just used for stats, - # it is part of the retention policy. - for func in cls._cached_functions: - for item in func.cache_info._caches.values(): - item.reset() - cls._policy = value - for func in cls._cached_functions: - func.cache_info.policy = value - - -class Caching(metaclass=_CachingMeta): - """Infrastructure to hold cached objects. - - Caching is used for pre-computed objects that are expensive to compute. - - Attributes: - maxsize (``int``): - Maximum number of caches to store. If the dictionary is full, new - caches are assigned according to the set cache retention policy. - policy (``'fifo'``, ``'lru'``, ``'lfu'``): - Cache retention policy. - """ - _enabled: bool = False - _policies: list = ['fifo', 'lru', 'lfu'] - _default_maxsize: int = 128 # class default maxsize - _default_policy: str = 'lru' # class default policy - _maxsize = _default_maxsize # user-defined maxsize - _policy = _default_policy # user-defined policy - _cached_functions: list = [] - - @classmethod - def _get_key(cls, func, *args, **kwargs): - """Calculate the hex hash from arguments and keyword arguments.""" - # get a dictionary of default parameters - params = func.cache_info._signature.parameters - # get a dictionary of the passed parameters - passed = {**dict(zip(params, args)), **kwargs} - # discard the values equal to the default - defaults = {param: value.default for param, value in params.items()} - return hex(hash_({**defaults, **passed})) - - @classmethod - def _get(cls, dic, key, policy): - """Get the cached object container - under the implemented caching policy. - """ - obj = dic[key] - if policy == "lru": - dic.move_to_end(key) - # update stats - obj.increment() - return obj - - @classmethod - def _pop(cls, dic, policy): - """Remove one cached item as per the implemented caching policy.""" - if policy == "lfu": - keys = list(dic) - idx = np.argmin([item.counter for item in dic.values()]) - dic.move_to_end(keys[idx], last=False) - dic.popitem(last=False) - - @classmethod - def _decorator(cls, func, maxsize, policy): - # assign caching attributes to decorated function - func.cache_info = CacheInfo(func, maxsize=maxsize, policy=policy) - func.clear_cache = func.cache_info._clear_cache - cls._cached_functions.append(func) - - @functools.wraps(func) - def wrapper(*args, **kwargs): - if not cls._enabled: - return func(*args, **kwargs) - - key = cls._get_key(func, *args, **kwargs) - # shorthand access - caches = func.cache_info._caches - maxsize = func.cache_info.maxsize - policy = func.cache_info.policy - - with RLock(): - if key in caches: - # output has been cached; update stats and return it - out = cls._get(caches, key, policy) - func.cache_info.hits += 1 - return out.item - - with RLock(): - while len(caches) >= maxsize: - # output not cached and no space available, so remove - # items as per the caching policy until there is space - cls._pop(caches, policy) - - # cache new entry and update stats - out = CachedObject(func(*args, **kwargs)) - caches[key] = out - func.cache_info.misses += 1 - return out.item - - return wrapper - - @classmethod - def cache(cls, func=None, *, maxsize=_maxsize, policy=_policy): - """Cache the output of the decorated function, using the input - arguments as a proxy to build a hash key. - - Arguments: - func (``function``): - Function to be decorated. - maxsize (``int``): - Maximum cache size for the decorated function. - policy (``'fifo'``, ``'lru'``, ``'lfu'``): - Cache retention policy. When the storage reaches maxsize - decide which cached object will be deleted. Default is 'lru'.\n - 'fifo': first-in-first-out,\n - 'lru': least-recently-used,\n - 'lfu': least-frequently-used. - """ - if maxsize < 0: - raise ValueError( - "`maxsize` should be larger than zero. " - "To disable caching, use `Caching.disable()`.") - if policy not in cls._policies: - raise ValueError("Cache retention policy not recognized.") - - if func is None: - # `@cache` with parentheses - return functools.partial( - cls._decorator, maxsize=maxsize, policy=policy) - # `@cache()` without parentheses - return cls._decorator(func, maxsize=maxsize, policy=policy) - - @classmethod - def enable(cls): - cls._enabled = True - - @classmethod - def disable(cls): - cls._enabled = False - - @classmethod - def reset(cls): - cls.maxsize = cls._default_maxsize - cls.policy = cls._default_policy - - @classmethod - def clear_cache(cls): - [func.clear_cache() for func in cls._cached_functions] - - -cache = Caching.cache - - -class CacheInfo: - """Cache info container. - Assigned to cached function as ``function.cache_info``. - - Parameters: - func (``function``): - Function in which an instance of this class will be assigned. - maxsize (``Caching.maxsize``): - Maximum number of caches to store. - policy (``Caching.policy``): - Cache retention policy. - - .. note :: - - To assist in deciding an optimal ``maxsize`` and ``policy``, instances - of this class contain the following attributes: - - ``hits``: number of times the function has been bypassed - - ``misses``: number of times the function has computed something - - ``current_size``: current size of the cache dictionary - """ - - def __init__(self, func, maxsize=Caching.maxsize, policy=Caching.policy): - # we store the signature of the function on import - # as it is the most expensive operation (~30x slower) - self._signature = signature(func) - self._caches = OrderedDict() - self.maxsize = maxsize - self.policy = policy - self.hits = self.misses = 0 - - @property - def current_size(self): - return len(self._caches) - - def __repr__(self): - s = f"<{self.__class__.__name__}>" - for par, val in self.__dict__.items(): - if not par.startswith("_"): - s += f"\n\t {par} = {val!r}" - s += f"\n\t current_size = {self.current_size!r}" - return s - - def _clear_cache(self): - self._caches = OrderedDict() - self.hits = self.misses = 0 - - -class CachedObject: - """A cached object container. - - Attributes: - counter (``int``): - Number of times the cached item has been retrieved. - """ - counter: int = 0 - - def __init__(self, obj): - self.item = obj - - def __repr__(self): - s = f"CachedObject(counter={self.counter})" - return s - - def increment(self): - self.counter += 1 - - def reset(self): - self.counter = 0 - - -class ObjectLock: - """Control the lock state (immutability) of a ``CCLObject``.""" - _locked: bool = False - _lock_id: int = None - - def __repr__(self): - return f"{self.__class__.__name__}(locked={self.locked})" - - @property - def locked(self): - """Check if the object is locked.""" - return self._locked - - @property - def active(self): - """Check if an unlocking context manager is active.""" - return self._lock_id is not None - - def lock(self): - """Lock the object.""" - self._locked = True - self._lock_id = None - - def unlock(self, manager_id=None): - """Unlock the object.""" - self._locked = False - if manager_id is not None: - self._lock_id = manager_id - - -class UnlockInstance: - """Context manager that temporarily unlocks an immutable instance - of ``CCLObject``. - - Parameters: - instance (``CCLObject``): - Instance of ``CCLObject`` to unlock within the scope - of the context manager. - mutate (``bool``): - If the enclosed function mutates the object, the stored - representation is automatically deleted. - """ - - def __init__(self, instance, *, mutate=True): - self.instance = instance - self.mutate = mutate - # Define these attributes for easy access. - self.id = id(self) - self.thread_lock = RLock() - # We want to catch and exit if the instance is not a CCLObject. - # Hopefully this will be caught downstream. - self.check_instance = isinstance(instance, CCLObject) - if self.check_instance: - self.object_lock = instance._object_lock - - def __enter__(self): - if not self.check_instance: - return - - with self.thread_lock: - # Prevent simultaneous enclosing of a single instance. - if self.object_lock.active: - # Context manager already active. - return - - # Unlock and store the fingerprint of this context manager so that - # only this context manager is allowed to run on the instance. - self.object_lock.unlock(manager_id=self.id) - - def __exit__(self, type, value, traceback): - if not self.check_instance: - return - - # If another context manager is running, - # do nothing; otherwise reset. - if self.id != self.object_lock._lock_id: - return - - with self.thread_lock: - # Reset `repr` if the object has been mutated. - if self.mutate: - try: - delattr(self.instance, "_repr") - delattr(self.instance, "_hash") - except AttributeError: - # Object mutated but none of these exist. - pass - - # Lock the instance on exit. - self.object_lock.lock() - - @classmethod - def unlock_instance(cls, func=None, *, name=None, mutate=True): - """Decorator that temporarily unlocks an instance of CCLObject. - - Arguments: - func (``function``): - Function which changes one of its ``CCLObject`` arguments. - name (``str``): - Name of the parameter to unlock. Defaults to the first one. - If not a ``CCLObject`` the decorator will do nothing. - mutate (``bool``): - If after the function ``instance_old != instance_new``, the - instance is mutated. If ``True``, the representation of the - object will be reset. - """ - if func is None: - # called with parentheses - return functools.partial(cls.unlock_instance, name=name, - mutate=mutate) - - if not hasattr(func, "__signature__"): - # store the function signature - func.__signature__ = signature(func) - names = list(func.__signature__.parameters.keys()) - name = names[0] if name is None else name # default name - if name not in names: - # ensure the name makes sense - raise NameError(f"{name} does not exist in {func.__name__}.") - - @functools.wraps(func) - def wrapper(*args, **kwargs): - bound = func.__signature__.bind(*args, **kwargs) - with UnlockInstance(bound.arguments[name], mutate=mutate): - return func(*args, **kwargs) - return wrapper - - @classmethod - def Funlock(cls, cl, name, mutate: bool): - """Allow an instance to change or mutate when `name` is called.""" - func = vars(cl).get(name) - if func is not None: - newfunc = cls.unlock_instance(mutate=mutate)(func) - setattr(cl, name, newfunc) - - -unlock_instance = UnlockInstance.unlock_instance - - -class FancyRepr: - """Controls the usage of fancy ``__repr__` for ``CCLObjects.""" - _enabled: bool = True - _classes: dict = {} - - def __init__(self): - # This is only a framework class, we do not instantiate it. - raise NotImplementedError - - @classmethod - def add(cls, cl): - """Add class to the internal dictionary of fancy-repr classes.""" - cls._classes[cl] = cl.__repr__ - - @classmethod - def enable(cls): - """Enable fancy representations if they exist.""" - for cl, method in cls._classes.items(): - FancyRepr.bind_and_replace(cl, method) - cls._enabled = True - - @classmethod - def disable(cls): - """Disable fancy representations and fall back to Python defaults.""" - for cl in cls._classes.keys(): - cl.__repr__ = object.__repr__ - cls._enabled = False - - @classmethod - def bind_and_replace(cls, cl, method): - """Bind ``method`` to class ``cl``, and replace original with default. - This helper only works for binding and replacing ``__repr__`` methods - for ``CCLObjects``. - """ - # If the class defines a custom `__repr__`, this will be the new - # `_repr` (which is cached). Decorator `cached_property` requires - # that `__set_name__` is called on it. - bmethod = functools.cached_property(method) - cl._repr = bmethod - bmethod.__set_name__(cl, "_repr") - # Fall back to using `__ccl_repr__` from `CCLObject`. - cl.__repr__ = cl.__ccl_repr__ - - -class _DisableGetMethod: - """Descriptor that disables the dot (``getattr``).""" - - def __get__(self, instance, owner): - raise AttributeError( - "To access fancy-repr info use `CCLObject._fancy_repr`.") - - -class CCLObject(ABC): - """Base for CCL objects. - - All CCL objects inherit ``__eq__`` and ``__hash__`` methods from here. - Both methods rely on ``__repr__`` uniqueness. This aims to homogenize - equivalence checking, and to standardize the use of hash. - - Overview - -------- - ``CCLObjects`` inherit ``__hash__``, which consistently hashes the - representation string. They also inherit ``__eq__`` which checks for - representation equivalence. - - In the implemented scheme, each ``CCLObject`` may have its own, specialized - ``__repr__`` method overloaded. Object representations have to be unique - for equivalent objects. If no ``__repr__`` is provided, the default from - ``object`` is used. - - Mutation - -------- - ``CCLObjects`` are by default immutable. This aims to provide a failsafe - mechanism, where, changing attributes has to trigger a re-computation - of something else inside of the instance, rather than simply doing a value - change. - - This immutability mechanism can be safely bypassed if a subclass defines an - ``update_parameters`` method. ``CCLObjects`` temporarily unlock whenever - this method is called. - - Internal State vs. Mutation - --------------------------- - Other methods that use ``setattr`` can only do that if they are decorated - with ``@unlock_instance`` or if the particular code block that makes the - change is enclosed within the ``UnlockInstance`` context manager. - If neither is provided, an exception is raised. - - If such methods only change the instance's internal state, the decorator - may be called with ``@unlock_instance(mutate=False)`` (or equivalently - for the context manager ``UnlockInstance(..., mutate=False)``). Otherwise, - the instance is assumed to have mutated. - """ - _fancy_repr = FancyRepr - - def __init_subclass__(cls, **kwargs): - super().__init_subclass__(**kwargs) - - # 1. Store the signature of the constructor on import. - cls.__signature__ = signature(cls.__init__) - - # 2. Replace repr (if implemented) with its cached version. - cls._fancy_repr = _DisableGetMethod() - if "__repr__" in vars(cls): - CCLObject._fancy_repr.add(cls) - FancyRepr.bind_and_replace(cls, cls.__repr__) - - # 3. Unlock instance on specific methods. - UnlockInstance.Funlock(cls, "__init__", mutate=False) - UnlockInstance.Funlock(cls, "update_parameters", mutate=True) - - def __new__(cls, *args, **kwargs): - # Populate every instance with an `ObjectLock` as attribute. - instance = super().__new__(cls) - object.__setattr__(instance, "_object_lock", ObjectLock()) - return instance - - def __setattr__(self, name, value): - if self._object_lock.locked: - raise AttributeError("CCL objects can only be updated via " - "`update_parameters`, if implemented.") - object.__setattr__(self, name, value) - - def update_parameters(self, **kwargs): - name = self.__class__.__qualname__ - raise NotImplementedError(f"{name} objects are immutable.") - - @functools.cached_property - def _repr(self): - # By default we use `__repr__` from `object`. - return object.__repr__(self) - - @functools.cached_property - def _hash(self): - # `__hash__` makes use of the `repr` of the object, - # so we have to make sure that the `repr` is unique. - return hash(repr(self)) - - def __ccl_repr__(self): - # The custom `__repr__` is converted to a - # cached property and is replaced by this method. - return self._repr - - __repr__ = __ccl_repr__ - - def __hash__(self): - return self._hash - - def __eq__(self, other): - # Two same-type objects are equal if their representations are equal. - if self.__class__ is not other.__class__: - return False - return repr(self) == repr(other) - - -class CCLHalosObject(CCLObject): - """Base for halo objects. Representations for instances are built from a - list of attribute names specified as a class variable in ``__repr_attrs__`` - (acting as a hook). - - Example: - The representation (also hash) of instances of the following class - is built based only on the attributes specified in ``__repr_attrs__``: - - >>> class MyClass(CCLHalosObject): - __repr_attrs__ = ("a", "b", "other") - def __init__(self, a=1, b=2, c=3, d=4, e=5): - self.a = a - self.b = b - self.c = c - self.other = d + e - - >>> repr(MyClass(6, 7, 8, 9, 10)) - <__main__.MyClass> - a = 6 - b = 7 - other = 19 - """ - - def __repr__(self): - # Build string from specified `__repr_attrs__` or use Python's default. - if hasattr(self.__class__, "__repr_attrs__"): - from ._repr import _build_string_from_attrs - return _build_string_from_attrs(self) - return object.__repr__(self) diff --git a/pyccl/base/__init__.py b/pyccl/base/__init__.py new file mode 100644 index 000000000..c935fce51 --- /dev/null +++ b/pyccl/base/__init__.py @@ -0,0 +1,4 @@ +from .caching import * +from .deprecations import * +from .parameters import * +from .schema import * diff --git a/pyccl/base/caching.py b/pyccl/base/caching.py new file mode 100644 index 000000000..7bdfb7f2e --- /dev/null +++ b/pyccl/base/caching.py @@ -0,0 +1,304 @@ +import sys +import functools +from collections import OrderedDict +import numpy as np +from _thread import RLock +from inspect import signature + + +__all__ = ("hash_", "Caching", "cache", "CacheInfo", "CachedObject",) + + +def _to_hashable(obj): + """Make unhashable objects hashable in a consistent manner.""" + + if isinstance(obj, (int, float, str)): + # Strings and Numbers are hashed directly. + return obj + + elif hasattr(obj, "__iter__"): + # Encapsulate all the iterables to quickly discard as needed. + + if isinstance(obj, np.ndarray): + # Numpy arrays: Convert the data buffer to a byte string. + return obj.tobytes() + + elif isinstance(obj, dict): + # Dictionaries: Build a tuple from key-value pairs, + # where all values are converted to hashables. + out = {key: _to_hashable(value) for key, value in obj.items()} + # Sort unordered dictionaries for hash consistency. + if isinstance(obj, OrderedDict): + return tuple(out.items()) + return tuple(sorted(out.items())) + + else: + # Iterables: Build a tuple from values converted to hashables. + out = [_to_hashable(item) for item in obj] + return tuple(out) + + elif hasattr(obj, "__hash__"): + # Hashables: Just return the object. + return obj + + # NotImplemented: Can't hash safely, so raise TypeError. + # Note: This will never be triggered since `type` has a repr slot wrapper. + raise TypeError(f"Hashing for {type(obj)} not implemented.") + + +def hash_(obj): + """Generic hash method, which changes between processes.""" + digest = hash(repr(_to_hashable(obj))) + sys.maxsize + 1 + return digest + + +class _CachingMeta(type): + """Implement ``property`` to a ``classmethod`` for ``Caching``.""" + # NOTE: Only in 3.8 < py < 3.11 can `classmethod` wrap `property`. + # https://docs.python.org/3.11/library/functions.html#classmethod + @property + def maxsize(cls): + return cls._maxsize + + @maxsize.setter + def maxsize(cls, value): + if value < 0: + raise ValueError( + "`maxsize` should be larger than zero. " + "To disable caching, use `Caching.disable()`.") + cls._maxsize = value + for func in cls._cached_functions: + func.cache_info.maxsize = value + + @property + def policy(cls): + return cls._policy + + @policy.setter + def policy(cls, value): + if value not in cls._policies: + raise ValueError("Cache retention policy not recognized.") + if value == "lfu" != cls._policy: + # Reset counter if we change policy to lfu + # otherwise new objects are prone to being discarded immediately. + # Now, the counter is not just used for stats, + # it is part of the retention policy. + for func in cls._cached_functions: + for item in func.cache_info._caches.values(): + item.reset() + cls._policy = value + for func in cls._cached_functions: + func.cache_info.policy = value + + +class Caching(metaclass=_CachingMeta): + """Infrastructure to hold cached objects. + + Caching is used for pre-computed objects that are expensive to compute. + + Attributes: + maxsize (``int``): + Maximum number of caches to store. If the dictionary is full, new + caches are assigned according to the set cache retention policy. + policy (``'fifo'``, ``'lru'``, ``'lfu'``): + Cache retention policy. + """ + _enabled: bool = False + _policies: list = ['fifo', 'lru', 'lfu'] + _default_maxsize: int = 128 # class default maxsize + _default_policy: str = 'lru' # class default policy + _maxsize = _default_maxsize # user-defined maxsize + _policy = _default_policy # user-defined policy + _cached_functions: list = [] + + @classmethod + def _get_key(cls, func, *args, **kwargs): + """Calculate the hex hash from arguments and keyword arguments.""" + # get a dictionary of default parameters + params = func.cache_info._signature.parameters + # get a dictionary of the passed parameters + passed = {**dict(zip(params, args)), **kwargs} + # discard the values equal to the default + defaults = {param: value.default for param, value in params.items()} + return hex(hash_({**defaults, **passed})) + + @classmethod + def _get(cls, dic, key, policy): + """Get the cached object container + under the implemented caching policy. + """ + obj = dic[key] + if policy == "lru": + dic.move_to_end(key) + # update stats + obj.increment() + return obj + + @classmethod + def _pop(cls, dic, policy): + """Remove one cached item as per the implemented caching policy.""" + if policy == "lfu": + keys = list(dic) + idx = np.argmin([item.counter for item in dic.values()]) + dic.move_to_end(keys[idx], last=False) + dic.popitem(last=False) + + @classmethod + def _decorator(cls, func, maxsize, policy): + # assign caching attributes to decorated function + func.cache_info = CacheInfo(func, maxsize=maxsize, policy=policy) + func.clear_cache = func.cache_info._clear_cache + cls._cached_functions.append(func) + + @functools.wraps(func) + def wrapper(*args, **kwargs): + if not cls._enabled: + return func(*args, **kwargs) + + key = cls._get_key(func, *args, **kwargs) + # shorthand access + caches = func.cache_info._caches + maxsize = func.cache_info.maxsize + policy = func.cache_info.policy + + with RLock(): + if key in caches: + # output has been cached; update stats and return it + out = cls._get(caches, key, policy) + func.cache_info.hits += 1 + return out.item + + with RLock(): + while len(caches) >= maxsize: + # output not cached and no space available, so remove + # items as per the caching policy until there is space + cls._pop(caches, policy) + + # cache new entry and update stats + out = CachedObject(func(*args, **kwargs)) + caches[key] = out + func.cache_info.misses += 1 + return out.item + + return wrapper + + @classmethod + def cache(cls, func=None, *, maxsize=_maxsize, policy=_policy): + """Cache the output of the decorated function, using the input + arguments as a proxy to build a hash key. + + Arguments: + func (``function``): + Function to be decorated. + maxsize (``int``): + Maximum cache size for the decorated function. + policy (``'fifo'``, ``'lru'``, ``'lfu'``): + Cache retention policy. When the storage reaches maxsize + decide which cached object will be deleted. Default is 'lru'.\n + 'fifo': first-in-first-out,\n + 'lru': least-recently-used,\n + 'lfu': least-frequently-used. + """ + if maxsize < 0: + raise ValueError( + "`maxsize` should be larger than zero. " + "To disable caching, use `Caching.disable()`.") + if policy not in cls._policies: + raise ValueError("Cache retention policy not recognized.") + + if func is None: + # `@cache` with parentheses + return functools.partial( + cls._decorator, maxsize=maxsize, policy=policy) + # `@cache()` without parentheses + return cls._decorator(func, maxsize=maxsize, policy=policy) + + @classmethod + def enable(cls): + cls._enabled = True + + @classmethod + def disable(cls): + cls._enabled = False + + @classmethod + def reset(cls): + cls.maxsize = cls._default_maxsize + cls.policy = cls._default_policy + + @classmethod + def clear_cache(cls): + [func.clear_cache() for func in cls._cached_functions] + + +cache = Caching.cache + + +class CacheInfo: + """Cache info container. + Assigned to cached function as ``function.cache_info``. + + Parameters: + func (``function``): + Function in which an instance of this class will be assigned. + maxsize (``Caching.maxsize``): + Maximum number of caches to store. + policy (``Caching.policy``): + Cache retention policy. + + .. note :: + + To assist in deciding an optimal ``maxsize`` and ``policy``, instances + of this class contain the following attributes: + - ``hits``: number of times the function has been bypassed + - ``misses``: number of times the function has computed something + - ``current_size``: current size of the cache dictionary + """ + + def __init__(self, func, maxsize=Caching.maxsize, policy=Caching.policy): + # we store the signature of the function on import + # as it is the most expensive operation (~30x slower) + self._signature = signature(func) + self._caches = OrderedDict() + self.maxsize = maxsize + self.policy = policy + self.hits = self.misses = 0 + + @property + def current_size(self): + return len(self._caches) + + def __repr__(self): + s = f"<{self.__class__.__name__}>" + for par, val in self.__dict__.items(): + if not par.startswith("_"): + s += f"\n\t {par} = {val!r}" + s += f"\n\t current_size = {self.current_size!r}" + return s + + def _clear_cache(self): + self._caches = OrderedDict() + self.hits = self.misses = 0 + + +class CachedObject: + """A cached object container. + + Attributes: + counter (``int``): + Number of times the cached item has been retrieved. + """ + counter: int = 0 + + def __init__(self, obj): + self.item = obj + + def __repr__(self): + s = f"CachedObject(counter={self.counter})" + return s + + def increment(self): + self.counter += 1 + + def reset(self): + self.counter = 0 diff --git a/pyccl/base/deprecations.py b/pyccl/base/deprecations.py new file mode 100644 index 000000000..648ab9497 --- /dev/null +++ b/pyccl/base/deprecations.py @@ -0,0 +1,226 @@ +from ..errors import CCLDeprecationWarning +from inspect import signature, Parameter +import functools +import warnings + + +__all__ = ("deprecated", "warn_api", "deprecate_attr",) + + +def deprecated(new_function=None): + """This is a decorator which can be used to mark functions + as deprecated. It will result in a warning being emitted + when the function is used. If there is a replacement function, + pass it as `new_function`. + """ + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + s = f"The function {func.__qualname__} is deprecated." + if new_function: + s += f" Use {new_function.__qualname__} instead." + warnings.warn(s, CCLDeprecationWarning) + return func(*args, **kwargs) + return wrapper + return decorator + + +def warn_api(func=None, *, pairs=[], reorder=[]): + """This decorator translates old API to new API for: + - functions/methods whose arguments have been ranamed, + - functions/methods with changed argument order, + - constructors in the ``halos`` sub-package where ``cosmo`` is removed, + - constructors in ``halos`` where the default ``MassDef`` is not None, + - functions/methods where ``normprof`` is deprecated. + + Parameters: + pairs : list of pairs, optional + List of renaming pairs ``('old', 'new')``. + reorder : list, optional + List of the **previous** order of the arguments whose order + has been changed, under their **new** name. + + Example: + We have the legacy constructor: + + >>> def __init__(self, cosmo, a, b, c=0, d=1, normprof=False): + # do something + return a, b, c, d, normprof + + and we want to change the API to + + >>> def __init__(self, a, *, see=0, bee, d=1, normprof=None): + # do the same thing + return a, bee, see, d, normprof + + Then, adding this decorator to our new function would preserve API + + >>> @warn_api(pairs=[('b', 'bee'), ('c', 'see')], + reorder=['bee', 'see']) + + - ``cosmo`` is automatically detected for all constructors in ``halos`` + - ``normprof`` is automatically detected for all decorated functions. + """ + if func is None: + # called with parentheses + return functools.partial(warn_api, pairs=pairs, reorder=reorder) + + name = func.__qualname__ + plural = lambda expr: "" if not len(expr)-1 else "s" # noqa: final 's' + params = signature(func).parameters + POK = Parameter.POSITIONAL_OR_KEYWORD + KWO = Parameter.KEYWORD_ONLY + pos_names = [k for k, v in params.items() if v.kind == POK] + kwo_names = [k for k, v in params.items() if v.kind == KWO] + npos = len(pos_names) + rename = dict(pairs) + + @functools.wraps(func) + def wrapper(*args, **kwargs): + # Custom definition of `isinstance` to avoic cyclic imports. + is_instance = lambda obj, cl: cl in obj.__class__.__name__ # noqa + + # API compatibility with `cosmo` as a first argument in `halos`. + catch_cosmo = args[1] if len(args) > 1 else kwargs.get("cosmo") + if ("pyccl.halos" in func.__module__ + and func.__name__ == "__init__" + and is_instance(catch_cosmo, "Cosmology")): + warnings.warn( + f"Use of argument `cosmo` has been deprecated in {name}. " + "This will trigger an exception in the future.", + CCLDeprecationWarning) + # `cosmo` may be in `args` or in `kwargs`, so we check both. + args = tuple( + item for item in args if not is_instance(item, "Cosmology")) + kwargs.pop("cosmo", None) + + # API compatibility for reordered positionals in `fourier_2pt`. + first_arg = args[1] if len(args) > 1 else None + if (func.__name__ == "fourier_2pt" + and is_instance(first_arg, "HaloProfile")): + api = dict(zip(["prof", "cosmo", "k", "M", "a"], args[1: 6])) + args = (args[0],) + args[6:] # discard args [1-5] + kwargs.update(api) # they are now kwargs + warnings.warn( + "API for Profile2pt.fourier_2pt has changed. " + "Argument order (prof, cosmo, k, M, a) has been replaced by " + "(cosmo, k, M, a, prof).", CCLDeprecationWarning) + + # API compatibility for renamed arguments. + warn_names = set(kwargs) - set(params) + unexpected = [k for k in warn_names if k not in rename] + if unexpected: + # emulate Python default behavior for arguments that don't exist + raise TypeError( + f"{func.__name__}() got an unexpected keyword argument " + f"'{unexpected[0]}'") + if warn_names: + s = plural(warn_names) + warnings.warn( + f"Use of argument{s} {list(warn_names)} is deprecated " + f"in {name}. Pass the new name{s} of the argument{s} " + f"{', '.join([rename[k] for k in warn_names])}, respectively.", + CCLDeprecationWarning) + for param in warn_names: + kwargs[rename[param]] = kwargs.pop(param) + + # API compatibility for star operator. + if len(args) > npos: + # API compatibility for shuffled order. + if reorder: + # Pick up the positions of the common elements. + mask = [param in reorder for param in kwo_names] + start = mask.index(True) + stop = start + len(reorder) + # Sort the reordered part of `kwo_names` by `reorder` indexing. + kwo_names[start: stop] = sorted(kwo_names[start: stop], + key=reorder.index) + extras = dict(zip(kwo_names, args[npos:])) + kwargs.update(extras) + s = plural(extras) + warnings.warn( + f"Use of argument{s} {list(extras)} as positional is " + f"deprecated in {func.__qualname__}.", CCLDeprecationWarning) + + # API compatibility for `normprof` as a required argument. + if any(["normprof" in par for par in kwargs.items()]): + warnings.warn( + "Argument `normprof` has been deprecated. Change the default " + "value only by subclassing. More comprehensive profile " + "normalization options will be provided with CCLv3.0.0.", + CCLDeprecationWarning) + + # API compatibility for deprecated HMCalculator argument k_min. + if func.__qualname__ == "HMCalculator.__init__" and "k_min" in kwargs: + warnings.warn( + "Argument `k_min` has been deprecated in `HMCalculator. " + "This is now specified in each profile's `_normalization()` " + "method.", CCLDeprecationWarning) + + # API compatibility for non-None default `MassDef` in `halos`. + if (params.get("mass_def") is not None + and "mass_def" in kwargs + and kwargs["mass_def"] is None): + kwargs["mass_def"] = params["mass_def"].default + warnings.warn( + "`None` has been deprecated as a value for mass_def. " + "To use the default, leave the parameter empty.", + CCLDeprecationWarning) + + # Collect what's remaining and sort to preserve signature order. + pos = dict(zip(pos_names, args)) + kwargs.update(pos) + kwargs = {param: kwargs[param] + for param in sorted(kwargs, key=list(params).index)} + + return func(**kwargs) + return wrapper + + +def deprecate_attr(getter=None, *, pairs=[]): + """This decorator can be used to deprecate attributes, + warning users about it and pointing them to the new attribute. + + Parameters + ---------- + getter : slot wrapper ``__getattribute__`` + This is the getter method to be decorated. + pairs : list of pairs + List of renaming pairs ``('old', 'new')``. + + Example + ------- + We have the legacy attribute ``old_name`` which we want to rename + to ``new_name``. To achieve this we decorate the ``__getattribute__`` + method of the parent class in the main class body to retrieve the + ``__getattr__`` method for the main class, like so: + + >>> __getattr__ = deprecate_attr([('old_name', 'new_name')])( + super.__getattribute__) + + Now, every time the attribute is called via its old name, the user will + be warned about the renaming, and the attribute value will be returned. + + .. note:: Make sure that you bind ``__getattr__`` to the decorator, + rather than ``__getattribute__``, because ``__getattr__`` + provides the fallback mechanism we want to use. Otherwise, + an infinite recursion will initiate. + + """ + if getter is None: + return functools.partial(deprecate_attr, pairs=pairs) + + rename = dict(pairs) + + @functools.wraps(getter) + def wrapper(cls, name): + if name in rename: + new_name = rename[name] + class_name = cls.__class__.__name__ + warnings.warn( + f"Attribute {name} is deprecated in {class_name}. " + f"Pass the new name {new_name}.", CCLDeprecationWarning) + name = new_name + + return cls.__getattribute__(name) + return wrapper diff --git a/pyccl/base/parameters/__init__.py b/pyccl/base/parameters/__init__.py new file mode 100644 index 000000000..1dda7dc20 --- /dev/null +++ b/pyccl/base/parameters/__init__.py @@ -0,0 +1,10 @@ +from .parameters_base import * +from .gsl_params import * +from .spline_params import * +from .physical_constants import * +from .cosmology_params import * +from .fftlog_params import * + +spline_params = SplineParams() +gsl_params = GSLParams() +physical_constants = PhysicalConstants() diff --git a/pyccl/base/parameters/cosmology_params.py b/pyccl/base/parameters/cosmology_params.py new file mode 100644 index 000000000..a07f09d03 --- /dev/null +++ b/pyccl/base/parameters/cosmology_params.py @@ -0,0 +1,28 @@ +from ... import ccllib as lib +from .parameters_base import Parameters + + +__all__ = ("CosmologyParams",) + + +class CosmologyParams(Parameters, factory=lib.parameters): + """Instances of this class hold cosmological parameters.""" + mgrowth: list = [] + + def __getattribute__(self, key): + if key == "m_nu": + N_nu_mass = self.N_nu_mass + nu_masses = lib.parameters_get_nu_masses(self._instance, N_nu_mass) + return nu_masses.tolist() + return super().__getattribute__(key) + + def __setattr__(self, key, value): + if key == "m_nu": + lib.parameters_m_nu_set_custom(self._instance, value) + return object.__setattr__(self, "m_nu", self._instance.m_nu) + if key == "mgrowth": + return lib.parameters_mgrowth_set_custom(self._instance, *value) + super().__setattr__(key, value) + + def __del__(self): + lib.parameters_free(self._instance) diff --git a/pyccl/base/parameters/fftlog_params.py b/pyccl/base/parameters/fftlog_params.py new file mode 100644 index 000000000..6b5a73263 --- /dev/null +++ b/pyccl/base/parameters/fftlog_params.py @@ -0,0 +1,80 @@ +__all__ = ("FFTLogParams",) + + +class FFTLogParams: + """Objects of this class store the FFTLog accuracy parameters.""" + padding_lo_fftlog = 0.1 # | Anti-aliasing: multiply the lower boundary. + padding_hi_fftlog = 10. # | multiply the upper boundary. + + n_per_decade = 100 # Samples per decade for the Hankel transforms. + extrapol = "linx_liny" # Extrapolation type. + + padding_lo_extra = 0.1 # Padding for the intermediate step of a double + padding_hi_extra = 10. # transform. Doesn't have to be as precise. + large_padding_2D = False # If True, high precision intermediate transform. + + plaw_fourier = -1.5 # Real <--> Fourier transforms. + plaw_projected = -1.0 # 2D projected & cumulative density profiles. + + @property + def params(self): + return ["padding_lo_fftlog", "padding_hi_fftlog", "n_per_decade", + "extrapol", "padding_lo_extra", "padding_hi_extra", + "large_padding_2D", "plaw_fourier", "plaw_projected"] + + def to_dict(self): + return {param: getattr(self, param) for param in self.params} + + def __getitem__(self, name): + return getattr(self, name) + + def __setattr__(self, name, value): + raise AttributeError("FFTLogParams can only be updated via " + "`updated_parameters`.") + + def __repr__(self): + return repr(self.to_dict()) + + def __eq__(self, other): + if type(self) != type(other): + return False + return self.to_dict() == other.to_dict() + + def update_parameters(self, **kwargs): + """Update the precision of FFTLog for the Hankel transforms. + + Arguments + --------- + padding_lo_fftlog, padding_hi_fftlog : float + Multiply the lower and upper boundary of the input range + to avoid aliasing. The defaults are 0.1 and 10.0, respectively. + n_per_decade : float + Samples per decade for the Hankel transforms. + The default is 100. + extrapol : {'linx_liny', 'linx_logy'} + Extrapolation type when FFTLog has narrower output support. + The default is 'linx_liny'. + padding_lo_extra, padding_hi_extra : float + Padding for the intermediate step of a double Hankel transform. + Used to compute the 2D projected profile and the 2D cumulative + density, where the first transform goes from 3D real space to + Fourier, then from Fourier to 2D real space. Usually, it doesn't + have to be as precise as ``padding_xx_fftlog``. + The defaults are 0.1 and 10.0, respectively. + large_padding_2D : bool + Override ``padding_xx_extra`` in the intermediate transform, + and use ``padding_xx_fftlog``. The default is False. + plaw_fourier, plaw_projected : float + FFTLog pre-whitens its arguments (makes them flatter) to avoid + aliasing. The ``plaw`` parameters describe the tilt of the profile, + :math:`P(r) \\sim r^{\\mathrm{tilt}}`, between real and Fourier + transforms, and between 2D projected and cumulative density, + respectively. Subclasses of ``HaloProfile`` may obtain finer + control via ``_get_plaw_[fourier | projected]``, and some level of + experimentation with these parameters is recommended. + The defaults are -1.5 and -1.0, respectively. + """ + for name, value in kwargs.items(): + if name not in self.params: + raise AttributeError(f"Parameter {name} does not exist.") + object.__setattr__(self, name, value) diff --git a/pyccl/base/parameters/gsl_params.py b/pyccl/base/parameters/gsl_params.py new file mode 100644 index 000000000..493775f63 --- /dev/null +++ b/pyccl/base/parameters/gsl_params.py @@ -0,0 +1,70 @@ +from ... import ccllib as lib +from .parameters_base import Parameters + + +__all__ = ("GSLParams",) + + +class GSLParams(Parameters, instance=lib.cvar.user_gsl_params): + """Instances of this class hold the GSL parameters.""" + + # Key for the number of Gauss-Kronrod points used in QAG integration. + # https://www.gnu.org/software/gsl/doc/html/integration.html + GSL_INTEG_GAUSS41 = 4 + + # Default relative precision. + GSL_EPSREL = 1e-4 + # Default number of iterations for integration and root-finding. + GSL_N_ITERATION = 1_000 + + N_ITERATION = GSL_N_ITERATION + # Default number of Gauss-Kronrod points in QAG integration. + GSL_INTEGRATION_GAUSS_KRONROD_POINTS = GSL_INTEG_GAUSS41 + # Relative precision in sigma_R calculations. + GSL_EPSREL_SIGMAR = 1e-5 + # Relative precision in k_NL calculations. + GSL_EPSREL_KNL = 1e-5 + # Relative precision in distance calculations. + GSL_EPSREL_DIST = 1e-6 + # Relative precision in growth calculations. + GSL_EPSREL_GROWTH = 1e-6 + # Relative precision in dNdz calculations. + GSL_EPSREL_DNDZ = 1e-6 + + # General parameters. + INTEGRATION_LIMBER_EPSREL = GSL_N_ITERATION + + # Integration. + INTEGRATION_GAUSS_KRONROD_POINTS = GSL_INTEGRATION_GAUSS_KRONROD_POINTS + INTEGRATION_EPSREL = GSL_EPSREL + # Limber integration. + INTEGRATION_LIMBER_GAUSS_KRONROD_POINTS = \ + GSL_INTEGRATION_GAUSS_KRONROD_POINTS + INTEGRATION_LIMBER_EPSREL = GSL_EPSREL + # Distance integrals. + INTEGRATION_DISTANCE_EPSREL = GSL_EPSREL_DIST + # sigma_R integral. + INTEGRATION_SIGMAR_EPSREL = GSL_EPSREL_SIGMAR + # kNL integral. + INTEGRATION_KNL_EPSREL = GSL_EPSREL_KNL + + # Root finding. + ROOT_EPSREL = GSL_EPSREL + ROOT_N_ITERATION = GSL_N_ITERATION + + # ODE. + ODE_GROWTH_EPSREL = GSL_EPSREL_GROWTH + # Growth. + EPS_SCALEFAC_GROWTH = 1e-6 + + # Halo model. + HM_MMIN = 1e7 + HM_MMAX = 1e17 + HM_EPSABS = 0. + HM_EPSREL = 1e-4 + HM_LIMIT = 1_000 + HM_INT_METHOD = GSL_INTEG_GAUSS41 + + # Flags for spline integration. + NZ_NORM_SPLINE_INTEGRATION = True + LENSING_KERNEL_SPLINE_INTEGRATION = True diff --git a/pyccl/base/parameters/parameters_base.py b/pyccl/base/parameters/parameters_base.py new file mode 100644 index 000000000..aae3d517e --- /dev/null +++ b/pyccl/base/parameters/parameters_base.py @@ -0,0 +1,154 @@ +from ...errors import CCLDeprecationWarning +from ..schema import ObjectLock +from functools import cached_property +import warnings + + +__all__ = ("Parameters",) + + +class Parameters: + """Base for classes holding global CCL parameters and their values. + + Subclasses are coupled to the C-struct with the collection of parameters + they represent (and their values), via SWIG. + """ + _allowed_keys = ("_object_lock", "_instance", "_index",) + + def __init_subclass__(cls, *, instance=None, factory=None, freeze=False): + """Routine for subclass initialization. + + Parameters + ---------- + instance : :obj:`pyccl.ccllib` + Reference to the instance where the parameters are implemented. + factory : :class:`pyccl.ccllib` + The SWIG factory class where the parameters are stored. + freeze : bool + Disable parameter mutation. + """ + super().__init_subclass__() + if not (bool(instance) ^ bool(factory)): # XNOR + raise ValueError( + "Provide either the instance, or an instance factory.") + cls._instance = instance + cls._factory = factory + cls._freeze = freeze + + def _new_setattr(self, key, value): + # Make instances of the SWIG-level class immutable + # so that everything is handled through this interface. + # SWIG only assigns `this` via the low level `_ccllib`; + # we therefore disable all other direct assignments. + if key == "this": + return object.__setattr__(self, key, value) + name = self.__class__.__name__ + # TODO: Deprecation cycle for fully immutable Cosmology objects. + # raise AttributeError(f"Direct assignment in {name} not supported.") # noqa + warnings.warn( + f"Direct assignment of {name} is deprecated " + "and an error will be raised in the next CCL release. " + f"Set via `pyccl.{name}.{key}` before instantiation.", + CCLDeprecationWarning) + object.__setattr__(self, key, value) + + # Replace C-level `__setattr__`. + class_ = cls._factory if cls._factory else cls._instance.__class__ + class_.__setattr__ = _new_setattr + + def __new__(cls, *args, **kwargs): + obj = super().__new__(cls) # create a new object + obj._object_lock = ObjectLock() # assign a lock to it + if cls._factory: + obj._instance = cls._factory() # instantiate C struct + if cls._freeze: + obj._object_lock.lock() # lock it if needed + return obj + + def __init__(self): + if self._factory: + return self.set_parameter_names() + self.reload() + + @cached_property + def _parameters(self): + return [par for par in dir(self) if self._is_parameter(par)] + + def _is_parameter(self, par) -> bool: + # Check if `par` is a parameter: exclude private and callables. + return not (par.startswith(("_", "this")) + or callable(getattr(self, par, None))) + + def __setattr__(self, key, value): + if key not in self._allowed_keys: + if self._object_lock.locked: + name = self.__class__.__name__ + raise AttributeError(f"Instances of {name} are frozen.") + if key not in self: + raise AttributeError(f"Parameter {key} does not exist.") + + object.__setattr__(self, key, value) # update Python + if self._is_parameter(key): + object.__setattr__(self._instance, key, value) # update C + + def __getitem__(self, key): + return getattr(self, key) + + __setitem__ = __setattr__ + + def __eq__(self, other): + if type(self) != type(other): + return False + for par in self: + if getattr(self, par) != getattr(other, par): + return False + return True + + def __repr__(self): + name = self.__class__.__name__ + pars = {par: self[par] for par in self} + return f"<{name}>\n\t" + "\n\t".join(repr(pars).split(",")) + + def __contains__(self, key): + return key in self._parameters + + def __len__(self): + return len(self._parameters) + + def __iter__(self): + self._index = 0 + return self + + def __next__(self): + if self._index >= len(self): + raise StopIteration + self._index += 1 + return self._parameters[self._index-1] + + def freeze(self): + """Freeze the parameters to make them immutable.""" + self._object_lock.lock() + + def unfreeze(self): + """Unfreeze the parameters to mutate them.""" + self._object_lock.unlock() + + def copy(self): + """Create a copy of the parameters.""" + out = type(self)() + out.reload(source=self) + return out + + def reload(self, source=None): + """Reload the original values of the parameters.""" + source = self.__class__ if source is None else source + for par in self: + value = getattr(source, par) + object.__setattr__(self, par, value) + object.__setattr__(self._instance, par, value) + + def set_parameter_names(self): + """Set the parameter names from the C library.""" + for par in dir(self._instance): + if self._is_parameter(par): + object.__setattr__(self, par, getattr(self._instance, par)) diff --git a/pyccl/base/parameters/physical_constants.py b/pyccl/base/parameters/physical_constants.py new file mode 100644 index 000000000..04ad47e98 --- /dev/null +++ b/pyccl/base/parameters/physical_constants.py @@ -0,0 +1,57 @@ +from ... import ccllib as lib +from .parameters_base import Parameters + + +class PhysicalConstants(Parameters, instance=lib.cvar.constants, freeze=True): + """Instances of this class hold the physical constants.""" + PI = 3.14_15_92_65_35_89_79_32 + + # ~~ ASTRONOMICAL CONSTANTS ~~ # + # Astronomical Unit, unit conversion (m/au). [exact] + AU = 149_597_870_800 + # Mean solar day (s/day). [exact] + DAY = 86400. + # Sidereal year (days/yr). [IERS2014 in J2000.0] + YEAR = 365.256_363_004 * DAY + + # ~~ FUNDAMENTAL PHYSICAL CONSTANTS ~~ # + # Speed of light (m/s). [exact] + CLIGHT = 299_792_458. + # Unit conversion (J/eV). [exact] + EV_IN_J = 1.602_176_634e-19 + ELECTRON_CHARGE = EV_IN_J + # Electron mass (kg). [CODATA2018] + ELECTRON_MASS = 9.109_383_701_5e-31 + # Planck's constant (J s). [exact] + HPLANCK = 6.626_070_15e-34 + # Boltzmann's constant (J/K). [exact] + KBOLTZ = 1.380_649e-23 + # Universal gravitational constant (m^3/kg/s^2). [CODATA2018] + GNEWT = 6.674_30e-11 + + # ~~ DERIVED CONSTANTS ~~ # + # Reduced Planck's constant (J s). + HBAR = HPLANCK / 2 / PI + # Speed of light (Mpc/h). + CLIGHT_HMPC = CLIGHT / 1e5 + # Unit conversion (m/pc). + PC_TO_METER = 180*60*60/PI * AU + # Unit conversion (m/Mpc). + MPC_TO_METER = 1e6 * PC_TO_METER + # Stefan-Boltzmann's constant (kg m^2 / s). + STBOLTZ = (PI**2/60) * KBOLTZ**4 / HBAR**3 / CLIGHT**2 + # Solar mass in (kg). + SOLAR_MASS = 4 * PI*PI * AU**3 / GNEWT / YEAR**2 + # Critical density (100 M_sun/h / (Mpc/h)^3). + RHO_CRITICAL = 3*1e4/(8*PI*GNEWT) * 1e6 * MPC_TO_METER / SOLAR_MASS + # Linear density contrast of spherical collapse. + DELTA_C = (3/20) * (12*PI)**(2/3) + + # ~~ OTHER CONSTANTS ~~ # + # Neutrino mass splitting differences. + # Lesgourgues & Pastor (2012) + # Adv. High Energy Phys. 2012 (2012) 608515 + # arXiv:1212.6154, p.13 + DELTAM12_sq = 7.62e-5 + DELTAM13_sq_pos = 2.55e-3 + DELTAM13_sq_neg = -2.43e-3 diff --git a/pyccl/base/parameters/spline_params.py b/pyccl/base/parameters/spline_params.py new file mode 100644 index 000000000..4628a1595 --- /dev/null +++ b/pyccl/base/parameters/spline_params.py @@ -0,0 +1,61 @@ +from ... import ccllib as lib +from .parameters_base import Parameters + + +__all__ = ("SplineParams",) + + +class SplineParams(Parameters, instance=lib.cvar.user_spline_params): + """Instances of this class hold the spline parameters.""" + + # Scale factor spline parameters + A_SPLINE_NA = 250 + A_SPLINE_MIN = 0.1 + A_SPLINE_MINLOG_PK = 0.01 + A_SPLINE_MIN_PK = 0.1 + A_SPLINE_MINLOG_SM = 0.01 + A_SPLINE_MIN_SM = 0.1 + A_SPLINE_MAX = 1.0 + A_SPLINE_MINLOG = 0.000_1 + A_SPLINE_NLOG = 250 + + # Mass splines + LOGM_SPLINE_DELTA = 0.025 + LOGM_SPLINE_NM = 50 + LOGM_SPLINE_MIN = 6 + LOGM_SPLINE_MAX = 17 + + # Power spectrum a- and k-splines + A_SPLINE_NA_SM = 13 + A_SPLINE_NLOG_SM = 6 + A_SPLINE_NA_PK = 40 + A_SPLINE_NLOG_PK = 11 + + # k-splines and integrals + K_MAX_SPLINE = 50 + K_MAX = 1e3 + K_MIN = 5e-5 + DLOGK_INTEGRATION = 0.025 + DCHI_INTEGRATION = 5 + N_K = 167 + N_K_3DCOR = 100_000 + + # Correlation function parameters + ELL_MIN_CORR = 0.01 + ELL_MAX_CORR = 60_000 + N_ELL_CORR = 5_000 + + # Interpolation types + A_SPLINE_TYPE = None + K_SPLINE_TYPE = None + M_SPLINE_TYPE = None + PNL_SPLINE_TYPE = None + PLIN_SPLINE_TYPE = None + CORR_SPLINE_TYPE = None + + def __setattr__(self, key, value): + if key == "A_SPLINE_MAX" and value != 1.0: + raise ValueError("A_SPLINE_MAX is fixed to 1.") + if "SPLINE_TYPE" in key and value is not None: + raise ValueError("Spline types are fixed constants.") + super().__setattr__(key, value) diff --git a/pyccl/_repr.py b/pyccl/base/repr_.py similarity index 96% rename from pyccl/_repr.py rename to pyccl/base/repr_.py index 7c268e0db..e53235adf 100644 --- a/pyccl/_repr.py +++ b/pyccl/base/repr_.py @@ -1,6 +1,6 @@ import numpy as np -from .base import _to_hashable, hash_ -from .pyutils import _get_spline1d_arrays, _get_spline2d_arrays +from ..pyutils import _get_spline1d_arrays, _get_spline2d_arrays +from .caching import _to_hashable, hash_ class Table: @@ -99,7 +99,7 @@ def build(self): return s -def _build_string_Cosmology(self): +def build_string_Cosmology(self): """Build the ``Cosmology`` representation. Cosmology equivalence is tested via its representation. Therefore, @@ -165,7 +165,7 @@ def printextras(dic): def metadata(): # Print hashes for the accuracy parameters and the stored Pk2D's. - H = hex(hash_(self._accuracy_params)) + H = hex(hash_(self._spline_params) + hash_(self._gsl_params)) s = f"{newline}HASH_ACCURACY_PARAMS = {H}" if self.__class__.__qualname__ == "CosmologyCalculator": # only need the pk's if we compare CosmologyCalculator objects @@ -186,7 +186,7 @@ def metadata(): return s -def _build_string_Pk2D(self, na=6, nk=6, decimals=2): +def build_string_Pk2D(self, na=6, nk=6, decimals=2): """Build the ``Pk2D`` representation. Example output :: @@ -223,12 +223,12 @@ def _build_string_Pk2D(self, na=6, nk=6, decimals=2): T = Table(n_y=na, n_x=nk, decimals=decimals, legend=legend, newline=newline, data_x=lk, data_y=a, data_z=pk, meta=meta) - s = _build_string_simple(self) + f"{newline}" + s = build_string_simple(self) + f"{newline}" s += T.build() return s -def _build_string_simple(self): +def build_string_simple(self): """Simple representation. Example output :: @@ -238,7 +238,7 @@ def _build_string_simple(self): return f"<{self.__module__}.{self.__class__.__qualname__}>" -def _build_string_from_attrs(self): +def build_string_from_attrs(self): """Build a representation for an object from a list of attribute names given in the hook ``__repr_attrs__`. @@ -254,7 +254,7 @@ def _build_string_from_attrs(self): for param, value in self.__signature__.parameters.items() if param != "self"} - s = _build_string_simple(self) + s = build_string_simple(self) newline = "\n\t" for param, value in params.items(): if param in defaults and value == defaults[param]: @@ -271,7 +271,7 @@ def _build_string_from_attrs(self): return s -def _build_string_Tracer(self): +def build_string_Tracer(self): """Buld a representation for a Tracer. .. note:: Tracer insertion order is important. @@ -319,14 +319,14 @@ def print_row(newline, num, kernel, transfer, prefac, bessel): return "pyccl.Tracer(empty=True)" newline = "\n\t" - s = _build_string_simple(self) + s = build_string_simple(self) s += print_row(newline, "num", "kernel", "transfer", "prefac", "bessel") for num, tracer in enumerate(tracers): s += print_row(newline, num, *get_tracer_info(tracer)) return s -def _build_string_Tk3D(self, na=2, nk=4, decimals=2): +def build_string_Tk3D(self, na=2, nk=4, decimals=2): """Build a representation for a Tk3D object. Example output :: @@ -373,7 +373,7 @@ def _build_string_Tk3D(self, na=2, nk=4, decimals=2): T = Table(n_y=na, n_x=nk, decimals=decimals, newline=newline, data_y=a, legend="a \\ log10(k1)", meta=[]) - s = _build_string_simple(self) + f"{newline}" + s = build_string_simple(self) + f"{newline}" T.data_x, T.data_z = lk1, tks[0] s += T.build() + f"{newline}" T.legend = "a \\ log10(k2)" diff --git a/pyccl/base/schema.py b/pyccl/base/schema.py new file mode 100644 index 000000000..85bc508c0 --- /dev/null +++ b/pyccl/base/schema.py @@ -0,0 +1,414 @@ +from _thread import RLock +from abc import ABC, abstractmethod +from inspect import signature +import functools +from operator import attrgetter + + +__all__ = ("ObjectLock", + "UnlockInstance", "unlock_instance", + "FancyRepr", + "abstractlinkedmethod", "templatemethod", + "CCLObject", "CCLAutoRepr", "CCLNamedClass",) + + +class ObjectLock: + """Control the lock state (immutability) of a ``CCLObject``.""" + _locked: bool = False + _lock_id: int = None + + def __repr__(self): + return f"{self.__class__.__name__}(locked={self.locked})" + + @property + def locked(self): + """Check if the object is locked.""" + return self._locked + + @property + def active(self): + """Check if an unlocking context manager is active.""" + return self._lock_id is not None + + def lock(self): + """Lock the object.""" + self._locked = True + self._lock_id = None + + def unlock(self, manager_id=None): + """Unlock the object.""" + self._locked = False + if manager_id is not None: + self._lock_id = manager_id + + +class UnlockInstance: + """Context manager that temporarily unlocks an immutable instance + of ``CCLObject``. + + Parameters: + instance (``CCLObject``): + Instance of ``CCLObject`` to unlock within the scope + of the context manager. + mutate (``bool``): + If the enclosed function mutates the object, the stored + representation is automatically deleted. + """ + + def __init__(self, instance, *, mutate=True): + self.instance = instance + self.mutate = mutate + # Define these attributes for easy access. + self.id = id(self) + self.thread_lock = RLock() + # We want to catch and exit if the instance is not a CCLObject. + # Hopefully this will be caught downstream. + self.check_instance = hasattr(instance, "_object_lock") + if self.check_instance: + self.object_lock = instance._object_lock + + def __enter__(self): + if not self.check_instance: + return + + with self.thread_lock: + # Prevent simultaneous enclosing of a single instance. + if self.object_lock.active: + # Context manager already active. + return + + # Unlock and store the fingerprint of this context manager so that + # only this context manager is allowed to run on the instance. + self.object_lock.unlock(manager_id=self.id) + + def __exit__(self, type, value, traceback): + if not self.check_instance: + return + + # If another context manager is running, + # do nothing; otherwise reset. + if self.id != self.object_lock._lock_id: + return + + # Lock the instance on exit. + # self.object_lock.lock() # TODO: Uncomment for CCLv3. + + @classmethod + def unlock_instance(cls, func=None, *, name=None, mutate=True): + """Decorator that temporarily unlocks an instance of CCLObject. + + Arguments: + func (``function``): + Function which changes one of its ``CCLObject`` arguments. + name (``str``): + Name of the parameter to unlock. Defaults to the first one. + If not a ``CCLObject`` the decorator will do nothing. + mutate (``bool``): + If after the function ``instance_old != instance_new``, the + instance is mutated. If ``True``, the representation of the + object will be reset. + """ + if func is None: + # called with parentheses + return functools.partial(cls.unlock_instance, name=name, + mutate=mutate) + + if not hasattr(func, "__signature__"): + # store the function signature + func.__signature__ = signature(func) + names = list(func.__signature__.parameters.keys()) + name = names[0] if name is None else name # default name + if name not in names: + # ensure the name makes sense + raise NameError(f"{name} does not exist in {func.__name__}.") + + @functools.wraps(func) + def wrapper(*args, **kwargs): + bound = func.__signature__.bind(*args, **kwargs) + with UnlockInstance(bound.arguments[name], mutate=mutate): + return func(*args, **kwargs) + return wrapper + + @classmethod + def Funlock(cls, cl, name, mutate: bool): + """Allow an instance to change or mutate when `name` is called.""" + func = vars(cl).get(name) + if func is not None: + newfunc = cls.unlock_instance(mutate=mutate)(func) + setattr(cl, name, newfunc) + + +unlock_instance = UnlockInstance.unlock_instance + + +class FancyRepr: + """Controls the usage of fancy ``__repr__` for ``CCLObjects.""" + _enabled: bool = True + _classes: dict = {} + + @classmethod + def add(cls, cl): + """Add class to the internal dictionary of fancy-repr classes.""" + cls._classes[cl] = cl.__repr__ + + @classmethod + def enable(cls): + """Enable fancy representations if they exist.""" + for cl, method in cls._classes.items(): + setattr(cl, "__repr__", method) + cls._enabled = True + + @classmethod + def disable(cls): + """Disable fancy representations and fall back to Python defaults.""" + for cl in cls._classes.keys(): + cl.__repr__ = object.__repr__ + cls._enabled = False + + +def _method_wrapper_factory(hook_name: str, default_value=True) -> callable: + """Decorator factory that sets hooks to functions. + + The hooks can be used as abstraction criteria for implementations departing + from ``abc.ABCMeta``, which enforces that abstract methods are implemented. + + Arguments + --------- + hook_name : str + Name of the hook. + default_value : object + Any default value for the hook. + + Returns + ------- + wrapper : callable + Wrapper that can be used as a decorator that sets hooks. + """ + def wrapper(func): + setattr(func, hook_name, default_value) + return func + return wrapper + + +# ~~ abstractlinkedmethod(func) ~~ +# Requires that the superclass is `CCLObject` or derived from it. +# A subclass of `CCLObject` cannot be instantiated unless at least one of +# its linked abstract methods is overridden. +# +# If a subclass of a linked abstract class has one implementation of the +# linked abstract methods, the linked abstract method register is cleared. +# `CCLObject._is_abstractlinked()` inspects whether the method is implemented. +abstractlinkedmethod = _method_wrapper_factory("__isabstractlinkedmethod__") + +# ~~ templatemethod(func) ~~ +# Marks the method as template. Instance attribute checks may then be made +# with `CCLObject._is_template()` to inspect if the method is implemented. +templatemethod = _method_wrapper_factory("__istemplatemethod__") + + +class CCLObject(ABC): + """Base for CCL objects. + + All CCL objects inherit ``__eq__`` and ``__hash__`` methods from here. + Both methods rely on ``__repr__`` uniqueness. This aims to homogenize + equivalence checking, and to standardize the use of hash. + + Overview + -------- + ``CCLObjects`` inherit ``__hash__``, which consistently hashes the + representation string. They also inherit ``__eq__`` which checks for + representation equivalence. + + In the implemented scheme, each ``CCLObject`` may have its own, specialized + ``__repr__`` method overloaded. Object representations have to be unique + for equivalent objects. If no ``__repr__`` is provided, the default from + ``object`` is used. + + Mutation + -------- + ``CCLObjects`` are by default immutable. This aims to provide a failsafe + mechanism, where, changing attributes has to trigger a re-computation + of something else inside of the instance, rather than simply doing a value + change. + + This immutability mechanism can be safely bypassed if a subclass defines an + ``update_parameters`` method. ``CCLObjects`` temporarily unlock whenever + this method is called. + + Internal State vs. Mutation + --------------------------- + Other methods that use ``setattr`` can only do that if they are decorated + with ``@unlock_instance`` or if the particular code block that makes the + change is enclosed within the ``UnlockInstance`` context manager. + If neither is provided, an exception is raised. + + If such methods only change the instance's internal state, the decorator + may be called with ``@unlock_instance(mutate=False)`` (or equivalently + for the context manager ``UnlockInstance(..., mutate=False)``). Otherwise, + the instance is assumed to have mutated. + """ + __abstractlinkedmethods__ = set() + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + + # 1. Store the initialization signature on import. + cls.__signature__ = signature(cls.__init__) + + # 2. Replace repr (if implemented) with its cached version. + if "__repr__" in vars(cls): + FancyRepr.add(cls) + + # 3. Unlock instance on specific methods. # TODO: Uncomment for CCLv3. + # UnlockInstance.Funlock(cls, "__init__", mutate=False) + # UnlockInstance.Funlock(cls, "update_parameters", mutate=True) + + # 4. Process linked abstract methods. + isabstract = lambda f: hasattr(f, "__isabstractlinkedmethod__") # noqa + abstracts = cls.__abstractlinkedmethods__.copy() + for name, obj in vars(cls).items(): + # add the new linked abstract methods + if isabstract(obj): + abstracts.add(name) + for name in abstracts: + # reset the linked abstract methods if one has been implemented + if not isabstract(getattr(cls, name)): + abstracts = set() + cls.__abstractlinkedmethods__ = abstracts + + def __new__(cls, *args, **kwargs): + # Check if there are any linked abstract methods. + if abstracts := cls.__abstractlinkedmethods__: + name, s = cls.__name__, (len(abstracts) > 1) * "s" + names = ", ".join(abstracts) + raise TypeError(f"Can't instantiate abstract class {name} " + f"with linked abstract methods{s} {names}.") + # Populate every instance with an `ObjectLock` as attribute. + instance = super().__new__(cls) + object.__setattr__(instance, "_object_lock", ObjectLock()) + return instance + + def __setattr__(self, name, value): + if self._object_lock.locked: + raise AttributeError("CCL objects can only be updated via " + "`update_parameters`, if implemented.") + object.__setattr__(self, name, value) + + def update_parameters(self, **kwargs): + name = self.__class__.__name__ + raise NotImplementedError(f"{name} objects are immutable.") + + def __repr__(self): + # By default we use `__repr__` from `object`. + return object.__repr__(self) + + def __hash__(self): + # `__hash__` makes use of the `repr` of the object, + # so we have to make sure that the `repr` is unique. + return hash(repr(self)) + + def __eq__(self, other): + # Two same-type objects are equal if their representations are equal. + if self.__class__ is not other.__class__: + return False + # Compare the attributes listed in `__eq_attrs__`. + if hasattr(self, "__eq_attrs__"): + return all([attrgetter(attr)(self) == attrgetter(attr)(other) + for attr in self.__eq_attrs__]) + # Fall back to repr comparison. + return repr(self) == repr(other) + + def _is_abstractlinked(self, name): + # Check whether the abstract linked method `name` is implemented. + return hasattr(getattr(self, name), "__isabstractlinkedmethod__") + + def _is_template(self, name): + # Check whether the template method `name` is implemented. + return hasattr(getattr(self, name), "__istemplatemethod__") + + def _is_implemented(self, name): + # Check whether the method is implemented (not template or abstract). + return not (self._is_abstractlinked(name) or self._is_template(name)) + + +class CCLAutoRepr(CCLObject): + """Base for objects with automatic representation. Representations + for instances are built from a list of attribute names specified as + a class variable in ``__repr_attrs__`` (acting as a hook). + + Example: + The representation (also hash) of instances of the following class + is built based only on the attributes specified in ``__repr_attrs__``: + + >>> class MyClass(CCLAutoRepr): + __repr_attrs__ = ("a", "b", "other") + def __init__(self, a=1, b=2, c=3, d=4, e=5): + self.a = a + self.b = b + self.c = c + self.other = d + e + + >>> repr(MyClass(6, 7, 8, 9, 10)) + <__main__.MyClass> + a = 6 + b = 7 + other = 19 + """ + + def __repr__(self): + # Build string from specified `__repr_attrs__` or use Python's default. + # Subclasses overriding `__repr__`, stop using `__repr_attrs__`. + if hasattr(self.__class__, "__repr_attrs__"): + from .repr_ import build_string_from_attrs + return build_string_from_attrs(self) + return object.__repr__(self) + + +def _subclasses(cls): + # This helper returns a set of all subclasses. + direct_subs = cls.__subclasses__() + deep_subs = [sub for cl in direct_subs for sub in cl._subclasses()] + return set(direct_subs).union(deep_subs) + + +def from_name(cls, name): + """Obtain particular model.""" + mod = {p.name: p for p in cls._subclasses() if hasattr(p, "name")} + return mod[name] + + +def create_instance(cls, input_, **kwargs): + """Process the input and generate an object of the class. + Input can be an instance of the class, or a name string. + Optional ``**kwargs`` may be passed. + """ + if isinstance(input_, cls): + return input_ + if isinstance(input_, str): + class_ = cls.from_name(input_) + return class_(**kwargs) + good, bad = cls.__name__, input_.__class__.__name__ + raise TypeError(f"Expected {good} or str but received {bad}.") + + +class CCLNamedClass(CCLObject): + """Base for objects that contain methods ``from_name()`` and + ``create_instance()``. + + Implementation + -------------- + Subclasses must define a ``name`` class attribute which allows the tree to + be searched to retrieve the particular model, using its name. + """ + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + cls._subclasses = classmethod(_subclasses) + if not hasattr(cls, "from_name"): + cls.from_name = classmethod(from_name) + cls.create_instance = classmethod(create_instance) + + @property + @abstractmethod + def name(self) -> str: + """Class attribute denoting the name of the model.""" diff --git a/pyccl/bcm.py b/pyccl/bcm.py index c09c0cb7a..dda4b3f23 100644 --- a/pyccl/bcm.py +++ b/pyccl/bcm.py @@ -1,10 +1,12 @@ -from . import ccllib as lib -from .pyutils import check -from .pk2d import Pk2D from .base import unlock_instance +from .baryons import BaryonsSchneider15 +from .base import deprecated +from .pyutils import check +from . import ccllib as lib import numpy as np +@deprecated(BaryonsSchneider15) def bcm_model_fka(cosmo, k, a): """The BCM model correction factor for baryons. @@ -25,17 +27,13 @@ def bcm_model_fka(cosmo, k, a): Returns: float or array_like: Correction factor to apply to the power spectrum. """ - k_use = np.atleast_1d(k) - status = 0 - fka, status = lib.bcm_model_fka_vec(cosmo.cosmo, a, k_use, - len(k_use), status) - check(status, cosmo) - - if np.ndim(k) == 0: - fka = fka[0] - return fka + bcm = BaryonsSchneider15(log10Mc=cosmo['bcm_log10Mc'], + eta_b=cosmo['bcm_etab'], + k_s=cosmo['bcm_ks']) + return bcm.boost_factor(cosmo, k, a) +@deprecated(BaryonsSchneider15) @unlock_instance(mutate=True, name="pk2d") def bcm_correct_pk2d(cosmo, pk2d): """Apply the BCM model correction factor to a given power spectrum. @@ -44,8 +42,21 @@ def bcm_correct_pk2d(cosmo, pk2d): cosmo (:class:`~pyccl.core.Cosmology`): Cosmological parameters. pk2d (:class:`~pyccl.pk2d.Pk2D`): power spectrum. """ - if not isinstance(pk2d, Pk2D): - raise TypeError("pk2d must be a Pk2D object") + + bcm = BaryonsSchneider15(log10Mc=cosmo['bcm_log10Mc'], + eta_b=cosmo['bcm_etab'], + k_s=cosmo['bcm_ks']) + a_arr, lk_arr, pk_arr = pk2d.get_spline_arrays() + k_arr = np.exp(lk_arr) + fka = bcm.boost_factor(cosmo, k_arr, a_arr) + pk_arr *= fka + if pk2d.psp.is_log: + np.log(pk_arr, out=pk_arr) + lib.f2d_t_free(pk2d.psp) status = 0 - status = lib.bcm_correct(cosmo.cosmo, pk2d.psp, status) - check(status, cosmo) + pk2d.psp, status = lib.set_pk2d_new_from_arrays( + lk_arr, a_arr, pk_arr.flatten(), + int(pk2d.extrap_order_lok), + int(pk2d.extrap_order_hik), + pk2d.psp.is_log, status) + check(status) diff --git a/pyccl/boltzmann.py b/pyccl/boltzmann.py index 7c32339b9..3a7f47570 100644 --- a/pyccl/boltzmann.py +++ b/pyccl/boltzmann.py @@ -1,18 +1,17 @@ import numpy as np +from .base import warn_api +from .pk2d import Pk2D +from .errors import CCLError + try: import isitgr # noqa: F401 except ModuleNotFoundError: pass # prevent nans from isitgr -from . import ccllib as lib -from .pyutils import check -from .pk2d import Pk2D -from .errors import CCLError -from .parameters import physical_constants - -def get_camb_pk_lin(cosmo, nonlin=False): +@warn_api +def get_camb_pk_lin(cosmo, *, nonlin=False): """Run CAMB and return the linear power spectrum. Args: @@ -38,11 +37,7 @@ def get_camb_pk_lin(cosmo, nonlin=False): pass # z sampling from CCL parameters - na = lib.get_pk_spline_na(cosmo.cosmo) - status = 0 - a_arr, status = lib.get_pk_spline_a(cosmo.cosmo, na, status) - check(status, cosmo=cosmo) - a_arr = np.sort(a_arr) + a_arr = cosmo.get_pk_spline_a() zs = 1.0 / a_arr - 1 zs = np.clip(zs, 0, np.inf) @@ -95,14 +90,14 @@ def get_camb_pk_lin(cosmo, nonlin=False): # where T_nu is the standard neutrino temperature from first order # computations # CLASS defines the temperature of each neutrino species to be - # T_i_eff = TNCDM * T_cmb where TNCDM is a fudge factor to get the + # T_i_eff = T_ncdm * T_cmb where T_ncdm is a fudge factor to get the # total mass in terms of eV to match second-order computations of the # relationship between m_nu and Omega_nu. # We are trying to get both codes to use the same neutrino temperature. # thus we set T_i_eff = T_i = g^(1/4) * T_nu and solve for the right - # value of g for CAMB. We get g = (TNCDM / (11/4)^(-1/3))^4 + # value of g for CAMB. We get g = (T_ncdm / (11/4)^(-1/3))^4 g = np.power( - physical_constants.TNCDM / np.power(11.0/4.0, -1.0/3.0), + cosmo["T_ncdm"] / np.power(11.0/4.0, -1.0/3.0), 4.0) if cosmo['N_nu_mass'] > 0: @@ -251,11 +246,7 @@ def get_isitgr_pk_lin(cosmo): pass # z sampling from CCL parameters - na = lib.get_pk_spline_na(cosmo.cosmo) - status = 0 - a_arr, status = lib.get_pk_spline_a(cosmo.cosmo, na, status) - check(status, cosmo=cosmo) - a_arr = np.sort(a_arr) + a_arr = cosmo.get_pk_spline_a() zs = 1.0 / a_arr - 1 zs = np.clip(zs, 0, np.inf) @@ -289,8 +280,7 @@ def get_isitgr_pk_lin(cosmo): cp.ombh2 = cosmo['Omega_b'] * h2 cp.omch2 = cosmo['Omega_c'] * h2 cp.omk = cosmo['Omega_k'] -# cp.GR = 1 means GR modified! - cp.GR = 1 + cp.GR = 1 # means GR modified! cp.ISiTGR_muSigma = True cp.mu0 = cosmo['mu_0'] cp.Sigma0 = cosmo['sigma_0'] @@ -317,14 +307,14 @@ def get_isitgr_pk_lin(cosmo): # where T_nu is the standard neutrino temperature from first order # computations # CLASS defines the temperature of each neutrino species to be - # T_i_eff = TNCDM * T_cmb where TNCDM is a fudge factor to get the + # T_i_eff = T_ncdm * T_cmb where T_ncdm is a fudge factor to get the # total mass in terms of eV to match second-order computations of the # relationship between m_nu and Omega_nu. # We are trying to get both codes to use the same neutrino temperature. # thus we set T_i_eff = T_i = g^(1/4) * T_nu and solve for the right - # value of g for CAMB. We get g = (TNCDM / (11/4)^(-1/3))^4 + # value of g for CAMB. We get g = (T_ncdm / (11/4)^(-1/3))^4 g = np.power( - physical_constants.TNCDM / np.power(11.0/4.0, -1.0/3.0), + cosmo["T_ncdm"] / np.power(11.0/4.0, -1.0/3.0), 4.0) if cosmo['N_nu_mass'] > 0: @@ -450,9 +440,7 @@ def get_class_pk_lin(cosmo): # massive neutrinos if cosmo["N_nu_mass"] > 0: params["N_ncdm"] = cosmo["N_nu_mass"] - masses = lib.parameters_get_nu_masses(cosmo._params, 3) - params["m_ncdm"] = ", ".join( - ["%g" % m for m in masses[:cosmo["N_nu_mass"]]]) + params["m_ncdm"] = ", ".join(["%g" % m for m in cosmo["m_nu"]]) params["T_cmb"] = cosmo["T_CMB"] @@ -477,21 +465,19 @@ def get_class_pk_lin(cosmo): model.compute() # Set k and a sampling from CCL parameters - nk = lib.get_pk_spline_nk(cosmo.cosmo) - na = lib.get_pk_spline_na(cosmo.cosmo) - status = 0 - a_arr, status = lib.get_pk_spline_a(cosmo.cosmo, na, status) - check(status, cosmo=cosmo) + nk = len(cosmo.get_pk_spline_lk()) + a_arr = cosmo.get_pk_spline_a() + na = len(a_arr) # FIXME - getting the lowest CLASS k value from the python interface # appears to be broken - setting to 1e-5 which is close to the # old value lk_arr = np.log(np.logspace( -5, - np.log10(cosmo.cosmo.spline_params.K_MAX_SPLINE), nk)) + np.log10(cosmo._spline_params.K_MAX_SPLINE), nk)) # we need to cut this to the max value used for calling CLASS - msk = lk_arr < np.log(cosmo.cosmo.spline_params.K_MAX_SPLINE) + msk = lk_arr < np.log(cosmo._spline_params.K_MAX_SPLINE) nk = int(np.sum(msk)) lk_arr = lk_arr[msk] diff --git a/pyccl/ccl.i b/pyccl/ccl.i index 0fbb47fb4..0e34f1455 100644 --- a/pyccl/ccl.i +++ b/pyccl/ccl.i @@ -44,7 +44,6 @@ from .errors import CCLError %include "ccl_tk3d.i" %include "ccl_background.i" %include "ccl_power.i" -%include "ccl_bcm.i" %include "ccl_correlation.i" %include "ccl_tracers.i" %include "ccl_cls.i" diff --git a/pyccl/ccl_bcm.i b/pyccl/ccl_bcm.i deleted file mode 100644 index 75720f7fe..000000000 --- a/pyccl/ccl_bcm.i +++ /dev/null @@ -1,31 +0,0 @@ -%module ccl_bcm - -%{ -/* put additional #include here */ -%} - -// Enable vectorised arguments for arrays -%apply (double* IN_ARRAY1, int DIM1) {(double* k, int nk)}; -%apply (int DIM1, double* ARGOUT_ARRAY1) {(int nout, double* output)}; - -%include "../include/ccl_bcm.h" - -/* The python code here will be executed before all of the functions that - follow this directive. */ -%feature("pythonprepend") %{ - if numpy.shape(k) != (nout,): - raise CCLError("Input shape for `k` must match `(nout,)`!") -%} - -%inline %{ -void bcm_model_fka_vec(ccl_cosmology * cosmo, double a, double* k, int nk, - int nout, double* output, int* status) { - for(int i=0; i < nk; i++){ - output[i] = ccl_bcm_model_fka(cosmo, k[i], a, status); - } -} - -%} - -/* The directive gets carried between files, so we reset it at the end. */ -%feature("pythonprepend") %{ %} diff --git a/pyccl/ccl_core.i b/pyccl/ccl_core.i index a1d98325d..ff3220d95 100644 --- a/pyccl/ccl_core.i +++ b/pyccl/ccl_core.i @@ -4,79 +4,42 @@ /* put additional #include here */ %} -// SWIG black magic. Change the behaviour of setting A_SPLINE_MAX to throwing an -// error. -%typemap(memberin) double A_SPLINE_MAX { - if($input) { - PyErr_SetString(PyExc_RuntimeError, "A_SPLINE_MAX is fixed to 1.0 and is not mutable."); - SWIG_fail; - } -} - %include "../include/ccl_core.h" -// Enable vectorised arguments for arrays + %apply (double* IN_ARRAY1, int DIM1) { - (double* zarr, int nz), - (double* dfarr, int nf), - (double* m_nu, int n_m) + (double* mass, int num), + (double* zarr, int nz), + (double* dfarr, int ndf) }; -%apply (int DIM1, double* ARGOUT_ARRAY1) {(int nout, double* output)}; %inline %{ - -void parameters_get_nu_masses(ccl_parameters *params, int nout, double* output) { - output[0] = 0; - output[1] = 0; - output[2] = 0; - - for (int i=0; iN_nu_mass; ++i) { - output[i] = params->m_nu[i]; - } +void parameters_m_nu_set_custom(ccl_parameters *params, double *mass, int num) { + params->m_nu = (double*) malloc(num*sizeof(double)); + memcpy(params->m_nu, mass, num*sizeof(double)); } -ccl_parameters parameters_create_nu( - double Omega_c, double Omega_b, double Omega_k, - double Neff, double w0, double wa, double h, - double norm_pk, double n_s, double bcm_log10Mc, - double bcm_etab, double bcm_ks, double mu_0, - double sigma_0, double c1_mg, double c2_mg, double lambda_mg, - double* m_nu, int n_m, int* status) -{ - return ccl_parameters_create( - Omega_c, Omega_b, Omega_k, Neff, m_nu, n_m, - w0, wa, h, norm_pk, n_s, bcm_log10Mc, bcm_etab, - bcm_ks, mu_0, sigma_0, c1_mg, c2_mg, lambda_mg, - -1, NULL, NULL, status ); +void parameters_mgrowth_set_custom(ccl_parameters *params, + double* zarr, int nz, double* dfarr, int ndf) { + if (nz > 0) { + params->has_mgrowth = true; + params->nz_mgrowth = nz; + params->z_mgrowth = (double*) malloc(nz*sizeof(double)); + params->df_mgrowth = (double*) malloc(nz*sizeof(double)); + memcpy(params->z_mgrowth, zarr, nz*sizeof(double)); + memcpy(params->df_mgrowth, dfarr, nz*sizeof(double)); + } } %} -%feature("pythonprepend") parameters_create_nu_vec %{ - if numpy.shape(zarr) != numpy.shape(dfarr): - raise CCLError("Input shape for `zarr` must match `dfarr`!") -%} + +%apply (int DIM1, double* ARGOUT_ARRAY1) {(int nout, double* output)}; %inline %{ -ccl_parameters parameters_create_nu_vec( - double Omega_c, double Omega_b, double Omega_k, - double Neff, double w0, double wa, double h, - double norm_pk, double n_s, double bcm_log10Mc, - double bcm_etab, double bcm_ks, double mu_0, - double sigma_0, double c1_mg, double c2_mg, double lambda_mg, - double* zarr, int nz, - double* dfarr, int nf, double* m_nu, - int n_m, int* status) -{ - if (nz == 0){ nz = -1; } - return ccl_parameters_create( - Omega_c, Omega_b, Omega_k, Neff, m_nu, n_m, - w0, wa, h, norm_pk, n_s, bcm_log10Mc, bcm_etab, bcm_ks, - mu_0, sigma_0, c1_mg, c2_mg, lambda_mg, - nz, zarr, dfarr, status); + +void parameters_get_nu_masses(ccl_parameters *params, int nout, double* output) { + memcpy(output, params->m_nu, nout*sizeof(double)); } %} - -/* The directive gets carried between files, so we reset it at the end. */ -%feature("pythonprepend") %{ %} diff --git a/pyccl/ccl_neutrinos.i b/pyccl/ccl_neutrinos.i index de5a8bcca..4201eb644 100644 --- a/pyccl/ccl_neutrinos.i +++ b/pyccl/ccl_neutrinos.i @@ -20,17 +20,17 @@ %inline %{ -void Omeganuh2_vec(int N_nu_mass, double T_CMB, double* a, int na, +void Omeganuh2_vec(int N_nu_mass, double T_CMB, double T_ncdm, double* a, int na, double* mnu, int nm, int nout, double* output, int* status) { for(int i=0; i < na; i++){ - output[i] = ccl_Omeganuh2(a[i], N_nu_mass, mnu, T_CMB, status); + output[i] = ccl_Omeganuh2(a[i], N_nu_mass, mnu, T_CMB, T_ncdm, status); } } -void nu_masses_vec(double OmNuh2, int label, double T_CMB, +void nu_masses_vec(double OmNuh2, int label, int nout, double* output, int* status) { double* mnu; - mnu = ccl_nu_masses(OmNuh2, label, T_CMB, status); + mnu = ccl_nu_masses(OmNuh2, label, status); for(int i=0; i < nout; i++){ output[i] = *(mnu+i); } diff --git a/pyccl/ccl_pk2d.i b/pyccl/ccl_pk2d.i index 4ada1750b..7feea3113 100644 --- a/pyccl/ccl_pk2d.i +++ b/pyccl/ccl_pk2d.i @@ -27,26 +27,6 @@ ccl_f2d_t *set_pk2d_new_from_arrays(double* lkarr,int nk, return psp; } -void get_pk_spline_a(ccl_cosmology *cosmo,int ndout,double* doutput,int *status) -{ - ccl_get_pk_spline_a_array(cosmo,ndout,doutput,status); -} - -void get_pk_spline_a_from_params(ccl_spline_params *spline_params, int ndout, double *doutput, int *status) -{ - ccl_get_pk_spline_a_array_from_params(spline_params, ndout, doutput, status); -} - -void get_pk_spline_lk(ccl_cosmology *cosmo,int ndout,double* doutput,int *status) -{ - ccl_get_pk_spline_lk_array(cosmo,ndout,doutput,status); -} - -void get_pk_spline_lk_from_params(ccl_spline_params *spline_params, int ndout, double *doutput, int *status) -{ - ccl_get_pk_spline_lk_array_from_params(spline_params, ndout, doutput, status); -} - double pk2d_eval_single(ccl_f2d_t *psp,double lk,double a,ccl_cosmology *cosmo,int *status) { return ccl_f2d_t_eval(psp,lk,a,cosmo,status); diff --git a/pyccl/cls.py b/pyccl/cells.py similarity index 89% rename from pyccl/cls.py rename to pyccl/cells.py index 4370a8356..7ba159120 100644 --- a/pyccl/cls.py +++ b/pyccl/cells.py @@ -5,22 +5,20 @@ from .errors import CCLWarning from . import ccllib as lib from .pyutils import check, integ_types +from .base import warn_api from .pk2d import parse_pk2d -# Define symbolic 'None' type for arrays, to allow proper handling by swig -# wrapper -NoneArr = np.array([]) - -def angular_cl(cosmo, cltracer1, cltracer2, ell, p_of_k_a=None, +@warn_api(pairs=[("cltracer1", "tracer1"), ("cltracer2", "tracer2")]) +def angular_cl(cosmo, tracer1, tracer2, ell, *, p_of_k_a=None, l_limber=-1., limber_integration_method='qag_quad'): """Calculate the angular (cross-)power spectrum for a pair of tracers. Args: cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - cltracer1 (:class:`~pyccl.tracers.Tracer`): a `Tracer` object, + tracer1 (:class:`~pyccl.tracers.Tracer`): a `Tracer` object, of any kind. - cltracer2 (:class:`~pyccl.tracers.Tracer`): a second `Tracer` object, + tracer2 (:class:`~pyccl.tracers.Tracer`): a second `Tracer` object, of any kind. ell (float or array_like): Angular wavenumber(s) at which to evaluate the angular power spectrum. @@ -46,7 +44,6 @@ def angular_cl(cosmo, cltracer1, cltracer2, ell, p_of_k_a=None, "CCL does not properly use the hyperspherical Bessel functions " "when computing angular power spectra in non-flat cosmologies!", category=CCLWarning) - if limber_integration_method not in ['qag_quad', 'spline']: raise ValueError("Integration method %s not supported" % limber_integration_method) @@ -64,9 +61,9 @@ def angular_cl(cosmo, cltracer1, cltracer2, ell, p_of_k_a=None, status = 0 clt1, status = lib.cl_tracer_collection_t_new(status) clt2, status = lib.cl_tracer_collection_t_new(status) - for t in cltracer1._trc: + for t in tracer1._trc: status = lib.add_cl_tracer_to_collection(clt1, t, status) - for t in cltracer2._trc: + for t in tracer2._trc: status = lib.add_cl_tracer_to_collection(clt2, t, status) ell_use = np.atleast_1d(ell) diff --git a/pyccl/core.py b/pyccl/core.py index ee6a53fe4..7abaa3cf3 100644 --- a/pyccl/core.py +++ b/pyccl/core.py @@ -6,6 +6,8 @@ import numpy as np import yaml from inspect import getmembers, isfunction, signature +from dataclasses import dataclass +from scipy.interpolate import Akima1DInterpolator from . import ccllib as lib from .errors import CCLError, CCLWarning @@ -15,7 +17,9 @@ from .pk2d import Pk2D from .bcm import bcm_correct_pk2d from .base import CCLObject, cache, unlock_instance -from .parameters import CCLParameters, physical_constants +from .base.parameters import gsl_params, spline_params, CosmologyParams +from .base.parameters import physical_constants as const + # Configuration types transfer_function_types = { @@ -67,6 +71,48 @@ } +class _Defaults: + """Default cosmological parameters used throughout the library.""" + T_CMB = 2.725 + T_ncdm = 0.71611 + + +def _methods_of_cosmology(cls=None, *, modules=[]): + """Assign all functions in ``modules`` which take ``cosmo`` as their + first argument as methods of the class ``cls``. + """ + import functools + from importlib import import_module + + if cls is None: + # called with parentheses + return functools.partial(_methods_of_cosmology, modules=modules) + + pkg = __name__.rsplit(".")[0] + modules = [import_module(f".{module}", pkg) for module in modules] + funcs = [getmembers(module, isfunction) for module in modules] + funcs = [func for sublist in funcs for func in sublist] + + for name, func in funcs: + pars = signature(func).parameters + if pars and list(pars)[0] == "cosmo": + setattr(cls, name, func) + + return cls + + +_modules = ["background", "bcm", "boltzmann", "cells", "correlations", + "covariances", "neutrinos", "pk2d", "power", "pyutils", + "tk3d", "tracers", "halos", "nl_pt"] + + +@dataclass +class CosmologyData: + lookback: Akima1DInterpolator = None + age0: float = None + + +@_methods_of_cosmology(modules=_modules) class Cosmology(CCLObject): """A cosmology including parameters and associated data. @@ -125,8 +171,7 @@ class Cosmology(CCLObject): wa (:obj:`float`, optional): Second order term of dark energy equation of state. Defaults to 0. T_CMB (:obj:`float`): The CMB temperature today. The default of - ``None`` uses the global CCL value in - ``pyccl.physical_constants.T_CMB``. + is 2.725. bcm_log10Mc (:obj:`float`, optional): One of the parameters of the BCM model. Defaults to `np.log10(1.2e14)`. bcm_etab (:obj:`float`, optional): One of the parameters of the BCM @@ -179,6 +224,8 @@ class Cosmology(CCLObject): extra_parameters (:obj:`dict`, optional): Dictionary holding extra parameters. Currently supports extra parameters for CAMB, with details described below. Defaults to None. + T_ncdm (:obj:`float`): Non-CDM temperature in units of photon + temperature. The default is 0.71611. Currently supported extra parameters for CAMB are: @@ -197,32 +244,16 @@ class Cosmology(CCLObject): "HMCode_logT_AGN": 7.8}} """ - from ._repr import _build_string_Cosmology as __repr__ - - # Go through all functions in the main package and the subpackages - # and make every function that takes `cosmo` as its first argument - # an attribute of this class. - from . import (background, bcm, boltzmann, cls, - correlations, covariances, neutrinos, - pk2d, power, pyutils, tk3d, tracers, halos, nl_pt) - subs = [background, boltzmann, bcm, cls, correlations, covariances, - neutrinos, pk2d, power, pyutils, tk3d, tracers, halos, nl_pt] - funcs = [getmembers(sub, isfunction) for sub in subs] - funcs = [func for sub in funcs for func in sub] - for name, func in funcs: - pars = list(signature(func).parameters) - if pars and pars[0] == "cosmo": - vars()[name] = func - # clear unnecessary locals - del (background, boltzmann, bcm, cls, correlations, covariances, - neutrinos, pk2d, power, pyutils, tk3d, tracers, halos, nl_pt, - subs, funcs, func, name, pars) + # TODO: Docstring - Move T_ncdm after T_CMB for CCLv3. + from .base.repr_ import build_string_Cosmology as __repr__ + __eq_attrs__ = ("_params_init_kwargs", "_config_init_kwargs", + "_spline_params", "_gsl_params",) def __init__( self, Omega_c=None, Omega_b=None, h=None, n_s=None, - sigma8=None, A_s=None, - Omega_k=0., Omega_g=None, Neff=3.046, m_nu=0., m_nu_type=None, - w0=-1., wa=0., T_CMB=None, + sigma8=None, A_s=None, Omega_k=0., Omega_g=None, + Neff=3.046, m_nu=0., m_nu_type=None, w0=-1., wa=0., + T_CMB=_Defaults.T_CMB, bcm_log10Mc=np.log10(1.2e14), bcm_etab=0.5, bcm_ks=55., mu_0=0., sigma_0=0., c1_mg=1., c2_mg=1., lambda_mg=0., z_mg=None, df_mg=None, @@ -232,13 +263,14 @@ def __init__( mass_function='tinker10', halo_concentration='duffy2008', emulator_neutrinos='strict', - extra_parameters=None): + extra_parameters=None, + T_ncdm=_Defaults.T_ncdm): # going to save these for later self._params_init_kwargs = dict( Omega_c=Omega_c, Omega_b=Omega_b, h=h, n_s=n_s, sigma8=sigma8, A_s=A_s, Omega_k=Omega_k, Omega_g=Omega_g, Neff=Neff, m_nu=m_nu, - m_nu_type=m_nu_type, w0=w0, wa=wa, T_CMB=T_CMB, + m_nu_type=m_nu_type, w0=w0, wa=wa, T_CMB=T_CMB, T_ncdm=T_ncdm, bcm_log10Mc=bcm_log10Mc, bcm_etab=bcm_etab, bcm_ks=bcm_ks, mu_0=mu_0, sigma_0=sigma_0, c1_mg=c1_mg, c2_mg=c2_mg, lambda_mg=lambda_mg, @@ -264,10 +296,10 @@ def _build_cosmo(self): # and then we make the cosmology. self._build_parameters(**self._params_init_kwargs) self._build_config(**self._config_init_kwargs) - self.cosmo = lib.cosmology_create(self._params, self._config) - self._spline_params = CCLParameters.get_params_dict("spline_params") - self._gsl_params = CCLParameters.get_params_dict("gsl_params") - self._accuracy_params = {**self._spline_params, **self._gsl_params} + self.cosmo = lib.cosmology_create(self._params._instance, self._config) + self.data = CosmologyData() + self._spline_params = spline_params.copy() + self._gsl_params = gsl_params.copy() if self.cosmo.status != 0: raise CCLError( @@ -419,51 +451,39 @@ def _build_config( def _build_parameters( self, Omega_c=None, Omega_b=None, h=None, n_s=None, sigma8=None, A_s=None, Omega_k=None, Neff=None, m_nu=None, m_nu_type=None, - w0=None, wa=None, T_CMB=None, + w0=None, wa=None, T_CMB=None, T_ncdm=None, bcm_log10Mc=None, bcm_etab=None, bcm_ks=None, mu_0=None, sigma_0=None, c1_mg=None, c2_mg=None, lambda_mg=None, z_mg=None, df_mg=None, Omega_g=None, extra_parameters=None): """Build a ccl_parameters struct""" + # Fill-in defaults (SWIG converts `numpy.nan` to `NAN`) + A_s = np.nan if A_s is None else A_s + sigma8 = np.nan if sigma8 is None else sigma8 + Omega_g = np.nan if Omega_g is None else Omega_g # Check to make sure Omega_k is within reasonable bounds. if Omega_k is not None and Omega_k < -1.0135: raise ValueError("Omega_k must be more than -1.0135.") - # Set nz_mg (no. of redshift bins for modified growth fns.) - if z_mg is not None and df_mg is not None: - # Get growth array size and do sanity check - z_mg = np.atleast_1d(z_mg) - df_mg = np.atleast_1d(df_mg) - if z_mg.size != df_mg.size: - raise ValueError( - "The parameters `z_mg` and `dF_mg` are " - "not the same shape!") - nz_mg = z_mg.size - else: - # If one or both of the MG growth arrays are set to zero, disable - # all of them - if z_mg is not None or df_mg is not None: - raise ValueError("Must specify both z_mg and df_mg.") - z_mg = None - df_mg = None - nz_mg = -1 + # Modified growth. + if (z_mg is None) != (df_mg is None): + raise ValueError("Both z_mg and df_mg must be arrays or None.") + if z_mg is not None: + z_mg, df_mg = map(np.atleast_1d, [z_mg, df_mg]) + if z_mg.shape != df_mg.shape: + raise ValueError("Shape mismatch for z_mg and df_mg.") # Check to make sure specified amplitude parameter is consistent - if ((A_s is None and sigma8 is None) or - (A_s is not None and sigma8 is not None)): - raise ValueError("Must set either A_s or sigma8 and not both.") - - # Set norm_pk to either A_s or sigma8 - norm_pk = A_s if A_s is not None else sigma8 - - # The C library decides whether A_s or sigma8 was the input parameter - # based on value, so we need to make sure this is consistent too - if norm_pk >= 1e-5 and A_s is not None: - raise ValueError("A_s must be less than 1e-5.") + if [A_s, sigma8].count(np.nan) != 1: + raise ValueError("Set either A_s or sigma8 and not both.") - if norm_pk < 1e-5 and sigma8 is not None: - raise ValueError("sigma8 must be greater than 1e-5.") + # Check if any compulsory parameters are not set + compul = [Omega_c, Omega_b, Omega_k, w0, wa, h, n_s] + names = ['Omega_c', 'Omega_b', 'Omega_k', 'w0', 'wa', 'h', 'n_s'] + for name, item in zip(names, compul): + if item is None: + raise ValueError(f"Must set parameter {name}.") # Make sure the neutrino parameters are consistent # and if a sum is given for mass, split into three masses. @@ -562,7 +582,7 @@ def _build_parameters( for i in range(0, 3): if (mnu_list[i] > 0.00017): # Lesgourges et al. 2012 N_nu_mass = N_nu_mass + 1 - N_nu_rel = Neff - (N_nu_mass * 0.71611**4 * (4./11.)**(-4./3.)) + N_nu_rel = Neff - (N_nu_mass * T_ncdm**4 * (4./11.)**(-4./3.)) if N_nu_rel < 0.: raise ValueError("Neff and m_nu must result in a number " "of relativistic neutrino species greater " @@ -570,95 +590,80 @@ def _build_parameters( # Fill an array with the non-relativistic neutrino masses if N_nu_mass > 0: - mnu_final_list = [0]*N_nu_mass + nu_mass = [0]*N_nu_mass relativistic = [0]*3 for i in range(0, N_nu_mass): for j in range(0, 3): if (mnu_list[j] > 0.00017 and relativistic[j] == 0): relativistic[j] = 1 - mnu_final_list[i] = mnu_list[j] + nu_mass[i] = mnu_list[j] break else: - mnu_final_list = [0.] - - # Check if any compulsory parameters are not set - compul = [Omega_c, Omega_b, Omega_k, w0, wa, h, norm_pk, - n_s] - names = ['Omega_c', 'Omega_b', 'Omega_k', 'w0', 'wa', - 'h', 'norm_pk', 'n_s'] - for nm, item in zip(names, compul): - if item is None: - raise ValueError("Necessary parameter '%s' was not set " - "(or set to None)." % nm) - - # Create new instance of ccl_parameters object - # Create an internal status variable; needed to check massive neutrino - # integral. - T_CMB_old = physical_constants.T_CMB - try: - if T_CMB is not None: - physical_constants.T_CMB = T_CMB - status = 0 - if nz_mg == -1: - # Create ccl_parameters without modified growth - self._params, status = lib.parameters_create_nu( - Omega_c, Omega_b, Omega_k, Neff, - w0, wa, h, norm_pk, n_s, bcm_log10Mc, - bcm_etab, bcm_ks, mu_0, sigma_0, c1_mg, - c2_mg, lambda_mg, mnu_final_list, status) - else: - # Create ccl_parameters with modified growth arrays - self._params, status = lib.parameters_create_nu_vec( - Omega_c, Omega_b, Omega_k, Neff, w0, wa, h, - norm_pk, n_s, bcm_log10Mc, bcm_etab, bcm_ks, - mu_0, sigma_0, c1_mg, c2_mg, lambda_mg, z_mg, - df_mg, mnu_final_list, status) - check(status) - finally: - physical_constants.T_CMB = T_CMB_old - - if Omega_g is not None: - total = self._params.Omega_g + self._params.Omega_l - self._params.Omega_g = Omega_g - self._params.Omega_l = total - Omega_g + nu_mass = [0.] + + c = const + # Curvature parameters. + k_sign = -np.sign(Omega_k) if np.abs(Omega_k) > 1e-6 else 0 + sqrtk = np.sqrt(np.abs(Omega_k)) * h / c.CLIGHT_HMPC + + # Fixed radiation parameters: (Omega_g h^2) is known from T_CMB. + rho_g = 4 * c.STBOLTZ / c.CLIGHT**3 * T_CMB**4 + rho_crit = c.RHO_CRITICAL * c.SOLAR_MASS / c.MPC_TO_METER**3 * h**2 + + # Get the N_nu_rel from Neff and N_nu_mass. + N_nu_rel = Neff - N_nu_mass * T_ncdm**4 / (4/11)**(4/3) + + # Temperature of the relativistic neutrinos in K. + T_nu = T_CMB * (4/11)**(1/3) + rho_nu_rel = N_nu_rel * (7/8) * 4 * c.STBOLTZ / c.CLIGHT**3 * T_nu**4 + Omega_nu_rel = rho_nu_rel / rho_crit + + # For non-relativistic neutrinos, calculate the phase-space integral. + self._fill_params(m_nu=nu_mass, N_nu_mass=N_nu_mass, + T_CMB=T_CMB, T_ncdm=T_ncdm, h=h) + Omega_nu_mass = self._get_Omega_nu() + + Omega_m = Omega_b + Omega_c + Omega_nu_mass + Omega_l = 1 - Omega_m - rho_g/rho_crit - Omega_nu_rel - Omega_k + if np.isnan(Omega_g): + # No value passed for Omega_g + Omega_g = rho_g/rho_crit + else: + # Omega_g was passed - modify Omega_l + Omega_l += rho_g/rho_crit - Omega_g + + self._fill_params( + sum_nu_masses=sum(nu_mass), N_nu_rel=N_nu_rel, Neff=Neff, + Omega_nu_mass=Omega_nu_mass, Omega_nu_rel=Omega_nu_rel, + Omega_m=Omega_m, Omega_c=Omega_c, Omega_b=Omega_b, Omega_k=Omega_k, + sqrtk=sqrtk, k_sign=int(k_sign), Omega_g=Omega_g, w0=w0, wa=wa, + Omega_l=Omega_l, H0=h*100, A_s=A_s, sigma8=sigma8, n_s=n_s, + mu_0=mu_0, sigma_0=sigma_0, c1_mg=c1_mg, c2_mg=c2_mg, + lambda_mg=lambda_mg, + bcm_log10Mc=bcm_log10Mc, bcm_etab=bcm_etab, bcm_ks=bcm_ks) + + # Modified growth (deprecated) + if z_mg is not None: + self._params.mgrowth = [z_mg, df_mg] + + def _fill_params(self, **kwargs): + if not hasattr(self, "_params"): + self._params = CosmologyParams() + [setattr(self._params, par, val) for par, val in kwargs.items()] def __getitem__(self, key): - """Access parameter values by name.""" - try: - if key == 'm_nu': - val = lib.parameters_get_nu_masses(self._params, 3) - elif key == 'extra_parameters': - val = self._params_init_kwargs["extra_parameters"] - else: - val = getattr(self._params, key) - except AttributeError: - raise KeyError("Parameter '%s' not recognized." % key) - return val - - def __setitem__(self, key, val): - """Set parameter values by name.""" - raise NotImplementedError("Cosmology objects are immutable; create a " - "new Cosmology() instance instead.") + if key == 'extra_parameters': + return self._params_init_kwargs["extra_parameters"] + return getattr(self._params, key) def __del__(self): """Free the C memory this object is managing as it is being garbage collected (hopefully).""" if hasattr(self, "cosmo"): - if (self.cosmo is not None and - hasattr(lib, 'cosmology_free') and - lib.cosmology_free is not None): - lib.cosmology_free(self.cosmo) - if hasattr(self, "_params"): - if (self._params is not None and - hasattr(lib, 'parameters_free') and - lib.parameters_free is not None): - lib.parameters_free(self._params) - - # finally delete some attributes we don't want to be around for safety - # when the context manager exits or if __del__ is called twice - if hasattr(self, "cosmo"): + lib.cosmology_free(self.cosmo) delattr(self, "cosmo") if hasattr(self, "_params"): + lib.parameters_free(self._params._instance) delattr(self, "_params") def __enter__(self): @@ -687,14 +692,6 @@ def __setstate__(self, state): self._build_cosmo() self._object_lock.lock() # Lock on exit. - def compute_distances(self): - """Compute the distance splines.""" - if self.has_distances: - return - status = 0 - status = lib.cosmology_compute_distances(self.cosmo, status) - check(status, self) - def compute_growth(self): """Compute the growth function.""" if self.has_growth: @@ -982,6 +979,31 @@ def get_nonlin_power(self, name='delta_matter:delta_matter'): raise KeyError("Unknown power spectrum %s." % name) return self._pk_nl[name] + def _get_Omega_nu(self, a=1): + r"""Compute :math:`\Omega_\nu`. + + Arguments + --------- + a : float or (na,) array-like + Scale factor(s), normalized to 1 today. + + Returns + ------- + omega_nu : float or (na,) ``numpy.ndarray`` + Value(s) of :math:`\Omega_\nu` at ``a``. + """ + a_use = np.atleast_1d(a).astype(float) + + status = 0 + OmNuh2, status = lib.Omeganuh2_vec( + self["N_nu_mass"], self["T_CMB"], self["T_ncdm"], + a_use, self["m_nu"], a_use.size, status) + check(status) + + if np.ndim(a) == 0: + return OmNuh2[0] / self["h"]**2 + return OmNuh2 / self["h"]**2 + @property def has_distances(self): """Checks if the distances have been precomputed.""" @@ -1107,9 +1129,8 @@ class CosmologyCalculator(Cosmology): equation of state. Defaults to -1. wa (:obj:`float`, optional): Second order term of dark energy equation of state. Defaults to 0. - T_CMB (:obj:`float`): The CMB temperature today. The default of - ``None`` uses the global CCL value in - ``pyccl.physical_constants.T_CMB``. + T_CMB (:obj:`float`): The CMB temperature today. The default is the + same as in the Cosmology base class. mu_0 (:obj:`float`, optional): One of the parameters of the mu-Sigma modified gravity model. Defaults to 0.0 sigma_0 (:obj:`float`, optional): One of the parameters of the mu-Sigma @@ -1166,13 +1187,21 @@ class CosmologyCalculator(Cosmology): computed. The only non-linear model supported is `'halofit'`, corresponding to the "HALOFIT" transformation of Takahashi et al. 2012 (arXiv:1208.2701). + T_ncdm (:obj:`float`): Non-CDM temperature in units of photon + temperature. The default is the same as in the base class """ + # TODO: Docstring - Move T_ncdm after T_CMB for CCLv3. + __eq_attrs__ = ("_params_init_kwargs", "_config_init_kwargs", + "_spline_params", "_gsl_params", "_pk_lin", "_pk_nl",) + def __init__( self, Omega_c=None, Omega_b=None, h=None, n_s=None, sigma8=None, A_s=None, Omega_k=0., Omega_g=None, Neff=3.046, m_nu=0., m_nu_type=None, w0=-1., wa=0., - T_CMB=None, mu_0=0., sigma_0=0., background=None, growth=None, - pk_linear=None, pk_nonlin=None, nonlinear_model=None): + T_CMB=_Defaults.T_CMB, mu_0=0., sigma_0=0., + background=None, growth=None, + pk_linear=None, pk_nonlin=None, nonlinear_model=None, + T_ncdm=_Defaults.T_ncdm): if pk_linear: transfer_function = 'calculator' else: @@ -1188,7 +1217,8 @@ def __init__( n_s=n_s, sigma8=sigma8, A_s=A_s, Omega_k=Omega_k, Omega_g=Omega_g, Neff=Neff, m_nu=m_nu, m_nu_type=m_nu_type, - w0=w0, wa=wa, T_CMB=T_CMB, mu_0=mu_0, sigma_0=sigma_0, + w0=w0, wa=wa, T_CMB=T_CMB, T_ncdm=T_ncdm, + mu_0=mu_0, sigma_0=sigma_0, transfer_function=transfer_function, matter_power_spectrum=matter_power_spectrum) diff --git a/pyccl/correlations.py b/pyccl/correlations.py index b2af018d9..ddbca82ef 100644 --- a/pyccl/correlations.py +++ b/pyccl/correlations.py @@ -1,4 +1,4 @@ -"""Correlation functon computations. +"""Correlation function computations. Choices of algorithms used to compute correlation functions: 'Bessel' is a direct integration using Bessel functions. @@ -10,6 +10,8 @@ from . import constants as const from .core import check from .pk2d import parse_pk2d +from .base import warn_api +from .errors import CCLDeprecationWarning import numpy as np import warnings @@ -27,7 +29,8 @@ } -def correlation(cosmo, ell, C_ell, theta, type='NN', corr_type=None, +@warn_api +def correlation(cosmo, *, ell, C_ell, theta, type='NN', corr_type=None, method='fftlog'): r"""Compute the angular correlation function. @@ -100,7 +103,6 @@ def correlation(cosmo, ell, C_ell, theta, type='NN', corr_type=None, float or array_like: Value(s) of the correlation function at the \ input angular separations. """ - from .errors import CCLWarning cosmo_in = cosmo cosmo = cosmo.cosmo status = 0 @@ -119,7 +121,7 @@ def correlation(cosmo, ell, C_ell, theta, type='NN', corr_type=None, else: raise ValueError("Unknown corr_type " + corr_type) warnings.warn("corr_type is deprecated. Use type = {}".format(type), - CCLWarning) + CCLDeprecationWarning) method = method.lower() if type not in correlation_types.keys(): @@ -149,14 +151,15 @@ def correlation(cosmo, ell, C_ell, theta, type='NN', corr_type=None, return wth -def correlation_3d(cosmo, a, r, p_of_k_a=None): +@warn_api(reorder=['a', 'r']) +def correlation_3d(cosmo, *, r, a, p_of_k_a=None): """Compute the 3D correlation function. Args: cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - a (float): scale factor. r (float or array_like): distance(s) at which to calculate the 3D - correlation function (in Mpc). + correlation function (in Mpc). + a (float): scale factor. p_of_k_a (:class:`~pyccl.pk2d.Pk2D`, `str` or None): 3D Power spectrum to integrate. If a string, it must correspond to one of the non-linear power spectra stored in `cosmo` (e.g. @@ -190,16 +193,18 @@ def correlation_3d(cosmo, a, r, p_of_k_a=None): return xi -def correlation_multipole(cosmo, a, beta, l, s, p_of_k_a=None): +@warn_api(pairs=[('s', 'r'), ('l', 'ell')], + reorder=['a', 'beta', 'ell', 'r']) +def correlation_multipole(cosmo, *, r, a, beta, ell, p_of_k_a=None): """Compute the correlation multipoles. Args: cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. + r (float or array_like): distance(s) at which to calculate the 3DRsd + correlation function (in Mpc). a (float): scale factor. beta (float): growth rate divided by galaxy bias. - l (int) : the desired multipole - s (float or array_like): distance(s) at which to calculate the 3DRsd - correlation function (in Mpc). + ell (int) : the desired multipole p_of_k_a (:class:`~pyccl.pk2d.Pk2D`, `str` or None): 3D Power spectrum to integrate. If a string, it must correspond to one of the non-linear power spectra stored in `cosmo` (e.g. @@ -220,39 +225,42 @@ def correlation_multipole(cosmo, a, beta, l, s, p_of_k_a=None): # Convert scalar input into an array scalar = False - if isinstance(s, (int, float)): + if isinstance(r, (int, float)): scalar = True - s = np.array([s, ]) + r = np.array([r, ]) # Call 3D correlation function - xis, status = lib.correlation_multipole_vec(cosmo, psp, a, beta, l, s, - len(s), status) + xis, status = lib.correlation_multipole_vec(cosmo, psp, a, beta, ell, r, + len(r), status) check(status, cosmo_in) if scalar: return xis[0] return xis -def correlation_3dRsd(cosmo, a, s, mu, beta, use_spline=True, p_of_k_a=None): +@warn_api(pairs=[('s', 'r')], + reorder=['a', 'r', 'mu', 'beta', 'use_spline', 'p_of_k_a']) +def correlation_3dRsd(cosmo, *, r, a, mu, beta, + p_of_k_a=None, use_spline=True): """ Compute the 3DRsd correlation function using linear approximation with multipoles. Args: cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. + r (float or array_like): distance(s) at which to calculate the + 3DRsd correlation function (in Mpc). a (float): scale factor. - s (float or array_like): distance(s) at which to calculate the - 3DRsd correlation function (in Mpc). mu (float): cosine of the angle at which to calculate the 3DRsd - correlation function (in Radian). + correlation function (in Radian). beta (float): growth rate divided by galaxy bias. - use_spline: switch that determines whether the RSD correlation - function is calculated using global splines of multipoles. p_of_k_a (:class:`~pyccl.pk2d.Pk2D`, `str` or None): 3D Power spectrum to integrate. If a string, it must correspond to one of the non-linear power spectra stored in `cosmo` (e.g. `'delta_matter:delta_matter'`). If `None`, the non-linear matter power spectrum stored in `cosmo` will be used. + use_spline: switch that determines whether the RSD correlation + function is calculated using global splines of multipoles. Returns: Value(s) of the correlation function at the input distance(s) & angle. @@ -268,28 +276,29 @@ def correlation_3dRsd(cosmo, a, s, mu, beta, use_spline=True, p_of_k_a=None): # Convert scalar input into an array scalar = False - if isinstance(s, (int, float)): + if isinstance(r, (int, float)): scalar = True - s = np.array([s, ]) + r = np.array([r, ]) # Call 3D correlation function - xis, status = lib.correlation_3dRsd_vec(cosmo, psp, a, mu, beta, s, - len(s), int(use_spline), status) + xis, status = lib.correlation_3dRsd_vec(cosmo, psp, a, mu, beta, r, + len(r), int(use_spline), status) check(status, cosmo_in) if scalar: return xis[0] return xis -def correlation_3dRsd_avgmu(cosmo, a, s, beta, p_of_k_a=None): +@warn_api(pairs=[('s', 'r')], reorder=['a', 'r']) +def correlation_3dRsd_avgmu(cosmo, *, r, a, beta, p_of_k_a=None): """ Compute the 3DRsd correlation function averaged over mu at constant s. Args: cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. + r (float or array_like): distance(s) at which to calculate the 3DRsd + correlation function (in Mpc). a (float): scale factor. - s (float or array_like): distance(s) at which to calculate the 3DRsd - correlation function (in Mpc). beta (float): growth rate divided by galaxy bias. p_of_k_a (:class:`~pyccl.pk2d.Pk2D`, `str` or None): 3D Power spectrum to integrate. If a string, it must correspond to one of the @@ -311,36 +320,40 @@ def correlation_3dRsd_avgmu(cosmo, a, s, beta, p_of_k_a=None): # Convert scalar input into an array scalar = False - if isinstance(s, (int, float)): + if isinstance(r, (int, float)): scalar = True - s = np.array([s, ]) + r = np.array([r, ]) # Call 3D correlation function - xis, status = lib.correlation_3dRsd_avgmu_vec(cosmo, psp, a, beta, s, - len(s), status) + xis, status = lib.correlation_3dRsd_avgmu_vec(cosmo, psp, a, beta, r, + len(r), status) check(status, cosmo_in) if scalar: return xis[0] return xis -def correlation_pi_sigma(cosmo, a, beta, pi, sig, +@warn_api(pairs=[("sig", "sigma")], + reorder=['a', 'beta', 'pi', 'sigma']) +def correlation_pi_sigma(cosmo, *, pi, sigma, a, beta, use_spline=True, p_of_k_a=None): """ Compute the 3DRsd correlation in pi-sigma space. Args: cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - a (float): scale factor. pi (float): distance times cosine of the angle (in Mpc). - sig (float or array-like): distance(s) times sine of the angle - (in Mpc). + sigma (float or array-like): distance(s) times sine of the angle + (in Mpc). + a (float): scale factor. beta (float): growth rate divided by galaxy bias. p_of_k_a (:class:`~pyccl.pk2d.Pk2D`, `str` or None): 3D Power spectrum to integrate. If a string, it must correspond to one of the non-linear power spectra stored in `cosmo` (e.g. `'delta_matter:delta_matter'`). If `None`, the non-linear matter power spectrum stored in `cosmo` will be used. + use_spline: switch that determines whether the RSD correlation + function is calculated using global splines of multipoles. Returns: Value(s) of the correlation function at the input pi and sigma. @@ -356,13 +369,13 @@ def correlation_pi_sigma(cosmo, a, beta, pi, sig, # Convert scalar input into an array scalar = False - if isinstance(sig, (int, float)): + if isinstance(sigma, (int, float)): scalar = True - sig = np.array([sig, ]) + sigma = np.array([sigma, ]) # Call 3D correlation function - xis, status = lib.correlation_pi_sigma_vec(cosmo, psp, a, beta, pi, sig, - len(sig), int(use_spline), + xis, status = lib.correlation_pi_sigma_vec(cosmo, psp, a, beta, pi, sigma, + len(sigma), int(use_spline), status) check(status, cosmo_in) if scalar: diff --git a/pyccl/covariances.py b/pyccl/covariances.py index 51826c339..c34dc2e2c 100644 --- a/pyccl/covariances.py +++ b/pyccl/covariances.py @@ -5,15 +5,16 @@ from .background import comoving_radial_distance, comoving_angular_distance from .tk3d import Tk3D from .pk2d import parse_pk2d +from .base import warn_api -# Define symbolic 'None' type for arrays, to allow proper handling by swig -# wrapper -NoneArr = np.array([]) - -def angular_cl_cov_cNG(cosmo, cltracer1, cltracer2, ell, tkka, fsky=1., - cltracer3=None, cltracer4=None, ell2=None, - integration_method='qag_quad'): +@warn_api(pairs=[("cltracer1", "tracer1"), ("cltracer2", "tracer2"), + ("cltracer3", "tracer3"), ("cltracer4", "tracer4"), + ("tkka", "t_of_kk_a")], + reorder=['fsky', 'tracer3', 'tracer4', 'ell2']) +def angular_cl_cov_cNG(cosmo, tracer1, tracer2, *, ell, t_of_kk_a, + tracer3=None, tracer4=None, ell2=None, + fsky=1., integration_method='qag_quad'): """Calculate the connected non-Gaussian covariance for a pair of power spectra :math:`C_{\\ell_1}^{ab}` and :math:`C_{\\ell_2}^{cd}`, between two pairs of tracers (:math:`(a,b)` and :math:`(c,d)`). @@ -38,21 +39,22 @@ def angular_cl_cov_cNG(cosmo, cltracer1, cltracer2, ell, tkka, fsky=1., Args: cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - cltracer1 (:class:`~pyccl.tracers.Tracer`): a `Tracer` object, + tracer1 (:class:`~pyccl.tracers.Tracer`): a `Tracer` object, of any kind. - cltracer2 (:class:`~pyccl.tracers.Tracer`): a second `Tracer` object, + tracer2 (:class:`~pyccl.tracers.Tracer`): a second `Tracer` object, of any kind. ell (float or array_like): Angular wavenumber(s) at which to evaluate the first dimension of the angular power spectrum covariance. - tkka (:class:`~pyccl.tk3d.Tk3D` or None): 3D connected trispectrum. - fsky (float): sky fraction. - cltracer3 (:class:`~pyccl.tracers.Tracer`): a `Tracer` object, - of any kind. If `None`, `cltracer1` will be used instead. - cltracer4 (:class:`~pyccl.tracers.Tracer`): a `Tracer` object, - of any kind. If `None`, `cltracer1` will be used instead. + t_of_kk_a (:class:`~pyccl.tk3d.Tk3D` or None): 3D connected + trispectrum. + tracer3 (:class:`~pyccl.tracers.Tracer`): a `Tracer` object, + of any kind. If `None`, `tracer1` will be used instead. + tracer4 (:class:`~pyccl.tracers.Tracer`): a `Tracer` object, + of any kind. If `None`, `tracer1` will be used instead. ell2 (float or array_like): Angular wavenumber(s) at which to evaluate the second dimension of the angular power spectrum covariance. If `None`, `ell` will be used instead. + fsky (float): sky fraction. integration_method (string) : integration method to be used for the Limber integrals. Possibilities: 'qag_quad' (GSL's `qag` method backed up by `quad` when it fails) and 'spline' (the @@ -77,30 +79,30 @@ def angular_cl_cov_cNG(cosmo, cltracer1, cltracer2, ell, tkka, fsky=1., cosmo_in = cosmo cosmo = cosmo.cosmo - if isinstance(tkka, Tk3D): - tsp = tkka.tsp + if isinstance(t_of_kk_a, Tk3D): + tsp = t_of_kk_a.tsp else: - raise ValueError("tkka must be a pyccl.Tk3D") + raise ValueError("t_of_kk_a must be of type pyccl.Tk3D") # Create tracer colections status = 0 clt1, status = lib.cl_tracer_collection_t_new(status) - for t in cltracer1._trc: + for t in tracer1._trc: status = lib.add_cl_tracer_to_collection(clt1, t, status) clt2, status = lib.cl_tracer_collection_t_new(status) - for t in cltracer2._trc: + for t in tracer2._trc: status = lib.add_cl_tracer_to_collection(clt2, t, status) - if cltracer3 is None: + if tracer3 is None: clt3 = clt1 else: clt3, status = lib.cl_tracer_collection_t_new(status) - for t in cltracer3._trc: + for t in tracer3._trc: status = lib.add_cl_tracer_to_collection(clt3, t, status) - if cltracer4 is None: + if tracer4 is None: clt4 = clt2 else: clt4, status = lib.cl_tracer_collection_t_new(status) - for t in cltracer4._trc: + for t in tracer4._trc: status = lib.add_cl_tracer_to_collection(clt4, t, status) ell1_use = np.atleast_1d(ell) @@ -122,16 +124,17 @@ def angular_cl_cov_cNG(cosmo, cltracer1, cltracer2, ell, tkka, fsky=1., # Free up tracer collections lib.cl_tracer_collection_t_free(clt1) lib.cl_tracer_collection_t_free(clt2) - if cltracer3 is not None: + if tracer3 is not None: lib.cl_tracer_collection_t_free(clt3) - if cltracer4 is not None: + if tracer4 is not None: lib.cl_tracer_collection_t_free(clt4) check(status, cosmo=cosmo_in) return cov -def sigma2_B_disc(cosmo, a=None, fsky=1., p_of_k_a=None): +@warn_api(pairs=[('a', 'a_arr')]) +def sigma2_B_disc(cosmo, a_arr=None, *, fsky=1., p_of_k_a=None): """Returns the variance of the projected linear density field over a circular disc covering a sky fraction `fsky` as a function of scale factor. This is given by @@ -154,35 +157,39 @@ def sigma2_B_disc(cosmo, a=None, fsky=1., p_of_k_a=None): power spectrum to use. Defaults to `None`, in which case the internal linear power spectrum from `cosmo` is used. + Returns: - float or array_like: values of the projected variance. + a_arr (array_like): an array of scale factor values at which the + projected variance has been evaluated. Only returned if `a_arr` is + `None` on input. + sigma2_B (float or array_like): projected variance. """ - status = 0 - if a is None: - na = lib.get_pk_spline_na(cosmo.cosmo) - a_arr, status = lib.get_pk_spline_a(cosmo.cosmo, na, status) - check(status, cosmo=cosmo) + full_output = a_arr is None + + if full_output: + a_arr = cosmo.get_pk_spline_a() else: - a_arr = np.atleast_1d(a) - na = len(a_arr) + ndim = np.ndim(a_arr) + a_arr = np.atleast_1d(a_arr) chi_arr = comoving_radial_distance(cosmo, a_arr) R_arr = chi_arr * np.arccos(1-2*fsky) psp = parse_pk2d(cosmo, p_of_k_a, is_linear=True) + status = 0 s2B_arr, status = lib.sigma2b_vec(cosmo.cosmo, a_arr, R_arr, psp, - na, status) + len(a_arr), status) check(status, cosmo=cosmo) - if a is None: + + if full_output: return a_arr, s2B_arr - else: - if np.ndim(a) == 0: - return s2B_arr[0] - else: - return s2B_arr + if ndim == 0: + return s2B_arr[0] + return s2B_arr -def sigma2_B_from_mask(cosmo, a=None, mask_wl=None, p_of_k_a=None): +@warn_api(pairs=[('a', 'a_arr')]) +def sigma2_B_from_mask(cosmo, a_arr=None, *, mask_wl=None, p_of_k_a=None): """ Returns the variance of the projected linear density field, given the angular power spectrum of the footprint mask and scale factor. This is given by @@ -198,7 +205,7 @@ def sigma2_B_from_mask(cosmo, a=None, mask_wl=None, p_of_k_a=None): Args: cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. - a (float, array_like or `None`): an array of scale factor + a_arr (float, array_like or `None`): an array of scale factor values at which to evaluate the projected variance. mask_wl (array_like): Array with the angular power spectrum of the masks. The power spectrum should be given at integer multipoles, @@ -211,15 +218,23 @@ def sigma2_B_from_mask(cosmo, a=None, mask_wl=None, p_of_k_a=None): internal linear power spectrum from `cosmo` is used. Returns: - float or array_like: values of the projected variance. + a_arr (array_like): an array of scale factor values at which the + projected variance has been evaluated. Only returned if `a_arr` is + `None` on input. + sigma2_B (float or array_like): projected variance. """ + full_output = a_arr is None + + if full_output: + a_arr = cosmo.get_pk_spline_a() + else: + ndim = np.ndim(a_arr) + a_arr = np.atleast_1d(a_arr) + if p_of_k_a is None: cosmo.compute_linear_power() p_of_k_a = cosmo.get_linear_power() - a_arr = np.atleast_1d(a) - chi = comoving_angular_distance(cosmo, a=a_arr) - ell = np.arange(mask_wl.size) sigma2_B = np.zeros(a_arr.size) @@ -228,23 +243,29 @@ def sigma2_B_from_mask(cosmo, a=None, mask_wl=None, p_of_k_a=None): # For a=1, the integral becomes independent of the footprint in # the flat-sky approximation. So we are just using the method # for the disc geometry here - sigma2_B[i] = sigma2_B_disc(cosmo=cosmo, a=a_arr[i], + sigma2_B[i] = sigma2_B_disc(cosmo=cosmo, a_arr=a_arr[i], p_of_k_a=p_of_k_a) else: + chi = comoving_angular_distance(cosmo, a=a_arr) k = (ell+0.5)/chi[i] pk = p_of_k_a.eval(k, a_arr[i], cosmo) # See eq. E.10 of 2007.01844 sigma2_B[i] = np.sum(pk * mask_wl)/chi[i]**2 - if np.ndim(a) == 0: + if full_output: + return a_arr, sigma2_B + if ndim == 0: return sigma2_B[0] - else: - return sigma2_B + return sigma2_B -def angular_cl_cov_SSC(cosmo, cltracer1, cltracer2, ell, tkka, +@warn_api(pairs=[("cltracer1", "tracer1"), ("cltracer2", "tracer2"), + ("cltracer3", "tracer3"), ("cltracer4", "tracer4"), + ('tkka', 't_of_kk_a')], + reorder=['sigma2_B', 'fsky', 'tracer3', 'tracer4', 'ell2']) +def angular_cl_cov_SSC(cosmo, tracer1, tracer2, *, ell, t_of_kk_a, + tracer3=None, tracer4=None, ell2=None, sigma2_B=None, fsky=1., - cltracer3=None, cltracer4=None, ell2=None, integration_method='qag_quad'): """Calculate the super-sample contribution to the connected non-Gaussian covariance for a pair of power spectra @@ -271,26 +292,27 @@ def angular_cl_cov_SSC(cosmo, cltracer1, cltracer2, ell, tkka, Args: cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - cltracer1 (:class:`~pyccl.tracers.Tracer`): a `Tracer` object, + tracer1 (:class:`~pyccl.tracers.Tracer`): a `Tracer` object, of any kind. - cltracer2 (:class:`~pyccl.tracers.Tracer`): a second `Tracer` object, + tracer2 (:class:`~pyccl.tracers.Tracer`): a second `Tracer` object, of any kind. ell (float or array_like): Angular wavenumber(s) at which to evaluate the first dimension of the angular power spectrum covariance. - tkka (:class:`~pyccl.tk3d.Tk3D` or None): 3D connected trispectrum. + t_of_kk_a (:class:`~pyccl.tk3d.Tk3D` or None): 3D connected + trispectrum. + tracer3 (:class:`~pyccl.tracers.Tracer`): a `Tracer` object, + of any kind. If `None`, `tracer1` will be used instead. + tracer4 (:class:`~pyccl.tracers.Tracer`): a `Tracer` object, + of any kind. If `None`, `tracer1` will be used instead. + ell2 (float or array_like): Angular wavenumber(s) at which to evaluate + the second dimension of the angular power spectrum covariance. If + `None`, `ell` will be used instead. sigma2_B (tuple of arrays or `None`): A tuple of arrays (a, sigma2_B(a)) containing the variance of the projected matter overdensity over the footprint as a function of the scale factor. If `None`, a compact circular footprint will be assumed covering a sky fraction `fsky`. fsky (float): sky fraction. - cltracer3 (:class:`~pyccl.tracers.Tracer`): a `Tracer` object, - of any kind. If `None`, `cltracer1` will be used instead. - cltracer4 (:class:`~pyccl.tracers.Tracer`): a `Tracer` object, - of any kind. If `None`, `cltracer1` will be used instead. - ell2 (float or array_like): Angular wavenumber(s) at which to evaluate - the second dimension of the angular power spectrum covariance. If - `None`, `ell` will be used instead. integration_method (string) : integration method to be used for the Limber integrals. Possibilities: 'qag_quad' (GSL's `qag` method backed up by `quad` when it fails) and 'spline' (the @@ -315,30 +337,30 @@ def angular_cl_cov_SSC(cosmo, cltracer1, cltracer2, ell, tkka, cosmo_in = cosmo cosmo = cosmo.cosmo - if isinstance(tkka, Tk3D): - tsp = tkka.tsp + if isinstance(t_of_kk_a, Tk3D): + tsp = t_of_kk_a.tsp else: - raise ValueError("tkka must be a pyccl.Tk3D") + raise ValueError("t_of_kk_a must be of type pyccl.Tk3D") # Create tracer colections status = 0 clt1, status = lib.cl_tracer_collection_t_new(status) - for t in cltracer1._trc: + for t in tracer1._trc: status = lib.add_cl_tracer_to_collection(clt1, t, status) clt2, status = lib.cl_tracer_collection_t_new(status) - for t in cltracer2._trc: + for t in tracer2._trc: status = lib.add_cl_tracer_to_collection(clt2, t, status) - if cltracer3 is None: + if tracer3 is None: clt3 = clt1 else: clt3, status = lib.cl_tracer_collection_t_new(status) - for t in cltracer3._trc: + for t in tracer3._trc: status = lib.add_cl_tracer_to_collection(clt3, t, status) - if cltracer4 is None: + if tracer4 is None: clt4 = clt2 else: clt4, status = lib.cl_tracer_collection_t_new(status) - for t in cltracer4._trc: + for t in tracer4._trc: status = lib.add_cl_tracer_to_collection(clt4, t, status) ell1_use = np.atleast_1d(ell) @@ -364,9 +386,9 @@ def angular_cl_cov_SSC(cosmo, cltracer1, cltracer2, ell, tkka, # Free up tracer collections lib.cl_tracer_collection_t_free(clt1) lib.cl_tracer_collection_t_free(clt2) - if cltracer3 is not None: + if tracer3 is not None: lib.cl_tracer_collection_t_free(clt3) - if cltracer4 is not None: + if tracer4 is not None: lib.cl_tracer_collection_t_free(clt4) check(status, cosmo=cosmo_in) diff --git a/pyccl/halomodel.py b/pyccl/halomodel.py index cd9bbf85e..01880f4c7 100644 --- a/pyccl/halomodel.py +++ b/pyccl/halomodel.py @@ -1,5 +1,5 @@ from . import ccllib as lib -from .pyutils import deprecated +from .base import deprecated from . import halos as hal @@ -47,8 +47,7 @@ def halo_concentration(cosmo, halo_mass, a, odelta=200): """ mdef = hal.MassDef(odelta, 'matter') c = _get_concentration(cosmo, mdef) - - return c.get_concentration(cosmo, halo_mass, a) + return c(cosmo, halo_mass, a) @deprecated(hal.halomod_power_spectrum) diff --git a/pyccl/haloprofile.py b/pyccl/haloprofile.py index 57d43c0d0..1069987f3 100644 --- a/pyccl/haloprofile.py +++ b/pyccl/haloprofile.py @@ -1,5 +1,5 @@ from . import halos as hal -from .pyutils import deprecated +from .base import deprecated @deprecated(hal.HaloProfileNFW) @@ -24,10 +24,9 @@ def nfw_profile_3d(cosmo, concentration, halo_mass, odelta, a, r): float or array_like: 3D NFW density at r, in units of Msun/Mpc^3. """ mdef = hal.MassDef(odelta, 'matter') - c = hal.ConcentrationConstant(c=concentration, - mdef=mdef) + c = hal.ConcentrationConstant(c=concentration, mass_def=mdef) p = hal.HaloProfileNFW(c, truncated=False) - return p.real(cosmo, r, halo_mass, a, mdef) + return p.real(cosmo, r, halo_mass, a, mass_def=mdef) @deprecated(hal.HaloProfileEinasto) @@ -55,12 +54,10 @@ def einasto_profile_3d(cosmo, concentration, halo_mass, odelta, a, r): float or array_like: 3D NFW density at r, in units of Msun/Mpc^3. """ mdef = hal.MassDef(odelta, 'matter') - c = hal.ConcentrationConstant(c=concentration, - mdef=mdef) - mdef = hal.MassDef(odelta, 'matter', - c_m_relation=c) + c = hal.ConcentrationConstant(c=concentration, mass_def=mdef) + mdef = hal.MassDef(odelta, 'matter', concentration=c) p = hal.HaloProfileEinasto(c, truncated=False) - return p.real(cosmo, r, halo_mass, a, mdef) + return p.real(cosmo, r, halo_mass, a, mass_def=mdef) @deprecated(hal.HaloProfileHernquist) @@ -86,10 +83,9 @@ def hernquist_profile_3d(cosmo, concentration, halo_mass, odelta, a, r): float or array_like: 3D NFW density at r, in units of Msun/Mpc^3. """ mdef = hal.MassDef(odelta, 'matter') - c = hal.ConcentrationConstant(c=concentration, - mdef=mdef) + c = hal.ConcentrationConstant(c=concentration, mass_def=mdef) p = hal.HaloProfileHernquist(c, truncated=False) - return p.real(cosmo, r, halo_mass, a, mdef) + return p.real(cosmo, r, halo_mass, a, mass_def=mdef) @deprecated(hal.HaloProfileNFW) @@ -116,8 +112,6 @@ def nfw_profile_2d(cosmo, concentration, halo_mass, odelta, a, r): in units of Msun/Mpc^2. """ mdef = hal.MassDef(odelta, 'matter') - c = hal.ConcentrationConstant(c=concentration, - mdef=mdef) - p = hal.HaloProfileNFW(c, truncated=False, - projected_analytic=True) - return p.projected(cosmo, r, halo_mass, a, mdef) + c = hal.ConcentrationConstant(c=concentration, mass_def=mdef) + p = hal.HaloProfileNFW(c, truncated=False, projected_analytic=True) + return p.projected(cosmo, r, halo_mass, a, mass_def=mdef) diff --git a/pyccl/halos/__init__.py b/pyccl/halos/__init__.py index afff41860..724385df5 100644 --- a/pyccl/halos/__init__.py +++ b/pyccl/halos/__init__.py @@ -1,109 +1,11 @@ -# Halo mass definitions -from .massdef import ( - mass2radius_lagrangian, - MassDef, - MassDef200m, - MassDef200c, - MassDef500c, - MassDefVir, - convert_concentration, -) - -# Halo mass-concentration relations -from .concentration import ( - Concentration, - ConcentrationDiemer15, - ConcentrationBhattacharya13, - ConcentrationPrada12, - ConcentrationKlypin11, - ConcentrationDuffy08, - ConcentrationIshiyama21, - ConcentrationConstant, - concentration_from_name, -) - -# Halo mass functions -from .hmfunc import ( - MassFunc, - MassFuncPress74, - MassFuncSheth99, - MassFuncJenkins01, - MassFuncTinker08, - MassFuncTinker10, - MassFuncWatson13, - MassFuncAngulo12, - MassFuncDespali16, - MassFuncBocquet16, - mass_function_from_name, -) - -# Halo bias functions -from .hbias import ( - HaloBias, - HaloBiasSheth99, - HaloBiasSheth01, - HaloBiasTinker10, - HaloBiasBhattacharya11, - halo_bias_from_name, -) - -# Halo profiles -from .profiles import ( - HaloProfile, - HaloProfileGaussian, - HaloProfilePowerLaw, - HaloProfileNFW, - HaloProfileEinasto, - HaloProfileHernquist, - HaloProfilePressureGNFW, - HaloProfileHOD, -) - -# Halo profile 2-point cumulants -from .profiles_2pt import ( - Profile2pt, - Profile2ptHOD, -) - -# Halo model power spectrum -from .halo_model import ( - HMCalculator, - halomod_mean_profile_1pt, - halomod_bias_1pt, - halomod_power_spectrum, - halomod_Pk2D, - halomod_trispectrum_1h, - halomod_Tk3D_1h, - halomod_Tk3D_SSC, - halomod_Tk3D_SSC_linear_bias, -) - -# CIB profiles -from .profiles_cib import ( - HaloProfileCIBShang12, - Profile2ptCIB, -) - - -__all__ = ( - 'mass2radius_lagrangian', 'MassDef', 'MassDef200m', 'MassDef200c', - 'MassDef500c', 'MassDefVir', 'convert_concentration', - 'Concentration', 'ConcentrationDiemer15', 'ConcentrationBhattacharya13', - 'ConcentrationPrada12', 'ConcentrationKlypin11', 'ConcentrationDuffy08', - 'ConcentrationIshiyama21', 'ConcentrationConstant', - 'concentration_from_name', - 'MassFunc', 'MassFuncPress74', 'MassFuncSheth99', 'MassFuncJenkins01', - 'MassFuncTinker08', 'MassFuncTinker10', 'MassFuncWatson13', - 'MassFuncAngulo12', 'MassFuncDespali16', 'MassFuncBocquet16', - 'mass_function_from_name', - 'HaloBias', 'HaloBiasSheth99', 'HaloBiasSheth01', 'HaloBiasTinker10', - 'HaloBiasBhattacharya11', 'halo_bias_from_name', - 'HaloProfile', 'HaloProfileGaussian', 'HaloProfilePowerLaw', - 'HaloProfileNFW', 'HaloProfileEinasto', 'HaloProfileHernquist', - 'HaloProfilePressureGNFW', 'HaloProfileHOD', - 'Profile2pt', 'Profile2ptHOD', - 'HMCalculator', 'halomod_mean_profile_1pt', 'halomod_bias_1pt', - 'halomod_power_spectrum', 'halomod_Pk2D', 'halomod_trispectrum_1h', - 'halomod_Tk3D_1h', 'halomod_Tk3D_SSC', 'halomod_Tk3D_SSC_linear_bias', - 'HaloProfileCIBShang12', 'Profile2ptCIB', -) +from .halo_model_base import * +from .concentration import * +from .hmfunc import * +from .hbias import * +from .massdef import * +from .profiles import * +from .profiles_2pt import * +from .halo_model import * +from .pk_1pt import * +from .pk_2pt import * +from .pk_4pt import * diff --git a/pyccl/halos/concentration.py b/pyccl/halos/concentration.py deleted file mode 100644 index eced4ddf2..000000000 --- a/pyccl/halos/concentration.py +++ /dev/null @@ -1,580 +0,0 @@ -from .. import ccllib as lib -from ..pyutils import check -from ..background import growth_factor, growth_rate -from .massdef import MassDef, mass2radius_lagrangian -from ..power import linear_matter_power, sigmaM -from ..base import CCLHalosObject -import numpy as np -from scipy.optimize import brentq, root_scalar -import functools -from abc import abstractmethod - - -class Concentration(CCLHalosObject): - """ This class enables the calculation of halo concentrations. - - Args: - mass_def (:class:`~pyccl.halos.massdef.MassDef`): a mass definition - object that fixes the mass definition used by this c(M) - parametrization. - """ - __repr_attrs__ = ("mdef",) - - def __init__(self, mass_def=None): - if mass_def is not None: - if self._check_mdef(mass_def): - raise ValueError( - f"Mass definition {mass_def.Delta}-{mass_def.rho_type} " - f"is not compatible with c(M) {self.name} configuration.") - self.mdef = mass_def - else: - self._default_mdef() - self._setup() - - @abstractmethod - def _default_mdef(self): - """ Assigns a default mass definition for this object if - none is passed at initialization. - """ - - def _setup(self): - """ Use this function to initialize any internal attributes - of this object. This function is called at the very end of the - constructor call. - """ - pass - - def _check_mdef(self, mdef): - """ Return False if the input mass definition agrees with - the definitions for which this concentration-mass relation - works. True otherwise. This function gets called at the - start of the constructor call. - - Args: - mdef (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - - Returns: - bool: True if the mass definition is not compatible with - this parametrization. False otherwise. - """ - return False - - def _get_consistent_mass(self, cosmo, M, a, mdef_other): - """ Transform a halo mass with a given mass definition into - the corresponding mass definition that was used to initialize - this object. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - M (float or array_like): halo mass in units of M_sun. - a (float): scale factor. - mdef_other (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - - Returns: - float or array_like: mass according to this object's - mass definition. - """ - if mdef_other is not None: - M_use = mdef_other.translate_mass(cosmo, M, a, self.mdef) - else: - M_use = M - return M_use - - @abstractmethod - def _concentration(self, cosmo, M, a): - """Implementation of the c(M) relation.""" - - def get_concentration(self, cosmo, M, a, mdef_other=None): - """ Returns the concentration for input parameters. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - M (float or array_like): halo mass in units of M_sun. - a (float): scale factor. - mdef_other (:class:`~pyccl.halos.massdef.MassDef`): - the mass definition object that defines M. - - Returns: - float or array_like: concentration. - """ - M_use = self._get_consistent_mass(cosmo, - np.atleast_1d(M), - a, mdef_other) - - c = self._concentration(cosmo, M_use, a) - if np.ndim(M) == 0: - c = c[0] - return c - - @classmethod - def from_name(cls, name): - """ Returns halo concentration subclass from name string - - Args: - name (string): a concentration name - - Returns: - Concentration subclass corresponding to the input name. - """ - concentrations = {c.name: c for c in cls.__subclasses__()} - if name in concentrations: - return concentrations[name] - else: - raise ValueError(f"Concentration {name} not implemented.") - - -class ConcentrationDiemer15(Concentration): - """ Concentration-mass relation by Diemer & Kravtsov 2015 - (arXiv:1407.4730). This parametrization is only valid for - S.O. masses with Delta = 200-critical. - - Args: - mdef (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object that fixes - the mass definition used by this c(M) - parametrization. - """ - name = 'Diemer15' - - def __init__(self, mdef=None): - super(ConcentrationDiemer15, self).__init__(mdef) - - def _default_mdef(self): - self.mdef = MassDef(200, 'critical') - - def _setup(self): - self.kappa = 1.0 - self.phi_0 = 6.58 - self.phi_1 = 1.27 - self.eta_0 = 7.28 - self.eta_1 = 1.56 - self.alpha = 1.08 - self.beta = 1.77 - - def _check_mdef(self, mdef): - if isinstance(mdef.Delta, str): - return True - elif not ((int(mdef.Delta) == 200) and - (mdef.rho_type == 'critical')): - return True - return False - - def _concentration(self, cosmo, M, a): - M_use = np.atleast_1d(M) - - # Compute power spectrum slope - R = mass2radius_lagrangian(cosmo, M_use) - lk_R = np.log(2.0 * np.pi / R * self.kappa) - # Using central finite differences - lk_hi = lk_R + 0.005 - lk_lo = lk_R - 0.005 - dlpk = np.log(linear_matter_power(cosmo, np.exp(lk_hi), a) / - linear_matter_power(cosmo, np.exp(lk_lo), a)) - dlk = lk_hi - lk_lo - n = dlpk / dlk - - sig = sigmaM(cosmo, M_use, a) - delta_c = 1.68647 - nu = delta_c / sig - - floor = self.phi_0 + n * self.phi_1 - nu0 = self.eta_0 + n * self.eta_1 - c = 0.5 * floor * ((nu0 / nu)**self.alpha + - (nu / nu0)**self.beta) - if np.ndim(M) == 0: - c = c[0] - - return c - - -class ConcentrationBhattacharya13(Concentration): - """ Concentration-mass relation by Bhattacharya et al. 2013 - (arXiv:1112.5479). This parametrization is only valid for - S.O. masses with Delta = Delta_vir, 200-matter and 200-critical. - By default it will be initialized for Delta = 200-critical. - - Args: - mdef (:class:`~pyccl.halos.massdef.MassDef`): a mass - definition object that fixes - the mass definition used by this c(M) - parametrization. - """ - name = 'Bhattacharya13' - - def __init__(self, mdef=None): - super(ConcentrationBhattacharya13, self).__init__(mdef) - - def _default_mdef(self): - self.mdef = MassDef(200, 'critical') - - def _check_mdef(self, mdef): - if mdef.Delta != 'vir': - if isinstance(mdef.Delta, str): - return True - elif int(mdef.Delta) != 200: - return True - return False - - def _setup(self): - if self.mdef.Delta == 'vir': - self.A = 7.7 - self.B = 0.9 - self.C = -0.29 - else: # Now Delta has to be 200 - if self.mdef.rho_type == 'matter': - self.A = 9.0 - self.B = 1.15 - self.C = -0.29 - else: # Now rho_type has to be critical - self.A = 5.9 - self.B = 0.54 - self.C = -0.35 - - def _concentration(self, cosmo, M, a): - gz = growth_factor(cosmo, a) - status = 0 - delta_c, status = lib.dc_NakamuraSuto(cosmo.cosmo, a, status) - sig = sigmaM(cosmo, M, a) - nu = delta_c / sig - return self.A * gz**self.B * nu**self.C - - -class ConcentrationPrada12(Concentration): - """ Concentration-mass relation by Prada et al. 2012 - (arXiv:1104.5130). This parametrization is only valid for - S.O. masses with Delta = 200-critical. - - Args: - mdef (:class:`~pyccl.halos.massdef.MassDef`): a mass - definition object that fixes - the mass definition used by this c(M) - parametrization. - """ - name = 'Prada12' - - def __init__(self, mdef=None): - super(ConcentrationPrada12, self).__init__(mdef) - - def _default_mdef(self): - self.mdef = MassDef(200, 'critical') - - def _check_mdef(self, mdef): - if isinstance(mdef.Delta, str): - return True - elif not ((int(mdef.Delta) == 200) and - (mdef.rho_type == 'critical')): - return True - return False - - def _setup(self): - self.c0 = 3.681 - self.c1 = 5.033 - self.al = 6.948 - self.x0 = 0.424 - self.i0 = 1.047 - self.i1 = 1.646 - self.be = 7.386 - self.x1 = 0.526 - self.cnorm = 1. / self._cmin(1.393) - self.inorm = 1. / self._imin(1.393) - - def _cmin(self, x): - return self.c0 + (self.c1 - self.c0) * \ - (np.arctan(self.al * (x - self.x0)) / np.pi + 0.5) - - def _imin(self, x): - return self.i0 + (self.i1 - self.i0) * \ - (np.arctan(self.be * (x - self.x1)) / np.pi + 0.5) - - def _concentration(self, cosmo, M, a): - sig = sigmaM(cosmo, M, a) - om = cosmo.cosmo.params.Omega_m - ol = cosmo.cosmo.params.Omega_l - x = a * (ol / om)**(1. / 3.) - B0 = self._cmin(x) * self.cnorm - B1 = self._imin(x) * self.inorm - sig_p = B1 * sig - Cc = 2.881 * ((sig_p / 1.257)**1.022 + 1) * np.exp(0.060 / sig_p**2) - return B0 * Cc - - -class ConcentrationKlypin11(Concentration): - """ Concentration-mass relation by Klypin et al. 2011 - (arXiv:1002.3660). This parametrization is only valid for - S.O. masses with Delta = Delta_vir. - - Args: - mdef (:class:`~pyccl.halos.massdef.MassDef`): a mass - definition object that fixes - the mass definition used by this c(M) - parametrization. - """ - name = 'Klypin11' - - def __init__(self, mdef=None): - super(ConcentrationKlypin11, self).__init__(mdef) - - def _default_mdef(self): - self.mdef = MassDef('vir', 'critical') - - def _check_mdef(self, mdef): - if mdef.Delta != 'vir': - return True - return False - - def _concentration(self, cosmo, M, a): - M_pivot_inv = cosmo.cosmo.params.h * 1E-12 - return 9.6 * (M * M_pivot_inv)**-0.075 - - -class ConcentrationDuffy08(Concentration): - """ Concentration-mass relation by Duffy et al. 2008 - (arXiv:0804.2486). This parametrization is only valid for - S.O. masses with Delta = Delta_vir, 200-matter and 200-critical. - By default it will be initialized for Delta = 200-critical. - - Args: - mdef (:class:`~pyccl.halos.massdef.MassDef`): a mass - definition object that fixes - the mass definition used by this c(M) - parametrization. - """ - name = 'Duffy08' - - def __init__(self, mdef=None): - super(ConcentrationDuffy08, self).__init__(mdef) - - def _default_mdef(self): - self.mdef = MassDef(200, 'critical') - - def _check_mdef(self, mdef): - if mdef.Delta != 'vir': - if isinstance(mdef.Delta, str): - return True - elif int(mdef.Delta) != 200: - return True - return False - - def _setup(self): - if self.mdef.Delta == 'vir': - self.A = 7.85 - self.B = -0.081 - self.C = -0.71 - else: # Now Delta has to be 200 - if self.mdef.rho_type == 'matter': - self.A = 10.14 - self.B = -0.081 - self.C = -1.01 - else: # Now rho_type has to be critical - self.A = 5.71 - self.B = -0.084 - self.C = -0.47 - - def _concentration(self, cosmo, M, a): - M_pivot_inv = cosmo.cosmo.params.h * 5E-13 - return self.A * (M * M_pivot_inv)**self.B * a**(-self.C) - - -class ConcentrationIshiyama21(Concentration): - """ Concentration-mass relation by Ishiyama et al. 2021 - (arXiv:2007.14720). This parametrization is only valid for - S.O. masses with Delta = Delta_vir, 200-critical and 500-critical. - By default it will be initialized for Delta = 500-critical. - - Args: - mdef (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object that fixes the mass definition - used by this c(M) parametrization. - relaxed (bool): - If True, use concentration for relaxed halos. Otherwise, - use concentration for all halos. The default is False. - Vmax (bool): - If True, use the concentration found with the Vmax numerical - method. Otherwise, use the concentration found with profile - fitting. The default is False. - """ - __repr_attrs__ = ("mdef", "relaxed", "Vmax",) - name = 'Ishiyama21' - - def __init__(self, mdef=None, relaxed=False, Vmax=False): - self.relaxed = relaxed - self.Vmax = Vmax - super().__init__(mass_def=mdef) - - def _default_mdef(self): - self.mdef = MassDef(500, 'critical') - - def _check_mdef(self, mdef): - if mdef.Delta != 'vir': - if isinstance(mdef.Delta, str): - return True - elif mdef.rho_type != 'critical': - return True - elif mdef.Delta not in [200, 500]: - return True - elif (mdef.Delta == 500) and self.Vmax: - return True - return False - - def _setup(self): - if self.Vmax: # use numerical method - if self.relaxed: # fit only relaxed halos - if self.mdef.Delta == 'vir': - self.kappa = 2.40 - self.a0 = 2.27 - self.a1 = 1.80 - self.b0 = 0.56 - self.b1 = 13.24 - self.c_alpha = 0.079 - else: # now it's 200c - self.kappa = 1.79 - self.a0 = 2.15 - self.a1 = 2.06 - self.b0 = 0.88 - self.b1 = 9.24 - self.c_alpha = 0.51 - else: # fit all halos - if self.mdef.Delta == 'vir': - self.kappa = 0.76 - self.a0 = 2.34 - self.a1 = 1.82 - self.b0 = 1.83 - self.b1 = 3.52 - self.c_alpha = -0.18 - else: # now it's 200c - self.kappa = 1.10 - self.a0 = 2.30 - self.a1 = 1.64 - self.b0 = 1.72 - self.b1 = 3.60 - self.c_alpha = 0.32 - else: # use profile fitting method - if self.relaxed: # fit only relaxed halos - if self.mdef.Delta == 'vir': - self.kappa = 1.22 - self.a0 = 2.52 - self.a1 = 1.87 - self.b0 = 2.13 - self.b1 = 4.19 - self.c_alpha = -0.017 - else: # now it's either 200c or 500c - if int(self.mdef.Delta) == 200: - self.kappa = 0.60 - self.a0 = 2.14 - self.a1 = 2.63 - self.b0 = 1.69 - self.b1 = 6.36 - self.c_alpha = 0.37 - else: # now it's 500c - self.kappa = 0.38 - self.a0 = 1.44 - self.a1 = 3.41 - self.b0 = 2.86 - self.b1 = 2.99 - self.c_alpha = 0.42 - else: # fit all halos - if self.mdef.Delta == 'vir': - self.kappa = 1.64 - self.a0 = 2.67 - self.a1 = 1.23 - self.b0 = 3.92 - self.b1 = 1.30 - self.c_alpha = -0.19 - else: # now it's either 200c or 500c - if int(self.mdef.Delta) == 200: - self.kappa = 1.19 - self.a0 = 2.54 - self.a1 = 1.33 - self.b0 = 4.04 - self.b1 = 1.21 - self.c_alpha = 0.22 - else: # now it's 500c - self.kappa = 1.83 - self.a0 = 1.95 - self.a1 = 1.17 - self.b0 = 3.57 - self.b1 = 0.91 - self.c_alpha = 0.26 - - def _dlsigmaR(self, cosmo, M, a): - # kappa multiplies radius, so in log, 3*kappa multiplies mass - logM = 3*np.log10(self.kappa) + np.log10(M) - - status = 0 - dlns_dlogM, status = lib.dlnsigM_dlogM_vec(cosmo.cosmo, a, logM, - len(logM), status) - check(status, cosmo=cosmo) - return -3/np.log(10) * dlns_dlogM - - def _G(self, x, n_eff): - fx = np.log(1 + x) - x / (1 + x) - G = x / fx**((5 + n_eff) / 6) - return G - - def _G_inv(self, arg, n_eff): - # Numerical calculation of the inverse of `_G`. - roots = [] - for val, neff in zip(arg, n_eff): - func = lambda x: self._G(x, neff) - val # noqa: _G_inv Traceback - try: - rt = brentq(func, a=0.05, b=200) - except ValueError: - # No root in [0.05, 200] (rare, but it may happen). - rt = root_scalar(func, x0=1, x1=2).root.item() - roots.append(rt) - return np.asarray(roots) - - def _concentration(self, cosmo, M, a): - M_use = np.atleast_1d(M) - - nu = 1.686 / sigmaM(cosmo, M_use, a) - n_eff = -2 * self._dlsigmaR(cosmo, M_use, a) - 3 - alpha_eff = growth_rate(cosmo, a) - - A = self.a0 * (1 + self.a1 * (n_eff + 3)) - B = self.b0 * (1 + self.b1 * (n_eff + 3)) - C = 1 - self.c_alpha * (1 - alpha_eff) - arg = A / nu * (1 + nu**2 / B) - G = self._G_inv(arg, n_eff) - c = C * G - - if np.ndim(M) == 0: - c = c[0] - return c - - -class ConcentrationConstant(Concentration): - """ Constant contentration-mass relation. - - Args: - c (float): constant concentration value. - mdef (:class:`~pyccl.halos.massdef.MassDef`): a mass - definition object that fixes - the mass definition used by this c(M) - parametrization. In this case it's arbitrary. - """ - __repr_attrs__ = ("mdef", "c",) - name = 'Constant' - - def __init__(self, c=1, mdef=None): - self.c = c - super(ConcentrationConstant, self).__init__(mdef) - - def _default_mdef(self): - self.mdef = MassDef(200, 'critical') - - def _check_mdef(self, mdef): - return False - - def _concentration(self, cosmo, M, a): - if np.ndim(M) == 0: - return self.c - else: - return self.c * np.ones(M.size) - - -@functools.wraps(Concentration.from_name) -def concentration_from_name(name): - return Concentration.from_name(name) diff --git a/pyccl/halos/concentration/__init__.py b/pyccl/halos/concentration/__init__.py new file mode 100644 index 000000000..9fd3664e1 --- /dev/null +++ b/pyccl/halos/concentration/__init__.py @@ -0,0 +1,8 @@ +from ..halo_model_base import Concentration, concentration_from_name +from .bhattacharya13 import * +from .constant import * +from .diemer15 import * +from .duffy08 import * +from .ishiyama21 import * +from .klypin11 import * +from .prada12 import * diff --git a/pyccl/halos/concentration/bhattacharya13.py b/pyccl/halos/concentration/bhattacharya13.py new file mode 100644 index 000000000..d160b7598 --- /dev/null +++ b/pyccl/halos/concentration/bhattacharya13.py @@ -0,0 +1,43 @@ +from ... import ccllib as lib +from ...base import warn_api +from ..halo_model_base import Concentration + + +__all__ = ("ConcentrationBhattacharya13",) + + +class ConcentrationBhattacharya13(Concentration): + """ Concentration-mass relation by Bhattacharya et al. 2013 + (arXiv:1112.5479). This parametrization is only valid for + S.O. masses with Delta = Delta_vir, 200-matter and 200-critical. + By default it will be initialized for Delta = 200-critical. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef` or str): a mass + definition object that fixes + the mass definition used by this c(M) + parametrization, or a name string. + """ + name = 'Bhattacharya13' + + @warn_api(pairs=[("mdef", "mass_def")]) + def __init__(self, *, mass_def="200c"): + super().__init__(mass_def=mass_def) + + def _check_mass_def_strict(self, mass_def): + return mass_def.name not in ["vir", "200m", "200c"] + + def _setup(self): + vals = {"vir": (7.7, 0.9, -0.29), + "200m": (9.0, 1.15, -0.29), + "200c": (5.9, 0.54, -0.35)} + + self.A, self.B, self.C = vals[self.mass_def.name] + + def _concentration(self, cosmo, M, a): + gz = cosmo.growth_factor(a) + status = 0 + delta_c, status = lib.dc_NakamuraSuto(cosmo.cosmo, a, status) + sig = cosmo.sigmaM(M, a) + nu = delta_c / sig + return self.A * gz**self.B * nu**self.C diff --git a/pyccl/halos/concentration/constant.py b/pyccl/halos/concentration/constant.py new file mode 100644 index 000000000..4fcde451e --- /dev/null +++ b/pyccl/halos/concentration/constant.py @@ -0,0 +1,31 @@ +from ...base import warn_api +from ..halo_model_base import Concentration +import numpy as np + + +__all__ = ("ConcentrationConstant",) + + +class ConcentrationConstant(Concentration): + """ Constant contentration-mass relation. + + Args: + c (float): constant concentration value. + mass_def (:class:`~pyccl.halos.massdef.MassDef` or str): a mass + definition object that fixes + the mass definition used by this c(M) + parametrization, or a name string. In this case it's arbitrary. + """ + __repr_attrs__ = __eq_attrs__ = ("mass_def", "c",) + name = 'Constant' + + @warn_api(pairs=[("mdef", "mass_def")]) + def __init__(self, c=1, *, mass_def="200c"): + self.c = c + super().__init__(mass_def=mass_def) + + def _check_mass_def_strict(self, mass_def): + return False + + def _concentration(self, cosmo, M, a): + return np.full_like(M, self.c)[()] diff --git a/pyccl/halos/concentration/diemer15.py b/pyccl/halos/concentration/diemer15.py new file mode 100644 index 000000000..99d671229 --- /dev/null +++ b/pyccl/halos/concentration/diemer15.py @@ -0,0 +1,56 @@ +from ...base import warn_api +from ...base.parameters import physical_constants as const +from ..halo_model_base import Concentration +import numpy as np + + +__all__ = ("ConcentrationDiemer15",) + + +class ConcentrationDiemer15(Concentration): + """ Concentration-mass relation by Diemer & Kravtsov 2015 + (arXiv:1407.4730). This parametrization is only valid for + S.O. masses with Delta = 200-critical. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef` or str): + a mass definition object that fixes + the mass definition used by this c(M) + parametrization, or a name string. + """ + name = 'Diemer15' + + @warn_api(pairs=[("mdef", "mass_def")]) + def __init__(self, *, mass_def="200c"): + super().__init__(mass_def=mass_def) + + def _setup(self): + self.kappa = 1.0 + self.phi_0 = 6.58 + self.phi_1 = 1.27 + self.eta_0 = 7.28 + self.eta_1 = 1.56 + self.alpha = 1.08 + self.beta = 1.77 + + def _check_mass_def_strict(self, mass_def): + return mass_def.name != "200c" + + def _concentration(self, cosmo, M, a): + # Compute power spectrum slope + R = cosmo.mass2radius_lagrangian(M) + k_R = 2.0 * np.pi / R * self.kappa + + cosmo.compute_linear_power() + pk = cosmo.get_linear_power() + n = pk(k_R, a, derivative=True) + + sig = cosmo.sigmaM(M, a) + delta_c = const.DELTA_C + nu = delta_c / sig + + floor = self.phi_0 + n * self.phi_1 + nu0 = self.eta_0 + n * self.eta_1 + c = 0.5 * floor * ((nu0 / nu)**self.alpha + + (nu / nu0)**self.beta) + return c diff --git a/pyccl/halos/concentration/duffy08.py b/pyccl/halos/concentration/duffy08.py new file mode 100644 index 000000000..0485b1e67 --- /dev/null +++ b/pyccl/halos/concentration/duffy08.py @@ -0,0 +1,38 @@ +from ...base import warn_api +from ..halo_model_base import Concentration + + +__all__ = ("ConcentrationDuffy08",) + + +class ConcentrationDuffy08(Concentration): + """ Concentration-mass relation by Duffy et al. 2008 + (arXiv:0804.2486). This parametrization is only valid for + S.O. masses with Delta = Delta_vir, 200-matter and 200-critical. + By default it will be initialized for Delta = 200-critical. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef` or str): a mass + definition object that fixes + the mass definition used by this c(M) + parametrization, or a name string. + """ + name = 'Duffy08' + + @warn_api(pairs=[("mdef", "mass_def")]) + def __init__(self, *, mass_def="200c"): + super().__init__(mass_def=mass_def) + + def _check_mass_def_strict(self, mass_def): + return mass_def.name not in ["vir", "200m", "200c"] + + def _setup(self): + vals = {"vir": (7.85, -0.081, -0.71), + "200m": (10.14, -0.081, -1.01), + "200c": (5.71, -0.084, -0.47)} + + self.A, self.B, self.C = vals[self.mass_def.name] + + def _concentration(self, cosmo, M, a): + M_pivot_inv = cosmo["h"] * 5E-13 + return self.A * (M * M_pivot_inv)**self.B * a**(-self.C) diff --git a/pyccl/halos/concentration/ishiyama21.py b/pyccl/halos/concentration/ishiyama21.py new file mode 100644 index 000000000..916fa978a --- /dev/null +++ b/pyccl/halos/concentration/ishiyama21.py @@ -0,0 +1,100 @@ +from ... import ccllib as lib +from ...base import warn_api +from ...base.parameters import physical_constants as const +from ...pyutils import check +from ..halo_model_base import Concentration +import numpy as np +from scipy.optimize import brentq, root_scalar + + +__all__ = ("ConcentrationIshiyama21",) + + +class ConcentrationIshiyama21(Concentration): + """ Concentration-mass relation by Ishiyama et al. 2021 + (arXiv:2007.14720). This parametrization is only valid for + S.O. masses with Delta = Delta_vir, 200-critical and 500-critical. + By default it will be initialized for Delta = 500-critical. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef` or str): + a mass definition object that fixes the mass definition + used by this c(M) parametrization, or a name string. + relaxed (bool): + If True, use concentration for relaxed halos. Otherwise, + use concentration for all halos. The default is False. + Vmax (bool): + If True, use the concentration found with the Vmax numerical + method. Otherwise, use the concentration found with profile + fitting. The default is False. + """ + __repr_attrs__ = __eq_attrs__ = ("mass_def", "relaxed", "Vmax",) + name = 'Ishiyama21' + + @warn_api(pairs=[("mdef", "mass_def")]) + def __init__(self, *, mass_def="500c", + relaxed=False, Vmax=False): + self.relaxed = relaxed + self.Vmax = Vmax + super().__init__(mass_def=mass_def) + + def _check_mass_def_strict(self, mass_def): + is_500Vmax = mass_def.Delta == 500 and self.Vmax + return mass_def.name not in ["vir", "200c", "500c"] or is_500Vmax + + def _setup(self): + # key: (Vmax, relaxed, Delta) + vals = {(True, True, 200): (1.79, 2.15, 2.06, 0.88, 9.24, 0.51), + (True, False, 200): (1.10, 2.30, 1.64, 1.72, 3.60, 0.32), + (False, True, 200): (0.60, 2.14, 2.63, 1.69, 6.36, 0.37), + (False, False, 200): (1.19, 2.54, 1.33, 4.04, 1.21, 0.22), + (True, True, "vir"): (2.40, 2.27, 1.80, 0.56, 13.24, 0.079), + (True, False, "vir"): (0.76, 2.34, 1.82, 1.83, 3.52, -0.18), + (False, True, "vir"): (1.22, 2.52, 1.87, 2.13, 4.19, -0.017), + (False, False, "vir"): (1.64, 2.67, 1.23, 3.92, 1.30, -0.19), + (False, True, 500): (0.38, 1.44, 3.41, 2.86, 2.99, 0.42), + (False, False, 500): (1.83, 1.95, 1.17, 3.57, 0.91, 0.26)} + + key = (self.Vmax, self.relaxed, self.mass_def.Delta) + self.kappa, self.a0, self.a1, \ + self.b0, self.b1, self.c_alpha = vals[key] + + def _dlsigmaR(self, cosmo, M, a): + # kappa multiplies radius, so in log, 3*kappa multiplies mass + logM = 3*np.log10(self.kappa) + np.log10(M) + + status = 0 + dlns_dlogM, status = lib.dlnsigM_dlogM_vec(cosmo.cosmo, a, logM, + len(logM), status) + check(status, cosmo=cosmo) + return -3/np.log(10) * dlns_dlogM + + def _G(self, x, n_eff): + fx = np.log(1 + x) - x / (1 + x) + G = x / fx**((5 + n_eff) / 6) + return G + + def _G_inv(self, arg, n_eff): + # Numerical calculation of the inverse of `_G`. + roots = [] + for val, neff in zip(arg, n_eff): + func = lambda x: self._G(x, neff) - val # noqa: _G_inv Traceback + try: + rt = brentq(func, a=0.05, b=200) + except ValueError: + # No root in [0.05, 200] (rare, but it may happen). + rt = root_scalar(func, x0=1, x1=2).root.item() + roots.append(rt) + return np.asarray(roots) + + def _concentration(self, cosmo, M, a): + nu = const.DELTA_C / cosmo.sigmaM(M, a) + n_eff = -2 * self._dlsigmaR(cosmo, M, a) - 3 + alpha_eff = cosmo.growth_rate(a) + + A = self.a0 * (1 + self.a1 * (n_eff + 3)) + B = self.b0 * (1 + self.b1 * (n_eff + 3)) + C = 1 - self.c_alpha * (1 - alpha_eff) + arg = A / nu * (1 + nu**2 / B) + G = self._G_inv(arg, n_eff) + return C * G diff --git a/pyccl/halos/concentration/klypin11.py b/pyccl/halos/concentration/klypin11.py new file mode 100644 index 000000000..1fa4469a6 --- /dev/null +++ b/pyccl/halos/concentration/klypin11.py @@ -0,0 +1,30 @@ +from ...base import warn_api +from ..halo_model_base import Concentration + + +__all__ = ("ConcentrationKlypin11",) + + +class ConcentrationKlypin11(Concentration): + """ Concentration-mass relation by Klypin et al. 2011 + (arXiv:1002.3660). This parametrization is only valid for + S.O. masses with Delta = Delta_vir. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef` or str): a mass + definition object that fixes + the mass definition used by this c(M) + parametrization, or a name string. + """ + name = 'Klypin11' + + @warn_api(pairs=[("mdef", "mass_def")]) + def __init__(self, *, mass_def="vir"): + super().__init__(mass_def=mass_def) + + def _check_mass_def_strict(self, mass_def): + return mass_def.name != "vir" + + def _concentration(self, cosmo, M, a): + M_pivot_inv = cosmo["h"] * 1E-12 + return 9.6 * (M * M_pivot_inv)**(-0.075) diff --git a/pyccl/halos/concentration/prada12.py b/pyccl/halos/concentration/prada12.py new file mode 100644 index 000000000..d789ee33e --- /dev/null +++ b/pyccl/halos/concentration/prada12.py @@ -0,0 +1,58 @@ +from ...base import warn_api +from ..halo_model_base import Concentration +import numpy as np + + +__all__ = ("ConcentrationPrada12",) + + +class ConcentrationPrada12(Concentration): + """ Concentration-mass relation by Prada et al. 2012 + (arXiv:1104.5130). This parametrization is only valid for + S.O. masses with Delta = 200-critical. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef` or str): a mass + definition object that fixes + the mass definition used by this c(M) + parametrization, or a name string. + """ + name = 'Prada12' + + @warn_api(pairs=[("mdef", "mass_def")]) + def __init__(self, *, mass_def="200c"): + super().__init__(mass_def=mass_def) + + def _check_mass_def_strict(self, mass_def): + return mass_def.name != "200c" + + def _setup(self): + self.c0 = 3.681 + self.c1 = 5.033 + self.al = 6.948 + self.x0 = 0.424 + self.i0 = 1.047 + self.i1 = 1.646 + self.be = 7.386 + self.x1 = 0.526 + self.cnorm = 1. / self._cmin(1.393) + self.inorm = 1. / self._imin(1.393) + + def _form(self, x, x0, v0, v1, v2): + # form factor for `cmin` and `imin` + return v0 + (v1 - v0) * (np.arctan(v2 * (x - x0)) / np.pi + 0.5) + + def _cmin(self, x): + return self._form(x, x0=self.x0, v0=self.c0, v1=self.c1, v2=self.al) + + def _imin(self, x): + return self._form(x, x0=self.x1, v0=self.i0, v1=self.i1, v2=self.be) + + def _concentration(self, cosmo, M, a): + sig = cosmo.sigmaM(M, a) + x = a * (cosmo["Omega_l"] / cosmo["Omega_m"])**(1. / 3.) + B0 = self._cmin(x) * self.cnorm + B1 = self._imin(x) * self.inorm + sig_p = B1 * sig + Cc = 2.881 * ((sig_p / 1.257)**1.022 + 1) * np.exp(0.060 / sig_p**2) + return B0 * Cc diff --git a/pyccl/halos/halo_model.py b/pyccl/halos/halo_model.py index 107d69d45..126745849 100644 --- a/pyccl/halos/halo_model.py +++ b/pyccl/halos/halo_model.py @@ -1,23 +1,17 @@ -import warnings -from .. import ccllib as lib from .massdef import MassDef from .hmfunc import MassFunc from .hbias import HaloBias -from .profiles import HaloProfile, HaloProfileNFW -from .profiles_2pt import Profile2pt -from ..core import check -from ..pk2d import Pk2D -from ..tk3d import Tk3D -from ..power import linear_matter_power, nonlin_matter_power from ..pyutils import _spline_integrate -from .. import background -from ..errors import CCLWarning -from ..base import CCLHalosObject, unlock_instance -from ..parameters import physical_constants as const +from ..base import (CCLAutoRepr, unlock_instance, + warn_api, deprecate_attr, deprecated) +from ..base.parameters import physical_constants as const import numpy as np -class HMCalculator(CCLHalosObject): +__all__ = ("HMCalculator",) + + +class HMCalculator(CCLAutoRepr): """This class implements a set of methods that can be used to compute various halo model quantities. A lot of these quantities will involve integrals of the sort: @@ -29,100 +23,83 @@ class HMCalculator(CCLHalosObject): an arbitrary function of mass, scale factor and Fourier scales. Args: - cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. - massfunc (:class:`~pyccl.halos.hmfunc.MassFunc`): a mass - function object. - hbias (:class:`~pyccl.halos.hbias.HaloBias`): a halo bias - object. - mass_def (:class:`~pyccl.halos.massdef.MassDef`): a mass - definition object. - log10M_min (float): logarithmic mass (in units of solar mass) - corresponding to the lower bound of the integrals in - mass. Default: 8. - log10M_max (float): logarithmic mass (in units of solar mass) - corresponding to the upper bound of the integrals in - mass. Default: 16. - nlog10M (int): number of samples in log(Mass) to be used in - the mass integrals. Default: 128. + mass_function (str or :class:`~pyccl.halos.hmfunc.MassFunc`): + the mass function to use + halo_bias (str or :class:`~pyccl.halos.hbias.HaloBias`): + the halo bias function to use + mass_def (str or :class:`~pyccl.halos.massdef.MassDef`): + the halo mass definition to use + log10M_min, log10M_max (float): lower and upper integration bounds + of logarithmic (base-10) mass (in units of solar mass). + Default range: 8, 16. + nM (int): number of uniformly-spaced samples in log(Mass) + to be used in the mass integrals. Default: 128. integration_method_M (string): integration method to use in the mass integrals. Options: "simpson" and "spline". Default: "simpson". - k_min (float): some of the integrals solved by this class + k_min (float): Deprecated - do not use + some of the integrals solved by this class will often be normalized by their value on very large scales. This parameter (in units of inverse Mpc) determines what is considered a "very large" scale. Default: 1E-5. """ - __repr_attrs__ = ("_massfunc", "_hbias", "_mdef", "_prec",) - - def __init__(self, cosmo, massfunc, hbias, mass_def, - log10M_min=8., log10M_max=16., - nlog10M=128, integration_method_M='simpson', - k_min=1E-5): - # halo mass definition - if isinstance(mass_def, MassDef): - self._mdef = mass_def - elif isinstance(mass_def, str): - self._mdef = MassDef.from_name(mass_def)() - else: - raise TypeError("mass_def must be of type `MassDef` " - "or a mass definition name string") - - # halo mass function - if isinstance(massfunc, MassFunc): - self._massfunc = massfunc - elif isinstance(massfunc, str): - nMclass = MassFunc.from_name(massfunc) - self._massfunc = nMclass(cosmo, mass_def=self._mdef) - else: - raise TypeError("mass_function must be of type `MassFunc` " - "or a mass function name string") - - # halo bias function - if isinstance(hbias, HaloBias): - self._hbias = hbias - elif isinstance(hbias, str): - bMclass = HaloBias.from_name(hbias) - self._hbias = bMclass(cosmo, mass_def=self._mdef) - else: - raise TypeError("halo_bias must be of type `HaloBias` " - "or a halo bias name string") + __repr_attrs__ = __eq_attrs__ = ( + "mass_function", "halo_bias", "mass_def", "precision",) + __getattr__ = deprecate_attr(pairs=[('_mdef', 'mass_def'), + ('_massfunc', 'mass_function'), + ('_hbias', 'halo_bias'), + ('_prec', 'precision')] + )(super.__getattribute__) + + @warn_api(pairs=[("massfunc", "mass_function"), ("hbias", "halo_bias"), + ("nlog10M", "nM")]) + def __init__(self, *, mass_function, halo_bias, mass_def, + log10M_min=8., log10M_max=16., nM=128, + integration_method_M='simpson', k_min=1E-5): + # Initialize halo model ingredients + self.mass_def = MassDef.create_instance(mass_def) + kw = {"mass_def": self.mass_def} + self.mass_function = MassFunc.create_instance(mass_function, **kw) + self.halo_bias = HaloBias.create_instance(halo_bias, **kw) + + # Check mass definition consistency. + if not (self.mass_def + == self.mass_function.mass_def + == self.halo_bias.mass_def): + raise ValueError( + "HMCalculator received different mass definitions " + "in mass_def, mass_function, halo_bias.") - self._prec = {'log10M_min': log10M_min, - 'log10M_max': log10M_max, - 'nlog10M': nlog10M, - 'integration_method_M': integration_method_M, - 'k_min': k_min} - self._lmass = np.linspace(self._prec['log10M_min'], - self._prec['log10M_max'], - self._prec['nlog10M']) + self.precision = { + 'log10M_min': log10M_min, 'log10M_max': log10M_max, 'nM': nM, + 'integration_method_M': integration_method_M, 'k_min': k_min} + self._lmass = np.linspace(log10M_min, log10M_max, nM) self._mass = 10.**self._lmass self._m0 = self._mass[0] - if self._prec['integration_method_M'] not in ['spline', - 'simpson']: - raise NotImplementedError("Only \'simpson\' and 'spline' " - "supported as integration methods") - elif self._prec['integration_method_M'] == 'simpson': - from scipy.integrate import simps - self._integrator = simps - else: + if integration_method_M == "simpson": + from scipy.integrate import simpson + self._integrator = simpson + elif integration_method_M == "spline": self._integrator = self._integ_spline + else: + raise NotImplementedError( + "Only 'simpson' and 'spline integration is supported.") # Cache last results for mass function and halo bias. self._cosmo_mf = self._cosmo_bf = None self._a_mf = self._a_bf = -1 - def _integ_spline(self, fM, lM): + def _integ_spline(self, fM, log10M): # Spline integrator - return _spline_integrate(lM, fM, lM[0], lM[-1]) + return _spline_integrate(log10M, fM, log10M[0], log10M[-1]) @unlock_instance(mutate=False) def _get_mass_function(self, cosmo, a, rho0): # Compute the mass function at this cosmo and a. if a != self._a_mf or cosmo != self._cosmo_mf: - massfunc = self._massfunc.get_mass_function - self._mf = massfunc(cosmo, self._mass, a) + self._mf = self.mass_function(cosmo, self._mass, a) integ = self._integrator(self._mf*self._mass, self._lmass) self._mf0 = (rho0 - integ) / self._m0 self._cosmo_mf, self._a_mf = cosmo, a # cache @@ -130,14 +107,13 @@ def _get_mass_function(self, cosmo, a, rho0): @unlock_instance(mutate=False) def _get_halo_bias(self, cosmo, a, rho0): # Compute the halo bias at this cosmo and a. - if cosmo != self._cosmo_bf or a != self._a_bf: - hbias = self._hbias.get_halo_bias - self._bf = hbias(cosmo, self._mass, a) + if a != self._a_bf or cosmo != self._cosmo_bf: + self._bf = self.halo_bias(cosmo, self._mass, a) integ = self._integrator(self._mf*self._bf*self._mass, self._lmass) self._mbf0 = (rho0 - integ) / self._m0 self._cosmo_bf, self._a_bf = cosmo, a # cache - def _get_ingredients(self, cosmo, a, get_bf): + def _get_ingredients(self, cosmo, a, *, get_bf): """Compute mass function and halo bias at some scale factor.""" rho0 = const.RHO_CRITICAL * cosmo["Omega_m"] * cosmo["h"]**2 self._get_mass_function(cosmo, a, rho0) @@ -145,15 +121,16 @@ def _get_ingredients(self, cosmo, a, get_bf): self._get_halo_bias(cosmo, a, rho0) def _integrate_over_mf(self, array_2): - i1 = self._integrator(self._mf[..., :] * array_2, - self._lmass) + # ∫ dM n(M) f(M) + i1 = self._integrator(self._mf * array_2, self._lmass) return i1 + self._mf0 * array_2[..., 0] def _integrate_over_mbf(self, array_2): - i1 = self._integrator((self._mf * self._bf)[..., :] * array_2, - self._lmass) + # ∫ dM n(M) b(M) f(M) + i1 = self._integrator(self._mf * self._bf * array_2, self._lmass) return i1 + self._mbf0 * array_2[..., 0] + @deprecated() def profile_norm(self, cosmo, a, prof): """ Returns :math:`I^0_1(k\\rightarrow0,a|u)` (see :meth:`~HMCalculator.I_0_1`). @@ -167,14 +144,27 @@ def profile_norm(self, cosmo, a, prof): Returns: float or array_like: integral value. """ - # Compute mass function - self._get_ingredients(cosmo, a, False) - uk0 = prof.fourier(cosmo, self._prec['k_min'], - self._mass, a, mass_def=self._mdef).T - norm = 1. / self._integrate_over_mf(uk0) - return norm - - def number_counts(self, cosmo, sel, na=128, amin=None, amax=1.0): + self._get_ingredients(cosmo, a, get_bf=False) + uk0 = prof.fourier(cosmo, self.precision['k_min'], + self._mass, a, mass_def=self.mass_def).T + return 1. / self._integrate_over_mf(uk0) + + def get_profile_norm(self, cosmo, a, prof): + """Compute the normalization of a profile.""" + if not prof.normprof: # TODO: Remove for CCLv3. + return 1 + uk0 = prof._normalization(self)(cosmo=cosmo, a=a) + if isinstance(uk0, (int, float)): + return 1 / uk0 + self._get_ingredients(cosmo, a, get_bf=False) + return 1 / self._integrate_over_mf(uk0) + + @warn_api(pairs=[("sel", "selection"), + ("amin", "a_min"), + ("amax", "a_max")], + reorder=["na", "a_min", "a_max"]) + def number_counts(self, cosmo, *, selection, + a_min=None, a_max=1.0, na=128): """ Solves the integral: .. math:: @@ -190,50 +180,43 @@ def number_counts(self, cosmo, sel, na=128, amin=None, amax=1.0): Args: cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. - sel (callable): function of mass and scale factor that returns the - selection function. This function should take in floats or arrays - with a signature ``sel(m, a)`` and return an array with shape - ``(len(m), len(a))`` according to the numpy broadcasting rules. - na (int): number of samples in scale factor to be used in - the integrals. Default: 128. - amin (float): the minimum scale factor at which to start integrals + selection (callable): function of mass and scale factor + that returns the selection function. This function + should take in floats or arrays with a signature ``sel(m, a)`` + and return an array with shape ``(len(m), len(a))`` according + to the numpy broadcasting rules. + a_min (float): the minimum scale factor at which to start integrals over the selection function. Default: value of ``cosmo.cosmo.spline_params.A_SPLINE_MIN`` - amax (float): the maximum scale factor at which to end integrals + a_max (float): the maximum scale factor at which to end integrals over the selection function. Default: 1.0 + na (int): number of samples in scale factor to be used in + the integrals. Default: 128. Returns: float: the total number of clusters """ # noqa - # get a values for integral - if amin is None: - amin = cosmo.cosmo.spline_params.A_SPLINE_MIN - a = np.linspace(amin, amax, na) + if a_min is None: + a_min = cosmo.cosmo.spline_params.A_SPLINE_MIN + a = np.linspace(a_min, a_max, na) # compute the volume element - abs_dzda = 1 / a / a - dc = background.comoving_angular_distance(cosmo, a) - ez = background.h_over_h0(cosmo, a) - dh = const.CLIGHT_HMPC / cosmo['h'] - dvdz = dh * dc**2 / ez - dvda = dvdz * abs_dzda + dVda = cosmo.comoving_volume_element(a) # now do m intergrals in a loop mint = np.zeros_like(a) for i, _a in enumerate(a): - self._get_ingredients(cosmo, _a, False) - _selm = np.atleast_2d(sel(self._mass, _a)).T + self._get_ingredients(cosmo, _a, get_bf=False) + _selm = np.atleast_2d(selection(self._mass, _a)).T mint[i] = self._integrator( - dvda[i] * self._mf[..., :] * _selm[..., :], + dVda[i] * self._mf[..., :] * _selm[..., :], self._lmass ) # now do scale factor integral - mtot = self._integrator(mint, a) - - return mtot + return self._integrator(mint, a) def I_0_1(self, cosmo, k, a, prof): """ Solves the integral: @@ -256,12 +239,9 @@ def I_0_1(self, cosmo, k, a, prof): float or array_like: integral values evaluated at each value of `k`. """ - # Compute mass function - self._get_ingredients(cosmo, a, False) - uk = prof.fourier(cosmo, k, self._mass, a, - mass_def=self._mdef).T - i01 = self._integrate_over_mf(uk) - return i01 + self._get_ingredients(cosmo, a, get_bf=False) + uk = prof.fourier(cosmo, k, self._mass, a, mass_def=self.mass_def).T + return self._integrate_over_mf(uk) def I_1_1(self, cosmo, k, a, prof): """ Solves the integral: @@ -286,14 +266,12 @@ def I_1_1(self, cosmo, k, a, prof): float or array_like: integral values evaluated at each value of `k`. """ - # Compute mass function and halo bias - self._get_ingredients(cosmo, a, True) - uk = prof.fourier(cosmo, k, self._mass, a, - mass_def=self._mdef).T - i11 = self._integrate_over_mbf(uk) - return i11 + self._get_ingredients(cosmo, a, get_bf=True) + uk = prof.fourier(cosmo, k, self._mass, a, mass_def=self.mass_def).T + return self._integrate_over_mbf(uk) - def I_0_2(self, cosmo, k, a, prof1, prof_2pt, prof2=None): + @warn_api(pairs=[("prof1", "prof")], reorder=["prof_2pt", "prof2"]) + def I_0_2(self, cosmo, k, a, prof, *, prof2=None, prof_2pt): """ Solves the integral: .. math:: @@ -308,32 +286,32 @@ def I_0_2(self, cosmo, k, a, prof1, prof_2pt, prof2=None): cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. k (float or array_like): comoving wavenumber in Mpc^-1. a (float): scale factor. - prof1 (:class:`~pyccl.halos.profiles.HaloProfile`): halo + prof (:class:`~pyccl.halos.profiles.HaloProfile`): halo profile. + prof2 (:class:`~pyccl.halos.profiles.HaloProfile`): a + second halo profile. If `None`, `prof` will be used as prof_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): a profile covariance object returning the the two-point moment of the two profiles being correlated. - prof2 (:class:`~pyccl.halos.profiles.HaloProfile` or None): - a second halo profile. If `None`, `prof1` will be used as - `prof2`. + prof_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): + a profile covariance object returning the the two-point + moment of the two profiles being correlated. Returns: float or array_like: integral values evaluated at each value of `k`. """ if prof2 is None: - prof2 = prof1 + prof2 = prof - # Compute mass function - self._get_ingredients(cosmo, a, False) - uk = prof_2pt.fourier_2pt(prof1, cosmo, k, self._mass, a, - prof2=prof2, - mass_def=self._mdef).T - i02 = self._integrate_over_mf(uk) - return i02 + self._get_ingredients(cosmo, a, get_bf=False) + uk = prof_2pt.fourier_2pt(cosmo, k, self._mass, a, prof, + prof2=prof2, mass_def=self.mass_def).T + return self._integrate_over_mf(uk) - def I_1_2(self, cosmo, k, a, prof1, prof_2pt, prof2=None): + @warn_api(pairs=[("prof1", "prof")], reorder=["prof_2pt", "prof2"]) + def I_1_2(self, cosmo, k, a, prof, *, prof2=None, prof_2pt): """ Solves the integral: .. math:: @@ -349,34 +327,33 @@ def I_1_2(self, cosmo, k, a, prof1, prof_2pt, prof2=None): cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. k (float or array_like): comoving wavenumber in Mpc^-1. a (float): scale factor. - prof1 (:class:`~pyccl.halos.profiles.HaloProfile`): halo + prof (:class:`~pyccl.halos.profiles.HaloProfile`): halo profile. + prof2 (:class:`~pyccl.halos.profiles.HaloProfile`): a + second halo profile. If `None`, `prof` will be used as + `prof2`. prof_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): a profile covariance object returning the the two-point moment of the two profiles being correlated. - prof2 (:class:`~pyccl.halos.profiles.HaloProfile`): a - second halo profile. If `None`, `prof1` will be used as - `prof2`. Returns: float or array_like: integral values evaluated at each value of `k`. """ if prof2 is None: - prof2 = prof1 - - # Compute mass function - self._get_ingredients(cosmo, a, True) - uk = prof_2pt.fourier_2pt(prof1, cosmo, k, self._mass, a, - prof2=prof2, - mass_def=self._mdef).T - i02 = self._integrate_over_mbf(uk) - return i02 - - def I_0_22(self, cosmo, k, a, - prof1, prof12_2pt, prof2=None, - prof3=None, prof34_2pt=None, prof4=None): + prof2 = prof + + self._get_ingredients(cosmo, a, get_bf=True) + uk = prof_2pt.fourier_2pt(cosmo, k, self._mass, a, prof, + prof2=prof2, mass_def=self.mass_def).T + return self._integrate_over_mbf(uk) + + @warn_api(pairs=[("prof1", "prof")], + reorder=["prof12_2pt", "prof2", "prof3", "prof34_2pt", "prof4"]) + def I_0_22(self, cosmo, k, a, prof, *, + prof2=None, prof3=None, prof4=None, + prof12_2pt, prof34_2pt=None): """ Solves the integral: .. math:: @@ -393,1047 +370,47 @@ def I_0_22(self, cosmo, k, a, cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. k (float or array_like): comoving wavenumber in Mpc^-1. a (float): scale factor. - prof1 (:class:`~pyccl.halos.profiles.HaloProfile`): halo + prof (:class:`~pyccl.halos.profiles.HaloProfile`): halo profile. - prof12_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): - a profile covariance object returning the the - two-point moment of `prof1` and `prof2`. prof2 (:class:`~pyccl.halos.profiles.HaloProfile`): a - second halo profile. If `None`, `prof1` will be used as + second halo profile. If `None`, `prof` will be used as `prof2`. prof3 (:class:`~pyccl.halos.profiles.HaloProfile`): a - third halo profile. If `None`, `prof1` will be used as + third halo profile. If `None`, `prof` will be used as `prof3`. - prof34_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): - a profile covariance object returning the the - two-point moment of `prof3` and `prof4`. prof4 (:class:`~pyccl.halos.profiles.HaloProfile`): a fourth halo profile. If `None`, `prof2` will be used as `prof4`. + prof12_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): + a profile covariance object returning the the + two-point moment of `prof` and `prof2`. + prof34_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): + a profile covariance object returning the the + two-point moment of `prof3` and `prof4`. Returns: float or array_like: integral values evaluated at each value of `k`. """ if prof3 is None: - prof3 = prof1 + prof3 = prof if prof4 is None: prof4 = prof2 if prof34_2pt is None: prof34_2pt = prof12_2pt - self._get_ingredients(cosmo, a, False) - uk12 = prof12_2pt.fourier_2pt(prof1, cosmo, k, self._mass, a, - prof2=prof2, mass_def=self._mdef).T - if (prof1, prof2) == (prof3, prof4): + self._get_ingredients(cosmo, a, get_bf=False) + uk12 = prof12_2pt.fourier_2pt( + cosmo, k, self._mass, a, prof, + prof2=prof2, mass_def=self.mass_def).T + + if (prof, prof2, prof12_2pt) == (prof3, prof4, prof34_2pt): # 4pt approximation of the same profile uk34 = uk12 else: - uk34 = prof34_2pt.fourier_2pt(prof3, cosmo, k, self._mass, a, - prof2=prof4, mass_def=self._mdef).T - i04 = self._integrate_over_mf(uk12[None, :, :] * uk34[:, None, :]) - return i04 - - -def halomod_mean_profile_1pt(cosmo, hmc, k, a, prof, - normprof=False): - """ Returns the mass-weighted mean halo profile. - - .. math:: - I^0_1(k,a|u) = \\int dM\\,n(M,a)\\,\\langle u(k,a|M)\\rangle, - - where :math:`n(M,a)` is the halo mass function, and - :math:`\\langle u(k,a|M)\\rangle` is the halo profile as a - function of scale, scale factor and halo mass. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. - hmc (:class:`HMCalculator`): a halo model calculator. - k (float or array_like): comoving wavenumber in Mpc^-1. - a (float or array_like): scale factor. - prof (:class:`~pyccl.halos.profiles.HaloProfile`): halo - profile. - normprof (bool): if `True`, this integral will be - normalized by :math:`I^0_1(k\\rightarrow 0,a|u)`. - - Returns: - float or array_like: integral values evaluated at each - combination of `k` and `a`. The shape of the output will - be `(N_a, N_k)` where `N_k` and `N_a` are the sizes of - `k` and `a` respectively. If `k` or `a` are scalars, the - corresponding dimension will be squeezed out on output. - """ - a_use = np.atleast_1d(a).astype(float) - k_use = np.atleast_1d(k).astype(float) - - # Check inputs - if not isinstance(prof, HaloProfile): - raise TypeError("prof must be of type `HaloProfile`") - - na = len(a_use) - nk = len(k_use) - out = np.zeros([na, nk]) - for ia, aa in enumerate(a_use): - i01 = hmc.I_0_1(cosmo, k_use, aa, prof) - if normprof: - norm = hmc.profile_norm(cosmo, aa, prof) - i01 *= norm - out[ia, :] = i01 - - if np.ndim(a) == 0: - out = np.squeeze(out, axis=0) - if np.ndim(k) == 0: - out = np.squeeze(out, axis=-1) - return out - - -def halomod_bias_1pt(cosmo, hmc, k, a, prof, normprof=False): - """ Returns the mass-and-bias-weighted mean halo profile. - - .. math:: - I^1_1(k,a|u) = \\int dM\\,n(M,a)\\,b(M,a)\\, - \\langle u(k,a|M)\\rangle, - - where :math:`n(M,a)` is the halo mass function, - :math:`b(M,a)` is the halo bias, and - :math:`\\langle u(k,a|M)\\rangle` is the halo profile as a - function of scale, scale factor and halo mass. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. - hmc (:class:`HMCalculator`): a halo model calculator. - k (float or array_like): comoving wavenumber in Mpc^-1. - a (float or array_like): scale factor. - prof (:class:`~pyccl.halos.profiles.HaloProfile`): halo - profile. - normprof (bool): if `True`, this integral will be - normalized by :math:`I^0_1(k\\rightarrow 0,a|u)` - (see :meth:`~HMCalculator.I_0_1`). - - Returns: - float or array_like: integral values evaluated at each - combination of `k` and `a`. The shape of the output will - be `(N_a, N_k)` where `N_k` and `N_a` are the sizes of - `k` and `a` respectively. If `k` or `a` are scalars, the - corresponding dimension will be squeezed out on output. - """ - a_use = np.atleast_1d(a).astype(float) - k_use = np.atleast_1d(k).astype(float) - - # Check inputs - if not isinstance(prof, HaloProfile): - raise TypeError("prof must be of type `HaloProfile`") - - na = len(a_use) - nk = len(k_use) - out = np.zeros([na, nk]) - for ia, aa in enumerate(a_use): - i11 = hmc.I_1_1(cosmo, k_use, aa, prof) - if normprof: - norm = hmc.profile_norm(cosmo, aa, prof) - i11 *= norm - out[ia, :] = i11 - - if np.ndim(a) == 0: - out = np.squeeze(out, axis=0) - if np.ndim(k) == 0: - out = np.squeeze(out, axis=-1) - return out - - -def halomod_power_spectrum(cosmo, hmc, k, a, prof, - prof_2pt=None, prof2=None, p_of_k_a=None, - normprof1=False, normprof2=False, - get_1h=True, get_2h=True, - smooth_transition=None, supress_1h=None): - """ Computes the halo model power spectrum for two - quantities defined by their respective halo profiles. - The halo model power spectrum for two profiles - :math:`u` and :math:`v` is: - - .. math:: - P_{u,v}(k,a) = I^0_2(k,a|u,v) + - I^1_1(k,a|u)\\,I^1_1(k,a|v)\\,P_{\\rm lin}(k,a) - - where :math:`P_{\\rm lin}(k,a)` is the linear matter - power spectrum, :math:`I^1_1` is defined in the documentation - of :meth:`~HMCalculator.I_1_1`, and :math:`I^0_2` is defined - in the documentation of :meth:`~HMCalculator.I_0_2`. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. - hmc (:class:`HMCalculator`): a halo model calculator. - k (float or array_like): comoving wavenumber in Mpc^-1. - a (float or array_like): scale factor. - prof (:class:`~pyccl.halos.profiles.HaloProfile`): halo - profile. - prof_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): - a profile covariance object - returning the the two-point moment of the two profiles - being correlated. If `None`, the default second moment - will be used, corresponding to the products of the means - of both profiles. - prof2 (:class:`~pyccl.halos.profiles.HaloProfile`): a - second halo profile. If `None`, `prof` will be used as - `prof2`. - p_of_k_a (:class:`~pyccl.pk2d.Pk2D`): a `Pk2D` object to - be used as the linear matter power spectrum. If `None`, - the power spectrum stored within `cosmo` will be used. - normprof1 (bool): if `True`, this integral will be - normalized by :math:`I^0_1(k\\rightarrow 0,a|u)` - (see :meth:`~HMCalculator.I_0_1`), where - :math:`u` is the profile represented by `prof`. - normprof2 (bool): if `True`, this integral will be - normalized by :math:`I^0_1(k\\rightarrow 0,a|v)` - (see :meth:`~HMCalculator.I_0_1`), where - :math:`v` is the profile represented by `prof2`. - get_1h (bool): if `False`, the 1-halo term (i.e. the first - term in the first equation above) won't be computed. - get_2h (bool): if `False`, the 2-halo term (i.e. the second - term in the first equation above) won't be computed. - smooth_transition (function or None): - Modify the halo model 1-halo/2-halo transition region - via a time-dependent function :math:`\\alpha(a)`, - defined as in HMCODE-2020 (``arXiv:2009.01858``): :math:`P(k,a)= - (P_{1h}^{\\alpha(a)}(k)+P_{2h}^{\\alpha(a)}(k))^{1/\\alpha}`. - If `None` the extra factor is just 1. - supress_1h (function or None): - Supress the 1-halo large scale contribution by a - time- and scale-dependent function :math:`k_*(a)`, - defined as in HMCODE-2020 (``arXiv:2009.01858``): - :math:`\\frac{(k/k_*(a))^4}{1+(k/k_*(a))^4}`. - If `None` the standard 1-halo term is returned with no damping. - - Returns: - float or array_like: integral values evaluated at each - combination of `k` and `a`. The shape of the output will - be `(N_a, N_k)` where `N_k` and `N_a` are the sizes of - `k` and `a` respectively. If `k` or `a` are scalars, the - corresponding dimension will be squeezed out on output. - """ - a_use = np.atleast_1d(a).astype(float) - k_use = np.atleast_1d(k).astype(float) - - # Check inputs - if not isinstance(prof, HaloProfile): - raise TypeError("prof must be of type `HaloProfile`") - if prof2 is None: - prof2 = prof - elif not isinstance(prof2, HaloProfile): - raise TypeError("prof2 must be of type `HaloProfile` or `None`") - if prof_2pt is None: - prof_2pt = Profile2pt() - elif not isinstance(prof_2pt, Profile2pt): - raise TypeError("prof_2pt must be of type " - "`Profile2pt` or `None`") - if smooth_transition is not None: - if not (get_1h and get_2h): - raise ValueError("transition region can only be modified " - "when both 1-halo and 2-halo terms are queried") - if not hasattr(smooth_transition, "__call__"): - raise TypeError("smooth_transition must be " - "a function of `a` or None") - if supress_1h is not None: - if not get_1h: - raise ValueError("can't supress the 1-halo term " - "when get_1h is False") - if not hasattr(supress_1h, "__call__"): - raise TypeError("supress_1h must be " - "a function of `a` or None") - - # Power spectrum - if isinstance(p_of_k_a, Pk2D): - def pkf(sf): - return p_of_k_a.eval(k_use, sf, cosmo) - elif (p_of_k_a is None) or (str(p_of_k_a) == 'linear'): - def pkf(sf): - return linear_matter_power(cosmo, k_use, sf) - elif str(p_of_k_a) == 'nonlinear': - def pkf(sf): - return nonlin_matter_power(cosmo, k_use, sf) - else: - raise TypeError("p_of_k_a must be `None`, \'linear\', " - "\'nonlinear\' or a `Pk2D` object") - - na = len(a_use) - nk = len(k_use) - out = np.zeros([na, nk]) - for ia, aa in enumerate(a_use): - # Compute first profile normalization - if normprof1: - norm1 = hmc.profile_norm(cosmo, aa, prof) - else: - norm1 = 1 - # Compute second profile normalization - if prof2 == prof: - norm2 = norm1 - else: - if normprof2: - norm2 = hmc.profile_norm(cosmo, aa, prof2) - else: - norm2 = 1 - norm = norm1 * norm2 - - if get_2h: - # Compute first bias factor - i11_1 = hmc.I_1_1(cosmo, k_use, aa, prof) - - # Compute second bias factor - if prof2 == prof: - i11_2 = i11_1 - else: - i11_2 = hmc.I_1_1(cosmo, k_use, aa, prof2) - - # Compute 2-halo power spectrum - pk_2h = pkf(aa) * i11_1 * i11_2 - else: - pk_2h = 0. - - if get_1h: - pk_1h = hmc.I_0_2(cosmo, k_use, aa, prof, prof_2pt, prof2) - if supress_1h is not None: - ks = supress_1h(aa) - pk_1h *= (k_use / ks)**4 / (1 + (k_use / ks)**4) - else: - pk_1h = 0. - - # Transition region - if smooth_transition is None: - out[ia, :] = (pk_1h + pk_2h) * norm - else: - alpha = smooth_transition(aa) - out[ia, :] = (pk_1h**alpha + pk_2h**alpha)**(1/alpha) * norm - - if np.ndim(a) == 0: - out = np.squeeze(out, axis=0) - if np.ndim(k) == 0: - out = np.squeeze(out, axis=-1) - return out - - -def halomod_Pk2D(cosmo, hmc, prof, - prof_2pt=None, prof2=None, p_of_k_a=None, - normprof1=False, normprof2=False, - get_1h=True, get_2h=True, - lk_arr=None, a_arr=None, - extrap_order_lok=1, extrap_order_hik=2, - smooth_transition=None, supress_1h=None): - """ Returns a :class:`~pyccl.pk2d.Pk2D` object containing - the halo-model power spectrum for two quantities defined by - their respective halo profiles. See :meth:`halomod_power_spectrum` - for more details about the actual calculation. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. - hmc (:class:`HMCalculator`): a halo model calculator. - prof (:class:`~pyccl.halos.profiles.HaloProfile`): halo - profile. - prof_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): - a profile covariance object - returning the the two-point moment of the two profiles - being correlated. If `None`, the default second moment - will be used, corresponding to the products of the means - of both profiles. - prof2 (:class:`~pyccl.halos.profiles.HaloProfile`): a - second halo profile. If `None`, `prof` will be used as - `prof2`. - p_of_k_a (:class:`~pyccl.pk2d.Pk2D`): a `Pk2D` object to - be used as the linear matter power spectrum. If `None`, - the power spectrum stored within `cosmo` will be used. - normprof1 (bool): if `True`, this integral will be - normalized by :math:`I^0_1(k\\rightarrow 0,a|u)` - (see :meth:`~HMCalculator.I_0_1`), where - :math:`u` is the profile represented by `prof`. - normprof2 (bool): if `True`, this integral will be - normalized by :math:`I^0_1(k\\rightarrow 0,a|v)` - (see :meth:`~HMCalculator.I_0_1`), where - :math:`v` is the profile represented by `prof2`. - get_1h (bool): if `False`, the 1-halo term (i.e. the first - term in the first equation above) won't be computed. - get_2h (bool): if `False`, the 2-halo term (i.e. the second - term in the first equation above) won't be computed. - a_arr (array): an array holding values of the scale factor - at which the halo model power spectrum should be - calculated for interpolation. If `None`, the internal - values used by `cosmo` will be used. - lk_arr (array): an array holding values of the natural - logarithm of the wavenumber (in units of Mpc^-1) at - which the halo model power spectrum should be calculated - for interpolation. If `None`, the internal values used - by `cosmo` will be used. - extrap_order_lok (int): extrapolation order to be used on - k-values below the minimum of the splines. See - :class:`~pyccl.pk2d.Pk2D`. - extrap_order_hik (int): extrapolation order to be used on - k-values above the maximum of the splines. See - :class:`~pyccl.pk2d.Pk2D`. - smooth_transition (function or None): - Modify the halo model 1-halo/2-halo transition region - via a time-dependent function :math:`\\alpha(a)`, - defined as in HMCODE-2020 (``arXiv:2009.01858``): :math:`P(k,a)= - (P_{1h}^{\\alpha(a)}(k)+P_{2h}^{\\alpha(a)}(k))^{1/\\alpha}`. - If `None` the extra factor is just 1. - supress_1h (function or None): - Supress the 1-halo large scale contribution by a - time- and scale-dependent function :math:`k_*(a)`, - defined as in HMCODE-2020 (``arXiv:2009.01858``): - :math:`\\frac{(k/k_*(a))^4}{1+(k/k_*(a))^4}`. - If `None` the standard 1-halo term is returned with no damping. - - Returns: - :class:`~pyccl.pk2d.Pk2D`: halo model power spectrum. - """ - if lk_arr is None: - status = 0 - nk = lib.get_pk_spline_nk(cosmo.cosmo) - lk_arr, status = lib.get_pk_spline_lk(cosmo.cosmo, nk, status) - check(status, cosmo=cosmo) - if a_arr is None: - status = 0 - na = lib.get_pk_spline_na(cosmo.cosmo) - a_arr, status = lib.get_pk_spline_a(cosmo.cosmo, na, status) - check(status, cosmo=cosmo) - - pk_arr = halomod_power_spectrum(cosmo, hmc, np.exp(lk_arr), a_arr, - prof, prof_2pt=prof_2pt, - prof2=prof2, p_of_k_a=p_of_k_a, - normprof1=normprof1, normprof2=normprof2, - get_1h=get_1h, get_2h=get_2h, - smooth_transition=smooth_transition, - supress_1h=supress_1h) - - pk2d = Pk2D(a_arr=a_arr, lk_arr=lk_arr, pk_arr=pk_arr, - extrap_order_lok=extrap_order_lok, - extrap_order_hik=extrap_order_hik, - cosmo=cosmo, is_logp=False) - return pk2d - - -def halomod_trispectrum_1h(cosmo, hmc, k, a, - prof1, prof2=None, prof12_2pt=None, - prof3=None, prof4=None, prof34_2pt=None, - normprof1=False, normprof2=False, - normprof3=False, normprof4=False): - """ Computes the halo model 1-halo trispectrum for four different - quantities defined by their respective halo profiles. The 1-halo - trispectrum for four profiles :math:`u_{1,2}`, :math:`v_{1,2}` is - calculated as: - - .. math:: - T_{u_1,u_2;v_1,v_2}(k_u,k_v,a) = - I^0_{2,2}(k_u,k_v,a|u_{1,2},v_{1,2}) - - where :math:`I^0_{2,2}` is defined in the documentation - of :meth:`~HMCalculator.I_0_22`. - - .. note:: This approximation assumes that the 4-point - profile cumulant is the same as the product of two - 2-point cumulants. We may relax this assumption in - future versions of CCL. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. - hmc (:class:`HMCalculator`): a halo model calculator. - k (float or array_like): comoving wavenumber in Mpc^-1. - a (float or array_like): scale factor. - prof1 (:class:`~pyccl.halos.profiles.HaloProfile`): halo - profile (corresponding to :math:`u_1` above. - prof2 (:class:`~pyccl.halos.profiles.HaloProfile`): halo - profile (corresponding to :math:`u_2` above. If `None`, - `prof1` will be used as `prof2`. - prof12_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): - a profile covariance object returning the the two-point - moment of `prof1` and `prof2`. If `None`, the default - second moment will be used, corresponding to the - products of the means of both profiles. - prof3 (:class:`~pyccl.halos.profiles.HaloProfile`): halo - profile (corresponding to :math:`v_1` above. If `None`, - `prof1` will be used as `prof3`. - prof4 (:class:`~pyccl.halos.profiles.HaloProfile`): halo - profile (corresponding to :math:`v_2` above. If `None`, - `prof2` will be used as `prof4`. - prof34_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): - same as `prof12_2pt` for `prof3` and `prof4`. - normprof1 (bool): if `True`, this integral will be - normalized by :math:`I^0_1(k\\rightarrow 0,a|u)` - (see :meth:`~HMCalculator.I_0_1`), where - :math:`u` is the profile represented by `prof1`. - normprof2 (bool): same as `normprof1` for `prof2`. - normprof3 (bool): same as `normprof1` for `prof3`. - normprof4 (bool): same as `normprof1` for `prof4`. - - Returns: - float or array_like: integral values evaluated at each - combination of `k` and `a`. The shape of the output will - be `(N_a, N_k, N_k)` where `N_k` and `N_a` are the sizes of - `k` and `a` respectively. The ordering is such that - `output[ia, ik2, ik1] = T(k[ik1], k[ik2], a[ia])` - If `k` or `a` are scalars, the corresponding dimension will - be squeezed out on output. - """ - a_use = np.atleast_1d(a).astype(float) - k_use = np.atleast_1d(k).astype(float) - - # Check inputs - if not isinstance(prof1, HaloProfile): - raise TypeError("prof1 must be of type `HaloProfile`") - if prof2 is None: - prof2 = prof1 - elif not isinstance(prof2, HaloProfile): - raise TypeError("prof2 must be of type `HaloProfile` or `None`") - if prof3 is None: - prof3 = prof1 - elif not isinstance(prof3, HaloProfile): - raise TypeError("prof3 must be of type `HaloProfile` or `None`") - if prof4 is None: - prof4 = prof2 - elif not isinstance(prof4, HaloProfile): - raise TypeError("prof4 must be of type `HaloProfile` or `None`") - if prof12_2pt is None: - prof12_2pt = Profile2pt() - elif not isinstance(prof12_2pt, Profile2pt): - raise TypeError("prof12_2pt must be of type `Profile2pt` or `None`") - if prof34_2pt is None: - prof34_2pt = prof12_2pt - elif not isinstance(prof34_2pt, Profile2pt): - raise TypeError("prof34_2pt must be of type `Profile2pt` or `None`") - - def get_norm(normprof, prof, sf): - if normprof: - return hmc.profile_norm(cosmo, sf, prof) - else: - return 1 - - na = len(a_use) - nk = len(k_use) - out = np.zeros([na, nk, nk]) - for ia, aa in enumerate(a_use): - # Compute profile normalizations - norm1 = get_norm(normprof1, prof1, aa) - # Compute second profile normalization - if prof2 == prof1: - norm2 = norm1 - else: - norm2 = get_norm(normprof2, prof2, aa) - - if prof3 == prof1: - norm3 = norm1 - else: - norm3 = get_norm(normprof3, prof3, aa) - - if prof4 == prof2: - norm4 = norm2 - else: - norm4 = get_norm(normprof4, prof4, aa) - - norm = norm1 * norm2 * norm3 * norm4 - - # Compute trispectrum at this redshift - tk_1h = hmc.I_0_22(cosmo, k_use, aa, - prof1, prof12_2pt, prof2=prof2, - prof3=prof3, prof34_2pt=prof34_2pt, - prof4=prof4) - - # Normalize - out[ia, :, :] = tk_1h * norm - - if np.ndim(a) == 0: - out = np.squeeze(out, axis=0) - if np.ndim(k) == 0: - out = np.squeeze(out, axis=-1) - out = np.squeeze(out, axis=-1) - return out - - -def halomod_Tk3D_1h(cosmo, hmc, - prof1, prof2=None, prof12_2pt=None, - prof3=None, prof4=None, prof34_2pt=None, - normprof1=False, normprof2=False, - normprof3=False, normprof4=False, - lk_arr=None, a_arr=None, - extrap_order_lok=1, extrap_order_hik=1, - use_log=False): - """ Returns a :class:`~pyccl.tk3d.Tk3D` object containing - the 1-halo trispectrum for four quantities defined by - their respective halo profiles. See :meth:`halomod_trispectrum_1h` - for more details about the actual calculation. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. - hmc (:class:`HMCalculator`): a halo model calculator. - prof1 (:class:`~pyccl.halos.profiles.HaloProfile`): halo - profile (corresponding to :math:`u_1` above. - prof2 (:class:`~pyccl.halos.profiles.HaloProfile`): halo - profile (corresponding to :math:`u_2` above. If `None`, - `prof1` will be used as `prof2`. - prof12_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): - a profile covariance object returning the the two-point - moment of `prof1` and `prof2`. If `None`, the default - second moment will be used, corresponding to the - products of the means of both profiles. - prof3 (:class:`~pyccl.halos.profiles.HaloProfile`): halo - profile (corresponding to :math:`v_1` above. If `None`, - `prof1` will be used as `prof3`. - prof4 (:class:`~pyccl.halos.profiles.HaloProfile`): halo - profile (corresponding to :math:`v_2` above. If `None`, - `prof2` will be used as `prof4`. - prof34_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): - same as `prof12_2pt` for `prof3` and `prof4`. - normprof1 (bool): if `True`, this integral will be - normalized by :math:`I^0_1(k\\rightarrow 0,a|u)` - (see :meth:`~HMCalculator.I_0_1`), where - :math:`u` is the profile represented by `prof1`. - normprof2 (bool): same as `normprof1` for `prof2`. - normprof3 (bool): same as `normprof1` for `prof3`. - normprof4 (bool): same as `normprof1` for `prof4`. - a_arr (array): an array holding values of the scale factor - at which the trispectrum should be calculated for - interpolation. If `None`, the internal values used - by `cosmo` will be used. - lk_arr (array): an array holding values of the natural - logarithm of the wavenumber (in units of Mpc^-1) at - which the trispectrum should be calculated for - interpolation. If `None`, the internal values used - by `cosmo` will be used. - extrap_order_lok (int): extrapolation order to be used on - k-values below the minimum of the splines. See - :class:`~pyccl.tk3d.Tk3D`. - extrap_order_hik (int): extrapolation order to be used on - k-values above the maximum of the splines. See - :class:`~pyccl.tk3d.Tk3D`. - use_log (bool): if `True`, the trispectrum will be - interpolated in log-space (unless negative or - zero values are found). - - Returns: - :class:`~pyccl.tk3d.Tk3D`: 1-halo trispectrum. - """ - if lk_arr is None: - status = 0 - nk = lib.get_pk_spline_nk(cosmo.cosmo) - lk_arr, status = lib.get_pk_spline_lk(cosmo.cosmo, nk, status) - check(status, cosmo=cosmo) - if a_arr is None: - status = 0 - na = lib.get_pk_spline_na(cosmo.cosmo) - a_arr, status = lib.get_pk_spline_a(cosmo.cosmo, na, status) - check(status, cosmo=cosmo) - - tkk = halomod_trispectrum_1h(cosmo, hmc, np.exp(lk_arr), a_arr, - prof1, prof2=prof2, - prof12_2pt=prof12_2pt, - prof3=prof3, prof4=prof4, - prof34_2pt=prof34_2pt, - normprof1=normprof1, normprof2=normprof2, - normprof3=normprof3, normprof4=normprof4) - if use_log: - if np.any(tkk <= 0): - warnings.warn( - "Some values were not positive. " - "Will not interpolate in log-space.", - category=CCLWarning) - use_log = False - else: - tkk = np.log(tkk) - - tk3d = Tk3D(a_arr=a_arr, lk_arr=lk_arr, tkk_arr=tkk, - extrap_order_lok=extrap_order_lok, - extrap_order_hik=extrap_order_hik, is_logt=use_log) - return tk3d - - -def halomod_Tk3D_SSC_linear_bias(cosmo, hmc, prof, bias1=1, bias2=1, bias3=1, - bias4=1, - is_number_counts1=False, - is_number_counts2=False, - is_number_counts3=False, - is_number_counts4=False, - p_of_k_a=None, lk_arr=None, - a_arr=None, extrap_order_lok=1, - extrap_order_hik=1, use_log=False): - """ Returns a :class:`~pyccl.tk3d.Tk3D` object containing - the super-sample covariance trispectrum, given by the tensor - product of the power spectrum responses associated with the - two pairs of quantities being correlated. Each response is - calculated as: - - .. math:: - \\frac{\\partial P_{u,v}(k)}{\\partial\\delta_L} = b_u b_v \\left( - \\left(\\frac{68}{21}-\\frac{d\\log k^3P_L(k)}{d\\log k}\\right) - P_L(k)+I^1_2(k|u,v) - (b_{u} + b_{v}) P_{u,v}(k) \\right) - - where the :math:`I^1_2` is defined in the documentation - :meth:`~HMCalculator.I_1_2` and :math:`b_{}` and :math:`b_{vv}` are the - linear halo biases for quantities :math:`u` and :math:`v`, respectively - (zero if they are not clustering). - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. - hmc (:class:`HMCalculator`): a halo model calculator. - prof (:class:`~pyccl.halos.profiles.HaloProfile`): halo NFW - profile. - bias1 (float or array): linear galaxy bias for quantity 1. If an array, - it has to have the shape of `a_arr`. - bias2 (float or array): linear galaxy bias for quantity 2. - bias3 (float or array): linear galaxy bias for quantity 3. - bias4 (float or array): linear galaxy bias for quantity 4. - is_number_counts1 (bool): If True, quantity 1 will be considered - number counts and the clustering counter terms computed. Default False. - is_number_counts2 (bool): as is_number_counts1 but for quantity 2. - is_number_counts3 (bool): as is_number_counts1 but for quantity 3. - is_number_counts4 (bool): as is_number_counts1 but for quantity 4. - p_of_k_a (:class:`~pyccl.pk2d.Pk2D`): a `Pk2D` object to - be used as the linear matter power spectrum. If `None`, - the power spectrum stored within `cosmo` will be used. - a_arr (array): an array holding values of the scale factor - at which the trispectrum should be calculated for - interpolation. If `None`, the internal values used - by `cosmo` will be used. - lk_arr (array): an array holding values of the natural - logarithm of the wavenumber (in units of Mpc^-1) at - which the trispectrum should be calculated for - interpolation. If `None`, the internal values used - by `cosmo` will be used. - extrap_order_lok (int): extrapolation order to be used on - k-values below the minimum of the splines. See - :class:`~pyccl.tk3d.Tk3D`. - extrap_order_hik (int): extrapolation order to be used on - k-values above the maximum of the splines. See - :class:`~pyccl.tk3d.Tk3D`. - use_log (bool): if `True`, the trispectrum will be - interpolated in log-space (unless negative or - zero values are found). - - Returns: - :class:`~pyccl.tk3d.Tk3D`: SSC effective trispectrum. - """ - if lk_arr is None: - status = 0 - nk = lib.get_pk_spline_nk(cosmo.cosmo) - lk_arr, status = lib.get_pk_spline_lk(cosmo.cosmo, nk, status) - check(status, cosmo=cosmo) - if a_arr is None: - status = 0 - na = lib.get_pk_spline_na(cosmo.cosmo) - a_arr, status = lib.get_pk_spline_a(cosmo.cosmo, na, status) - check(status, cosmo=cosmo) - - # Make sure biases are of the form number of a x number of k - ones = np.ones_like(a_arr) - bias1 *= ones - bias2 *= ones - bias3 *= ones - bias4 *= ones - - k_use = np.exp(lk_arr) - - # Check inputs - if not isinstance(prof, HaloProfileNFW): - raise TypeError("prof must be of type `HaloProfileNFW`") - prof_2pt = Profile2pt() - - # Power spectrum - if isinstance(p_of_k_a, Pk2D): - pk2d = p_of_k_a - elif (p_of_k_a is None) or (str(p_of_k_a) == 'linear'): - pk2d = cosmo.get_linear_power('delta_matter:delta_matter') - elif str(p_of_k_a) == 'nonlinear': - pk2d = cosmo.get_nonlin_power('delta_matter:delta_matter') - else: - raise TypeError("p_of_k_a must be `None`, \'linear\', " - "\'nonlinear\' or a `Pk2D` object") - - na = len(a_arr) - nk = len(k_use) - dpk12 = np.zeros([na, nk]) - dpk34 = np.zeros([na, nk]) - for ia, aa in enumerate(a_arr): - # Compute profile normalizations - norm = hmc.profile_norm(cosmo, aa, prof) ** 2 - i12 = hmc.I_1_2(cosmo, k_use, aa, prof, prof_2pt, prof) * norm - - pk = pk2d.eval(k_use, aa, cosmo) - dpk = pk2d.eval_dlogpk_dlogk(k_use, aa, cosmo) - # ~ [(47/21 - 1/3 dlogPk/dlogk) * Pk+I12] - dpk12[ia] = ((2.2380952381-dpk/3)*pk + i12) - dpk34[ia] = dpk12[ia].copy() # Avoid surprises - - # Counter terms for clustering (i.e. - (bA + bB) * PAB - if is_number_counts1 or is_number_counts2 or is_number_counts3 or \ - is_number_counts4: - b1 = b2 = b3 = b4 = 0 - - i02 = hmc.I_0_2(cosmo, k_use, aa, prof, prof_2pt, prof) * norm - P_12 = P_34 = pk + i02 - - if is_number_counts1: - b1 = bias1[ia] - if is_number_counts2: - b2 = bias2[ia] - if is_number_counts3: - b3 = bias3[ia] - if is_number_counts4: - b4 = bias4[ia] - - dpk12[ia, :] -= (b1 + b2) * P_12 - dpk34[ia, :] -= (b3 + b4) * P_34 - - dpk12[ia] *= bias1[ia] * bias2[ia] - dpk34[ia] *= bias3[ia] * bias4[ia] - - if use_log: - if np.any(dpk12 <= 0) or np.any(dpk34 <= 0): - warnings.warn( - "Some values were not positive. " - "Will not interpolate in log-space.", - category=CCLWarning) - use_log = False - else: - dpk12 = np.log(dpk12) - dpk34 = np.log(dpk34) - - tk3d = Tk3D(a_arr=a_arr, lk_arr=lk_arr, - pk1_arr=dpk12, pk2_arr=dpk34, - extrap_order_lok=extrap_order_lok, - extrap_order_hik=extrap_order_hik, is_logt=use_log) - return tk3d - - -def halomod_Tk3D_SSC(cosmo, hmc, - prof1, prof2=None, prof12_2pt=None, - prof3=None, prof4=None, prof34_2pt=None, - normprof1=False, normprof2=False, - normprof3=False, normprof4=False, - p_of_k_a=None, lk_arr=None, a_arr=None, - extrap_order_lok=1, extrap_order_hik=1, - use_log=False): - """ Returns a :class:`~pyccl.tk3d.Tk3D` object containing - the super-sample covariance trispectrum, given by the tensor - product of the power spectrum responses associated with the - two pairs of quantities being correlated. Each response is - calculated as: - - .. math:: - \\frac{\\partial P_{u,v}(k)}{\\partial\\delta_L} = - \\left(\\frac{68}{21}-\\frac{d\\log k^3P_L(k)}{d\\log k}\\right) - P_L(k)I^1_1(k,|u)I^1_1(k,|v)+I^1_2(k|u,v) - (b_{u} + b_{v}) - P_{u,v}(k) - - where the :math:`I^a_b` are defined in the documentation - of :meth:`~HMCalculator.I_1_1` and :meth:`~HMCalculator.I_1_2` and - :math:`b_{u}` and :math:`b_{v}` are the linear halo biases for quantities - :math:`u` and :math:`v`, respectively (zero if they are not clustering). - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. - hmc (:class:`HMCalculator`): a halo model calculator. - prof1 (:class:`~pyccl.halos.profiles.HaloProfile`): halo - profile (corresponding to :math:`u_1` above. - prof2 (:class:`~pyccl.halos.profiles.HaloProfile`): halo - profile (corresponding to :math:`u_2` above. If `None`, - `prof1` will be used as `prof2`. - prof12_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): - a profile covariance object returning the the two-point - moment of `prof1` and `prof2`. If `None`, the default - second moment will be used, corresponding to the - products of the means of both profiles. - prof3 (:class:`~pyccl.halos.profiles.HaloProfile`): halo - profile (corresponding to :math:`v_1` above. If `None`, - `prof1` will be used as `prof3`. - prof4 (:class:`~pyccl.halos.profiles.HaloProfile`): halo - profile (corresponding to :math:`v_2` above. If `None`, - `prof2` will be used as `prof4`. - prof34_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): - same as `prof12_2pt` for `prof3` and `prof4`. - normprof1 (bool): if `True`, this integral will be - normalized by :math:`I^0_1(k\\rightarrow 0,a|u)` - (see :meth:`~HMCalculator.I_0_1`), where - :math:`u` is the profile represented by `prof1`. - normprof2 (bool): same as `normprof1` for `prof2`. - normprof3 (bool): same as `normprof1` for `prof3`. - normprof4 (bool): same as `normprof1` for `prof4`. - p_of_k_a (:class:`~pyccl.pk2d.Pk2D`): a `Pk2D` object to - be used as the linear matter power spectrum. If `None`, - the power spectrum stored within `cosmo` will be used. - a_arr (array): an array holding values of the scale factor - at which the trispectrum should be calculated for - interpolation. If `None`, the internal values used - by `cosmo` will be used. - lk_arr (array): an array holding values of the natural - logarithm of the wavenumber (in units of Mpc^-1) at - which the trispectrum should be calculated for - interpolation. If `None`, the internal values used - by `cosmo` will be used. - extrap_order_lok (int): extrapolation order to be used on - k-values below the minimum of the splines. See - :class:`~pyccl.tk3d.Tk3D`. - extrap_order_hik (int): extrapolation order to be used on - k-values above the maximum of the splines. See - :class:`~pyccl.tk3d.Tk3D`. - use_log (bool): if `True`, the trispectrum will be - interpolated in log-space (unless negative or - zero values are found). - - Returns: - :class:`~pyccl.tk3d.Tk3D`: SSC effective trispectrum. - """ - if lk_arr is None: - status = 0 - nk = lib.get_pk_spline_nk(cosmo.cosmo) - lk_arr, status = lib.get_pk_spline_lk(cosmo.cosmo, nk, status) - check(status, cosmo=cosmo) - if a_arr is None: - status = 0 - na = lib.get_pk_spline_na(cosmo.cosmo) - a_arr, status = lib.get_pk_spline_a(cosmo.cosmo, na, status) - check(status, cosmo=cosmo) - - k_use = np.exp(lk_arr) - - if prof2 is None: - prof2 = prof1 - normprof2 = normprof1 - if prof3 is None: - prof3 = prof1 - normprof3 = normprof1 - if prof4 is None: - prof4 = prof2 - normprof4 = normprof2 - if prof12_2pt is None: - prof12_2pt = Profile2pt() - if prof34_2pt is None: - prof34_2pt = prof12_2pt - - # Check inputs - if not isinstance(prof1, HaloProfile): - raise TypeError("prof1 must be of type `HaloProfile`") - if not isinstance(prof2, HaloProfile): - raise TypeError("prof2 must be of type `HaloProfile` or `None`") - if not isinstance(prof3, HaloProfile): - raise TypeError("prof3 must be of type `HaloProfile` or `None`") - if not isinstance(prof4, HaloProfile): - raise TypeError("prof4 must be of type `HaloProfile` or `None`") - if not isinstance(prof12_2pt, Profile2pt): - raise TypeError("prof12_2pt must be of type `Profile2pt` or `None`") - if not isinstance(prof34_2pt, Profile2pt): - raise TypeError("prof34_2pt must be of type `Profile2pt` or `None`") - - # number counts profiles must be normalized - profs = {prof1: normprof1, prof2: normprof2, - prof3: normprof3, prof4: normprof4} - - for i, (prof, norm) in enumerate(profs.items()): - if prof.is_number_counts and not norm: - raise ValueError( - f"normprof{i+1} must be True if prof{i+1}.is_number_counts") - - # Power spectrum - if isinstance(p_of_k_a, Pk2D): - pk2d = p_of_k_a - elif (p_of_k_a is None) or str(p_of_k_a) == 'linear': - pk2d = cosmo.get_linear_power('delta_matter:delta_matter') - elif str(p_of_k_a) == 'nonlinear': - pk2d = cosmo.get_nonlin_power('delta_matter:delta_matter') - else: - raise ValueError("p_of_k_a must be `None`, 'linear', " - "'nonlinear' or a `Pk2D` object") - - def get_norm(normprof, prof, sf): - return hmc.profile_norm(cosmo, sf, prof) if normprof else 1 - - dpk12, dpk34 = [np.zeros((len(a_arr), len(k_use))) for _ in range(2)] - for ia, aa in enumerate(a_arr): - # Compute profile normalizations - norm1 = get_norm(normprof1, prof1, aa) - i11_1 = hmc.I_1_1(cosmo, k_use, aa, prof1) - # Compute second profile normalization - if prof2 == prof1: - norm2 = norm1 - i11_2 = i11_1 - else: - norm2 = get_norm(normprof2, prof2, aa) - i11_2 = hmc.I_1_1(cosmo, k_use, aa, prof2) - - if prof3 == prof1: - norm3 = norm1 - i11_3 = i11_1 - else: - norm3 = get_norm(normprof3, prof3, aa) - i11_3 = hmc.I_1_1(cosmo, k_use, aa, prof3) - - if prof4 == prof2: - norm4 = norm2 - i11_4 = i11_2 - else: - norm4 = get_norm(normprof4, prof4, aa) - i11_4 = hmc.I_1_1(cosmo, k_use, aa, prof4) - - i12_12 = hmc.I_1_2(cosmo, k_use, aa, prof1, prof12_2pt, prof2) - - if (prof1, prof2) == (prof3, prof4): - i12_34 = i12_12 - else: - i12_34 = hmc.I_1_2(cosmo, k_use, aa, prof3, prof34_2pt, prof4) - - norm12 = norm1 * norm2 - norm34 = norm3 * norm4 - - pk = pk2d.eval(k_use, aa, cosmo) - dpk = pk2d.eval_dlogpk_dlogk(k_use, aa, cosmo) - # (47/21 - 1/3 dlogPk/dlogk) * I11 * I11 * Pk+I12 - dpk12[ia, :] = norm12*((47/21 - dpk/3)*i11_1*i11_2*pk + i12_12) - dpk34[ia, :] = norm34*((47/21 - dpk/3)*i11_3*i11_4*pk + i12_34) - - # Counter terms for clustering (i.e. - (bA + bB) * PAB - if prof1.is_number_counts or prof2.is_number_counts: - b1 = b2 = np.zeros_like(k_use) - i02_12 = hmc.I_0_2(cosmo, k_use, aa, prof1, prof12_2pt, prof2) - P_12 = norm12 * (pk * i11_1 * i11_2 + i02_12) - - if prof1.is_number_counts: - b1 = i11_1 * norm1 - - if prof2 == prof1: - b2 = b1 - elif prof2.is_number_counts: - b2 = i11_2 * norm2 - - dpk12[ia, :] -= (b1 + b2) * P_12 - - if any([p.is_number_counts for p in [prof3, prof4]]): - b3 = b4 = np.zeros_like(k_use) - if (prof1, prof2, prof12_2pt) == (prof3, prof4, prof34_2pt): - i02_34 = i02_12 - else: - i02_34 = hmc.I_0_2(cosmo, k_use, aa, prof3, prof34_2pt, prof4) - P_34 = norm34 * (pk * i11_3 * i11_4 + i02_34) - - if prof3 == prof1: - b3 = b1 - elif prof3.is_number_counts: - b3 = i11_3 * norm3 - - if prof4 == prof2: - b4 = b2 - elif prof4.is_number_counts: - b4 = i11_4 * norm4 - - dpk34[ia, :] -= (b3 + b4) * P_34 - - if use_log: - if np.any(dpk12 <= 0) or np.any(dpk34 <= 0): - warnings.warn( - "Some values were not positive. " - "Will not interpolate in log-space.", - category=CCLWarning) - use_log = False - else: - dpk12 = np.log(dpk12) - dpk34 = np.log(dpk34) + uk34 = prof34_2pt.fourier_2pt( + cosmo, k, self._mass, a, prof3, + prof2=prof4, mass_def=self.mass_def).T - tk3d = Tk3D(a_arr=a_arr, lk_arr=lk_arr, - pk1_arr=dpk12, pk2_arr=dpk34, - extrap_order_lok=extrap_order_lok, - extrap_order_hik=extrap_order_hik, is_logt=use_log) - return tk3d + return self._integrate_over_mf(uk12[None, :, :] * uk34[:, None, :]) diff --git a/pyccl/halos/halo_model_base.py b/pyccl/halos/halo_model_base.py new file mode 100644 index 000000000..bcdb35546 --- /dev/null +++ b/pyccl/halos/halo_model_base.py @@ -0,0 +1,289 @@ +from .. import ccllib as lib +from ..core import check +from ..base.parameters import physical_constants as const +from ..base import (CCLAutoRepr, CCLNamedClass, abstractlinkedmethod, + warn_api, deprecated, deprecate_attr) +from .massdef import MassDef +import numpy as np +import functools +from abc import abstractmethod + + +__all__ = ("HMIngredients",) + + +class HMIngredients(CCLAutoRepr, CCLNamedClass): + """Base class for halo model ingredients.""" + __repr_attrs__ = __eq_attrs__ = ("mass_def", "mass_def_strict",) + __getattr__ = deprecate_attr(pairs=[('mdef', 'mass_def')] + )(super.__getattribute__) + + @warn_api + def __init__(self, *, mass_def, mass_def_strict=True): + # Check mass definition consistency. + mass_def = MassDef.create_instance(mass_def) + self.mass_def_strict = mass_def_strict + self._check_mass_def(mass_def) + self.mass_def = mass_def + self._setup() + + @property + @abstractmethod + def _mass_def_strict_always(self) -> bool: + """Property that dictates whether ``mass_def_strict`` can be set + as False on initialization. + + Some models are set up in a way so that the set of fitted parameters + depends on the mass definition, (i.e. no one universal model exists + to cover all cases). Setting this to True fixes ``mass_def_strict`` + to True irrespective of what the user passes. + + Set this propery to False to allow users to override strict checks. + """ + + @abstractmethod + def _check_mass_def_strict(self, mass_def) -> bool: + """Check if this class is defined for mass definition ``mass_def``.""" + + def _setup(self) -> None: + """ Use this function to initialize any internal attributes + of this object. This function is called at the very end of the + constructor call. + """ + + def _check_mass_def(self, mass_def) -> None: + """ Return False if the input mass definition agrees with + the definitions for which this mass function parametrization + works. True otherwise. This function gets called at the + start of the constructor call. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef`): + a mass definition object. + """ + classname = self.__class__.__name__ + msg = f"{classname} is not defined for {mass_def.name} masses" + + if self._check_mass_def_strict(mass_def): + # Passed mass is incompatible with model. + + if self._mass_def_strict_always: + # Class has no universal model and mass is incompatible. + raise ValueError( + f"{msg} and this requirement cannot be relaxed.") + + if self.mass_def_strict: + # Strict mass_def check enabled and mass is incompatible. + raise ValueError( + f"{msg}. To relax this check set `mass_def_strict=False`.") + + def _get_logM_sigM(self, cosmo, M, a, *, return_dlns=False): + """Compute ``logM``, ``sigM``, and (optionally) ``dlns_dlogM``.""" + cosmo.compute_sigma() # initialize sigma(M) splines if needed + logM = np.log10(M) + + # sigma(M) + status = 0 + sigM, status = lib.sigM_vec(cosmo.cosmo, a, logM, len(logM), status) + check(status, cosmo=cosmo) + if not return_dlns: + return logM, sigM + + # dlogsigma(M)/dlog10(M) + dlns_dlogM, status = lib.dlnsigM_dlogM_vec(cosmo.cosmo, a, logM, + len(logM), status) + check(status, cosmo=cosmo) + return logM, sigM, dlns_dlogM + + +class MassFunc(HMIngredients): + """ This class enables the calculation of halo mass functions. + We currently assume that all mass functions can be written as + + .. math:: + \\frac{dn}{d\\log_{10}M} = f(\\sigma_M)\\,\\frac{\\rho_M}{M}\\, + \\frac{d\\log \\sigma_M}{d\\log_{10} M} + + where :math:`\\sigma_M^2` is the overdensity variance on spheres with a + radius given by the Lagrangian radius for mass M. + + * Subclasses implementing analytical mass function parametrizations + can be created by overriding the ``_get_fsigma`` method. + + * Subclasses may have particular implementations of + ``_check_mass_def_strict`` to ensure consistency of the halo mass + definition. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef`): + a mass definition object that fixes + the mass definition used by this mass function + parametrization. + mass_def_strict (bool): if False, consistency of the mass + definition will be ignored. + """ + _mass_def_strict_always = False + + @abstractlinkedmethod + def _get_fsigma(self, cosmo, sigM, a, lnM): + """ Get the :math:`f(\\sigma_M)` function for this mass function + object (see description of this class for details). + + Args: + cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. + sigM (float or array_like): standard deviation in the + overdensity field on the scale of this halo. + a (float): scale factor. + lnM (float or array_like): natural logarithm of the + halo mass in units of M_sun (provided in addition + to sigM for convenience in some mass function + parametrizations). + + Returns: + float or array_like: :math:`f(\\sigma_M)` function. + """ + + @abstractlinkedmethod + def __call__(self, cosmo, M, a): + """ Returns the mass function for input parameters. + + Args: + cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. + M (float or array_like): halo mass in units of M_sun. + a (float): scale factor. + + Returns: + float or array_like: mass function \ + :math:`dn/d\\log_{10}M` in units of Mpc^-3 (comoving). + """ + M_use = np.atleast_1d(M) + logM, sigM, dlns_dlogM = self._get_logM_sigM( + cosmo, M_use, a, return_dlns=True) + + rho = (const.RHO_CRITICAL * cosmo['Omega_m'] * cosmo['h']**2) + f = self._get_fsigma(cosmo, sigM, a, 2.302585092994046 * logM) + mf = f * rho * dlns_dlogM / M_use + if np.ndim(M) == 0: + return mf[0] + return mf + + @deprecated(new_function=__call__) + def get_mass_function(self, cosmo, M, a): + return self(cosmo, M, a) + + +class HaloBias(HMIngredients): + """ This class enables the calculation of halo bias functions. + We currently assume that all halo bias functions can be written + as functions that depend on M only through sigma_M (where + sigma_M^2 is the overdensity variance on spheres with a + radius given by the Lagrangian radius for mass M). + All sub-classes implementing specific parametrizations + can therefore be simply created by replacing this class' + `_get_bsigma method`. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef`): a mass + definition object that fixes + the mass definition used by this halo bias + parametrization. + mass_def_strict (bool): if False, consistency of the mass + definition will be ignored. + """ + _mass_def_strict_always = False + + @abstractlinkedmethod + def _get_bsigma(self, cosmo, sigM, a): + """ Get the halo bias as a function of sigmaM. + + Args: + cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. + sigM (float or array_like): standard deviation in the + overdensity field on the scale of this halo. + a (float): scale factor. + + Returns: + float or array_like: f(sigma_M) function. + """ + + @abstractlinkedmethod + def __call__(self, cosmo, M, a): + """ Returns the halo bias for input parameters. + + Args: + cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. + M (float or array_like): halo mass in units of M_sun. + a (float): scale factor. + + Returns: + float or array_like: halo bias. + """ + M_use = np.atleast_1d(M) + logM, sigM = self._get_logM_sigM(cosmo, M_use, a) + b = self._get_bsigma(cosmo, sigM, a) + if np.ndim(M) == 0: + return b[0] + return b + + @deprecated(new_function=__call__) + def get_halo_bias(self, cosmo, M, a): + return self(cosmo, M, a) + + +class Concentration(HMIngredients): + """ This class enables the calculation of halo concentrations. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef`): a mass definition + object that fixes the mass definition used by this c(M) + parametrization. + """ + _mass_def_strict_always = True + + @warn_api + def __init__(self, *, mass_def): + super().__init__(mass_def=mass_def, mass_def_strict=True) + + @abstractlinkedmethod + def _concentration(self, cosmo, M, a): + """Implementation of the c(M) relation.""" + + @abstractlinkedmethod + def __call__(self, cosmo, M, a): + """ Returns the concentration for input parameters. + + Args: + cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. + M (float or array_like): halo mass in units of M_sun. + a (float): scale factor. + + Returns: + float or array_like: concentration. + """ + M_use = np.atleast_1d(M) + c = self._concentration(cosmo, M_use, a) + if np.ndim(M) == 0: + return c[0] + return c + + @deprecated(new_function=__call__) + def get_concentration(self, cosmo, M, a): + return self(cosmo, M, a) + + +@functools.wraps(MassFunc.from_name) +@deprecated(new_function=MassFunc.from_name) +def mass_function_from_name(name): + return MassFunc.from_name(name) + + +@functools.wraps(HaloBias.from_name) +@deprecated(new_function=HaloBias.from_name) +def halo_bias_from_name(name): + return HaloBias.from_name(name) + + +@functools.wraps(Concentration.from_name) +@deprecated(new_function=Concentration.from_name) +def concentration_from_name(name): + return Concentration.from_name(name) diff --git a/pyccl/halos/hbias.py b/pyccl/halos/hbias.py deleted file mode 100644 index 88f4a0005..000000000 --- a/pyccl/halos/hbias.py +++ /dev/null @@ -1,372 +0,0 @@ -from .. import ccllib as lib -from ..core import check -from ..background import omega_x -from ..base import CCLHalosObject -from .massdef import MassDef, MassDef200m -import numpy as np -import functools -from abc import abstractmethod - - -class HaloBias(CCLHalosObject): - """ This class enables the calculation of halo bias functions. - We currently assume that all halo bias functions can be written - as functions that depend on M only through sigma_M (where - sigma_M^2 is the overdensity variance on spheres with a - radius given by the Lagrangian radius for mass M). - All sub-classes implementing specific parametrizations - can therefore be simply created by replacing this class' - `_get_bsigma method`. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - mass_def (:class:`~pyccl.halos.massdef.MassDef`): a mass - definition object that fixes - the mass definition used by this halo bias - parametrization. - mass_def_strict (bool): if False, consistency of the mass - definition will be ignored. - """ - __repr_attrs__ = ("mdef", "mass_def_strict",) - - def __init__(self, cosmo, mass_def=None, mass_def_strict=True): - cosmo.compute_sigma() - self.mass_def_strict = mass_def_strict - if mass_def is not None: - if self._check_mdef(mass_def): - raise ValueError("Halo bias " + self.name + - " is not compatible with mass definition" + - " Delta = %s, " % (mass_def.Delta) + - " rho = " + mass_def.rho_type) - self.mdef = mass_def - else: - self._default_mdef() - self._setup(cosmo) - - @abstractmethod - def _default_mdef(self): - """ Assigns a default mass definition for this object if - none is passed at initialization. - """ - - def _setup(self, cosmo): - """ Use this function to initialize any internal attributes - of this object. This function is called at the very end of the - constructor call. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - """ - pass - - def _check_mdef_strict(self, mdef): - return False - - def _check_mdef(self, mdef): - """ Return False if the input mass definition agrees with - the definitions for which this parametrization - works. True otherwise. This function gets called at the - start of the constructor call. - - Args: - mdef (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - - Returns: - bool: True if the mass definition is not compatible with - this parametrization. False otherwise. - """ - if self.mass_def_strict: - return self._check_mdef_strict(mdef) - return False - - def _get_consistent_mass(self, cosmo, M, a, mdef_other): - """ Transform a halo mass with a given mass definition into - the corresponding mass definition that was used to initialize - this object. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - M (float or array_like): halo mass in units of M_sun. - a (float): scale factor. - mdef_other (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - - Returns: - float or array_like: mass according to this object's - mass definition. - """ - if mdef_other is not None: - M_use = mdef_other.translate_mass(cosmo, M, a, self.mdef) - else: - M_use = M - return np.log10(M_use) - - def _get_Delta_m(self, cosmo, a): - """ For SO-based mass definitions, this returns the corresponding - value of Delta for a rho_matter-based definition. This is useful - mostly for the Tinker mass functions, which are defined for any - SO mass in general, but explicitly only for Delta_matter. - """ - delta = self.mdef.get_Delta(cosmo, a) - if self.mdef.rho_type == 'matter': - return delta - else: - om_this = omega_x(cosmo, a, self.mdef.rho_type) - om_matt = omega_x(cosmo, a, 'matter') - return delta * om_this / om_matt - - def get_halo_bias(self, cosmo, M, a, mdef_other=None): - """ Returns the halo bias for input parameters. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - M (float or array_like): halo mass in units of M_sun. - a (float): scale factor. - mdef_other (:class:`~pyccl.halos.massdef.MassDef`): - the mass definition object that defines M. - - Returns: - float or array_like: halo bias. - """ - M_use = np.atleast_1d(M) - logM = self._get_consistent_mass(cosmo, M_use, a, mdef_other) - - # sigma(M) - status = 0 - sigM, status = lib.sigM_vec(cosmo.cosmo, a, logM, len(logM), status) - check(status, cosmo=cosmo) - - b = self._get_bsigma(cosmo, sigM, a) - if np.ndim(M) == 0: - b = b[0] - return b - - @abstractmethod - def _get_bsigma(self, cosmo, sigM, a): - """ Get the halo bias as a function of sigmaM. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - sigM (float or array_like): standard deviation in the - overdensity field on the scale of this halo. - a (float): scale factor. - - Returns: - float or array_like: f(sigma_M) function. - """ - - @classmethod - def from_name(cls, name): - """Returns halo bias subclass from name string - - Args: - name (string): a halo bias name - - Returns: - HaloBias subclass corresponding to the input name. - """ - bias_functions = {c.name: c for c in HaloBias.__subclasses__()} - if name in bias_functions: - return bias_functions[name] - else: - raise ValueError( - f"Halo bias parametrization {name} not implemented") - - -class HaloBiasSheth99(HaloBias): - """ Implements halo bias described in 1999MNRAS.308..119S - This parametrization is only valid for 'fof' masses. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - mass_def (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - this parametrization accepts FoF masses only. - If `None`, FoF masses will be used. - mass_def_strict (bool): if False, consistency of the mass - definition will be ignored. - use_delta_c_fit (bool): if True, use delta_crit given by - the fit of Nakamura & Suto 1997. Otherwise use - delta_crit = 1.68647. - """ - __repr_attrs__ = ("mdef", "mass_def_strict", "use_delta_c_fit",) - name = "Sheth99" - - def __init__(self, cosmo, mass_def=None, - mass_def_strict=True, - use_delta_c_fit=False): - self.use_delta_c_fit = use_delta_c_fit - super(HaloBiasSheth99, self).__init__(cosmo, - mass_def, - mass_def_strict) - - def _default_mdef(self): - self.mdef = MassDef('fof', 'matter') - - def _setup(self, cosmo): - self.p = 0.3 - self.a = 0.707 - - def _check_mdef_strict(self, mdef): - if self.mass_def_strict: - if mdef.Delta != 'fof': - return True - return False - - def _get_bsigma(self, cosmo, sigM, a): - if self.use_delta_c_fit: - status = 0 - delta_c, status = lib.dc_NakamuraSuto(cosmo.cosmo, a, status) - check(status, cosmo=cosmo) - else: - delta_c = 1.68647 - - nu = delta_c / sigM - anu2 = self.a * nu**2 - return 1. + (anu2 - 1. + 2. * self.p / (1. + anu2**self.p))/delta_c - - -class HaloBiasSheth01(HaloBias): - """ Implements halo bias described in arXiv:astro-ph/9907024. - This parametrization is only valid for 'fof' masses. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - mass_def (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - this parametrization accepts FoF masses only. - If `None`, FoF masses will be used. - mass_def_strict (bool): if False, consistency of the mass - definition will be ignored. - """ - name = "Sheth01" - - def __init__(self, cosmo, mass_def=None, mass_def_strict=True): - super(HaloBiasSheth01, self).__init__(cosmo, - mass_def, - mass_def_strict) - - def _default_mdef(self): - self.mdef = MassDef('fof', 'matter') - - def _setup(self, cosmo): - self.a = 0.707 - self.sqrta = 0.84083292038 - self.b = 0.5 - self.c = 0.6 - self.dc = 1.68647 - - def _check_mdef_strict(self, mdef): - if mdef.Delta != 'fof': - return True - return False - - def _get_bsigma(self, cosmo, sigM, a): - nu = self.dc/sigM - anu2 = self.a * nu**2 - anu2c = anu2**self.c - t1 = self.b * (1.0 - self.c) * (1.0 - 0.5 * self.c) - return 1. + (self.sqrta * anu2 * (1 + self.b / anu2c) - - anu2c / (anu2c + t1)) / (self.sqrta * self.dc) - - -class HaloBiasBhattacharya11(HaloBias): - """ Implements halo bias described in arXiv:1005.2239. - This parametrization is only valid for 'fof' masses. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - mass_def (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - this parametrization accepts FoF masses only. - If `None`, FoF masses will be used. - mass_def_strict (bool): if False, consistency of the mass - definition will be ignored. - """ - name = "Bhattacharya11" - - def __init__(self, cosmo, mass_def=None, mass_def_strict=True): - super(HaloBiasBhattacharya11, self).__init__(cosmo, - mass_def, - mass_def_strict) - - def _default_mdef(self): - self.mdef = MassDef('fof', 'matter') - - def _setup(self, cosmo): - self.a = 0.788 - self.az = 0.01 - self.p = 0.807 - self.q = 1.795 - self.dc = 1.68647 - - def _check_mdef_strict(self, mdef): - if mdef.Delta != 'fof': - return True - return False - - def _get_bsigma(self, cosmo, sigM, a): - nu = self.dc / sigM - a = self.a * a**self.az - anu2 = a * nu**2 - return 1. + (anu2 - self.q + 2*self.p / (1 + anu2**self.p)) / self.dc - - -class HaloBiasTinker10(HaloBias): - """ Implements halo bias described in arXiv:1001.3162. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - mass_def (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - this parametrization accepts SO masses with - 200 < Delta < 3200 with respect to the matter density. - If `None`, Delta = 200 (matter) will be used. - mass_def_strict (bool): if False, consistency of the mass - definition will be ignored. - """ - name = "Tinker10" - - def __init__(self, cosmo, mass_def=None, mass_def_strict=True): - super(HaloBiasTinker10, self).__init__(cosmo, - mass_def, - mass_def_strict) - - def _default_mdef(self): - self.mdef = MassDef200m() - - def _AC(self, ld): - xp = np.exp(-(4./ld)**4.) - A = 1.0 + 0.24 * ld * xp - C = 0.019 + 0.107 * ld + 0.19*xp - return A, C - - def _a(self, ld): - return 0.44 * ld - 0.88 - - def _setup(self, cosmo): - self.B = 0.183 - self.b = 1.5 - self.c = 2.4 - self.dc = 1.68647 - - def _check_mdef_strict(self, mdef): - if mdef.Delta == 'fof': - return True - return False - - def _get_bsigma(self, cosmo, sigM, a): - nu = self.dc / sigM - - ld = np.log10(self._get_Delta_m(cosmo, a)) - A, C = self._AC(ld) - aa = self._a(ld) - nupa = nu**aa - return 1. - A * nupa / (nupa + self.dc**aa) + \ - self.B * nu**self.b + C * nu**self.c - - -@functools.wraps(HaloBias.from_name) -def halo_bias_from_name(name): - return HaloBias.from_name(name) diff --git a/pyccl/halos/hbias/__init__.py b/pyccl/halos/hbias/__init__.py new file mode 100644 index 000000000..cc8265f41 --- /dev/null +++ b/pyccl/halos/hbias/__init__.py @@ -0,0 +1,5 @@ +from ..halo_model_base import HaloBias, halo_bias_from_name +from .bhattacharya11 import * +from .sheth01 import * +from .sheth99 import * +from .tinker10 import * diff --git a/pyccl/halos/hbias/bhattacharya11.py b/pyccl/halos/hbias/bhattacharya11.py new file mode 100644 index 000000000..964b5a413 --- /dev/null +++ b/pyccl/halos/hbias/bhattacharya11.py @@ -0,0 +1,43 @@ +from ...base import warn_api +from ...base.parameters import physical_constants as const +from ..halo_model_base import HaloBias + + +__all__ = ("HaloBiasBhattacharya11",) + + +class HaloBiasBhattacharya11(HaloBias): + """ Implements halo bias described in arXiv:1005.2239. + This parametrization is only valid for 'fof' masses. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef` or str): + a mass definition object, or a name string. + this parametrization accepts FoF masses only. + If `None`, FoF masses will be used. + mass_def_strict (bool): if False, consistency of the mass + definition will be ignored. + """ + name = "Bhattacharya11" + + @warn_api + def __init__(self, *, + mass_def="fof", + mass_def_strict=True): + super().__init__(mass_def=mass_def, mass_def_strict=mass_def_strict) + + def _check_mass_def_strict(self, mass_def): + return mass_def.Delta != "fof" + + def _setup(self): + self.a = 0.788 + self.az = 0.01 + self.p = 0.807 + self.q = 1.795 + self.dc = const.DELTA_C + + def _get_bsigma(self, cosmo, sigM, a): + nu = self.dc / sigM + a = self.a * a**self.az + anu2 = a * nu**2 + return 1 + (anu2 - self.q + 2*self.p / (1 + anu2**self.p)) / self.dc diff --git a/pyccl/halos/hbias/sheth01.py b/pyccl/halos/hbias/sheth01.py new file mode 100644 index 000000000..1af22812d --- /dev/null +++ b/pyccl/halos/hbias/sheth01.py @@ -0,0 +1,45 @@ +from ...base import warn_api +from ...base.parameters import physical_constants as const +from ..halo_model_base import HaloBias + + +__all__ = ("HaloBiasSheth01",) + + +class HaloBiasSheth01(HaloBias): + """ Implements halo bias described in arXiv:astro-ph/9907024. + This parametrization is only valid for 'fof' masses. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef` or str): + a mass definition object, or a name string. + This parametrization accepts FoF masses only. + If `None`, FoF masses will be used. + mass_def_strict (bool): if False, consistency of the mass + definition will be ignored. + """ + name = "Sheth01" + + @warn_api + def __init__(self, *, + mass_def="fof", + mass_def_strict=True): + super().__init__(mass_def=mass_def, mass_def_strict=mass_def_strict) + + def _check_mass_def_strict(self, mass_def): + return mass_def.Delta != "fof" + + def _setup(self): + self.a = 0.707 + self.sqrta = 0.84083292038 + self.b = 0.5 + self.c = 0.6 + self.dc = const.DELTA_C + + def _get_bsigma(self, cosmo, sigM, a): + nu = self.dc/sigM + anu2 = self.a * nu**2 + anu2c = anu2**self.c + t1 = self.b * (1.0 - self.c) * (1.0 - 0.5 * self.c) + return 1 + (self.sqrta * anu2 * (1 + self.b / anu2c) - + anu2c / (anu2c + t1)) / (self.sqrta * self.dc) diff --git a/pyccl/halos/hbias/sheth99.py b/pyccl/halos/hbias/sheth99.py new file mode 100644 index 000000000..9c7c1bda4 --- /dev/null +++ b/pyccl/halos/hbias/sheth99.py @@ -0,0 +1,55 @@ +from ... import ccllib as lib +from ...base import warn_api +from ...base.parameters import physical_constants as const +from ...core import check +from ..halo_model_base import HaloBias + + +__all__ = ("HaloBiasSheth99",) + + +class HaloBiasSheth99(HaloBias): + """ Implements halo bias described in 1999MNRAS.308..119S + This parametrization is only valid for 'fof' masses. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef` or str): + a mass definition object, or a name string. + This parametrization accepts FoF masses only. + If `None`, FoF masses will be used. + mass_def_strict (bool): if False, consistency of the mass + definition will be ignored. + use_delta_c_fit (bool): if True, use delta_c given by + the fit of Nakamura & Suto 1997. Otherwise use + delta_c = 1.68647. + """ + __repr_attrs__ = __eq_attrs__ = ("mass_def", "mass_def_strict", + "use_delta_c_fit",) + name = "Sheth99" + + @warn_api + def __init__(self, *, + mass_def="fof", + mass_def_strict=True, + use_delta_c_fit=False): + self.use_delta_c_fit = use_delta_c_fit + super().__init__(mass_def=mass_def, mass_def_strict=mass_def_strict) + + def _check_mass_def_strict(self, mass_def): + return mass_def.Delta != "fof" + + def _setup(self): + self.p = 0.3 + self.a = 0.707 + + def _get_bsigma(self, cosmo, sigM, a): + if self.use_delta_c_fit: + status = 0 + delta_c, status = lib.dc_NakamuraSuto(cosmo.cosmo, a, status) + check(status, cosmo=cosmo) + else: + delta_c = const.DELTA_C + + nu = delta_c / sigM + anu2 = self.a * nu**2 + return 1 + (anu2 - 1. + 2. * self.p / (1. + anu2**self.p))/delta_c diff --git a/pyccl/halos/hbias/tinker10.py b/pyccl/halos/hbias/tinker10.py new file mode 100644 index 000000000..83b08ff97 --- /dev/null +++ b/pyccl/halos/hbias/tinker10.py @@ -0,0 +1,49 @@ +from ...base import warn_api +from ...base.parameters import physical_constants as const +from ..halo_model_base import HaloBias +import numpy as np + + +__all__ = ("HaloBiasTinker10",) + + +class HaloBiasTinker10(HaloBias): + """ Implements halo bias described in arXiv:1001.3162. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef` or str): + a mass definition object, or a name string. + This parametrization accepts SO masses with + 200 < Delta < 3200 with respect to the matter density. + If `None`, Delta = 200 (matter) will be used. + mass_def_strict (bool): if False, consistency of the mass + definition will be ignored. + """ + name = "Tinker10" + + @warn_api + def __init__(self, *, + mass_def="200m", + mass_def_strict=True): + super().__init__(mass_def=mass_def, mass_def_strict=mass_def_strict) + + def _check_mass_def_strict(self, mass_def): + # True for FoF since Tinker10 is not defined for this mass def. + return mass_def.Delta == "fof" + + def _setup(self): + self.B = 0.183 + self.b = 1.5 + self.c = 2.4 + self.dc = const.DELTA_C + + def _get_bsigma(self, cosmo, sigM, a): + nu = self.dc / sigM + ld = np.log10(self.mass_def._get_Delta_m(cosmo, a)) + xp = np.exp(-(4./ld)**4.) + A = 1.0 + 0.24 * ld * xp + C = 0.019 + 0.107 * ld + 0.19*xp + aa = 0.44 * ld - 0.88 + nupa = nu**aa + return 1 - A * nupa / (nupa + self.dc**aa) + ( + self.B * nu**self.b + C * nu**self.c) diff --git a/pyccl/halos/hmfunc.py b/pyccl/halos/hmfunc.py deleted file mode 100644 index d687ac790..000000000 --- a/pyccl/halos/hmfunc.py +++ /dev/null @@ -1,777 +0,0 @@ -from .. import ccllib as lib -from ..core import check -from ..background import omega_x -from ..parameters import physical_constants -from ..base import CCLHalosObject -from .massdef import MassDef, MassDef200m -import numpy as np -import functools -from abc import abstractmethod - - -class MassFunc(CCLHalosObject): - """ This class enables the calculation of halo mass functions. - We currently assume that all mass functions can be written as - - .. math:: - \\frac{dn}{d\\log_{10}M} = f(\\sigma_M)\\,\\frac{\\rho_M}{M}\\, - \\frac{d\\log \\sigma_M}{d\\log_{10} M} - - where :math:`\\sigma_M^2` is the overdensity variance on spheres with a - radius given by the Lagrangian radius for mass M. - All sub-classes implementing specific mass function parametrizations - can therefore be simply created by replacing this class' - `_get_fsigma` method. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - mass_def (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object that fixes - the mass definition used by this mass function - parametrization. - mass_def_strict (bool): if False, consistency of the mass - definition will be ignored. - """ - __repr_attrs__ = ("mdef", "mass_def_strict",) - - def __init__(self, cosmo, mass_def=None, mass_def_strict=True): - # Initialize sigma(M) splines if needed - cosmo.compute_sigma() - self.mass_def_strict = mass_def_strict - # Check if mass function was provided and check that it's - # sensible. - if mass_def is not None: - if self._check_mdef(mass_def): - raise ValueError("Mass function " + self.name + - " is not compatible with mass definition" + - " Delta = %s, " % (mass_def.Delta) + - " rho = " + mass_def.rho_type) - self.mdef = mass_def - else: - self._default_mdef() - self._setup(cosmo) - - @abstractmethod - def _default_mdef(self): - """ Assigns a default mass definition for this object if - none is passed at initialization. - """ - - def _setup(self, cosmo): - """ Use this function to initialize any internal attributes - of this object. This function is called at the very end of the - constructor call. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - """ - pass - - def _check_mdef_strict(self, mdef): - return False - - def _check_mdef(self, mdef): - """ Return False if the input mass definition agrees with - the definitions for which this mass function parametrization - works. True otherwise. This function gets called at the - start of the constructor call. - - Args: - mdef (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - - Returns: - bool: True if the mass definition is not compatible with \ - this mass function parametrization. False otherwise. - """ - if self.mass_def_strict: - return self._check_mdef_strict(mdef) - return False - - def _get_consistent_mass(self, cosmo, M, a, mdef_other): - """ Transform a halo mass with a given mass definition into - the corresponding mass definition that was used to initialize - this object. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - M (float or array_like): halo mass in units of M_sun. - a (float): scale factor. - mdef_other (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - - Returns: - float or array_like: mass according to this object's \ - mass definition. - """ - if mdef_other is not None: - M_use = mdef_other.translate_mass(cosmo, M, a, self.mdef) - else: - M_use = M - return np.log10(M_use) - - def _get_Delta_m(self, cosmo, a): - """ For SO-based mass definitions, this returns the corresponding - value of Delta for a rho_matter-based definition. This is useful - mostly for the Tinker mass functions, which are defined for any - SO mass in general, but explicitly only for Delta_matter. - """ - delta = self.mdef.get_Delta(cosmo, a) - if self.mdef.rho_type == 'matter': - return delta - else: - om_this = omega_x(cosmo, a, self.mdef.rho_type) - om_matt = omega_x(cosmo, a, 'matter') - return delta * om_this / om_matt - - def get_mass_function(self, cosmo, M, a, mdef_other=None): - """ Returns the mass function for input parameters. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - M (float or array_like): halo mass in units of M_sun. - a (float): scale factor. - mdef_other (:class:`~pyccl.halos.massdef.MassDef`): - the mass definition object that defines M. - - Returns: - float or array_like: mass function \ - :math:`dn/d\\log_{10}M` in units of Mpc^-3 (comoving). - """ - M_use = np.atleast_1d(M) - logM = self._get_consistent_mass(cosmo, M_use, - a, mdef_other) - - # sigma(M) - status = 0 - sigM, status = lib.sigM_vec(cosmo.cosmo, a, logM, - len(logM), status) - check(status, cosmo=cosmo) - # dlogsigma(M)/dlog10(M) - dlns_dlogM, status = lib.dlnsigM_dlogM_vec(cosmo.cosmo, a, logM, - len(logM), status) - check(status, cosmo=cosmo) - - rho = (physical_constants.RHO_CRITICAL * - cosmo['Omega_m'] * cosmo['h']**2) - f = self._get_fsigma(cosmo, sigM, a, 2.302585092994046 * logM) - mf = f * rho * dlns_dlogM / M_use - - if np.ndim(M) == 0: - mf = mf[0] - return mf - - @abstractmethod - def _get_fsigma(self, cosmo, sigM, a, lnM): - """ Get the :math:`f(\\sigma_M)` function for this mass function - object (see description of this class for details). - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - sigM (float or array_like): standard deviation in the - overdensity field on the scale of this halo. - a (float): scale factor. - lnM (float or array_like): natural logarithm of the - halo mass in units of M_sun (provided in addition - to sigM for convenience in some mass function - parametrizations). - - Returns: - float or array_like: :math:`f(\\sigma_M)` function. - """ - - @classmethod - def from_name(cls, name): - """ Returns mass function subclass from name string - - Args: - name (string): a mass function name - - Returns: - MassFunc subclass corresponding to the input name. - """ - mass_functions = {c.name: c for c in cls.__subclasses__()} - if name in mass_functions: - return mass_functions[name] - else: - raise ValueError(f"Mass function {name} not implemented.") - - -class MassFuncPress74(MassFunc): - """ Implements mass function described in 1974ApJ...187..425P. - This parametrization is only valid for 'fof' masses. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - mass_def (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - this parametrization accepts FoF masses only. - If `None`, FoF masses will be used. - mass_def_strict (bool): if False, consistency of the mass - definition will be ignored. - """ - name = 'Press74' - - def __init__(self, cosmo, mass_def=None, mass_def_strict=True): - super(MassFuncPress74, self).__init__(cosmo, - mass_def, - mass_def_strict) - - def _default_mdef(self): - self.mdef = MassDef('fof', 'matter') - - def _setup(self, cosmo): - self.norm = np.sqrt(2/np.pi) - - def _check_mdef_strict(self, mdef): - if mdef.Delta != 'fof': - return True - return False - - def _get_fsigma(self, cosmo, sigM, a, lnM): - delta_c = 1.68647 - - nu = delta_c/sigM - return self.norm * nu * np.exp(-0.5 * nu**2) - - -class MassFuncSheth99(MassFunc): - """ Implements mass function described in arXiv:astro-ph/9901122 - This parametrization is only valid for 'fof' masses. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - mass_def (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - this parametrization accepts FoF masses only. - If `None`, FoF masses will be used. - mass_def_strict (bool): if False, consistency of the mass - definition will be ignored. - use_delta_c_fit (bool): if True, use delta_crit given by - the fit of Nakamura & Suto 1997. Otherwise use - delta_crit = 1.68647. - """ - __repr_attrs__ = ("mdef", "mass_def_strict", "use_delta_c_fit",) - name = 'Sheth99' - - def __init__(self, cosmo, mass_def=None, mass_def_strict=True, - use_delta_c_fit=False): - self.use_delta_c_fit = use_delta_c_fit - super(MassFuncSheth99, self).__init__(cosmo, - mass_def, - mass_def_strict) - - def _default_mdef(self): - self.mdef = MassDef('fof', 'matter') - - def _setup(self, cosmo): - self.A = 0.21615998645 - self.p = 0.3 - self.a = 0.707 - - def _check_mdef_strict(self, mdef): - if mdef.Delta != 'fof': - return True - - def _get_fsigma(self, cosmo, sigM, a, lnM): - if self.use_delta_c_fit: - status = 0 - delta_c, status = lib.dc_NakamuraSuto(cosmo.cosmo, a, status) - check(status, cosmo=cosmo) - else: - delta_c = 1.68647 - - nu = delta_c / sigM - return nu * self.A * (1. + (self.a * nu**2)**(-self.p)) * \ - np.exp(-self.a * nu**2/2.) - - -class MassFuncJenkins01(MassFunc): - """ Implements mass function described in astro-ph/0005260. - This parametrization is only valid for 'fof' masses. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - mass_def (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - this parametrization accepts FoF masses only. - If `None`, FoF masses will be used. - mass_def_strict (bool): if False, consistency of the mass - definition will be ignored. - """ - name = 'Jenkins01' - - def __init__(self, cosmo, mass_def=None, mass_def_strict=True): - super(MassFuncJenkins01, self).__init__(cosmo, - mass_def=mass_def, - mass_def_strict=True) - - def _default_mdef(self): - self.mdef = MassDef('fof', 'matter') - - def _setup(self, cosmo): - self.A = 0.315 - self.b = 0.61 - self.q = 3.8 - - def _check_mdef_strict(self, mdef): - if mdef.Delta != 'fof': - return True - return False - - def _get_fsigma(self, cosmo, sigM, a, lnM): - return self.A * np.exp(-np.fabs(-np.log(sigM) + self.b)**self.q) - - -class MassFuncTinker08(MassFunc): - """ Implements mass function described in arXiv:0803.2706. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - mass_def (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - this parametrization accepts SO masses with - 200 < Delta < 3200 with respect to the matter density. - If `None`, Delta = 200 (matter) will be used. - mass_def_strict (bool): if False, consistency of the mass - definition will be ignored. - """ - name = 'Tinker08' - - def __init__(self, cosmo, mass_def=None, mass_def_strict=True): - super(MassFuncTinker08, self).__init__(cosmo, - mass_def, - mass_def_strict) - - def _default_mdef(self): - self.mdef = MassDef200m() - - def _pd(self, ld): - return 10.**(-(0.75/(ld - 1.8750612633))**1.2) - - def _setup(self, cosmo): - from scipy.interpolate import interp1d - - delta = np.array([200.0, 300.0, 400.0, 600.0, 800.0, - 1200.0, 1600.0, 2400.0, 3200.0]) - alpha = np.array([0.186, 0.200, 0.212, 0.218, 0.248, - 0.255, 0.260, 0.260, 0.260]) - beta = np.array([1.47, 1.52, 1.56, 1.61, 1.87, - 2.13, 2.30, 2.53, 2.66]) - gamma = np.array([2.57, 2.25, 2.05, 1.87, 1.59, - 1.51, 1.46, 1.44, 1.41]) - phi = np.array([1.19, 1.27, 1.34, 1.45, 1.58, - 1.80, 1.97, 2.24, 2.44]) - ldelta = np.log10(delta) - self.pA0 = interp1d(ldelta, alpha) - self.pa0 = interp1d(ldelta, beta) - self.pb0 = interp1d(ldelta, gamma) - self.pc = interp1d(ldelta, phi) - - def _check_mdef_strict(self, mdef): - if mdef.Delta == 'fof': - return True - return False - - def _get_fsigma(self, cosmo, sigM, a, lnM): - ld = np.log10(self._get_Delta_m(cosmo, a)) - pA = self.pA0(ld) * a**0.14 - pa = self.pa0(ld) * a**0.06 - pb = self.pb0(ld) * a**self._pd(ld) - return pA * ((pb / sigM)**pa + 1) * np.exp(-self.pc(ld)/sigM**2) - - -class MassFuncDespali16(MassFunc): - """ Implements mass function described in arXiv:1507.05627. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - mass_def (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - this parametrization accepts any SO masses. - If `None`, Delta = 200 (matter) will be used. - mass_def_strict (bool): if False, consistency of the mass - definition will be ignored. - """ - __repr_attrs__ = ("mdef", "mass_def_strict", "ellipsoidal",) - name = 'Despali16' - - def __init__(self, cosmo, mass_def=None, mass_def_strict=True, - ellipsoidal=False): - super(MassFuncDespali16, self).__init__(cosmo, - mass_def, - mass_def_strict) - self.ellipsoidal = ellipsoidal - - def _default_mdef(self): - self.mdef = MassDef200m() - - def _setup(self, cosmo): - pass - - def _check_mdef_strict(self, mdef): - if mdef.Delta == 'fof': - return True - return False - - def _get_fsigma(self, cosmo, sigM, a, lnM): - status = 0 - delta_c, status = lib.dc_NakamuraSuto(cosmo.cosmo, a, status) - check(status, cosmo=cosmo) - - Dv, status = lib.Dv_BryanNorman(cosmo.cosmo, a, status) - check(status, cosmo=cosmo) - - x = np.log10(self.mdef.get_Delta(cosmo, a) * - omega_x(cosmo, a, self.mdef.rho_type) / Dv) - - if self.ellipsoidal: - A = -0.1768 * x + 0.3953 - a = 0.3268 * x**2 + 0.2125 * x + 0.7057 - p = -0.04570 * x**2 + 0.1937 * x + 0.2206 - else: - A = -0.1362 * x + 0.3292 - a = 0.4332 * x**2 + 0.2263 * x + 0.7665 - p = -0.1151 * x**2 + 0.2554 * x + 0.2488 - - nu = delta_c/sigM - nu_p = a * nu**2 - - return 2.0 * A * np.sqrt(nu_p / 2.0 / np.pi) * \ - np.exp(-0.5 * nu_p) * (1.0 + nu_p**-p) - - -class MassFuncTinker10(MassFunc): - """ Implements mass function described in arXiv:1001.3162. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - mass_def (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - this parametrization accepts SO masses with - 200 < Delta < 3200 with respect to the matter density. - If `None`, Delta = 200 (matter) will be used. - mass_def_strict (bool): if False, consistency of the mass - definition will be ignored. - norm_all_z (bool): should we normalize the mass function - at z=0 or at all z? - """ - __repr_attrs__ = ("mdef", "mass_def_strict", "norm_all_z",) - name = 'Tinker10' - - def __init__(self, cosmo, mass_def=None, mass_def_strict=True, - norm_all_z=False): - self.norm_all_z = norm_all_z - super(MassFuncTinker10, self).__init__(cosmo, - mass_def, - mass_def_strict) - - def _default_mdef(self): - self.mdef = MassDef200m() - - def _setup(self, cosmo): - from scipy.interpolate import interp1d - - delta = np.array([200.0, 300.0, 400.0, 600.0, 800.0, - 1200.0, 1600.0, 2400.0, 3200.0]) - alpha = np.array([0.368, 0.363, 0.385, 0.389, 0.393, - 0.365, 0.379, 0.355, 0.327]) - beta = np.array([0.589, 0.585, 0.544, 0.543, 0.564, - 0.623, 0.637, 0.673, 0.702]) - gamma = np.array([0.864, 0.922, 0.987, 1.09, 1.20, - 1.34, 1.50, 1.68, 1.81]) - phi = np.array([-0.729, -0.789, -0.910, -1.05, -1.20, - -1.26, -1.45, -1.50, -1.49]) - eta = np.array([-0.243, -0.261, -0.261, -0.273, -0.278, - -0.301, -0.301, -0.319, -0.336]) - - ldelta = np.log10(delta) - self.pA0 = interp1d(ldelta, alpha) - self.pa0 = interp1d(ldelta, eta) - self.pb0 = interp1d(ldelta, beta) - self.pc0 = interp1d(ldelta, gamma) - self.pd0 = interp1d(ldelta, phi) - if self.norm_all_z: - p = np.array([-0.158, -0.195, -0.213, -0.254, -0.281, - -0.349, -0.367, -0.435, -0.504]) - q = np.array([0.0128, 0.0128, 0.0143, 0.0154, 0.0172, - 0.0174, 0.0199, 0.0203, 0.0205]) - self.pp0 = interp1d(ldelta, p) - self.pq0 = interp1d(ldelta, q) - - def _check_mdef_strict(self, mdef): - if mdef.Delta == 'fof': - return True - return False - - def _get_fsigma(self, cosmo, sigM, a, lnM): - ld = np.log10(self._get_Delta_m(cosmo, a)) - nu = 1.686 / sigM - # redshift evolution only up to z=3 - a = np.clip(a, 0.25, 1) - pa = self.pa0(ld) * a**(-0.27) - pb = self.pb0(ld) * a**(-0.20) - pc = self.pc0(ld) * a**0.01 - pd = self.pd0(ld) * a**0.08 - pA0 = self.pA0(ld) - if self.norm_all_z: - z = 1./a - 1 - pp = self.pp0(ld) - pq = self.pq0(ld) - pA0 *= np.exp(z*(pp+pq*z)) - return nu * pA0 * (1 + (pb * nu)**(-2 * pd)) * \ - nu**(2 * pa) * np.exp(-0.5 * pc * nu**2) - - -class MassFuncBocquet16(MassFunc): - """ Implements mass function described in arXiv:1502.07357. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - mass_def (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - this parametrization accepts SO masses with - Delta = 200 (matter, critical) and 500 (critical). - If `None`, Delta = 200 (matter) will be used. - mass_def_strict (bool): if False, consistency of the mass - definition will be ignored. - hydro (bool): if `False`, use the parametrization found - using dark-matter-only simulations. Otherwise, include - baryonic effects (default). - """ - __repr_attrs__ = ("mdef", "mass_def_strict", "hydro",) - name = 'Bocquet16' - - def __init__(self, cosmo, mass_def=None, mass_def_strict=True, - hydro=True): - self.hydro = hydro - super(MassFuncBocquet16, self).__init__(cosmo, - mass_def, - mass_def_strict) - - def _default_mdef(self): - self.mdef = MassDef200m() - - def _setup(self, cosmo): - if int(self.mdef.Delta) == 200: - if self.mdef.rho_type == 'matter': - self.mdef_type = '200m' - elif self.mdef.rho_type == 'critical': - self.mdef_type = '200c' - elif int(self.mdef.Delta) == 500: - if self.mdef.rho_type == 'critical': - self.mdef_type = '500c' - if self.mdef_type == '200m': - if self.hydro: - self.A0 = 0.228 - self.a0 = 2.15 - self.b0 = 1.69 - self.c0 = 1.30 - self.Az = 0.285 - self.az = -0.058 - self.bz = -0.366 - self.cz = -0.045 - else: - self.A0 = 0.175 - self.a0 = 1.53 - self.b0 = 2.55 - self.c0 = 1.19 - self.Az = -0.012 - self.az = -0.040 - self.bz = -0.194 - self.cz = -0.021 - elif self.mdef_type == '200c': - if self.hydro: - self.A0 = 0.202 - self.a0 = 2.21 - self.b0 = 2.00 - self.c0 = 1.57 - self.Az = 1.147 - self.az = 0.375 - self.bz = -1.074 - self.cz = -0.196 - else: - self.A0 = 0.222 - self.a0 = 1.71 - self.b0 = 2.24 - self.c0 = 1.46 - self.Az = 0.269 - self.az = 0.321 - self.bz = -0.621 - self.cz = -0.153 - elif self.mdef_type == '500c': - if self.hydro: - self.A0 = 0.180 - self.a0 = 2.29 - self.b0 = 2.44 - self.c0 = 1.97 - self.Az = 1.088 - self.az = 0.150 - self.bz = -1.008 - self.cz = -0.322 - else: - self.A0 = 0.241 - self.a0 = 2.18 - self.b0 = 2.35 - self.c0 = 2.02 - self.Az = 0.370 - self.az = 0.251 - self.bz = -0.698 - self.cz = -0.310 - - def _check_mdef_strict(self, mdef): - if isinstance(mdef.Delta, str): - return True - elif int(mdef.Delta) == 200: - if (mdef.rho_type != 'matter') and \ - (mdef.rho_type != 'critical'): - return True - elif int(mdef.Delta) == 500: - if mdef.rho_type != 'critical': - return True - else: - return True - return False - - def _get_fsigma(self, cosmo, sigM, a, lnM): - zp1 = 1./a - AA = self.A0 * zp1**self.Az - aa = self.a0 * zp1**self.az - bb = self.b0 * zp1**self.bz - cc = self.c0 * zp1**self.cz - - f = AA * ((sigM / bb)**-aa + 1.0) * np.exp(-cc / sigM**2) - - if self.mdef_type == '200c': - z = 1./a-1 - Omega_m = omega_x(cosmo, a, "matter") - gamma0 = 3.54E-2 + Omega_m**0.09 - gamma1 = 4.56E-2 + 2.68E-2 / Omega_m - gamma2 = 0.721 + 3.50E-2 / Omega_m - gamma3 = 0.628 + 0.164 / Omega_m - delta0 = -1.67E-2 + 2.18E-2 * Omega_m - delta1 = 6.52E-3 - 6.86E-3 * Omega_m - gamma = gamma0 + gamma1 * np.exp(-((gamma2 - z) / gamma3)**2) - delta = delta0 + delta1 * z - M200c_M200m = gamma + delta * lnM - f *= M200c_M200m - elif self.mdef_type == '500c': - z = 1./a-1 - Omega_m = omega_x(cosmo, a, "matter") - alpha0 = 0.880 + 0.329 * Omega_m - alpha1 = 1.00 + 4.31E-2 / Omega_m - alpha2 = -0.365 + 0.254 / Omega_m - alpha = alpha0 * (alpha1 * z + alpha2) / (z + alpha2) - beta = -1.7E-2 + 3.74E-3 * Omega_m - M500c_M200m = alpha + beta * lnM - f *= M500c_M200m - return f - - -class MassFuncWatson13(MassFunc): - """ Implements mass function described in arXiv:1212.0095. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - mass_def (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - this parametrization accepts fof and any SO masses. - If `None`, Delta = 200 (matter) will be used. - mass_def_strict (bool): if False, consistency of the mass - definition will be ignored. - """ - name = 'Watson13' - - def __init__(self, cosmo, mass_def=None, mass_def_strict=True): - super(MassFuncWatson13, self).__init__(cosmo, - mass_def, - mass_def_strict) - - def _default_mdef(self): - self.mdef = MassDef200m() - - def _setup(self, cosmo): - self.is_fof = self.mdef.Delta == 'fof' - - def _check_mdef_strict(self, mdef): - if mdef.Delta == 'vir': - return True - return False - - def _get_fsigma(self, cosmo, sigM, a, lnM): - if self.is_fof: - pA = 0.282 - pa = 2.163 - pb = 1.406 - pc = 1.210 - return pA * ((pb / sigM)**pa + 1.) * np.exp(-pc / sigM**2) - else: - om = omega_x(cosmo, a, "matter") - Delta_178 = self.mdef.Delta / 178.0 - - if a == 1.0: - pA = 0.194 - pa = 1.805 - pb = 2.267 - pc = 1.287 - elif a < 0.14285714285714285: # z>6 - pA = 0.563 - pa = 3.810 - pb = 0.874 - pc = 1.453 - else: - pA = om * (1.097 * a**3.216 + 0.074) - pa = om * (5.907 * a**3.058 + 2.349) - pb = om * (3.136 * a**3.599 + 2.344) - pc = 1.318 - - f_178 = pA * ((pb / sigM)**pa + 1.) * np.exp(-pc / sigM**2) - C = np.exp(0.023 * (Delta_178 - 1.0)) - d = -0.456 * om - 0.139 - Gamma = (C * Delta_178**d * - np.exp(0.072 * (1.0 - Delta_178) / sigM**2.130)) - return f_178 * Gamma - - -class MassFuncAngulo12(MassFunc): - """ Implements mass function described in arXiv:1203.3216. - This parametrization is only valid for 'fof' masses. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - mass_def (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - this parametrization accepts FoF masses only. - If `None`, FoF masses will be used. - mass_def_strict (bool): if False, consistency of the mass - definition will be ignored. - """ - name = 'Angulo12' - - def __init__(self, cosmo, mass_def=None, mass_def_strict=True): - super(MassFuncAngulo12, self).__init__(cosmo, - mass_def, - mass_def_strict) - - def _default_mdef(self): - self.mdef = MassDef('fof', 'matter') - - def _setup(self, cosmo): - self.A = 0.201 - self.a = 2.08 - self.b = 1.7 - self.c = 1.172 - - def _check_mdef_strict(self, mdef): - if mdef.Delta != 'fof': - return True - return False - - def _get_fsigma(self, cosmo, sigM, a, lnM): - return self.A * ((self.a / sigM)**self.b + 1.) * \ - np.exp(-self.c / sigM**2) - - -@functools.wraps(MassFunc.from_name) -def mass_function_from_name(name): - return MassFunc.from_name(name) diff --git a/pyccl/halos/hmfunc/__init__.py b/pyccl/halos/hmfunc/__init__.py new file mode 100644 index 000000000..f65c26483 --- /dev/null +++ b/pyccl/halos/hmfunc/__init__.py @@ -0,0 +1,10 @@ +from ..halo_model_base import MassFunc, mass_function_from_name +from .angulo12 import * +from .bocquet16 import * +from .despali16 import * +from .jenkins01 import * +from .press74 import * +from .sheth99 import * +from .tinker08 import * +from .tinker10 import * +from .watson13 import * diff --git a/pyccl/halos/hmfunc/angulo12.py b/pyccl/halos/hmfunc/angulo12.py new file mode 100644 index 000000000..3bc31b582 --- /dev/null +++ b/pyccl/halos/hmfunc/angulo12.py @@ -0,0 +1,40 @@ +from ...base import warn_api +from ..halo_model_base import MassFunc +import numpy as np + + +__all__ = ("MassFuncAngulo12",) + + +class MassFuncAngulo12(MassFunc): + """ Implements mass function described in arXiv:1203.3216. + This parametrization is only valid for 'fof' masses. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef` or str): + a mass definition object, or a name string. + this parametrization accepts FoF masses only. + If `None`, FoF masses will be used. + mass_def_strict (bool): if False, consistency of the mass + definition will be ignored. + """ + name = 'Angulo12' + + @warn_api + def __init__(self, *, + mass_def="fof", + mass_def_strict=True): + super().__init__(mass_def=mass_def, mass_def_strict=mass_def_strict) + + def _check_mass_def_strict(self, mass_def): + return mass_def.Delta != "fof" + + def _setup(self): + self.A = 0.201 + self.a = 2.08 + self.b = 1.7 + self.c = 1.172 + + def _get_fsigma(self, cosmo, sigM, a, lnM): + return self.A * ((self.a / sigM)**self.b + 1.) * ( + np.exp(-self.c / sigM**2)) diff --git a/pyccl/halos/hmfunc/bocquet16.py b/pyccl/halos/hmfunc/bocquet16.py new file mode 100644 index 000000000..155038e40 --- /dev/null +++ b/pyccl/halos/hmfunc/bocquet16.py @@ -0,0 +1,101 @@ +from ...base import warn_api +from ..halo_model_base import MassFunc +import numpy as np + + +__all__ = ("MassFuncBocquet16",) + + +class MassFuncBocquet16(MassFunc): + """ Implements mass function described in arXiv:1502.07357. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef` or str): + a mass definition object, or a name string. + This parametrization accepts SO masses with + Delta = 200 (matter, critical) and 500 (critical). + The default is '200m'. + mass_def_strict (bool): if False, consistency of the mass + definition will be ignored. + hydro (bool): if `False`, use the parametrization found + using dark-matter-only simulations. Otherwise, include + baryonic effects (default). + """ + __repr_attrs__ = __eq_attrs__ = ("mass_def", "mass_def_strict", "hydro",) + _mass_def_strict_always = True + name = 'Bocquet16' + + @warn_api + def __init__(self, *, + mass_def="200m", + mass_def_strict=True, + hydro=True): + self.hydro = hydro + super().__init__(mass_def=mass_def, mass_def_strict=mass_def_strict) + + def _check_mass_def_strict(self, mass_def): + return mass_def.name not in ["200m", "200c", "500c"] + + def _setup(self): + # key: (hydro, mass_def.name) + vals = {(True, "200m"): (0.228, 2.15, 1.69, 1.30, + 0.285, -0.058, -0.366, -0.045), + (False, "200m"): (0.175, 1.53, 2.55, 1.19, + -0.012, -0.040, -0.194, -0.021), + (True, "200c"): (0.202, 2.21, 2.00, 1.57, + 1.147, 0.375, -1.074, -0.196), + (False, "200c"): (0.222, 1.71, 2.24, 1.46, + 0.269, 0.321, -0.621, -0.153), + (True, "500c"): (0.180, 2.29, 2.44, 1.97, + 1.088, 0.150, -1.008, -0.322), + (False, "500c"): (0.241, 2.18, 2.35, 2.02, + 0.370, 0.251, -0.698, -0.310)} + + key = (self.hydro, self.mass_def.name) + self.A0, self.a0, self.b0, self.c0, \ + self.Az, self.az, self.bz, self.cz = vals[key] + + def _M200c_M200m(self, cosmo, a): + # Translates the parameters of M200c to those of M200m + # for which the base Bocquet16 model is defined. + z = 1/a - 1 + Omega_m = cosmo.omega_x(a, "matter") + gamma0 = 3.54E-2 + Omega_m**0.09 + gamma1 = 4.56E-2 + 2.68E-2 / Omega_m + gamma2 = 0.721 + 3.50E-2 / Omega_m + gamma3 = 0.628 + 0.164 / Omega_m + delta0 = -1.67E-2 + 2.18E-2 * Omega_m + delta1 = 6.52E-3 - 6.86E-3 * Omega_m + gamma = gamma0 + gamma1 * np.exp(-((gamma2 - z) / gamma3)**2) + delta = delta0 + delta1 * z + return gamma, delta + + def _M500c_M200m(self, cosmo, a): + # Translates the parameters of M500c to those of M200m + # for which the base Bocquet16 model is defined. + z = 1/a - 1 + Omega_m = cosmo.omega_x(a, "matter") + alpha0 = 0.880 + 0.329 * Omega_m + alpha1 = 1.00 + 4.31E-2 / Omega_m + alpha2 = -0.365 + 0.254 / Omega_m + alpha = alpha0 * (alpha1 * z + alpha2) / (z + alpha2) + beta = -1.7E-2 + 3.74E-3 * Omega_m + return alpha, beta + + def _get_fsigma(self, cosmo, sigM, a, lnM): + zp1 = 1./a + AA = self.A0 * zp1**self.Az + aa = self.a0 * zp1**self.az + bb = self.b0 * zp1**self.bz + cc = self.c0 * zp1**self.cz + + f = AA * ((sigM / bb)**-aa + 1.0) * np.exp(-cc / sigM**2) + + if self.mass_def.name == '200c': + gamma, delta = self._M200c_M200m(cosmo, a) + f *= gamma + delta * lnM + elif self.mass_def.name == '500c': + alpha, beta = self._M500c_M200m(cosmo, a) + f *= alpha + beta * lnM + + return f diff --git a/pyccl/halos/hmfunc/despali16.py b/pyccl/halos/hmfunc/despali16.py new file mode 100644 index 000000000..175f00c9e --- /dev/null +++ b/pyccl/halos/hmfunc/despali16.py @@ -0,0 +1,65 @@ +from ...base import warn_api +from ... import ccllib as lib +from ...pyutils import check +from ..halo_model_base import MassFunc +import numpy as np + + +__all__ = ("MassFuncDespali16",) + + +class MassFuncDespali16(MassFunc): + """ Implements mass function described in arXiv:1507.05627. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef` or str): + a mass definition object, or a name string. + This parametrization accepts any SO masses. + The default is '200m'. + mass_def_strict (bool): if False, consistency of the mass + definition will be ignored. + ellipsoidal (bool): use the ellipsoidal parametrization. + """ + __repr_attrs__ = __eq_attrs__ = ("mass_def", "mass_def_strict", + "ellipsoidal",) + name = 'Despali16' + + @warn_api + def __init__(self, *, + mass_def="200m", + mass_def_strict=True, + ellipsoidal=False): + self.ellipsoidal = ellipsoidal + super().__init__(mass_def=mass_def, mass_def_strict=mass_def_strict) + + def _check_mass_def_strict(self, mass_def): + # True for FoF since Despali16 is not defined for this mass def. + return mass_def.Delta == "fof" + + def _setup(self): + # key: ellipsoidal + vals = {True: (0.3953, -0.1768, 0.7057, 0.2125, 0.3268, + 0.2206, 0.1937, -0.04570), + False: (0.3292, -0.1362, 0.7665, 0.2263, 0.4332, + 0.2488, 0.2554, -0.1151)} + + A0, A1, a0, a1, a2, p0, p1, p2 = vals[self.ellipsoidal] + coeffs = [[A1, A0], [a2, a1, a0], [p2, p2, p0]] + self.poly_A, self.poly_a, self.poly_p = map(np.poly1d, coeffs) + + def _get_fsigma(self, cosmo, sigM, a, lnM): + status = 0 + delta_c, status = lib.dc_NakamuraSuto(cosmo.cosmo, a, status) + check(status, cosmo=cosmo) + + Dv, status = lib.Dv_BryanNorman(cosmo.cosmo, a, status) + check(status, cosmo=cosmo) + + x = np.log10(self.mass_def.get_Delta(cosmo, a) * + cosmo.omega_x(a, self.mass_def.rho_type) / Dv) + + A, a, p = self.poly_A(x), self.poly_a(x), self.poly_p(x) + + nu_p = a * (delta_c/sigM)**2 + return 2.0 * A * np.sqrt(nu_p / 2.0 / np.pi) * ( + np.exp(-0.5 * nu_p) * (1.0 + nu_p**-p)) diff --git a/pyccl/halos/hmfunc/jenkins01.py b/pyccl/halos/hmfunc/jenkins01.py new file mode 100644 index 000000000..2dee3c5e2 --- /dev/null +++ b/pyccl/halos/hmfunc/jenkins01.py @@ -0,0 +1,38 @@ +from ...base import warn_api +from ..halo_model_base import MassFunc +import numpy as np + + +__all__ = ("MassFuncJenkins01",) + + +class MassFuncJenkins01(MassFunc): + """ Implements mass function described in astro-ph/0005260. + This parametrization is only valid for 'fof' masses. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef` or str): + a mass definition object, or a name string. + This parametrization accepts FoF masses only. + The default is 'fof'. + mass_def_strict (bool): if False, consistency of the mass + definition will be ignored. + """ + name = 'Jenkins01' + + @warn_api + def __init__(self, *, + mass_def="fof", + mass_def_strict=True): + super().__init__(mass_def=mass_def, mass_def_strict=mass_def_strict) + + def _check_mass_def_strict(self, mass_def): + return mass_def.Delta != "fof" + + def _setup(self): + self.A = 0.315 + self.b = 0.61 + self.q = 3.8 + + def _get_fsigma(self, cosmo, sigM, a, lnM): + return self.A * np.exp(-np.abs(-np.log(sigM) + self.b)**self.q) diff --git a/pyccl/halos/hmfunc/press74.py b/pyccl/halos/hmfunc/press74.py new file mode 100644 index 000000000..0925a50f8 --- /dev/null +++ b/pyccl/halos/hmfunc/press74.py @@ -0,0 +1,37 @@ +from ...base import warn_api +from ...base.parameters import physical_constants as const +from ..halo_model_base import MassFunc +import numpy as np + + +__all__ = ("MassFuncPress74",) + + +class MassFuncPress74(MassFunc): + """ Implements mass function described in 1974ApJ...187..425P. + This parametrization is only valid for 'fof' masses. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef` or str): + a mass definition object, or a name string. + This parametrization accepts FoF masses only. + The default is 'fof'. + mass_def_strict (bool): if False, consistency of the mass + definition will be ignored. + """ + name = 'Press74' + + @warn_api + def __init__(self, *, + mass_def="fof", + mass_def_strict=True): + super().__init__(mass_def=mass_def, mass_def_strict=mass_def_strict) + self._norm = np.sqrt(2/np.pi) + + def _check_mass_def_strict(self, mass_def): + return mass_def.Delta != "fof" + + def _get_fsigma(self, cosmo, sigM, a, lnM): + delta_c = const.DELTA_C + nu = delta_c/sigM + return self._norm * nu * np.exp(-0.5 * nu**2) diff --git a/pyccl/halos/hmfunc/sheth99.py b/pyccl/halos/hmfunc/sheth99.py new file mode 100644 index 000000000..5a3b1fc21 --- /dev/null +++ b/pyccl/halos/hmfunc/sheth99.py @@ -0,0 +1,57 @@ +from ... import ccllib as lib +from ...base import warn_api +from ...base.parameters import physical_constants as const +from ...pyutils import check +from ..halo_model_base import MassFunc +import numpy as np + + +__all__ = ("MassFuncSheth99",) + + +class MassFuncSheth99(MassFunc): + """ Implements mass function described in arXiv:astro-ph/9901122 + This parametrization is only valid for 'fof' masses. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef` or str): + a mass definition object, or a name string. + This parametrization accepts FoF masses only. + The default is 'fof'. + mass_def_strict (bool): if False, consistency of the mass + definition will be ignored. + use_delta_c_fit (bool): if True, use delta_c given by + the fit of Nakamura & Suto 1997. Otherwise use + delta_c = 1.68647. + """ + __repr_attrs__ = __eq_attrs__ = ("mass_def", "mass_def_strict", + "use_delta_c_fit",) + name = 'Sheth99' + + @warn_api + def __init__(self, *, + mass_def="fof", + mass_def_strict=True, + use_delta_c_fit=False): + self.use_delta_c_fit = use_delta_c_fit + super().__init__(mass_def=mass_def, mass_def_strict=mass_def_strict) + + def _check_mass_def_strict(self, mass_def): + return mass_def.Delta != "fof" + + def _setup(self): + self.A = 0.21615998645 + self.p = 0.3 + self.a = 0.707 + + def _get_fsigma(self, cosmo, sigM, a, lnM): + if self.use_delta_c_fit: + status = 0 + delta_c, status = lib.dc_NakamuraSuto(cosmo.cosmo, a, status) + check(status, cosmo=cosmo) + else: + delta_c = const.DELTA_C + + nu = delta_c / sigM + return nu * self.A * (1. + (self.a * nu**2)**(-self.p)) * ( + np.exp(-self.a * nu**2/2.)) diff --git a/pyccl/halos/hmfunc/tinker08.py b/pyccl/halos/hmfunc/tinker08.py new file mode 100644 index 000000000..d4602b48c --- /dev/null +++ b/pyccl/halos/hmfunc/tinker08.py @@ -0,0 +1,56 @@ +from ...base import warn_api +from ..halo_model_base import MassFunc +import numpy as np +from scipy.interpolate import interp1d + + +__all__ = ("MassFuncTinker08",) + + +class MassFuncTinker08(MassFunc): + """ Implements mass function described in arXiv:0803.2706. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef` or str): + a mass definition object, or a name string. + This parametrization accepts SO masses with + 200 < Delta < 3200 with respect to the matter density. + The default is '200m'. + mass_def_strict (bool): if False, consistency of the mass + definition will be ignored. + """ + name = 'Tinker08' + + @warn_api + def __init__(self, *, + mass_def="200m", + mass_def_strict=True): + super().__init__(mass_def=mass_def, mass_def_strict=mass_def_strict) + + def _check_mass_def_strict(self, mass_def): + return mass_def.Delta == "fof" + + def _setup(self): + delta = np.array( + [200., 300., 400., 600., 800., 1200., 1600., 2400., 3200.]) + alpha = np.array( + [0.186, 0.200, 0.212, 0.218, 0.248, 0.255, 0.260, 0.260, 0.260]) + beta = np.array( + [1.47, 1.52, 1.56, 1.61, 1.87, 2.13, 2.30, 2.53, 2.66]) + gamma = np.array( + [2.57, 2.25, 2.05, 1.87, 1.59, 1.51, 1.46, 1.44, 1.41]) + phi = np.array( + [1.19, 1.27, 1.34, 1.45, 1.58, 1.80, 1.97, 2.24, 2.44]) + ldelta = np.log10(delta) + self.pA0 = interp1d(ldelta, alpha) + self.pa0 = interp1d(ldelta, beta) + self.pb0 = interp1d(ldelta, gamma) + self.pc = interp1d(ldelta, phi) + + def _get_fsigma(self, cosmo, sigM, a, lnM): + ld = np.log10(self.mass_def._get_Delta_m(cosmo, a)) + pA = self.pA0(ld) * a**0.14 + pa = self.pa0(ld) * a**0.06 + pd = 10.**(-(0.75/(ld - 1.8750612633))**1.2) + pb = self.pb0(ld) * a**pd + return pA * ((pb / sigM)**pa + 1) * np.exp(-self.pc(ld)/sigM**2) diff --git a/pyccl/halos/hmfunc/tinker10.py b/pyccl/halos/hmfunc/tinker10.py new file mode 100644 index 000000000..5cf760a77 --- /dev/null +++ b/pyccl/halos/hmfunc/tinker10.py @@ -0,0 +1,87 @@ +from ...base import warn_api +from ...base.parameters import physical_constants as const +from ..halo_model_base import MassFunc +import numpy as np +from scipy.interpolate import interp1d + + +__all__ = ("MassFuncTinker10",) + + +class MassFuncTinker10(MassFunc): + """ Implements mass function described in arXiv:1001.3162. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef` or str): + a mass definition object, or a name string. + This parametrization accepts SO masses with + 200 < Delta < 3200 with respect to the matter density. + If `None`, Delta = 200 (matter) will be used. + mass_def_strict (bool): if False, consistency of the mass + definition will be ignored. + norm_all_z (bool): should we normalize the mass function + at z=0 or at all z? + """ + __repr_attrs__ = __eq_attrs__ = ("mass_def", "mass_def_strict", + "norm_all_z",) + name = 'Tinker10' + + @warn_api + def __init__(self, *, + mass_def="200m", + mass_def_strict=True, + norm_all_z=False): + self.norm_all_z = norm_all_z + super().__init__(mass_def=mass_def, mass_def_strict=mass_def_strict) + + def _check_mass_def_strict(self, mass_def): + return mass_def.Delta == "fof" + + def _setup(self): + delta = np.array( + [200., 300., 400., 600., 800., 1200., 1600., 2400., 3200.]) + alpha = np.array( + [0.368, 0.363, 0.385, 0.389, 0.393, 0.365, 0.379, 0.355, 0.327]) + beta = np.array( + [0.589, 0.585, 0.544, 0.543, 0.564, 0.623, 0.637, 0.673, 0.702]) + gamma = np.array( + [0.864, 0.922, 0.987, 1.09, 1.20, 1.34, 1.50, 1.68, 1.81]) + phi = np.array( + [-0.729, -0.789, -0.910, -1.05, -1.20, -1.26, -1.45, -1.50, -1.49]) + eta = np.array( + [-0.243, -0.261, -0.261, -0.273, + -0.278, -0.301, -0.301, -0.319, -0.336]) + + ldelta = np.log10(delta) + self.pA0 = interp1d(ldelta, alpha) + self.pa0 = interp1d(ldelta, eta) + self.pb0 = interp1d(ldelta, beta) + self.pc0 = interp1d(ldelta, gamma) + self.pd0 = interp1d(ldelta, phi) + if self.norm_all_z: + p = np.array( + [-0.158, -0.195, -0.213, -0.254, -0.281, + -0.349, -0.367, -0.435, -0.504]) + q = np.array( + [0.0128, 0.0128, 0.0143, 0.0154, 0.0172, + 0.0174, 0.0199, 0.0203, 0.0205]) + self.pp0 = interp1d(ldelta, p) + self.pq0 = interp1d(ldelta, q) + + def _get_fsigma(self, cosmo, sigM, a, lnM): + ld = np.log10(self.mass_def._get_Delta_m(cosmo, a)) + nu = const.DELTA_C / sigM + # redshift evolution only up to z=3 + a = np.clip(a, 0.25, 1) + pa = self.pa0(ld) * a**(-0.27) + pb = self.pb0(ld) * a**(-0.20) + pc = self.pc0(ld) * a**0.01 + pd = self.pd0(ld) * a**0.08 + pA0 = self.pA0(ld) + if self.norm_all_z: + z = 1/a - 1 + pp = self.pp0(ld) + pq = self.pq0(ld) + pA0 *= np.exp(z * (pp + pq * z)) + return nu * pA0 * (1 + (pb * nu)**(-2 * pd)) * ( + nu**(2 * pa) * np.exp(-0.5 * pc * nu**2)) diff --git a/pyccl/halos/hmfunc/watson13.py b/pyccl/halos/hmfunc/watson13.py new file mode 100644 index 000000000..740403209 --- /dev/null +++ b/pyccl/halos/hmfunc/watson13.py @@ -0,0 +1,70 @@ +from ...base import warn_api +from ..halo_model_base import MassFunc +import numpy as np + + +__all__ = ("MassFuncWatson13",) + + +class MassFuncWatson13(MassFunc): + """ Implements mass function described in arXiv:1212.0095. + + Args: + mass_def (:class:`~pyccl.halos.massdef.MassDef` or str): + a mass definition object, or a name string. + This parametrization accepts fof and any SO masses. + The default is '200m'. + If `None`, Delta = 200 (matter) will be used. + mass_def_strict (bool): if False, consistency of the mass + definition will be ignored. + """ + name = 'Watson13' + + @warn_api + def __init__(self, *, + mass_def="200m", + mass_def_strict=True): + super().__init__(mass_def=mass_def, mass_def_strict=mass_def_strict) + + def _check_mass_def_strict(self, mass_def): + return mass_def.name == "vir" + + def _get_fsigma_fof(self, cosmo, sigM, a, lnM): + pA = 0.282 + pa = 2.163 + pb = 1.406 + pc = 1.210 + return pA * ((pb / sigM)**pa + 1.) * np.exp(-pc / sigM**2) + + def _get_fsigma_SO(self, cosmo, sigM, a, lnM): + om = cosmo.omega_x(a, "matter") + Delta_178 = self.mass_def.Delta / 178 + + # TODO: this has to be vectorized with numpy + if a == 1: + pA = 0.194 + pa = 1.805 + pb = 2.267 + pc = 1.287 + elif a < 1/(1+6): + pA = 0.563 + pa = 3.810 + pb = 0.874 + pc = 1.453 + else: + pA = om * (1.097 * a**3.216 + 0.074) + pa = om * (5.907 * a**3.058 + 2.349) + pb = om * (3.136 * a**3.599 + 2.344) + pc = 1.318 + + f_178 = pA * ((pb / sigM)**pa + 1.) * np.exp(-pc / sigM**2) + C = np.exp(0.023 * (Delta_178 - 1.0)) + d = -0.456 * om - 0.139 + Gamma = (C * Delta_178**d * + np.exp(0.072 * (1.0 - Delta_178) / sigM**2.130)) + return f_178 * Gamma + + def _get_fsigma(self, cosmo, sigM, a, lnM): + if self.mass_def.name == 'fof': + return self._get_fsigma_fof(cosmo, sigM, a, lnM) + return self._get_fsigma_SO(cosmo, sigM, a, lnM) diff --git a/pyccl/halos/massdef.py b/pyccl/halos/massdef.py index e897e2d5d..99f467faf 100644 --- a/pyccl/halos/massdef.py +++ b/pyccl/halos/massdef.py @@ -1,10 +1,15 @@ from .. import ccllib as lib from ..core import check -from ..background import species_types, rho_x, omega_x -from ..base import CCLHalosObject +from ..background import species_types +from ..base import CCLAutoRepr, CCLNamedClass, warn_api, deprecate_attr import numpy as np +__all__ = ("mass2radius_lagrangian", "convert_concentration", "MassDef", + "MassDef200m", "MassDef200c", "MassDef500c", "MassDefVir", + "MassDefFof",) + + def mass2radius_lagrangian(cosmo, M): """ Returns Lagrangian radius for a halo of mass M. The lagrangian radius is defined as that enclosing @@ -18,13 +23,14 @@ def mass2radius_lagrangian(cosmo, M): float or array_like: lagrangian radius in comoving Mpc. """ M_use = np.atleast_1d(M) - R = (M_use / (4.18879020479 * rho_x(cosmo, 1, 'matter')))**(1./3.) + R = (M_use / (4.18879020479 * cosmo.rho_x(1, 'matter')))**(1./3.) if np.ndim(M) == 0: - R = R[0] + return R[0] return R -def convert_concentration(cosmo, c_old, Delta_old, Delta_new): +@warn_api +def convert_concentration(cosmo, *, c_old, Delta_old, Delta_new): """ Computes the concentration parameter for a different mass definition. This is done assuming an NFW profile. The output concentration `c_new` is found by solving the equation: @@ -55,14 +61,14 @@ def convert_concentration(cosmo, c_old, Delta_old, Delta_new): Delta_old, c_old_use, Delta_new, c_old_use.size, status) - if np.isscalar(c_old): - c_new = c_new[0] - check(status, cosmo=cosmo) + + if np.isscalar(c_old): + return c_new[0] return c_new -class MassDef(CCLHalosObject): +class MassDef(CCLAutoRepr, CCLNamedClass): """Halo mass definition. Halo masses are defined in terms of an overdensity parameter :math:`\\Delta` and an associated density :math:`X` (either the matter density or the critical density): @@ -78,33 +84,36 @@ class MassDef(CCLHalosObject): Delta (float): overdensity parameter. Pass 'vir' if using virial overdensity. rho_type (string): either 'critical' or 'matter'. - c_m_relation (function, optional): concentration-mass relation. + concentration (function, optional): concentration-mass relation. Provided as a `Concentration` object, or a string corresponding to one of the supported concentration-mass relations. If `None`, no c(M) relation will be attached to this mass definition (and hence one can't translate into other definitions). """ - __repr_attrs__ = ("name",) + __repr_attrs__ = __eq_attrs__ = ("name",) + __getattr__ = deprecate_attr(pairs=[('c_m_relation', 'concentration')] + )(super.__getattribute__) - def __init__(self, Delta, rho_type, c_m_relation=None): + @warn_api(pairs=[("c_m_relation", "concentration")]) + def __init__(self, Delta, rho_type=None, *, concentration=None): # Check it makes sense - if (Delta != 'fof') and (Delta != 'vir'): - if isinstance(Delta, str): - raise ValueError("Unknown Delta type " + Delta) - if Delta <= 0: - raise ValueError("Delta must be a positive number") - self.Delta = Delta - # Can only be matter or critical + if isinstance(Delta, str) and Delta not in ["fof", "vir"]: + raise ValueError(f"Unknown Delta type {Delta}.") + if isinstance(Delta, (int, float)) and Delta < 0: + raise ValueError("Delta must be a positive number.") if rho_type not in ['matter', 'critical']: - raise ValueError("rho_type must be either \'matter\' " - "or \'critical\'") + raise ValueError("rho_type must be either ['matter'|'critical].'") + + self.Delta = Delta self.rho_type = rho_type self.species = species_types[rho_type] # c(M) relation - if c_m_relation is None: + if concentration is None: self.concentration = None else: - self._concentration_init(c_m_relation) + from .concentration import Concentration + self.concentration = Concentration.create_instance( + concentration, mass_def=self) @property def name(self): @@ -113,19 +122,6 @@ def name(self): return f"{self.Delta}{self.rho_type[0]}" return f"{self.Delta}" - def _concentration_init(self, c_m_relation): - from .concentration import Concentration, concentration_from_name - if isinstance(c_m_relation, Concentration): - self.concentration = c_m_relation - elif isinstance(c_m_relation, str): - # Grab class - conc_class = concentration_from_name(c_m_relation) - # instantiate with this mass definition - self.concentration = conc_class(mdef=self) - else: - raise ValueError("c_m_relation must be `None`, " - " a string or a `Concentration` object") - def get_Delta(self, cosmo, a): """ Gets overdensity parameter associated to this mass definition. @@ -137,15 +133,25 @@ def get_Delta(self, cosmo, a): Returns: float : value of the overdensity parameter. """ + if self.Delta == 'fof': + raise ValueError("FoF masses don't have an associated overdensity." + "Nor can they be translated into other masses") if self.Delta == 'vir': status = 0 D, status = lib.Dv_BryanNorman(cosmo.cosmo, a, status) return D - elif self.Delta == 'fof': - raise ValueError("FoF masses don't have an associated overdensity." - "Nor can they be translated into other masses") - else: - return self.Delta + return self.Delta + + def _get_Delta_m(self, cosmo, a): + """ For SO-based mass definitions, this returns the corresponding + value of Delta for a rho_matter-based definition. + """ + delta = self.get_Delta(cosmo, a) + if self.rho_type == 'matter': + return delta + om_this = cosmo.omega_x(a, self.rho_type) + om_matt = cosmo.omega_x(a, 'matter') + return delta * om_this / om_matt def get_mass(self, cosmo, R, a): """ Translates a halo radius into a mass @@ -164,9 +170,9 @@ def get_mass(self, cosmo, R, a): """ R_use = np.atleast_1d(R) Delta = self.get_Delta(cosmo, a) - M = 4.18879020479 * rho_x(cosmo, a, self.rho_type) * Delta * R_use**3 + M = 4.18879020479 * cosmo.rho_x(a, self.rho_type) * Delta * R_use**3 if np.ndim(R) == 0: - M = M[0] + return M[0] return M def get_radius(self, cosmo, M, a): @@ -184,56 +190,39 @@ def get_radius(self, cosmo, M, a): M_use = np.atleast_1d(M) Delta = self.get_Delta(cosmo, a) R = (M_use / (4.18879020479 * Delta * - rho_x(cosmo, a, self.rho_type)))**(1./3.) + cosmo.rho_x(a, self.rho_type)))**(1./3.) if np.ndim(M) == 0: - R = R[0] + return R[0] return R - def _get_concentration(self, cosmo, M, a): - """ Returns concentration for this mass definition. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. - M (float or array_like): halo mass in units of M_sun. - a (float): scale factor. - - Returns: - float or array_like: halo concentration. - """ - if self.concentration is None: - raise RuntimeError("This mass definition doesn't have " - "an associated c(M) relation") - else: - return self.concentration.get_concentration(cosmo, M, a) - - def translate_mass(self, cosmo, M, a, m_def_other): + @warn_api(pairs=[("mdef_other", "mass_def_other")]) + def translate_mass(self, cosmo, M, a, *, mass_def_other): """ Translate halo mass in this definition into another definition Args: cosmo (:class:`~pyccl.core.Cosmology`): A Cosmology object. M (float or array_like): halo mass in units of M_sun. a (float): scale factor. - m_def_other (:obj:`MassDef`): another mass definition. + mass_def_other (:obj:`MassDef`): another mass definition. Returns: float or array_like: halo masses in new definition. """ - if self == m_def_other: + if self == mass_def_other: return M - else: - if self.concentration is None: - raise RuntimeError("This mass definition doesn't have " - "an associated c(M) relation") - else: - om_this = omega_x(cosmo, a, self.rho_type) - D_this = self.get_Delta(cosmo, a) * om_this - c_this = self._get_concentration(cosmo, M, a) - R_this = self.get_radius(cosmo, M, a) - om_new = omega_x(cosmo, a, m_def_other.rho_type) - D_new = m_def_other.get_Delta(cosmo, a) * om_new - c_new = convert_concentration(cosmo, c_this, D_this, D_new) - R_new = c_new * R_this / c_this - return m_def_other.get_mass(cosmo, R_new, a) + if self.concentration is None: + raise AttributeError("mass_def has no associated concentration.") + om_this = cosmo.omega_x(a, self.rho_type) + D_this = self.get_Delta(cosmo, a) * om_this + c_this = self.concentration(cosmo, M, a) + R_this = self.get_radius(cosmo, M, a) + om_new = cosmo.omega_x(a, mass_def_other.rho_type) + D_new = mass_def_other.get_Delta(cosmo, a) * om_new + c_new = convert_concentration(cosmo, c_old=c_this, + Delta_old=D_this, + Delta_new=D_new) + R_new = c_new * R_this / c_this + return mass_def_other.get_mass(cosmo, R_new, a) @classmethod def from_name(cls, name): @@ -246,43 +235,63 @@ def from_name(cls, name): Returns: MassDef subclass corresponding to the input name. """ - try: - return eval(f"MassDef{name.capitalize()}") - except NameError: - raise ValueError(f"Mass definition {name} not implemented.") - - -def MassDef200m(c_m='Duffy08'): + MassDefName = f"MassDef{name.capitalize()}" + if MassDefName in globals(): + # MassDef is defined in one of the implementations below. + return globals()[MassDefName] + parser = {"c": "critical", "m": "matter"} + if len(name) < 2 or name[-1] not in parser: + # Bogus input - can't parse it. + raise ValueError("Could not parse mass definition string.") + Delta, rho_type = name[:-1], parser[name[-1]] + return lambda cm=None: cls(Delta, rho_type, concentration=cm) # noqa + + +@warn_api(pairs=[('c_m', 'concentration')]) +def MassDef200m(concentration='Duffy08'): r""":math:`\Delta = 200m` mass definition. Args: - c_m (string): concentration-mass relation. + concentration (string): concentration-mass relation. """ - return MassDef(200, 'matter', c_m_relation=c_m) + return MassDef(200, 'matter', concentration=concentration) -def MassDef200c(c_m='Duffy08'): +@warn_api(pairs=[('c_m', 'concentration')]) +def MassDef200c(concentration='Duffy08'): r""":math:`\Delta = 200c` mass definition. Args: - c_m (string): concentration-mass relation. + concentration (string): concentration-mass relation. """ - return MassDef(200, 'critical', c_m_relation=c_m) + return MassDef(200, 'critical', concentration=concentration) -def MassDef500c(c_m='Ishiyama21'): +@warn_api(pairs=[('c_m', 'concentration')]) +def MassDef500c(concentration='Ishiyama21'): r""":math:`\Delta = 500m` mass definition. Args: c_m (string): concentration-mass relation. """ - return MassDef(500, 'critical', c_m_relation=c_m) + return MassDef(500, 'critical', concentration=concentration) -def MassDefVir(c_m='Klypin11'): +@warn_api(pairs=[('c_m', 'concentration')]) +def MassDefVir(concentration='Klypin11'): r""":math:`\Delta = \rm vir` mass definition. Args: - c_m (string): concentration-mass relation. + concentration (string): concentration-mass relation. + """ + return MassDef('vir', 'critical', concentration=concentration) + + +@warn_api(pairs=[('c_m', 'concentration')]) +def MassDefFof(concentration=None): + r""":math:`\Delta = \rm FoF` mass definition. + + Args: + concentration (string): concentration-mass relation. """ - return MassDef('vir', 'critical', c_m_relation=c_m) + return MassDef('fof', 'matter', concentration=concentration) diff --git a/pyccl/halos/pk_1pt.py b/pyccl/halos/pk_1pt.py new file mode 100644 index 000000000..b0d528607 --- /dev/null +++ b/pyccl/halos/pk_1pt.py @@ -0,0 +1,87 @@ +from ..base import UnlockInstance, warn_api +import numpy as np + + +__all__ = ("halomod_mean_profile_1pt", "halomod_bias_1pt",) + + +def _Ix1(func, cosmo, hmc, k, a, prof, normprof): + # I_X_1 dispatcher for internal use + """ + Args: + cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. + hmc (:class:`HMCalculator`): a halo model calculator. + k (float or array_like): comoving wavenumber in Mpc^-1. + a (float or array_like): scale factor. + prof (:class:`~pyccl.halos.profiles.HaloProfile`): halo + profile. + normprof (bool): (Deprecated - do not use) + if `True`, this integral will be + normalized by :math:`I^0_1(k\\rightarrow 0,a|u)` + (see :meth:`~HMCalculator.I_0_1`), where + :math:`u` is the profile represented by `prof`. + + Returns: + float or array_like: integral values evaluated at each + combination of `k` and `a`. The shape of the output will + be `(N_a, N_k)` where `N_k` and `N_a` are the sizes of + `k` and `a` respectively. If `k` or `a` are scalars, the + corresponding dimension will be squeezed out on output. + """ + # TODO: Remove for CCLv3. + if normprof is not None: + with UnlockInstance(prof): + prof.normprof = normprof + + func = getattr(hmc, func) + + a_use = np.atleast_1d(a).astype(float) + k_use = np.atleast_1d(k).astype(float) + + na = len(a_use) + nk = len(k_use) + out = np.zeros([na, nk]) + for ia, aa in enumerate(a_use): + i11 = func(cosmo, k_use, aa, prof) + norm = hmc.get_profile_norm(cosmo, aa, prof) + out[ia] = i11 * norm + + if np.ndim(a) == 0: + out = np.squeeze(out, axis=0) + if np.ndim(k) == 0: + out = np.squeeze(out, axis=-1) + return out + + +@warn_api +def halomod_mean_profile_1pt(cosmo, hmc, k, a, prof, *, normprof=None): + """ Returns the mass-weighted mean halo profile. + + .. math:: + I^0_1(k,a|u) = \\int dM\\,n(M,a)\\,\\langle u(k,a|M)\\rangle, + + where :math:`n(M,a)` is the halo mass function, and + :math:`\\langle u(k,a|M)\\rangle` is the halo profile as a + function of scale, scale factor and halo mass. + """ + return _Ix1("I_0_1", cosmo, hmc, k, a, prof, normprof) + + +@warn_api +def halomod_bias_1pt(cosmo, hmc, k, a, prof, *, normprof=None): + """ Returns the mass-and-bias-weighted mean halo profile. + + .. math:: + I^1_1(k,a|u) = \\int dM\\,n(M,a)\\,b(M,a)\\, + \\langle u(k,a|M)\\rangle, + + where :math:`n(M,a)` is the halo mass function, + :math:`b(M,a)` is the halo bias, and + :math:`\\langle u(k,a|M)\\rangle` is the halo profile as a + function of scale, scale factor and halo mass. + """ + return _Ix1("I_1_1", cosmo, hmc, k, a, prof, normprof) + + +halomod_mean_profile_1pt.__doc__ += _Ix1.__doc__ +halomod_bias_1pt.__doc__ += _Ix1.__doc__ diff --git a/pyccl/halos/pk_2pt.py b/pyccl/halos/pk_2pt.py new file mode 100644 index 000000000..8c1bcee64 --- /dev/null +++ b/pyccl/halos/pk_2pt.py @@ -0,0 +1,264 @@ +from ..base import UnlockInstance, warn_api +from ..pk2d import Pk2D, parse_pk +from .profiles_2pt import Profile2pt +import numpy as np + + +__all__ = ("halomod_power_spectrum", "halomod_Pk2D",) + + +@warn_api(pairs=[("supress_1h", "suppress_1h")], + reorder=["prof_2pt", "prof2", "p_of_k_a", "normprof1", "normprof2"]) +def halomod_power_spectrum(cosmo, hmc, k, a, prof, *, + prof2=None, prof_2pt=None, + normprof1=None, normprof2=None, + p_of_k_a=None, + get_1h=True, get_2h=True, + smooth_transition=None, suppress_1h=None, + extrap_pk=False): + """ Computes the halo model power spectrum for two + quantities defined by their respective halo profiles. + The halo model power spectrum for two profiles + :math:`u` and :math:`v` is: + + .. math:: + P_{u,v}(k,a) = I^0_2(k,a|u,v) + + I^1_1(k,a|u)\\,I^1_1(k,a|v)\\,P_{\\rm lin}(k,a) + + where :math:`P_{\\rm lin}(k,a)` is the linear matter + power spectrum, :math:`I^1_1` is defined in the documentation + of :meth:`~HMCalculator.I_1_1`, and :math:`I^0_2` is defined + in the documentation of :meth:`~HMCalculator.I_0_2`. + + Args: + cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. + hmc (:class:`HMCalculator`): a halo model calculator. + k (float or array_like): comoving wavenumber in Mpc^-1. + a (float or array_like): scale factor. + prof (:class:`~pyccl.halos.profiles.HaloProfile`): halo + profile. + prof2 (:class:`~pyccl.halos.profiles.HaloProfile`): a + second halo profile. If `None`, `prof` will be used as + `prof2`. + normprof1 (bool): (Deprecated - do not use) + if `True`, this integral will be + normalized by :math:`I^0_1(k\\rightarrow 0,a|u)` + (see :meth:`~HMCalculator.I_0_1`), where + :math:`u` is the profile represented by `prof`. + normprof2 (bool): (Deprecated - do not use) + if `True`, this integral will be + normalized by :math:`I^0_1(k\\rightarrow 0,a|v)` + (see :meth:`~HMCalculator.I_0_1`), where + :math:`v` is the profile represented by `prof2`. + prof_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): + a profile covariance object + returning the the two-point moment of the two profiles + being correlated. If `None`, the default second moment + will be used, corresponding to the products of the means + of both profiles. + p_of_k_a (:class:`~pyccl.pk2d.Pk2D`): a `Pk2D` object to + be used as the linear matter power spectrum. If `None`, + the power spectrum stored within `cosmo` will be used. + get_1h (bool): if `False`, the 1-halo term (i.e. the first + term in the first equation above) won't be computed. + get_2h (bool): if `False`, the 2-halo term (i.e. the second + term in the first equation above) won't be computed. + smooth_transition (function or None): + Modify the halo model 1-halo/2-halo transition region + via a time-dependent function :math:`\\alpha(a)`, + defined as in HMCODE-2020 (``arXiv:2009.01858``): :math:`P(k,a)= + (P_{1h}^{\\alpha(a)}(k)+P_{2h}^{\\alpha(a)}(k))^{1/\\alpha}`. + If `None` the extra factor is just 1. + suppress_1h (function or None): + Suppress the 1-halo large scale contribution by a + time- and scale-dependent function :math:`k_*(a)`, + defined as in HMCODE-2020 (``arXiv:2009.01858``): + :math:`\\frac{(k/k_*(a))^4}{1+(k/k_*(a))^4}`. + If `None` the standard 1-halo term is returned with no damping. + extrap_pk (bool): + Whether to extrapolate ``p_of_k_a`` in case ``a`` is out of its + support. If False, and the queried values are out of bounds, + an error is raised. The default is False. + + Returns: + float or array_like: integral values evaluated at each + combination of `k` and `a`. The shape of the output will + be `(N_a, N_k)` where `N_k` and `N_a` are the sizes of + `k` and `a` respectively. If `k` or `a` are scalars, the + corresponding dimension will be squeezed out on output. + """ + a_use = np.atleast_1d(a).astype(float) + k_use = np.atleast_1d(k).astype(float) + + # Check inputs + if smooth_transition is not None: + if not (get_1h and get_2h): + raise ValueError("Transition region can only be modified " + "when both 1-halo and 2-halo terms are queried.") + if suppress_1h is not None: + if not get_1h: + raise ValueError("Can't suppress the 1-halo term " + "when get_1h is False.") + + if prof2 is None: + prof2 = prof + if prof_2pt is None: + prof_2pt = Profile2pt() + + # TODO: Remove for CCLv3. + if normprof1 is not None: + with UnlockInstance(prof): + prof.normprof = normprof1 + if normprof2 is not None: + with UnlockInstance(prof2): + prof2.normprof = normprof2 + + pk2d = parse_pk(cosmo, p_of_k_a) + extrap = cosmo if extrap_pk else None # extrapolation rule for pk2d + + na = len(a_use) + nk = len(k_use) + out = np.zeros([na, nk]) + for ia, aa in enumerate(a_use): + # normalizations + norm1 = hmc.get_profile_norm(cosmo, aa, prof) + + if prof2 == prof: + norm2 = norm1 + else: + norm2 = hmc.get_profile_norm(cosmo, aa, prof2) + + if get_2h: + # bias factors + i11_1 = hmc.I_1_1(cosmo, k_use, aa, prof) + + if prof2 == prof: + i11_2 = i11_1 + else: + i11_2 = hmc.I_1_1(cosmo, k_use, aa, prof2) + + pk_2h = pk2d(k_use, aa, cosmo=extrap) * i11_1 * i11_2 # 2h term + else: + pk_2h = 0 + + if get_1h: + pk_1h = hmc.I_0_2(cosmo, k_use, aa, prof, + prof2=prof2, prof_2pt=prof_2pt) # 1h term + + if suppress_1h is not None: + # large-scale damping of 1-halo term + ks = suppress_1h(aa) + pk_1h *= (k_use / ks)**4 / (1 + (k_use / ks)**4) + else: + pk_1h = 0 + + # smooth 1h/2h transition region + if smooth_transition is None: + out[ia] = (pk_1h + pk_2h) * norm1 * norm2 + else: + alpha = smooth_transition(aa) + out[ia] = (pk_1h**alpha + pk_2h**alpha)**(1/alpha) * norm1 * norm2 + + if np.ndim(a) == 0: + out = np.squeeze(out, axis=0) + if np.ndim(k) == 0: + out = np.squeeze(out, axis=-1) + return out + + +@warn_api(pairs=[("supress_1h", "suppress_1h")], + reorder=["prof_2pt", "prof2", "p_of_k_a", "normprof1", "normprof2"]) +def halomod_Pk2D(cosmo, hmc, prof, *, + prof2=None, prof_2pt=None, + normprof1=None, normprof2=None, + p_of_k_a=None, + get_1h=True, get_2h=True, + lk_arr=None, a_arr=None, + extrap_order_lok=1, extrap_order_hik=2, + smooth_transition=None, suppress_1h=None, extrap_pk=False): + """ Returns a :class:`~pyccl.pk2d.Pk2D` object containing + the halo-model power spectrum for two quantities defined by + their respective halo profiles. See :meth:`halomod_power_spectrum` + for more details about the actual calculation. + + Args: + cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. + hmc (:class:`HMCalculator`): a halo model calculator. + prof (:class:`~pyccl.halos.profiles.HaloProfile`): halo + profile. + prof2 (:class:`~pyccl.halos.profiles.HaloProfile`): a + second halo profile. If `None`, `prof` will be used as + `prof2`. + prof_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): + a profile covariance object + returning the the two-point moment of the two profiles + being correlated. If `None`, the default second moment + will be used, corresponding to the products of the means + of both profiles. + normprof1 (bool): if `True`, this integral will be + normalized by :math:`I^0_1(k\\rightarrow 0,a|u)` + (see :meth:`~HMCalculator.I_0_1`), where + :math:`u` is the profile represented by `prof`. + normprof2 (bool): if `True`, this integral will be + normalized by :math:`I^0_1(k\\rightarrow 0,a|v)` + (see :meth:`~HMCalculator.I_0_1`), where + :math:`v` is the profile represented by `prof2`. + p_of_k_a (:class:`~pyccl.pk2d.Pk2D`): a `Pk2D` object to + be used as the linear matter power spectrum. If `None`, + the power spectrum stored within `cosmo` will be used. + get_1h (bool): if `False`, the 1-halo term (i.e. the first + term in the first equation above) won't be computed. + get_2h (bool): if `False`, the 2-halo term (i.e. the second + term in the first equation above) won't be computed. + a_arr (array): an array holding values of the scale factor + at which the halo model power spectrum should be + calculated for interpolation. If `None`, the internal + values used by `cosmo` will be used. + lk_arr (array): an array holding values of the natural + logarithm of the wavenumber (in units of Mpc^-1) at + which the halo model power spectrum should be calculated + for interpolation. If `None`, the internal values used + by `cosmo` will be used. + extrap_order_lok (int): extrapolation order to be used on + k-values below the minimum of the splines. See + :class:`~pyccl.pk2d.Pk2D`. + extrap_order_hik (int): extrapolation order to be used on + k-values above the maximum of the splines. See + :class:`~pyccl.pk2d.Pk2D`. + smooth_transition (function or None): + Modify the halo model 1-halo/2-halo transition region + via a time-dependent function :math:`\\alpha(a)`, + defined as in HMCODE-2020 (``arXiv:2009.01858``): :math:`P(k,a)= + (P_{1h}^{\\alpha(a)}(k)+P_{2h}^{\\alpha(a)}(k))^{1/\\alpha}`. + If `None` the extra factor is just 1. + suppress_1h (function or None): + Suppress the 1-halo large scale contribution by a + time- and scale-dependent function :math:`k_*(a)`, + defined as in HMCODE-2020 (``arXiv:2009.01858``): + :math:`\\frac{(k/k_*(a))^4}{1+(k/k_*(a))^4}`. + If `None` the standard 1-halo term is returned with no damping. + extrap_pk (bool): + Whether to extrapolate ``p_of_k_a`` in case ``a`` is out of its + support. If False, and the queried values are out of bounds, + an error is raised. The default is False. + + Returns: + :class:`~pyccl.pk2d.Pk2D`: halo model power spectrum. + """ + if lk_arr is None: + lk_arr = cosmo.get_pk_spline_lk() + if a_arr is None: + a_arr = cosmo.get_pk_spline_a() + + pk_arr = halomod_power_spectrum( + cosmo, hmc, np.exp(lk_arr), a_arr, + prof, prof2=prof2, prof_2pt=prof_2pt, p_of_k_a=p_of_k_a, + normprof1=normprof1, normprof2=normprof2, # TODO: remove for CCLv3 + get_1h=get_1h, get_2h=get_2h, + smooth_transition=smooth_transition, suppress_1h=suppress_1h, + extrap_pk=extrap_pk) + + return Pk2D(a_arr=a_arr, lk_arr=lk_arr, pk_arr=pk_arr, + extrap_order_lok=extrap_order_lok, + extrap_order_hik=extrap_order_hik, + is_logp=False) diff --git a/pyccl/halos/pk_4pt.py b/pyccl/halos/pk_4pt.py new file mode 100644 index 000000000..575c1e5dd --- /dev/null +++ b/pyccl/halos/pk_4pt.py @@ -0,0 +1,562 @@ +from ..base import UnlockInstance, warn_api +from ..pk2d import parse_pk +from ..tk3d import Tk3D +from ..errors import CCLWarning +from .profiles import HaloProfileNFW +from .profiles import HaloProfileNumberCounts as ProfNC +from .profiles_2pt import Profile2pt +import numpy as np +import warnings + + +__all__ = ("halomod_trispectrum_1h", "halomod_Tk3D_1h", + "halomod_Tk3D_SSC_linear_bias", "halomod_Tk3D_SSC",) + + +@warn_api(pairs=[("prof1", "prof")], reorder=["prof12_2pt", "prof3", "prof4"]) +def halomod_trispectrum_1h(cosmo, hmc, k, a, prof, *, + prof2=None, prof3=None, prof4=None, + prof12_2pt=None, prof34_2pt=None, + normprof1=None, normprof2=None, + normprof3=None, normprof4=None): + """ Computes the halo model 1-halo trispectrum for four different + quantities defined by their respective halo profiles. The 1-halo + trispectrum for four profiles :math:`u_{1,2}`, :math:`v_{1,2}` is + calculated as: + + .. math:: + T_{u_1,u_2;v_1,v_2}(k_u,k_v,a) = + I^0_{2,2}(k_u,k_v,a|u_{1,2},v_{1,2}) + + where :math:`I^0_{2,2}` is defined in the documentation + of :meth:`~HMCalculator.I_0_22`. + + .. note:: This approximation assumes that the 4-point + profile cumulant is the same as the product of two + 2-point cumulants. We may relax this assumption in + future versions of CCL. + + Args: + cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. + hmc (:class:`HMCalculator`): a halo model calculator. + k (float or array_like): comoving wavenumber in Mpc^-1. + a (float or array_like): scale factor. + prof (:class:`~pyccl.halos.profiles.HaloProfile`): halo + profile (corresponding to :math:`u_1` above. + prof2 (:class:`~pyccl.halos.profiles.HaloProfile`): halo + profile (corresponding to :math:`u_2` above. If `None`, + `prof` will be used as `prof2`. + prof12_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): + a profile covariance object returning the the two-point + moment of `prof` and `prof2`. If `None`, the default + second moment will be used, corresponding to the + products of the means of both profiles. + prof3 (:class:`~pyccl.halos.profiles.HaloProfile`): halo + profile (corresponding to :math:`v_1` above. If `None`, + `prof` will be used as `prof3`. + prof4 (:class:`~pyccl.halos.profiles.HaloProfile`): halo + profile (corresponding to :math:`v_2` above. If `None`, + `prof2` will be used as `prof4`. + prof34_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): + same as `prof12_2pt` for `prof3` and `prof4`. + normprof1 (bool): (Deprecated - do not use) + if `True`, this integral will be + normalized by :math:`I^0_1(k\\rightarrow 0,a|u)` + (see :meth:`~HMCalculator.I_0_1`), where + :math:`u` is the profile represented by `prof`. + normprof2 (bool): same as `normprof1` for `prof2`. + normprof3 (bool): same as `normprof1` for `prof3`. + normprof4 (bool): same as `normprof1` for `prof4`. + + Returns: + float or array_like: integral values evaluated at each + combination of `k` and `a`. The shape of the output will + be `(N_a, N_k, N_k)` where `N_k` and `N_a` are the sizes of + `k` and `a` respectively. The ordering is such that + `output[ia, ik2, ik1] = T(k[ik1], k[ik2], a[ia])` + If `k` or `a` are scalars, the corresponding dimension will + be squeezed out on output. + """ + a_use = np.atleast_1d(a).astype(float) + k_use = np.atleast_1d(k).astype(float) + + # define all the profiles + prof, prof2, prof3, prof4, prof12_2pt, prof34_2pt = \ + _allocate_profiles(prof, prof2, prof3, prof4, prof12_2pt, prof34_2pt, + normprof1, normprof2, normprof3, normprof4) + + na = len(a_use) + nk = len(k_use) + out = np.zeros([na, nk, nk]) + for ia, aa in enumerate(a_use): + # normalizations + norm1 = hmc.get_profile_norm(cosmo, aa, prof) + + if prof2 == prof: + norm2 = norm1 + else: + norm2 = hmc.get_profile_norm(cosmo, aa, prof2) + + if prof3 == prof: + norm3 = norm1 + else: + norm3 = hmc.get_profile_norm(cosmo, aa, prof3) + + if prof4 == prof2: + norm4 = norm2 + else: + norm4 = hmc.get_profile_norm(cosmo, aa, prof4) + + # trispectrum + tk_1h = hmc.I_0_22(cosmo, k_use, aa, + prof=prof, prof2=prof2, + prof4=prof4, prof3=prof3, + prof12_2pt=prof12_2pt, + prof34_2pt=prof34_2pt) + + out[ia] = tk_1h * norm1 * norm2 * norm3 * norm4 # assign + + if np.ndim(a) == 0: + out = np.squeeze(out, axis=0) + if np.ndim(k) == 0: + out = np.squeeze(out, axis=-1) + out = np.squeeze(out, axis=-1) + return out + + +@warn_api(pairs=[("prof1", "prof")], + reorder=["prof12_2pt", "prof3", "prof4"]) +def halomod_Tk3D_1h(cosmo, hmc, prof, *, + prof2=None, prof3=None, prof4=None, + prof12_2pt=None, prof34_2pt=None, + normprof1=None, normprof2=None, + normprof3=None, normprof4=None, + lk_arr=None, a_arr=None, + extrap_order_lok=1, extrap_order_hik=1, + use_log=False): + """ Returns a :class:`~pyccl.tk3d.Tk3D` object containing + the 1-halo trispectrum for four quantities defined by + their respective halo profiles. See :meth:`halomod_trispectrum_1h` + for more details about the actual calculation. + + Args: + cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. + hmc (:class:`HMCalculator`): a halo model calculator. + prof (:class:`~pyccl.halos.profiles.HaloProfile`): halo + profile (corresponding to :math:`u_1` above. + prof2 (:class:`~pyccl.halos.profiles.HaloProfile`): halo + profile (corresponding to :math:`u_2` above. If `None`, + `prof` will be used as `prof2`. + prof3 (:class:`~pyccl.halos.profiles.HaloProfile`): halo + profile (corresponding to :math:`v_1` above. If `None`, + `prof` will be used as `prof3`. + prof4 (:class:`~pyccl.halos.profiles.HaloProfile`): halo + profile (corresponding to :math:`v_2` above. If `None`, + `prof2` will be used as `prof4`. + prof12_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): + a profile covariance object returning the the two-point + moment of `prof` and `prof2`. If `None`, the default + second moment will be used, corresponding to the + products of the means of both profiles. + prof34_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): + same as `prof12_2pt` for `prof3` and `prof4`. + normprof1 (bool): (Deprecated - do not use) + if `True`, this integral will be + normalized by :math:`I^0_1(k\\rightarrow 0,a|u)` + (see :meth:`~HMCalculator.I_0_1`), where + :math:`u` is the profile represented by `prof`. + normprof2 (bool): same as `normprof1` for `prof2`. + normprof3 (bool): same as `normprof1` for `prof3`. + normprof4 (bool): same as `normprof1` for `prof4`. + a_arr (array): an array holding values of the scale factor + at which the trispectrum should be calculated for + interpolation. If `None`, the internal values used + by `cosmo` will be used. + lk_arr (array): an array holding values of the natural + logarithm of the wavenumber (in units of Mpc^-1) at + which the trispectrum should be calculated for + interpolation. If `None`, the internal values used + by `cosmo` will be used. + extrap_order_lok (int): extrapolation order to be used on + k-values below the minimum of the splines. See + :class:`~pyccl.tk3d.Tk3D`. + extrap_order_hik (int): extrapolation order to be used on + k-values above the maximum of the splines. See + :class:`~pyccl.tk3d.Tk3D`. + use_log (bool): if `True`, the trispectrum will be + interpolated in log-space (unless negative or + zero values are found). + + Returns: + :class:`~pyccl.tk3d.Tk3D`: 1-halo trispectrum. + """ + if lk_arr is None: + lk_arr = cosmo.get_pk_spline_lk() + if a_arr is None: + a_arr = cosmo.get_pk_spline_a() + + tkk = halomod_trispectrum_1h(cosmo, hmc, np.exp(lk_arr), a_arr, + prof, prof2=prof2, + prof12_2pt=prof12_2pt, + prof3=prof3, prof4=prof4, + prof34_2pt=prof34_2pt, + normprof1=normprof1, normprof2=normprof2, + normprof3=normprof3, normprof4=normprof4) + + tkk, use_log = _logged_output(tkk, log=use_log) + + return Tk3D(a_arr=a_arr, lk_arr=lk_arr, tkk_arr=tkk, + extrap_order_lok=extrap_order_lok, + extrap_order_hik=extrap_order_hik, is_logt=use_log) + + +@warn_api +def halomod_Tk3D_SSC_linear_bias(cosmo, hmc, *, prof, + bias1=1, bias2=1, bias3=1, bias4=1, + is_number_counts1=False, + is_number_counts2=False, + is_number_counts3=False, + is_number_counts4=False, + p_of_k_a=None, lk_arr=None, + a_arr=None, extrap_order_lok=1, + extrap_order_hik=1, use_log=False, + extrap_pk=False): + """ Returns a :class:`~pyccl.tk3d.Tk3D` object containing + the super-sample covariance trispectrum, given by the tensor + product of the power spectrum responses associated with the + two pairs of quantities being correlated. Each response is + calculated as: + + .. math:: + \\frac{\\partial P_{u,v}(k)}{\\partial\\delta_L} = b_u b_v \\left( + \\left(\\frac{68}{21}-\\frac{d\\log k^3P_L(k)}{d\\log k}\\right) + P_L(k)+I^1_2(k|u,v) - (b_{u} + b_{v}) P_{u,v}(k) \\right) + + where the :math:`I^1_2` is defined in the documentation + :meth:`~HMCalculator.I_1_2` and :math:`b_{}` and :math:`b_{vv}` are the + linear halo biases for quantities :math:`u` and :math:`v`, respectively + (zero if they are not clustering). + + Args: + cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. + hmc (:class:`HMCalculator`): a halo model calculator. + prof (:class:`~pyccl.halos.profiles.HaloProfile`): halo NFW + profile. + bias1 (float or array): linear galaxy bias for quantity 1. If an array, + it has to have the shape of `a_arr`. + bias2 (float or array): linear galaxy bias for quantity 2. + bias3 (float or array): linear galaxy bias for quantity 3. + bias4 (float or array): linear galaxy bias for quantity 4. + is_number_counts1 (bool): If True, quantity 1 will be considered + number counts and the clustering counter terms computed. Default False. + is_number_counts2 (bool): as is_number_counts1 but for quantity 2. + is_number_counts3 (bool): as is_number_counts1 but for quantity 3. + is_number_counts4 (bool): as is_number_counts1 but for quantity 4. + p_of_k_a (:class:`~pyccl.pk2d.Pk2D`): a `Pk2D` object to + be used as the linear matter power spectrum. If `None`, + the power spectrum stored within `cosmo` will be used. + a_arr (array): an array holding values of the scale factor + at which the trispectrum should be calculated for + interpolation. If `None`, the internal values used + by `cosmo` will be used. + lk_arr (array): an array holding values of the natural + logarithm of the wavenumber (in units of Mpc^-1) at + which the trispectrum should be calculated for + interpolation. If `None`, the internal values used + by `cosmo` will be used. + extrap_order_lok (int): extrapolation order to be used on + k-values below the minimum of the splines. See + :class:`~pyccl.tk3d.Tk3D`. + extrap_order_hik (int): extrapolation order to be used on + k-values above the maximum of the splines. See + :class:`~pyccl.tk3d.Tk3D`. + use_log (bool): if `True`, the trispectrum will be + interpolated in log-space (unless negative or + zero values are found). + extrap_pk (bool): + Whether to extrapolate ``p_of_k_a`` in case ``a`` is out of its + support. If False, and the queried values are out of bounds, + an error is raised. The default is False. + + Returns: + :class:`~pyccl.tk3d.Tk3D`: SSC effective trispectrum. + """ + if lk_arr is None: + lk_arr = cosmo.get_pk_spline_lk() + if a_arr is None: + a_arr = cosmo.get_pk_spline_a() + + if not isinstance(prof, HaloProfileNFW): + raise TypeError("prof should be HaloProfileNFW.") + + # Make sure biases are of the form number of a x number of k + ones = np.ones_like(a_arr) + bias1 *= ones + bias2 *= ones + bias3 *= ones + bias4 *= ones + + k_use = np.exp(lk_arr) + prof_2pt = Profile2pt() + + pk2d = parse_pk(cosmo, p_of_k_a) + extrap = cosmo if extrap_pk else None # extrapolation rule for pk2d + + na = len(a_arr) + nk = len(k_use) + dpk12, dpk34 = [np.zeros([na, nk]) for _ in range(2)] + for ia, aa in enumerate(a_arr): + norm = hmc.get_profile_norm(cosmo, aa, prof)**2 + i12 = hmc.I_1_2(cosmo, k_use, aa, prof, prof2=prof, prof_2pt=prof_2pt) + + pk = pk2d(k_use, aa, cosmo=extrap) + dpk = pk2d(k_use, aa, derivative=True, cosmo=extrap) + + # ~ (47/21 - 1/3 dlogPk/dlogk) * Pk + I12 + dpk12[ia] = ((47/21 - dpk/3)*pk + i12 * norm) + dpk34[ia] = dpk12[ia].copy() + + # Counter terms for clustering (i.e. - (bA + bB) * PAB) + if any([is_number_counts1, is_number_counts2, + is_number_counts3, is_number_counts4]): + b1 = b2 = b3 = b4 = 0 + i02 = hmc.I_0_2(cosmo, k_use, aa, prof, + prof2=prof, prof_2pt=prof_2pt) + + P_12 = P_34 = pk + i02 * norm + + if is_number_counts1: + b1 = bias1[ia] + if is_number_counts2: + b2 = bias2[ia] + if is_number_counts3: + b3 = bias3[ia] + if is_number_counts4: + b4 = bias4[ia] + + dpk12[ia] -= (b1 + b2) * P_12 + dpk34[ia] -= (b3 + b4) * P_34 + + dpk12[ia] *= bias1[ia] * bias2[ia] + dpk34[ia] *= bias3[ia] * bias4[ia] + + dpk12, dpk34, use_log = _logged_output(dpk12, dpk34, log=use_log) + + return Tk3D(a_arr=a_arr, lk_arr=lk_arr, + pk1_arr=dpk12, pk2_arr=dpk34, + extrap_order_lok=extrap_order_lok, + extrap_order_hik=extrap_order_hik, is_logt=use_log) + + +@warn_api(pairs=[("prof1", "prof")], + reorder=["prof12_2pt", "prof3", "prof4"]) +def halomod_Tk3D_SSC( + cosmo, hmc, prof, *, prof2=None, prof3=None, prof4=None, + prof12_2pt=None, prof34_2pt=None, + normprof1=None, normprof2=None, normprof3=None, normprof4=None, + p_of_k_a=None, lk_arr=None, a_arr=None, + extrap_order_lok=1, extrap_order_hik=1, use_log=False, + extrap_pk=False): + """ Returns a :class:`~pyccl.tk3d.Tk3D` object containing + the super-sample covariance trispectrum, given by the tensor + product of the power spectrum responses associated with the + two pairs of quantities being correlated. Each response is + calculated as: + + .. math:: + \\frac{\\partial P_{u,v}(k)}{\\partial\\delta_L} = + \\left(\\frac{68}{21}-\\frac{d\\log k^3P_L(k)}{d\\log k}\\right) + P_L(k)I^1_1(k,|u)I^1_1(k,|v)+I^1_2(k|u,v) - (b_{u} + b_{v}) + P_{u,v}(k) + + where the :math:`I^a_b` are defined in the documentation + of :meth:`~HMCalculator.I_1_1` and :meth:`~HMCalculator.I_1_2` and + :math:`b_{u}` and :math:`b_{v}` are the linear halo biases for quantities + :math:`u` and :math:`v`, respectively (zero if they are not clustering). + + Args: + cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. + hmc (:class:`HMCalculator`): a halo model calculator. + prof (:class:`~pyccl.halos.profiles.HaloProfile`): halo + profile (corresponding to :math:`u_1` above. + prof2 (:class:`~pyccl.halos.profiles.HaloProfile`): halo + profile (corresponding to :math:`u_2` above. If `None`, + `prof` will be used as `prof2`. + prof3 (:class:`~pyccl.halos.profiles.HaloProfile`): halo + profile (corresponding to :math:`v_1` above. If `None`, + `prof` will be used as `prof3`. + prof4 (:class:`~pyccl.halos.profiles.HaloProfile`): halo + profile (corresponding to :math:`v_2` above. If `None`, + `prof2` will be used as `prof4`. + prof12_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): + a profile covariance object returning the the two-point + moment of `prof` and `prof2`. If `None`, the default + second moment will be used, corresponding to the + products of the means of both profiles. + prof34_2pt (:class:`~pyccl.halos.profiles_2pt.Profile2pt`): + same as `prof12_2pt` for `prof3` and `prof4`. + normprof1 (bool): (Deprecated - do not use) + if `True`, this integral will be + normalized by :math:`I^0_1(k\\rightarrow 0,a|u)` + (see :meth:`~HMCalculator.I_0_1`), where + :math:`u` is the profile represented by `prof`. + normprof2 (bool): same as `normprof1` for `prof2`. + normprof3 (bool): same as `normprof1` for `prof3`. + normprof4 (bool): same as `normprof1` for `prof4`. + p_of_k_a (:class:`~pyccl.pk2d.Pk2D`): a `Pk2D` object to + be used as the linear matter power spectrum. If `None`, + the power spectrum stored within `cosmo` will be used. + a_arr (array): an array holding values of the scale factor + at which the trispectrum should be calculated for + interpolation. If `None`, the internal values used + by `cosmo` will be used. + lk_arr (array): an array holding values of the natural + logarithm of the wavenumber (in units of Mpc^-1) at + which the trispectrum should be calculated for + interpolation. If `None`, the internal values used + by `cosmo` will be used. + extrap_order_lok (int): extrapolation order to be used on + k-values below the minimum of the splines. See + :class:`~pyccl.tk3d.Tk3D`. + extrap_order_hik (int): extrapolation order to be used on + k-values above the maximum of the splines. See + :class:`~pyccl.tk3d.Tk3D`. + use_log (bool): if `True`, the trispectrum will be + interpolated in log-space (unless negative or + zero values are found). + extrap_pk (bool): + Whether to extrapolate ``p_of_k_a`` in case ``a`` is out of its + support. If False, and the queried values are out of bounds, + an error is raised. The default is False. + + Returns: + :class:`~pyccl.tk3d.Tk3D`: SSC effective trispectrum. + """ + if lk_arr is None: + lk_arr = cosmo.get_pk_spline_lk() + if a_arr is None: + a_arr = cosmo.get_pk_splne_a() + + # define all the profiles + prof, prof2, prof3, prof4, prof12_2pt, prof34_2pt = \ + _allocate_profiles(prof, prof2, prof3, prof4, prof12_2pt, prof34_2pt, + normprof1, normprof2, normprof3, normprof4) + + k_use = np.exp(lk_arr) + pk2d = parse_pk(cosmo, p_of_k_a) + extrap = cosmo if extrap_pk else None # extrapolation rule for pk2d + + dpk12, dpk34 = [np.zeros((len(a_arr), len(k_use))) for _ in range(2)] + for ia, aa in enumerate(a_arr): + # normalizations & I11 integral + norm1 = hmc.get_profile_norm(cosmo, aa, prof) + i11_1 = hmc.I_1_1(cosmo, k_use, aa, prof) + + if prof2 == prof: + norm2 = norm1 + i11_2 = i11_1 + else: + norm2 = hmc.get_profile_norm(cosmo, aa, prof2) + i11_2 = hmc.I_1_1(cosmo, k_use, aa, prof2) + + if prof3 == prof: + norm3 = norm1 + i11_3 = i11_1 + else: + norm3 = hmc.get_profile_norm(cosmo, aa, prof3) + i11_3 = hmc.I_1_1(cosmo, k_use, aa, prof3) + + if prof4 == prof2: + norm4 = norm2 + i11_4 = i11_2 + else: + norm4 = hmc.get_profile_norm(cosmo, aa, prof4) + i11_4 = hmc.I_1_1(cosmo, k_use, aa, prof4) + + # I12 integral + i12_12 = hmc.I_1_2(cosmo, k_use, aa, prof, + prof2=prof2, prof_2pt=prof12_2pt) + if (prof, prof2, prof12_2pt) == (prof3, prof4, prof34_2pt): + i12_34 = i12_12 + else: + i12_34 = hmc.I_1_2(cosmo, k_use, aa, prof3, + prof2=prof4, prof_2pt=prof34_2pt) + + # power spectrum + pk = pk2d(k_use, aa, cosmo=extrap) + dpk = pk2d(k_use, aa, derivative=True, cosmo=extrap) + + # (47/21 - 1/3 dlogPk/dlogk) * I11 * I11 * Pk + I12 + dpk12[ia] = norm1 * norm2 * ((47/21 - dpk/3)*i11_1*i11_2*pk + i12_12) + dpk34[ia] = norm3 * norm4 * ((47/21 - dpk/3)*i11_3*i11_4*pk + i12_34) + + # Counter terms for clustering (i.e. - (bA + bB) * PAB) + def _get_counterterm(pA, pB, p2pt, nA, nB, i11_A, i11_B): + """Helper to compute counter-terms.""" + # p : profiles | p2pt : 2-point | n : norms | i11 : I_1_1 integral + bA = i11_A * nA if isinstance(pA, ProfNC) else np.zeros_like(k_use) + bB = i11_B * nB if isinstance(pB, ProfNC) else np.zeros_like(k_use) + i02 = hmc.I_0_2(cosmo, k_use, aa, pA, prof2=pB, prof_2pt=p2pt) + P = nA * nB * (pk * i11_A * i11_B + i02) + return (bA + bB) * P + + if isinstance(prof, ProfNC) or isinstance(prof2, ProfNC): + dpk12[ia] -= _get_counterterm(prof, prof2, prof12_2pt, + norm1, norm2, i11_1, i11_2) + + if isinstance(prof3, ProfNC) or isinstance(prof4, ProfNC): + if (prof, prof2, prof12_2pt) == (prof3, prof4, prof34_2pt): + dpk34[ia] -= dpk12[ia] + else: + dpk34[ia] -= _get_counterterm(prof3, prof4, prof34_2pt, + norm3, norm4, i11_3, i11_4) + + dpk12, dpk34, use_log = _logged_output(dpk12, dpk34, log=use_log) + + return Tk3D(a_arr=a_arr, lk_arr=lk_arr, + pk1_arr=dpk12, pk2_arr=dpk34, + extrap_order_lok=extrap_order_lok, + extrap_order_hik=extrap_order_hik, is_logt=use_log) + + +def _allocate_profiles(prof, prof2, prof3, prof4, prof12_2pt, prof34_2pt, + normprof1, normprof2, normprof3, normprof4): + """Helper that controls how the undefined profiles are allocated.""" + if prof2 is None: + prof2 = prof + if prof3 is None: + prof3 = prof + if prof4 is None: + prof4 = prof2 + if prof12_2pt is None: + prof12_2pt = Profile2pt() + if prof34_2pt is None: + prof34_2pt = prof12_2pt + + # TODO: Remove for CCLv3. + if normprof1 is not None: + with UnlockInstance(prof): + prof.normprof = normprof1 + if normprof2 is not None: + with UnlockInstance(prof2): + prof2.normprof = normprof2 + if normprof3 is not None: + with UnlockInstance(prof3): + prof3.normprof = normprof3 + if normprof4 is not None: + with UnlockInstance(prof4): + prof4.normprof = normprof4 + + return prof, prof2, prof3, prof4, prof12_2pt, prof34_2pt + + +def _logged_output(*arrs, log): + """Helper that logs the output if needed.""" + if not log: + return *arrs, log + is_negative = [(arr <= 0).any() for arr in arrs] + if any(is_negative): + warnings.warn("Some values were non-positive. " + "Interpolating linearly.", CCLWarning) + return *arrs, False + return *[np.log(arr) for arr in arrs], log diff --git a/pyccl/halos/profiles.py b/pyccl/halos/profiles.py deleted file mode 100644 index a03864f8c..000000000 --- a/pyccl/halos/profiles.py +++ /dev/null @@ -1,1736 +0,0 @@ -from ..background import h_over_h0, sigma_critical -from ..power import sigmaM -from ..pyutils import resample_array, _fftlog_transform -from ..base import CCLHalosObject, UnlockInstance, unlock_instance -from .concentration import Concentration -from .massdef import MassDef -import numpy as np -from scipy.special import sici, erf, gamma, gammainc - - -class HaloProfile(CCLHalosObject): - """ This class implements functionality associated to - halo profiles. You should not use this class directly. - Instead, use one of the subclasses implemented in CCL - for specific halo profiles, or write your own subclass. - `HaloProfile` classes contain methods to compute halo - profiles in real (3D) and Fourier spaces, as well as the - projected (2D) profile and the cumulative mean surface - density. - - A minimal implementation of a `HaloProfile` subclass - should contain a method `_real` that returns the - real-space profile as a function of cosmology, - comoving radius, mass and scale factor. The default - functionality in the base `HaloProfile` class will then - allow the automatic calculation of the Fourier-space - and projected profiles, as well as the cumulative - mass density, based on the real-space profile using - FFTLog to carry out fast Hankel transforms. See the - CCL note for details. Alternatively, you can implement - a `_fourier` method for the Fourier-space profile, and - all other quantities will be computed from it. It is - also possible to implement specific versions of any - of these quantities if one wants to avoid the FFTLog - calculation. - """ - is_number_counts = False - - def __init__(self): - # Check that at least one of (`_real`, `_fourier`) exist. - if not (hasattr(self, "_real") or hasattr(self, "_fourier")): - raise TypeError( - f"Can't instantiate class {self.__class__.__name__} " - "with no methods _real or _fourier") - - self.precision_fftlog = {'padding_lo_fftlog': 0.1, - 'padding_lo_extra': 0.1, - 'padding_hi_fftlog': 10., - 'padding_hi_extra': 10., - 'large_padding_2D': False, - 'n_per_decade': 100, - 'extrapol': 'linx_liny', - 'plaw_fourier': -1.5, - 'plaw_projected': -1.} - - __eq__ = object.__eq__ - - __hash__ = object.__hash__ # TODO: remove once __eq__ is replaced. - - @unlock_instance(mutate=True) - def update_precision_fftlog(self, **kwargs): - """ Update any of the precision parameters used by - FFTLog to compute Hankel transforms. The available - parameters are: - - Args: - padding_lo_fftlog (float): when computing a Hankel - transform we often need to extend the range of the - input (e.g. the r-range for the real-space profile - when computing the Fourier-space one) to avoid - aliasing and boundary effects. This parameter - controls the factor by which we multiply the lower - end of the range (e.g. a value of 0.1 implies that - we will extend the range by one decade on the - left). Note that FFTLog works in logarithmic - space. Default value: 0.1. - padding_hi_fftlog (float): same as `padding_lo_fftlog` - for the upper end of the range (e.g. a value of - 10 implies extending the range by one decade on - the right). Default value: 10. - n_per_decade (float): number of samples of the - profile taken per decade when computing Hankel - transforms. - padding_lo_extra (float): when computing the projected - 2D profile or the 2D cumulative density, - sometimes two Hankel transforms are needed (from - 3D real-space to Fourier, then from Fourier to - 2D real-space). This parameter controls the k range - of the intermediate transform. The logic here is to - avoid the range twice by `padding_lo_fftlog` (which - can be overkill and slow down the calculation). - Default value: 0.1. - padding_hi_extra (float): same as `padding_lo_extra` - for the upper end of the range. Default value: 10. - large_padding_2D (bool): if set to `True`, the - intermediate Hankel transform in the calculation of - the 2D projected profile and cumulative mass - density will use `padding_lo_fftlog` and - `padding_hi_fftlog` instead of `padding_lo_extra` - and `padding_hi_extra` to extend the range of the - intermediate Hankel transform. - extrapol (string): type of extrapolation used in the - uncommon scenario that FFTLog returns a profile on a - range that does not cover the intended output range. - Pass `linx_liny` if you want to extrapolate linearly - in the profile and `linx_logy` if you want to - extrapolate linearly in its logarithm. - Default value: `linx_liny`. - plaw_fourier (float): FFTLog is able to perform more - accurate Hankel transforms by prewhitening its arguments - (essentially making them flatter over the range of - integration to avoid aliasing). This parameter - corresponds to a guess of what the tilt of the profile - is (i.e. profile(r) = r^tilt), which FFTLog uses to - prewhiten it. This parameter is used when computing the - real <-> Fourier transforms. The methods - `_get_plaw_fourier` allows finer control over this - parameter. The default value allows for a slightly faster - (but potentially less accurate) FFTLog transform. Some - level of experimentation with this parameter is - recommended when implementing a new profile. - Default value: -1.5. - plaw_projected (float): same as `plaw_fourier` for the - calculation of the 2D projected and cumulative density - profiles. Finer control can be achieved with the - `_get_plaw_projected`. The default value allows for a - slightly faster (but potentially less accurate) FFTLog - transform. Some level of experimentation with this - parameter is recommended when implementing a new profile. - Default value: -1. - """ - self.precision_fftlog.update(kwargs) - - def _get_plaw_fourier(self, cosmo, a): - """ This controls the value of `plaw_fourier` to be used - as a function of cosmology and scale factor. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. - a (float): scale factor. - - Returns: - float: power law index to be used with FFTLog. - """ - return self.precision_fftlog['plaw_fourier'] - - def _get_plaw_projected(self, cosmo, a): - """ This controls the value of `plaw_projected` to be - used as a function of cosmology and scale factor. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. - a (float): scale factor. - - Returns: - float: power law index to be used with FFTLog. - """ - return self.precision_fftlog['plaw_projected'] - - def real(self, cosmo, r, M, a, mass_def=None): - """ Returns the 3D real-space value of the profile as a - function of cosmology, radius, halo mass and scale factor. - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. - r (float or array_like): comoving radius in Mpc. - M (float or array_like): halo mass in units of M_sun. - a (float): scale factor. - mass_def (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - - Returns: - float or array_like: halo profile. The shape of the - output will be `(N_M, N_r)` where `N_r` and `N_m` are - the sizes of `r` and `M` respectively. If `r` or `M` - are scalars, the corresponding dimension will be - squeezed out on output. - """ - if getattr(self, '_real', None): - f_r = self._real(cosmo, r, M, a, mass_def) - elif getattr(self, '_fourier', None): - f_r = self._fftlog_wrap(cosmo, r, M, a, mass_def, - fourier_out=False) - return f_r - - def fourier(self, cosmo, k, M, a, mass_def): - """ Returns the Fourier-space value of the profile as a - function of cosmology, wavenumber, halo mass and - scale factor. - - .. math:: - \\rho(k)=\\frac{1}{2\\pi^2} \\int dr\\, r^2\\, - \\rho(r)\\, j_0(k r) - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. - k (float or array_like): comoving wavenumber in Mpc^-1. - M (float or array_like): halo mass in units of M_sun. - a (float): scale factor. - mass_def (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - - Returns: - float or array_like: halo profile. The shape of the - output will be `(N_M, N_k)` where `N_k` and `N_m` are - the sizes of `k` and `M` respectively. If `k` or `M` - are scalars, the corresponding dimension will be - squeezed out on output. - """ - if getattr(self, '_fourier', None): - f_k = self._fourier(cosmo, k, M, a, mass_def) - elif getattr(self, '_real', None): - f_k = self._fftlog_wrap(cosmo, k, M, a, mass_def, fourier_out=True) - return f_k - - def projected(self, cosmo, r_t, M, a, mass_def): - """ Returns the 2D projected profile as a function of - cosmology, radius, halo mass and scale factor. - - .. math:: - \\Sigma(R)= \\int dr_\\parallel\\, - \\rho(\\sqrt{r_\\parallel^2 + R^2}) - - Args: - cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. - r (float or array_like): comoving radius in Mpc. - M (float or array_like): halo mass in units of M_sun. - a (float): scale factor. - mass_def (:class:`~pyccl.halos.massdef.MassDef`): - a mass definition object. - - Returns: - float or array_like: halo profile. The shape of the - output will be `(N_M, N_r)` where `N_r` and `N_m` are - the sizes of `r` and `M` respectively. If `r` or `M` - are scalars, the corresponding dimension will be - squeezed out on output. - """ - if getattr(self, '_projected', None): - s_r_t = self._projected(cosmo, r_t, M, a, mass_def) - else: - s_r_t = self._projected_fftlog_wrap(cosmo, r_t, M, - a, mass_def, - is_cumul2d=False) - return s_r_t - - def cumul2d(self, cosmo, r_t, M, a, mass_def): - """ Returns the 2D cumulative surface density as a - function of cosmology, radius, halo mass and scale - factor. - - .. math:: - \\Sigma( R_M[:, None]] = 0 - - norm = self._norm(M_use, R_s, c_M) - prof = prof[:, :] * norm[:, None] - - if np.ndim(r) == 0: - prof = np.squeeze(prof, axis=-1) - if np.ndim(M) == 0: - prof = np.squeeze(prof, axis=0) - return prof - - def _fx_projected(self, x): - - def f1(xx): - x2m1 = xx * xx - 1 - return 1 / x2m1 + np.arccosh(1 / xx) / np.fabs(x2m1)**1.5 - - def f2(xx): - x2m1 = xx * xx - 1 - return 1 / x2m1 - np.arccos(1 / xx) / np.fabs(x2m1)**1.5 - - xf = x.flatten() - return np.piecewise(xf, - [xf < 1, xf > 1], - [f1, f2, 1./3.]).reshape(x.shape) - - def _projected_analytic(self, cosmo, r, M, a, mass_def): - r_use = np.atleast_1d(r) - M_use = np.atleast_1d(M) - - # Comoving virial radius - R_M = mass_def.get_radius(cosmo, M_use, a) / a - c_M = self._get_cM(cosmo, M_use, a, mdef=mass_def) - R_s = R_M / c_M - - x = r_use[None, :] / R_s[:, None] - prof = self._fx_projected(x) - norm = 2 * R_s * self._norm(M_use, R_s, c_M) - prof = prof[:, :] * norm[:, None] - - if np.ndim(r) == 0: - prof = np.squeeze(prof, axis=-1) - if np.ndim(M) == 0: - prof = np.squeeze(prof, axis=0) - return prof - - def _fx_cumul2d(self, x): - - def f1(xx): - sqx2m1 = np.sqrt(np.fabs(xx * xx - 1)) - return np.log(0.5 * xx) + np.arccosh(1 / xx) / sqx2m1 - - def f2(xx): - sqx2m1 = np.sqrt(np.fabs(xx * xx - 1)) - return np.log(0.5 * xx) + np.arccos(1 / xx) / sqx2m1 - - xf = x.flatten() - omln2 = 0.3068528194400547 # 1-Log[2] - f = np.piecewise(xf, - [xf < 1, xf > 1], - [f1, f2, omln2]).reshape(x.shape) - return 2 * f / x**2 - - def _cumul2d_analytic(self, cosmo, r, M, a, mass_def): - r_use = np.atleast_1d(r) - M_use = np.atleast_1d(M) - - # Comoving virial radius - R_M = mass_def.get_radius(cosmo, M_use, a) / a - c_M = self._get_cM(cosmo, M_use, a, mdef=mass_def) - R_s = R_M / c_M - - x = r_use[None, :] / R_s[:, None] - prof = self._fx_cumul2d(x) - norm = 2 * R_s * self._norm(M_use, R_s, c_M) - prof = prof[:, :] * norm[:, None] - - if np.ndim(r) == 0: - prof = np.squeeze(prof, axis=-1) - if np.ndim(M) == 0: - prof = np.squeeze(prof, axis=0) - return prof - - def _fourier_analytic(self, cosmo, k, M, a, mass_def): - M_use = np.atleast_1d(M) - k_use = np.atleast_1d(k) - - # Comoving virial radius - R_M = mass_def.get_radius(cosmo, M_use, a) / a - c_M = self._get_cM(cosmo, M_use, a, mdef=mass_def) - R_s = R_M / c_M - - x = k_use[None, :] * R_s[:, None] - Si2, Ci2 = sici(x) - P1 = M_use / (np.log(1 + c_M) - c_M / (1 + c_M)) - if self.truncated: - Si1, Ci1 = sici((1 + c_M[:, None]) * x) - P2 = np.sin(x) * (Si1 - Si2) + np.cos(x) * (Ci1 - Ci2) - P3 = np.sin(c_M[:, None] * x) / ((1 + c_M[:, None]) * x) - prof = P1[:, None] * (P2 - P3) - else: - P2 = np.sin(x) * (0.5 * np.pi - Si2) - np.cos(x) * Ci2 - prof = P1[:, None] * P2 - - if np.ndim(k) == 0: - prof = np.squeeze(prof, axis=-1) - if np.ndim(M) == 0: - prof = np.squeeze(prof, axis=0) - return prof - - -class HaloProfileEinasto(HaloProfile): - """ Einasto profile (1965TrAlm...5...87E). - - .. math:: - \\rho(r) = \\rho_0\\,\\exp(-2 ((r/r_s)^\\alpha-1) / \\alpha) - - where :math:`r_s` is related to the spherical overdensity - halo radius :math:`R_\\Delta(M)` through the concentration - parameter :math:`c(M)` as - - .. math:: - R_\\Delta(M) = c(M)\\,r_s - - and the normalization :math:`\\rho_0` is the mean density - within the :math:`R_\\Delta(M)` of the halo. The index - :math:`\\alpha` depends on halo mass and redshift, and we - use the parameterization of Diemer & Kravtsov - (arXiv:1401.1216). - - By default, this profile is truncated at :math:`r = R_\\Delta(M)`. - - Args: - c_M_relation (:obj:`Concentration`): concentration-mass - relation to use with this profile. - truncated (bool): set to `True` if the profile should be - truncated at :math:`r = R_\\Delta` (i.e. zero at larger - radii. - alpha (float, 'cosmo'): Set the Einasto alpha parameter or set to - 'cosmo' to calculate the value from cosmology. Default: 'cosmo' - """ - __repr_attrs__ = ("cM", "truncated", "alpha", "precision_fftlog",) - name = 'Einasto' - - def __init__(self, c_M_relation, truncated=True, alpha='cosmo'): - if not isinstance(c_M_relation, Concentration): - raise TypeError("c_M_relation must be of type `Concentration`") - - self.cM = c_M_relation - self.truncated = truncated - self.alpha = alpha - super(HaloProfileEinasto, self).__init__() - self.update_precision_fftlog(padding_hi_fftlog=1E2, - padding_lo_fftlog=1E-2, - n_per_decade=1000, - plaw_fourier=-2.) - - def update_parameters(self, alpha=None): - """Update any of the parameters associated with this profile. - Any parameter set to ``None`` won't be updated. - - Arguments - --------- - alpha : float, 'cosmo' - Profile shape parameter. Set to - 'cosmo' to calculate the value from cosmology - """ - if alpha is not None and alpha != self.alpha: - self.alpha = alpha - - def _get_cM(self, cosmo, M, a, mdef=None): - return self.cM.get_concentration(cosmo, M, a, mdef_other=mdef) - - def _get_alpha(self, cosmo, M, a, mdef): - if self.alpha == 'cosmo': - mdef_vir = MassDef('vir', 'matter') - Mvir = mdef.translate_mass(cosmo, M, a, mdef_vir) - sM = sigmaM(cosmo, Mvir, a) - nu = 1.686 / sM - alpha = 0.155 + 0.0095 * nu * nu - else: - alpha = np.full_like(M, self.alpha) - return alpha - - def _norm(self, M, Rs, c, alpha): - # Einasto normalization from mass, radius, concentration and alpha - return M / (np.pi * Rs**3 * 2**(2-3/alpha) * alpha**(-1+3/alpha) - * np.exp(2/alpha) - * gamma(3/alpha) * gammainc(3/alpha, 2/alpha*c**alpha)) - - def _real(self, cosmo, r, M, a, mass_def): - r_use = np.atleast_1d(r) - M_use = np.atleast_1d(M) - - # Comoving virial radius - R_M = mass_def.get_radius(cosmo, M_use, a) / a - c_M = self._get_cM(cosmo, M_use, a, mdef=mass_def) - R_s = R_M / c_M - - alpha = self._get_alpha(cosmo, M_use, a, mass_def) - - norm = self._norm(M_use, R_s, c_M, alpha) - - x = r_use[None, :] / R_s[:, None] - prof = norm[:, None] * np.exp(-2. * (x**alpha[:, None] - 1) / - alpha[:, None]) - if self.truncated: - prof[r_use[None, :] > R_M[:, None]] = 0 - - if np.ndim(r) == 0: - prof = np.squeeze(prof, axis=-1) - if np.ndim(M) == 0: - prof = np.squeeze(prof, axis=0) - return prof - - -class HaloProfileHernquist(HaloProfile): - """ Hernquist (1990ApJ...356..359H). - - .. math:: - \\rho(r) = \\frac{\\rho_0} - {\\frac{r}{r_s}\\left(1+\\frac{r}{r_s}\\right)^3} - - where :math:`r_s` is related to the spherical overdensity - halo radius :math:`R_\\Delta(M)` through the concentration - parameter :math:`c(M)` as - - .. math:: - R_\\Delta(M) = c(M)\\,r_s - - and the normalization :math:`\\rho_0` is the mean density - within the :math:`R_\\Delta(M)` of the halo. - - By default, this profile is truncated at :math:`r = R_\\Delta(M)`. - - Args: - c_M_relation (:obj:`Concentration`): concentration-mass - relation to use with this profile. - fourier_analytic (bool): set to `True` if you want to compute - the Fourier profile analytically (and not through FFTLog). - Default: `False`. - projected_analytic (bool): set to `True` if you want to - compute the 2D projected profile analytically (and not - through FFTLog). Default: `False`. - cumul2d_analytic (bool): set to `True` if you want to - compute the 2D cumulative surface density analytically - (and not through FFTLog). Default: `False`. - truncated (bool): set to `True` if the profile should be - truncated at :math:`r = R_\\Delta` (i.e. zero at larger - radii. - """ - __repr_attrs__ = ("cM", "fourier_analytic", "projected_analytic", - "cumul2d_analytic", "truncated", "precision_fftlog",) - name = 'Hernquist' - - def __init__(self, c_M_relation, - truncated=True, - fourier_analytic=False, - projected_analytic=False, - cumul2d_analytic=False): - if not isinstance(c_M_relation, Concentration): - raise TypeError("c_M_relation must be of type `Concentration`") - - self.cM = c_M_relation - self.truncated = truncated - self.fourier_analytic = fourier_analytic - self.projected_analytic = projected_analytic - self.cumul2d_analytic = cumul2d_analytic - if fourier_analytic: - self._fourier = self._fourier_analytic - if projected_analytic: - if truncated: - raise ValueError("Analytic projected profile not supported " - "for truncated Hernquist. Set `truncated` or " - "`projected_analytic` to `False`.") - self._projected = self._projected_analytic - if cumul2d_analytic: - if truncated: - raise ValueError("Analytic cumuative 2d profile not supported " - "for truncated Hernquist. Set `truncated` or " - "`cumul2d_analytic` to `False`.") - self._cumul2d = self._cumul2d_analytic - super(HaloProfileHernquist, self).__init__() - self.update_precision_fftlog(padding_hi_fftlog=1E2, - padding_lo_fftlog=1E-4, - n_per_decade=1000, - plaw_fourier=-2.) - - def _get_cM(self, cosmo, M, a, mdef=None): - return self.cM.get_concentration(cosmo, M, a, mdef_other=mdef) - - def _norm(self, M, Rs, c): - # Hernquist normalization from mass, radius and concentration - return M / (2 * np.pi * Rs**3 * (c / (1 + c))**2) - - def _real(self, cosmo, r, M, a, mass_def): - r_use = np.atleast_1d(r) - M_use = np.atleast_1d(M) - - # Comoving virial radius - R_M = mass_def.get_radius(cosmo, M_use, a) / a - c_M = self._get_cM(cosmo, M_use, a, mdef=mass_def) - R_s = R_M / c_M - - norm = self._norm(M_use, R_s, c_M) - - x = r_use[None, :] / R_s[:, None] - prof = norm[:, None] / (x * (1 + x)**3) - if self.truncated: - prof[r_use[None, :] > R_M[:, None]] = 0 - - if np.ndim(r) == 0: - prof = np.squeeze(prof, axis=-1) - if np.ndim(M) == 0: - prof = np.squeeze(prof, axis=0) - return prof - - def _fx_projected(self, x): - - def f1(xx): - x2m1 = xx * xx - 1 - return (-3 / 2 / x2m1**2 - + (x2m1+3) * np.arccosh(1 / xx) / 2 / np.fabs(x2m1)**2.5) - - def f2(xx): - x2m1 = xx * xx - 1 - return (-3 / 2 / x2m1**2 - + (x2m1+3) * np.arccos(1 / xx) / 2 / np.fabs(x2m1)**2.5) - - xf = x.flatten() - return np.piecewise(xf, - [xf < 1, xf > 1], - [f1, f2, 2./15.]).reshape(x.shape) - - def _projected_analytic(self, cosmo, r, M, a, mass_def): - r_use = np.atleast_1d(r) - M_use = np.atleast_1d(M) - - # Comoving virial radius - R_M = mass_def.get_radius(cosmo, M_use, a) / a - c_M = self._get_cM(cosmo, M_use, a, mdef=mass_def) - R_s = R_M / c_M - - x = r_use[None, :] / R_s[:, None] - prof = self._fx_projected(x) - norm = 2 * R_s * self._norm(M_use, R_s, c_M) - prof = prof[:, :] * norm[:, None] - - if np.ndim(r) == 0: - prof = np.squeeze(prof, axis=-1) - if np.ndim(M) == 0: - prof = np.squeeze(prof, axis=0) - return prof - - def _fx_cumul2d(self, x): - - def f1(xx): - x2m1 = xx * xx - 1 - return (1 + 1 / x2m1 - + (x2m1 + 1) * np.arccosh(1 / xx) / np.fabs(x2m1)**1.5) - - def f2(xx): - x2m1 = xx * xx - 1 - return (1 + 1 / x2m1 - - (x2m1 + 1) * np.arccos(1 / xx) / np.fabs(x2m1)**1.5) - - xf = x.flatten() - f = np.piecewise(xf, - [xf < 1, xf > 1], - [f1, f2, 1./3.]).reshape(x.shape) - - return f / x**2 - - def _cumul2d_analytic(self, cosmo, r, M, a, mass_def): - r_use = np.atleast_1d(r) - M_use = np.atleast_1d(M) - - # Comoving virial radius - R_M = mass_def.get_radius(cosmo, M_use, a) / a - c_M = self._get_cM(cosmo, M_use, a, mdef=mass_def) - R_s = R_M / c_M - - x = r_use[None, :] / R_s[:, None] - prof = self._fx_cumul2d(x) - norm = 2 * R_s * self._norm(M_use, R_s, c_M) - prof = prof[:, :] * norm[:, None] - - if np.ndim(r) == 0: - prof = np.squeeze(prof, axis=-1) - if np.ndim(M) == 0: - prof = np.squeeze(prof, axis=0) - return prof - - def _fourier_analytic(self, cosmo, k, M, a, mass_def): - M_use = np.atleast_1d(M) - k_use = np.atleast_1d(k) - - # Comoving virial radius - R_M = mass_def.get_radius(cosmo, M_use, a) / a - c_M = self._get_cM(cosmo, M_use, a, mdef=mass_def) - R_s = R_M / c_M - - x = k_use[None, :] * R_s[:, None] - Si2, Ci2 = sici(x) - P1 = M / ((c_M / (c_M + 1))**2 / 2) - c_Mp1 = c_M[:, None] + 1 - if self.truncated: - Si1, Ci1 = sici(c_Mp1 * x) - P2 = x * np.sin(x) * (Ci1 - Ci2) - x * np.cos(x) * (Si1 - Si2) - P3 = (-1 + np.sin(c_M[:, None] * x) / (c_Mp1**2 * x) - + c_Mp1 * np.cos(c_M[:, None] * x) / (c_Mp1**2)) - prof = P1[:, None] * (P2 - P3) / 2 - else: - P2 = (-x * (2 * np.sin(x) * Ci2 + np.pi * np.cos(x)) - + 2 * x * np.cos(x) * Si2 + 2) / 4 - prof = P1[:, None] * P2 - - if np.ndim(k) == 0: - prof = np.squeeze(prof, axis=-1) - if np.ndim(M) == 0: - prof = np.squeeze(prof, axis=0) - return prof - - -class HaloProfilePressureGNFW(HaloProfile): - """ Generalized NFW pressure profile by Arnaud et al. - (2010A&A...517A..92A). - - The parametrization is: - - .. math:: - - P_e(r) = C\\times P_0 h_{70}^E (c_{500} x)^{-\\gamma} - [1+(c_{500}x)^\\alpha]^{(\\gamma-\\beta)/\\alpha}, - - where - - .. math:: - - C = 1.65\\,h_{70}^2\\left(\\frac{H(z)}{H_0}\\right)^{8/3} - \\left[\\frac{h_{70}\\tilde{M}_{500}} - {3\\times10^{14}\\,M_\\odot}\\right]^{2/3+\\alpha_{\\mathrm{P}}}, - - :math:`x = r/\\tilde{r}_{500}`, :math:`h_{70}=h/0.7`, and the - exponent :math:`E` is -1 for SZ-based profile normalizations - and -1.5 for X-ray-based normalizations. The biased mass - :math:`\\tilde{M}_{500}` is related to the true overdensity - mass :math:`M_{500}` via the mass bias parameter :math:`(1-b)` - as :math:`\\tilde{M}_{500}=(1-b)M_{500}`. :math:`\\tilde{r}_{500}` - is the overdensity halo radius associated with :math:`\\tilde{M}_{500}` - (note the intentional tilde!), and the profile is defined for - a halo overdensity :math:`\\Delta=500` with respect to the - critical density. - - The default arguments (other than ``mass_bias``), correspond to the - profile parameters used in the Planck 2013 (V) paper. The profile is - calculated in physical (non-comoving) units of :math:`\\mathrm{eV/cm^3}`. - - Parameters - ---------- - mass_bias : float - The mass bias parameter :math:`1-b`. - P0 : float - Profile normalization. - c500 : float - Concentration parameter. - alpha, beta, gamma : float - Profile shape parameters. - alpha_P : float - Additional mass dependence exponent - P0_hexp : float - Power of :math:`h` with which the normalization parameter scales. - Equal to :math:`-1` for SZ-based normalizations, - and :math:`-3/2` for X-ray-based normalizations. - qrange : 2-sequence - Limits of integration used when computing the Fourier-space - profile template, in units of :math:`R_{\\mathrm{vir}}`. - nq : int - Number of sampling points of the Fourier-space profile template. - x_out : float - Profile threshold, in units of :math:`R_{\\mathrm{500c}}`. - Defaults to :math:`+\\infty`. - """ - __repr_attrs__ = ("mass_bias", "P0", "c500", "alpha", "alpha_P", "beta", - "gamma", "P0_hexp", "qrange", "nq", "x_out", - "precision_fftlog",) - name = 'GNFW' - - def __init__(self, mass_bias=0.8, P0=6.41, - c500=1.81, alpha=1.33, alpha_P=0.12, - beta=4.13, gamma=0.31, P0_hexp=-1., - qrange=(1e-3, 1e3), nq=128, x_out=np.inf): - self.qrange = qrange - self.nq = nq - self.mass_bias = mass_bias - self.P0 = P0 - self.c500 = c500 - self.alpha = alpha - self.alpha_P = alpha_P - self.beta = beta - self.gamma = gamma - self.P0_hexp = P0_hexp - self.x_out = x_out - - # Interpolator for dimensionless Fourier-space profile - self._fourier_interp = None - super(HaloProfilePressureGNFW, self).__init__() - - def update_parameters(self, mass_bias=None, P0=None, - c500=None, alpha=None, beta=None, gamma=None, - alpha_P=None, P0_hexp=None, x_out=None): - """Update any of the parameters associated with this profile. - Any parameter set to ``None`` won't be updated. - - .. note:: - - A change in ``alpha``, ``beta``, ``gamma``, ``c500``, or ``x_out`` - recomputes the Fourier-space template, which may be slow. - - Arguments - --------- - mass_bias : float - The mass bias parameter :math:`1-b`. - P0 : float - Profile normalization. - c500 : float - Concentration parameter. - alpha, beta, gamma : float - Profile shape parameter. - alpha_P : float - Additional mass-dependence exponent. - P0_hexp : float - Power of ``h`` with which the normalization scales. - SZ-based normalizations: -1. X-ray-based normalizations: -3/2. - x_out : float - Profile threshold (as a fraction of r500c). - """ - if mass_bias is not None: - self.mass_bias = mass_bias - if alpha_P is not None: - self.alpha_P = alpha_P - if P0 is not None: - self.P0 = P0 - if P0_hexp is not None: - self.P0_hexp = P0_hexp - - # Check if we need to recompute the Fourier profile. - re_fourier = False - if alpha is not None and alpha != self.alpha: - re_fourier = True - self.alpha = alpha - if beta is not None and beta != self.beta: - re_fourier = True - self.beta = beta - if gamma is not None and gamma != self.gamma: - re_fourier = True - self.gamma = gamma - if c500 is not None and c500 != self.c500: - re_fourier = True - self.c500 = c500 - if x_out is not None and x_out != self.x_out: - re_fourier = True - self.x_out = x_out - - if re_fourier and (self._fourier_interp is not None): - self._fourier_interp = self._integ_interp() - - def _form_factor(self, x): - # Scale-dependent factor of the GNFW profile. - f1 = (self.c500*x)**(-self.gamma) - exponent = -(self.beta-self.gamma)/self.alpha - f2 = (1+(self.c500*x)**self.alpha)**exponent - return f1*f2 - - def _integ_interp(self): - # Precomputes the Fourier transform of the profile in terms - # of the scaled radius x and creates a spline interpolator - # for it. - from scipy.interpolate import interp1d - from scipy.integrate import quad - - def integrand(x): - return self._form_factor(x)*x - - q_arr = np.geomspace(self.qrange[0], self.qrange[1], self.nq) - # We use the `weight` feature of quad to quickly estimate - # the Fourier transform. We could use the existing FFTLog - # framework, but this is a lot less of a kerfuffle. - f_arr = np.array([quad(integrand, - a=1e-4, b=self.x_out, # limits of integration - weight="sin", # fourier sine weight - wvar=q)[0] / q - for q in q_arr]) - Fq = interp1d(np.log(q_arr), f_arr, - fill_value="extrapolate", - bounds_error=False) - return Fq - - def _norm(self, cosmo, M, a, mb): - # Computes the normalisation factor of the GNFW profile. - # Normalisation factor is given in units of eV/cm^3. - # (Bolliet et al. 2017). - h70 = cosmo["h"]/0.7 - C0 = 1.65*h70**2 - CM = (h70*M*mb/3E14)**(2/3+self.alpha_P) # M dependence - Cz = h_over_h0(cosmo, a)**(8/3) # z dependence - P0_corr = self.P0 * h70**self.P0_hexp # h-corrected P_0 - return P0_corr * C0 * CM * Cz - - def _real(self, cosmo, r, M, a, mass_def): - # Real-space profile. - # Output in units of eV/cm^3 - r_use = np.atleast_1d(r) - M_use = np.atleast_1d(M) - - # Comoving virial radius - # (1-b) - mb = self.mass_bias - # R_Delta*(1+z) - R = mass_def.get_radius(cosmo, M_use * mb, a) / a - - nn = self._norm(cosmo, M_use, a, mb) - prof = self._form_factor(r_use[None, :] / R[:, None]) - prof *= nn[:, None] - - if np.ndim(r) == 0: - prof = np.squeeze(prof, axis=-1) - if np.ndim(M) == 0: - prof = np.squeeze(prof, axis=0) - return prof - - def _fourier(self, cosmo, k, M, a, mass_def): - # Fourier-space profile. - # Output in units of eV * Mpc^3 / cm^3. - - # Tabulate if not done yet - if self._fourier_interp is None: - with UnlockInstance(self): - self._fourier_interp = self._integ_interp() - - # Input handling - M_use = np.atleast_1d(M) - k_use = np.atleast_1d(k) - - # hydrostatic bias - mb = self.mass_bias - # R_Delta*(1+z) - R = mass_def.get_radius(cosmo, M_use * mb, a) / a - - ff = self._fourier_interp(np.log(k_use[None, :] * R[:, None])) - nn = self._norm(cosmo, M_use, a, mb) - - prof = (4*np.pi*R**3 * nn)[:, None] * ff - - if np.ndim(k) == 0: - prof = np.squeeze(prof, axis=-1) - if np.ndim(M) == 0: - prof = np.squeeze(prof, axis=0) - return prof - - -class HaloProfileHOD(HaloProfile): - """ A generic halo occupation distribution (HOD) - profile describing the number density of galaxies - as a function of halo mass. - - The parametrization for the mean profile is: - - .. math:: - \\langle n_g(r)|M,a\\rangle = \\bar{N}_c(M,a) - \\left[f_c(a)+\\bar{N}_s(M,a) u_{\\rm sat}(r|M,a)\\right] - - where :math:`\\bar{N}_c` and :math:`\\bar{N}_s` are the - mean number of central and satellite galaxies respectively, - :math:`f_c` is the observed fraction of central galaxies, and - :math:`u_{\\rm sat}(r|M,a)` is the distribution of satellites - as a function of distance to the halo centre. - - These quantities are parametrized as follows: - - .. math:: - \\bar{N}_c(M,a)=\\frac{1}{2}\\left[1+{\\rm erf} - \\left(\\frac{\\log(M/M_{\\rm min})}{\\sigma_{{\\rm ln}M}} - \\right)\\right] - - .. math:: - \\bar{N}_s(M,a)=\\Theta(M-M_0)\\left(\\frac{M-M_0}{M_1} - \\right)^\\alpha - - .. math:: - u_s(r|M,a)\\propto\\frac{\\Theta(r_{\\rm max}-r)} - {(r/r_g)(1+r/r_g)^2} - - Where :math:`\\Theta(x)` is the Heaviside step function, - and the proportionality constant in the last equation is - such that the volume integral of :math:`u_s` is 1. The - radius :math:`r_g` is related to the NFW scale radius :math:`r_s` - through :math:`r_g=\\beta_g\\,r_s`, and the radius - :math:`r_{\\rm max}` is related to the overdensity radius - :math:`r_\\Delta` as :math:`r_{\\rm max}=\\beta_{\\rm max}r_\\Delta`. - The scale radius is related to the comoving overdensity halo - radius via :math:`R_\\Delta(M) = c(M)\\,r_s`. - - All the quantities :math:`\\log_{10}M_{\\rm min}`, - :math:`\\log_{10}M_0`, :math:`\\log_{10}M_1`, - :math:`\\sigma_{{\\rm ln}M}`, :math:`f_c`, :math:`\\alpha`, - :math:`\\beta_g` and :math:`\\beta_{\\rm max}` are - time-dependent via a linear expansion around a pivot scale - factor :math:`a_*` with an offset (:math:`X_0`) and a tilt - parameter (:math:`X_p`): - - .. math:: - X(a) = X_0 + X_p\\,(a-a_*). - - This definition of the HOD profile draws from several papers - in the literature, including: astro-ph/0408564, arXiv:1706.05422 - and arXiv:1912.08209. The default values used here are roughly - compatible with those found in the latter paper. - - See :class:`~pyccl.halos.profiles_2pt.Profile2ptHOD`) for a - description of the Fourier-space two-point correlator of the - HOD profile. - - Args: - c_M_relation (:obj:`Concentration`): concentration-mass - relation to use with this profile. - lMmin_0 (float): offset parameter for - :math:`\\log_{10}M_{\\rm min}`. - lMmin_p (float): tilt parameter for - :math:`\\log_{10}M_{\\rm min}`. - siglM_0 (float): offset parameter for - :math:`\\sigma_{{\\rm ln}M}`. - siglM_p (float): tilt parameter for - :math:`\\sigma_{{\\rm ln}M}`. - lM0_0 (float): offset parameter for - :math:`\\log_{10}M_0`. - lM0_p (float): tilt parameter for - :math:`\\log_{10}M_0`. - lM1_0 (float): offset parameter for - :math:`\\log_{10}M_1`. - lM1_p (float): tilt parameter for - :math:`\\log_{10}M_1`. - alpha_0 (float): offset parameter for - :math:`\\alpha`. - alpha_p (float): tilt parameter for - :math:`\\alpha`. - fc_0 (float): offset parameter for - :math:`f_c`. - fc_p (float): tilt parameter for - :math:`f_c`. - bg_0 (float): offset parameter for - :math:`\\beta_g`. - bg_p (float): tilt parameter for - :math:`\\beta_g`. - bmax_0 (float): offset parameter for - :math:`\\beta_{\\rm max}`. - bmax_p (float): tilt parameter for - :math:`\\beta_{\\rm max}`. - a_pivot (float): pivot scale factor :math:`a_*`. - ns_independent (bool): drop requirement to only form - satellites when centrals are present. - """ - __repr_attrs__ = ("cM", "lMmin_0", "lMmin_p", "siglM_0", "siglM_p", - "lM0_0", "lM0_p", "lM1_0", "lM1_p", "alpha_0", "alpha_p", - "fc_0", "fc_p", "bg_0", "bg_p", "bmax_0", "bmax_p", - "a_pivot", "ns_independent", "precision_fftlog",) - name = 'HOD' - is_number_counts = True - - def __init__(self, c_M_relation, - lMmin_0=12., lMmin_p=0., siglM_0=0.4, - siglM_p=0., lM0_0=7., lM0_p=0., - lM1_0=13.3, lM1_p=0., alpha_0=1., - alpha_p=0., fc_0=1., fc_p=0., - bg_0=1., bg_p=0., bmax_0=1., bmax_p=0., - a_pivot=1., ns_independent=False): - if not isinstance(c_M_relation, Concentration): - raise TypeError("c_M_relation must be of type `Concentration`") - - self.cM = c_M_relation - self.lMmin_0 = lMmin_0 - self.lMmin_p = lMmin_p - self.lM0_0 = lM0_0 - self.lM0_p = lM0_p - self.lM1_0 = lM1_0 - self.lM1_p = lM1_p - self.siglM_0 = siglM_0 - self.siglM_p = siglM_p - self.alpha_0 = alpha_0 - self.alpha_p = alpha_p - self.fc_0 = fc_0 - self.fc_p = fc_p - self.bg_0 = bg_0 - self.bg_p = bg_p - self.bmax_0 = bmax_0 - self.bmax_p = bmax_p - self.a_pivot = a_pivot - self.ns_independent = ns_independent - super(HaloProfileHOD, self).__init__() - - def _get_cM(self, cosmo, M, a, mdef=None): - return self.cM.get_concentration(cosmo, M, a, mdef_other=mdef) - - def update_parameters(self, lMmin_0=None, lMmin_p=None, - siglM_0=None, siglM_p=None, - lM0_0=None, lM0_p=None, - lM1_0=None, lM1_p=None, - alpha_0=None, alpha_p=None, - fc_0=None, fc_p=None, - bg_0=None, bg_p=None, - bmax_0=None, bmax_p=None, - a_pivot=None, - ns_independent=None): - """ Update any of the parameters associated with - this profile. Any parameter set to `None` won't be updated. - - Args: - lMmin_0 (float): offset parameter for - :math:`\\log_{10}M_{\\rm min}`. - lMmin_p (float): tilt parameter for - :math:`\\log_{10}M_{\\rm min}`. - siglM_0 (float): offset parameter for - :math:`\\sigma_{{\\rm ln}M}`. - siglM_p (float): tilt parameter for - :math:`\\sigma_{{\\rm ln}M}`. - lM0_0 (float): offset parameter for - :math:`\\log_{10}M_0`. - lM0_p (float): tilt parameter for - :math:`\\log_{10}M_0`. - lM1_0 (float): offset parameter for - :math:`\\log_{10}M_1`. - lM1_p (float): tilt parameter for - :math:`\\log_{10}M_1`. - alpha_0 (float): offset parameter for - :math:`\\alpha`. - alpha_p (float): tilt parameter for - :math:`\\alpha`. - fc_0 (float): offset parameter for - :math:`f_c`. - fc_p (float): tilt parameter for - :math:`f_c`. - bg_0 (float): offset parameter for - :math:`\\beta_g`. - bg_p (float): tilt parameter for - :math:`\\beta_g`. - bmax_0 (float): offset parameter for - :math:`\\beta_{\\rm max}`. - bmax_p (float): tilt parameter for - :math:`\\beta_{\\rm max}`. - a_pivot (float): pivot scale factor :math:`a_*`. - ns_independent (bool): drop requirement to only form - satellites when centrals are present - """ - if lMmin_0 is not None: - self.lMmin_0 = lMmin_0 - if lMmin_p is not None: - self.lMmin_p = lMmin_p - if lM0_0 is not None: - self.lM0_0 = lM0_0 - if lM0_p is not None: - self.lM0_p = lM0_p - if lM1_0 is not None: - self.lM1_0 = lM1_0 - if lM1_p is not None: - self.lM1_p = lM1_p - if siglM_0 is not None: - self.siglM_0 = siglM_0 - if siglM_p is not None: - self.siglM_p = siglM_p - if alpha_0 is not None: - self.alpha_0 = alpha_0 - if alpha_p is not None: - self.alpha_p = alpha_p - if fc_0 is not None: - self.fc_0 = fc_0 - if fc_p is not None: - self.fc_p = fc_p - if bg_0 is not None: - self.bg_0 = bg_0 - if bg_p is not None: - self.bg_p = bg_p - if bmax_0 is not None: - self.bmax_0 = bmax_0 - if bmax_p is not None: - self.bmax_p = bmax_p - if a_pivot is not None: - self.a_pivot = a_pivot - if ns_independent is not None: - self.ns_independent = ns_independent - - def _usat_real(self, cosmo, r, M, a, mass_def): - r_use = np.atleast_1d(r) - M_use = np.atleast_1d(M) - - # Comoving virial radius - bg = self.bg_0 + self.bg_p * (a - self.a_pivot) - bmax = self.bmax_0 + self.bmax_p * (a - self.a_pivot) - R_M = mass_def.get_radius(cosmo, M_use, a) / a - c_M = self._get_cM(cosmo, M_use, a, mdef=mass_def) - R_s = R_M / c_M - c_M *= bmax / bg - - x = r_use[None, :] / (R_s[:, None] * bg) - prof = 1./(x * (1 + x)**2) - # Truncate - prof[r_use[None, :] > R_M[:, None]*bmax] = 0 - - norm = 1. / (4 * np.pi * (bg*R_s)**3 * (np.log(1+c_M) - c_M/(1+c_M))) - prof = prof[:, :] * norm[:, None] - - if np.ndim(r) == 0: - prof = np.squeeze(prof, axis=-1) - if np.ndim(M) == 0: - prof = np.squeeze(prof, axis=0) - return prof - - def _usat_fourier(self, cosmo, k, M, a, mass_def): - M_use = np.atleast_1d(M) - k_use = np.atleast_1d(k) - - # Comoving virial radius - bg = self.bg_0 + self.bg_p * (a - self.a_pivot) - bmax = self.bmax_0 + self.bmax_p * (a - self.a_pivot) - R_M = mass_def.get_radius(cosmo, M_use, a) / a - c_M = self._get_cM(cosmo, M_use, a, mdef=mass_def) - R_s = R_M / c_M - c_M *= bmax / bg - - x = k_use[None, :] * R_s[:, None] * bg - Si1, Ci1 = sici((1 + c_M[:, None]) * x) - Si2, Ci2 = sici(x) - - P1 = 1. / (np.log(1+c_M) - c_M/(1+c_M)) - P2 = np.sin(x) * (Si1 - Si2) + np.cos(x) * (Ci1 - Ci2) - P3 = np.sin(c_M[:, None] * x) / ((1 + c_M[:, None]) * x) - prof = P1[:, None] * (P2 - P3) - - if np.ndim(k) == 0: - prof = np.squeeze(prof, axis=-1) - if np.ndim(M) == 0: - prof = np.squeeze(prof, axis=0) - return prof - - def _real(self, cosmo, r, M, a, mass_def): - r_use = np.atleast_1d(r) - M_use = np.atleast_1d(M) - - Nc = self._Nc(M_use, a) - Ns = self._Ns(M_use, a) - fc = self._fc(a) - # NFW profile - ur = self._usat_real(cosmo, r_use, M_use, a, mass_def) - - if self.ns_independent: - prof = Nc[:, None] * fc + Ns[:, None] * ur - else: - prof = Nc[:, None] * (fc + Ns[:, None] * ur) - - if np.ndim(r) == 0: - prof = np.squeeze(prof, axis=-1) - if np.ndim(M) == 0: - prof = np.squeeze(prof, axis=0) - return prof - - def _fourier(self, cosmo, k, M, a, mass_def): - M_use = np.atleast_1d(M) - k_use = np.atleast_1d(k) - - Nc = self._Nc(M_use, a) - Ns = self._Ns(M_use, a) - fc = self._fc(a) - # NFW profile - uk = self._usat_fourier(cosmo, k_use, M_use, a, mass_def) - - if self.ns_independent: - prof = Nc[:, None] * fc + Ns[:, None] * uk - else: - prof = Nc[:, None] * (fc + Ns[:, None] * uk) - - if np.ndim(k) == 0: - prof = np.squeeze(prof, axis=-1) - if np.ndim(M) == 0: - prof = np.squeeze(prof, axis=0) - return prof - - def _fourier_variance(self, cosmo, k, M, a, mass_def): - # Fourier-space variance of the HOD profile - M_use = np.atleast_1d(M) - k_use = np.atleast_1d(k) - - Nc = self._Nc(M_use, a) - Ns = self._Ns(M_use, a) - fc = self._fc(a) - # NFW profile - uk = self._usat_fourier(cosmo, k_use, M_use, a, mass_def) - - prof = Ns[:, None] * uk - if self.ns_independent: - prof = 2 * Nc[:, None] * fc * prof + prof**2 - else: - prof = Nc[:, None] * (2 * fc * prof + prof**2) - - if np.ndim(k) == 0: - prof = np.squeeze(prof, axis=-1) - if np.ndim(M) == 0: - prof = np.squeeze(prof, axis=0) - return prof - - def _fc(self, a): - # Observed fraction of centrals - return self.fc_0 + self.fc_p * (a - self.a_pivot) - - def _Nc(self, M, a): - # Number of centrals - Mmin = 10.**(self.lMmin_0 + self.lMmin_p * (a - self.a_pivot)) - siglM = self.siglM_0 + self.siglM_p * (a - self.a_pivot) - return 0.5 * (1 + erf(np.log(M/Mmin)/siglM)) - - def _Ns(self, M, a): - # Number of satellites - M0 = 10.**(self.lM0_0 + self.lM0_p * (a - self.a_pivot)) - M1 = 10.**(self.lM1_0 + self.lM1_p * (a - self.a_pivot)) - alpha = self.alpha_0 + self.alpha_p * (a - self.a_pivot) - return np.heaviside(M-M0, 1) * (np.fabs(M-M0) / M1)**alpha diff --git a/pyccl/halos/profiles/__init__.py b/pyccl/halos/profiles/__init__.py new file mode 100644 index 000000000..48fe8dcdf --- /dev/null +++ b/pyccl/halos/profiles/__init__.py @@ -0,0 +1,9 @@ +from .profile_base import * +from .cib_shang12 import * +from .gaussian import * +from .powerlaw import * +from .nfw import * +from .einasto import * +from .hernquist import * +from .pressure_gnfw import * +from .hod import * diff --git a/pyccl/halos/profiles_cib.py b/pyccl/halos/profiles/cib_shang12.py similarity index 67% rename from pyccl/halos/profiles_cib.py rename to pyccl/halos/profiles/cib_shang12.py index 20deafeb5..fba9bc894 100644 --- a/pyccl/halos/profiles_cib.py +++ b/pyccl/halos/profiles/cib_shang12.py @@ -1,12 +1,15 @@ -from .profiles import HaloProfile, HaloProfileNFW -from .profiles_2pt import Profile2pt -from .concentration import Concentration +from ...base import warn_api, deprecate_attr +from .profile_base import HaloProfileCIB +from .nfw import HaloProfileNFW import numpy as np -from scipy.integrate import simps +from scipy.integrate import simpson from scipy.special import lambertw -class HaloProfileCIBShang12(HaloProfile): +__all__ = ("HaloProfileCIBShang12",) + + +class HaloProfileCIBShang12(HaloProfileCIB): """ CIB profile implementing the model by Shang et al. (2012MNRAS.421.2832S). @@ -63,7 +66,7 @@ class HaloProfileCIBShang12(HaloProfile): dependence of the form :math:`T_d=T_0(1+z)^\\alpha`. Args: - c_M_relation (:obj:`Concentration`): concentration-mass + concentration (:obj:`Concentration`): concentration-mass relation to use with this profile. nu_GHz (float): frequency in GHz. alpha (float): dust temperature evolution parameter. @@ -71,22 +74,28 @@ class HaloProfileCIBShang12(HaloProfile): beta (float): dust spectral index. gamma (float): high frequency slope. s_z (float): luminosity evolution slope. - log10meff (float): log10 of the most efficient mass. - sigLM (float): logarithmic scatter in mass. + log10Meff (float): log10 of the most efficient mass. + siglog10M (float): logarithmic scatter in mass. Mmin (float): minimum subhalo mass. L0 (float): luminosity scale (in :math:`{\\rm Jy}\\,{\\rm Mpc}^2\\,M_\\odot^{-1}`). """ - __repr_attrs__ = ("cM", "nu", "alpha", "T0", "beta", "gamma", "s_z", - "l10meff", "sigLM", "Mmin", "L0", "precision_fftlog",) + __repr_attrs__ = __eq_attrs__ = ( + "cM", "nu", "alpha", "T0", "beta", "gamma", "s_z", + "l10meff", "sigLM", "Mmin", "L0", "precision_fftlog", "normprof",) + __getattr__ = deprecate_attr(pairs=[('cM', 'concentration'), + ('l10meff', 'log10Meff'), + ('sigLM', 'siglog10M')] + )(super.__getattribute__) name = 'CIBShang12' _one_over_4pi = 0.07957747154 - def __init__(self, c_M_relation, nu_GHz, alpha=0.36, T0=24.4, beta=1.75, - gamma=1.7, s_z=3.6, log10meff=12.6, sigLM=0.707, Mmin=1E10, - L0=6.4E-8): - if not isinstance(c_M_relation, Concentration): - raise TypeError("c_M_relation must be of type `Concentration`") + @warn_api(pairs=[("c_M_relation", "concentration"), + ("log10meff", "log10Meff"), + ("sigLM", "siglog10M")]) + def __init__(self, *, concentration, nu_GHz, alpha=0.36, T0=24.4, + beta=1.75, gamma=1.7, s_z=3.6, log10Meff=12.6, + siglog10M=0.707, Mmin=1E10, L0=6.4E-8): self.nu = nu_GHz self.alpha = alpha @@ -94,13 +103,13 @@ def __init__(self, c_M_relation, nu_GHz, alpha=0.36, T0=24.4, beta=1.75, self.beta = beta self.gamma = gamma self.s_z = s_z - self.l10meff = log10meff - self.sigLM = sigLM + self.log10Meff = log10Meff + self.siglog10M = siglog10M self.Mmin = Mmin self.L0 = L0 - self.cM = c_M_relation - self.pNFW = HaloProfileNFW(c_M_relation) - super(HaloProfileCIBShang12, self).__init__() + self.concentration = concentration + self.pNFW = HaloProfileNFW(concentration=concentration) + super().__init__() def dNsub_dlnM_TinkerWetzel10(self, Msub, Mparent): """Subhalo mass function of Tinker & Wetzel (2010ApJ...719...88T) @@ -114,9 +123,11 @@ def dNsub_dlnM_TinkerWetzel10(self, Msub, Mparent): """ return 0.30*(Msub/Mparent)**(-0.7)*np.exp(-9.9*(Msub/Mparent)**2.5) + @warn_api(pairs=[("log10meff", "log10Meff"), + ("sigLM", "siglog10M")]) def update_parameters(self, nu_GHz=None, alpha=None, T0=None, beta=None, gamma=None, - s_z=None, log10meff=None, sigLM=None, + s_z=None, log10Meff=None, siglog10M=None, Mmin=None, L0=None): """ Update any of the parameters associated with this profile. Any parameter set to `None` won't be updated. @@ -128,8 +139,8 @@ def update_parameters(self, nu_GHz=None, beta (float): dust spectral index. gamma (float): high frequency slope. s_z (float): luminosity evolution slope. - log10meff (float): log10 of the most efficient mass. - sigLM (float): logarithmic scatter in mass. + log10Meff (float): log10 of the most efficient mass. + siglog10M (float): logarithmic scatter in mass. Mmin (float): minimum subhalo mass. L0 (float): luminosity scale (in :math:`{\\rm Jy}\\,{\\rm Mpc}^2\\,M_\\odot^{-1}`). @@ -146,10 +157,10 @@ def update_parameters(self, nu_GHz=None, self.gamma = gamma if s_z is not None: self.s_z = s_z - if log10meff is not None: - self.l10meff = log10meff - if sigLM is not None: - self.sigLM = sigLM + if log10Meff is not None: + self.log10Meff = log10Meff + if siglog10M is not None: + self.siglog10M = siglog10M if Mmin is not None: self.Mmin = Mmin if L0 is not None: @@ -181,9 +192,10 @@ def _Lum(self, l10M, a): # Redshift evolution phi_z = a**(-self.s_z) # Mass dependence - # M/sqrt(2*pi*sigLM^2) - sig_pref = 10**l10M/(2.50662827463*self.sigLM) - sigma_m = sig_pref * np.exp(-0.5*((l10M - self.l10meff)/self.sigLM)**2) + # M/sqrt(2*pi*siglog10M^2) + sig_pref = 10**l10M/(2.50662827463*self.siglog10M) + sigma_m = sig_pref * np.exp(-0.5*((l10M - self.log10Meff) + / self.siglog10M)**2) return self.L0*phi_z*sigma_m def _Lumcen(self, M, a): @@ -205,7 +217,7 @@ def _Lumsat(self, M, a): Lum = self._Lum(msub, a) dnsubdlnm = self.dNsub_dlnM_TinkerWetzel10(10**msub, M_use) integ = dnsubdlnm * Lum - Lumsat = simps(integ, x=np.log(10)*msub) + Lumsat = simpson(integ, x=np.log(10)*msub) res[-len(Lumsat):] = Lumsat return res @@ -272,50 +284,3 @@ def _fourier_variance(self, cosmo, k, M, a, mass_def, nu_other=None): if np.ndim(M) == 0: prof = np.squeeze(prof, axis=0) return prof - - -class Profile2ptCIB(Profile2pt): - """ This class implements the Fourier-space 1-halo 2-point - correlator for the CIB profile. It follows closely the - implementation of the equivalent HOD quantity - (see :class:`~pyccl.halos.profiles_2pt.Profile2ptHOD` - and Eq. 15 of McCarthy & Madhavacheril (2021PhRvD.103j3515M)). - """ - def fourier_2pt(self, prof, cosmo, k, M, a, - prof2=None, mass_def=None): - """ Returns the Fourier-space two-point moment for the CIB - profile. - - Args: - prof (:class:`HaloProfileCIBShang12`): - halo profile for which the second-order moment - is desired. - cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. - k (float or array_like): comoving wavenumber in Mpc^-1. - M (float or array_like): halo mass in units of M_sun. - a (float): scale factor. - prof2 (:class:`HaloProfileCIBShang12`): - second halo profile for which the second-order moment - is desired. If `None`, the assumption is that you want - an auto-correlation. Note that only auto-correlations - are allowed in this case. - mass_def (:obj:`~pyccl.halos.massdef.MassDef`): a mass - definition object. - - Returns: - float or array_like: second-order Fourier-space - moment. The shape of the output will be `(N_M, N_k)` - where `N_k` and `N_m` are the sizes of `k` and `M` - respectively. If `k` or `M` are scalars, the - corresponding dimension will be squeezed out on output. - """ - if not isinstance(prof, HaloProfileCIBShang12): - raise TypeError("prof must be of type `HaloProfileCIB`") - - nu2 = None - if prof2 is not None: - if not isinstance(prof2, HaloProfileCIBShang12): - raise TypeError("prof must be of type `HaloProfileCIB`") - nu2 = prof2.nu - return prof._fourier_variance(cosmo, k, M, a, mass_def, - nu_other=nu2) diff --git a/pyccl/halos/profiles/einasto.py b/pyccl/halos/profiles/einasto.py new file mode 100644 index 000000000..0cb3003ed --- /dev/null +++ b/pyccl/halos/profiles/einasto.py @@ -0,0 +1,113 @@ +from ...base import warn_api +from ...base.parameters import physical_constants as const +from ...power import sigmaM +from ..concentration import Concentration +from ..massdef import MassDef +from .profile_base import HaloProfileMatter +import numpy as np +from scipy.special import gamma, gammainc + + +__all__ = ("HaloProfileEinasto",) + + +class HaloProfileEinasto(HaloProfileMatter): + """ Einasto profile (1965TrAlm...5...87E). + + .. math:: + \\rho(r) = \\rho_0\\,\\exp(-2 ((r/r_s)^\\alpha-1) / \\alpha) + + where :math:`r_s` is related to the spherical overdensity + halo radius :math:`R_\\Delta(M)` through the concentration + parameter :math:`c(M)` as + + .. math:: + R_\\Delta(M) = c(M)\\,r_s + + and the normalization :math:`\\rho_0` is the mean density + within the :math:`R_\\Delta(M)` of the halo. The index + :math:`\\alpha` depends on halo mass and redshift, and we + use the parameterization of Diemer & Kravtsov + (arXiv:1401.1216). + + By default, this profile is truncated at :math:`r = R_\\Delta(M)`. + + Args: + concentration (:obj:`Concentration`): concentration-mass + relation to use with this profile. + truncated (bool): set to `True` if the profile should be + truncated at :math:`r = R_\\Delta` (i.e. zero at larger + radii. + alpha (float, 'cosmo'): Set the Einasto alpha parameter or set to + 'cosmo' to calculate the value from cosmology. Default: 'cosmo' + """ + __repr_attrs__ = __eq_attrs__ = ("concentration", "truncated", "alpha", + "precision_fftlog", "normprof",) + name = 'Einasto' + + @warn_api(pairs=[("c_M_relation", "concentration")]) + def __init__(self, *, concentration, truncated=True, alpha='cosmo'): + if not isinstance(concentration, Concentration): + raise TypeError("concentration must be of type `Concentration`") + + self.concentration = concentration + self.truncated = truncated + self.alpha = alpha + super().__init__() + self.update_precision_fftlog(padding_hi_fftlog=1E2, + padding_lo_fftlog=1E-2, + n_per_decade=1000, + plaw_fourier=-2.) + + def update_parameters(self, alpha=None): + """Update any of the parameters associated with this profile. + Any parameter set to ``None`` won't be updated. + + Arguments + --------- + alpha : float, 'cosmo' + Profile shape parameter. Set to + 'cosmo' to calculate the value from cosmology + """ + if alpha is not None and alpha != self.alpha: + self.alpha = alpha + + def _get_alpha(self, cosmo, M, a, mass_def): + if self.alpha == 'cosmo': + Mvir = mass_def.translate_mass( + cosmo, M, a, mass_def_other=MassDef('vir', 'matter')) + sM = sigmaM(cosmo, Mvir, a) + nu = const.DELTA_C / sM + return 0.155 + 0.0095 * nu * nu + return np.full_like(M, self.alpha) + + def _norm(self, M, Rs, c, alpha): + # Einasto normalization from mass, radius, concentration and alpha + return M / (np.pi * Rs**3 * 2**(2-3/alpha) * alpha**(-1+3/alpha) + * np.exp(2/alpha) + * gamma(3/alpha) * gammainc(3/alpha, 2/alpha*c**alpha)) + + def _real(self, cosmo, r, M, a, mass_def): + r_use = np.atleast_1d(r) + M_use = np.atleast_1d(M) + + # Comoving virial radius + R_M = mass_def.get_radius(cosmo, M_use, a) / a + c_M = self.concentration(cosmo, M_use, a) + R_s = R_M / c_M + + alpha = self._get_alpha(cosmo, M_use, a, mass_def) + + norm = self._norm(M_use, R_s, c_M, alpha) + + x = r_use[None, :] / R_s[:, None] + prof = norm[:, None] * np.exp(-2. * (x**alpha[:, None] - 1) / + alpha[:, None]) + if self.truncated: + prof[r_use[None, :] > R_M[:, None]] = 0 + + if np.ndim(r) == 0: + prof = np.squeeze(prof, axis=-1) + if np.ndim(M) == 0: + prof = np.squeeze(prof, axis=0) + return prof diff --git a/pyccl/halos/profiles/gaussian.py b/pyccl/halos/profiles/gaussian.py new file mode 100644 index 000000000..f689b3d63 --- /dev/null +++ b/pyccl/halos/profiles/gaussian.py @@ -0,0 +1,56 @@ +from ...base import warn_api, deprecated +from .profile_base import HaloProfile +import numpy as np + + +__all__ = ("HaloProfileGaussian",) + + +class HaloProfileGaussian(HaloProfile): + """ Gaussian profile + + .. math:: + \\rho(r) = \\rho_0\\, e^{-(r/r_s)^2} + + Args: + r_scale (:obj:`function`): the width of the profile. + The signature of this function should be + `f(cosmo, M, a, mass_def)`, where `cosmo` is a + :class:`~pyccl.core.Cosmology` object, `M` is a halo mass in + units of M_sun, `a` is the scale factor and `mass_def` + is a :class:`~pyccl.halos.massdef.MassDef` object. + rho0 (:obj:`function`): the amplitude of the profile. + It should have the same signature as `r_scale`. + """ + __repr_attrs__ = __eq_attrs__ = ("r_scale", "rho_0", "precision_fftlog", + "normprof",) + name = 'Gaussian' + normprof = False + + @deprecated() + @warn_api + def __init__(self, *, r_scale, rho0): + self.rho_0 = rho0 + self.r_scale = r_scale + super().__init__() + self.update_precision_fftlog(padding_lo_fftlog=0.01, + padding_hi_fftlog=100., + n_per_decade=10000) + + def _real(self, cosmo, r, M, a, mass_def): + r_use = np.atleast_1d(r) + M_use = np.atleast_1d(M) + + # Compute scale + rs = self.r_scale(cosmo, M_use, a, mass_def) + # Compute normalization + rho0 = self.rho_0(cosmo, M_use, a, mass_def) + # Form factor + prof = np.exp(-(r_use[None, :] / rs[:, None])**2) + prof = prof * rho0[:, None] + + if np.ndim(r) == 0: + prof = np.squeeze(prof, axis=-1) + if np.ndim(M) == 0: + prof = np.squeeze(prof, axis=0) + return prof diff --git a/pyccl/halos/profiles/hernquist.py b/pyccl/halos/profiles/hernquist.py new file mode 100644 index 000000000..9f9539b2d --- /dev/null +++ b/pyccl/halos/profiles/hernquist.py @@ -0,0 +1,215 @@ +from ...base import warn_api +from ..concentration import Concentration +from .profile_base import HaloProfileMatter +import numpy as np +from scipy.special import sici + + +__all__ = ("HaloProfileHernquist",) + + +class HaloProfileHernquist(HaloProfileMatter): + """ Hernquist (1990ApJ...356..359H). + + .. math:: + \\rho(r) = \\frac{\\rho_0} + {\\frac{r}{r_s}\\left(1+\\frac{r}{r_s}\\right)^3} + + where :math:`r_s` is related to the spherical overdensity + halo radius :math:`R_\\Delta(M)` through the concentration + parameter :math:`c(M)` as + + .. math:: + R_\\Delta(M) = c(M)\\,r_s + + and the normalization :math:`\\rho_0` is the mean density + within the :math:`R_\\Delta(M)` of the halo. + + By default, this profile is truncated at :math:`r = R_\\Delta(M)`. + + Args: + concentration (:obj:`Concentration`): concentration-mass + relation to use with this profile. + fourier_analytic (bool): set to `True` if you want to compute + the Fourier profile analytically (and not through FFTLog). + Default: `False`. + projected_analytic (bool): set to `True` if you want to + compute the 2D projected profile analytically (and not + through FFTLog). Default: `False`. + cumul2d_analytic (bool): set to `True` if you want to + compute the 2D cumulative surface density analytically + (and not through FFTLog). Default: `False`. + truncated (bool): set to `True` if the profile should be + truncated at :math:`r = R_\\Delta` (i.e. zero at larger + radii. + """ + __repr_attrs__ = __eq_attrs__ = ( + "concentration", "fourier_analytic", "projected_analytic", + "cumul2d_analytic", "truncated", "precision_fftlog", "normprof",) + name = 'Hernquist' + + @warn_api(pairs=[("c_M_relation", "concentration")]) + def __init__(self, *, concentration, + truncated=True, + fourier_analytic=False, + projected_analytic=False, + cumul2d_analytic=False): + if not isinstance(concentration, Concentration): + raise TypeError("concentration must be of type `Concentration`") + + self.concentration = concentration + self.truncated = truncated + self.fourier_analytic = fourier_analytic + self.projected_analytic = projected_analytic + self.cumul2d_analytic = cumul2d_analytic + if fourier_analytic: + self._fourier = self._fourier_analytic + if projected_analytic: + if truncated: + raise ValueError("Analytic projected profile not supported " + "for truncated Hernquist. Set `truncated` or " + "`projected_analytic` to `False`.") + self._projected = self._projected_analytic + if cumul2d_analytic: + if truncated: + raise ValueError("Analytic cumuative 2d profile not supported " + "for truncated Hernquist. Set `truncated` or " + "`cumul2d_analytic` to `False`.") + self._cumul2d = self._cumul2d_analytic + super().__init__() + self.update_precision_fftlog(padding_hi_fftlog=1E2, + padding_lo_fftlog=1E-4, + n_per_decade=1000, + plaw_fourier=-2.) + + def _norm(self, M, Rs, c): + # Hernquist normalization from mass, radius and concentration + return M / (2 * np.pi * Rs**3 * (c / (1 + c))**2) + + def _real(self, cosmo, r, M, a, mass_def): + r_use = np.atleast_1d(r) + M_use = np.atleast_1d(M) + + # Comoving virial radius + R_M = mass_def.get_radius(cosmo, M_use, a) / a + c_M = self.concentration(cosmo, M_use, a) + R_s = R_M / c_M + + norm = self._norm(M_use, R_s, c_M) + + x = r_use[None, :] / R_s[:, None] + prof = norm[:, None] / (x * (1 + x)**3) + if self.truncated: + prof[r_use[None, :] > R_M[:, None]] = 0 + + if np.ndim(r) == 0: + prof = np.squeeze(prof, axis=-1) + if np.ndim(M) == 0: + prof = np.squeeze(prof, axis=0) + return prof + + def _fx_projected(self, x): + + def f1(xx): + x2m1 = xx * xx - 1 + return (-3 / 2 / x2m1**2 + + (x2m1+3) * np.arccosh(1 / xx) / 2 / np.fabs(x2m1)**2.5) + + def f2(xx): + x2m1 = xx * xx - 1 + return (-3 / 2 / x2m1**2 + + (x2m1+3) * np.arccos(1 / xx) / 2 / np.fabs(x2m1)**2.5) + + xf = x.flatten() + return np.piecewise(xf, + [xf < 1, xf > 1], + [f1, f2, 2./15.]).reshape(x.shape) + + def _projected_analytic(self, cosmo, r, M, a, mass_def): + r_use = np.atleast_1d(r) + M_use = np.atleast_1d(M) + + # Comoving virial radius + R_M = mass_def.get_radius(cosmo, M_use, a) / a + c_M = self.concentration(cosmo, M_use, a) + R_s = R_M / c_M + + x = r_use[None, :] / R_s[:, None] + prof = self._fx_projected(x) + norm = 2 * R_s * self._norm(M_use, R_s, c_M) + prof = prof[:, :] * norm[:, None] + + if np.ndim(r) == 0: + prof = np.squeeze(prof, axis=-1) + if np.ndim(M) == 0: + prof = np.squeeze(prof, axis=0) + return prof + + def _fx_cumul2d(self, x): + + def f1(xx): + x2m1 = xx * xx - 1 + return (1 + 1 / x2m1 + + (x2m1 + 1) * np.arccosh(1 / xx) / np.fabs(x2m1)**1.5) + + def f2(xx): + x2m1 = xx * xx - 1 + return (1 + 1 / x2m1 + - (x2m1 + 1) * np.arccos(1 / xx) / np.fabs(x2m1)**1.5) + + xf = x.flatten() + f = np.piecewise(xf, + [xf < 1, xf > 1], + [f1, f2, 1./3.]).reshape(x.shape) + + return f / x**2 + + def _cumul2d_analytic(self, cosmo, r, M, a, mass_def): + r_use = np.atleast_1d(r) + M_use = np.atleast_1d(M) + + # Comoving virial radius + R_M = mass_def.get_radius(cosmo, M_use, a) / a + c_M = self.concentration(cosmo, M_use, a) + R_s = R_M / c_M + + x = r_use[None, :] / R_s[:, None] + prof = self._fx_cumul2d(x) + norm = 2 * R_s * self._norm(M_use, R_s, c_M) + prof = prof * norm[:, None] + + if np.ndim(r) == 0: + prof = np.squeeze(prof, axis=-1) + if np.ndim(M) == 0: + prof = np.squeeze(prof, axis=0) + return prof + + def _fourier_analytic(self, cosmo, k, M, a, mass_def): + M_use = np.atleast_1d(M) + k_use = np.atleast_1d(k) + + # Comoving virial radius + R_M = mass_def.get_radius(cosmo, M_use, a) / a + c_M = self.concentration(cosmo, M_use, a) + R_s = R_M / c_M + + x = k_use[None, :] * R_s[:, None] + Si2, Ci2 = sici(x) + P1 = M / ((c_M / (c_M + 1))**2 / 2) + c_Mp1 = c_M[:, None] + 1 + if self.truncated: + Si1, Ci1 = sici(c_Mp1 * x) + P2 = x * np.sin(x) * (Ci1 - Ci2) - x * np.cos(x) * (Si1 - Si2) + P3 = (-1 + np.sin(c_M[:, None] * x) / (c_Mp1**2 * x) + + c_Mp1 * np.cos(c_M[:, None] * x) / (c_Mp1**2)) + prof = P1[:, None] * (P2 - P3) / 2 + else: + P2 = (-x * (2 * np.sin(x) * Ci2 + np.pi * np.cos(x)) + + 2 * x * np.cos(x) * Si2 + 2) / 4 + prof = P1[:, None] * P2 + + if np.ndim(k) == 0: + prof = np.squeeze(prof, axis=-1) + if np.ndim(M) == 0: + prof = np.squeeze(prof, axis=0) + return prof diff --git a/pyccl/halos/profiles/hod.py b/pyccl/halos/profiles/hod.py new file mode 100644 index 000000000..4bbe28e3b --- /dev/null +++ b/pyccl/halos/profiles/hod.py @@ -0,0 +1,385 @@ +from ...base import warn_api, deprecate_attr +from ..concentration import Concentration +from .profile_base import HaloProfileNumberCounts +import numpy as np +from scipy.special import sici, erf + + +__all__ = ("HaloProfileHOD",) + + +class HaloProfileHOD(HaloProfileNumberCounts): + """ A generic halo occupation distribution (HOD) + profile describing the number density of galaxies + as a function of halo mass. + + The parametrization for the mean profile is: + + .. math:: + \\langle n_g(r)|M,a\\rangle = \\bar{N}_c(M,a) + \\left[f_c(a)+\\bar{N}_s(M,a) u_{\\rm sat}(r|M,a)\\right] + + where :math:`\\bar{N}_c` and :math:`\\bar{N}_s` are the + mean number of central and satellite galaxies respectively, + :math:`f_c` is the observed fraction of central galaxies, and + :math:`u_{\\rm sat}(r|M,a)` is the distribution of satellites + as a function of distance to the halo centre. + + These quantities are parametrized as follows: + + .. math:: + \\bar{N}_c(M,a)=\\frac{1}{2}\\left[1+{\\rm erf} + \\left(\\frac{\\log(M/M_{\\rm min})}{\\sigma_{{\\rm ln}M}} + \\right)\\right] + + .. math:: + \\bar{N}_s(M,a)=\\Theta(M-M_0)\\left(\\frac{M-M_0}{M_1} + \\right)^\\alpha + + .. math:: + u_s(r|M,a)\\propto\\frac{\\Theta(r_{\\rm max}-r)} + {(r/r_g)(1+r/r_g)^2} + + Where :math:`\\Theta(x)` is the Heaviside step function, + and the proportionality constant in the last equation is + such that the volume integral of :math:`u_s` is 1. The + radius :math:`r_g` is related to the NFW scale radius :math:`r_s` + through :math:`r_g=\\beta_g\\,r_s`, and the radius + :math:`r_{\\rm max}` is related to the overdensity radius + :math:`r_\\Delta` as :math:`r_{\\rm max}=\\beta_{\\rm max}r_\\Delta`. + The scale radius is related to the comoving overdensity halo + radius via :math:`R_\\Delta(M) = c(M)\\,r_s`. + + All the quantities :math:`\\log_{10}M_{\\rm min}`, + :math:`\\log_{10}M_0`, :math:`\\log_{10}M_1`, + :math:`\\sigma_{{\\rm ln}M}`, :math:`f_c`, :math:`\\alpha`, + :math:`\\beta_g` and :math:`\\beta_{\\rm max}` are + time-dependent via a linear expansion around a pivot scale + factor :math:`a_*` with an offset (:math:`X_0`) and a tilt + parameter (:math:`X_p`): + + .. math:: + X(a) = X_0 + X_p\\,(a-a_*). + + This definition of the HOD profile draws from several papers + in the literature, including: astro-ph/0408564, arXiv:1706.05422 + and arXiv:1912.08209. The default values used here are roughly + compatible with those found in the latter paper. + + See :class:`~pyccl.halos.profiles_2pt.Profile2ptHOD`) for a + description of the Fourier-space two-point correlator of the + HOD profile. + + Args: + concentration (:obj:`Concentration`): concentration-mass + relation to use with this profile. + log10Mmin_0 (float): offset parameter for + :math:`\\log_{10}M_{\\rm min}`. + log10Mmin_p (float): tilt parameter for + :math:`\\log_{10}M_{\\rm min}`. + siglnM_0 (float): offset parameter for + :math:`\\sigma_{{\\rm ln}M}`. + siglnM_p (float): tilt parameter for + :math:`\\sigma_{{\\rm ln}M}`. + log10M0_0 (float): offset parameter for + :math:`\\log_{10}M_0`. + log10M0_p (float): tilt parameter for + :math:`\\log_{10}M_0`. + log10M1_0 (float): offset parameter for + :math:`\\log_{10}M_1`. + log10M1_p (float): tilt parameter for + :math:`\\log_{10}M_1`. + alpha_0 (float): offset parameter for + :math:`\\alpha`. + alpha_p (float): tilt parameter for + :math:`\\alpha`. + fc_0 (float): offset parameter for + :math:`f_c`. + fc_p (float): tilt parameter for + :math:`f_c`. + bg_0 (float): offset parameter for + :math:`\\beta_g`. + bg_p (float): tilt parameter for + :math:`\\beta_g`. + bmax_0 (float): offset parameter for + :math:`\\beta_{\\rm max}`. + bmax_p (float): tilt parameter for + :math:`\\beta_{\\rm max}`. + a_pivot (float): pivot scale factor :math:`a_*`. + ns_independent (bool): drop requirement to only form + satellites when centrals are present. + """ + __repr_attrs__ = __eq_attrs__ = ( + "concentration", "log10Mmin_0", "log10Mmin_p", "siglnM_0", "siglnM_p", + "log10M0_0", "log10M0_p", "log10M1_0", "log10M1_p", "alpha_0", + "alpha_p", "fc_0", "fc_p", "bg_0", "bg_p", "bmax_0", "bmax_p", + "a_pivot", "ns_independent", "precision_fftlog", "normprof",) + __getattr__ = deprecate_attr(pairs=[ + ("lMmin_0", "log10Mmin_0"), ("lMmin_p", "log10Mmin_p"), + ("siglM_0", "siglnM_0"), ("siglM_p", "siglnM_p"), + ("lM0_0", "log10M0_0"), ("lM0_p", "log10M0_p"), + ("lM1_0", "log10M1_0"), ("lM1_p", "log10M1_p")] + )(super.__getattribute__) + name = 'HOD' + + @warn_api(pairs=[("c_M_relation", "concentration"), + ("siglM_0", "siglnM_0"), ("siglM_p", "siglnM_p"), + ("lMmin_0", "log10Mmin_0"), ("lMmin_p", "log10Mmin_p"), + ("lM0_0", "log10M0_0"), ("lM0_p", "log10M0_p"), + ("lM1_0", "log10M1_0"), ("lM1_p", "log10M1_p")]) + def __init__(self, *, concentration, + log10Mmin_0=12., log10Mmin_p=0., siglnM_0=0.4, + siglnM_p=0., log10M0_0=7., log10M0_p=0., + log10M1_0=13.3, log10M1_p=0., alpha_0=1., + alpha_p=0., fc_0=1., fc_p=0., + bg_0=1., bg_p=0., bmax_0=1., bmax_p=0., + a_pivot=1., ns_independent=False): + if not isinstance(concentration, Concentration): + raise TypeError("concentration must be of type `Concentration`") + + self.concentration = concentration + self.log10Mmin_0 = log10Mmin_0 + self.log10Mmin_p = log10Mmin_p + self.log10M0_0 = log10M0_0 + self.log10M0_p = log10M0_p + self.log10M1_0 = log10M1_0 + self.log10M1_p = log10M1_p + self.siglnM_0 = siglnM_0 + self.siglnM_p = siglnM_p + self.alpha_0 = alpha_0 + self.alpha_p = alpha_p + self.fc_0 = fc_0 + self.fc_p = fc_p + self.bg_0 = bg_0 + self.bg_p = bg_p + self.bmax_0 = bmax_0 + self.bmax_p = bmax_p + self.a_pivot = a_pivot + self.ns_independent = ns_independent + super().__init__() + + @warn_api(pairs=[("lMmin_0", "log10Mmin_0"), ("lMmin_p", "log10Mmin_p"), + ("siglM_0", "siglnM_0"), ("siglM_p", "siglnM_p"), + ("lM0_0", "log10M0_0"), ("lM0_p", "log10M0_p"), + ("lM1_0", "log10M1_0"), ("lM1_p", "log10M1_p")]) + def update_parameters(self, *, log10Mmin_0=None, log10Mmin_p=None, + siglnM_0=None, siglnM_p=None, + log10M0_0=None, log10M0_p=None, + log10M1_0=None, log10M1_p=None, + alpha_0=None, alpha_p=None, + fc_0=None, fc_p=None, + bg_0=None, bg_p=None, + bmax_0=None, bmax_p=None, + a_pivot=None, + ns_independent=None): + """ Update any of the parameters associated with + this profile. Any parameter set to `None` won't be updated. + + Args: + log10Mmin_0 (float): offset parameter for + :math:`\\log_{10}M_{\\rm min}`. + log10Mmin_p (float): tilt parameter for + :math:`\\log_{10}M_{\\rm min}`. + siglnM_0 (float): offset parameter for + :math:`\\sigma_{{\\rm ln}M}`. + siglnM_p (float): tilt parameter for + :math:`\\sigma_{{\\rm ln}M}`. + log10M0_0 (float): offset parameter for + :math:`\\log_{10}M_0`. + log10M0_p (float): tilt parameter for + :math:`\\log_{10}M_0`. + log10M1_0 (float): offset parameter for + :math:`\\log_{10}M_1`. + log10M1_p (float): tilt parameter for + :math:`\\log_{10}M_1`. + alpha_0 (float): offset parameter for + :math:`\\alpha`. + alpha_p (float): tilt parameter for + :math:`\\alpha`. + fc_0 (float): offset parameter for + :math:`f_c`. + fc_p (float): tilt parameter for + :math:`f_c`. + bg_0 (float): offset parameter for + :math:`\\beta_g`. + bg_p (float): tilt parameter for + :math:`\\beta_g`. + bmax_0 (float): offset parameter for + :math:`\\beta_{\\rm max}`. + bmax_p (float): tilt parameter for + :math:`\\beta_{\\rm max}`. + a_pivot (float): pivot scale factor :math:`a_*`. + ns_independent (bool): drop requirement to only form + satellites when centrals are present + """ + if log10Mmin_0 is not None: + self.log10Mmin_0 = log10Mmin_0 + if log10Mmin_p is not None: + self.log10Mmin_p = log10Mmin_p + if log10M0_0 is not None: + self.log10M0_0 = log10M0_0 + if log10M0_p is not None: + self.log10M0_p = log10M0_p + if log10M1_0 is not None: + self.log10M1_0 = log10M1_0 + if log10M1_p is not None: + self.log10M1_p = log10M1_p + if siglnM_0 is not None: + self.siglnM_0 = siglnM_0 + if siglnM_p is not None: + self.siglnM_p = siglnM_p + if alpha_0 is not None: + self.alpha_0 = alpha_0 + if alpha_p is not None: + self.alpha_p = alpha_p + if fc_0 is not None: + self.fc_0 = fc_0 + if fc_p is not None: + self.fc_p = fc_p + if bg_0 is not None: + self.bg_0 = bg_0 + if bg_p is not None: + self.bg_p = bg_p + if bmax_0 is not None: + self.bmax_0 = bmax_0 + if bmax_p is not None: + self.bmax_p = bmax_p + if a_pivot is not None: + self.a_pivot = a_pivot + if ns_independent is not None: + self.ns_independent = ns_independent + + def _usat_real(self, cosmo, r, M, a, mass_def): + r_use = np.atleast_1d(r) + M_use = np.atleast_1d(M) + + # Comoving virial radius + bg = self.bg_0 + self.bg_p * (a - self.a_pivot) + bmax = self.bmax_0 + self.bmax_p * (a - self.a_pivot) + R_M = mass_def.get_radius(cosmo, M_use, a) / a + c_M = self.concentration(cosmo, M_use, a) + R_s = R_M / c_M + c_M *= bmax / bg + + x = r_use[None, :] / (R_s[:, None] * bg) + prof = 1./(x * (1 + x)**2) + # Truncate + prof[r_use[None, :] > R_M[:, None]*bmax] = 0 + + norm = 1. / (4 * np.pi * (bg*R_s)**3 * (np.log(1+c_M) - c_M/(1+c_M))) + prof = prof[:, :] * norm[:, None] + + if np.ndim(r) == 0: + prof = np.squeeze(prof, axis=-1) + if np.ndim(M) == 0: + prof = np.squeeze(prof, axis=0) + return prof + + def _usat_fourier(self, cosmo, k, M, a, mass_def): + M_use = np.atleast_1d(M) + k_use = np.atleast_1d(k) + + # Comoving virial radius + bg = self.bg_0 + self.bg_p * (a - self.a_pivot) + bmax = self.bmax_0 + self.bmax_p * (a - self.a_pivot) + R_M = mass_def.get_radius(cosmo, M_use, a) / a + c_M = self.concentration(cosmo, M_use, a) + R_s = R_M / c_M + c_M *= bmax / bg + + x = k_use[None, :] * R_s[:, None] * bg + Si1, Ci1 = sici((1 + c_M[:, None]) * x) + Si2, Ci2 = sici(x) + + P1 = 1. / (np.log(1+c_M) - c_M/(1+c_M)) + P2 = np.sin(x) * (Si1 - Si2) + np.cos(x) * (Ci1 - Ci2) + P3 = np.sin(c_M[:, None] * x) / ((1 + c_M[:, None]) * x) + prof = P1[:, None] * (P2 - P3) + + if np.ndim(k) == 0: + prof = np.squeeze(prof, axis=-1) + if np.ndim(M) == 0: + prof = np.squeeze(prof, axis=0) + return prof + + def _real(self, cosmo, r, M, a, mass_def): + r_use = np.atleast_1d(r) + M_use = np.atleast_1d(M) + + Nc = self._Nc(M_use, a) + Ns = self._Ns(M_use, a) + fc = self._fc(a) + # NFW profile + ur = self._usat_real(cosmo, r_use, M_use, a, mass_def) + + if self.ns_independent: + prof = Nc[:, None] * fc + Ns[:, None] * ur + else: + prof = Nc[:, None] * (fc + Ns[:, None] * ur) + + if np.ndim(r) == 0: + prof = np.squeeze(prof, axis=-1) + if np.ndim(M) == 0: + prof = np.squeeze(prof, axis=0) + return prof + + def _fourier(self, cosmo, k, M, a, mass_def): + M_use = np.atleast_1d(M) + k_use = np.atleast_1d(k) + + Nc = self._Nc(M_use, a) + Ns = self._Ns(M_use, a) + fc = self._fc(a) + # NFW profile + uk = self._usat_fourier(cosmo, k_use, M_use, a, mass_def) + + if self.ns_independent: + prof = Nc[:, None] * fc + Ns[:, None] * uk + else: + prof = Nc[:, None] * (fc + Ns[:, None] * uk) + + if np.ndim(k) == 0: + prof = np.squeeze(prof, axis=-1) + if np.ndim(M) == 0: + prof = np.squeeze(prof, axis=0) + return prof + + def _fourier_variance(self, cosmo, k, M, a, mass_def): + # Fourier-space variance of the HOD profile + M_use = np.atleast_1d(M) + k_use = np.atleast_1d(k) + + Nc = self._Nc(M_use, a) + Ns = self._Ns(M_use, a) + fc = self._fc(a) + # NFW profile + uk = self._usat_fourier(cosmo, k_use, M_use, a, mass_def) + + prof = Ns[:, None] * uk + if self.ns_independent: + prof = 2 * Nc[:, None] * fc * prof + prof**2 + else: + prof = Nc[:, None] * (2 * fc * prof + prof**2) + + if np.ndim(k) == 0: + prof = np.squeeze(prof, axis=-1) + if np.ndim(M) == 0: + prof = np.squeeze(prof, axis=0) + return prof + + def _fc(self, a): + # Observed fraction of centrals + return self.fc_0 + self.fc_p * (a - self.a_pivot) + + def _Nc(self, M, a): + # Number of centrals + Mmin = 10.**(self.log10Mmin_0 + self.log10Mmin_p * (a - self.a_pivot)) + siglnM = self.siglnM_0 + self.siglnM_p * (a - self.a_pivot) + return 0.5 * (1 + erf(np.log(M/Mmin)/siglnM)) + + def _Ns(self, M, a): + # Number of satellites + M0 = 10.**(self.log10M0_0 + self.log10M0_p * (a - self.a_pivot)) + M1 = 10.**(self.log10M1_0 + self.log10M1_p * (a - self.a_pivot)) + alpha = self.alpha_0 + self.alpha_p * (a - self.a_pivot) + return np.heaviside(M-M0, 1) * (np.fabs(M-M0) / M1)**alpha diff --git a/pyccl/halos/profiles/nfw.py b/pyccl/halos/profiles/nfw.py new file mode 100644 index 000000000..83552be4e --- /dev/null +++ b/pyccl/halos/profiles/nfw.py @@ -0,0 +1,211 @@ +from ...base import warn_api +from ..concentration import Concentration +from .profile_base import HaloProfileMatter +import numpy as np +from scipy.special import sici + + +__all__ = ("HaloProfileNFW",) + + +class HaloProfileNFW(HaloProfileMatter): + """ Navarro-Frenk-White (astro-ph:astro-ph/9508025) profile. + + .. math:: + \\rho(r) = \\frac{\\rho_0} + {\\frac{r}{r_s}\\left(1+\\frac{r}{r_s}\\right)^2} + + where :math:`r_s` is related to the spherical overdensity + halo radius :math:`R_\\Delta(M)` through the concentration + parameter :math:`c(M)` as + + .. math:: + R_\\Delta(M) = c(M)\\,r_s + + and the normalization :math:`\\rho_0` is + + .. math:: + \\rho_0 = \\frac{M}{4\\pi\\,r_s^3\\,[\\log(1+c) - c/(1+c)]} + + By default, this profile is truncated at :math:`r = R_\\Delta(M)`. + + Args: + concentration (:obj:`Concentration`): concentration-mass + relation to use with this profile. + fourier_analytic (bool): set to `True` if you want to compute + the Fourier profile analytically (and not through FFTLog). + Default: `True`. + projected_analytic (bool): set to `True` if you want to + compute the 2D projected profile analytically (and not + through FFTLog). Default: `False`. + cumul2d_analytic (bool): set to `True` if you want to + compute the 2D cumulative surface density analytically + (and not through FFTLog). Default: `False`. + truncated (bool): set to `True` if the profile should be + truncated at :math:`r = R_\\Delta` (i.e. zero at larger + radii. + """ + __repr_attrs__ = __eq_attrs__ = ( + "concentration", "fourier_analytic", "projected_analytic", + "cumul2d_analytic", "truncated", "precision_fftlog", "normprof",) + name = 'NFW' + + @warn_api(pairs=[("c_M_relation", "concentration")]) + def __init__(self, *, concentration, + fourier_analytic=True, + projected_analytic=False, + cumul2d_analytic=False, + truncated=True): + if not isinstance(concentration, Concentration): + raise TypeError("concentration must be of type `Concentration`") + + self.concentration = concentration + self.truncated = truncated + self.fourier_analytic = fourier_analytic + self.projected_analytic = projected_analytic + self.cumul2d_analytic = cumul2d_analytic + if fourier_analytic: + self._fourier = self._fourier_analytic + if projected_analytic: + if truncated: + raise ValueError("Analytic projected profile not supported " + "for truncated NFW. Set `truncated` or " + "`projected_analytic` to `False`.") + self._projected = self._projected_analytic + if cumul2d_analytic: + if truncated: + raise ValueError("Analytic cumuative 2d profile not supported " + "for truncated NFW. Set `truncated` or " + "`cumul2d_analytic` to `False`.") + self._cumul2d = self._cumul2d_analytic + self._omln2 = 1 - np.log(2) + super().__init__() + self.update_precision_fftlog(padding_hi_fftlog=1E2, + padding_lo_fftlog=1E-2, + n_per_decade=1000, + plaw_fourier=-2.) + + def _norm(self, M, Rs, c): + # NFW normalization from mass, radius and concentration + return M / (4 * np.pi * Rs**3 * (np.log(1+c) - c/(1+c))) + + def _real(self, cosmo, r, M, a, mass_def): + r_use = np.atleast_1d(r) + M_use = np.atleast_1d(M) + + # Comoving virial radius + R_M = mass_def.get_radius(cosmo, M_use, a) / a + c_M = self.concentration(cosmo, M_use, a) + R_s = R_M / c_M + + x = r_use[None, :] / R_s[:, None] + prof = 1./(x * (1 + x)**2) + if self.truncated: + prof[r_use[None, :] > R_M[:, None]] = 0 + + norm = self._norm(M_use, R_s, c_M) + prof = prof[:, :] * norm[:, None] + + if np.ndim(r) == 0: + prof = np.squeeze(prof, axis=-1) + if np.ndim(M) == 0: + prof = np.squeeze(prof, axis=0) + return prof + + def _fx_projected(self, x): + + def f1(xx): + x2m1 = xx * xx - 1 + return 1 / x2m1 + np.arccosh(1 / xx) / np.fabs(x2m1)**1.5 + + def f2(xx): + x2m1 = xx * xx - 1 + return 1 / x2m1 - np.arccos(1 / xx) / np.fabs(x2m1)**1.5 + + xf = x.flatten() + return np.piecewise(xf, + [xf < 1, xf > 1], + [f1, f2, 1./3.]).reshape(x.shape) + + def _projected_analytic(self, cosmo, r, M, a, mass_def): + r_use = np.atleast_1d(r) + M_use = np.atleast_1d(M) + + # Comoving virial radius + R_M = mass_def.get_radius(cosmo, M_use, a) / a + c_M = self.concentration(cosmo, M_use, a) + R_s = R_M / c_M + + x = r_use[None, :] / R_s[:, None] + prof = self._fx_projected(x) + norm = 2 * R_s * self._norm(M_use, R_s, c_M) + prof *= norm[:, None] + + if np.ndim(r) == 0: + prof = np.squeeze(prof, axis=-1) + if np.ndim(M) == 0: + prof = np.squeeze(prof, axis=0) + return prof + + def _fx_cumul2d(self, x): + + def f1(xx): + sqx2m1 = np.sqrt(np.fabs(xx * xx - 1)) + return np.log(0.5 * xx) + np.arccosh(1 / xx) / sqx2m1 + + def f2(xx): + sqx2m1 = np.sqrt(np.fabs(xx * xx - 1)) + return np.log(0.5 * xx) + np.arccos(1 / xx) / sqx2m1 + + xf = x.flatten() + f = np.piecewise(xf, + [xf < 1, xf > 1], + [f1, f2, self._omln2]).reshape(x.shape) + return 2 * f / x**2 + + def _cumul2d_analytic(self, cosmo, r, M, a, mass_def): + r_use = np.atleast_1d(r) + M_use = np.atleast_1d(M) + + # Comoving virial radius + R_M = mass_def.get_radius(cosmo, M_use, a) / a + c_M = self.concentration(cosmo, M_use, a) + R_s = R_M / c_M + + x = r_use[None, :] / R_s[:, None] + prof = self._fx_cumul2d(x) + norm = 2 * R_s * self._norm(M_use, R_s, c_M) + prof = prof[:, :] * norm[:, None] + + if np.ndim(r) == 0: + prof = np.squeeze(prof, axis=-1) + if np.ndim(M) == 0: + prof = np.squeeze(prof, axis=0) + return prof + + def _fourier_analytic(self, cosmo, k, M, a, mass_def): + M_use = np.atleast_1d(M) + k_use = np.atleast_1d(k) + + # Comoving virial radius + R_M = mass_def.get_radius(cosmo, M_use, a) / a + c_M = self.concentration(cosmo, M_use, a) + R_s = R_M / c_M + + x = k_use[None, :] * R_s[:, None] + Si2, Ci2 = sici(x) + P1 = M_use / (np.log(1 + c_M) - c_M / (1 + c_M)) + if self.truncated: + Si1, Ci1 = sici((1 + c_M[:, None]) * x) + P2 = np.sin(x) * (Si1 - Si2) + np.cos(x) * (Ci1 - Ci2) + P3 = np.sin(c_M[:, None] * x) / ((1 + c_M[:, None]) * x) + prof = P1[:, None] * (P2 - P3) + else: + P2 = np.sin(x) * (0.5 * np.pi - Si2) - np.cos(x) * Ci2 + prof = P1[:, None] * P2 + + if np.ndim(k) == 0: + prof = np.squeeze(prof, axis=-1) + if np.ndim(M) == 0: + prof = np.squeeze(prof, axis=0) + return prof diff --git a/pyccl/halos/profiles/powerlaw.py b/pyccl/halos/profiles/powerlaw.py new file mode 100644 index 000000000..bae524c0a --- /dev/null +++ b/pyccl/halos/profiles/powerlaw.py @@ -0,0 +1,61 @@ +from ...base import warn_api, deprecated +from .profile_base import HaloProfile +import numpy as np + +__all__ = ("HaloProfilePowerLaw",) + + +class HaloProfilePowerLaw(HaloProfile): + """ Power-law profile + + .. math:: + \\rho(r) = (r/r_s)^\\alpha + + Args: + r_scale (:obj:`function`): the correlation length of + the profile. The signature of this function + should be `f(cosmo, M, a, mass_def)`, where `cosmo` + is a :class:`~pyccl.core.Cosmology` object, `M` is a halo mass + in units of M_sun, `a` is the scale factor and + `mass_def` is a :class:`~pyccl.halos.massdef.MassDef` object. + tilt (:obj:`function`): the power law index of the + profile. The signature of this function should + be `f(cosmo, a)`. + """ + __repr_attrs__ = __eq_attrs__ = ("r_scale", "tilt", "precision_fftlog", + "normprof",) + name = 'PowerLaw' + normprof = False + + @deprecated() + @warn_api + def __init__(self, *, r_scale, tilt): + self.r_scale = r_scale + self.tilt = tilt + super().__init__() + + def _get_plaw_fourier(self, cosmo, a): + # This is the optimal value for a pure power law + # profile. + return self.tilt(cosmo, a) + + def _get_plaw_projected(self, cosmo, a): + # This is the optimal value for a pure power law + # profile. + return -3 - self.tilt(cosmo, a) + + def _real(self, cosmo, r, M, a, mass_def): + r_use = np.atleast_1d(r) + M_use = np.atleast_1d(M) + + # Compute scale + rs = self.r_scale(cosmo, M_use, a, mass_def) + tilt = self.tilt(cosmo, a) + # Form factor + prof = (r_use[None, :] / rs[:, None])**tilt + + if np.ndim(r) == 0: + prof = np.squeeze(prof, axis=-1) + if np.ndim(M) == 0: + prof = np.squeeze(prof, axis=0) + return prof diff --git a/pyccl/halos/profiles/pressure_gnfw.py b/pyccl/halos/profiles/pressure_gnfw.py new file mode 100644 index 000000000..c7420b03e --- /dev/null +++ b/pyccl/halos/profiles/pressure_gnfw.py @@ -0,0 +1,246 @@ +from ...base import UnlockInstance, warn_api +from .profile_base import HaloProfilePressure +import numpy as np + + +__all__ = ("HaloProfilePressureGNFW",) + + +class HaloProfilePressureGNFW(HaloProfilePressure): + """ Generalized NFW pressure profile by Arnaud et al. + (2010A&A...517A..92A). + + The parametrization is: + + .. math:: + + P_e(r) = C\\times P_0 h_{70}^E (c_{500} x)^{-\\gamma} + [1+(c_{500}x)^\\alpha]^{(\\gamma-\\beta)/\\alpha}, + + where + + .. math:: + + C = 1.65\\,h_{70}^2\\left(\\frac{H(z)}{H_0}\\right)^{8/3} + \\left[\\frac{h_{70}\\tilde{M}_{500}} + {3\\times10^{14}\\,M_\\odot}\\right]^{2/3+\\alpha_{\\mathrm{P}}}, + + :math:`x = r/\\tilde{r}_{500}`, :math:`h_{70}=h/0.7`, and the + exponent :math:`E` is -1 for SZ-based profile normalizations + and -1.5 for X-ray-based normalizations. The biased mass + :math:`\\tilde{M}_{500}` is related to the true overdensity + mass :math:`M_{500}` via the mass bias parameter :math:`(1-b)` + as :math:`\\tilde{M}_{500}=(1-b)M_{500}`. :math:`\\tilde{r}_{500}` + is the overdensity halo radius associated with :math:`\\tilde{M}_{500}` + (note the intentional tilde!), and the profile is defined for + a halo overdensity :math:`\\Delta=500` with respect to the + critical density. + + The default arguments (other than ``mass_bias``), correspond to the + profile parameters used in the Planck 2013 (V) paper. The profile is + calculated in physical (non-comoving) units of :math:`\\mathrm{eV/cm^3}`. + + Parameters + ---------- + mass_bias : float + The mass bias parameter :math:`1-b`. + P0 : float + Profile normalization. + c500 : float + Concentration parameter. + alpha, beta, gamma : float + Profile shape parameters. + alpha_P : float + Additional mass dependence exponent + P0_hexp : float + Power of :math:`h` with which the normalization parameter scales. + Equal to :math:`-1` for SZ-based normalizations, + and :math:`-3/2` for X-ray-based normalizations. + qrange : 2-sequence + Limits of integration used when computing the Fourier-space + profile template, in units of :math:`R_{\\mathrm{vir}}`. + nq : int + Number of sampling points of the Fourier-space profile template. + x_out : float + Profile threshold, in units of :math:`R_{\\mathrm{500c}}`. + Defaults to :math:`+\\infty`. + """ + __repr_attrs__ = __eq_attrs__ = ( + "mass_bias", "P0", "c500", "alpha", "alpha_P", "beta", + "gamma", "P0_hexp", "qrange", "nq", "x_out", + "precision_fftlog", "normprof",) + name = 'GNFW' + + @warn_api + def __init__(self, *, mass_bias=0.8, P0=6.41, + c500=1.81, alpha=1.33, alpha_P=0.12, + beta=4.13, gamma=0.31, P0_hexp=-1., + qrange=(1e-3, 1e3), nq=128, x_out=np.inf): + self.qrange = qrange + self.nq = nq + self.mass_bias = mass_bias + self.P0 = P0 + self.c500 = c500 + self.alpha = alpha + self.alpha_P = alpha_P + self.beta = beta + self.gamma = gamma + self.P0_hexp = P0_hexp + self.x_out = x_out + + # Interpolator for dimensionless Fourier-space profile + self._fourier_interp = None + super().__init__() + + @warn_api + def update_parameters(self, *, mass_bias=None, P0=None, + c500=None, alpha=None, beta=None, gamma=None, + alpha_P=None, P0_hexp=None, x_out=None): + """Update any of the parameters associated with this profile. + Any parameter set to ``None`` won't be updated. + + .. note:: + + A change in ``alpha``, ``beta``, ``gamma``, ``c500``, or ``x_out`` + recomputes the Fourier-space template, which may be slow. + + Arguments + --------- + mass_bias : float + The mass bias parameter :math:`1-b`. + P0 : float + Profile normalization. + c500 : float + Concentration parameter. + alpha, beta, gamma : float + Profile shape parameter. + alpha_P : float + Additional mass-dependence exponent. + P0_hexp : float + Power of ``h`` with which the normalization scales. + SZ-based normalizations: -1. X-ray-based normalizations: -3/2. + x_out : float + Profile threshold (as a fraction of r500c). + """ + if mass_bias is not None: + self.mass_bias = mass_bias + if alpha_P is not None: + self.alpha_P = alpha_P + if P0 is not None: + self.P0 = P0 + if P0_hexp is not None: + self.P0_hexp = P0_hexp + + # Check if we need to recompute the Fourier profile. + re_fourier = False + if alpha is not None and alpha != self.alpha: + re_fourier = True + self.alpha = alpha + if beta is not None and beta != self.beta: + re_fourier = True + self.beta = beta + if gamma is not None and gamma != self.gamma: + re_fourier = True + self.gamma = gamma + if c500 is not None and c500 != self.c500: + re_fourier = True + self.c500 = c500 + if x_out is not None and x_out != self.x_out: + re_fourier = True + self.x_out = x_out + + if re_fourier and (self._fourier_interp is not None): + self._fourier_interp = self._integ_interp() + + def _form_factor(self, x): + # Scale-dependent factor of the GNFW profile. + f1 = (self.c500*x)**(-self.gamma) + exponent = -(self.beta-self.gamma)/self.alpha + f2 = (1+(self.c500*x)**self.alpha)**exponent + return f1*f2 + + def _integ_interp(self): + # Precomputes the Fourier transform of the profile in terms + # of the scaled radius x and creates a spline interpolator + # for it. + from scipy.interpolate import interp1d + from scipy.integrate import quad + + def integrand(x): + return self._form_factor(x)*x + + q_arr = np.geomspace(self.qrange[0], self.qrange[1], self.nq) + # We use the `weight` feature of quad to quickly estimate + # the Fourier transform. We could use the existing FFTLog + # framework, but this is a lot less of a kerfuffle. + f_arr = np.array([quad(integrand, + a=1e-4, b=self.x_out, # limits of integration + weight="sin", # fourier sine weight + wvar=q)[0] / q + for q in q_arr]) + Fq = interp1d(np.log(q_arr), f_arr, + fill_value="extrapolate", + bounds_error=False) + return Fq + + def _norm(self, cosmo, M, a, mb): + # Computes the normalisation factor of the GNFW profile. + # Normalisation factor is given in units of eV/cm^3. + # (Bolliet et al. 2017). + h70 = cosmo["h"]/0.7 + C0 = 1.65*h70**2 + CM = (h70*M*mb/3E14)**(2/3+self.alpha_P) # M dependence + Cz = cosmo.h_over_h0(a)**(8/3) # z dependence + P0_corr = self.P0 * h70**self.P0_hexp # h-corrected P_0 + return P0_corr * C0 * CM * Cz + + def _real(self, cosmo, r, M, a, mass_def): + # Real-space profile. + # Output in units of eV/cm^3 + r_use = np.atleast_1d(r) + M_use = np.atleast_1d(M) + + # Comoving virial radius + # (1-b) + mb = self.mass_bias + # R_Delta*(1+z) + R = mass_def.get_radius(cosmo, M_use * mb, a) / a + + nn = self._norm(cosmo, M_use, a, mb) + prof = self._form_factor(r_use[None, :] / R[:, None]) + prof *= nn[:, None] + + if np.ndim(r) == 0: + prof = np.squeeze(prof, axis=-1) + if np.ndim(M) == 0: + prof = np.squeeze(prof, axis=0) + return prof + + def _fourier(self, cosmo, k, M, a, mass_def): + # Fourier-space profile. + # Output in units of eV * Mpc^3 / cm^3. + + # Tabulate if not done yet + if self._fourier_interp is None: + with UnlockInstance(self): + self._fourier_interp = self._integ_interp() + + # Input handling + M_use = np.atleast_1d(M) + k_use = np.atleast_1d(k) + + # hydrostatic bias + mb = self.mass_bias + # R_Delta*(1+z) + R = mass_def.get_radius(cosmo, M_use * mb, a) / a + + ff = self._fourier_interp(np.log(k_use[None, :] * R[:, None])) + nn = self._norm(cosmo, M_use, a, mb) + + prof = (4*np.pi*R**3 * nn)[:, None] * ff + + if np.ndim(k) == 0: + prof = np.squeeze(prof, axis=-1) + if np.ndim(M) == 0: + prof = np.squeeze(prof, axis=0) + return prof diff --git a/pyccl/halos/profiles/profile_base.py b/pyccl/halos/profiles/profile_base.py new file mode 100644 index 000000000..d3ed098cb --- /dev/null +++ b/pyccl/halos/profiles/profile_base.py @@ -0,0 +1,505 @@ +from ...pyutils import resample_array, _fftlog_transform +from ...base import (CCLAutoRepr, abstractlinkedmethod, templatemethod, + unlock_instance, warn_api, deprecate_attr) +from ...base.parameters import FFTLogParams +import numpy as np +from abc import abstractmethod +import functools + + +__all__ = ("HaloProfile", "HaloProfileNumberCounts", "HaloProfileMatter", + "HaloProfilePressure", "HaloProfileCIB",) + + +class HaloProfile(CCLAutoRepr): + """ This class implements functionality associated to + halo profiles. You should not use this class directly. + Instead, use one of the subclasses implemented in CCL + for specific halo profiles, or write your own subclass. + `HaloProfile` classes contain methods to compute halo + profiles in real (3D) and Fourier spaces, as well as the + projected (2D) profile and the cumulative mean surface + density. + + A minimal implementation of a `HaloProfile` subclass + should contain a method `_real` that returns the + real-space profile as a function of cosmology, + comoving radius, mass and scale factor. The default + functionality in the base `HaloProfile` class will then + allow the automatic calculation of the Fourier-space + and projected profiles, as well as the cumulative + mass density, based on the real-space profile using + FFTLog to carry out fast Hankel transforms. See the + CCL note for details. Alternatively, you can implement + a `_fourier` method for the Fourier-space profile, and + all other quantities will be computed from it. It is + also possible to implement specific versions of any + of these quantities if one wants to avoid the FFTLog + calculation. + """ + __repr_attrs__ = __eq_attrs__ = ("precision_fftlog",) + __getattr__ = deprecate_attr(pairs=[('cM', 'concentration')] + )(super.__getattribute__) + + def __init__(self): + self.precision_fftlog = FFTLogParams() + + @property + @abstractmethod + def normprof(self) -> bool: + """Normalize the profile in auto- and cross-correlations by + :math:`I^0_1(k\\rightarrow 0, a|u)` + (see :meth:`~pyccl.halos.halo_model.HMCalculator.I_0_1`). + """ + + # TODO: CCLv3 - Rename & allocate _normprof_bool to the subclasses. + + def _normprof_false(self, hmc, **settings): + """Option for ``normprof = False``.""" + return lambda *args, cosmo, a, **kwargs: 1. + + def _normprof_true(self, hmc, k_min=1e-5): + """Option for ``normprof = True``.""" + # TODO: remove the first two lines in CCLv3. + k_hmc = hmc.precision["k_min"] + k_min = k_hmc if k_hmc != k_min else k_min + M, mass_def = hmc._mass, hmc.mass_def + return functools.partial(self.fourier, k=k_min, M=M, mass_def=mass_def) + + def _normalization(self, hmc, **settings): + """This is the API adapter and it decides which norm to use. + It returns a function of ``cosmo`` and ``a``. Optional args & kwargs. + """ + if self.normprof: + return self._normprof_true(hmc, **settings) + return self._normprof_false(hmc, **settings) + + @unlock_instance(mutate=True) + @functools.wraps(FFTLogParams.update_parameters) + def update_precision_fftlog(self, **kwargs): + self.precision_fftlog.update_parameters(**kwargs) + + def _get_plaw_fourier(self, cosmo, a): + """ This controls the value of `plaw_fourier` to be used + as a function of cosmology and scale factor. + + Args: + cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. + a (float): scale factor. + + Returns: + float: power law index to be used with FFTLog. + """ + return self.precision_fftlog['plaw_fourier'] + + def _get_plaw_projected(self, cosmo, a): + """ This controls the value of `plaw_projected` to be + used as a function of cosmology and scale factor. + + Args: + cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. + a (float): scale factor. + + Returns: + float: power law index to be used with FFTLog. + """ + return self.precision_fftlog['plaw_projected'] + + @abstractlinkedmethod + def _real(self, cosmo, r, M, a, mass_def=None): + """TODO: Write some useful docstring.""" + + @abstractlinkedmethod + def _fourier(self, cosmo, k, M, a, mass_def=None): + "TODO: Write some useful docstring.""" + + @templatemethod + def _projected(self, cosmo, r, M, a, mass_def=None): + """TODO: Write some useful docstring.""" + + @templatemethod + def _cumul2d(self, cosmo, r, M, a, mass_def=None): + """TODO: Write some useful docstring.""" + + @warn_api + def real(self, cosmo, r, M, a, *, mass_def=None): + """ Returns the 3D real-space value of the profile as a + function of cosmology, radius, halo mass and scale factor. + + Args: + cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. + r (float or array_like): comoving radius in Mpc. + M (float or array_like): halo mass in units of M_sun. + a (float): scale factor. + mass_def (:class:`~pyccl.halos.massdef.MassDef`): + a mass definition object. + + Returns: + float or array_like: halo profile. The shape of the + output will be `(N_M, N_r)` where `N_r` and `N_m` are + the sizes of `r` and `M` respectively. If `r` or `M` + are scalars, the corresponding dimension will be + squeezed out on output. + """ + if self._is_implemented("_real"): + return self._real(cosmo, r, M, a, mass_def) + return self._fftlog_wrap(cosmo, r, M, a, mass_def, fourier_out=False) + + @warn_api + def fourier(self, cosmo, k, M, a, *, mass_def=None): + """ Returns the Fourier-space value of the profile as a + function of cosmology, wavenumber, halo mass and + scale factor. + + .. math:: + \\rho(k)=\\frac{1}{2\\pi^2} \\int dr\\, r^2\\, + \\rho(r)\\, j_0(k r) + + Args: + cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. + k (float or array_like): comoving wavenumber in Mpc^-1. + M (float or array_like): halo mass in units of M_sun. + a (float): scale factor. + mass_def (:class:`~pyccl.halos.massdef.MassDef`): + a mass definition object. + + Returns: + float or array_like: halo profile. The shape of the + output will be `(N_M, N_k)` where `N_k` and `N_m` are + the sizes of `k` and `M` respectively. If `k` or `M` + are scalars, the corresponding dimension will be + squeezed out on output. + """ + if self._is_implemented("_fourier"): + return self._fourier(cosmo, k, M, a, mass_def) + return self._fftlog_wrap(cosmo, k, M, a, mass_def, fourier_out=True) + + @warn_api(pairs=[("r_t", "r")]) + def projected(self, cosmo, r, M, a, *, mass_def=None): + """ Returns the 2D projected profile as a function of + cosmology, radius, halo mass and scale factor. + + .. math:: + \\Sigma(R)= \\int dr_\\parallel\\, + \\rho(\\sqrt{r_\\parallel^2 + R^2}) + + Args: + cosmo (:class:`~pyccl.core.Cosmology`): a Cosmology object. + r (float or array_like): comoving radius in Mpc. + M (float or array_like): halo mass in units of M_sun. + a (float): scale factor. + mass_def (:class:`~pyccl.halos.massdef.MassDef`): + a mass definition object. + + Returns: + float or array_like: halo profile. The shape of the + output will be `(N_M, N_r)` where `N_r` and `N_m` are + the sizes of `r` and `M` respectively. If `r` or `M` + are scalars, the corresponding dimension will be + squeezed out on output. + """ + if self._is_implemented("_projected"): + return self._projected(cosmo, r, M, a, mass_def) + return self._projected_fftlog_wrap(cosmo, r, M, a, mass_def, + is_cumul2d=False) + + @warn_api(pairs=[("r_t", "r")]) + def cumul2d(self, cosmo, r, M, a, *, mass_def=None): + """ Returns the 2D cumulative surface density as a + function of cosmology, radius, halo mass and scale + factor. + + .. math:: + \\Sigma( 0.] N_nu_mass = len(m_nu) - # Call function - OmNuh2, status = lib.Omeganuh2_vec(N_nu_mass, T_CMB, + OmNuh2, status = lib.Omeganuh2_vec(N_nu_mass, T_CMB, T_ncdm, a, m_nu, a.size, status) # Check status and return @@ -50,23 +60,26 @@ def Omeganuh2(a, m_nu, T_CMB=None): return OmNuh2 -def nu_masses(OmNuh2, mass_split, T_CMB=None): - """Returns the neutrinos mass(es) for a given OmNuh2, according to the +@warn_api(pairs=[("OmNuh2", "Omega_nu_h2")]) +def nu_masses(*, Omega_nu_h2, mass_split, T_CMB=None): + """Returns the neutrinos mass(es) for a given Omega_nu_h2, according to the splitting convention specified by the user. Args: - OmNuh2 (float): Neutrino energy density at z=0 times h^2 + Omega_nu_h2 (float): Neutrino energy density at z=0 times h^2 mass_split (str): indicates how the masses should be split up Should be one of 'normal', 'inverted', 'equal' or 'sum'. - T_CMB (float, optional): Temperature of the CMB (K). Default: 2.725. + T_CMB (float, optional): Deprecated - do not use. + Temperature of the CMB (K). Default: 2.725. Returns: float or array-like: Neutrino mass(es) corresponding to this Omeganuh2 """ status = 0 - if T_CMB is None: - T_CMB = physical_constants.T_CMB + if T_CMB is not None: + warnings.warn("T_CMB is deprecated as an argument of `nu_masses.", + CCLDeprecationWarning) if mass_split not in neutrino_mass_splits.keys(): raise ValueError( @@ -77,10 +90,10 @@ def nu_masses(OmNuh2, mass_split, T_CMB=None): # Call function if mass_split in ['normal', 'inverted', 'equal']: mnu, status = lib.nu_masses_vec( - OmNuh2, neutrino_mass_splits[mass_split], T_CMB, 3, status) + Omega_nu_h2, neutrino_mass_splits[mass_split], 3, status) elif mass_split in ['sum', 'single']: mnu, status = lib.nu_masses_vec( - OmNuh2, neutrino_mass_splits[mass_split], T_CMB, 1, status) + Omega_nu_h2, neutrino_mass_splits[mass_split], 1, status) mnu = mnu[0] # Check status and return diff --git a/pyccl/nl_pt/power.py b/pyccl/nl_pt/power.py index 86e550bda..94da27879 100644 --- a/pyccl/nl_pt/power.py +++ b/pyccl/nl_pt/power.py @@ -1,6 +1,4 @@ import numpy as np -from .. import ccllib as lib -from ..core import check from ..pk2d import Pk2D from ..power import linear_matter_power, nonlin_matter_power from ..background import growth_factor @@ -511,10 +509,7 @@ def get_pt_pk2d(cosmo, tracer1, tracer2=None, ptc=None, :class:`~pyccl.nl_pt.power.PTCalculator`: PT Calc [optional] """ if a_arr is None: - status = 0 - na = lib.get_pk_spline_na(cosmo.cosmo) - a_arr, status = lib.get_pk_spline_a(cosmo.cosmo, na, status) - check(status, cosmo=cosmo) + a_arr = cosmo.get_pk_spline_a() if tracer2 is None: tracer2 = tracer1 diff --git a/pyccl/nl_pt/tracers.py b/pyccl/nl_pt/tracers.py index 419fb6867..ea946d163 100644 --- a/pyccl/nl_pt/tracers.py +++ b/pyccl/nl_pt/tracers.py @@ -2,7 +2,7 @@ from scipy.interpolate import interp1d from ..pyutils import _check_array_params from ..background import growth_factor -from ..parameters import physical_constants +from ..base.parameters import physical_constants def translate_IA_norm(cosmo, z, a1=1.0, a1delta=None, a2=None, diff --git a/pyccl/parameters.py b/pyccl/parameters.py deleted file mode 100644 index 059e3b1ce..000000000 --- a/pyccl/parameters.py +++ /dev/null @@ -1,129 +0,0 @@ -from . import ccllib as lib -from .errors import warnings, CCLDeprecationWarning - - -class CCLParameters: - """Base for classes holding global CCL parameters and their values. - - Subclasses contain a reference to the C-struct with the collection - of parameters and their values (via SWIG). All subclasses act as proxies - to the CCL parameters at the C-level. - - Subclasses automatically store a backup of the initial parameter state - to enable ad hoc reloading. - """ - - def __init_subclass__(cls, instance=None, freeze=False): - """Routine for subclass initialization. - - Parameters: - instance (``pyccl.ccllib.cvar``): - Reference to ``cvar`` where the parameters are implemented. - freeze (``bool``): - Disable parameter mutation. - """ - super().__init_subclass__() - cls._instance = instance - cls._frozen = freeze - - def _new_setattr(self, key, value): - # Make instances of the SWIG-level class immutable - # so that everything is handled through this interface. - # SWIG only assigns `this` via the low level `_ccllib`; - # we therefore disable all other direct assignments. - if key == "this": - return object.__setattr__(self, key, value) - name = self.__class__.__name__ - # TODO: Deprecation cycle for fully immutable Cosmology objects. - # raise AttributeError(f"Direct assignment in {name} not supported.") # noqa - warnings.warn( - f"Direct assignment of {name} is deprecated " - "and an error will be raised in the next CCL release. " - f"Set via `pyccl.{name}.{key}` before instantiation.", - CCLDeprecationWarning) - object.__setattr__(self, key, value) - - cls._instance.__class__.__setattr__ = _new_setattr - - def __init__(self): - # Keep a copy of the default parameters. - object.__setattr__(self, "_bak", CCLParameters.get_params_dict(self)) - - def __getattribute__(self, name): - get = object.__getattribute__ - try: - return get(get(self, "_instance"), name) - except AttributeError: - return get(self, name) - - def __setattr__(self, key, value): - if self._frozen and key != "T_CMB": - # `T_CMB` mutates in Cosmology. - name = self.__class__.__name__ - raise AttributeError(f"Instances of {name} are frozen.") - if not hasattr(self._instance, key): - raise KeyError(f"Parameter {key} does not exist.") - if (key, value) == ("A_SPLINE_MAX", 1.0): - # Setting `A_SPLINE_MAX` to its default value; do nothing. - return - object.__setattr__(self._instance, key, value) - - __getitem__ = __getattribute__ - - __setitem__ = __setattr__ - - def __repr__(self): - out = self._bak.copy() - for par in out: - out[par] = getattr(self, par) - return repr(out) - - def reload(self): - """Reload the C-level default CCL parameters.""" - frozen = self._frozen - if frozen: - object.__setattr__(self, "_frozen", False) - for param, value in self._bak.items(): - setattr(self, param, value) - object.__setattr__(self, "_frozen", frozen) - - def freeze(self): - """Freeze an instance of ``CCLParameters``.""" - object.__setattr__(self, "_frozen", True) - - def unfreeze(self): - """Unfreeze an instance of ``CCLParameters``.""" - object.__setattr__(self, "_frozen", False) - - @classmethod - def get_params_dict(cls, name): - """Get a dictionary of the current parameters. - - Arguments: - name (str or :obj:`CCLParameters`): - Name or instance of the parameters to look up. - """ - pars = eval(name) if isinstance(name, str) else name - out = {} - for par in dir(pars): - if not par.startswith("_") and par not in ["this", "thisown"]: - out[par] = getattr(pars, par) - return out - - -class SplineParams(CCLParameters, instance=lib.cvar.user_spline_params): - """Instances of this class hold the spline parameters.""" - - -class GSLParams(CCLParameters, instance=lib.cvar.user_gsl_params): - """Instances of this class hold the gsl parameters.""" - - -class PhysicalConstants(CCLParameters, instance=lib.cvar.constants, - freeze=True): - """Instances of this class hold the physical constants.""" - - -spline_params = SplineParams() -gsl_params = GSLParams() -physical_constants = PhysicalConstants() diff --git a/pyccl/pk2d.py b/pyccl/pk2d.py index 9c46547a2..811cf03df 100644 --- a/pyccl/pk2d.py +++ b/pyccl/pk2d.py @@ -85,7 +85,7 @@ class Pk2D(CCLObject): empty : bool If ``True``, just create an empty object, to be filled out later. """ - from ._repr import _build_string_Pk2D as __repr__ + from .base.repr_ import build_string_Pk2D as __repr__ def __init__(self, pkfunc=None, a_arr=None, lk_arr=None, pk_arr=None, is_logp=True, extrap_order_lok=1, extrap_order_hik=2, @@ -130,17 +130,37 @@ def __init__(self, pkfunc=None, a_arr=None, lk_arr=None, pk_arr=None, int(is_logp), status) check(status, cosmo=cosmo) + def __eq__(self, other): + # Check the object class. + if self.__class__ is not other.__class__: + return False + # If the objects contain no data, return early. + if not (self or other): + return True + # Check extrapolation orders. + if not (self.extrap_order_lok == other.extrap_order_lok + and self.extrap_order_hik == other.extrap_order_hik): + return False + # Check the individual splines. + a1, lk1, pk1 = self.get_spline_arrays() + a2, lk2, pk2 = other.get_spline_arrays() + return ((a1 == a2).all() and (lk1 == lk2).all() + and np.allclose(pk1, pk2, atol=0, rtol=1e-12)) + + def __hash__(self): + return hash(repr(self)) + @property def has_psp(self): return 'psp' in vars(self) @property def extrap_order_lok(self): - return self.psp.extrap_order_lok + return self.psp.extrap_order_lok if self else None @property def extrap_order_hik(self): - return self.psp.extrap_order_hik + return self.psp.extrap_order_hik if self else None @classmethod def from_model(cls, cosmo, model): @@ -279,7 +299,7 @@ def eval(self, k, a, cosmo=None, *, derivative=False): # Catch scale factor extrapolation bounds error. if status == lib.CCL_ERROR_SPLINE_EV: - raise TypeError( + raise ValueError( "Pk2D evaluation scale factor is outside of the " "interpolation range. To extrapolate, pass a Cosmology.") check(status, cosmo) @@ -527,3 +547,16 @@ def parse_pk2d(cosmo, p_of_k_a, is_linear=False): pk = cosmo.get_nonlin_power(name) psp = pk.psp return psp + + +def parse_pk(cosmo, p_of_k_a=None): + """Helper to retrieve the power spectrum in the halo model.""" + if isinstance(p_of_k_a, Pk2D): + return p_of_k_a + elif p_of_k_a is None or p_of_k_a == "linear": + cosmo.compute_linear_power() + return cosmo.get_linear_power() + elif p_of_k_a == "nonlinear": + cosmo.compute_nonlin_power() + return cosmo.get_nonlin_power() + raise TypeError("p_of_k_a must [None|'linear'|'nonlinear'] or Pk2D.") diff --git a/pyccl/power.py b/pyccl/power.py index 610717a26..1b00d9206 100644 --- a/pyccl/power.py +++ b/pyccl/power.py @@ -1,10 +1,17 @@ from . import ccllib as lib -import numpy as np -from .core import check +from .pyutils import check from .pk2d import parse_pk2d +from .base import warn_api +import numpy as np + + +__all__ = ("linear_power", "nonlin_power", "linear_matter_power", + "nonlin_matter_power", "sigmaM", "sigmaR", "sigmaV", "sigma8", + "kNL",) -def linear_power(cosmo, k, a, p_of_k_a='delta_matter:delta_matter'): +@warn_api +def linear_power(cosmo, k, a, *, p_of_k_a='delta_matter:delta_matter'): """The linear power spectrum. Args: @@ -24,7 +31,8 @@ def linear_power(cosmo, k, a, p_of_k_a='delta_matter:delta_matter'): return cosmo._pk_lin[p_of_k_a].eval(k, a, cosmo) -def nonlin_power(cosmo, k, a, p_of_k_a='delta_matter:delta_matter'): +@warn_api +def nonlin_power(cosmo, k, a, *, p_of_k_a='delta_matter:delta_matter'): """The non-linear power spectrum. Args: @@ -56,8 +64,7 @@ def linear_matter_power(cosmo, k, a): float or array_like: Linear matter power spectrum; Mpc^3. """ cosmo.compute_linear_power() - return cosmo._pk_lin['delta_matter:delta_matter'].eval(k, a, - cosmo) + return cosmo._pk_lin['delta_matter:delta_matter'].eval(k, a, cosmo) def nonlin_matter_power(cosmo, k, a): @@ -72,8 +79,7 @@ def nonlin_matter_power(cosmo, k, a): float or array_like: Nonlinear matter power spectrum; Mpc^3. """ cosmo.compute_nonlin_power() - return cosmo._pk_nl['delta_matter:delta_matter'].eval(k, a, - cosmo) + return cosmo._pk_nl['delta_matter:delta_matter'].eval(k, a, cosmo) def sigmaM(cosmo, M, a): @@ -101,7 +107,8 @@ def sigmaM(cosmo, M, a): return sigM -def sigmaR(cosmo, R, a=1., p_of_k_a=None): +@warn_api +def sigmaR(cosmo, R, a=1., *, p_of_k_a=None): """RMS variance in a top-hat sphere of radius R in Mpc. Args: @@ -129,7 +136,8 @@ def sigmaR(cosmo, R, a=1., p_of_k_a=None): return sR -def sigmaV(cosmo, R, a=1., p_of_k_a=None): +@warn_api +def sigmaV(cosmo, R, a=1., *, p_of_k_a=None): """RMS variance in the displacement field in a top-hat sphere of radius R. The linear displacement field is the gradient of the linear density field. @@ -158,7 +166,8 @@ def sigmaV(cosmo, R, a=1., p_of_k_a=None): return sV -def sigma8(cosmo, p_of_k_a=None): +@warn_api +def sigma8(cosmo, *, p_of_k_a=None): """RMS variance in a top-hat sphere of radius 8 Mpc/h. .. note:: 8 Mpc/h is rescaled based on the chosen value of the Hubble @@ -184,7 +193,8 @@ def sigma8(cosmo, p_of_k_a=None): return s8 -def kNL(cosmo, a, p_of_k_a=None): +@warn_api +def kNL(cosmo, a, *, p_of_k_a=None): """Scale for the non-linear cut. .. note:: k_NL is calculated based on Lagrangian perturbation theory as the diff --git a/pyccl/pyutils.py b/pyccl/pyutils.py index 58e3e2459..d2afdd017 100644 --- a/pyccl/pyutils.py +++ b/pyccl/pyutils.py @@ -2,15 +2,10 @@ well as wrappers to automatically vectorize functions.""" from . import ccllib as lib from ._types import error_types -from .parameters import spline_params -from .errors import CCLError, CCLWarning -import functools -import warnings +from .base.parameters import spline_params +from .errors import CCLError import numpy as np -try: - from collections.abc import Iterable -except ImportError: # pragma: no cover (for py2.7) - from collections import Iterable +from collections.abc import Iterable NoneArr = np.array([]) @@ -354,65 +349,43 @@ def _vectorize_fn6(fn, fn_vec, cosmo, x1, x2, returns_status=True): return f -def get_pk_spline_nk(cosmo=None): - """Get the number of sampling points in the wavenumber dimension. +def loglin_spacing(logstart, xmin, xmax, num_log, num_lin): + """Create an array spaced first logarithmically, then linearly. - Arguments: - cosmo (``~pyccl.ccllib.cosmology`` via SWIG, optional): - Input cosmology. - """ - if cosmo is not None: - return lib.get_pk_spline_nk(cosmo.cosmo) - ndecades = np.log10(spline_params.K_MAX / spline_params.K_MIN) - return int(np.ceil(ndecades*spline_params.N_K)) + .. note:: + The number of logarithmically spaced points used is ``num_log - 1`` + because the first point of the linearly spaced points is the same as + the end point of the logarithmically spaced points. -def get_pk_spline_na(cosmo=None): - """Get the number of sampling points in the scale factor dimension. + .. code-block:: text - Arguments: - cosmo (``~pyccl.ccllib.cosmology`` via SWIG, optional): - Input cosmology. + |=== num_log ==| |============== num_lin ================| + --*-*--*---*-----*---*---*---*---*---*---*---*---*---*---*---*--> (axis) + ^ ^ ^ + logstart xmin xmax """ - if cosmo is not None: - return lib.get_pk_spline_na(cosmo.cosmo) - return spline_params.A_SPLINE_NA_PK + spline_params.A_SPLINE_NLOG_PK - 1 + log = np.geomspace(logstart, xmin, num_log-1, endpoint=False) + lin = np.linspace(xmin, xmax, num_lin) + return np.concatenate((log, lin)) -def get_pk_spline_lk(cosmo=None): - """Get a log(k)-array with sampling rate defined by ``ccl.spline_params`` - or by the spline parameters of the input ``cosmo``. - - Arguments: - cosmo (``~pyccl.ccllib.cosmology`` via SWIG, optional): - Input cosmology. - """ - nk = get_pk_spline_nk(cosmo=cosmo) +def get_pk_spline_a(cosmo=None, spline_params=spline_params): + """Get a sampling a-array. Used for P(k) splines.""" if cosmo is not None: - lk_arr, status = lib.get_pk_spline_lk(cosmo.cosmo, nk, 0) - check(status, cosmo) - return lk_arr - lk_arr, status = lib.get_pk_spline_lk_from_params(spline_params, nk, 0) - check(status) - return lk_arr + spline_params = cosmo._spline_params + s = spline_params + return loglin_spacing(s.A_SPLINE_MINLOG_PK, s.A_SPLINE_MIN_PK, + s.A_SPLINE_MAX, s.A_SPLINE_NLOG_PK, s.A_SPLINE_NA_PK) -def get_pk_spline_a(cosmo=None): - """Get an a-array with sampling rate defined by ``ccl.spline_params`` - or by the spline parameters of the input ``cosmo``. - - Arguments: - cosmo (``~pyccl.ccllib.cosmology`` via SWIG, optional): - Input cosmology. - """ - na = get_pk_spline_na(cosmo=cosmo) +def get_pk_spline_lk(cosmo=None, spline_params=spline_params): + """Get a sampling log(k)-array. Used for P(k) splines.""" if cosmo is not None: - a_arr, status = lib.get_pk_spline_a(cosmo.cosmo, na, 0) - check(status, cosmo) - return a_arr - a_arr, status = lib.get_pk_spline_a_from_params(spline_params, na, 0) - check(status) - return a_arr + spline_params = cosmo._spline_params + s = spline_params + nk = int(np.ceil(np.log10(s.K_MAX/s.K_MIN)*s.N_K)) + return np.linspace(np.log(s.K_MIN), np.log(s.K_MAX), nk) def resample_array(x_in, y_in, x_out, @@ -457,24 +430,6 @@ def resample_array(x_in, y_in, x_out, return y_out -def deprecated(new_function=None): - """This is a decorator which can be used to mark functions - as deprecated. It will result in a warning being emitted - when the function is used. If there is a replacement function, - pass it as `new_function`. - """ - def _depr_decorator(func): - @functools.wraps(func) - def new_func(*args, **kwargs): - s = "The function {} is deprecated.".format(func.__name__) - if new_function: - s += " Use {} instead.".format(new_function.__name__) - warnings.warn(s, CCLWarning) - return func(*args, **kwargs) - return new_func - return _depr_decorator - - def _fftlog_transform(rs, frs, dim, mu, power_law_index): if np.ndim(rs) != 1: diff --git a/pyccl/tests/conftest.py b/pyccl/tests/conftest.py index b1ca800ec..33b6d7aeb 100644 --- a/pyccl/tests/conftest.py +++ b/pyccl/tests/conftest.py @@ -1,7 +1,7 @@ """Config file for pytest. Allow caching for faster test completion. `$ pytest tests/ --use-cache` """ -from pyccl import CCLHalosObject +from pyccl import CCLAutoRepr from .test_cclobject import all_subclasses, init_decorator @@ -16,8 +16,8 @@ def pytest_generate_tests(metafunc): # For testing, we want to make sure that all instances of the subclasses -# of `CCLHalosObject` contain all attributes listed in `__repr_attrs__`. +# of `CCLAutoRepr` contain all attributes listed in `__repr_attrs__`. # We run some things post-init for these subclasses, which are triggered # during smoke tests. -for sub in list(all_subclasses(CCLHalosObject)): +for sub in list(all_subclasses(CCLAutoRepr)): sub.__init__ = init_decorator(sub.__init__) diff --git a/pyccl/tests/test__caching.py b/pyccl/tests/test__caching.py index d3ba50159..8ba542f35 100644 --- a/pyccl/tests/test__caching.py +++ b/pyccl/tests/test__caching.py @@ -89,7 +89,7 @@ def test_caching_lru(): # create new and discard the least recently used cosmo_create_and_compute_linpow(0.43) t2 = timeit_(sigma8=s8_arr[2]) # retrieved - assert np.abs(np.log10(t2/t1)) < 1.0 + assert np.abs(np.log10(t2/t1)) < 1.5 def test_caching_lfu(): @@ -105,7 +105,7 @@ def test_caching_lfu(): # create new and discard the least frequently used cosmo_create_and_compute_linpow(0.44) t2 = timeit_(sigma8=0.43) # cached again - assert t2/t1 > SPEEDUP + assert t2/t1 > SPEEDUP/10 def test_cache_info(): @@ -147,5 +147,13 @@ def func2(): ccl.Caching.policy = "my_policy" +def test_caching_parentheses(): + """Verify that ``cache`` can be used with or without parentheses.""" + func1, func2 = [lambda: None for _ in range(2)] + ccl.cache(func1) + ccl.cache(maxsize=10, policy="lru")(func2) + assert all([hasattr(func, "cache_info") for func in [func1, func2]]) + + # Revert to defaults. ccl.Caching._enabled = DEFAULT_CACHING_STATUS diff --git a/pyccl/tests/test_background.py b/pyccl/tests/test_background.py index 75dd9fad8..b477e7b59 100644 --- a/pyccl/tests/test_background.py +++ b/pyccl/tests/test_background.py @@ -85,6 +85,29 @@ def test_background_a_interface(a, func): assert np.allclose(val1, val2) +def test_background_age_of_universe(): + # Check that the we get an age close to the one given in Planck18. + Ombh2, Omch2, h, n_s, sigma8 = 0.02234, 0.11907, 0.6766, 0.9671, 0.8083 + Omega_c, Omega_b = Omch2 / h**2, Ombh2 / h**2 + cosmoP18 = ccl.Cosmology(Omega_c=Omega_c, Omega_b=Omega_b, h=h, + n_s=n_s, sigma8=sigma8) + T_P18 = 13.796 + T_CCL = cosmoP18.age_of_universe(1) + assert np.allclose(T_CCL, T_P18, atol=0, rtol=1.5e-3) + + +@pytest.mark.parametrize("Omega_k", [0, -0.05, +0.05]) +def test_background_comoving_volume(Omega_k): + # Check that integrating the comoving volume element gives comoving volume. + cosmo = ccl.CosmologyVanillaLCDM(Omega_k=Omega_k) + from scipy.integrate import quad + a_min = 0.4 + V = cosmo.comoving_volume(a=a_min, solid_angle=1.0) + vdv = quad(cosmo.comoving_volume_element, a=a_min, b=1.0)[0] + rtol = 1e-7 if Omega_k == 0 else 1e-2 + assert np.allclose(vdv, V, atol=0, rtol=rtol) + + @pytest.mark.parametrize('a', AVALS) @pytest.mark.parametrize('kind', [ 'matter', @@ -133,14 +156,14 @@ def test_background_omega_x_raises(): 'neutrinos_massive']) @pytest.mark.parametrize('is_comoving', [True, False]) def test_background_rho_x(a, kind, is_comoving): - val = ccl.rho_x(COSMO_NU, a, kind, is_comoving) + val = ccl.rho_x(COSMO_NU, a, kind, is_comoving=is_comoving) assert np.all(np.isfinite(val)) assert np.shape(val) == np.shape(a) def test_background_rho_x_raises(): with pytest.raises(ValueError): - ccl.rho_x(COSMO, 1, 'blah', False) + ccl.rho_x(COSMO, 1, 'blah', is_comoving=False) def test_input_arrays(): diff --git a/pyccl/tests/test_baryons.py b/pyccl/tests/test_baryons.py new file mode 100644 index 000000000..3b173f37e --- /dev/null +++ b/pyccl/tests/test_baryons.py @@ -0,0 +1,43 @@ +import pytest +import numpy as np +import pyccl as ccl + + +COSMO = ccl.CosmologyVanillaLCDM() +bar = ccl.BaryonsSchneider15() + + +@pytest.mark.parametrize('k', [ + 1, + 1.0, + [0.3, 0.5, 10], + np.array([0.3, 0.5, 10])]) +def test_bcm_smoke(k): + a = 0.8 + fka = bar.boost_factor(COSMO, k, a) + assert np.all(np.isfinite(fka)) + assert np.shape(fka) == np.shape(k) + + +def test_bcm_correct_smoke(): + k_arr = np.geomspace(1E-2, 1, 10) + fka = bar.boost_factor(COSMO, k_arr, 0.5) + pk_nobar = ccl.nonlin_matter_power(COSMO, k_arr, 0.5) + pkb = bar.include_baryonic_effects( + COSMO, COSMO.get_nonlin_power()) + pk_wbar = pkb.eval(k_arr, 0.5) + assert np.all(np.fabs(pk_wbar/(pk_nobar*fka)-1) < 1E-5) + + +def test_bcm_update_params(): + bar2 = ccl.BaryonsSchneider15(log10Mc=14.1, eta_b=0.7, k_s=40.) + bar2.update_parameters(log10Mc=bar.log10Mc, + eta_b=bar.eta_b, + k_s=bar.k_s) + assert bar == bar2 + + +def test_baryons_from_name(): + bar2 = ccl.Baryons.from_name('Schneider15') + assert bar.name == bar2.name + assert bar2.name == 'Schneider15' diff --git a/pyccl/tests/test_bcm.py b/pyccl/tests/test_bcm.py index a33903d7a..ea7984a40 100644 --- a/pyccl/tests/test_bcm.py +++ b/pyccl/tests/test_bcm.py @@ -15,21 +15,19 @@ np.array([0.3, 0.5, 10])]) def test_bcm_smoke(k): a = 0.8 - fka = ccl.bcm_model_fka(COSMO, k, a) + with pytest.warns(ccl.CCLDeprecationWarning): + fka = ccl.bcm_model_fka(COSMO, k, a) assert np.all(np.isfinite(fka)) assert np.shape(fka) == np.shape(k) def test_bcm_correct_smoke(): k_arr = np.geomspace(1E-2, 1, 10) - fka = ccl.bcm_model_fka(COSMO, k_arr, 0.5) + with pytest.warns(ccl.CCLDeprecationWarning): + fka = ccl.bcm_model_fka(COSMO, k_arr, 0.5) pk_nobar = ccl.nonlin_matter_power(COSMO, k_arr, 0.5) - ccl.bcm_correct_pk2d(COSMO, - COSMO._pk_nl['delta_matter:delta_matter']) + with pytest.warns(ccl.CCLDeprecationWarning): + ccl.bcm_correct_pk2d( + COSMO, COSMO._pk_nl['delta_matter:delta_matter']) pk_wbar = ccl.nonlin_matter_power(COSMO, k_arr, 0.5) assert np.all(np.fabs(pk_wbar/(pk_nobar*fka)-1) < 1E-5) - - -def test_bcm_correct_raises(): - with pytest.raises(TypeError): - ccl.bcm_correct_pk2d(COSMO, None) diff --git a/pyccl/tests/test_cclobject.py b/pyccl/tests/test_cclobject.py index cd61aeaf9..ec7319523 100644 --- a/pyccl/tests/test_cclobject.py +++ b/pyccl/tests/test_cclobject.py @@ -1,105 +1,43 @@ import pytest -import numpy as np import pyccl as ccl import functools def all_subclasses(cls): """Get all subclasses of ``cls``. NOTE: Used in ``conftest.py``.""" - return set(cls.__subclasses__()).union([s for c in cls.__subclasses__() - for s in all_subclasses(c)]) + return set(cls.__subclasses__()).union( + [s for c in cls.__subclasses__() for s in all_subclasses(c)]) def test_fancy_repr(): # Test fancy-repr controls. - cosmo1 = ccl.CosmologyVanillaLCDM() - cosmo2 = ccl.CosmologyVanillaLCDM() + cosmo = ccl.CosmologyVanillaLCDM() - ccl.CCLObject._fancy_repr.disable() - assert repr(cosmo1) == object.__repr__(cosmo1) - assert cosmo1 != cosmo2 + ccl.FancyRepr.disable() + assert repr(cosmo) == object.__repr__(cosmo) - ccl.CCLObject._fancy_repr.enable() - assert repr(cosmo1) != object.__repr__(cosmo1) - assert cosmo1 == cosmo2 + ccl.FancyRepr.enable() + assert repr(cosmo) != object.__repr__(cosmo) with pytest.raises(AttributeError): - cosmo1._fancy_repr.disable() + cosmo._fancy_repr.disable() with pytest.raises(AttributeError): ccl.Cosmology._fancy_repr.disable() - with pytest.raises(NotImplementedError): - ccl.base.FancyRepr() - - -def test_CCLObject(): - # Test eq --> repr <-- hash for all kinds of CCL objects. - - # 1.1. Using a complicated Cosmology object. - extras = {"camb": {"halofit_version": "mead2020", "HMCode_logT_AGN": 7.8}} - kwargs = {"transfer_function": "bbks", - "matter_power_spectrum": "emu", - "z_mg": np.ones(10), - "df_mg": np.ones(10), - "extra_parameters": extras} - COSMO1 = ccl.CosmologyVanillaLCDM(**kwargs) - COSMO2 = ccl.CosmologyVanillaLCDM(**kwargs) - assert COSMO1 == COSMO2 - kwargs["df_mg"] *= 2 - COSMO2 = ccl.CosmologyVanillaLCDM(**kwargs) - assert COSMO1 != COSMO2 - - # 2. Using a Pk2D object. - cosmo = ccl.CosmologyVanillaLCDM(transfer_function="bbks") - cosmo.compute_linear_power() - PK1 = cosmo.get_linear_power() - PK2 = ccl.Pk2D.pk_from_model(cosmo, "bbks") - assert PK1 == PK2 - assert ccl.Pk2D(empty=True) == ccl.Pk2D(empty=True) - assert 2*PK1 != PK2 - - # 3.1. Using a factorizable Tk3D object. - a_arr, lk_arr, pk_arr = PK1.get_spline_arrays() - TK1 = ccl.Tk3D(a_arr=a_arr, lk_arr=lk_arr, - pk1_arr=pk_arr, pk2_arr=pk_arr, is_logt=False) - TK2 = ccl.Tk3D(a_arr=a_arr, lk_arr=lk_arr, - pk1_arr=pk_arr, pk2_arr=pk_arr, is_logt=False) - TK3 = ccl.Tk3D(a_arr=a_arr, lk_arr=lk_arr, - pk1_arr=2*pk_arr, pk2_arr=2*pk_arr, is_logt=False) - assert TK1 == TK2 - assert TK1 != TK3 - - # 3.2. Using a non-factorizable Tk3D object. - a_arr_2 = np.arange(0.5, 0.9, 0.1) - lk_arr_2 = np.linspace(-2, 1, 8) - TK1 = ccl.Tk3D( - a_arr=a_arr_2, lk_arr=lk_arr_2, - tkk_arr=np.ones((a_arr_2.size, lk_arr_2.size, lk_arr_2.size))) - TK2 = ccl.Tk3D( - a_arr=a_arr_2, lk_arr=lk_arr_2, - tkk_arr=np.ones((a_arr_2.size, lk_arr_2.size, lk_arr_2.size))) - assert TK1 == TK2 - - # 4. Using a CosmologyCalculator. - pk_linear = {"a": a_arr, - "k": np.exp(lk_arr), - "delta_matter:delta_matter": pk_arr} - COSMO1 = ccl.CosmologyCalculator( - Omega_c=0.25, Omega_b=0.05, h=0.67, n_s=0.96, sigma8=0.81, - pk_linear=pk_linear, pk_nonlin=pk_linear) - COSMO2 = ccl.CosmologyCalculator( - Omega_c=0.25, Omega_b=0.05, h=0.67, n_s=0.96, sigma8=0.81, - pk_linear=pk_linear, pk_nonlin=pk_linear) - assert COSMO1 == COSMO2 - - # 5. Using a Tracer object. - TR1 = ccl.CMBLensingTracer(cosmo, z_source=1101) - TR2 = ccl.CMBLensingTracer(cosmo, z_source=1101) - assert TR1 == TR2 - - -def test_CCLHalosObject(): + +def check_eq_repr_hash(self, other, *, equal=True): + # Helper to ensure `__eq__`, `__repr__`, `__hash__` are consistent. + if equal: + return (self == other + and repr(self) == repr(other) + and hash(self) == hash(other)) + return (self != other + and repr(self) != repr(other) + and hash(self) != hash(other)) + + +def test_CCLAutoRepr(): # Test eq --> repr <-- hash for all kinds of CCL halo objects. # 1. Build a halo model calculator using the default parametrizations. @@ -137,7 +75,9 @@ def test_CCLHalosObject(): # assert PCOV1 == PCOV2 -def test_CCLObject_immutable(): +def test_CCLObject_immutability(): + # These tests check the behavior of immutable objects, i.e. instances + # of classes where `Funlock` or `unlock_instance` is not used. # test `CCLObject` lock obj = ccl.CCLObject() obj._object_lock.unlock() @@ -147,46 +87,44 @@ def test_CCLObject_immutable(): # `update_parameters` not implemented. cosmo = ccl.CosmologyVanillaLCDM() - with pytest.raises(AttributeError): - cosmo.my_attr = "hello_world" + # with pytest.raises(AttributeError): # TODO: Uncomment for CCLv3. + # cosmo.my_attr = "hello_world" with pytest.raises(NotImplementedError): cosmo.update_parameters(A_SPLINE_NA=120) # `update_parameters` implemented. prof = ccl.halos.HaloProfilePressureGNFW(mass_bias=0.5) - with pytest.raises(AttributeError): - prof.mass_bias = 0.7 + # with pytest.raises(AttributeError): # TODO: Uncomment for CCLv3. + # prof.mass_bias = 0.7 assert prof.mass_bias == 0.5 prof.update_parameters(mass_bias=0.7) assert prof.mass_bias == 0.7 - # TODO: uncomment once __eq__ methods are implemented. - # Check that the hash repr is deleted as required. - # prof1 = ccl.halos.HaloProfilePressureGNFW(mass_bias=0.5) - # prof2 = ccl.halos.HaloProfilePressureGNFW(mass_bias=0.5) - # assert prof1 == prof2 # repr is cached - # prof2.update_parameters(mass_bias=0.7) # cached repr is deleted - # assert prof1 != prof2 # repr is cached again - def test_CCLObject_default_behavior(): # Test that if `__repr__` is not defined the fall back is safe. MyType = type("MyType", (ccl.CCLObject,), {"test": 0}) instances = [MyType() for _ in range(2)] + assert check_eq_repr_hash(*instances, equal=False) + + # Test that all subclasses of ``CCLAutoRepr`` use Python's default + # ``repr`` if no ``__repr_attrs__`` has been defined. + instances = [ccl.CCLAutoRepr() for _ in range(2)] + assert check_eq_repr_hash(*instances, equal=False) + + MyType = type("MyType", (ccl.CCLAutoRepr,), {"test": 0}) + instances = [MyType() for _ in range(2)] assert instances[0] != instances[1] - assert hash(instances[0]) != hash(instances[1]) -def test_HaloProfile_abstractmethods(): - # Test that `HaloProfile` and its subclasses can't be instantiated if - # either `_real` or `_fourier` have not been defined. - with pytest.raises(TypeError): - ccl.halos.HaloProfile() +# +==========================================================================+ +# | The following functions are used by `conftest.py` to check correct setup.| +# +==========================================================================+ def init_decorator(func): """Check that all attributes listed in ``__repr_attrs__`` are defined in - the constructor of all subclasses of ``CCLHalosObject``. + the constructor of all subclasses of ``CCLAutoRepr``. NOTE: Used in ``conftest.py``. """ @@ -233,3 +171,7 @@ def func2(item, pk, a0=0, *, a1=None, a2): with pytest.raises(TypeError): func2() + + # 3. Doesn't do anything if instance is not CCLObject. + with ccl.UnlockInstance(True, mutate=False): + pass diff --git a/pyccl/tests/test_cls.py b/pyccl/tests/test_cells.py similarity index 75% rename from pyccl/tests/test_cls.py rename to pyccl/tests/test_cells.py index 72eadc291..e90e16df8 100644 --- a/pyccl/tests/test_cls.py +++ b/pyccl/tests/test_cells.py @@ -10,22 +10,26 @@ PKA = ccl.Pk2D(lambda k, a: np.log(a/k), cosmo=COSMO) ZZ = np.linspace(0., 1., 200) NN = np.exp(-((ZZ-0.5)/0.1)**2) -LENS = ccl.WeakLensingTracer(COSMO, (ZZ, NN)) +LENS = ccl.WeakLensingTracer(COSMO, dndz=(ZZ, NN)) + +with pytest.warns(ccl.CCLDeprecationWarning): + ccl.cls @pytest.mark.parametrize('p_of_k_a', [None, PKA]) -def test_cls_smoke(p_of_k_a): +def test_cells_smoke(p_of_k_a): # make a set of tracers to test with z = np.linspace(0., 1., 200) n = np.exp(-((z-0.5)/0.1)**2) b = np.sqrt(1. + z) - lens1 = ccl.WeakLensingTracer(COSMO, (z, n)) + lens1 = ccl.WeakLensingTracer(COSMO, dndz=(z, n)) lens2 = ccl.WeakLensingTracer(COSMO, dndz=(z, n), ia_bias=(z, n)) - nc1 = ccl.NumberCountsTracer(COSMO, False, dndz=(z, n), bias=(z, b)) - nc2 = ccl.NumberCountsTracer(COSMO, True, dndz=(z, n), bias=(z, b)) + nc1 = ccl.NumberCountsTracer(COSMO, has_rsd=False, dndz=(z, n), + bias=(z, b)) + nc2 = ccl.NumberCountsTracer(COSMO, has_rsd=True, dndz=(z, n), bias=(z, b)) nc3 = ccl.NumberCountsTracer( - COSMO, True, dndz=(z, n), bias=(z, b), mag_bias=(z, b)) - cmbl = ccl.CMBLensingTracer(COSMO, 1100.) + COSMO, has_rsd=True, dndz=(z, n), bias=(z, b), mag_bias=(z, b)) + cmbl = ccl.CMBLensingTracer(COSMO, z_source=1100.) tracers = [lens1, lens2, nc1, nc2, nc3, cmbl] ell_scl = 4. @@ -49,13 +53,14 @@ def test_cls_smoke(p_of_k_a): # Check invalid dndz with assert_raises(ValueError): - ccl.NumberCountsTracer(COSMO, False, dndz=z, bias=(z, b)) + ccl.NumberCountsTracer(COSMO, has_rsd=False, dndz=z, bias=(z, b)) with assert_raises(ValueError): - ccl.NumberCountsTracer(COSMO, False, dndz=(z, n, n), bias=(z, b)) + ccl.NumberCountsTracer(COSMO, has_rsd=False, dndz=(z, n, n), + bias=(z, b)) with assert_raises(ValueError): - ccl.NumberCountsTracer(COSMO, False, dndz=(z,), bias=(z, b)) + ccl.NumberCountsTracer(COSMO, has_rsd=False, dndz=(z,), bias=(z, b)) with assert_raises(ValueError): - ccl.NumberCountsTracer(COSMO, False, dndz=(1, 2), bias=(z, b)) + ccl.NumberCountsTracer(COSMO, has_rsd=False, dndz=(1, 2), bias=(z, b)) with assert_raises(ValueError): ccl.WeakLensingTracer(COSMO, dndz=z) with assert_raises(ValueError): @@ -67,25 +72,25 @@ def test_cls_smoke(p_of_k_a): @pytest.mark.parametrize('ells', [[3, 2, 1], [1, 3, 2], [2, 3, 1]]) -def test_cls_raise_ell_reversed(ells): +def test_cells_raise_ell_reversed(ells): with pytest.raises(ValueError): ccl.angular_cl(COSMO, LENS, LENS, ells) -def test_cls_raise_integ_method(): +def test_cells_raise_integ_method(): ells = [10, 11] with pytest.raises(ValueError): ccl.angular_cl(COSMO, LENS, LENS, ells, limber_integration_method='guad') -def test_cls_raise_weird_pk(): +def test_cells_raise_weird_pk(): ells = [10, 11] with pytest.raises(ValueError): ccl.angular_cl(COSMO, LENS, LENS, ells, p_of_k_a=lambda k, a: 10) -def test_cls_mg(): +def test_cells_mg(): # Check that if we feed the non-linear matter power spectrum from a MG # cosmology into a Calculator and get Cells using MG tracers, we get the # same results. @@ -106,8 +111,8 @@ def test_cls_mg(): # get the Cells ell = np.geomspace(2, 2000, 128) - tr_MG = ccl.CMBLensingTracer(cosmo_MG, 1100.) - tr_calc = ccl.CMBLensingTracer(cosmo_calc, 1100.) + tr_MG = ccl.CMBLensingTracer(cosmo_MG, z_source=1100.) + tr_calc = ccl.CMBLensingTracer(cosmo_calc, z_source=1100.) cl0 = ccl.angular_cl(cosmo_MG, tr_MG, tr_MG, ell) cosmo_calc.compute_growth() diff --git a/pyccl/tests/test_concentration.py b/pyccl/tests/test_concentration.py index 21ac0ffb6..77e741eae 100644 --- a/pyccl/tests/test_concentration.py +++ b/pyccl/tests/test_concentration.py @@ -1,6 +1,7 @@ import numpy as np import pytest import pyccl as ccl +from .test_cclobject import check_eq_repr_hash COSMO = ccl.Cosmology( @@ -20,11 +21,24 @@ M400 = ccl.halos.MassDef(400, 'critical') +def test_Concentration_eq_repr_hash(): + # Test eq, repr, hash for Concentration. + CM1 = ccl.halos.Concentration.from_name("Duffy08")() + CM2 = ccl.halos.ConcentrationDuffy08() + assert check_eq_repr_hash(CM1.mdef, CM2.mdef) + assert check_eq_repr_hash(CM1, CM2) + + M200m = ccl.halos.MassDef200m() + CM3 = ccl.halos.ConcentrationDuffy08(mdef=M200m) + assert check_eq_repr_hash(CM1.mdef, CM3.mdef, equal=False) + assert check_eq_repr_hash(CM1, CM3, equal=False) + + @pytest.mark.parametrize('cM_class', CONCS) def test_cM_subclasses_smoke(cM_class): cM = cM_class() for m in MS: - c = cM.get_concentration(COSMO, m, 0.9) + c = cM(COSMO, m, 0.9) assert np.all(np.isfinite(c)) assert np.shape(c) == np.shape(m) @@ -33,7 +47,7 @@ def test_cM_duffy_smoke(): md = ccl.halos.MassDef('vir', 'critical') cM = ccl.halos.ConcentrationDuffy08(md) for m in MS: - c = cM.get_concentration(COSMO, m, 0.9) + c = cM(COSMO, m, 0.9) assert np.all(np.isfinite(c)) assert np.shape(c) == np.shape(m) @@ -63,11 +77,11 @@ def test_cM_from_string(name): assert cM_class == ccl.halos.concentration_from_name(name) cM = cM_class() for m in MS: - c = cM.get_concentration(COSMO, m, 0.9) + c = cM(COSMO, m, 0.9) assert np.all(np.isfinite(c)) assert np.shape(c) == np.shape(m) def test_cM_from_string_raises(): - with pytest.raises(ValueError): + with pytest.raises(KeyError): ccl.halos.Concentration.from_name('Duffy09') diff --git a/pyccl/tests/test_correlations.py b/pyccl/tests/test_correlations.py index 8069312b3..a2b6ee8b1 100644 --- a/pyccl/tests/test_correlations.py +++ b/pyccl/tests/test_correlations.py @@ -24,7 +24,7 @@ def test_correlation_smoke(method): for tval in [t_arr, t_lst, t_scl, t_int]: corr = ccl.correlation( - COSMO, ell, cl, tval, type='NN', method=method) + COSMO, ell=ell, C_ell=cl, theta=tval, type='NN', method=method) assert np.all(np.isfinite(corr)) assert np.shape(corr) == np.shape(tval) @@ -34,7 +34,6 @@ def test_correlation_smoke(method): ['l+', 'GG+'], ['l-', 'GG-']]) def test_correlation_newtypes(typs): - from pyccl.pyutils import assert_warns z = np.linspace(0., 1., 200) n = np.ones(z.shape) lens = ccl.WeakLensingTracer(COSMO, dndz=(z, n)) @@ -43,10 +42,10 @@ def test_correlation_newtypes(typs): cl = ccl.angular_cl(COSMO, lens, lens, ell) theta = np.logspace(-2., np.log10(5.), 5) - corr_old = assert_warns( - ccl.CCLWarning, - ccl.correlation, COSMO, ell, cl, theta, corr_type=typs[0]) - corr_new = ccl.correlation(COSMO, ell, cl, theta, + with pytest.warns(ccl.CCLDeprecationWarning): + corr_old = ccl.correlation(COSMO, ell=ell, C_ell=cl, theta=theta, + corr_type=typs[0]) + corr_new = ccl.correlation(COSMO, ell=ell, C_ell=cl, theta=theta, type=typs[1]) assert np.all(corr_new == corr_old) @@ -59,7 +58,7 @@ def test_correlation_newtypes(typs): [r for r in np.logspace(1, 2, 5)]]) def test_correlation_3d_smoke(rval): a = 0.8 - corr = ccl.correlation_3d(COSMO, a, rval) + corr = ccl.correlation_3d(COSMO, a=a, r=rval) assert np.all(np.isfinite(corr)) assert np.shape(corr) == np.shape(rval) @@ -74,7 +73,7 @@ def test_correlation_3dRSD_smoke(sval): a = 0.8 mu = 0.7 beta = 0.5 - corr = ccl.correlation_3dRsd(COSMO, a, sval, mu, beta) + corr = ccl.correlation_3dRsd(COSMO, a=a, r=sval, mu=mu, beta=beta) assert np.all(np.isfinite(corr)) assert np.shape(corr) == np.shape(sval) @@ -88,7 +87,7 @@ def test_correlation_3dRSD_smoke(sval): def test_correlation_3dRSD_avgmu_smoke(sval): a = 0.8 beta = 0.5 - corr = ccl.correlation_3dRsd_avgmu(COSMO, a, sval, beta) + corr = ccl.correlation_3dRsd_avgmu(COSMO, a=a, r=sval, beta=beta) assert np.all(np.isfinite(corr)) assert np.shape(corr) == np.shape(sval) @@ -103,7 +102,8 @@ def test_correlation_3dRSD_avgmu_smoke(sval): def test_correlation_3dRSD_multipole_smoke(sval, l): a = 0.8 beta = 0.5 - corr = ccl.correlation_multipole(COSMO, a, beta, l, sval) + corr = ccl.correlation_multipole(COSMO, a=a, beta=beta, + ell=l, r=sval) assert np.all(np.isfinite(corr)) assert np.shape(corr) == np.shape(sval) @@ -118,18 +118,19 @@ def test_correlation_pi_sigma_smoke(sval): a = 0.8 beta = 0.5 pie = 1 - corr = ccl.correlation_pi_sigma(COSMO, a, beta, pie, sval) + corr = ccl.correlation_pi_sigma(COSMO, a=a, beta=beta, pi=pie, sigma=sval) assert np.all(np.isfinite(corr)) assert np.shape(corr) == np.shape(sval) def test_correlation_raises(): with pytest.raises(ValueError): - ccl.correlation(COSMO, [1], [1e-3], [1], method='blah') + ccl.correlation(COSMO, ell=[1], C_ell=[1e-3], theta=[1], method='blah') with pytest.raises(ValueError): - ccl.correlation(COSMO, [1], [1e-3], [1], type='blah') + ccl.correlation(COSMO, ell=[1], C_ell=[1e-3], theta=[1], type='blah') with pytest.raises(ValueError): - ccl.correlation(COSMO, [1], [1e-3], [1], corr_type='blah') + ccl.correlation(COSMO, ell=[1], C_ell=[1e-3], + theta=[1], corr_type='blah') def test_correlation_zero(): @@ -137,7 +138,7 @@ def test_correlation_zero(): C_ell = np.zeros(ell.size) theta = np.logspace(0, 2, 10000) t0 = default_timer() - corr = ccl.correlation(COSMO, ell, C_ell, theta) + corr = ccl.correlation(COSMO, ell=ell, C_ell=C_ell, theta=theta) t1 = default_timer() # if the short-cut has worked this should take # less than 1 second at the absolute outside @@ -152,4 +153,4 @@ def test_correlation_zero_ends(): C_ell[500] = 1.0 theta = np.logspace(0, 2, 20) with pytest.raises(ccl.CCLError): - ccl.correlation(COSMO, ell, C_ell, theta) + ccl.correlation(COSMO, ell=ell, C_ell=C_ell, theta=theta) diff --git a/pyccl/tests/test_cosmology.py b/pyccl/tests/test_cosmology.py index 2b17a8762..ad07bb1a0 100644 --- a/pyccl/tests/test_cosmology.py +++ b/pyccl/tests/test_cosmology.py @@ -4,7 +4,47 @@ import numpy as np from numpy.testing import assert_raises, assert_, assert_no_warnings import pyccl as ccl +import copy import warnings +from .test_cclobject import check_eq_repr_hash + + +def test_HMIngredients_eq_repr_hash(): + # Test eq, repr, hash for Cosmology and CosmologyCalculator. + # 1. Using a complicated Cosmology object. + extras = {"camb": {"halofit_version": "mead2020", "HMCode_logT_AGN": 7.8}} + kwargs = {"transfer_function": "bbks", + "matter_power_spectrum": "linear", + "extra_parameters": extras} + COSMO1 = ccl.CosmologyVanillaLCDM(**kwargs) + COSMO2 = ccl.CosmologyVanillaLCDM(**kwargs) + assert check_eq_repr_hash(COSMO1, COSMO2) + + # 2. Now make a copy and change it. + kwargs = copy.deepcopy(kwargs) + kwargs["extra_parameters"]["camb"]["halofit_version"] = "mead2020_feedback" + COSMO3 = ccl.CosmologyVanillaLCDM(**kwargs) + assert check_eq_repr_hash(COSMO1, COSMO3, equal=False) + + # 3. Using a CosmologyCalculator. + COSMO1.compute_linear_power() + a_arr, lk_arr, pk_arr = COSMO1.get_linear_power().get_spline_arrays() + pk_linear = {"a": a_arr, + "k": np.exp(lk_arr), + "delta_matter:delta_matter": pk_arr} + COSMO4 = ccl.CosmologyCalculator( + Omega_c=0.25, Omega_b=0.05, h=0.67, n_s=0.96, sigma8=0.81, + pk_linear=pk_linear, pk_nonlin=pk_linear) + COSMO5 = ccl.CosmologyCalculator( + Omega_c=0.25, Omega_b=0.05, h=0.67, n_s=0.96, sigma8=0.81, + pk_linear=pk_linear, pk_nonlin=pk_linear) + assert check_eq_repr_hash(COSMO4, COSMO5) + + pk_linear["delta_matter:delta_matter"] *= 2 + COSMO6 = ccl.CosmologyCalculator( + Omega_c=0.25, Omega_b=0.05, h=0.67, n_s=0.96, sigma8=0.81, + pk_linear=pk_linear, pk_nonlin=pk_linear) + assert check_eq_repr_hash(COSMO4, COSMO6, equal=False) def test_cosmo_methods(): @@ -13,10 +53,10 @@ def test_cosmo_methods(): """ from inspect import getmembers, isfunction, signature from pyccl import background, bcm, boltzmann, \ - cls, correlations, covariances, neutrinos, \ + cells, correlations, covariances, neutrinos, \ pk2d, power, tk3d, tracers, halos, nl_pt cosmo = ccl.CosmologyVanillaLCDM() - subs = [background, boltzmann, bcm, cls, correlations, covariances, + subs = [background, boltzmann, bcm, cells, correlations, covariances, neutrinos, pk2d, power, tk3d, tracers, halos, nl_pt] funcs = [getmembers(sub, isfunction) for sub in subs] funcs = [func for sub in funcs for func in sub] @@ -110,12 +150,6 @@ def test_cosmology_init(): m_nu_type='normal') -def test_cosmology_setitem(): - cosmo = ccl.CosmologyVanillaLCDM() - with pytest.raises(NotImplementedError): - cosmo['a'] = 3 - - def test_cosmology_output(): """ Check that status messages and other output from Cosmology() object works @@ -225,19 +259,19 @@ def test_pyccl_default_params(): assert ccl.gsl_params.HM_MMIN == 1e6 # does not accept extra assignment - with pytest.raises(KeyError): + with pytest.raises(AttributeError): ccl.gsl_params.test = "hello_world" - with pytest.raises(KeyError): - ccl.gsl_params["test"] = "hallo_world" + with pytest.raises(AttributeError): + ccl.gsl_params["test"] = "hello_world" # complains when we try to set A_SPLINE_MAX != 1.0 ccl.spline_params.A_SPLINE_MAX = 1.0 - with pytest.raises(RuntimeError): + with pytest.raises(ValueError): ccl.spline_params.A_SPLINE_MAX = 0.9 # complains when we try to change the spline type ccl.spline_params.A_SPLINE_TYPE = None - with pytest.raises(TypeError): + with pytest.raises(ValueError): ccl.spline_params.A_SPLINE_TYPE = "something_else" # complains when we try to change the physical constants @@ -270,7 +304,6 @@ def test_cosmology_default_params(): cosmo2 = ccl.CosmologyVanillaLCDM() v2 = cosmo2.cosmo.gsl_params.HM_MMIN assert v2 == v1*10 - assert v2 != v1 ccl.gsl_params.reload() cosmo3 = ccl.CosmologyVanillaLCDM() @@ -284,8 +317,3 @@ def test_cosmology_default_params(): def test_ccl_physical_constants_smoke(): assert ccl.physical_constants.CLIGHT == ccl.ccllib.cvar.constants.CLIGHT - - -def test_ccl_global_parameters_repr(): - ccl.spline_params.reload() - assert eval(repr(ccl.spline_params)) == ccl.spline_params._bak diff --git a/pyccl/tests/test_covs.py b/pyccl/tests/test_covariances.py similarity index 79% rename from pyccl/tests/test_covs.py rename to pyccl/tests/test_covariances.py index fd0048000..f00b0621f 100644 --- a/pyccl/tests/test_covs.py +++ b/pyccl/tests/test_covariances.py @@ -1,6 +1,5 @@ import numpy as np import pytest -from numpy.testing import assert_raises import pyccl as ccl @@ -60,7 +59,9 @@ def test_cov_NG_sanity(alpha, beta, typ): cov_p = pred_covar(ls[None, :], ls[:, None], alpha, beta) def cov_f(ll, **kwargs): - return ccl.angular_cl_cov_cNG(COSMO, tr, tr, ll, tsp, **kwargs) + return ccl.angular_cl_cov_cNG( + COSMO, tr, tr, ell=ll, t_of_kk_a=tsp, + **kwargs) elif typ == 'SSC': a_s = np.linspace(0.1, 1., 1024) s2b = np.ones_like(a_s) @@ -68,8 +69,9 @@ def cov_f(ll, **kwargs): prefac=1., chi_power=4) def cov_f(ll, **kwargs): - return ccl.angular_cl_cov_SSC(COSMO, tr, tr, ll, tsp, - sigma2_B=(a_s, s2b), **kwargs) + return ccl.angular_cl_cov_SSC( + COSMO, tr, tr, ell=ll, t_of_kk_a=tsp, + sigma2_B=(a_s, s2b), **kwargs) cov = cov_f(ls) assert np.all(np.fabs(cov/cov_p-1).flatten() < 1E-5) @@ -79,7 +81,7 @@ def cov_f(ll, **kwargs): assert np.all(np.fabs(cov/cov_p-1).flatten() < 4E-2) # Different tracers - cov = cov_f(ls, cltracer3=tr, cltracer4=tr) + cov = cov_f(ls, tracer3=tr, tracer4=tr) assert np.all(np.fabs(cov/cov_p-1).flatten() < 1E-5) # Different ells @@ -110,19 +112,19 @@ def test_cov_NG_errors(typ): tr = get_tracer() ls = np.array([2., 20., 200.]) - assert_raises(ValueError, cov_f, - COSMO, tr, tr, ls, tsp, - integration_method='cag_cuad') + with pytest.raises(ValueError): + cov_f(COSMO, tr, tr, ell=ls, t_of_kk_a=tsp, + integration_method='cag_cuad') - assert_raises(ValueError, cov_f, - COSMO, tr, tr, ls, tr) + with pytest.raises(ValueError): + cov_f(COSMO, tr, tr, ell=ls, t_of_kk_a=tr) def test_Sigma2B(): # Check projected variance calculation against # explicit integration from scipy.special import jv - from scipy.integrate import simps + from scipy.integrate import simpson fsky = 0.1 # Default sampling @@ -131,9 +133,9 @@ def test_Sigma2B(): a_use = a[idx] # Input sampling - s2b_b = ccl.sigma2_B_disc(COSMO, a=a_use, fsky=fsky) + s2b_b = ccl.sigma2_B_disc(COSMO, a_arr=a_use, fsky=fsky) # Scalar input sampling - s2b_c = np.array([ccl.sigma2_B_disc(COSMO, a=a, fsky=fsky) + s2b_c = np.array([ccl.sigma2_B_disc(COSMO, a_arr=a, fsky=fsky) for a in a_use]) # Alternative calculation @@ -148,7 +150,7 @@ def integrand(lk, a, R): return w*w*k*k*pk/(2*np.pi) lk_arr = np.log(np.geomspace(1E-4, 1E1, 1024)) - s2b_d = np.array([simps(integrand(lk_arr, a, R), x=lk_arr) + s2b_d = np.array([simpson(integrand(lk_arr, a, R), x=lk_arr) for a, R in zip(a_use, Rs)]) assert np.all(np.fabs(s2b_b/s2b_a[idx]-1) < 1E-10) @@ -162,6 +164,13 @@ def integrand(lk, a, R): mask_wl = (ell+0.5)/(2*np.pi) * (2*jv(1, kR)/(kR))**2 a_use = np.array([0.2, 0.5, 1.0]) - s2b_e = ccl.sigma2_B_from_mask(COSMO, a=a_use, mask_wl=mask_wl) - s2b_f = ccl.sigma2_B_disc(COSMO, a=a_use, fsky=fsky) + s2b_e = ccl.sigma2_B_from_mask(COSMO, a_arr=a_use, mask_wl=mask_wl) + s2b_f = ccl.sigma2_B_disc(COSMO, a_arr=a_use, fsky=fsky) assert np.all(np.fabs(s2b_e/s2b_f-1) < 1E-3) + + # Test passing a_arr=None (smoke) + a_s, s2b = ccl.sigma2_B_from_mask(COSMO, a_arr=None, + mask_wl=mask_wl) + a_cosmo = COSMO.get_pk_spline_a() + assert np.all(a_s == a_cosmo) + assert len(a_s) == len(s2b) diff --git a/pyccl/tests/test_halomodel.py b/pyccl/tests/test_halomodel.py index ed2b63c66..091d43263 100644 --- a/pyccl/tests/test_halomodel.py +++ b/pyccl/tests/test_halomodel.py @@ -25,7 +25,7 @@ def test_halomodel_power(k, kind): else: func = ccl.halomodel_matter_power - pk = assert_warns(ccl.CCLWarning, func, COSMO, k, a) + pk = assert_warns(ccl.CCLDeprecationWarning, func, COSMO, k, a) assert np.all(np.isfinite(pk)) assert np.shape(k) == np.shape(pk) @@ -39,7 +39,7 @@ def test_halo_concentration(m): a = 0.8 # Deprecated. c = assert_warns( - ccl.CCLWarning, + ccl.CCLDeprecationWarning, ccl.halo_concentration, COSMO, m, a) assert np.all(np.isfinite(c)) assert np.shape(c) == np.shape(m) @@ -90,7 +90,8 @@ def test_halomodel_choices_smoke(mf_c): # TODO: Convert this and other places to using the non-deprecated syntax # Or, since this wasn't already done, maybe this is a useful convenience # function? - p = assert_warns(ccl.CCLWarning, ccl.twohalo_matter_power, cosmo, k, a) + p = assert_warns(ccl.CCLDeprecationWarning, + ccl.twohalo_matter_power, cosmo, k, a) pb = get_pk_new(mf, c, cosmo, a, k, False, True) assert np.all(np.isfinite(p)) @@ -106,7 +107,8 @@ def test_halomodel_choices_raises(): k = np.geomspace(1E-2, 1, 10) with pytest.raises(ValueError): - assert_warns(ccl.CCLWarning, ccl.twohalo_matter_power, cosmo, k, a) + assert_warns(ccl.CCLDeprecationWarning, + ccl.twohalo_matter_power, cosmo, k, a) def test_halomodel_power_consistent(): @@ -114,13 +116,13 @@ def test_halomodel_power_consistent(): k = np.logspace(-1, 1, 10) # These are all deprecated. tot = assert_warns( - ccl.CCLWarning, + ccl.CCLDeprecationWarning, ccl.halomodel_matter_power, COSMO, k, a) one = assert_warns( - ccl.CCLWarning, + ccl.CCLDeprecationWarning, ccl.onehalo_matter_power, COSMO, k, a) two = assert_warns( - ccl.CCLWarning, + ccl.CCLDeprecationWarning, ccl.twohalo_matter_power, COSMO, k, a) assert np.allclose(one + two, tot) diff --git a/pyccl/tests/test_haloprofile.py b/pyccl/tests/test_haloprofile.py index 26003c8a8..6689215d9 100644 --- a/pyccl/tests/test_haloprofile.py +++ b/pyccl/tests/test_haloprofile.py @@ -22,7 +22,7 @@ def test_haloprofile_smoke(func, r): odelta = 200 # These are all deprecated prof = assert_warns( - ccl.CCLWarning, + ccl.CCLDeprecationWarning, getattr(ccl, func), cosmo, c, mass, odelta, a, r) assert np.all(np.isfinite(prof)) assert np.shape(prof) == np.shape(r) diff --git a/pyccl/tests/test_hbias.py b/pyccl/tests/test_hbias.py index 235dd92a1..c7f2ac2b9 100644 --- a/pyccl/tests/test_hbias.py +++ b/pyccl/tests/test_hbias.py @@ -20,7 +20,7 @@ def test_bM_subclasses_smoke(bM_class): bM = bM_class(COSMO) for m in MS: - b = bM.get_halo_bias(COSMO, m, 0.9) + b = bM(COSMO, m, 0.9) assert np.all(np.isfinite(b)) assert np.shape(b) == np.shape(m) @@ -35,7 +35,7 @@ def test_bM_mdef_raises(bM_pair): def test_bM_SO_allgood(): bM = ccl.halos.HaloBiasTinker10(COSMO, MVIR) for m in MS: - b = bM.get_halo_bias(COSMO, m, 0.9) + b = bM(COSMO, m, 0.9) assert np.all(np.isfinite(b)) assert np.shape(b) == np.shape(m) @@ -46,11 +46,11 @@ def test_bM_from_string(name): assert bM_class == ccl.halos.halo_bias_from_name(name) bM = bM_class(COSMO) for m in MS: - b = bM.get_halo_bias(COSMO, m, 0.9) + b = bM(COSMO, m, 0.9) assert np.all(np.isfinite(b)) assert np.shape(b) == np.shape(m) def test_bM_from_string_raises(): - with pytest.raises(ValueError): + with pytest.raises(KeyError): ccl.halos.HaloBias.from_name('Tinker11') diff --git a/pyccl/tests/test_hmcalculator_number_counts.py b/pyccl/tests/test_hmcalculator_number_counts.py index 1b06ca420..80f1515f9 100644 --- a/pyccl/tests/test_hmcalculator_number_counts.py +++ b/pyccl/tests/test_hmcalculator_number_counts.py @@ -119,7 +119,7 @@ def test_hmcalculator_number_counts_scipy_dblquad(): matter_power_spectrum='linear' ) mdef = ccl.halos.MassDef(200, 'matter') - hmf = ccl.halos.MassFuncTinker08(cosmo, mdef, + hmf = ccl.halos.MassFuncTinker08(cosmo, mass_def=mdef, mass_def_strict=False) hbf = ccl.halos.HaloBiasTinker10(cosmo, mass_def=mdef, mass_def_strict=False) @@ -156,10 +156,7 @@ def _func(m, a): dvdz = dh * dc**2 / ez dvda = dvdz * abs_dzda - val = hmf.get_mass_function( - cosmo, 10**m, a, mdef_other=mdef - ) - val *= sel(10**m, a) + val = hmf(cosmo, 10**m, a) * sel(10**m, a) return val[0, 0] * dvda mtot, _ = scipy.integrate.dblquad( diff --git a/pyccl/tests/test_hmfunc.py b/pyccl/tests/test_hmfunc.py index 9b739cde7..75c60196b 100644 --- a/pyccl/tests/test_hmfunc.py +++ b/pyccl/tests/test_hmfunc.py @@ -1,7 +1,7 @@ import numpy as np import pytest import pyccl as ccl - +from pyccl import physical_constants as const COSMO = ccl.Cosmology( Omega_c=0.27, Omega_b=0.045, h=0.67, sigma8=0.8, n_s=0.96, @@ -37,7 +37,7 @@ def test_sM_raises(): def test_nM_subclasses_smoke(nM_class): nM = nM_class(COSMO) for m in MS: - n = nM.get_mass_function(COSMO, m, 0.9) + n = nM(COSMO, m, 0.9) assert np.all(np.isfinite(n)) assert np.shape(n) == np.shape(m) @@ -61,7 +61,7 @@ def test_nM_mdef_bad_delta(nM_class): def test_nM_SO_allgood(nM_class): nM = nM_class(COSMO, MVIR) for m in MS: - n = nM.get_mass_function(COSMO, m, 0.9) + n = nM(COSMO, m, 0.9) assert np.all(np.isfinite(n)) assert np.shape(n) == np.shape(m) @@ -70,7 +70,7 @@ def test_nM_despali_smoke(): nM = ccl.halos.MassFuncDespali16(COSMO, ellipsoidal=True) for m in MS: - n = nM.get_mass_function(COSMO, m, 0.9) + n = nM(COSMO, m, 0.9) assert np.all(np.isfinite(n)) assert np.shape(n) == np.shape(m) @@ -80,11 +80,11 @@ def test_nM_watson_smoke(mdef): nM = ccl.halos.MassFuncWatson13(COSMO, mdef) for m in MS: - n = nM.get_mass_function(COSMO, m, 0.9) + n = nM(COSMO, m, 0.9) assert np.all(np.isfinite(n)) assert np.shape(n) == np.shape(m) for m in MS: - n = nM.get_mass_function(COSMO, m, 0.1) + n = nM(COSMO, m, 0.1) assert np.all(np.isfinite(n)) assert np.shape(n) == np.shape(m) @@ -99,7 +99,7 @@ def test_nM_bocquet_smoke(with_hydro): nM = ccl.halos.MassFuncBocquet16(COSMO, md, hydro=with_hydro) for m in MS: - n = nM.get_mass_function(COSMO, m, 0.9) + n = nM(COSMO, m, 0.9) assert np.all(np.isfinite(n)) assert np.shape(n) == np.shape(m) @@ -111,13 +111,13 @@ def test_nM_from_string(name): assert nM_class == ccl.halos.mass_function_from_name(name) nM = nM_class(COSMO) for m in MS: - n = nM.get_mass_function(COSMO, m, 0.9) + n = nM(COSMO, m, 0.9) assert np.all(np.isfinite(n)) assert np.shape(n) == np.shape(m) def test_nM_from_string_raises(): - with pytest.raises(ValueError): + with pytest.raises(KeyError): ccl.halos.MassFunc.from_name('Tinker09') @@ -133,8 +133,7 @@ def test_nM_tinker_crit(mf): mdef_m = ccl.halos.MassDef(delta_m, 'matter') nM_c = mf(COSMO, mdef_c) nM_m = mf(COSMO, mdef_m) - assert np.allclose(nM_c.get_mass_function(COSMO, 1E13, a), - nM_m.get_mass_function(COSMO, 1E13, a)) + assert np.allclose(nM_c(COSMO, 1E13, a), nM_m(COSMO, 1E13, a)) def test_nM_tinker10_norm(): @@ -148,7 +147,7 @@ def test_nM_tinker10_norm(): def integrand(lnu, z): nu = np.exp(lnu) a = 1./(1+z) - gnu = mf._get_fsigma(COSMO, 1.686/nu, a, 1) + gnu = mf._get_fsigma(COSMO, const.DELTA_C/nu, a, 1) bnu = bf._get_bsigma(COSMO, bf.dc/nu, a) return gnu*bnu @@ -158,3 +157,11 @@ def norm(z): zs = np.linspace(0, 1, 4) ns = np.array([norm(z) for z in zs]) assert np.all(np.fabs(ns-1) < 0.005) + + +def test_mass_function_mass_def_strict_always_raises(): + # Verify that when the property `_mass_def_strict_always` is set to True, + # the `mass_def_strict` check cannot be relaxed. + mdef = ccl.halos.MassDef(400, "critical") + with pytest.raises(ValueError): + ccl.halos.MassFuncBocquet16(mass_def=mdef, mass_def_strict=False) diff --git a/pyccl/tests/test_massdef.py b/pyccl/tests/test_massdef.py index 145528990..bd67da1d2 100644 --- a/pyccl/tests/test_massdef.py +++ b/pyccl/tests/test_massdef.py @@ -74,21 +74,15 @@ def test_get_radius(): assert np.shape(r) == np.shape(M) -def test_get_concentration(): +def test_concentration(): hmd = ccl.halos.MassDef200m() for M in [1E12, [1E12, 2E12], np.array([1E12, 2E12])]: - c = hmd._get_concentration(COSMO, M, 1.) + c = hmd.concentration(COSMO, M, 1.) assert np.all(np.isfinite(c)) assert np.shape(c) == np.shape(M) -def test_get_concentration_raises(): - hmd = ccl.halos.MassDef(200, 'matter') - with pytest.raises(RuntimeError): - hmd._get_concentration(COSMO, 1E12, 1.) - - def test_translate_mass(): hmd = ccl.halos.MassDef200m() hmdb = ccl.halos.MassDef200c() @@ -103,9 +97,8 @@ def test_translate_mass(): def test_translate_mass_raises(): hmd = ccl.halos.MassDef(200, 'matter') hmdb = ccl.halos.MassDef(200, 'critical') - with pytest.raises(RuntimeError): - hmd.translate_mass(COSMO, 1E12, - 1., hmdb) + with pytest.raises(AttributeError): + hmd.translate_mass(COSMO, 1E12, 1., hmdb) @pytest.mark.parametrize('scls', [ccl.halos.MassDef200m, diff --git a/pyccl/tests/test_massfunction.py b/pyccl/tests/test_massfunction.py index e99f11fd6..2b58d68ce 100644 --- a/pyccl/tests/test_massfunction.py +++ b/pyccl/tests/test_massfunction.py @@ -26,8 +26,9 @@ def test_massfunc_models_smoke(mf_type): hmf = hmf_cls(cosmo) for m in MS: # Deprecated - nm_old = assert_warns(ccl.CCLWarning, ccl.massfunc, cosmo, m, 1.) - nm_new = hmf.get_mass_function(cosmo, m, 1.) + nm_old = assert_warns(ccl.CCLDeprecationWarning, + ccl.massfunc, cosmo, m, 1.) + nm_new = hmf(cosmo, m, 1.) assert np.all(np.isfinite(nm_old)) assert np.shape(nm_old) == np.shape(m) assert np.all(np.array(nm_old) == @@ -44,8 +45,9 @@ def test_halo_bias_models_smoke(mf_type): hbf = hbf_cls(cosmo) for m in MS: # Deprecated - bm_old = assert_warns(ccl.CCLWarning, ccl.halo_bias, cosmo, m, 1.) - bm_new = hbf.get_halo_bias(cosmo, m, 1.) + bm_old = assert_warns(ccl.CCLDeprecationWarning, + ccl.halo_bias, cosmo, m, 1.) + bm_new = hbf(cosmo, m, 1.) assert np.all(np.isfinite(bm_old)) assert np.shape(bm_old) == np.shape(m) assert np.all(np.array(bm_old) == @@ -59,7 +61,7 @@ def test_halo_bias_models_smoke(mf_type): np.array([1e14, 1e15])]) def test_massfunc_smoke(m): a = 0.8 - mf = ccl.halos.MassFuncTinker10(COSMO).get_mass_function(COSMO, m, a) + mf = ccl.halos.MassFuncTinker10(COSMO)(COSMO, m, a) assert np.all(np.isfinite(mf)) assert np.shape(mf) == np.shape(m) @@ -72,7 +74,7 @@ def test_massfunc_smoke(m): def test_massfunc_m2r_smoke(m): # Deprecated # TODO: switch to mass2radius_lagrangian - r = assert_warns(ccl.CCLWarning, ccl.massfunc_m2r, COSMO, m) + r = assert_warns(ccl.CCLDeprecationWarning, ccl.massfunc_m2r, COSMO, m) assert np.all(np.isfinite(r)) assert np.shape(r) == np.shape(m) @@ -98,6 +100,6 @@ def test_halo_bias_smoke(m): a = 0.8 # Deprecated # TODO: switch to HaloBias - b = assert_warns(ccl.CCLWarning, ccl.halo_bias, COSMO, m, a) + b = assert_warns(ccl.CCLDeprecationWarning, ccl.halo_bias, COSMO, m, a) assert np.all(np.isfinite(b)) assert np.shape(b) == np.shape(m) diff --git a/pyccl/tests/test_neutrinos.py b/pyccl/tests/test_neutrinos.py index 87ad8cd54..ea5664fe8 100644 --- a/pyccl/tests/test_neutrinos.py +++ b/pyccl/tests/test_neutrinos.py @@ -12,7 +12,8 @@ [0.1, 0.8, 0.3], np.array([0.1, 0.8, 0.3])]) def test_omnuh2_smoke(a, m): - om = ccl.Omeganuh2(a, m) + with pytest.warns(ccl.CCLDeprecationWarning): + om = ccl.Omeganuh2(a, m_nu=m) assert np.all(np.isfinite(om)) assert np.shape(om) == np.shape(a) @@ -20,7 +21,7 @@ def test_omnuh2_smoke(a, m): @pytest.mark.parametrize('split', ['normal', 'inverted', 'equal', 'sum', 'single']) def test_nu_masses_smoke(split): - m = ccl.nu_masses(0.1, split) + m = ccl.nu_masses(Omega_nu_h2=0.1, mass_split=split) if split in ['sum', 'single']: assert np.ndim(m) == 0 else: @@ -30,7 +31,7 @@ def test_nu_masses_smoke(split): def test_neutrinos_raises(): with pytest.raises(ValueError): - ccl.nu_masses(0.1, 'blah') + ccl.nu_masses(Omega_nu_h2=0.1, mass_split='blah') @pytest.mark.parametrize('a', [ @@ -38,5 +39,7 @@ def test_neutrinos_raises(): 1.]) @pytest.mark.parametrize('split', ['normal', 'inverted', 'equal']) def test_nu_mass_consistency(a, split): - m = ccl.nu_masses(0.1, split) - assert np.allclose(ccl.Omeganuh2(a, m), 0.1, rtol=0, atol=1e-4) + m = ccl.nu_masses(Omega_nu_h2=0.1, mass_split=split) + with pytest.warns(ccl.CCLDeprecationWarning): + om = ccl.Omeganuh2(a, m_nu=m) + assert np.allclose(om, 0.1, rtol=0, atol=1e-4) diff --git a/pyccl/tests/test_parameters.py b/pyccl/tests/test_parameters.py index a9ad214d5..7165a4c63 100644 --- a/pyccl/tests/test_parameters.py +++ b/pyccl/tests/test_parameters.py @@ -27,14 +27,13 @@ def test_parameters_lcdm_defaults(): assert np.allclose(cosmo['A_s'], 2.1e-9) assert np.allclose(cosmo['n_s'], 0.96) assert np.isnan(cosmo['sigma8']) - assert np.isnan(cosmo['z_star']) assert np.allclose(cosmo['Neff'], 3.046) assert cosmo['N_nu_mass'] == 0 assert np.allclose(cosmo['N_nu_rel'], 3.046) assert np.allclose(cosmo['sum_nu_masses'], 0) assert np.allclose(cosmo['m_nu'], 0) assert np.allclose(cosmo['Omega_nu_mass'], 0) - assert np.allclose(cosmo['T_CMB'], ccl.physical_constants.T_CMB) + assert np.allclose(cosmo['T_CMB'], ccl.core._Defaults.T_CMB) assert np.allclose(cosmo['bcm_ks'], 55.0) assert np.allclose(cosmo['bcm_log10Mc'], np.log10(1.2e14)) @@ -87,9 +86,8 @@ def test_parameters_nu(m_nu_type): cosmo['m_nu'][2]**2 - cosmo['m_nu'][0]**2, ccl.physical_constants.DELTAM13_sq_pos, atol=1e-4, rtol=0) elif m_nu_type == 'single': + assert len(cosmo["m_nu"]) == 1 assert np.allclose(cosmo['m_nu'][0], 0.15, atol=1e-4, rtol=0) - assert np.allclose(cosmo['m_nu'][1], 0., atol=1e-4, rtol=0) - assert np.allclose(cosmo['m_nu'][2], 0., atol=1e-4, rtol=0) def test_parameters_nu_Nnurel_neg(): @@ -121,8 +119,7 @@ def test_parameters_nu_list(): assert np.allclose(cosmo['A_s'], 2.1e-9) assert np.allclose(cosmo['n_s'], 0.96) assert np.isnan(cosmo['sigma8']) - assert np.isnan(cosmo['z_star']) - assert np.allclose(cosmo['T_CMB'], ccl.physical_constants.T_CMB) + assert np.allclose(cosmo['T_CMB'], ccl.core._Defaults.T_CMB) assert np.allclose(cosmo['bcm_ks'], 55.0) assert np.allclose(cosmo['bcm_log10Mc'], np.log10(1.2e14)) @@ -172,8 +169,7 @@ def test_parameters_nu_normal(): assert np.allclose(cosmo['A_s'], 2.1e-9) assert np.allclose(cosmo['n_s'], 0.96) assert np.isnan(cosmo['sigma8']) - assert np.isnan(cosmo['z_star']) - assert np.allclose(cosmo['T_CMB'], ccl.physical_constants.T_CMB) + assert np.allclose(cosmo['T_CMB'], ccl.core._Defaults.T_CMB) assert np.allclose(cosmo['bcm_ks'], 55.0) assert np.allclose(cosmo['bcm_log10Mc'], np.log10(1.2e14)) @@ -222,8 +218,7 @@ def test_parameters_nu_inverted(): assert np.allclose(cosmo['A_s'], 2.1e-9) assert np.allclose(cosmo['n_s'], 0.96) assert np.isnan(cosmo['sigma8']) - assert np.isnan(cosmo['z_star']) - assert np.allclose(cosmo['T_CMB'], ccl.physical_constants.T_CMB) + assert np.allclose(cosmo['T_CMB'], ccl.core._Defaults.T_CMB) assert np.allclose(cosmo['bcm_ks'], 55.0) assert np.allclose(cosmo['bcm_log10Mc'], np.log10(1.2e14)) @@ -272,8 +267,7 @@ def test_parameters_nu_equal(): assert np.allclose(cosmo['A_s'], 2.1e-9) assert np.allclose(cosmo['n_s'], 0.96) assert np.isnan(cosmo['sigma8']) - assert np.isnan(cosmo['z_star']) - assert np.allclose(cosmo['T_CMB'], ccl.physical_constants.T_CMB) + assert np.allclose(cosmo['T_CMB'], ccl.core._Defaults.T_CMB) assert np.allclose(cosmo['bcm_ks'], 55.0) assert np.allclose(cosmo['bcm_log10Mc'], np.log10(1.2e14)) @@ -398,24 +392,6 @@ def test_parameters_missing(): df_mg=None) -def test_parameters_set(): - """ - Check that a Cosmology object doesn't let parameters be set. - """ - params = ccl.Cosmology( - Omega_c=0.25, Omega_b=0.05, h=0.7, A_s=2.1e-9, - n_s=0.96) - - # Check that values of sigma8 and A_s won't be misinterpreted by the C code - assert_raises(ValueError, ccl.Cosmology, Omega_c=0.25, Omega_b=0.05, - h=0.7, A_s=2e-5, n_s=0.96) - assert_raises(ValueError, ccl.Cosmology, Omega_c=0.25, Omega_b=0.05, - h=0.7, sigma8=9e-6, n_s=0.96) - - # Check that error is raised when unrecognized parameter requested - assert_raises(KeyError, lambda: params['wibble']) - - def test_parameters_mgrowth(): """ Check that valid modified growth inputs are allowed, and invalid ones are diff --git a/pyccl/tests/test_pk2d.py b/pyccl/tests/test_pk2d.py index 19f178273..de5a2be29 100644 --- a/pyccl/tests/test_pk2d.py +++ b/pyccl/tests/test_pk2d.py @@ -5,6 +5,29 @@ assert_raises, assert_almost_equal, assert_allclose) import pyccl as ccl from pyccl import CCLWarning +from .test_cclobject import check_eq_repr_hash + + +def test_Pk2D_eq_repr_hash(): + # Test eq, repr, hash for Pk2D. + cosmo = ccl.CosmologyVanillaLCDM(transfer_function="bbks") + cosmo.compute_linear_power() + PK1 = cosmo.get_linear_power() + PK2 = ccl.Pk2D.pk_from_model(cosmo, "bbks") + assert check_eq_repr_hash(PK1, PK2) + assert check_eq_repr_hash(ccl.Pk2D(empty=True), ccl.Pk2D(empty=True)) + assert check_eq_repr_hash(2*PK1, PK2, equal=False) + + # edge-case: comparing different types + assert check_eq_repr_hash(PK1, 1, equal=False) + + # edge-case: different extrapolation orders + a_arr, lk_arr, pk_arr = PK1.get_spline_arrays() + pk1 = ccl.Pk2D(a_arr=a_arr, lk_arr=lk_arr, pk_arr=pk_arr, + is_logp=False, extrap_order_lok=0) + pk2 = ccl.Pk2D(a_arr=a_arr, lk_arr=lk_arr, pk_arr=pk_arr, + is_logp=False, extrap_order_lok=1) + assert check_eq_repr_hash(pk1, pk2, equal=False) def pk1d(k): @@ -190,7 +213,7 @@ def test_pk2d_function(): assert_allclose(dphere, -1.*np.ones_like(dphere), 6) -def test_pk2d_cls(): +def test_pk2d_cells(): """ Test interplay between Pk2D and the Limber integrator """ @@ -199,7 +222,7 @@ def test_pk2d_cls(): Omega_c=0.27, Omega_b=0.045, h=0.67, A_s=1e-10, n_s=0.96) z = np.linspace(0., 1., 200) n = np.exp(-((z-0.5)/0.1)**2) - lens1 = ccl.WeakLensingTracer(cosmo, (z, n)) + lens1 = ccl.WeakLensingTracer(cosmo, dndz=(z, n)) ells = np.arange(2, 10) # Check that passing no power spectrum is fine @@ -231,24 +254,24 @@ def test_pk2d_parsing(): 'a:b': pk_arr}) z = np.linspace(0., 1., 200) n = np.exp(-((z-0.5)/0.1)**2) - lens1 = ccl.WeakLensingTracer(cosmo, (z, n)) + lens1 = ccl.WeakLensingTracer(cosmo, dndz=(z, n)) ells = np.linspace(2, 100, 10) - cls1 = ccl.angular_cl(cosmo, lens1, lens1, ells, - p_of_k_a=None) - cls2 = ccl.angular_cl(cosmo, lens1, lens1, ells, - p_of_k_a='delta_matter:delta_matter') - cls3 = ccl.angular_cl(cosmo, lens1, lens1, ells, - p_of_k_a='a:b') - cls4 = ccl.angular_cl(cosmo, lens1, lens1, ells, - p_of_k_a=psp) - assert all_finite(cls1) - assert all_finite(cls2) - assert all_finite(cls3) - assert all_finite(cls4) - assert np.all(np.fabs(cls2/cls1-1) < 1E-10) - assert np.all(np.fabs(cls3/cls1-1) < 1E-10) - assert np.all(np.fabs(cls4/cls1-1) < 1E-10) + cells1 = ccl.angular_cl(cosmo, lens1, lens1, ells, + p_of_k_a=None) + cells2 = ccl.angular_cl(cosmo, lens1, lens1, ells, + p_of_k_a='delta_matter:delta_matter') + cells3 = ccl.angular_cl(cosmo, lens1, lens1, ells, + p_of_k_a='a:b') + cells4 = ccl.angular_cl(cosmo, lens1, lens1, ells, + p_of_k_a=psp) + assert all_finite(cells1) + assert all_finite(cells2) + assert all_finite(cells3) + assert all_finite(cells4) + assert np.all(np.fabs(cells2/cells1-1) < 1E-10) + assert np.all(np.fabs(cells3/cells1-1) < 1E-10) + assert np.all(np.fabs(cells4/cells1-1) < 1E-10) # Wrong name with pytest.raises(KeyError): @@ -402,7 +425,7 @@ def test_pk2d_eval_cosmo(): amin = pk.psp.amin pk.eval(1., amin*0.99, cosmo) # doesn't fail because cosmo is provided - with pytest.raises(TypeError): + with pytest.raises(ValueError): pk.eval(1., amin*0.99) diff --git a/pyccl/tests/test_pkhm.py b/pyccl/tests/test_pkhm.py index 8549022fb..5e273b836 100644 --- a/pyccl/tests/test_pkhm.py +++ b/pyccl/tests/test_pkhm.py @@ -1,6 +1,7 @@ import numpy as np import pytest import pyccl as ccl +from .test_cclobject import check_eq_repr_hash COSMO = ccl.Cosmology( @@ -35,18 +36,6 @@ def test_prof2pt_smoke(): assert np.all(np.fabs((cv_NE - uk_NFW * uk_EIN)) < 1E-10) -def test_prof2pt_errors(): - # Wrong first profile - with pytest.raises(TypeError): - PKC.fourier_2pt(None, COSMO, KK, MM, AA, - prof2=None, mass_def=M200) - - # Wrong second profile - with pytest.raises(TypeError): - PKC.fourier_2pt(P1, COSMO, KK, MM, AA, - prof2=M200, mass_def=M200) - - def smoke_assert_pkhm_real(func): sizes = [(0, 0), (2, 0), @@ -74,6 +63,34 @@ def smoke_assert_pkhm_real(func): assert np.all(np.isfinite(p)) +def test_HMIngredients_eq_repr_hash(): + # Test eq, repr, hash for the HMCalculator and its ingredients. + # 1. Build a halo model calculator using the default parametrizations. + cosmo = ccl.CosmologyVanillaLCDM(transfer_function="bbks") + HMC = ccl.halos.HMCalculator( + cosmo, massfunc="Tinker08", hbias="Tinker10", mass_def="200m") + + # 2. Define separate default halo model ingredients. + MDEF = ccl.halos.MassDef200m() + HMF = ccl.halos.MassFuncTinker08(cosmo, mass_def=MDEF) + HBF = ccl.halos.HaloBiasTinker10(cosmo, mass_def=MDEF) + HMC2 = ccl.halos.HMCalculator( + cosmo, massfunc=HMF, hbias=HBF, mass_def=MDEF) # equal + HMC3 = ccl.halos.HMCalculator( + cosmo, massfunc="Press74", hbias="Sheth01", mass_def="fof") # unequal + + # 3. Test equivalence. + assert check_eq_repr_hash(MDEF, HMC._mdef) + assert check_eq_repr_hash(HMF, HMC._massfunc) + assert check_eq_repr_hash(HBF, HMC._hbias) + assert check_eq_repr_hash(HMC, HMC2) + + assert check_eq_repr_hash(MDEF, HMC3._mdef, equal=False) + assert check_eq_repr_hash(HMF, HMC3._massfunc, equal=False) + assert check_eq_repr_hash(HBF, HMC3._hbias, equal=False) + assert check_eq_repr_hash(HMC, HMC3, equal=False) + + @pytest.mark.parametrize('norm', [True, False]) def test_pkhm_mean_profile_smoke(norm): hmc = ccl.halos.HMCalculator(COSMO, HMF, HBF, mass_def=M200, @@ -289,33 +306,16 @@ def test_pkhm_errors(): with pytest.raises(TypeError): ccl.halos.HMCalculator(COSMO, HMF, None, mass_def=M200) - # Wrong mass_def - with pytest.raises(TypeError): - ccl.halos.HMCalculator(COSMO, HMF, HBF, mass_def=None) - hmc = ccl.halos.HMCalculator(COSMO, HMF, HBF, mass_def=M200) - # Wrong profile - with pytest.raises(TypeError): - ccl.halos.halomod_mean_profile_1pt(COSMO, hmc, KK, AA, None) - with pytest.raises(TypeError): - ccl.halos.halomod_bias_1pt(COSMO, hmc, KK, AA, None) - with pytest.raises(TypeError): - ccl.halos.halomod_power_spectrum(COSMO, hmc, KK, AA, None) - with pytest.raises(TypeError): - ccl.halos.halomod_Tk3D_SSC(COSMO, hmc, P1, - prof2=P1, prof3=P1, prof4="hello", - normprof1=True) - - # Wrong prof2 - with pytest.raises(TypeError): - ccl.halos.halomod_power_spectrum(COSMO, hmc, KK, AA, P1, - prof2=KK) - - # Wrong prof_2pt - with pytest.raises(TypeError): - ccl.halos.halomod_power_spectrum(COSMO, hmc, KK, AA, P1, - prof_2pt=KK) + # Inconsistent mass definitions + m200c = ccl.halos.MassDef.create_instance("200c") + m200m = ccl.halos.MassDef.create_instance("200m") + hmf = ccl.halos.MassFunc.create_instance("Tinker08", mass_def=m200c) + hbf = ccl.halos.HaloBias.create_instance("Tinker10", mass_def=m200m) + with pytest.raises(ValueError): + ccl.halos.HMCalculator(mass_function=hmf, halo_bias=hbf, + mass_def=m200c) # Wrong pk2d with pytest.raises(TypeError): @@ -326,24 +326,17 @@ def func(): pass # Wrong 1h/2h smoothing - with pytest.raises(TypeError): - ccl.halos.halomod_power_spectrum(COSMO, hmc, KK, AA, P1, - smooth_transition=True) with pytest.raises(ValueError): ccl.halos.halomod_power_spectrum(COSMO, hmc, KK, AA, P1, smooth_transition=func, get_1h=False) # Wrong 1h damping - with pytest.raises(TypeError): - ccl.halos.halomod_power_spectrum(COSMO, hmc, KK, AA, P1, - supress_1h=True) - with pytest.raises(ValueError): ccl.halos.halomod_power_spectrum(COSMO, hmc, KK, AA, P1, supress_1h=func, get_1h=False) -def test_calculator_from_string_smoke(): +def test_hmcalculator_from_string_smoke(): hmc1 = ccl.halos.HMCalculator( COSMO, massfunc=HMF, hbias=HBF, mass_def=M200) hmc2 = ccl.halos.HMCalculator( diff --git a/pyccl/tests/test_profiles.py b/pyccl/tests/test_profiles.py index 3728f839e..9896377e5 100644 --- a/pyccl/tests/test_profiles.py +++ b/pyccl/tests/test_profiles.py @@ -2,6 +2,7 @@ import pytest import pyccl as ccl from pyccl import UnlockInstance +from .test_cclobject import check_eq_repr_hash COSMO = ccl.Cosmology( @@ -11,7 +12,30 @@ M500c = ccl.halos.MassDef(500, 'critical') -def one_f(cosmo, M, a=1, mdef=M200): +def test_HaloProfile_eq_repr_hash(): + # Test eq, repr, hash for HaloProfile and Profile2pt. + # 1. HaloProfile + CM1 = ccl.halos.Concentration.from_name("Duffy08")() + CM2 = ccl.halos.Concentration.from_name("Duffy08")() + + P1 = ccl.halos.HaloProfileHOD(c_M_relation=CM1) + P2 = ccl.halos.HaloProfileHOD(c_M_relation=CM2) + assert check_eq_repr_hash(CM1, CM2) + assert check_eq_repr_hash(P1, P2) + + P1.update_parameters(lMmin_0=P1.lMmin_0/2) + assert check_eq_repr_hash(P1, P2, equal=False) + + # 2. Profile2pt + PCOV1 = ccl.halos.Profile2pt(r_corr=1.0) + PCOV2 = ccl.halos.Profile2pt(r_corr=1.0) + assert check_eq_repr_hash(PCOV1, PCOV2) + + PCOV2.update_parameters(r_corr=1.5) + assert check_eq_repr_hash(PCOV1, PCOV2, equal=False) + + +def one_f(cosmo, M, a=1, mass_def=M200): if np.ndim(M) == 0: return 1 else: @@ -41,14 +65,15 @@ def smoke_assert_prof_real(profile, method='_real'): m = 1E12 else: m = np.geomspace(1E10, 1E14, sm) - p = getattr(profile, method)(COSMO, r, m, 1., M200) + p = getattr(profile, method)(COSMO, r, m, 1., mass_def=M200) assert np.shape(p) == sh +# TODO: Uncomment once the profiles can be compared. # def test_profiles_equal(): # M200m = ccl.halos.MassDef200m() -# cm = ccl.halos.ConcentrationDuffy08(mdef=M200m) -# p1 = ccl.halos.HaloProfileHOD(c_M_relation=cm, lMmin_0=12.) +# cm = ccl.halos.ConcentrationDuffy08(mass_def=M200m) +# p1 = ccl.halos.HaloProfileHOD(concentration=cm, lMmin_0=12.) # # different profile types # p2 = ccl.halos.HaloProfilePressureGNFW() @@ -59,27 +84,27 @@ def smoke_assert_prof_real(profile, method='_real'): # assert p1 == p2 # # equivalent profiles -# cm2 = ccl.halos.ConcentrationDuffy08(mdef=M200m) -# p2 = ccl.halos.HaloProfileHOD(c_M_relation=cm2, lMmin_0=12.) +# cm2 = ccl.halos.ConcentrationDuffy08(mass_def=M200m) +# p2 = ccl.halos.HaloProfileHOD(concentration=cm2, lMmin_0=12.) # assert p1 == p2 # # different parameters -# p2 = ccl.halos.HaloProfileHOD(c_M_relation=cm, lMmin_0=11.) +# p2 = ccl.halos.HaloProfileHOD(concentration=cm, lMmin_0=11.) # assert p1 != p2 # # different mass-concentration # cm2 = ccl.halos.ConcentrationConstant() -# p2 = ccl.halos.HaloProfileHOD(c_M_relation=cm2, lMmin_0=12.) +# p2 = ccl.halos.HaloProfileHOD(concentration=cm2, lMmin_0=12.) # assert p1 != p2 # # different mass-concentration mass definition # M200c = ccl.halos.MassDef200c() -# cm2 = ccl.halos.ConcentrationDuffy08(mdef=M200c) -# p2 = ccl.halos.HaloProfileHOD(c_M_relation=cm2, lMmin_0=12.) +# cm2 = ccl.halos.ConcentrationDuffy08(mass_def=M200c) +# p2 = ccl.halos.HaloProfileHOD(concentration=cm2, lMmin_0=12.) # assert p1 != p2 # # different FFTLog -# p2 = ccl.halos.HaloProfileHOD(c_M_relation=cm, lMmin_0=12.) +# p2 = ccl.halos.HaloProfileHOD(concentration=cm, lMmin_0=12.) # p2.update_precision_fftlog(**{"plaw_fourier": -2.0}) # assert p1 != p2 @@ -89,26 +114,26 @@ def smoke_assert_prof_real(profile, method='_real'): ccl.halos.HaloProfileHernquist, ccl.halos.HaloProfileEinasto]) def test_empirical_smoke(prof_class): - c = ccl.halos.ConcentrationDuffy08(M200) + c = ccl.halos.ConcentrationDuffy08(mass_def=M200) with pytest.raises(TypeError): - p = prof_class(None) + p = prof_class(concentration=None) if prof_class in [ccl.halos.HaloProfileNFW, ccl.halos.HaloProfileHernquist]: with pytest.raises(ValueError): - p = prof_class(c, + p = prof_class(concentration=c, projected_analytic=True, truncated=True) with pytest.raises(ValueError): - p = prof_class(c, + p = prof_class(concentration=c, cumul2d_analytic=True, truncated=True) - p = prof_class(c) + p = prof_class(concentration=c) smoke_assert_prof_real(p, method='_fourier_analytic') smoke_assert_prof_real(p, method='_projected_analytic') smoke_assert_prof_real(p, method='_cumul2d_analytic') - p = prof_class(c) + p = prof_class(concentration=c) smoke_assert_prof_real(p, method='real') smoke_assert_prof_real(p, method='projected') smoke_assert_prof_real(p, method='fourier') @@ -116,12 +141,12 @@ def test_empirical_smoke(prof_class): def test_empirical_smoke_CIB(): with pytest.raises(TypeError): - ccl.halos.HaloProfileCIBShang12(None, nu_GHz=417) + ccl.halos.HaloProfileCIBShang12(concentration=None, nu_GHz=417) def test_cib_smoke(): - c = ccl.halos.ConcentrationDuffy08(M200) - p = ccl.halos.HaloProfileCIBShang12(c, 217) + c = ccl.halos.ConcentrationDuffy08(mass_def=M200) + p = ccl.halos.HaloProfileCIBShang12(concentration=c, nu_GHz=217) beta_old = p.beta smoke_assert_prof_real(p, method='_real') smoke_assert_prof_real(p, method='_fourier') @@ -136,22 +161,27 @@ def test_cib_smoke(): assert getattr(p, n) == 1234. +def test_cib_raises(): + with pytest.raises(TypeError): + ccl.halos.HaloProfileCIBShang12(concentration="my_conc", nu_GHz=217) + + def test_cib_2pt_raises(): - c = ccl.halos.ConcentrationDuffy08(M200) - p_cib = ccl.halos.HaloProfileCIBShang12(c, 217) + c = ccl.halos.ConcentrationDuffy08(mass_def=M200) + p_cib = ccl.halos.HaloProfileCIBShang12(concentration=c, nu_GHz=217) p_tSZ = ccl.halos.HaloProfilePressureGNFW() p2pt = ccl.halos.Profile2ptCIB() with pytest.raises(TypeError): - p2pt.fourier_2pt(p_tSZ, COSMO, 0.1, 1E13, 1., + p2pt.fourier_2pt(COSMO, 0.1, 1E13, 1., p_tSZ, mass_def=M200) with pytest.raises(TypeError): - p2pt.fourier_2pt(p_cib, COSMO, 0.1, 1E13, 1., + p2pt.fourier_2pt(COSMO, 0.1, 1E13, 1., p_cib, prof2=p_tSZ, mass_def=M200) def test_einasto_smoke(): - c = ccl.halos.ConcentrationDuffy08(M200) - p = ccl.halos.HaloProfileEinasto(c) + c = ccl.halos.ConcentrationDuffy08(mass_def=M200) + p = ccl.halos.HaloProfileEinasto(concentration=c) for M in [1E14, [1E14, 1E15]]: alpha_from_cosmo = p._get_alpha(COSMO, M, 1., M200) @@ -185,21 +215,21 @@ def test_gnfw_refourier(): p = ccl.halos.HaloProfilePressureGNFW() # Create Fourier template p._integ_interp() - p_f1 = p.fourier(COSMO, 1., 1E13, 1., M500c) + p_f1 = p.fourier(COSMO, 1., 1E13, 1., mass_def=M500c) # Check the Fourier profile gets recalculated p.update_parameters(alpha=1.32, c500=p.c500+0.1) - p_f2 = p.fourier(COSMO, 1., 1E13, 1., M500c) + p_f2 = p.fourier(COSMO, 1., 1E13, 1., mass_def=M500c) assert p_f1 != p_f2 def test_hod_smoke(): prof_class = ccl.halos.HaloProfileHOD - c = ccl.halos.ConcentrationDuffy08(M200) + c = ccl.halos.ConcentrationDuffy08(mass_def=M200) with pytest.raises(TypeError): - p = prof_class(None) + p = prof_class(concentration=None) - p = prof_class(c_M_relation=c) + p = prof_class(concentration=c) smoke_assert_prof_real(p) smoke_assert_prof_real(p, method='_usat_real') smoke_assert_prof_real(p, method='_usat_fourier') @@ -219,31 +249,31 @@ def test_hod_ns_independent(real_prof): def func(prof): return prof._real if real_prof else prof._fourier - c = ccl.halos.ConcentrationDuffy08(M200) - hmd = c.mdef - p1 = ccl.halos.HaloProfileHOD(c_M_relation=c, + c = ccl.halos.ConcentrationDuffy08(mass_def=M200) + hmd = c.mass_def + p1 = ccl.halos.HaloProfileHOD(concentration=c, lMmin_0=12., ns_independent=False) - p2 = ccl.halos.HaloProfileHOD(c_M_relation=c, + p2 = ccl.halos.HaloProfileHOD(concentration=c, lMmin_0=12., ns_independent=True) # M < Mmin - f1 = func(p1)(COSMO, 0.01, 1e10, 1., hmd) + f1 = func(p1)(COSMO, 0.01, 1e10, 1., mass_def=hmd) assert np.all(f1 == 0) - f2 = func(p2)(COSMO, 0.01, 1e10, 1., hmd) + f2 = func(p2)(COSMO, 0.01, 1e10, 1., mass_def=hmd) assert np.all(f2 > 0) # M > Mmin - f1 = func(p1)(COSMO, 0.01, 1e14, 1., hmd) - f2 = func(p2)(COSMO, 0.01, 1e14, 1., hmd) + f1 = func(p1)(COSMO, 0.01, 1e14, 1., mass_def=hmd) + f2 = func(p2)(COSMO, 0.01, 1e14, 1., mass_def=hmd) assert np.allclose(f1, f2, rtol=0) # M == Mmin - f1 = func(p1)(COSMO, 0.01, 1e12, 1., hmd) - f2 = func(p2)(COSMO, 0.01, 1e12, 1., hmd) + f1 = func(p1)(COSMO, 0.01, 1e12, 1., mass_def=hmd) + f2 = func(p2)(COSMO, 0.01, 1e12, 1., mass_def=hmd) assert np.allclose(2*f1, f2+0.5, rtol=0) if not real_prof: - f1 = p1._fourier_variance(COSMO, 0.01, 1e10, 1., hmd) - f2 = p2._fourier_variance(COSMO, 0.01, 1e10, 1., hmd) + f1 = p1._fourier_variance(COSMO, 0.01, 1e10, 1., mass_def=hmd) + f2 = p2._fourier_variance(COSMO, 0.01, 1e10, 1., mass_def=hmd) assert f2 > f1 == 0 p1.update_parameters(ns_independent=True) @@ -252,71 +282,67 @@ def func(prof): def test_hod_2pt(): pbad = ccl.halos.HaloProfilePressureGNFW() - c = ccl.halos.ConcentrationDuffy08(M200) - pgood = ccl.halos.HaloProfileHOD(c_M_relation=c, lM0_0=11.) - pgood_b = ccl.halos.HaloProfileHOD(c_M_relation=c, lM0_0=11.) + c = ccl.halos.ConcentrationDuffy08(mass_def=M200) + pgood = ccl.halos.HaloProfileHOD(concentration=c) + pgood_b = ccl.halos.HaloProfileHOD(concentration=c) p2 = ccl.halos.Profile2ptHOD() - F0 = p2.fourier_2pt(pgood, COSMO, 1., 1e13, 1., + F0 = p2.fourier_2pt(COSMO, 1., 1e13, 1., pgood, prof2=pgood, mass_def=M200) - assert np.allclose(p2.fourier_2pt(pgood, COSMO, 1., 1e13, 1., + assert np.allclose(p2.fourier_2pt(COSMO, 1., 1e13, 1., pgood, prof2=None, mass_def=M200), F0, rtol=0) with pytest.raises(TypeError): - p2.fourier_2pt(pbad, COSMO, 1., 1E13, 1., - mass_def=M200) - - with pytest.raises(TypeError): - p2.fourier_2pt(pgood, COSMO, 1., 1e13, 1., - prof2=pbad, + p2.fourier_2pt(COSMO, 1., 1E13, 1., pbad, mass_def=M200) + # TODO: bring back when proper __eq__s are implemented # doesn't raise because profiles are equivalent - p2.fourier_2pt(pgood, COSMO, 1., 1E13, 1., - prof2=pgood, mass_def=M200) + # p2.fourier_2pt(COSMO, 1., 1E13, 1., pgood, + # prof2=pgood, mass_def=M200) - # TODO: bring back when proper __eq__s are implemented - # p2.fourier_2pt(pgood, COSMO, 1., 1E13, 1., + # p2.fourier_2pt(COSMO, 1., 1E13, 1., pgood, # prof2=pgood_b, mass_def=M200) with pytest.raises(ValueError): pgood_b.update_parameters(lM0_0=10.) - p2.fourier_2pt(pgood, COSMO, 1., 1E13, 1., + p2.fourier_2pt(COSMO, 1., 1E13, 1., pgood, prof2=pgood_b, mass_def=M200) + with pytest.raises(ValueError): + p2.fourier_2pt(COSMO, 1., 1e13, 1., pgood, + prof2=pbad, mass_def=M200) + def test_2pt_rcorr_smoke(): - c = ccl.halos.ConcentrationDuffy08(M200) - p = ccl.halos.HaloProfileNFW(c_M_relation=c) - F0 = ccl.halos.Profile2pt().fourier_2pt(p, COSMO, 1., 1e13, 1., + c = ccl.halos.ConcentrationDuffy08(mass_def=M200) + p = ccl.halos.HaloProfileNFW(concentration=c) + F0 = ccl.halos.Profile2pt().fourier_2pt(COSMO, 1., 1e13, 1., p, mass_def=M200) p2pt = ccl.halos.Profile2pt(r_corr=0) - F1 = p2pt.fourier_2pt(p, COSMO, 1., 1e13, 1., mass_def=M200) + F1 = p2pt.fourier_2pt(COSMO, 1., 1e13, 1., p, mass_def=M200) assert F0 == F1 - F2 = p2pt.fourier_2pt(p, COSMO, 1., 1e13, 1., prof2=p, mass_def=M200) + F2 = p2pt.fourier_2pt(COSMO, 1., 1e13, 1., p, prof2=p, mass_def=M200) assert F1 == F2 p2pt.update_parameters(r_corr=-1.) assert p2pt.r_corr == -1. - F3 = p2pt.fourier_2pt(p, COSMO, 1., 1e13, 1., mass_def=M200) + F3 = p2pt.fourier_2pt(COSMO, 1., 1e13, 1., p, mass_def=M200) assert F3 == 0 - # Errors - with pytest.raises(TypeError): - p2pt.fourier_2pt(None, COSMO, 1., 1e13, 1., mass_def=M200) - with pytest.raises(TypeError): - p2pt.fourier_2pt(p, COSMO, 1., 1e13, 1., prof2=0, mass_def=M200) - @pytest.mark.parametrize('prof_class', [ccl.halos.HaloProfileGaussian, ccl.halos.HaloProfilePowerLaw]) def test_simple_smoke(prof_class): - def r_s(cosmo, M, a, mdef): - return mdef.get_radius(cosmo, M, a) + def r_s(cosmo, M, a, mass_def): + return mass_def.get_radius(cosmo, M, a) - p = prof_class(r_s, one_f) + if prof_class == ccl.halos.HaloProfileGaussian: + p = prof_class(r_scale=r_s, rho0=one_f) + else: + p = prof_class(r_scale=r_s, tilt=one_f) smoke_assert_prof_real(p) @@ -324,10 +350,10 @@ def test_gaussian_accuracy(): def fk(k): return np.pi**1.5 * np.exp(-k**2 / 4) - p = ccl.halos.HaloProfileGaussian(one_f, one_f) + p = ccl.halos.HaloProfileGaussian(r_scale=one_f, rho0=one_f) k_arr = np.logspace(-3, 2, 1024) - fk_arr = p.fourier(COSMO, k_arr, 1., 1., M200) + fk_arr = p.fourier(COSMO, k_arr, 1., 1., mass_def=M200) fk_arr_pred = fk(k_arr) res = np.fabs(fk_arr - fk_arr_pred) assert np.all(res < 5E-3) @@ -346,11 +372,11 @@ def s_r_t(rt): def alpha_f(cosmo, a): return alpha - p = ccl.halos.HaloProfilePowerLaw(one_f, alpha_f) - p.update_precision_fftlog(plaw_index=alpha) + p = ccl.halos.HaloProfilePowerLaw(r_scale=one_f, tilt=alpha_f) + p.update_precision_fftlog(plaw_fourier=alpha) rt_arr = np.logspace(-3, 2, 1024) - srt_arr = p.projected(COSMO, rt_arr, 1., 1., M200) + srt_arr = p.projected(COSMO, rt_arr, 1., 1., mass_def=M200) srt_arr_pred = s_r_t(rt_arr) res = np.fabs(srt_arr / srt_arr_pred - 1) assert np.all(res < 5E-3) @@ -370,11 +396,11 @@ def fk(k): def alpha_f(cosmo, a): return alpha - p = ccl.halos.HaloProfilePowerLaw(one_f, alpha_f) - p.update_precision_fftlog(plaw_index=alpha) + p = ccl.halos.HaloProfilePowerLaw(r_scale=one_f, tilt=alpha_f) + p.update_precision_fftlog(plaw_fourier=alpha) k_arr = np.logspace(-3, 2, 1024) - fk_arr = p.fourier(COSMO, k_arr, 1., 1., M200) + fk_arr = p.fourier(COSMO, k_arr, 1., 1., mass_def=M200) fk_arr_pred = fk(k_arr) res = np.fabs(fk_arr / fk_arr_pred - 1) assert np.all(res < 5E-3) @@ -395,11 +421,11 @@ def test_nfw_accuracy(use_analytic): from scipy.special import sici tol = 1E-10 if use_analytic else 5E-3 - cM = ccl.halos.ConcentrationDuffy08(M200) - p = get_nfw(real=True, fourier=use_analytic)(cM) + cM = ccl.halos.ConcentrationDuffy08(mass_def=M200) + p = get_nfw(real=True, fourier=use_analytic)(concentration=cM) M = 1E14 a = 0.5 - c = cM.get_concentration(COSMO, M, a) + c = cM(COSMO, M, a) r_Delta = M200.get_radius(COSMO, M, a) / a r_s = r_Delta / c @@ -413,7 +439,7 @@ def fk(k): return M * P1 * (P2 - P3) k_arr = np.logspace(-2, 2, 256) / r_Delta - fk_arr = p.fourier(COSMO, k_arr, M, a, M200) + fk_arr = p.fourier(COSMO, k_arr, M, a, mass_def=M200) fk_arr_pred = fk(k_arr) # Normalize to 1 at low k res = np.fabs((fk_arr - fk_arr_pred) / M) @@ -421,11 +447,11 @@ def fk(k): def test_nfw_f2r(): - cM = ccl.halos.ConcentrationDuffy08(M200) - p1 = ccl.halos.HaloProfileNFW(cM) + cM = ccl.halos.ConcentrationDuffy08(mass_def=M200) + p1 = ccl.halos.HaloProfileNFW(concentration=cM) # We force p2 to compute the real-space profile # by FFT-ing the Fourier-space one. - p2 = get_nfw(fourier=True)(cM) + p2 = get_nfw(fourier=True)(concentration=cM) p2.update_precision_fftlog(padding_hi_fftlog=1E3) M = 1E14 @@ -433,8 +459,8 @@ def test_nfw_f2r(): r_Delta = M200.get_radius(COSMO, M, a) / a r_arr = np.logspace(-2, 1, ) * r_Delta - pr_1 = p1.real(COSMO, r_arr, M, a, M200) - pr_2 = p2.real(COSMO, r_arr, M, a, M200) + pr_1 = p1.real(COSMO, r_arr, M, a, mass_def=M200) + pr_2 = p2.real(COSMO, r_arr, M, a, mass_def=M200) id_good = r_arr < r_Delta # Profile is 0 otherwise res = np.fabs(pr_2[id_good] / pr_1[id_good] - 1) @@ -443,18 +469,18 @@ def test_nfw_f2r(): @pytest.mark.parametrize('fourier_analytic', [True, False]) def test_nfw_projected_accuracy(fourier_analytic): - cM = ccl.halos.ConcentrationDuffy08(M200) + cM = ccl.halos.ConcentrationDuffy08(mass_def=M200) # Analytic projected profile - p1 = ccl.halos.HaloProfileNFW(cM, truncated=False, + p1 = ccl.halos.HaloProfileNFW(concentration=cM, truncated=False, projected_analytic=True) # FFTLog - p2 = get_nfw(fourier=fourier_analytic)(cM, truncated=False) + p2 = get_nfw(fourier=fourier_analytic)(concentration=cM, truncated=False) M = 1E14 a = 0.5 rt = np.logspace(-3, 2, 1024) - srt1 = p1.projected(COSMO, rt, M, a, M200) - srt2 = p2.projected(COSMO, rt, M, a, M200) + srt1 = p1.projected(COSMO, rt, M, a, mass_def=M200) + srt2 = p2.projected(COSMO, rt, M, a, mass_def=M200) res2 = np.fabs(srt2/srt1-1) assert np.all(res2 < 5E-3) @@ -462,33 +488,45 @@ def test_nfw_projected_accuracy(fourier_analytic): @pytest.mark.parametrize('fourier_analytic', [True, False]) def test_nfw_cumul2d_accuracy(fourier_analytic): - cM = ccl.halos.ConcentrationDuffy08(M200) + cM = ccl.halos.ConcentrationDuffy08(mass_def=M200) # Analytic cumul2d profile - p1 = ccl.halos.HaloProfileNFW(cM, truncated=False, + p1 = ccl.halos.HaloProfileNFW(concentration=cM, truncated=False, cumul2d_analytic=True) # FFTLog - p2 = get_nfw(fourier=fourier_analytic)(cM, truncated=False) + p2 = get_nfw(fourier=fourier_analytic)(concentration=cM, truncated=False) M = 1E14 a = 1.0 rt = np.logspace(-3, 2, 1024) - srt1 = p1.cumul2d(COSMO, rt, M, a, M200) - srt2 = p2.cumul2d(COSMO, rt, M, a, M200) + srt1 = p1.cumul2d(COSMO, rt, M, a, mass_def=M200) + srt2 = p2.cumul2d(COSMO, rt, M, a, mass_def=M200) res2 = np.fabs(srt2/srt1-1) assert np.all(res2 < 5E-3) +def test_upd_fftlog_raises(): + # Verify that FFTLogParams is immutable unless changed in a control manner. + prof = ccl.halos.HaloProfilePressureGNFW() + new_params = {"hello_there": 0.} + with pytest.raises(AttributeError): + prof.update_precision_fftlog(**new_params) + + with pytest.raises(AttributeError): + prof.precision_fftlog.plaw_projected = 1 + + @pytest.mark.parametrize("use_analytic", [True, False]) def test_hernquist_accuracy(use_analytic): from scipy.special import sici tol = 1E-10 if use_analytic else 5E-3 - cM = ccl.halos.ConcentrationDuffy08(M200) - p = ccl.halos.HaloProfileHernquist(cM, fourier_analytic=use_analytic) + cM = ccl.halos.ConcentrationDuffy08(mass_def=M200) + p = ccl.halos.HaloProfileHernquist(concentration=cM, + fourier_analytic=use_analytic) M = 1E14 a = 0.5 - c = cM.get_concentration(COSMO, M, a) + c = cM(COSMO, M, a) r_Delta = M200.get_radius(COSMO, M, a) / a r_s = r_Delta / c @@ -504,7 +542,7 @@ def fk(k): return M * P1 * (P2 - P3) / 2 k_arr = np.logspace(-2, 2, 256) / r_Delta - fk_arr = p.fourier(COSMO, k_arr, M, a, M200) + fk_arr = p.fourier(COSMO, k_arr, M, a, mass_def=M200) fk_arr_pred = fk(k_arr) # Normalize to 1 at low k res = np.fabs((fk_arr - fk_arr_pred) / M) @@ -512,13 +550,18 @@ def fk(k): def test_hernquist_f2r(): - cM = ccl.halos.ConcentrationDuffy08(M200) - p1 = ccl.halos.HaloProfileHernquist(cM) + cM = ccl.halos.ConcentrationDuffy08(mass_def=M200) + p1 = ccl.halos.HaloProfileHernquist(concentration=cM) # We force p2 to compute the real-space profile # by FFT-ing the Fourier-space one. - p2 = ccl.halos.HaloProfileHernquist(cM, fourier_analytic=True) + p2 = ccl.halos.HaloProfileHernquist(concentration=cM, + fourier_analytic=True) with UnlockInstance(p2): - p2._real = None + # For the fourier computation, we require that the profile does not + # have an implemented `_real` method. This is internally checked + # by verifying that the hook `__islinkedabstractmethod__` is not there. + # Here, we replace the bound method with a function that has this hook. + p2._real = ccl.halos.HaloProfile._real p2.update_precision_fftlog(padding_hi_fftlog=1E3) M = 1E14 @@ -526,8 +569,8 @@ def test_hernquist_f2r(): r_Delta = M200.get_radius(COSMO, M, a) / a r_arr = np.logspace(-2, 1, ) * r_Delta - pr_1 = p1.real(COSMO, r_arr, M, a, M200) - pr_2 = p2.real(COSMO, r_arr, M, a, M200) + pr_1 = p1.real(COSMO, r_arr, M, a, mass_def=M200) + pr_2 = p2.real(COSMO, r_arr, M, a, mass_def=M200) id_good = r_arr < r_Delta # Profile is 0 otherwise res = np.fabs(pr_2[id_good] / pr_1[id_good] - 1) @@ -536,19 +579,19 @@ def test_hernquist_f2r(): @pytest.mark.parametrize('fourier_analytic', [True, False]) def test_hernquist_projected_accuracy(fourier_analytic): - cM = ccl.halos.ConcentrationDuffy08(M200) + cM = ccl.halos.ConcentrationDuffy08(mass_def=M200) # Analytic projected profile - p1 = ccl.halos.HaloProfileHernquist(cM, truncated=False, + p1 = ccl.halos.HaloProfileHernquist(concentration=cM, truncated=False, projected_analytic=True) # FFTLog - p2 = ccl.halos.HaloProfileHernquist(cM, truncated=False, + p2 = ccl.halos.HaloProfileHernquist(concentration=cM, truncated=False, fourier_analytic=fourier_analytic) M = 1E14 a = 0.5 rt = np.logspace(-3, 2, 1024) - srt1 = p1.projected(COSMO, rt, M, a, M200) - srt2 = p2.projected(COSMO, rt, M, a, M200) + srt1 = p1.projected(COSMO, rt, M, a, mass_def=M200) + srt2 = p2.projected(COSMO, rt, M, a, mass_def=M200) res2 = np.fabs(srt2/srt1-1) assert np.all(res2 < 5E-3) @@ -556,18 +599,25 @@ def test_hernquist_projected_accuracy(fourier_analytic): @pytest.mark.parametrize('fourier_analytic', [True, False]) def test_hernquist_cumul2d_accuracy(fourier_analytic): - cM = ccl.halos.ConcentrationDuffy08(M200) + cM = ccl.halos.ConcentrationDuffy08(mass_def=M200) # Analytic cumul2d profile - p1 = ccl.halos.HaloProfileHernquist(cM, truncated=False, + p1 = ccl.halos.HaloProfileHernquist(concentration=cM, truncated=False, cumul2d_analytic=True) # FFTLog - p2 = ccl.halos.HaloProfileHernquist(cM, truncated=False, + p2 = ccl.halos.HaloProfileHernquist(concentration=cM, truncated=False, fourier_analytic=fourier_analytic) M = 1E14 a = 1.0 rt = np.logspace(-3, 2, 1024) - srt1 = p1.cumul2d(COSMO, rt, M, a, M200) - srt2 = p2.cumul2d(COSMO, rt, M, a, M200) + srt1 = p1.cumul2d(COSMO, rt, M, a, mass_def=M200) + srt2 = p2.cumul2d(COSMO, rt, M, a, mass_def=M200) res2 = np.fabs(srt2/srt1-1) assert np.all(res2 < 5E-3) + + +def test_HaloProfile_abstractmethods(): + # Test that `HaloProfile` and its subclasses can't be instantiated if + # either `_real` or `_fourier` have not been defined. + with pytest.raises(TypeError): + ccl.halos.HaloProfile() diff --git a/pyccl/tests/test_swig_interface.py b/pyccl/tests/test_swig_interface.py index 6eb8d2729..a4f3d83b1 100644 --- a/pyccl/tests/test_swig_interface.py +++ b/pyccl/tests/test_swig_interface.py @@ -108,18 +108,6 @@ def test_swig_cls(): status) -def test_swig_core(): - status = 0 - assert_raises( - CCLError, - ccllib.parameters_create_nu_vec, - 0.25, 0.05, 0.0, 3.0, -1.0, 0.0, 0.7, 2e-9, 0.95, 1, 0.0, 0.0, - 0.0, 0.0, 1.0, 1.0, 0.0, [1.0, 2.0], - [0.0, 0.3, 0.5], - [0.02, 0.01, 0.2], - status) - - def test_swig_correlation(): status = 0 assert_raises( @@ -160,7 +148,7 @@ def test_swig_neurtinos(): assert_raises( CCLError, ccllib.Omeganuh2_vec, - 3, 2.7, + 3, 2.7, 0.72, [0.0, 1.0], [0.05, 0.1, 0.2], 4, @@ -169,7 +157,7 @@ def test_swig_neurtinos(): assert_raises( CCLError, ccllib.Omeganuh2_vec, - 3, 2.7, + 3, 2.7, 0.72, [0.0, 1.0], [0.1, 0.2], 2, diff --git a/pyccl/tests/test_tk3d.py b/pyccl/tests/test_tk3d.py index 565861305..c4b1b874a 100644 --- a/pyccl/tests/test_tk3d.py +++ b/pyccl/tests/test_tk3d.py @@ -4,6 +4,60 @@ assert_, assert_raises, assert_almost_equal, assert_allclose) import pyccl as ccl +from .test_cclobject import check_eq_repr_hash + + +def test_Tk3D_eq_repr_hash(): + # Test eq, repr, hash for Tk3D. + cosmo = ccl.CosmologyVanillaLCDM(transfer_function="bbks") + cosmo.compute_linear_power() + PK1 = cosmo.get_linear_power() + + # 1. Using a factorizable Tk3D object. + a_arr, lk_arr, pk_arr = PK1.get_spline_arrays() + TK1 = ccl.Tk3D(a_arr=a_arr, lk_arr=lk_arr, + pk1_arr=pk_arr, pk2_arr=pk_arr, is_logt=False) + TK2 = ccl.Tk3D(a_arr=a_arr, lk_arr=lk_arr, + pk1_arr=pk_arr, pk2_arr=pk_arr, is_logt=False) + assert check_eq_repr_hash(TK1, TK2) + + TK3 = ccl.Tk3D(a_arr=a_arr, lk_arr=lk_arr, + pk1_arr=2*pk_arr, pk2_arr=2*pk_arr, is_logt=False) + assert check_eq_repr_hash(TK1, TK3, equal=False) + + # 2. Using a non-factorizable Tk3D object. + a_arr_2 = np.arange(0.5, 0.9, 0.1) + lk_arr_2 = np.linspace(-2, 1, 8) + TK4 = ccl.Tk3D( + a_arr=a_arr_2, lk_arr=lk_arr_2, + tkk_arr=np.ones((a_arr_2.size, lk_arr_2.size, lk_arr_2.size))) + TK5 = ccl.Tk3D( + a_arr=a_arr_2, lk_arr=lk_arr_2, + tkk_arr=np.ones((a_arr_2.size, lk_arr_2.size, lk_arr_2.size))) + assert check_eq_repr_hash(TK4, TK5) + + TK6 = ccl.Tk3D( + a_arr=a_arr_2, lk_arr=lk_arr_2, + tkk_arr=2*np.ones((a_arr_2.size, lk_arr_2.size, lk_arr_2.size))) + assert check_eq_repr_hash(TK4, TK6, equal=False) + + # edge-case: comparing different types + assert check_eq_repr_hash(TK1, 1, equal=False) + + # edge-case: empty objects + tka1, tka2 = [ccl.Tk3D.__new__(ccl.Tk3D) for _ in range(2)] + assert check_eq_repr_hash(tka1, tka2) + + # edge-case: only one Tk is factorizable (exits early) + assert check_eq_repr_hash(TK1, TK4, equal=False) + + # edge-case: different extrapolation orders + a_arr, lk_arr, pk_arr = PK1.get_spline_arrays() + t1 = ccl.Tk3D(a_arr=a_arr, lk_arr=lk_arr, pk1_arr=pk_arr, pk2_arr=pk_arr, + is_logt=False, extrap_order_lok=0) + t2 = ccl.Tk3D(a_arr=a_arr, lk_arr=lk_arr, pk1_arr=pk_arr, pk2_arr=pk_arr, + is_logt=False, extrap_order_lok=1) + assert check_eq_repr_hash(t1, t2, equal=False) def kf(k): diff --git a/pyccl/tests/test_tkk1h.py b/pyccl/tests/test_tkk1h.py index b623229f7..c2d72c19d 100644 --- a/pyccl/tests/test_tkk1h.py +++ b/pyccl/tests/test_tkk1h.py @@ -3,9 +3,8 @@ import pyccl as ccl -COSMO = ccl.Cosmology( - Omega_c=0.27, Omega_b=0.045, h=0.67, sigma8=0.8, n_s=0.96, - transfer_function='bbks', matter_power_spectrum='linear') +COSMO = ccl.CosmologyVanillaLCDM(transfer_function='bbks', + matter_power_spectrum='linear') M200 = ccl.halos.MassDef200m() HMF = ccl.halos.MassFuncTinker10(COSMO, mass_def=M200) HBF = ccl.halos.HaloBiasTinker10(COSMO, mass_def=M200) @@ -14,12 +13,9 @@ P2 = ccl.halos.HaloProfileHOD(ccl.halos.ConcentrationDuffy08(M200)) P3 = ccl.halos.HaloProfilePressureGNFW() P4 = P1 -Pneg = ccl.halos.HaloProfilePressureGNFW(P0=-1) PKC = ccl.halos.Profile2pt() PKCH = ccl.halos.Profile2ptHOD() KK = np.geomspace(1E-3, 10, 32) -MM = np.geomspace(1E11, 1E15, 16) -AA = 1.0 def smoke_assert_tkk1h_real(func): @@ -49,127 +45,67 @@ def smoke_assert_tkk1h_real(func): assert np.all(np.isfinite(p)) -@pytest.mark.parametrize('pars', - [{'p1': P1, 'p2': None, 'cv12': None, - 'p3': None, 'p4': None, 'cv34': None, - 'norm': False}, - {'p1': P1, 'p2': None, 'cv12': None, - 'p3': None, 'p4': None, 'cv34': None, - 'norm': True}, - {'p1': P1, 'p2': P2, 'cv12': None, - 'p3': None, 'p4': None, 'cv34': None, - 'norm': False}, - {'p1': P1, 'p2': None, 'cv12': None, - 'p3': P3, 'p4': None, 'cv34': None, - 'norm': False}, - {'p1': P1, 'p2': None, 'cv12': None, - 'p3': None, 'p4': P4, 'cv34': None, - 'norm': False}, - {'p1': P1, 'p2': P2, 'cv12': None, - 'p3': P3, 'p4': P4, 'cv34': None, - 'norm': False}, - {'p1': P2, 'p2': P2, 'cv12': PKCH, - 'p3': P2, 'p4': P2, 'cv34': None, - 'norm': False}, - {'p1': P1, 'p2': P2, 'cv12': PKC, - 'p3': P3, 'p4': P4, 'cv34': PKC, - 'norm': True}],) -def test_tkk1h_smoke(pars): - hmc = ccl.halos.HMCalculator(COSMO, HMF, HBF, mass_def=M200, - nlog10M=2) +@pytest.mark.parametrize( + "p1,p2,cv12,p3,p4,cv34", + [(P1, None, None, None, None, None), + (P1, None, None, None, None, None), + (P1, P2, None, None, None, None), + (P1, None, None, P3, None, None), + (P1, None, None, None, P4, None), + (P1, P2, None, P3, P4, None), + (P2, P2, PKCH, P2, P2, None), + (P1, P2, PKC, P3, P4, PKC)]) +def test_tkk1h_smoke(p1, p2, cv12, p3, p4, cv34): + hmc = ccl.halos.HMCalculator(mass_function=HMF, halo_bias=HBF, + mass_def=M200, nlog10M=2) def f(k, a): - return ccl.halos.halomod_trispectrum_1h(COSMO, hmc, k, a, - prof1=pars['p1'], - prof2=pars['p2'], - prof12_2pt=pars['cv12'], - prof3=pars['p3'], - prof4=pars['p4'], - prof34_2pt=pars['cv34'], - normprof1=pars['norm'], - normprof2=pars['norm'], - normprof3=pars['norm'], - normprof4=pars['norm']) + return ccl.halos.halomod_trispectrum_1h( + COSMO, hmc, k, a, + prof=p1, prof2=p2, prof12_2pt=cv12, + prof3=p3, prof4=p4, prof34_2pt=cv34) smoke_assert_tkk1h_real(f) def test_tkk1h_tk3d(): - hmc = ccl.halos.HMCalculator(COSMO, HMF, HBF, mass_def=M200) + hmc = ccl.halos.HMCalculator(mass_function=HMF, halo_bias=HBF, + mass_def=M200) k_arr = KK a_arr = np.array([0.1, 0.4, 0.7, 1.0]) - tkk_arr = ccl.halos.halomod_trispectrum_1h(COSMO, hmc, k_arr, a_arr, - P1, prof2=P2, - prof12_2pt=PKC, - prof3=P3, prof4=P4, - prof34_2pt=PKC, - normprof1=True, - normprof2=True, - normprof3=True, - normprof4=True) + tkk_arr = ccl.halos.halomod_trispectrum_1h( + COSMO, hmc, k_arr, a_arr, + prof=P1, prof2=P2, prof12_2pt=PKC, + prof3=P3, prof4=P4, prof34_2pt=PKC) # Input sampling - tk3d = ccl.halos.halomod_Tk3D_1h(COSMO, hmc, - P1, prof2=P2, - prof12_2pt=PKC, - prof3=P3, prof4=P4, - prof34_2pt=PKC, - normprof1=True, - normprof2=True, - normprof3=True, - normprof4=True, - lk_arr=np.log(k_arr), - a_arr=a_arr, - use_log=True) - tkk_arr_2 = np.array([tk3d.eval(k_arr, a) for a in a_arr]) - assert np.all(np.fabs((tkk_arr / tkk_arr_2 - 1)).flatten() - < 1E-4) + tk3d = ccl.halos.halomod_Tk3D_1h( + COSMO, hmc, + prof=P1, prof2=P2, prof12_2pt=PKC, + prof3=P3, prof4=P4, prof34_2pt=PKC, + lk_arr=np.log(k_arr), a_arr=a_arr, use_log=True) + + tkk_arr_2 = tk3d(k_arr, a_arr) + assert np.allclose(tkk_arr, tkk_arr_2, atol=0, rtol=1e-4) # Standard sampling - tk3d = ccl.halos.halomod_Tk3D_1h(COSMO, hmc, - P1, prof2=P2, - prof12_2pt=PKC, - prof3=P3, prof4=P4, - prof34_2pt=PKC, - normprof1=True, - normprof2=True, - normprof3=True, - normprof4=True, - lk_arr=np.log(k_arr), - use_log=True) - tkk_arr_2 = np.array([tk3d.eval(k_arr, a) for a in a_arr]) - assert np.all(np.fabs((tkk_arr / tkk_arr_2 - 1)).flatten() - < 1E-4) - - -def test_tkk1h_errors(): - hmc = ccl.halos.HMCalculator(COSMO, HMF, HBF, mass_def=M200) - k_arr = KK - a_arr = np.array([0.1, 0.4, 0.7, 1.0]) + tk3d = ccl.halos.halomod_Tk3D_1h( + COSMO, hmc, + prof=P1, prof2=P2, prof12_2pt=PKC, + prof3=P3, prof4=P4, prof34_2pt=PKC, + lk_arr=np.log(k_arr), use_log=True) - # Wrong first profile - with pytest.raises(TypeError): - ccl.halos.halomod_trispectrum_1h(COSMO, hmc, k_arr, a_arr, None) - # Wrong other profiles - with pytest.raises(TypeError): - ccl.halos.halomod_trispectrum_1h(COSMO, hmc, k_arr, a_arr, - P1, prof2=PKC) - with pytest.raises(TypeError): - ccl.halos.halomod_trispectrum_1h(COSMO, hmc, k_arr, a_arr, - P1, prof3=PKC) - with pytest.raises(TypeError): - ccl.halos.halomod_trispectrum_1h(COSMO, hmc, k_arr, a_arr, - P1, prof4=PKC) - # Wrong 2pts - with pytest.raises(TypeError): - ccl.halos.halomod_trispectrum_1h(COSMO, hmc, k_arr, a_arr, - P1, prof12_2pt=P2) - with pytest.raises(TypeError): - ccl.halos.halomod_trispectrum_1h(COSMO, hmc, k_arr, a_arr, - P1, prof34_2pt=P2) + tkk_arr_2 = tk3d(k_arr, a_arr) + assert np.allclose(tkk_arr, tkk_arr_2, atol=0, rtol=1e-4) + + +def test_tkk1h_warns(): + hmc = ccl.halos.HMCalculator(mass_function=HMF, halo_bias=HBF, + mass_def=M200) + a_arr = np.array([0.1, 0.4, 0.7, 1.0]) # Negative profile in logspace + Pneg = ccl.halos.HaloProfilePressureGNFW(P0=-1) with pytest.warns(ccl.CCLWarning): - ccl.halos.halomod_Tk3D_1h(COSMO, hmc, P3, prof2=Pneg, - prof3=P3, prof4=P3, - lk_arr=np.log(k_arr), a_arr=a_arr, - use_log=True) + ccl.halos.halomod_Tk3D_1h( + COSMO, hmc, P3, prof2=Pneg, prof3=P3, prof4=P3, + lk_arr=np.log(KK), a_arr=a_arr, use_log=True) diff --git a/pyccl/tests/test_tkkssc.py b/pyccl/tests/test_tkkssc.py index 5c5a25fc6..75d94c58f 100644 --- a/pyccl/tests/test_tkkssc.py +++ b/pyccl/tests/test_tkkssc.py @@ -1,397 +1,128 @@ -import itertools import numpy as np import pytest +import itertools import pyccl as ccl -from pyccl import UnlockInstance -from pyccl.pyutils import assert_warns -COSMO = ccl.Cosmology( - Omega_c=0.27, Omega_b=0.045, h=0.67, sigma8=0.8, n_s=0.96, - transfer_function='bbks', matter_power_spectrum='linear') +COSMO = ccl.CosmologyVanillaLCDM(transfer_function='bbks', + matter_power_spectrum='linear') COSMO.compute_nonlin_power() M200 = ccl.halos.MassDef200m() -HMF = ccl.halos.MassFuncTinker10(COSMO, mass_def=M200) -HBF = ccl.halos.HaloBiasTinker10(COSMO, mass_def=M200) -P1 = ccl.halos.HaloProfileNFW(ccl.halos.ConcentrationDuffy08(M200), - fourier_analytic=True) -# P2 will have is_number_counts = True -P2 = ccl.halos.HaloProfileHOD(ccl.halos.ConcentrationDuffy08(M200)) -P2_nogc = ccl.halos.HaloProfileHOD(ccl.halos.ConcentrationDuffy08(M200)) -with UnlockInstance(P2_nogc): - P2_nogc.is_number_counts = False -P3 = ccl.halos.HaloProfilePressureGNFW() -P4 = P1 -Pneg = ccl.halos.HaloProfilePressureGNFW(P0=-1) +HMF = ccl.halos.MassFuncTinker10(mass_def=M200) +HBF = ccl.halos.HaloBiasTinker10(mass_def=M200) +HMC = ccl.halos.HMCalculator( + mass_function=HMF, halo_bias=HBF, mass_def=M200, nlog10M=2) +CON = ccl.halos.ConcentrationDuffy08(mass_def=M200) + +NFW = ccl.halos.HaloProfileNFW(concentration=CON, fourier_analytic=True) +HOD = ccl.halos.HaloProfileHOD(concentration=CON) +GNFW = ccl.halos.HaloProfilePressureGNFW() + PKC = ccl.halos.Profile2pt() PKCH = ccl.halos.Profile2ptHOD() KK = np.geomspace(1E-3, 10, 32) -MM = np.geomspace(1E11, 1E15, 16) -AA = 1.0 - - -def get_ssc_counterterm_gc(k, a, hmc, prof1, prof2, prof12_2pt, - normalize=False): - - P_12 = b1 = b2 = np.zeros_like(k) - if prof1.is_number_counts or prof2.is_number_counts: - norm1 = hmc.profile_norm(COSMO, a, prof1) - norm2 = hmc.profile_norm(COSMO, a, prof2) - norm12 = 1 - if prof1.is_number_counts or normalize: - norm12 *= norm1 - if prof2.is_number_counts or normalize: - norm12 *= norm2 - - i11_1 = hmc.I_1_1(COSMO, k, a, prof1) - i11_2 = hmc.I_1_1(COSMO, k, a, prof2) - i02_12 = hmc.I_0_2(COSMO, k, a, prof1, prof12_2pt, prof2) - - pk = ccl.linear_matter_power(COSMO, k, a) - P_12 = norm12 * (pk * i11_1 * i11_2 + i02_12) - - if prof1.is_number_counts: - b1 = ccl.halos.halomod_bias_1pt(COSMO, hmc, k, a, prof1) * norm1 - if prof2.is_number_counts: - b2 = ccl.halos.halomod_bias_1pt(COSMO, hmc, k, a, prof2) * norm2 - - return (b1 + b2) * P_12 - - -@pytest.mark.parametrize('pars', - [{'p1': P1, 'p2': None, 'cv12': None, - 'p3': None, 'p4': None, 'cv34': None, - 'norm': False, 'pk': None}, - {'p1': P1, 'p2': None, 'cv12': None, - 'p3': None, 'p4': None, 'cv34': None, - 'norm': True, 'pk': None}, - {'p1': P1, 'p2': P2, 'cv12': None, - 'p3': None, 'p4': None, 'cv34': None, - 'norm': True, 'pk': None}, - {'p1': P1, 'p2': None, 'cv12': None, - 'p3': P3, 'p4': None, 'cv34': None, - 'norm': False, 'pk': None}, - {'p1': P1, 'p2': None, 'cv12': None, - 'p3': None, 'p4': P4, 'cv34': None, - 'norm': False, 'pk': None}, - {'p1': P1, 'p2': P2, 'cv12': None, - 'p3': P3, 'p4': P4, 'cv34': None, - 'norm': True, 'pk': None}, - {'p1': P2, 'p2': P2, 'cv12': PKCH, - 'p3': P2, 'p4': P2, 'cv34': None, - 'norm': True, 'pk': None}, - {'p1': P1, 'p2': P2, 'cv12': PKC, - 'p3': P3, 'p4': P4, 'cv34': PKC, - 'norm': True, 'pk': None}, - {'p1': P1, 'p2': None, 'cv12': None, - 'p3': None, 'p4': None, 'cv34': None, - 'norm': False, 'pk': 'linear'}, - {'p1': P1, 'p2': None, 'cv12': None, - 'p3': None, 'p4': None, 'cv34': None, - 'norm': False, 'pk': 'nonlinear'}, - {'p1': P1, 'p2': None, 'cv12': None, - 'p3': None, 'p4': None, 'cv34': None, - 'norm': False, 'pk': COSMO.get_nonlin_power()}, - ],) -def test_tkkssc_smoke(pars): - hmc = ccl.halos.HMCalculator(COSMO, HMF, HBF, mass_def=M200, - nlog10M=2) - k_arr = KK - a_arr = np.array([0.3, 0.5, 0.7, 1.0]) - - tkk = ccl.halos.halomod_Tk3D_SSC(COSMO, hmc, - prof1=pars['p1'], - prof2=pars['p2'], - prof12_2pt=pars['cv12'], - prof3=pars['p3'], - prof4=pars['p4'], - prof34_2pt=pars['cv34'], - normprof1=pars['norm'], - normprof2=pars['norm'], - normprof3=pars['norm'], - normprof4=pars['norm'], - p_of_k_a=pars['pk'], - lk_arr=np.log(k_arr), a_arr=a_arr, - ) - tk = tkk.eval(0.1, 0.5) - assert np.all(np.isfinite(tk)) - - -def test_tkkssc_errors(): - hmc = ccl.halos.HMCalculator(COSMO, HMF, HBF, mass_def=M200) - k_arr = KK - a_arr = np.array([0.3, 0.5, 0.7, 1.0]) - - # Wrong first profile - with pytest.raises(TypeError): - ccl.halos.halomod_Tk3D_SSC(COSMO, hmc, None) - # Wrong other profiles - for i in range(2, 4): - kw = {'prof%d' % i: PKC} - with pytest.raises(TypeError): - ccl.halos.halomod_Tk3D_SSC(COSMO, hmc, P1, **kw) - # Wrong 2pts - with pytest.raises(TypeError): - ccl.halos.halomod_Tk3D_SSC(COSMO, hmc, P1, - prof12_2pt=P2) - with pytest.raises(TypeError): - ccl.halos.halomod_Tk3D_SSC(COSMO, hmc, P1, - prof34_2pt=P2) - - # No normalization for number counts profile - with pytest.raises(ValueError): - ccl.halos.halomod_Tk3D_SSC(COSMO, hmc, P2, normprof1=False) - with pytest.raises(ValueError): - ccl.halos.halomod_Tk3D_SSC(COSMO, hmc, P1, prof2=P2, normprof2=False) - with pytest.raises(ValueError): - ccl.halos.halomod_Tk3D_SSC(COSMO, hmc, P1, prof3=P2, normprof3=False) - with pytest.raises(ValueError): - ccl.halos.halomod_Tk3D_SSC(COSMO, hmc, P1, prof4=P2, normprof4=False) - - # Negative profile in logspace +AA = np.linspace(0.3, 1.0, 4) + + +@pytest.mark.parametrize( + "p1,p2,cv12,p3,p4,cv34,pk", + [(NFW, None, None, None, None, None, None), + (HOD, None, None, None, None, None, None), + (NFW, HOD, None, None, None, None, None), + (NFW, None, None, GNFW, None, None, None), + (NFW, None, None, None, NFW, None, None), + (NFW, HOD, None, GNFW, NFW, None, None), + (HOD, HOD, PKCH, HOD, HOD, None, None), + (NFW, HOD, PKC, GNFW, NFW, PKC, None), + (NFW, None, None, None, None, None, "linear"), + (NFW, None, None, None, None, None, "nonlinear"), + (NFW, None, None, None, None, None, COSMO.get_nonlin_power())]) +def test_tkkssc_smoke(p1, p2, cv12, p3, p4, cv34, pk): + tkk = ccl.halos.halomod_Tk3D_SSC( + COSMO, HMC, + prof=p1, prof2=p2, prof12_2pt=cv12, + prof3=p3, prof4=p4, prof34_2pt=cv34, + p_of_k_a=pk, lk_arr=np.log(KK), a_arr=AA) + + assert (np.isfinite(tkk(0.1, 0.5))).all() + + +def test_tkkssc_linear_bias_smoke(): + tkkl = ccl.halos.halomod_Tk3D_SSC_linear_bias( + COSMO, HMC, prof=NFW, p_of_k_a="linear") + *_, tkkl_arrs = tkkl.get_spline_arrays() + assert all([(np.isfinite(tk)).all() for tk in tkkl_arrs]) + + tkknl = ccl.halos.halomod_Tk3D_SSC_linear_bias( + COSMO, HMC, prof=NFW, p_of_k_a="nonlinear") + *_, tkknl_arrs = tkknl.get_spline_arrays() + assert all([(np.isfinite(tk)).all() for tk in tkknl_arrs]) + + tkk_pk = ccl.halos.halomod_Tk3D_SSC_linear_bias( + COSMO, HMC, prof=NFW, p_of_k_a=COSMO.get_nonlin_power()) + *_, tkk_pk_arrs = tkk_pk.get_spline_arrays() + assert all([(np.isfinite(tk)).all() for tk in tkk_pk_arrs]) + + +@pytest.mark.parametrize( + "isNC1,isNC2,isNC3,isNC4", + itertools.product([True, False], repeat=4)) +def test_tkkssc_linear_bias(isNC1, isNC2, isNC3, isNC4): + bias1, bias2, bias3, bias4 = 2, 3, 4, 5 + + # Test that if we remove the biases 12 will be the same as 34. + tkkl = ccl.halos.halomod_Tk3D_SSC_linear_bias( + COSMO, HMC, prof=NFW, + bias1=bias1, bias2=bias2, bias3=bias3, bias4=bias4, + is_number_counts1=False, is_number_counts2=False, + is_number_counts3=False, is_number_counts4=False, + lk_arr=np.log(KK), a_arr=AA) + + *_, (tkkl_12, tkkl_34) = tkkl.get_spline_arrays() + tkkl_12 /= (bias1 * bias2) + tkkl_34 /= (bias3 * bias4) + assert np.allclose(tkkl_12, tkkl_34, atol=0, rtol=1e-12) + + # Test with the full T(k1,k2,a) for an NFW profile with bias ~1. + tkk = ccl.halos.halomod_Tk3D_SSC( + COSMO, HMC, prof=NFW, lk_arr=np.log(KK), a_arr=AA) + *_, (tkk_12, tkk_34) = tkk.get_spline_arrays() + assert np.allclose(tkkl_12, tkk_12, atol=0, rtol=5e-3) + assert np.allclose(tkkl_34, tkk_34, atol=0, rtol=5e-3) + + # Test with clustering profile. + tkkl_nc = ccl.halos.halomod_Tk3D_SSC_linear_bias( + COSMO, HMC, prof=NFW, + bias1=bias1, bias2=bias2, bias3=bias3, bias4=bias4, + is_number_counts1=isNC1, is_number_counts2=isNC2, + is_number_counts3=isNC3, is_number_counts4=isNC4, + lk_arr=np.log(KK), a_arr=AA) + *_, (tkkl_nc_12, tkkl_nc_34) = tkkl_nc.get_spline_arrays() + tkkl_nc_12 /= (bias1 * bias2) + tkkl_nc_34 /= (bias3 * bias4) + + # recover the factors + factor12 = isNC1*bias1 + isNC2*bias2 + factor34 = isNC3*bias3 + isNC4*bias4 + + # calculate what the Tkk's would be with the counterterms. + tkkl_ct_12 = (tkkl_12 - tkkl_nc_12) / factor12 if factor12 else None + tkkl_ct_34 = (tkkl_34 - tkkl_nc_34) / factor34 if factor34 else None + + if factor12*factor34: + assert np.allclose(tkkl_ct_12, tkkl_ct_34, atol=0, rtol=1e-5) + + +def test_tkkssc_warns(): + """Test that it warns if the profile is negative and use_log is True.""" + Pneg = ccl.halos.HaloProfilePressureGNFW(P0=-1) with pytest.warns(ccl.CCLWarning): - ccl.halos.halomod_Tk3D_SSC(COSMO, hmc, P3, prof2=Pneg, - prof3=P3, prof4=P3, - lk_arr=np.log(k_arr), a_arr=a_arr, - use_log=True) - - -@pytest.mark.parametrize('kwargs', [ - # All is_number_counts = False - {'prof1': P1, 'prof2': P1, - 'prof3': P1, 'prof4': P1}, - # - {'prof1': P2, 'prof2': P1, - 'prof3': P1, 'prof4': P1}, - # - {'prof1': P2, 'prof2': P2, - 'prof3': P1, 'prof4': P1}, - # - {'prof1': P2, 'prof2': P2, - 'prof3': P2, 'prof4': P1}, - # - {'prof1': P1, 'prof2': P1, - 'prof3': P2, 'prof4': P2}, - # - {'prof1': P2, 'prof2': P1, - 'prof3': P2, 'prof4': P2}, - # - {'prof1': P2, 'prof2': P2, - 'prof3': P1, 'prof4': P2}, - # - {'prof1': P2, 'prof2': None, - 'prof3': P1, 'prof4': None}, - # - {'prof1': P2, 'prof2': None, - 'prof3': None, 'prof4': None}, - # - {'prof1': P2, 'prof2': P2, - 'prof3': None, 'prof4': None}, - # As in benchmarks/test_covariances.py - {'prof1': P1, 'prof2': P1, - 'prof3': None, 'prof4': None}, - # Setting prof34_2pt - {'prof1': P1, 'prof2': P1, - 'prof3': None, 'prof4': None, - 'prof34_2pt': PKC}, - # All is_number_counts = True - {'prof1': P2, 'prof2': P2, - 'prof3': P2, 'prof4': P2}, - - ] - ) -def test_tkkssc_counterterms_gc(kwargs): - hmc = ccl.halos.HMCalculator(COSMO, HMF, HBF, mass_def=M200, - nlog10M=2) - k_arr = KK - a_arr = np.array([0.3, 0.5, 0.7, 1.0]) - - # Tk's without clustering terms. Set is_number_counts=False for HOD - # profiles - # Ensure HOD profiles are normalized - kwargs_nogc = kwargs.copy() - keys = list(kwargs.keys()) - for k in keys: - v = kwargs[k] - if isinstance(v, ccl.halos.HaloProfileHOD): - kwargs_nogc[k] = P2_nogc - kwargs_nogc['norm' + k] = True - kwargs['norm' + k] = True - tkk_nogc = ccl.halos.halomod_Tk3D_SSC(COSMO, hmc, - lk_arr=np.log(k_arr), a_arr=a_arr, - **kwargs_nogc) - _, _, _, tkk_nogc_arrs = tkk_nogc.get_spline_arrays() - tk_nogc_12, tk_nogc_34 = tkk_nogc_arrs - - # Tk's with clustering terms - tkk_gc = ccl.halos.halomod_Tk3D_SSC(COSMO, hmc, - lk_arr=np.log(k_arr), a_arr=a_arr, - **kwargs) - _, _, _, tkk_gc_arrs = tkk_gc.get_spline_arrays() - tk_gc_12, tk_gc_34 = tkk_gc_arrs + ccl.halos.halomod_Tk3D_SSC( + COSMO, HMC, prof=GNFW, prof2=Pneg, + lk_arr=np.log(KK), a_arr=AA, use_log=True) - # Update the None's to their corresponding values - if kwargs['prof2'] is None: - kwargs['prof2'] = kwargs['prof1'] - if kwargs['prof3'] is None: - kwargs['prof3'] = kwargs['prof1'] - if kwargs['prof4'] is None: - kwargs['prof4'] = kwargs['prof2'] - # Tk's of the clustering terms - tkc12 = [] - tkc34 = [] - for aa in a_arr: - tkc12.append(get_ssc_counterterm_gc(k_arr, aa, hmc, kwargs['prof1'], - kwargs['prof2'], PKC)) - tkc34.append(get_ssc_counterterm_gc(k_arr, aa, hmc, kwargs['prof3'], - kwargs['prof4'], PKC)) - tkc12 = np.array(tkc12) - tkc34 = np.array(tkc34) - - assert np.abs((tk_nogc_12 - tkc12) / tk_gc_12 - 1).max() < 1e-5 - assert np.abs((tk_nogc_34 - tkc34) / tk_gc_34 - 1).max() < 1e-5 - - -@pytest.mark.parametrize('kwargs', [{f'is_number_counts{i+1}': nc[i] for i in - range(4)} for nc in - itertools.product([True, False], - repeat=4)]) -def test_tkkssc_linear_bias(kwargs): - hmc = ccl.halos.HMCalculator(COSMO, HMF, HBF, mass_def=M200, - nlog10M=2) - k_arr = KK - a_arr = np.array([0.3, 0.5, 0.7, 1.0]) - - # Tk's exact version - prof = ccl.halos.HaloProfileNFW(ccl.halos.ConcentrationDuffy08(M200), - fourier_analytic=True) - bias1 = 2 - bias2 = 3 - bias3 = 4 - bias4 = 5 - is_nc = False - - # Tk's from tkkssc_linear - tkk_lin = ccl.halos.halomod_Tk3D_SSC_linear_bias(COSMO, hmc, prof=prof, - bias1=bias1, - bias2=bias2, - bias3=bias3, - bias4=bias4, - is_number_counts1=is_nc, - is_number_counts2=is_nc, - is_number_counts3=is_nc, - is_number_counts4=is_nc, - lk_arr=np.log(k_arr), - a_arr=a_arr) - _, _, _, tkk_lin_arrs = tkk_lin.get_spline_arrays() - tk_lin_12, tk_lin_34 = tkk_lin_arrs - # Remove the biases - tk_lin_12 /= (bias1 * bias2) - tk_lin_34 /= (bias3 * bias4) - assert np.abs(tk_lin_12 / tk_lin_34 - 1).max() < 1e-5 - - # True Tk's (biases for NFW ~ 1) - tkk = ccl.halos.halomod_Tk3D_SSC(COSMO, hmc, prof1=prof, - lk_arr=np.log(k_arr), a_arr=a_arr, - normprof1=True, normprof2=True, - normprof3=True, normprof4=True) - _, _, _, tkk_arrs = tkk.get_spline_arrays() - tk_12, tk_34 = tkk_arrs - - assert np.abs(tk_lin_12 / tk_12 - 1).max() < 1e-2 - assert np.abs(tk_lin_34 / tk_34 - 1).max() < 1e-2 - - # Now with clustering - tkk_lin_nc = ccl.halos.halomod_Tk3D_SSC_linear_bias(COSMO, hmc, prof=prof, - bias1=bias1, - bias2=bias2, - bias3=bias3, - bias4=bias4, - **kwargs, - lk_arr=np.log(k_arr), - a_arr=a_arr) - _, _, _, tkk_lin_nc_arrs = tkk_lin_nc.get_spline_arrays() - tk_lin_nc_12, tk_lin_nc_34 = tkk_lin_nc_arrs - tk_lin_nc_12 /= (bias1 * bias2) - tk_lin_nc_34 /= (bias3 * bias4) - - factor12 = 0 - factor12 += bias1 if kwargs['is_number_counts1'] else 0 - factor12 += bias2 if kwargs['is_number_counts2'] else 0 - - factor34 = 0 - factor34 += bias3 if kwargs['is_number_counts3'] else 0 - factor34 += bias4 if kwargs['is_number_counts4'] else 0 - - tk_lin_ct_12 = np.zeros_like(tk_lin_nc_12) - if factor12 != 0: - tk_lin_ct_12 = (tk_lin_12 - tk_lin_nc_12) / factor12 # = pk+i02 - - tk_lin_ct_34 = np.zeros_like(tk_lin_nc_34) - if factor34 != 0: - tk_lin_ct_34 = (tk_lin_34 - tk_lin_nc_34) / factor34 # = pk+i02 - - if (factor12 != 0) and (factor34 != 0): - assert np.abs(tk_lin_ct_12 / tk_lin_ct_34 - 1).max() < 1e-5 - - # True counter terms - tkc12 = [] - tkc34 = [] - with UnlockInstance(prof): - prof.is_number_counts = True # Trick the function below - for aa in a_arr: - # Divide by 2 to account for ~(1 + 1) - tkc_ia = get_ssc_counterterm_gc(k_arr, aa, hmc, prof, prof, PKC, - normalize=True) / 2 - if factor12 == 0: - tkc12.append(np.zeros_like(tkc_ia)) - else: - tkc12.append(tkc_ia) - - if factor34 == 0: - tkc34.append(np.zeros_like(tkc_ia)) - else: - tkc34.append(tkc_ia) - - tkc12 = np.array(tkc12) - tkc34 = np.array(tkc34) - - # Add 1e-100 for the cases when the counter terms are 0 - assert np.abs((tk_lin_ct_12 + 1e-100) / (tkc12 + 1e-100) - 1).max() < 1e-2 - assert np.abs((tk_lin_ct_34 + 1e-100) / (tkc34 + 1e-100) - 1).max() < 1e-2 - - -def test_tkkssc_linear_bias_smoke_and_errors(): - hmc = ccl.halos.HMCalculator(COSMO, HMF, HBF, mass_def=M200, - nlog10M=2) - k_arr = KK - a_arr = np.array([0.3, 0.5, 0.7, 1.0]) - - # Tk's exact version - prof = ccl.halos.HaloProfileNFW(ccl.halos.ConcentrationDuffy08(M200), - fourier_analytic=True) - - ccl.halos.halomod_Tk3D_SSC_linear_bias(COSMO, hmc, prof=prof, - p_of_k_a='linear') - - ccl.halos.halomod_Tk3D_SSC_linear_bias(COSMO, hmc, prof=prof, - p_of_k_a='nonlinear') - - pk = COSMO.get_nonlin_power() - ccl.halos.halomod_Tk3D_SSC_linear_bias(COSMO, hmc, prof=prof, p_of_k_a=pk) - - # Error when prof is not NFW +def test_tkkssc_linear_bias_raises(): + """Test that it raises if the profile is not NFW.""" with pytest.raises(TypeError): - ccl.halos.halomod_Tk3D_SSC_linear_bias(COSMO, hmc, prof=P2) - - # Error when p_of_k_a is wrong - with pytest.raises(TypeError): - ccl.halos.halomod_Tk3D_SSC_linear_bias(COSMO, hmc, prof=prof, - p_of_k_a=P1) - - # Negative profile in logspace - assert_warns(ccl.CCLWarning, ccl.halos.halomod_Tk3D_SSC_linear_bias, - COSMO, hmc, prof, bias1=-1, - lk_arr=np.log(k_arr), a_arr=a_arr, - use_log=True) + ccl.halos.halomod_Tk3D_SSC_linear_bias(COSMO, HMC, prof=GNFW) diff --git a/pyccl/tests/test_tracers.py b/pyccl/tests/test_tracers.py index 1ca00e63e..3db885804 100644 --- a/pyccl/tests/test_tracers.py +++ b/pyccl/tests/test_tracers.py @@ -2,10 +2,69 @@ import pytest import pyccl as ccl from pyccl import CCLWarning +from .test_cclobject import check_eq_repr_hash COSMO = ccl.Cosmology( Omega_c=0.27, Omega_b=0.045, h=0.67, sigma8=0.8, n_s=0.96, transfer_function='bbks', matter_power_spectrum='linear') +COSMO.compute_distances() # needed to suppress C-level warnings + + +def test_Tracer_eq_repr_hash(): + # Test eq, repr, hash for Tracer. + # empty Tracer + assert check_eq_repr_hash(ccl.Tracer(), ccl.Tracer()) + + # no transfer + TR1 = ccl.CMBLensingTracer(COSMO, z_source=1101) + TR2 = ccl.CMBLensingTracer(COSMO, z_source=1101) + assert check_eq_repr_hash(TR1, TR2) + + TR3 = ccl.CMBLensingTracer(COSMO, z_source=1100) + assert check_eq_repr_hash(TR1, TR3, equal=False) + + # transfer_fka + lk = np.linspace(-5, 1, 32) + a = np.linspace(0.5, 1, 4) + tka = np.ones((a.size, lk.size)) + TR1.add_tracer(COSMO, transfer_ka=(a, lk, tka)) + TR2.add_tracer(COSMO, transfer_ka=(a, lk, 2*tka)) + assert check_eq_repr_hash(TR1, TR2, equal=False) + + # transfer_fk + TR4 = ccl.CMBLensingTracer(COSMO, z_source=1101) + TR5 = ccl.CMBLensingTracer(COSMO, z_source=1101) + TR4.add_tracer(COSMO, transfer_k=(lk, np.ones_like(lk))) + TR5.add_tracer(COSMO, transfer_k=(lk, 2*np.ones_like(lk))) + assert check_eq_repr_hash(TR4, TR5, equal=False) + + # edge-case: different type + assert check_eq_repr_hash(ccl.Tracer(), 1, equal=False) + + # edge-case: different `der_angles` & `der_bessel` + t1, t2 = ccl.Tracer(), ccl.Tracer() + t1.add_tracer(COSMO, der_angles=0, der_bessel=0) + t2.add_tracer(COSMO, der_angles=1, der_bessel=1) + assert check_eq_repr_hash(t1, t2, equal=False) + + # edge-case: only one has kernel + chi = np.linspace(0, 50, 16) + t1, t2 = ccl.Tracer(), ccl.Tracer() + t1.add_tracer(COSMO, kernel=(chi, np.ones_like(chi))) + t2.add_tracer(COSMO) + assert check_eq_repr_hash(t1, t2, equal=False) + + # edge-case: only one has transfer + t1, t2 = ccl.Tracer(), ccl.Tracer() + t1.add_tracer(COSMO, transfer_k=(lk, np.ones_like(lk))) + t2.add_tracer(COSMO) + assert check_eq_repr_hash(t1, t2, equal=False) + + # edge-case: only one has specific transfer type + t1, t2 = ccl.Tracer(), ccl.Tracer() + t1.add_tracer(COSMO, transfer_k=(lk, np.ones_like(lk))) + t2.add_tracer(COSMO, transfer_a=(a, np.ones_like(a))) + assert check_eq_repr_hash(t1, t2, equal=False) def dndz(z): @@ -21,7 +80,7 @@ def get_tracer(tracer_type, cosmo=None, **tracer_kwargs): if tracer_type == 'nc': ntr = 3 - tr = ccl.NumberCountsTracer(cosmo, True, + tr = ccl.NumberCountsTracer(cosmo, has_rsd=True, dndz=(z, n), bias=(z, b), mag_bias=(z, b), @@ -34,7 +93,7 @@ def get_tracer(tracer_type, cosmo=None, **tracer_kwargs): **tracer_kwargs) elif tracer_type == 'cl': ntr = 1 - tr = ccl.CMBLensingTracer(cosmo, 1100., **tracer_kwargs) + tr = ccl.CMBLensingTracer(cosmo, z_source=1100., **tracer_kwargs) else: ntr = 0 tr = ccl.Tracer(**tracer_kwargs) @@ -50,16 +109,16 @@ def test_tracer_mag_0p4(): s_no = np.ones_like(z)*0.4 s_yes = np.zeros_like(z) # Tracer with no magnification by construction - t1 = ccl.NumberCountsTracer(COSMO, True, + t1 = ccl.NumberCountsTracer(COSMO, has_rsd=True, dndz=(z, n), bias=(z, b)) # Tracer with s=0.4 - t2 = ccl.NumberCountsTracer(COSMO, True, + t2 = ccl.NumberCountsTracer(COSMO, has_rsd=True, dndz=(z, n), bias=(z, b), mag_bias=(z, s_no)) # Tracer with magnification - t3 = ccl.NumberCountsTracer(COSMO, True, + t3 = ccl.NumberCountsTracer(COSMO, has_rsd=True, dndz=(z, n), bias=(z, b), mag_bias=(z, s_yes)) @@ -83,13 +142,6 @@ def test_tracer_dndz_smoke(tracer_type): assert np.all(np.fabs(n1 / n2 - 1) < 1E-5) -@pytest.mark.parametrize('tracer_type', ['cl', 'not']) -def test_tracer_dndz_errors(tracer_type): - tr, _ = get_tracer(tracer_type) - with pytest.raises(NotImplementedError): - tr.get_dndz(0.5) - - @pytest.mark.parametrize('tracer_type', ['nc', 'wl', 'cl', 'not']) def test_tracer_kernel_smoke(tracer_type): tr, ntr = get_tracer(tracer_type) @@ -180,7 +232,7 @@ def test_tracer_nz_support(): n = dndz(z) with pytest.raises(ValueError): - _ = ccl.WeakLensingTracer(calculator_cosmo, (z, n)) + _ = ccl.WeakLensingTracer(calculator_cosmo, dndz=(z, n)) with pytest.raises(ValueError): _ = ccl.NumberCountsTracer(calculator_cosmo, has_rsd=False, @@ -279,13 +331,13 @@ def test_tracer_magnification_kernel_spline_vs_gsl_intergation(z_min, z_max, assert n[0] > 0 ccl.gsl_params.LENSING_KERNEL_SPLINE_INTEGRATION = True - tr_mg = ccl.NumberCountsTracer(cosmo, False, dndz=(z, n), + tr_mg = ccl.NumberCountsTracer(cosmo, has_rsd=False, dndz=(z, n), bias=(z, b), mag_bias=(z, b)) w_mg_spline, _ = tr_mg.get_kernel(chi=None) ccl.gsl_params.reload() ccl.gsl_params.LENSING_KERNEL_SPLINE_INTEGRATION = True - tr_mg = ccl.NumberCountsTracer(cosmo, False, dndz=(z, n), + tr_mg = ccl.NumberCountsTracer(cosmo, has_rsd=False, dndz=(z, n), bias=(z, b), mag_bias=(z, b)) w_mg_gsl, chi = tr_mg.get_kernel(chi=None) tr_wl = ccl.WeakLensingTracer(cosmo, dndz=(z, n)) @@ -316,7 +368,7 @@ def test_tracer_delta_function_nz(): # Single source plane tracer to compare against chi_kappa, w_kappa = ccl.tracers.get_kappa_kernel(COSMO, z_source=z_s, - nsamples=100) + n_samples=100) # Use the same comoving distances w = tr_wl.get_kernel(chi=chi_kappa) @@ -374,6 +426,12 @@ def test_tracer_chi_min_max(): assert tr.chi_max == tr._trc[1].chi_max +def test_zpower_raises(): + with pytest.raises(ValueError): + ccl.Tracer.from_zPower(COSMO, A=1., z_min=1.0, + z_max=0.1, alpha=1.0) + + def test_tracer_increase_sf(): z = np.linspace(0, 3., 32) one = np.ones(len(z)) @@ -405,7 +463,7 @@ def test_tracer_repr(): # Check empty tracer. z = np.linspace(0, 0.5, 128) nz = np.ones_like(z) - tr4 = ccl.Tracer() + tr4 = ccl.NzTracer() tr5 = ccl.NumberCountsTracer(COSMO, dndz=(z, nz), has_rsd=False) # all off assert tr4 == tr5 # Check tracers with transfer functions. @@ -418,6 +476,11 @@ def test_tracer_repr(): t_k = np.ones_like(lk) tr6.add_tracer(COSMO, transfer_k=(lk, t_k)) tr7.add_tracer(COSMO, transfer_k=(lk, t_k)) + assert tr6 == tr7 + # transfer_a + tr6.add_tracer(COSMO, transfer_a=(1/(1+z)[::-1], nz)) + tr7.add_tracer(COSMO, transfer_a=(1/(1+z)[::-1], nz)) + assert tr6 == tr7 # transfer_ka a = np.linspace(0.5, 1.0, 8) t_ka = np.ones((a.size, lk.size)) diff --git a/pyccl/tests/test_v3_api.py b/pyccl/tests/test_v3_api.py new file mode 100644 index 000000000..71fb7009a --- /dev/null +++ b/pyccl/tests/test_v3_api.py @@ -0,0 +1,10 @@ +import pyccl as ccl +import pytest + + +def test_unexpected_argument_raises(): + # Test that if an argument has been renamed it will still raise when + # a wrong argument is passed. + with pytest.raises(TypeError): + # here, `c_m` was renamed to `concentration` + ccl.halos.MassDef200c(hello="Duffy08") diff --git a/pyccl/tk3d.py b/pyccl/tk3d.py index 2cfd8002a..d6a79c991 100644 --- a/pyccl/tk3d.py +++ b/pyccl/tk3d.py @@ -85,7 +85,7 @@ class Tk3D(CCLObject): expected. Note that arrays will be interpolated in log space if `is_logt` is set to `True`. """ - from ._repr import _build_string_Tk3D as __repr__ + from .base.repr_ import build_string_Tk3D as __repr__ def __init__(self, a_arr, lk_arr, tkk_arr=None, pk1_arr=None, pk2_arr=None, extrap_order_lok=1, @@ -133,10 +133,42 @@ def __init__(self, a_arr, lk_arr, tkk_arr=None, int(is_logt), status) check(status) + def __eq__(self, other): + # Check the object class. + if self.__class__ is not other.__class__: + return False + # If the objects contain no data, return early. + if not (self or other): + return True + # If one is factorizable and the other one is not, return early. + if self.tsp.is_product ^ other.tsp.is_product: + return False + # Check extrapolation orders. + if not (self.extrap_order_lok == other.extrap_order_lok + and self.extrap_order_hik == other.extrap_order_hik): + return False + # Check the individual splines. + a1, lk11, lk12, tk1 = self.get_spline_arrays() + a2, lk21, lk22, tk2 = other.get_spline_arrays() + return ((a1 == a2).all() + and (lk11 == lk21).all() and (lk21 == lk22).all() + and np.allclose(tk1, tk2, atol=0, rtol=1e-12)) + + def __hash__(self): + return hash(repr(self)) + @property def has_tsp(self): return 'tsp' in vars(self) + @property + def extrap_order_lok(self): + return self.tsp.extrap_order_lok if self else None + + @property + def extrap_order_hik(self): + return self.tsp.extrap_order_hik if self else None + def eval(self, k, a): """Evaluate trispectrum. If `k` is a 1D array with size `nk`, the output `out` will be a 2D array with shape `[nk,nk]` holding diff --git a/pyccl/tracers.py b/pyccl/tracers.py index c1401c927..bff9d47e6 100644 --- a/pyccl/tracers.py +++ b/pyccl/tracers.py @@ -4,16 +4,14 @@ from . import ccllib as lib from .core import check -from .background import (comoving_radial_distance, growth_rate, - growth_factor, scale_factor_of_chi, h_over_h0) from .errors import CCLWarning -from .parameters import physical_constants -from .base import CCLObject, UnlockInstance, unlock_instance +from .base.parameters import physical_constants +from .base import CCLObject, UnlockInstance, unlock_instance, warn_api from .pyutils import (_check_array_params, NoneArr, _vectorize_fn6, - _get_spline1d_arrays) + _get_spline1d_arrays, _get_spline2d_arrays) -def _Sig_MG(cosmo, a, k=None): +def _Sig_MG(cosmo, a, k): """Redshift-dependent modification to Poisson equation for massless particles under modified gravity. @@ -28,6 +26,7 @@ def _Sig_MG(cosmo, a, k=None): Sig_MG is assumed to be proportional to Omega_Lambda(z), see e.g. Abbott et al. 2018, 1810.02499, Eq. 9. """ + cosmo.compute_distances() return _vectorize_fn6(lib.Sig_MG, lib.Sig_MG_vec, cosmo, a, k) @@ -46,7 +45,8 @@ def _check_background_spline_compatibility(cosmo, z): f"splines: z=[{1/a_bg.max()-1}, {1/a_bg.min()-1}].") -def get_density_kernel(cosmo, dndz): +@warn_api +def get_density_kernel(cosmo, *, dndz): """This convenience function returns the radial kernel for galaxy-clustering-like tracers. Given an unnormalized redshift distribution, it returns two arrays: chi, w(chi), @@ -66,7 +66,7 @@ def get_density_kernel(cosmo, dndz): z_n, n = _check_array_params(dndz, 'dndz') _check_background_spline_compatibility(cosmo, dndz[0]) # this call inits the distance splines neded by the kernel functions - chi = comoving_radial_distance(cosmo, 1./(1.+z_n)) + chi = cosmo.comoving_radial_distance(1./(1.+z_n)) status = 0 wchi, status = lib.get_number_counts_kernel_wrapper(cosmo.cosmo, z_n, n, @@ -76,7 +76,8 @@ def get_density_kernel(cosmo, dndz): return chi, wchi -def get_lensing_kernel(cosmo, dndz, mag_bias=None, n_chi=None): +@warn_api +def get_lensing_kernel(cosmo, *, dndz, mag_bias=None, n_chi=None): """This convenience function returns the radial kernel for weak-lensing-like. Given an unnormalized redshift distribution and an optional magnification bias function, it returns @@ -107,14 +108,14 @@ def get_lensing_kernel(cosmo, dndz, mag_bias=None, n_chi=None): # Calculate number of samples in chi n_chi = lib.get_nchi_lensing_kernel_wrapper(z_n) - if n_chi > len(z_n): + if (n_chi > len(z_n) + and cosmo.cosmo.gsl_params.LENSING_KERNEL_SPLINE_INTEGRATION): warnings.warn( f"The number of samples in the n(z) ({len(z_n)}) is smaller than " f"the number of samples in the lensing kernel ({n_chi}). Consider " - f"disabling spline integration for the lensing kernel by setting " - f"pyccl.gsl_params.LENSING_KERNEL_SPLINE_INTEGRATION " - f"= False", - category=CCLWarning) + "disabling spline integration for the lensing kernel by setting " + "pyccl.gsl_params.LENSING_KERNEL_SPLINE_INTEGRATION = False " + "before instantiating the Cosmology passed.", category=CCLWarning) # Compute array of chis status = 0 @@ -129,26 +130,27 @@ def get_lensing_kernel(cosmo, dndz, mag_bias=None, n_chi=None): return chi, wchi -def get_kappa_kernel(cosmo, z_source, nsamples): +@warn_api(pairs=[("nsamples", "n_samples")]) +def get_kappa_kernel(cosmo, *, z_source, n_samples=100): """This convenience function returns the radial kernel for CMB-lensing-like tracers. Args: cosmo (:class:`~pyccl.core.Cosmology`): Cosmology object. z_source (float): Redshift of source plane for CMB lensing. - nsamples (int): number of samples over which the kernel + n_samples (int): number of samples over which the kernel is desired. These will be equi-spaced in radial distance. The kernel is quite smooth, so usually O(100) samples is enough. """ _check_background_spline_compatibility(cosmo, np.array([z_source])) # this call inits the distance splines neded by the kernel functions - chi_source = comoving_radial_distance(cosmo, 1./(1.+z_source)) - chi = np.linspace(0, chi_source, nsamples) + chi_source = cosmo.comoving_radial_distance(1./(1.+z_source)) + chi = np.linspace(0, chi_source, n_samples) status = 0 wchi, status = lib.get_kappa_kernel_wrapper(cosmo.cosmo, chi_source, - chi, nsamples, status) + chi, n_samples, status) check(status, cosmo=cosmo) return chi, wchi @@ -176,7 +178,7 @@ class Tracer(CCLObject): tracers that get combined linearly when computing power spectra. Further details can be found in Section 4.9 of the CCL note. """ - from ._repr import _build_string_Tracer as __repr__ + from .base.repr_ import build_string_Tracer as __repr__ def __init__(self): """By default this `Tracer` object will contain no actual @@ -185,6 +187,79 @@ def __init__(self): # Do nothing, just initialize list of tracers self._trc = [] + def __eq__(self, other): + # Check the object class. + if self.__class__ is not other.__class__: + return False + + # If the tracer collections are empty, return early. + if not (self or other): + return True + + # If the tracer collections are not the same length, return early. + if len(self._trc) != len(other._trc): + return False + + # Check `der_angles` & `der_bessel` for each tracer in the collection. + bessel = self.get_bessel_derivative(), other.get_bessel_derivative() + angles = self.get_angles_derivative(), other.get_angles_derivative() + if not (np.array_equal(*bessel) and np.array_equal(*angles)): + return False + + # Check the kernels. + kwargs = {"atol": 0, "rtol": 1e-12, "equal_nan": True} + for t1, t2 in zip(self._trc, other._trc): + if bool(t1.kernel) ^ bool(t2.kernel): + # only one of them has a kernel + return False + if t1.kernel is None: + # none of them has a kernel + continue + if not np.allclose(_get_spline1d_arrays(t1.kernel.spline), + _get_spline1d_arrays(t2.kernel.spline), + **kwargs): + # both have kernels, but they are unequal + return False + + # Check the transfer functions. + for t1, t2 in zip(self._trc, other._trc): + if bool(t1.transfer) ^ bool(t2.transfer): + # only one of them has a transfer + return False + if t1.transfer is None: + # none of them has a transfer + continue + # Check the characteristics of the transfer function. + for arg in ("extrap_order_lok", "extrap_order_hik", + "is_factorizable", "is_log"): + if getattr(t1.transfer, arg) != getattr(t2.transfer, arg): + return False + + c2py = {"fa": _get_spline1d_arrays, + "fk": _get_spline1d_arrays, + "fka": _get_spline2d_arrays} + for attr in c2py.keys(): + spl1 = getattr(t1.transfer, attr, None) + spl2 = getattr(t2.transfer, attr, None) + if bool(spl1) ^ bool(spl2): + # only one of them has this transfer type + return False + if spl1 is None: + # none of them has this transfer type + continue + # `pts` contain the the grid points and the transfer functions + pts1, pts2 = c2py[attr](spl1), c2py[attr](spl2) + for pt1, pt2 in zip(pts1, pts2): + # loop through output points of `_get_splinend_arrays` + if not np.allclose(pt1, pt2, **kwargs): + # both have this transfer type, but they are unequal + # or are defined at different grid points + return False + return True + + def __hash__(self): + return hash(repr(self)) + def __bool__(self): return bool(self._trc) @@ -206,24 +281,6 @@ def chi_max(self): chis = [tr.chi_max for tr in self._trc] return max(chis) if chis else None - def _dndz(self, z): - raise NotImplementedError("`get_dndz` not implemented for " - "this `Tracer` type.") - - def get_dndz(self, z): - """Get the redshift distribution for this tracer. - Only available for some tracers (:class:`NumberCountsTracer` and - :class:`WeakLensingTracer`). - - Args: - z (float or array_like): redshift values. - - Returns: - array_like: redshift distribution evaluated at the - input values of `z`. - """ - return self._dndz(z) - def get_kernel(self, chi=None): """Get the radial kernels for all tracers contained in this `Tracer`. @@ -247,26 +304,28 @@ def get_kernel(self, chi=None): chis = [] else: chi_use = np.atleast_1d(chi) + kernels = [] for t in self._trc: - if chi is None: - chi_use, w = _get_spline1d_arrays(t.kernel.spline) - chis.append(chi_use) + if t.kernel is None: + continue else: - status = 0 - w, status = lib.cl_tracer_get_kernel(t, chi_use, - chi_use.size, - status) - check(status) - kernels.append(w) + if chi is None: + chi_use, w = _get_spline1d_arrays(t.kernel.spline) + chis.append(chi_use) + else: + status = 0 + w, status = lib.cl_tracer_get_kernel( + t, chi_use, chi_use.size, status) + check(status) + kernels.append(w) + if chi is None: return kernels, chis - else: - kernels = np.array(kernels) - if np.ndim(chi) == 0: - if kernels.shape != (0,): - kernels = np.squeeze(kernels, axis=-1) - return kernels + kernels = np.array(kernels) + if np.ndim(chi) == 0 and kernels.shape != (0,): + kernels = np.squeeze(kernels, axis=-1) + return kernels def get_f_ell(self, ell): """Get the ell-dependent prefactors for all tracers @@ -343,6 +402,12 @@ def get_bessel_derivative(self): """ return np.array([t.der_bessel for t in self._trc]) + def get_angles_derivative(self): + r"""Get ``enum`` of the :math:`\ell`-dependent prefactor for all + tracers contained in this tracer collection. + """ + return np.array([t.der_angles for t in self._trc]) + def _MG_add_tracer(self, cosmo, kernel, z_b, der_bessel=0, der_angles=0, bias_transfer_a=None, bias_transfer_k=None): """ function to set mg_transfer in the right format and add MG tracers @@ -354,7 +419,7 @@ def _MG_add_tracer(self, cosmo, kernel, z_b, der_bessel=0, der_angles=0, # case with no astro biases if ((bias_transfer_a is None) and (bias_transfer_k is None)): - self.add_tracer(cosmo, kernel, transfer_ka=mg_transfer, + self.add_tracer(cosmo, kernel=kernel, transfer_ka=mg_transfer, der_bessel=der_bessel, der_angles=der_angles) # case of an astro bias depending on a and k @@ -362,21 +427,21 @@ def _MG_add_tracer(self, cosmo, kernel, z_b, der_bessel=0, der_angles=0, mg_transfer_new = (mg_transfer[0], mg_transfer[1], (bias_transfer_a[1] * (bias_transfer_k[1] * mg_transfer[2]).T).T) - self.add_tracer(cosmo, kernel, transfer_ka=mg_transfer_new, + self.add_tracer(cosmo, kernel=kernel, transfer_ka=mg_transfer_new, der_bessel=der_bessel, der_angles=der_angles) # case of an astro bias depending on a but not k elif ((bias_transfer_a is not None) and (bias_transfer_k is None)): mg_transfer_new = (mg_transfer[0], mg_transfer[1], (bias_transfer_a[1] * mg_transfer[2].T).T) - self.add_tracer(cosmo, kernel, transfer_ka=mg_transfer_new, + self.add_tracer(cosmo, kernel=kernel, transfer_ka=mg_transfer_new, der_bessel=der_bessel, der_angles=der_angles) # case of an astro bias depending on k but not a elif ((bias_transfer_a is None) and (bias_transfer_k is not None)): mg_transfer_new = (mg_transfer[0], mg_transfer[1], (bias_transfer_k[1] * mg_transfer[2])) - self.add_tracer(cosmo, kernel, transfer_ka=mg_transfer_new, + self.add_tracer(cosmo, kernel=kernel, transfer_ka=mg_transfer_new, der_bessel=der_bessel, der_angles=der_angles) def _get_MG_transfer_function(self, cosmo, z): @@ -417,10 +482,7 @@ def _get_MG_transfer_function(self, cosmo, z): a = 1./(1.+z) a.sort() # Scale-dependant MG case with an array of k - nk = lib.get_pk_spline_nk(cosmo.cosmo) - status = 0 - lk, status = lib.get_pk_spline_lk(cosmo.cosmo, nk, status) - check(status, cosmo=cosmo) + lk = cosmo.get_pk_spline_lk() k = np.exp(lk) # computing MG factor array mgfac_1d = 1 @@ -434,8 +496,9 @@ def _get_MG_transfer_function(self, cosmo, z): return mg_transfer + @warn_api @unlock_instance - def add_tracer(self, cosmo, kernel=None, + def add_tracer(self, cosmo, *, kernel=None, transfer_ka=None, transfer_k=None, transfer_a=None, der_bessel=0, der_angles=0, is_logt=False, extrap_order_lok=0, extrap_order_hik=2): @@ -568,6 +631,40 @@ def add_tracer(self, cosmo, kernel=None, status) self._trc.append(_check_returned_tracer(ret)) + @classmethod + def from_zPower(cls, cosmo, *, A, alpha, z_min=0., z_max=6., n_chi=1024): + """Constructor for tracers associated with a radial kernel of the form + + .. math:: + W(\\chi) = \\frac{A}{(1+z)^\alpha}, + + where :math:`A` is an amplitude and :math:`\alpha` is a power + law index. The kernel only has support in the redshift range + [`z_min`, `z_max`]. + + Args: + cosmo (:class:`~pyccl.core.Cosmology`): Cosmology object. + A (float): amplitude parameter. + alpha (float): power law index. + z_min (float): minimum redshift from to which we define the kernel. + z_max (float): maximum redshift up to which we define the kernel. + n_chi (float): number of intervals in the radial comoving + distance on which we sample the kernel. + """ + if z_min >= z_max: + raise ValueError("z_min should be smaller than z_max.") + + tracer = cls() + + chi_min = cosmo.comoving_radial_distance(1./(1+z_min)) + chi_max = cosmo.comoving_radial_distance(1./(1+z_max)) + chi_arr = np.linspace(chi_min, chi_max, n_chi) + a_arr = cosmo.scale_factor_of_chi(chi_arr) + w_arr = A * a_arr**alpha + + tracer.add_tracer(cosmo, kernel=(chi_arr, w_arr)) + return tracer + def __del__(self): # Sometimes lib is freed before some Tracers, in which case, this # doesn't work. @@ -577,16 +674,32 @@ def __del__(self): lib.cl_tracer_t_free(t) -def NumberCountsTracer(cosmo, has_rsd, dndz, bias=None, - mag_bias=None, n_samples=256): +class NzTracer(Tracer): + """Specific for tracers with an internal `_dndz` redshift + distribution interpolator. + """ + def get_dndz(self, z): + """Get the redshift distribution for this tracer. + + Args: + z (float or array_like): redshift values. + + Returns: + array_like: redshift distribution evaluated at the + input values of `z`. + """ + return self._dndz(z) + + +@warn_api(reorder=["has_rsd", "dndz", "bias", "mag_bias"]) +def NumberCountsTracer(cosmo, *, dndz, bias=None, mag_bias=None, + has_rsd, n_samples=256): """Specific `Tracer` associated to galaxy clustering with linear scale-independent bias, including redshift-space distortions and magnification. Args: cosmo (:class:`~pyccl.core.Cosmology`): Cosmology object. - has_rsd (bool): Flag for whether the tracer has a - redshift-space distortion term. dndz (tuple of arrays): A tuple of arrays (z, N(z)) giving the redshift distribution of the objects. The units are arbitrary; N(z) will be normalized to unity. @@ -597,12 +710,14 @@ def NumberCountsTracer(cosmo, has_rsd, dndz, bias=None, giving the magnification bias as a function of redshift. If `None`, the tracer is assumed to not have magnification bias terms. Defaults to None. + has_rsd (bool): Flag for whether the tracer has a + redshift-space distortion term. n_samples (int, optional): number of samples over which the magnification lensing kernel is desired. These will be equi-spaced in radial distance. The kernel is quite smooth, so usually O(100) samples is enough. """ - tracer = Tracer() + tracer = NzTracer() # we need the distance functions at the C layer cosmo.compute_distances() @@ -616,7 +731,7 @@ def NumberCountsTracer(cosmo, has_rsd, dndz, bias=None, if bias is not None: # Has density term # Kernel if kernel_d is None: - kernel_d = get_density_kernel(cosmo, dndz) + kernel_d = get_density_kernel(cosmo, dndz=dndz) # Transfer z_b, b = _check_array_params(bias, 'bias') # Reverse order for increasing a @@ -626,16 +741,16 @@ def NumberCountsTracer(cosmo, has_rsd, dndz, bias=None, if has_rsd: # Has RSDs # Kernel if kernel_d is None: - kernel_d = get_density_kernel(cosmo, dndz) + kernel_d = get_density_kernel(cosmo, dndz=dndz) # Transfer (growth rate) z_b, _ = _check_array_params(dndz, 'dndz') a_s = 1./(1+z_b[::-1]) - t_a = (a_s, -growth_rate(cosmo, a_s)) + t_a = (a_s, -cosmo.growth_rate(a_s)) tracer.add_tracer(cosmo, kernel=kernel_d, transfer_a=t_a, der_bessel=2) if mag_bias is not None: # Has magnification bias # Kernel - chi, w = get_lensing_kernel(cosmo, dndz, mag_bias=mag_bias, + chi, w = get_lensing_kernel(cosmo, dndz=dndz, mag_bias=mag_bias, n_chi=n_samples) # Multiply by -2 for magnification kernel_m = (chi, -2 * w) @@ -651,7 +766,8 @@ def NumberCountsTracer(cosmo, has_rsd, dndz, bias=None, return tracer -def WeakLensingTracer(cosmo, dndz, has_shear=True, ia_bias=None, +@warn_api +def WeakLensingTracer(cosmo, *, dndz, has_shear=True, ia_bias=None, use_A_ia=True, n_samples=256): """Specific `Tracer` associated to galaxy shape distortions including lensing shear and intrinsic alignments within the L-NLA model. @@ -676,7 +792,7 @@ def WeakLensingTracer(cosmo, dndz, has_shear=True, ia_bias=None, The kernel is quite smooth, so usually O(100) samples is enough. """ - tracer = Tracer() + tracer = NzTracer() # we need the distance functions at the C layer cosmo.compute_distances() @@ -687,7 +803,7 @@ def WeakLensingTracer(cosmo, dndz, has_shear=True, ia_bias=None, tracer._dndz = interp1d(z_n, n, bounds_error=False, fill_value=0) if has_shear: - kernel_l = get_lensing_kernel(cosmo, dndz, n_chi=n_samples) + kernel_l = get_lensing_kernel(cosmo, dndz=dndz, n_chi=n_samples) if (cosmo['sigma_0'] == 0): # GR case tracer.add_tracer(cosmo, kernel=kernel_l, @@ -699,10 +815,10 @@ def WeakLensingTracer(cosmo, dndz, has_shear=True, ia_bias=None, if ia_bias is not None: # Has intrinsic alignments z_a, tmp_a = _check_array_params(ia_bias, 'ia_bias') # Kernel - kernel_i = get_density_kernel(cosmo, dndz) + kernel_i = get_density_kernel(cosmo, dndz=dndz) if use_A_ia: # Normalize so that A_IA=1 - D = growth_factor(cosmo, 1./(1+z_a)) + D = cosmo.growth_factor(1./(1+z_a)) # Transfer # See Joachimi et al. (2011), arXiv: 1008.3491, Eq. 6. # and note that we use C_1= 5e-14 from arXiv:0705.0166 @@ -720,7 +836,8 @@ def WeakLensingTracer(cosmo, dndz, has_shear=True, ia_bias=None, return tracer -def CMBLensingTracer(cosmo, z_source, n_samples=100): +@warn_api +def CMBLensingTracer(cosmo, *, z_source, n_samples=100): """A Tracer for CMB lensing. Args: @@ -735,7 +852,7 @@ def CMBLensingTracer(cosmo, z_source, n_samples=100): # we need the distance functions at the C layer cosmo.compute_distances() - kernel = get_kappa_kernel(cosmo, z_source, n_samples) + kernel = get_kappa_kernel(cosmo, z_source=z_source, n_samples=n_samples) if (cosmo['sigma_0'] == 0): tracer.add_tracer(cosmo, kernel=kernel, der_bessel=-1, der_angles=1) else: @@ -744,7 +861,8 @@ def CMBLensingTracer(cosmo, z_source, n_samples=100): return tracer -def tSZTracer(cosmo, z_max=6., n_chi=1024): +@warn_api +def tSZTracer(cosmo, *, z_max=6., n_chi=1024): """Specific :class:`Tracer` associated with the thermal Sunyaev Zel'dovich Compton-y parameter. The radial kernel for this tracer is simply given by @@ -760,25 +878,19 @@ def tSZTracer(cosmo, z_max=6., n_chi=1024): Args: cosmo (:class:`~pyccl.core.Cosmology`): Cosmology object. - zmax (float): maximum redshift up to which we define the + z_max (float): maximum redshift up to which we define the kernel. n_chi (float): number of intervals in the radial comoving distance on which we sample the kernel. """ - tracer = Tracer() - - chi_max = comoving_radial_distance(cosmo, 1./(1+z_max)) - chi_arr = np.linspace(0, chi_max, n_chi) - a_arr = scale_factor_of_chi(cosmo, chi_arr) # This is \sigma_T / (m_e * c^2) prefac = 4.01710079e-06 - w_arr = prefac * a_arr - - tracer.add_tracer(cosmo, kernel=(chi_arr, w_arr)) - return tracer + return Tracer.from_zPower(cosmo, A=prefac, alpha=1, z_min=0., + z_max=z_max, n_chi=n_chi) -def CIBTracer(cosmo, z_min=0., z_max=6., n_chi=1024): +@warn_api +def CIBTracer(cosmo, *, z_min=0., z_max=6., n_chi=1024): """Specific :class:`Tracer` associated with the cosmic infrared background (CIB). The radial kernel for this tracer is simply @@ -795,23 +907,16 @@ def CIBTracer(cosmo, z_min=0., z_max=6., n_chi=1024): cosmo (:class:`~pyccl.core.Cosmology`): Cosmology object. zmin (float): minimum redshift down to which we define the kernel. - zmax (float): maximum redshift up to which we define the + z_max (float): maximum redshift up to which we define the kernel. n_chi (float): number of intervals in the radial comoving distance on which we sample the kernel. """ - tracer = Tracer() - - chi_max = comoving_radial_distance(cosmo, 1./(1+z_max)) - chi_min = comoving_radial_distance(cosmo, 1./(1+z_min)) - chi_arr = np.linspace(chi_min, chi_max, n_chi) - a_arr = scale_factor_of_chi(cosmo, chi_arr) - - tracer.add_tracer(cosmo, kernel=(chi_arr, a_arr)) - return tracer + return Tracer.from_zPower(cosmo, A=1.0, alpha=1, z_min=z_min, + z_max=z_max, n_chi=n_chi) -def ISWTracer(cosmo, z_max=6., n_chi=1024): +def ISWTracer(cosmo, *, z_max=6., n_chi=1024): """Specific :class:`Tracer` associated with the integrated Sachs-Wolfe effect (ISW). Useful when cross-correlating any low-redshift probe with the primary CMB anisotropies. The ISW contribution to the temperature @@ -831,20 +936,20 @@ def ISWTracer(cosmo, z_max=6., n_chi=1024): Args: cosmo (:class:`~pyccl.core.Cosmology`): Cosmology object. - zmax (float): maximum redshift up to which we define the + z_max (float): maximum redshift up to which we define the kernel. n_chi (float): number of intervals in the radial comoving distance on which we sample the kernel. """ tracer = Tracer() - chi_max = comoving_radial_distance(cosmo, 1./(1+z_max)) + chi_max = cosmo.comoving_radial_distance(1./(1+z_max)) chi = np.linspace(0, chi_max, n_chi) - a_arr = scale_factor_of_chi(cosmo, chi) + a_arr = cosmo.scale_factor_of_chi(chi) H0 = cosmo['h'] / physical_constants.CLIGHT_HMPC OM = cosmo['Omega_c']+cosmo['Omega_b'] - Ez = h_over_h0(cosmo, a_arr) - fz = growth_rate(cosmo, a_arr) + Ez = cosmo.h_over_h0(a_arr) + fz = cosmo.growth_rate(a_arr) w_arr = 3*cosmo['T_CMB']*H0**3*OM*Ez*chi**2*(1-fz) tracer.add_tracer(cosmo, kernel=(chi, w_arr), der_bessel=-1) diff --git a/readthedocs/source/notation_and_other_cosmological_conventions.rst b/readthedocs/source/notation_and_other_cosmological_conventions.rst index 9b658cbf0..19cd023fd 100644 --- a/readthedocs/source/notation_and_other_cosmological_conventions.rst +++ b/readthedocs/source/notation_and_other_cosmological_conventions.rst @@ -240,19 +240,23 @@ for a discussion of the values of these constants from different sources. basic physical constants + - AU: astronomical unit in units of m + - DAY: mean solar day in units of s + - YEAR: sidereal year in units of s - CLIGHT_HMPC: speed of light / H0 in units of Mpc/h - GNEWT: Newton's gravitational constant in units of m^3/Kg/s^2 - SOLAR_MASS: solar mass in units of kg - - MPC_TO_METER: conversion factor for Mpc to meters. - - PC_TO_METER: conversion factor for parsecs to meters. + - MPC_TO_METER: conversion factor for Mpc to m. + - PC_TO_METER: conversion factor for parsecs to m. - RHO_CRITICAL: critical density in units of M_sun/h / (Mpc/h)^3 - KBOLTZ: Boltzmann constant in units of J/K - STBOLTZ: Stefan-Boltzmann constant in units of kg/s^3 / K^4 - - HPLANCK: Planck's constant in units kg m^2 / s + - HPLANCK: Planck's constant in units of kg m^2 / s + - HBAR: reduced Planck's constant in units of kg m^2 / s - CLIGHT: speed of light in m/s - EV_IN_J: conversion factor between electron volts and Joules - - T_CMB: temperature of the CMB in K - - TNCDM: temperature of the cosmological neutrino background in K + - ELECTRON_CHARGE: another alias for EV_IN_J + - DELTA_C: linear density contrast of spherical collapse neutrino mass splittings diff --git a/readthedocs/source/understanding_the_python_c_interface.rst b/readthedocs/source/understanding_the_python_c_interface.rst index 769152965..156b29939 100644 --- a/readthedocs/source/understanding_the_python_c_interface.rst +++ b/readthedocs/source/understanding_the_python_c_interface.rst @@ -72,8 +72,9 @@ Example ``Python`` and ``C`` Code ================================= Putting together all of the information above, here is example code based on the -CCL ``pyccl.bcm`` module. Note that the code snippets here may be out of date -relative to their current form in the repository. +old CCL ``pyccl.bcm`` module. Note that these code snippets are outdated, since +the BCM module has now been fully migrated into python. Still, this provides a +useful example of how the ``C`` and ``Python`` interact in CCL. C Header File ``include/ccl_bcm.h`` diff --git a/src/ccl_background.c b/src/ccl_background.c index ba4bf4518..8c0f6ad8b 100644 --- a/src/ccl_background.c +++ b/src/ccl_background.c @@ -23,6 +23,7 @@ static double h_over_h0(double a, ccl_cosmology * cosmo, int *status) if ((cosmo->params.N_nu_mass)>1e-12) { Om_mass_nu = ccl_Omeganuh2( a, cosmo->params.N_nu_mass, cosmo->params.m_nu, cosmo->params.T_CMB, + cosmo->params.T_ncdm, status) / (cosmo->params.h) / (cosmo->params.h); } else { @@ -63,21 +64,15 @@ double ccl_omega_x(ccl_cosmology * cosmo, double a, ccl_species_x_label label, i // If massive neutrinos are present, compute the phase-space integral and // get OmegaNuh2. If not, set OmegaNuh2 to zero. double OmNuh2; - if ((cosmo->params.N_nu_mass) > 0.0001) { - // Call the massive neutrino density function just once at this redshift. - OmNuh2 = ccl_Omeganuh2(a, cosmo->params.N_nu_mass, cosmo->params.m_nu, - cosmo->params.T_CMB, status); - } - else { - OmNuh2 = 0.; - } - double hnorm = h_over_h0(a, cosmo, status); switch(label) { case ccl_species_crit_label : return 1.; case ccl_species_m_label : + // Call the massive neutrino density function just once at this redshift. + OmNuh2 = ccl_Omeganuh2(a, cosmo->params.N_nu_mass, cosmo->params.m_nu, + cosmo->params.T_CMB, cosmo->params.T_ncdm, status); return (cosmo->params.Omega_c + cosmo->params.Omega_b) / (a*a*a) / hnorm / hnorm + OmNuh2 / (cosmo->params.h) / (cosmo->params.h) / hnorm / hnorm; case ccl_species_l_label : @@ -92,6 +87,9 @@ double ccl_omega_x(ccl_cosmology * cosmo, double a, ccl_species_x_label label, i case ccl_species_ur_label : return cosmo->params.Omega_nu_rel / (a*a*a*a) / hnorm / hnorm; case ccl_species_nu_label : + // Call the massive neutrino density function just once at this redshift. + OmNuh2 = ccl_Omeganuh2(a, cosmo->params.N_nu_mass, cosmo->params.m_nu, + cosmo->params.T_CMB, cosmo->params.T_ncdm, status); return OmNuh2 / (cosmo->params.h) / (cosmo->params.h) / hnorm / hnorm; default: *status = CCL_ERROR_PARAMETERS; diff --git a/src/ccl_bcm.c b/src/ccl_bcm.c deleted file mode 100644 index b442cfe19..000000000 --- a/src/ccl_bcm.c +++ /dev/null @@ -1,118 +0,0 @@ -#include -#include -#include - -#include -#include - -#include "ccl.h" - -/* BCM correction */ -// See Schneider & Teyssier (2015) for details of the model. -double ccl_bcm_model_fka(ccl_cosmology * cosmo, double k, double a, int *status) { - double fkz; - double b0; - double bfunc, bfunc4; - double kg; - double gf,scomp; - double kh; - double z; - - z = 1./a - 1.; - kh = k / cosmo->params.h; - b0 = 0.105*cosmo->params.bcm_log10Mc - 1.27; - bfunc = b0 / (1. + pow(z/2.3, 2.5)); - bfunc4 = (1-bfunc) * (1-bfunc) * (1-bfunc) * (1-bfunc); - kg = 0.7 * bfunc4 * pow(cosmo->params.bcm_etab, -1.6); - gf = bfunc / (1 + pow(kh/kg, 3.)) + 1. - bfunc; //k in h/Mpc - scomp = 1 + (kh / cosmo->params.bcm_ks) * (kh / cosmo->params.bcm_ks); //k in h/Mpc - fkz = gf * scomp; - return fkz; -} - - -void ccl_bcm_correct(ccl_cosmology *cosmo, ccl_f2d_t *psp, int *status) -{ - size_t nk, na; - double *x, *z, *y2d=NULL; - - //Find lk array - if(psp->fk != NULL) { - nk = psp->fk->size; - x = psp->fk->x; - } - else { - nk = psp->fka->interp_object.xsize; - x = psp->fka->xarr; - } - - //Find a array - if(psp->fa != NULL) { - na = psp->fa->size; - z = psp->fa->x; - } - else { - na = psp->fka->interp_object.ysize; - z = psp->fka->yarr; - } - - //Allocate pka array - y2d = malloc(nk * na * sizeof(double)); - if (y2d == NULL) { - *status = CCL_ERROR_MEMORY; - ccl_cosmology_set_status_message(cosmo, - "ccl_bcm.c: ccl_bcm_correct(): " - "memory allocation\n"); - } - - if (*status == 0) { - for (int j = 0; jis_log) - y2d[j*nk + i] = log(pk*fbcm); - else - y2d[j*nk + i] = pk*fbcm; - } - } - } - } - - if (*status == 0) { - gsl_spline2d *fka = gsl_spline2d_alloc(gsl_interp2d_bicubic, nk, na); - - if (fka == NULL) { - *status = CCL_ERROR_MEMORY; - ccl_cosmology_set_status_message(cosmo, - "ccl_bcm.c: ccl_bcm_correct(): " - "memory allocation\n"); - } - if(*status == 0) { - int spstatus = gsl_spline2d_init(fka, x, z, y2d, nk, na); - if(spstatus) { - *status = CCL_ERROR_MEMORY; - ccl_cosmology_set_status_message(cosmo, - "ccl_bcm.c: ccl_bcm_correct(): " - "Error initializing spline\n"); - } - } - if(*status == 0) { - if(psp->fa != NULL) - gsl_spline_free(psp->fa); - if(psp->fk != NULL) - gsl_spline_free(psp->fk); - if(psp->fka != NULL) - gsl_spline2d_free(psp->fka); - psp->fka = fka; - psp->is_factorizable = 0; - psp->is_k_constant = 0; - psp->is_a_constant = 0; - } - else - gsl_spline2d_free(fka); - } - - free(y2d); -} diff --git a/src/ccl_core.c b/src/ccl_core.c index 48d79c41f..2cfe637f6 100644 --- a/src/ccl_core.c +++ b/src/ccl_core.c @@ -13,7 +13,6 @@ #include "ccl.h" -// // Macros for replacing relative paths #define EXPAND_STR(s) STRING(s) #define STRING(s) #s @@ -24,219 +23,10 @@ const ccl_configuration default_config = { ccl_tinker10, ccl_duffy2008, ccl_emu_strict}; -//Precision parameters -/** - * Default relative precision if not otherwise specified - */ -#define GSL_EPSREL 1E-4 - -/** - * Default number of iterations for integration and root-finding if not otherwise - * specified - */ -#define GSL_N_ITERATION 1000 - -/** - * Default number of Gauss-Kronrod points in QAG integration if not otherwise - * specified - */ -#define GSL_INTEGRATION_GAUSS_KRONROD_POINTS GSL_INTEG_GAUSS41 - -/** - * Relative precision in sigma_R calculations - */ -#define GSL_EPSREL_SIGMAR 1E-5 - -/** - * Relative precision in k_NL calculations - */ -#define GSL_EPSREL_KNL 1E-5 - -/** - * Relative precision in distance calculations - */ -#define GSL_EPSREL_DIST 1E-6 - -/** - * Relative precision in growth calculations - */ -#define GSL_EPSREL_GROWTH 1E-6 - -/** - * Relative precision in dNdz calculations - */ -#define GSL_EPSREL_DNDZ 1E-6 - -ccl_gsl_params ccl_user_gsl_params = { - GSL_N_ITERATION, // N_ITERATION - GSL_INTEGRATION_GAUSS_KRONROD_POINTS,// INTEGRATION_GAUSS_KRONROD_POINTS - GSL_EPSREL, // INTEGRATION_EPSREL - GSL_INTEGRATION_GAUSS_KRONROD_POINTS,// INTEGRATION_LIMBER_GAUSS_KRONROD_POINTS - GSL_EPSREL, // INTEGRATION_LIMBER_EPSREL - GSL_EPSREL_DIST, // INTEGRATION_DISTANCE_EPSREL - GSL_EPSREL_SIGMAR, // INTEGRATION_SIGMAR_EPSREL - GSL_EPSREL_KNL, // INTEGRATION_KNL_EPSREL - GSL_EPSREL, // ROOT_EPSREL - GSL_N_ITERATION, // ROOT_N_ITERATION - GSL_EPSREL_GROWTH, // ODE_GROWTH_EPSREL - 1E-6, // EPS_SCALEFAC_GROWTH - 1E7, // HM_MMIN - 1E17, // HM_MMAX - 0.0, // HM_EPSABS - 1E-4, // HM_EPSREL - 1000, // HM_LIMIT - GSL_INTEG_GAUSS41, // HM_INT_METHOD - true, // NZ_NORM_SPLINE_INTEGRATION - true // LENSING_KERNEL_SPLINE_INTEGRATION - }; - -#undef GSL_EPSREL -#undef GSL_N_ITERATION -#undef GSL_INTEGRATION_GAUSS_KRONROD_POINTS -#undef GSL_EPSREL_SIGMAR -#undef GSL_EPSREL_KNL -#undef GSL_EPSREL_DIST -#undef GSL_EPSREL_GROWTH -#undef GSL_EPSREL_DNDZ - - -ccl_spline_params ccl_user_spline_params = { - - // scale factor spline params - 250, // A_SPLINE_NA - 0.1, // A_SPLINE_MIN - 0.01, // A_SPLINE_MINLOG_PK - 0.1, // A_SPLINE_MIN_PK, - 0.01, // A_SPLINE_MINLOG_SM, - 0.1, // A_SPLINE_MIN_SM, - 1.0, // A_SPLINE_MAX, - 0.0001, // A_SPLINE_MINLOG, - 250, // A_SPLINE_NLOG, - - // mass splines - 0.025, // LOGM_SPLINE_DELTA - 50, // LOGM_SPLINE_NM - 6, // LOGM_SPLINE_MIN - 17, // LOGM_SPLINE_MAX - - // PS a and k spline - 13, // A_SPLINE_NA_SM - 6, // A_SPLINE_NLOG_SM - 40, // A_SPLINE_NA_PK - 11, // A_SPLINE_NLOG_PK - - // k-splines and integrals - 50, // K_MAX_SPLINE - 1E3, // K_MAX - 5E-5, // K_MIN - 0.025, // DLOGK_INTEGRATION - 5., // DCHI_INTEGRATION - 167, // N_K - 100000, // N_K_3DCOR - - // correlation function parameters - 0.01, // ELL_MIN_CORR - 60000, // ELL_MAX_CORR - 5000, // N_ELL_CORR - - //Spline types - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL -}; - - -ccl_physical_constants ccl_constants = { - /** - * Lightspeed / H0 in units of Mpc/h (from CODATA 2014) - */ - 2997.92458, - - /** - * Newton's gravitational constant in units of m^3/Kg/s^2 - */ - //6.6738e-11, /(from PDG 2013) in m^3/Kg/s^2 - //6.67428e-11, // CLASS VALUE - 6.67408e-11, // from CODATA 2014 - - /** - * Solar mass in units of kg (from GSL) - */ - //GSL_CONST_MKSA_SOLAR_MASS, - //1.9885e30, //(from PDG 2015) in Kg - 1.9884754153381438E+30, //from IAU 2015 - - /** - * Mpc to meters (from PDG 2016 and using M_PI) - */ - 3.085677581491367399198952281E+22, - - /** - * pc to meters (from PDG 2016 and using M_PI) - */ - 3.085677581491367399198952281E+16, - - /** - * Rho critical in units of M_sun/h / (Mpc/h)^3 - */ - ((3*100*100)/(8*M_PI*6.67408e-11)) * (1000*1000*3.085677581491367399198952281E+22/1.9884754153381438E+30), - - /** - * Boltzmann constant in units of J/K - */ - //GSL_CONST_MKSA_BOLTZMANN, - 1.38064852E-23, //from CODATA 2014 - - /** - * Stefan-Boltzmann constant in units of kg/s^3 / K^4 - */ - //GSL_CONST_MKSA_STEFAN_BOLTZMANN_CONSTANT, - 5.670367E-8, //from CODATA 2014 - - /** - * Planck's constant in units kg m^2 / s - */ - //GSL_CONST_MKSA_PLANCKS_CONSTANT_H, - 6.626070040E-34, //from CODATA 2014 - - /** - * The speed of light in m/s - */ - //GSL_CONST_MKSA_SPEED_OF_LIGHT, - 299792458.0, //from CODATA 2014 - - /** - * Electron volt to Joules convestion - */ - //GSL_CONST_MKSA_ELECTRON_VOLT, - 1.6021766208e-19, //from CODATA 2014 - - /** - * Temperature of the CMB in K - */ - 2.725, - //2.7255, // CLASS value - - /** - * T_ncdm, as taken from CLASS, explanatory.ini - */ - 0.71611, - - /** - * neutrino mass splitting differences - * See Lesgourgues and Pastor, 2012 for these values. - * Adv. High Energy Phys. 2012 (2012) 608515, - * arXiv:1212.6154, page 13 - */ - 7.62E-5, - 2.55E-3, - -2.43E-3 -}; - +// Initialize these extern consts - to be filled out by Python. +ccl_gsl_params ccl_user_gsl_params; +ccl_physical_constants ccl_constants; +ccl_spline_params ccl_user_spline_params; /* ------- ROUTINE: ccl_cosmology_create ------ @@ -306,15 +96,14 @@ TASK: fill parameters not set by ccl_parameters_create with some initial values Omega_g = (Omega_g*h^2)/h^2 is the radiation parameter; "g" is for photons, as in CLASS T_CMB: CMB temperature in Kelvin Omega_l: Lambda -A_s: amplitude of the primordial PS, enforced here to initially set to NaN -sigma8: variance in 8 Mpc/h spheres for normalization of matter PS, enforced here to initially set to NaN +A_s: amplitude of the primordial PS +sigma8: variance in 8 Mpc/h spheres for normalization of matter PS z_star: recombination redshift */ void ccl_parameters_fill_initial(ccl_parameters * params, int *status) { // Fixed radiation parameters // Omega_g * h**2 is known from T_CMB - params->T_CMB = ccl_constants.T_CMB; // kg / m^3 double rho_g = 4. * ccl_constants.STBOLTZ / pow(ccl_constants.CLIGHT, 3) * pow(params->T_CMB, 4); // kg / m^3 @@ -322,10 +111,9 @@ void ccl_parameters_fill_initial(ccl_parameters * params, int *status) ccl_constants.RHO_CRITICAL * ccl_constants.SOLAR_MASS/pow(ccl_constants.MPC_TO_METER, 3) * pow(params->h, 2); - params->Omega_g = rho_g/rho_crit; // Get the N_nu_rel from Neff and N_nu_mass - params->N_nu_rel = params->Neff - params->N_nu_mass * pow(ccl_constants.TNCDM, 4) / pow(4./11.,4./3.); + params->N_nu_rel = params->Neff - params->N_nu_mass * pow(params->T_ncdm, 4) / pow(4./11.,4./3.); // Temperature of the relativistic neutrinos in K double T_nu= (params->T_CMB) * pow(4./11.,1./3.); @@ -339,18 +127,27 @@ void ccl_parameters_fill_initial(ccl_parameters * params, int *status) // If non-relativistic neutrinos are present, calculate the phase_space integral. if((params->N_nu_mass)>0) { params->Omega_nu_mass = ccl_Omeganuh2( - 1.0, params->N_nu_mass, params->m_nu, params->T_CMB, status) / ((params->h)*(params->h)); + 1.0, params->N_nu_mass, params->m_nu, params->T_CMB, params->T_ncdm, + status) / ((params->h)*(params->h)); } else{ params->Omega_nu_mass = 0.; } params->Omega_m = params->Omega_b + params-> Omega_c + params->Omega_nu_mass; - params->Omega_l = 1.0 - params->Omega_m - params->Omega_g - params->Omega_nu_rel - params->Omega_k; - // Initially undetermined parameters - set to nan to trigger - // problems if they are mistakenly used. - if (isfinite(params->A_s)) {params->sigma8 = NAN;} - if (isfinite(params->sigma8)) {params->A_s = NAN;} + params->Omega_l = 1.0 - params->Omega_m - rho_g/rho_crit - params->Omega_nu_rel - params->Omega_k; + + if (isnan(params->Omega_g)) { + // No value passed for Omega_g + params->Omega_g = rho_g/rho_crit; + } + else { + // Omega_g was passed - modify Omega_l + double total = rho_g/rho_crit + params->Omega_l; + params->Omega_l = total - params->Omega_g; + } + + // NULL to NAN in case it is not set params->z_star = NAN; if(fabs(params->Omega_k)<1E-6) @@ -380,14 +177,18 @@ wa: Dark energy eq of state parameter, time variation H0: Hubble's constant in km/s/Mpc. h: Hubble's constant divided by (100 km/s/Mpc). A_s: amplitude of the primordial PS +sigma8: variance of matter density fluctuations at 8 Mpc/h n_s: index of the primordial PS +T_CMB: CMB temperature +Omega_g: radiation density parameter */ ccl_parameters ccl_parameters_create(double Omega_c, double Omega_b, double Omega_k, double Neff, double* mnu, int n_mnu, - double w0, double wa, double h, double norm_pk, - double n_s, double bcm_log10Mc, double bcm_etab, - double bcm_ks, double mu_0, double sigma_0, + double w0, double wa, double h, double A_s, double sigma8, + double n_s, double T_CMB, double Omega_g, double T_ncdm, + double bcm_log10Mc, double bcm_etab, double bcm_ks, + double mu_0, double sigma_0, double c1_mg, double c2_mg, double lambda_mg, int nz_mgrowth, double *zarr_mgrowth, double *dfarr_mgrowth, int *status) @@ -401,24 +202,30 @@ ccl_parameters ccl_parameters_create(double Omega_c, double Omega_b, double Omeg params.m_nu = NULL; params.z_mgrowth=NULL; params.df_mgrowth=NULL; - params.sigma8 = NAN; - params.A_s = NAN; - params.Omega_c = Omega_c; - params.Omega_b = Omega_b; - params.Omega_k = Omega_k; - params.Neff = Neff; params.m_nu = malloc(n_mnu*sizeof(double)); + + // Neutrinos params.sum_nu_masses = 0.; for(int i = 0; idata); free(cosmo); } - -int ccl_get_pk_spline_na(ccl_cosmology *cosmo) { - return cosmo->spline_params.A_SPLINE_NA_PK + cosmo->spline_params.A_SPLINE_NLOG_PK - 1; -} - -void ccl_get_pk_spline_a_array(ccl_cosmology *cosmo,int ndout,double* doutput,int *status) { - double *d = NULL; - if (ndout != ccl_get_pk_spline_na(cosmo)) - *status = CCL_ERROR_INCONSISTENT; - if (*status == 0) { - d = ccl_linlog_spacing(cosmo->spline_params.A_SPLINE_MINLOG_PK, - cosmo->spline_params.A_SPLINE_MIN_PK, - cosmo->spline_params.A_SPLINE_MAX, - cosmo->spline_params.A_SPLINE_NLOG_PK, - cosmo->spline_params.A_SPLINE_NA_PK); - if (d == NULL) - *status = CCL_ERROR_MEMORY; - } - if(*status==0) - memcpy(doutput, d, ndout*sizeof(double)); - - free(d); -} - -void ccl_get_pk_spline_a_array_from_params(ccl_spline_params *spline_params, - int ndout, double *doutput, int *status) { - double *d = NULL; - if (*status == 0) { - d = ccl_linlog_spacing(spline_params->A_SPLINE_MINLOG_PK, - spline_params->A_SPLINE_MIN_PK, - spline_params->A_SPLINE_MAX, - spline_params->A_SPLINE_NLOG_PK, - spline_params->A_SPLINE_NA_PK); - if (d == NULL) - *status = CCL_ERROR_MEMORY; - } - if(*status==0) - memcpy(doutput, d, ndout*sizeof(double)); - - free(d); -} - -int ccl_get_pk_spline_nk(ccl_cosmology *cosmo) { - double ndecades = log10(cosmo->spline_params.K_MAX) - log10(cosmo->spline_params.K_MIN); - return (int)ceil(ndecades*cosmo->spline_params.N_K); -} - -void ccl_get_pk_spline_lk_array(ccl_cosmology *cosmo,int ndout,double* doutput,int *status) { - double *d = NULL; - if (ndout != ccl_get_pk_spline_nk(cosmo)) - *status = CCL_ERROR_INCONSISTENT; - if (*status == 0) { - d = ccl_log_spacing(cosmo->spline_params.K_MIN, cosmo->spline_params.K_MAX, ndout); - if (d == NULL) - *status = CCL_ERROR_MEMORY; - } - if (*status == 0) { - for(int ii=0; ii < ndout; ii++) - doutput[ii] = log(d[ii]); - } - free(d); -} - -void ccl_get_pk_spline_lk_array_from_params(ccl_spline_params *spline_params, - int ndout, double *doutput, int *status) { - double *d = NULL; - if (*status == 0) { - d = ccl_log_spacing(spline_params->K_MIN, spline_params->K_MAX, ndout); - if (d == NULL) - *status = CCL_ERROR_MEMORY; - } - if (*status == 0) { - for(int ii=0; ii < ndout; ii++) - doutput[ii] = log(d[ii]); - } - free(d); -} diff --git a/src/ccl_halofit.c b/src/ccl_halofit.c index c5e8ea44c..e25a7e270 100644 --- a/src/ccl_halofit.c +++ b/src/ccl_halofit.c @@ -43,7 +43,6 @@ static ccl_cosmology *create_w0eff_cosmo(double w0eff, ccl_cosmology *cosmo, int // create a cosmology with the same parameters as the input except w0-wa. Instead // the cosmology is created with w0 = w0eff. ccl_parameters params_w0eff; - double norm_pk; double mnu[3]; int i; @@ -52,16 +51,12 @@ static ccl_cosmology *create_w0eff_cosmo(double w0eff, ccl_cosmology *cosmo, int for(i=0; iparams.N_nu_mass; ++i) mnu[i] = cosmo->params.m_nu[i]; - if (isnan(cosmo->params.A_s)) - norm_pk = cosmo->params.sigma8; - else - norm_pk = cosmo->params.A_s; - params_w0eff = ccl_parameters_create( cosmo->params.Omega_c, cosmo->params.Omega_b, cosmo->params.Omega_k, cosmo->params.Neff, mnu, cosmo->params.N_nu_mass, - w0eff, 0, cosmo->params.h, norm_pk, - cosmo->params.n_s, cosmo->params.bcm_log10Mc, cosmo->params.bcm_etab, + w0eff, 0, cosmo->params.h, cosmo->params.A_s, cosmo->params.sigma8, + cosmo->params.n_s, cosmo->params.T_CMB, cosmo->params.Omega_g, cosmo->params.T_ncdm, + cosmo->params.bcm_log10Mc, cosmo->params.bcm_etab, cosmo->params.bcm_ks, cosmo->params.mu_0, cosmo->params.sigma_0, cosmo->params.c1_mg, cosmo->params.c2_mg, cosmo->params.lambda_mg, cosmo->params.nz_mgrowth, diff --git a/src/ccl_massfunc.c b/src/ccl_massfunc.c index 47fac82ba..4b4e86e65 100644 --- a/src/ccl_massfunc.c +++ b/src/ccl_massfunc.c @@ -69,6 +69,7 @@ void ccl_cosmology_compute_sigma(ccl_cosmology *cosmo, ccl_f2d_t *psp, int *stat // create linearly-spaced values of log-mass. m = ccl_linear_spacing(cosmo->spline_params.LOGM_SPLINE_MIN, cosmo->spline_params.LOGM_SPLINE_MAX, nm); + if (m == NULL || (fabs(m[0]-cosmo->spline_params.LOGM_SPLINE_MIN)>1e-5) || (fabs(m[nm-1]-cosmo->spline_params.LOGM_SPLINE_MAX)>1e-5) || @@ -140,7 +141,6 @@ void ccl_cosmology_compute_sigma(ccl_cosmology *cosmo, ccl_f2d_t *psp, int *stat "error allocating 2D spline\n"); } } - if(*status == 0) { int s2dstatus=gsl_spline2d_init(lsM, m, aa, y, nm, na); if (s2dstatus) { @@ -210,7 +210,7 @@ double ccl_dlnsigM_dlogM(ccl_cosmology *cosmo, double log_halomass, double a, in int gslstatus = gsl_spline2d_eval_deriv_x_e(cosmo->data.logsigma, log_halomass, a, NULL, NULL, &dlsdlgm); - if(gslstatus) { + if(gslstatus) { ccl_raise_gsl_warning(gslstatus, "ccl_massfunc.c: ccl_dlnsigM_dlogM():"); *status |= gslstatus; } diff --git a/src/ccl_musigma.c b/src/ccl_musigma.c index 7550859f9..44cf3885c 100644 --- a/src/ccl_musigma.c +++ b/src/ccl_musigma.c @@ -118,7 +118,6 @@ void ccl_rescale_musigma_s8(ccl_cosmology* cosmo, ccl_f2d_t *psp, // current one but with mu_0 and Sigma_0=0, for scaling P(k) // Get a list of the three neutrino masses already calculated - double norm_pk; double mnu_list[3] = {0, 0, 0}; ccl_parameters params_GR; params_GR.m_nu = NULL; @@ -129,13 +128,7 @@ void ccl_rescale_musigma_s8(ccl_cosmology* cosmo, ccl_f2d_t *psp, for (int i=0; i< cosmo->params.N_nu_mass; i=i+1) mnu_list[i] = cosmo->params.m_nu[i]; - if (isfinite(cosmo->params.A_s)) { - norm_pk = cosmo->params.A_s; - } - else if (isfinite(cosmo->params.sigma8)) { - norm_pk = cosmo->params.sigma8; - } - else { + if (isnan(cosmo->params.A_s) && isnan(cosmo->params.sigma8)) { *status = CCL_ERROR_PARAMETERS; ccl_cosmology_set_status_message(cosmo, "ccl_musigma.c: ccl_rescale_musigma_s8(): " @@ -147,7 +140,8 @@ void ccl_rescale_musigma_s8(ccl_cosmology* cosmo, ccl_f2d_t *psp, cosmo->params.Omega_c, cosmo->params.Omega_b, cosmo->params.Omega_k, cosmo->params.Neff, mnu_list, cosmo->params.N_nu_mass, cosmo->params.w0, cosmo->params.wa, cosmo->params.h, - norm_pk, cosmo->params.n_s, + cosmo->params.A_s, cosmo->params.sigma8, cosmo->params.n_s, + cosmo->params.T_CMB, cosmo->params.Omega_g, cosmo->params.T_ncdm, cosmo->params.bcm_log10Mc, cosmo->params.bcm_etab, cosmo->params.bcm_ks, 0., 0., 1., 1., 0.,cosmo->params.nz_mgrowth, cosmo->params.z_mgrowth, cosmo->params.df_mgrowth, status); diff --git a/src/ccl_neutrinos.c b/src/ccl_neutrinos.c index 8ee132481..c5463d1c5 100644 --- a/src/ccl_neutrinos.c +++ b/src/ccl_neutrinos.c @@ -156,11 +156,13 @@ static double nu_phasespace_intg(double mnuOT, int* status) /* -------- ROUTINE: Omeganuh2 --------- INPUTS: a: scale factor, Nnumass: number of massive neutrino species, - mnu: total mass in eV of neutrinos, T_CMB: CMB temperature, status: pointer to status integer. + mnu: total mass in eV of neutrinos, T_CMB: CMB temperature, + T_ncdm: non-CDM temperature in units of photon temperature, + status: pointer to status integer. TASK: Compute Omeganu * h^2 as a function of time. !! To all practical purposes, Neff is simply N_nu_mass !! */ -double ccl_Omeganuh2(double a, int N_nu_mass, double* mnu, double T_CMB, int* status) { +double ccl_Omeganuh2(double a, int N_nu_mass, double* mnu, double T_CMB, double T_ncdm, int* status) { double Tnu, a4, prefix_massless, OmNuh2; double Tnu_eff, mnuOT, intval, prefix_massive; @@ -169,24 +171,24 @@ double ccl_Omeganuh2(double a, int N_nu_mass, double* mnu, double T_CMB, int* st Tnu = T_CMB*pow(4./11.,1./3.); a4 = a*a*a*a; - + // Tnu_eff is used in the massive case because CLASS uses an effective // temperature of nonLCDM components to match to mnu / Omeganu =93.14eV. Tnu_eff = T_ncdm * T_CMB = 0.71611 * T_CMB - Tnu_eff = Tnu * ccl_constants.TNCDM / (pow(4./11.,1./3.)); + Tnu_eff = Tnu * T_ncdm / (pow(4./11.,1./3.)); // Define the prefix using the effective temperature (to get mnu / Omega = 93.14 eV) for the massive case: prefix_massive = NU_CONST * Tnu_eff * Tnu_eff * Tnu_eff * Tnu_eff; OmNuh2 = 0.; // Initialize to 0 - we add to this for each massive neutrino species. for(int i=0; i < N_nu_mass; i++) { - + // Check whether this species is effectively massless // In this case, invoke the analytic massless limit: if (mnu[i] < 0.00017) { // Limit taken from Lesgourges et al. 2012 - prefix_massless = NU_CONST * Tnu * Tnu * Tnu * Tnu; - OmNuh2 = N_nu_mass*prefix_massless*7./8./a4 + OmNuh2; - } else { - // For the true massive case: + prefix_massless = NU_CONST * Tnu * Tnu * Tnu * Tnu; + OmNuh2 = N_nu_mass*prefix_massless*7./8./a4 + OmNuh2; + } else { + // For the true massive case: // Get mass over T (mass (eV) / ((kb eV/s/K) Tnu_eff (K)) // This returns the density normalized so that we get nuh2 at a=0 mnuOT = mnu[i] / (Tnu_eff/a) * (ccl_constants.EV_IN_J / (ccl_constants.KBOLTZ)); @@ -203,12 +205,11 @@ double ccl_Omeganuh2(double a, int N_nu_mass, double* mnu, double T_CMB, int* st /* -------- ROUTINE: Omeganuh2_to_Mnu --------- INPUTS: OmNuh2: neutrino mass density today Omeganu * h^2, label: how you want to split up the masses, see ccl_neutrinos.h for options, - T_CMB: CMB temperature, status: pointer to status integer. + status: pointer to status integer. TASK: Given Omeganuh2 today, the method of splitting into masses, and the temperature of the CMB, output a pointer to the array of neutrino masses (may be length 1 if label asks for sum) */ -double* ccl_nu_masses(double OmNuh2, ccl_neutrino_mass_splits mass_split, - double T_CMB, int* status) { +double* ccl_nu_masses(double OmNuh2, ccl_neutrino_mass_splits mass_split, int* status) { double sumnu; double *mnu = NULL; diff --git a/src/ccl_power.c b/src/ccl_power.c index 81be3adf3..5072ee529 100644 --- a/src/ccl_power.c +++ b/src/ccl_power.c @@ -226,7 +226,7 @@ ccl_f2d_t *ccl_compute_power_emu(ccl_cosmology * cosmo, int * status) double mnu_eq[3] = {cosmo->params.sum_nu_masses / 3., cosmo->params.sum_nu_masses / 3., cosmo->params.sum_nu_masses / 3.}; - Omeganuh2_eq = ccl_Omeganuh2(1.0, 3, mnu_eq, cosmo->params.T_CMB, status); + Omeganuh2_eq = ccl_Omeganuh2(1.0, 3, mnu_eq, cosmo->params.T_CMB, cosmo->params.T_ncdm, status); } } else { if(fabs(cosmo->params.N_nu_rel - 3.04)>1.e-6){