lsstdesc-crow 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crow/__init__.py +11 -0
- crow/cluster_modules/_clmm_patches.py +174 -0
- crow/cluster_modules/abundance.py +87 -0
- crow/cluster_modules/completeness_models.py +75 -0
- crow/cluster_modules/kernel.py +37 -0
- crow/cluster_modules/mass_proxy/__init__.py +8 -0
- crow/cluster_modules/mass_proxy/gaussian_protocol.py +79 -0
- crow/cluster_modules/mass_proxy/murata.py +109 -0
- crow/cluster_modules/parameters.py +56 -0
- crow/cluster_modules/purity_models.py +76 -0
- crow/cluster_modules/shear_profile.py +467 -0
- crow/integrator/__init__.py +1 -0
- crow/integrator/integrator.py +31 -0
- crow/integrator/numcosmo_integrator.py +94 -0
- crow/integrator/scipy_integrator.py +52 -0
- crow/properties.py +19 -0
- crow/recipes/__init__.py +1 -0
- crow/recipes/binned_exact.py +349 -0
- crow/recipes/binned_grid.py +404 -0
- crow/recipes/binned_parent.py +112 -0
- lsstdesc_crow-1.0.2.dist-info/METADATA +29 -0
- lsstdesc_crow-1.0.2.dist-info/RECORD +25 -0
- lsstdesc_crow-1.0.2.dist-info/WHEEL +5 -0
- lsstdesc_crow-1.0.2.dist-info/licenses/LICENSE +26 -0
- lsstdesc_crow-1.0.2.dist-info/top_level.txt +1 -0
crow/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Module that contains the cluster model classes."""
|
|
2
|
+
|
|
3
|
+
from .cluster_modules import completeness_models, kernel, mass_proxy, purity_models
|
|
4
|
+
from .cluster_modules.abundance import ClusterAbundance
|
|
5
|
+
from .cluster_modules.shear_profile import ClusterShearProfile
|
|
6
|
+
from .properties import ClusterProperty
|
|
7
|
+
from .recipes.binned_exact import ExactBinnedClusterRecipe
|
|
8
|
+
from .recipes.binned_grid import GridBinnedClusterRecipe
|
|
9
|
+
from .recipes.binned_parent import BinnedClusterRecipe
|
|
10
|
+
|
|
11
|
+
__version__ = "1.0.2"
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.integrate import quad, simpson
|
|
3
|
+
from scipy.interpolate import splev, splrep
|
|
4
|
+
from scipy.special import gamma, gammainc, jv
|
|
5
|
+
|
|
6
|
+
from crow.integrator.numcosmo_integrator import NumCosmoIntegrator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def numcosmo_miscentered_mean_surface_density( # pragma: no cover
|
|
10
|
+
r_proj, r_mis, integrand, norm, aux_args, extra_integral
|
|
11
|
+
):
|
|
12
|
+
"""
|
|
13
|
+
NumCosmo replacement for `integrate_azimuthially_miscentered_mean_surface_density`.
|
|
14
|
+
|
|
15
|
+
Integrates azimuthally and radially for the mean surface mass density kernel.
|
|
16
|
+
"""
|
|
17
|
+
integrator = NumCosmoIntegrator(
|
|
18
|
+
relative_tolerance=1e-6,
|
|
19
|
+
absolute_tolerance=1e-3,
|
|
20
|
+
)
|
|
21
|
+
integrand = np.vectorize(integrand)
|
|
22
|
+
r_proj = np.atleast_1d(r_proj)
|
|
23
|
+
r_lower = np.full_like(r_proj, 1e-6)
|
|
24
|
+
r_lower[1:] = r_proj[:-1]
|
|
25
|
+
|
|
26
|
+
results = []
|
|
27
|
+
args = (r_mis, *aux_args)
|
|
28
|
+
integrator.extra_args = np.array(args)
|
|
29
|
+
for r_low, r_high in zip(r_lower, r_proj):
|
|
30
|
+
if extra_integral:
|
|
31
|
+
integrator.integral_bounds = [
|
|
32
|
+
(r_low, r_high),
|
|
33
|
+
(1.0e-6, np.pi),
|
|
34
|
+
(0.0, np.inf),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
def integrand_numcosmo(int_args, extra_args):
|
|
38
|
+
r_local = int_args[:, 0]
|
|
39
|
+
theta = int_args[:, 1]
|
|
40
|
+
extra = int_args[:, 2]
|
|
41
|
+
return integrand(theta, r_local, extra, *extra_args)
|
|
42
|
+
|
|
43
|
+
else:
|
|
44
|
+
integrator.integral_bounds = [(r_low, r_high), (1.0e-6, np.pi)]
|
|
45
|
+
|
|
46
|
+
def integrand_numcosmo(int_args, extra_args):
|
|
47
|
+
r_local = int_args[:, 0]
|
|
48
|
+
theta = int_args[:, 1]
|
|
49
|
+
return integrand(theta, r_local, *extra_args)
|
|
50
|
+
|
|
51
|
+
res = integrator.integrate(integrand_numcosmo)
|
|
52
|
+
results.append(res)
|
|
53
|
+
|
|
54
|
+
results = np.array(results)
|
|
55
|
+
mean_surface_density = np.cumsum(results) * norm * 2 / np.pi / r_proj**2
|
|
56
|
+
if not np.iterable(r_proj):
|
|
57
|
+
return res[0] * norm * 2 / np.pi / r_proj**2
|
|
58
|
+
return mean_surface_density
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _eval_2halo_term_generic_orig( # pragma: no cover
|
|
62
|
+
self,
|
|
63
|
+
sph_harm_ord,
|
|
64
|
+
r_proj,
|
|
65
|
+
z_cl,
|
|
66
|
+
halobias=1.0,
|
|
67
|
+
logkbounds=(-5, 5),
|
|
68
|
+
ksteps=1000,
|
|
69
|
+
loglbounds=(0, 6),
|
|
70
|
+
lsteps=500,
|
|
71
|
+
):
|
|
72
|
+
"""eval excess surface density from the 2-halo term (original)"""
|
|
73
|
+
# pylint: disable=protected-access
|
|
74
|
+
da = self.cosmo.eval_da(z_cl)
|
|
75
|
+
rho_m = self.cosmo._get_rho_m(z_cl)
|
|
76
|
+
|
|
77
|
+
k_values = np.logspace(logkbounds[0], logkbounds[1], ksteps)
|
|
78
|
+
pk_values = self.cosmo._eval_linear_matter_powerspectrum(k_values, z_cl)
|
|
79
|
+
interp_pk = splrep(k_values, pk_values)
|
|
80
|
+
theta = r_proj / da
|
|
81
|
+
|
|
82
|
+
# calculate integral, units [Mpc]**-3
|
|
83
|
+
def __integrand__(l_value, theta):
|
|
84
|
+
k_value = l_value / ((1 + z_cl) * da)
|
|
85
|
+
return l_value * jv(sph_harm_ord, l_value * theta) * splev(k_value, interp_pk)
|
|
86
|
+
|
|
87
|
+
l_values = np.logspace(loglbounds[0], loglbounds[1], lsteps)
|
|
88
|
+
kernel = np.array([simpson(__integrand__(l_values, t), x=l_values) for t in theta])
|
|
89
|
+
return halobias * kernel * rho_m / (2 * np.pi * (1 + z_cl) ** 3 * da**2)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _eval_2halo_term_generic_new( # pragma: no cover
|
|
93
|
+
####################################################################################
|
|
94
|
+
# NOTE: This function is just a small optimization of the one implemented on CLMM #
|
|
95
|
+
# here just to benchmark the difference due the restructuration of the integration #
|
|
96
|
+
####################################################################################
|
|
97
|
+
self,
|
|
98
|
+
sph_harm_ord,
|
|
99
|
+
r_proj,
|
|
100
|
+
z_cl,
|
|
101
|
+
halobias=1.0,
|
|
102
|
+
logkbounds=(-5, 5),
|
|
103
|
+
ksteps=1000,
|
|
104
|
+
loglbounds=(0, 6),
|
|
105
|
+
lsteps=500,
|
|
106
|
+
):
|
|
107
|
+
"""eval excess surface density from the 2-halo term (updated integration)"""
|
|
108
|
+
# pylint: disable=protected-access
|
|
109
|
+
da = self.cosmo.eval_da(z_cl)
|
|
110
|
+
rho_m = self.cosmo._get_rho_m(z_cl)
|
|
111
|
+
theta = r_proj / da
|
|
112
|
+
|
|
113
|
+
# interp pk
|
|
114
|
+
_k_values = np.logspace(logkbounds[0], logkbounds[1], ksteps)
|
|
115
|
+
interp_pk = splrep(
|
|
116
|
+
_k_values, self.cosmo._eval_linear_matter_powerspectrum(_k_values, z_cl)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# integrate
|
|
120
|
+
l_values = np.logspace(loglbounds[0], loglbounds[1], lsteps)
|
|
121
|
+
kernel = simpson(
|
|
122
|
+
(
|
|
123
|
+
l_values
|
|
124
|
+
* jv(sph_harm_ord, l_values * theta)
|
|
125
|
+
* splev(l_values / ((1 + z_cl) * da), interp_pk)
|
|
126
|
+
),
|
|
127
|
+
x=l_values,
|
|
128
|
+
)
|
|
129
|
+
return [halobias * kernel * rho_m / (2 * np.pi * (1 + z_cl) ** 3 * da**2)]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _eval_2halo_term_generic_vec( # pragma: no cover
|
|
133
|
+
self,
|
|
134
|
+
sph_harm_ord,
|
|
135
|
+
r_proj,
|
|
136
|
+
z_cl,
|
|
137
|
+
halobias=1.0,
|
|
138
|
+
logkbounds=(-5, 5),
|
|
139
|
+
ksteps=1000,
|
|
140
|
+
loglbounds=(0, 6),
|
|
141
|
+
lsteps=500,
|
|
142
|
+
):
|
|
143
|
+
"""eval excess surface density from the 2-halo term (vectorized)"""
|
|
144
|
+
z_cl = np.atleast_1d(z_cl)
|
|
145
|
+
# pylint: disable=protected-access
|
|
146
|
+
da = self.cosmo.eval_da(z_cl)
|
|
147
|
+
rho_m = self.cosmo._get_rho_m(z_cl)
|
|
148
|
+
|
|
149
|
+
# (n_z, n_r)
|
|
150
|
+
theta = (r_proj / da[None, :]).T
|
|
151
|
+
|
|
152
|
+
# calculate integral, units [Mpc]**-3
|
|
153
|
+
l_values = np.logspace(loglbounds[0], loglbounds[1], lsteps)
|
|
154
|
+
|
|
155
|
+
# (n_l, n_z)
|
|
156
|
+
k_values = l_values[:, np.newaxis] / ((1 + z_cl) * da)
|
|
157
|
+
pk_values = np.zeros(k_values.shape)
|
|
158
|
+
for i in range(z_cl.size):
|
|
159
|
+
pk_values[:, i] = self.cosmo._eval_linear_matter_powerspectrum(
|
|
160
|
+
k_values[:, i], z_cl[i]
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# (n_l, n_z, n_r)
|
|
164
|
+
jv_values = jv(sph_harm_ord, l_values[:, None, None] * theta[None, :, :])
|
|
165
|
+
kernel = l_values[:, None, None] * pk_values[:, :, None] * jv_values
|
|
166
|
+
|
|
167
|
+
# (n_z, n_r)
|
|
168
|
+
integ = simpson(kernel, x=l_values, axis=0)
|
|
169
|
+
|
|
170
|
+
# (n_z, n_r)
|
|
171
|
+
out = halobias * integ * (rho_m / (2 * np.pi * (1 + z_cl) ** 3 * da**2))[:, None]
|
|
172
|
+
|
|
173
|
+
# (n_r, n_z)
|
|
174
|
+
return out.T
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""The module responsible for building the cluster abundance calculation.
|
|
2
|
+
|
|
3
|
+
The galaxy cluster abundance integral is a combination of both theoretical
|
|
4
|
+
and phenomenological predictions. This module contains the classes and
|
|
5
|
+
functions that produce those predictions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import numpy.typing as npt
|
|
10
|
+
import pyccl
|
|
11
|
+
import pyccl.background as bkg
|
|
12
|
+
from pyccl.cosmology import Cosmology
|
|
13
|
+
|
|
14
|
+
from .parameters import Parameters
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ClusterAbundance:
|
|
18
|
+
"""The class that calculates the predicted number counts of galaxy clusters.
|
|
19
|
+
|
|
20
|
+
The abundance is a function of a specific cosmology, a mass and redshift range,
|
|
21
|
+
an area on the sky, a halo mass function, as well as multiple kernels, where
|
|
22
|
+
each kernel represents a different distribution involved in the final cluster
|
|
23
|
+
abundance integrand.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def cosmo(self) -> Cosmology | None:
|
|
28
|
+
"""The cosmology used to predict the cluster number count."""
|
|
29
|
+
return self._cosmo
|
|
30
|
+
|
|
31
|
+
@cosmo.setter
|
|
32
|
+
def cosmo(self, cosmo: Cosmology) -> None:
|
|
33
|
+
"""Update the cluster abundance calculation with a new cosmology."""
|
|
34
|
+
self._cosmo = cosmo
|
|
35
|
+
self._hmf_cache: dict[tuple[float, float], float] = {}
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
cosmo: Cosmology,
|
|
40
|
+
halo_mass_function: pyccl.halos.MassFunc,
|
|
41
|
+
) -> None:
|
|
42
|
+
super().__init__()
|
|
43
|
+
self.cosmo = cosmo
|
|
44
|
+
self.halo_mass_function = halo_mass_function
|
|
45
|
+
self.parameters = Parameters({})
|
|
46
|
+
|
|
47
|
+
def comoving_volume(
|
|
48
|
+
self, z: npt.NDArray[np.float64], sky_area: float = 0
|
|
49
|
+
) -> npt.NDArray[np.float64]:
|
|
50
|
+
"""The differential comoving volume given area sky_area at redshift z.
|
|
51
|
+
|
|
52
|
+
:param sky_area: The area of the survey on the sky in square degrees.
|
|
53
|
+
"""
|
|
54
|
+
assert self.cosmo is not None
|
|
55
|
+
scale_factor = 1.0 / (1.0 + z)
|
|
56
|
+
angular_diam_dist = bkg.angular_diameter_distance(self.cosmo, scale_factor)
|
|
57
|
+
h_over_h0 = bkg.h_over_h0(self.cosmo, scale_factor)
|
|
58
|
+
|
|
59
|
+
dV = (
|
|
60
|
+
pyccl.physical_constants.CLIGHT_HMPC
|
|
61
|
+
* (angular_diam_dist**2)
|
|
62
|
+
* ((1.0 + z) ** 2)
|
|
63
|
+
/ (self.cosmo["h"] * h_over_h0)
|
|
64
|
+
)
|
|
65
|
+
assert isinstance(dV, np.ndarray)
|
|
66
|
+
|
|
67
|
+
sky_area_rad = sky_area * (np.pi / 180.0) ** 2
|
|
68
|
+
|
|
69
|
+
return np.array(dV * sky_area_rad, dtype=np.float64)
|
|
70
|
+
|
|
71
|
+
def mass_function(
|
|
72
|
+
self,
|
|
73
|
+
log_mass: npt.NDArray[np.float64],
|
|
74
|
+
z: npt.NDArray[np.float64],
|
|
75
|
+
) -> npt.NDArray[np.float64]:
|
|
76
|
+
"""The mass function at z and mass."""
|
|
77
|
+
scale_factor = 1.0 / (1.0 + z)
|
|
78
|
+
return_vals = []
|
|
79
|
+
|
|
80
|
+
for logm, a in zip(log_mass.astype(float), scale_factor.astype(float)):
|
|
81
|
+
val = self._hmf_cache.get((logm, a))
|
|
82
|
+
if val is None:
|
|
83
|
+
val = self.halo_mass_function(self.cosmo, 10**logm, a)
|
|
84
|
+
self._hmf_cache[(logm, a)] = val
|
|
85
|
+
return_vals.append(val)
|
|
86
|
+
|
|
87
|
+
return np.asarray(return_vals, dtype=np.float64)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""The cluster completeness module.
|
|
2
|
+
|
|
3
|
+
This module holds the classes that define the kernels that can be included
|
|
4
|
+
in the cluster abundance integrand.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import numpy.typing as npt
|
|
9
|
+
|
|
10
|
+
from .parameters import Parameters
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Completeness:
|
|
14
|
+
"""The completeness kernel for the numcosmo simulated survey.
|
|
15
|
+
|
|
16
|
+
This kernel will affect the integrand by accounting for the incompleteness
|
|
17
|
+
of a cluster selection.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
def distribution(
|
|
24
|
+
self,
|
|
25
|
+
log_mass: npt.NDArray[np.float64],
|
|
26
|
+
z: npt.NDArray[np.float64],
|
|
27
|
+
) -> npt.NDArray[np.float64]:
|
|
28
|
+
"""Evaluates and returns the completeness contribution to the integrand."""
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
REDMAPPER_DEFAULT_PARAMETERS = {
|
|
33
|
+
"a_n": 0.38,
|
|
34
|
+
"b_n": 1.2634,
|
|
35
|
+
"a_logm_piv": 13.31,
|
|
36
|
+
"b_logm_piv": 0.2025,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CompletenessAguena16(Completeness):
|
|
41
|
+
"""The completeness kernel for the numcosmo simulated survey.
|
|
42
|
+
|
|
43
|
+
This kernel will affect the integrand by accounting for the incompleteness
|
|
44
|
+
of a cluster selection.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
):
|
|
50
|
+
self.parameters = Parameters({**REDMAPPER_DEFAULT_PARAMETERS})
|
|
51
|
+
|
|
52
|
+
def _mpiv(self, z: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
|
|
53
|
+
log_mpiv = self.parameters["a_logm_piv"] + self.parameters["b_logm_piv"] * (
|
|
54
|
+
1.0 + z
|
|
55
|
+
)
|
|
56
|
+
mpiv = 10.0**log_mpiv
|
|
57
|
+
return mpiv.astype(np.float64)
|
|
58
|
+
|
|
59
|
+
def _nc(self, z: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
|
|
60
|
+
nc = self.parameters["a_n"] + self.parameters["b_n"] * (1.0 + z)
|
|
61
|
+
assert isinstance(nc, np.ndarray)
|
|
62
|
+
return nc
|
|
63
|
+
|
|
64
|
+
def distribution(
|
|
65
|
+
self,
|
|
66
|
+
log_mass: npt.NDArray[np.float64],
|
|
67
|
+
z: npt.NDArray[np.float64],
|
|
68
|
+
) -> npt.NDArray[np.float64]:
|
|
69
|
+
"""Evaluates and returns the completeness contribution to the integrand."""
|
|
70
|
+
|
|
71
|
+
mass_norm_pow = (10.0**log_mass / self._mpiv(z)) ** self._nc(z)
|
|
72
|
+
|
|
73
|
+
completeness = mass_norm_pow / (mass_norm_pow + 1.0)
|
|
74
|
+
assert isinstance(completeness, np.ndarray)
|
|
75
|
+
return completeness
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""The cluster kernel module.
|
|
2
|
+
|
|
3
|
+
This module holds the classes that define the kernels that can be included
|
|
4
|
+
in the cluster abundance integrand.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import numpy.typing as npt
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TrueMass:
|
|
12
|
+
"""The true mass kernel.
|
|
13
|
+
|
|
14
|
+
Assuming we measure the true mass, this will always be 1.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def distribution(self) -> npt.NDArray[np.float64]:
|
|
18
|
+
"""Evaluates and returns the mass distribution contribution to the integrand.
|
|
19
|
+
|
|
20
|
+
We have set this to 1.0 (i.e. it does not affect the mass distribution)
|
|
21
|
+
"""
|
|
22
|
+
return np.atleast_1d(1.0)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SpectroscopicRedshift:
|
|
26
|
+
"""The spec-z kernel.
|
|
27
|
+
|
|
28
|
+
Assuming the spectroscopic redshift has no uncertainties, this is akin to
|
|
29
|
+
multiplying by 1.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def distribution(self) -> npt.NDArray[np.float64]:
|
|
33
|
+
"""Evaluates and returns the z distribution contribution to the integrand.
|
|
34
|
+
|
|
35
|
+
We have set this to 1.0 (i.e. it does not affect the redshift distribution)
|
|
36
|
+
"""
|
|
37
|
+
return np.atleast_1d(1.0)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""The mass richness kernel module.
|
|
2
|
+
|
|
3
|
+
This module holds the classes that define the mass richness relations
|
|
4
|
+
that can be included in the cluster abundance integrand. These are
|
|
5
|
+
implementations of Kernels.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .murata import MurataBinned, MurataUnbinned
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Gaussian class for mass richness distributiosn."""
|
|
2
|
+
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import numpy.typing as npt
|
|
7
|
+
from scipy import special
|
|
8
|
+
|
|
9
|
+
from crow.integrator.numcosmo_integrator import NumCosmoIntegrator
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MassRichnessGaussian:
|
|
13
|
+
"""The representation of mass richness relations that are of a gaussian form."""
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def get_ln_mass_proxy_mean(
|
|
17
|
+
self,
|
|
18
|
+
log_mass: npt.NDArray[np.float64],
|
|
19
|
+
z: npt.NDArray[np.float64],
|
|
20
|
+
) -> npt.NDArray[np.float64]:
|
|
21
|
+
"""Return observed quantity corrected by redshift and mass."""
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def get_ln_mass_proxy_sigma(
|
|
25
|
+
self,
|
|
26
|
+
log_mass: npt.NDArray[np.float64],
|
|
27
|
+
z: npt.NDArray[np.float64],
|
|
28
|
+
) -> npt.NDArray[np.float64]:
|
|
29
|
+
"""Return observed scatter corrected by redshift and mass."""
|
|
30
|
+
|
|
31
|
+
def integrated_gaussian(
|
|
32
|
+
self,
|
|
33
|
+
log_mass: npt.NDArray[np.float64],
|
|
34
|
+
z: npt.NDArray[np.float64],
|
|
35
|
+
log_mass_proxy_limits: tuple[float, float],
|
|
36
|
+
) -> npt.NDArray[np.float64]:
|
|
37
|
+
ln_mass_proxy_mean = self.get_ln_mass_proxy_mean(log_mass, z)
|
|
38
|
+
ln_mass_proxy_sigma = self.get_ln_mass_proxy_sigma(log_mass, z)
|
|
39
|
+
|
|
40
|
+
x_min = (ln_mass_proxy_mean - log_mass_proxy_limits[0] * np.log(10.0)) / (
|
|
41
|
+
np.sqrt(2.0) * ln_mass_proxy_sigma
|
|
42
|
+
)
|
|
43
|
+
x_max = (ln_mass_proxy_mean - log_mass_proxy_limits[1] * np.log(10.0)) / (
|
|
44
|
+
np.sqrt(2.0) * ln_mass_proxy_sigma
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return_vals = np.empty_like(x_min)
|
|
48
|
+
mask1 = (x_max > 3.0) | (x_min < -3.0)
|
|
49
|
+
mask2 = ~mask1
|
|
50
|
+
|
|
51
|
+
# pylint: disable=no-member
|
|
52
|
+
return_vals[mask1] = (
|
|
53
|
+
-(special.erfc(x_min[mask1]) - special.erfc(x_max[mask1])) / 2.0
|
|
54
|
+
)
|
|
55
|
+
# pylint: disable=no-member
|
|
56
|
+
return_vals[mask2] = (
|
|
57
|
+
special.erf(x_min[mask2]) - special.erf(x_max[mask2])
|
|
58
|
+
) / 2.0
|
|
59
|
+
assert isinstance(return_vals, np.ndarray)
|
|
60
|
+
return return_vals
|
|
61
|
+
|
|
62
|
+
def gaussian_kernel(
|
|
63
|
+
self,
|
|
64
|
+
log_mass: npt.NDArray[np.float64],
|
|
65
|
+
z: npt.NDArray[np.float64],
|
|
66
|
+
log_mass_proxy: npt.NDArray[np.float64],
|
|
67
|
+
) -> npt.NDArray[np.float64]:
|
|
68
|
+
ln_mass_proxy_mean = self.get_ln_mass_proxy_mean(log_mass, z)
|
|
69
|
+
ln_mass_proxy_sigma = self.get_ln_mass_proxy_sigma(log_mass, z)
|
|
70
|
+
|
|
71
|
+
normalization = 1 / np.sqrt(2 * np.pi * ln_mass_proxy_sigma**2)
|
|
72
|
+
result = normalization * np.exp(
|
|
73
|
+
-0.5
|
|
74
|
+
* ((log_mass_proxy * np.log(10) - ln_mass_proxy_mean) / ln_mass_proxy_sigma)
|
|
75
|
+
** 2
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
assert isinstance(result, np.ndarray)
|
|
79
|
+
return result
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""The Murata et al. 19 mass richness kernel models."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import numpy.typing as npt
|
|
5
|
+
|
|
6
|
+
from ..parameters import Parameters
|
|
7
|
+
from .gaussian_protocol import MassRichnessGaussian
|
|
8
|
+
|
|
9
|
+
MURATA_DEFAULT_PARAMETERS = {
|
|
10
|
+
"mu0": 3.0,
|
|
11
|
+
"mu1": 0.8,
|
|
12
|
+
"mu2": -0.3,
|
|
13
|
+
"sigma0": 0.3,
|
|
14
|
+
"sigma1": 0.0,
|
|
15
|
+
"sigma2": 0.0,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MurataModel:
|
|
20
|
+
"""The mass richness modeling defined in Murata 19."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
pivot_log_mass: float,
|
|
25
|
+
pivot_redshift: float,
|
|
26
|
+
):
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.pivot_redshift = pivot_redshift
|
|
29
|
+
self.pivot_ln_mass = pivot_log_mass * np.log(10.0) # ln(M)
|
|
30
|
+
self.log1p_pivot_redshift = np.log1p(self.pivot_redshift)
|
|
31
|
+
|
|
32
|
+
self.parameters = Parameters({**MURATA_DEFAULT_PARAMETERS})
|
|
33
|
+
|
|
34
|
+
# Verify this gets called last or first
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def observed_value(
|
|
38
|
+
p: tuple[float, float, float],
|
|
39
|
+
log_mass: npt.NDArray[np.float64],
|
|
40
|
+
z: npt.NDArray[np.float64],
|
|
41
|
+
pivot_ln_mass: float,
|
|
42
|
+
log1p_pivot_redshift: float,
|
|
43
|
+
) -> npt.NDArray[np.float64]:
|
|
44
|
+
"""Return observed quantity corrected by redshift and mass."""
|
|
45
|
+
ln_mass = log_mass * np.log(10)
|
|
46
|
+
delta_ln_mass = ln_mass - pivot_ln_mass
|
|
47
|
+
delta_z = np.log1p(z) - log1p_pivot_redshift
|
|
48
|
+
|
|
49
|
+
result = p[0] + p[1] * delta_ln_mass + p[2] * delta_z
|
|
50
|
+
assert isinstance(result, np.ndarray)
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
def get_ln_mass_proxy_mean(
|
|
54
|
+
self,
|
|
55
|
+
log_mass: npt.NDArray[np.float64],
|
|
56
|
+
z: npt.NDArray[np.float64],
|
|
57
|
+
) -> npt.NDArray[np.float64]:
|
|
58
|
+
"""Return observed quantity corrected by redshift and mass."""
|
|
59
|
+
return MurataModel.observed_value(
|
|
60
|
+
(self.parameters["mu0"], self.parameters["mu1"], self.parameters["mu2"]),
|
|
61
|
+
log_mass,
|
|
62
|
+
z,
|
|
63
|
+
self.pivot_ln_mass,
|
|
64
|
+
self.log1p_pivot_redshift,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def get_ln_mass_proxy_sigma(
|
|
68
|
+
self,
|
|
69
|
+
log_mass: npt.NDArray[np.float64],
|
|
70
|
+
z: npt.NDArray[np.float64],
|
|
71
|
+
) -> npt.NDArray[np.float64]:
|
|
72
|
+
"""Return observed scatter corrected by redshift and mass."""
|
|
73
|
+
return MurataModel.observed_value(
|
|
74
|
+
(
|
|
75
|
+
self.parameters["sigma0"],
|
|
76
|
+
self.parameters["sigma1"],
|
|
77
|
+
self.parameters["sigma2"],
|
|
78
|
+
),
|
|
79
|
+
log_mass,
|
|
80
|
+
z,
|
|
81
|
+
self.pivot_ln_mass,
|
|
82
|
+
self.log1p_pivot_redshift,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class MurataBinned(MurataModel, MassRichnessGaussian):
|
|
87
|
+
"""The mass richness relation defined in Murata 19 for a binned data vector."""
|
|
88
|
+
|
|
89
|
+
def distribution(
|
|
90
|
+
self,
|
|
91
|
+
log_mass: npt.NDArray[np.float64],
|
|
92
|
+
z: npt.NDArray[np.float64],
|
|
93
|
+
log_mass_proxy_limits: tuple[float, float],
|
|
94
|
+
) -> npt.NDArray[np.float64]:
|
|
95
|
+
"""Evaluates and returns the mass-richness contribution to the integrand."""
|
|
96
|
+
return self.integrated_gaussian(log_mass, z, log_mass_proxy_limits)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class MurataUnbinned(MurataModel, MassRichnessGaussian):
|
|
100
|
+
"""The mass richness relation defined in Murata 19 for a unbinned data vector."""
|
|
101
|
+
|
|
102
|
+
def distribution(
|
|
103
|
+
self,
|
|
104
|
+
log_mass: npt.NDArray[np.float64],
|
|
105
|
+
z: npt.NDArray[np.float64],
|
|
106
|
+
log_mass_proxy: npt.NDArray[np.float64],
|
|
107
|
+
) -> npt.NDArray[np.float64]:
|
|
108
|
+
"""Evaluates and returns the mass-richness contribution to the integrand."""
|
|
109
|
+
return self.gaussian_kernel(log_mass, z, log_mass_proxy)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""The parameters module.
|
|
2
|
+
|
|
3
|
+
This module holds the class that stores and manages parameter to be used in
|
|
4
|
+
each cluster_module object.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Parameters:
|
|
9
|
+
"""The parameter class that stores and manages parameter to be used in
|
|
10
|
+
each cluster_module object.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, default_parameters_dict):
|
|
14
|
+
"""
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
default_parameters_dict: dict
|
|
18
|
+
Dictionary with the default parameters names and values.
|
|
19
|
+
Only parameters defined in this dictionary will be accepted
|
|
20
|
+
in this class
|
|
21
|
+
"""
|
|
22
|
+
self.__pars = {**default_parameters_dict}
|
|
23
|
+
|
|
24
|
+
def __getitem__(self, item):
|
|
25
|
+
return self.__pars[item]
|
|
26
|
+
|
|
27
|
+
def __setitem__(self, item, value):
|
|
28
|
+
if item not in self.__pars:
|
|
29
|
+
raise KeyError(
|
|
30
|
+
f"key={item} not accepted, " f"must be in {list(self.__pars.keys())}"
|
|
31
|
+
)
|
|
32
|
+
self.__pars[item] = value
|
|
33
|
+
|
|
34
|
+
def keys(self):
|
|
35
|
+
return self.__pars.keys()
|
|
36
|
+
|
|
37
|
+
def values(self):
|
|
38
|
+
return self.__pars.values()
|
|
39
|
+
|
|
40
|
+
def items(self):
|
|
41
|
+
return self.__pars.items()
|
|
42
|
+
|
|
43
|
+
def __iter__(self):
|
|
44
|
+
for key in self.keys():
|
|
45
|
+
yield key
|
|
46
|
+
|
|
47
|
+
def update(self, update_dict):
|
|
48
|
+
if not isinstance(update_dict, (dict, Parameters)):
|
|
49
|
+
raise ValueError(
|
|
50
|
+
"argument of update must be dict or Parameters, "
|
|
51
|
+
f"{type(update_dict)} given!"
|
|
52
|
+
)
|
|
53
|
+
bad_keys = list(filter(lambda key: key not in self.__pars.keys(), update_dict))
|
|
54
|
+
if len(bad_keys) > 0:
|
|
55
|
+
raise KeyError(f"bad keys provided for update: {bad_keys}")
|
|
56
|
+
self.__pars.update(update_dict)
|