lsstdesc-crow 1.0.2__tar.gz

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.
Files changed (39) hide show
  1. lsstdesc_crow-1.0.2/LICENSE +26 -0
  2. lsstdesc_crow-1.0.2/PKG-INFO +29 -0
  3. lsstdesc_crow-1.0.2/README.md +2 -0
  4. lsstdesc_crow-1.0.2/crow/__init__.py +11 -0
  5. lsstdesc_crow-1.0.2/crow/cluster_modules/_clmm_patches.py +174 -0
  6. lsstdesc_crow-1.0.2/crow/cluster_modules/abundance.py +87 -0
  7. lsstdesc_crow-1.0.2/crow/cluster_modules/completeness_models.py +75 -0
  8. lsstdesc_crow-1.0.2/crow/cluster_modules/kernel.py +37 -0
  9. lsstdesc_crow-1.0.2/crow/cluster_modules/mass_proxy/__init__.py +8 -0
  10. lsstdesc_crow-1.0.2/crow/cluster_modules/mass_proxy/gaussian_protocol.py +79 -0
  11. lsstdesc_crow-1.0.2/crow/cluster_modules/mass_proxy/murata.py +109 -0
  12. lsstdesc_crow-1.0.2/crow/cluster_modules/parameters.py +56 -0
  13. lsstdesc_crow-1.0.2/crow/cluster_modules/purity_models.py +76 -0
  14. lsstdesc_crow-1.0.2/crow/cluster_modules/shear_profile.py +467 -0
  15. lsstdesc_crow-1.0.2/crow/integrator/__init__.py +1 -0
  16. lsstdesc_crow-1.0.2/crow/integrator/integrator.py +31 -0
  17. lsstdesc_crow-1.0.2/crow/integrator/numcosmo_integrator.py +94 -0
  18. lsstdesc_crow-1.0.2/crow/integrator/scipy_integrator.py +52 -0
  19. lsstdesc_crow-1.0.2/crow/properties.py +19 -0
  20. lsstdesc_crow-1.0.2/crow/recipes/__init__.py +1 -0
  21. lsstdesc_crow-1.0.2/crow/recipes/binned_exact.py +349 -0
  22. lsstdesc_crow-1.0.2/crow/recipes/binned_grid.py +404 -0
  23. lsstdesc_crow-1.0.2/crow/recipes/binned_parent.py +112 -0
  24. lsstdesc_crow-1.0.2/lsstdesc_crow.egg-info/PKG-INFO +29 -0
  25. lsstdesc_crow-1.0.2/lsstdesc_crow.egg-info/SOURCES.txt +37 -0
  26. lsstdesc_crow-1.0.2/lsstdesc_crow.egg-info/dependency_links.txt +1 -0
  27. lsstdesc_crow-1.0.2/lsstdesc_crow.egg-info/requires.txt +5 -0
  28. lsstdesc_crow-1.0.2/lsstdesc_crow.egg-info/top_level.txt +1 -0
  29. lsstdesc_crow-1.0.2/pyproject.toml +52 -0
  30. lsstdesc_crow-1.0.2/setup.cfg +4 -0
  31. lsstdesc_crow-1.0.2/tests/test_cluster_abundance.py +55 -0
  32. lsstdesc_crow-1.0.2/tests/test_cluster_integrators.py +50 -0
  33. lsstdesc_crow-1.0.2/tests/test_cluster_kernels.py +118 -0
  34. lsstdesc_crow-1.0.2/tests/test_cluster_mass_richness.py +277 -0
  35. lsstdesc_crow-1.0.2/tests/test_cluster_parameters.py +51 -0
  36. lsstdesc_crow-1.0.2/tests/test_cluster_shear_profile.py +325 -0
  37. lsstdesc_crow-1.0.2/tests/test_recipe_binned_counts.py +659 -0
  38. lsstdesc_crow-1.0.2/tests/test_recipe_binned_parent.py +59 -0
  39. lsstdesc_crow-1.0.2/tests/test_recipe_binned_shear_profile.py +492 -0
@@ -0,0 +1,26 @@
1
+ Copyright (c) 2018-2019, The LSST DESC clump Contributors
2
+
3
+ All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without modification,
6
+ are permitted provided that the following conditions are met:
7
+
8
+ * Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+ * Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+ * Neither the name of clump nor the names of its contributors may be used to
14
+ endorse or promote products derived from this software without specific
15
+ prior written permission.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
21
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.4
2
+ Name: lsstdesc-crow
3
+ Version: 1.0.2
4
+ Summary: Cluster Reconstruction of Observables Workbench
5
+ Author: The LSST DESC CROW Contributors
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/LSSTDESC/crow
8
+ Project-URL: Repository, https://github.com/LSSTDESC/crow
9
+ Project-URL: Issues, https://github.com/LSSTDESC/crow/issues
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: clmm
22
+ Requires-Dist: numcosmo_py>=0.21.1
23
+ Requires-Dist: numpy>=2.0
24
+ Requires-Dist: pyccl>=2.8.0
25
+ Requires-Dist: scipy>=1.12
26
+ Dynamic: license-file
27
+
28
+ # CROW
29
+ **C**luster **R**econstruction of **O**bservables **W**orkbench: **CROW**
@@ -0,0 +1,2 @@
1
+ # CROW
2
+ **C**luster **R**econstruction of **O**bservables **W**orkbench: **CROW**
@@ -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)