• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LSSTDESC / CCL / 5191066774

06 Jun 2023 04:42PM UTC coverage: 97.543% (+0.4%) from 97.136%
5191066774

Pull #1087

github

web-flow
Merge 987bfdbae into 17a0e5a2a
Pull Request #1087: v3 preparation

83 of 83 new or added lines in 36 files covered. (100.0%)

5122 of 5251 relevant lines covered (97.54%)

0.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.23
/pyccl/halos/massdef.py
1
__all__ = ("mass2radius_lagrangian", "convert_concentration", "MassDef",
1✔
2
           "MassDef200m", "MassDef200c", "MassDef500c", "MassDefVir",
3
           "MassDefFof", "mass_translator",)
4

5
from functools import cached_property
1✔
6

7
import numpy as np
1✔
8

9
from .. import CCLAutoRepr, CCLNamedClass, lib, check
1✔
10
from . import Concentration, HaloBias, MassFunc
1✔
11

12

13
def mass2radius_lagrangian(cosmo, M):
1✔
14
    """ Returns Lagrangian radius for a halo of mass :math:`M`.
15
    The lagrangian radius is defined as that enclosing
16
    the mass of the halo assuming a homogeneous Universe.
17

18
    .. math::
19
        R = \\left(\\frac{3\\,M}{4\\pi\\,\\rho_{M,0}}\\right)^{1/3}
20

21
    Args:
22
        cosmo (:class:`~pyccl.cosmology.Cosmology`): A Cosmology object.
23
        M (:obj:`float` or `array`): halo mass in units of :math:`M_\\odot`.
24

25
    Returns:
26
        (:obj:`float` or `array`): lagrangian radius in comoving Mpc.
27
    """
28
    M_use = np.atleast_1d(M)
1✔
29
    R = (M_use / (4.18879020479 * cosmo.rho_x(1, 'matter')))**(1./3.)
1✔
30
    if np.ndim(M) == 0:
1✔
31
        return R[0]
1✔
32
    return R
1✔
33

34

35
def convert_concentration(cosmo, *, c_old, Delta_old, Delta_new):
1✔
36
    """ Computes the concentration parameter for a different mass definition.
37
    This is done assuming an NFW profile. The output concentration ``c_new`` is
38
    found by solving the equation:
39

40
    .. math::
41
        f(c_{\\rm old}) \\Delta_{\\rm old} = f(c_{\\rm new}) \\Delta_{\\rm new}
42

43
    where
44

45
    .. math::
46
        f(x) = \\frac{x^3}{\\log(1+x) - x/(1+x)}.
47

48
    Args:
49
        cosmo (:class:`~pyccl.cosmology.Cosmology`): A Cosmology object.
50
        c_old (:obj:`float` or `array`): concentration to translate from.
51
        Delta_old (:obj:`float`): Delta parameter associated to the input
52
            concentration. See description of the :class:`MassDef` class.
53
        Delta_new (:obj:`float`): Delta parameter associated to the output
54
            concentration.
55

56
    Returns:
57
        (:obj:`float` or `array`): concentration parameter for the new
58
        mass definition.
59
    """
60
    status = 0
1✔
61
    c_old_use = np.atleast_1d(c_old)
1✔
62
    c_new, status = lib.convert_concentration_vec(cosmo.cosmo,
1✔
63
                                                  Delta_old, c_old_use,
64
                                                  Delta_new, c_old_use.size,
65
                                                  status)
66
    check(status, cosmo=cosmo)
1✔
67

68
    if np.isscalar(c_old):
1✔
69
        return c_new[0]
1✔
70
    return c_new
1✔
71

72

73
class MassDef(CCLAutoRepr, CCLNamedClass):
1✔
74
    """Halo mass definition. Halo masses are defined in terms of an overdensity
1✔
75
    parameter :math:`\\Delta` and an associated density :math:`X` (either the
76
    matter density or the critical density):
77

78
    .. math::
79
        M_\\Delta = \\frac{4 \\pi}{3} \\Delta\\,\\rho_X\\, R_\\Delta^3
80

81
    where :math:`R_\\Delta` is the halo radius. This object also holds methods
82
    to translate between :math:`R_\\Delta` and :math:`M_\\Delta`, and to
83
    translate masses between different definitions if a concentration-mass
84
    relation is provided.
85

86
    You may also define halo masses based on a Friends-of-Friends algorithm,
87
    in which case simply pass ``Delta='fof'`` below.
88

89
    Args:
90
        Delta (:obj:`float`): overdensity parameter. Pass ``'vir'`` if using virial
91
            overdensity. Pass ``'fof'`` for Friends-of-Friends halos.
92
        rho_type (:obj:`str`): either 'critical' or 'matter'.
93
    """ # noqa
94
    __eq_attrs__ = ("name",)
1✔
95

96
    def __init__(self, Delta, rho_type):
1✔
97
        # Check it makes sense
98
        if isinstance(Delta, str):
1✔
99
            if Delta.isdigit():
1✔
100
                Delta = int(Delta)
1✔
101
            elif Delta not in ["fof", "vir"]:
1✔
102
                raise ValueError(f"Unknown Delta type {Delta}.")
1✔
103
        if isinstance(Delta, (int, float)) and Delta < 0:
1✔
104
            raise ValueError("Delta must be a positive number.")
1✔
105
        if rho_type not in ['matter', 'critical']:
1✔
106
            raise ValueError("rho_type must be {'matter', 'critical'}.")
1✔
107

108
        self.Delta = Delta
1✔
109
        self.rho_type = rho_type
1✔
110

111
    @cached_property
1✔
112
    def name(self):
1✔
113
        """Give a name to this mass definition."""
114
        if isinstance(self.Delta, (int, float)):
1✔
115
            return f"{self.Delta}{self.rho_type[0]}"
1✔
116
        return f"{self.Delta}"
1✔
117

118
    def __repr__(self):
1✔
119
        return f"MassDef(Delta={self.Delta}, rho_type={self.rho_type})"
1✔
120

121
    def get_Delta(self, cosmo, a):
1✔
122
        """ Gets overdensity parameter associated to this mass
123
        definition.
124

125
        Args:
126
            cosmo (:class:`~pyccl.cosmology.Cosmology`): A Cosmology object.
127
            a (:obj:`float`): scale factor
128

129
        Returns:
130
            :obj:`float`: value of the overdensity parameter.
131
        """
132
        if self.Delta == 'fof':
1✔
133
            raise ValueError("FoF masses don't have an associated overdensity."
1✔
134
                             "Nor can they be translated into other masses")
135
        if self.Delta == 'vir':
1✔
136
            status = 0
1✔
137
            D, status = lib.Dv_BryanNorman(cosmo.cosmo, a, status)
1✔
138
            return D
1✔
139
        return self.Delta
1✔
140

141
    def _get_Delta_m(self, cosmo, a):
1✔
142
        """ For SO-based mass definitions, this returns the corresponding
143
        value of Delta for a rho_matter-based definition.
144
        """
145
        delta = self.get_Delta(cosmo, a)
1✔
146
        if self.rho_type == 'matter':
1✔
147
            return delta
1✔
148
        om_this = cosmo.omega_x(a, self.rho_type)
1✔
149
        om_matt = cosmo.omega_x(a, 'matter')
1✔
150
        return delta * om_this / om_matt
1✔
151

152
    def get_mass(self, cosmo, R, a):
1✔
153
        """ Translates a halo radius into a mass
154

155
        .. math::
156
            M_\\Delta = \\frac{4 \\pi}{3} \\Delta\\,\\rho_X\\, R_\\Delta^3
157

158
        Args:
159
            cosmo (:class:`~pyccl.cosmology.Cosmology`): A Cosmology object.
160
            R (:obj:`float` or `array`): halo radius (:math:`{\\rm Mpc}`,
161
                physical, not comoving).
162
            a (:obj:`float`): scale factor.
163

164
        Returns:
165
            (:obj:`float` or `array`): halo mass in units of :math:`M_\\odot`.
166
        """
167
        R_use = np.atleast_1d(R)
1✔
168
        Delta = self.get_Delta(cosmo, a)
1✔
169
        M = 4.18879020479 * cosmo.rho_x(a, self.rho_type) * Delta * R_use**3
1✔
170
        if np.ndim(R) == 0:
1✔
171
            return M[0]
1✔
172
        return M
1✔
173

174
    def get_radius(self, cosmo, M, a):
1✔
175
        """ Translates a halo mass into a radius
176

177
        .. math::
178
            R_\\Delta = \\left(\\frac{3M_\\Delta}{4 \\pi
179
            \\rho_X\\,\\Delta}\\right)^{1/3}
180

181
        Args:
182
            cosmo (:class:`~pyccl.cosmology.Cosmology`):
183
                A Cosmology object.
184
            M (:obj:`float` or `array`): halo mass in units of
185
                :math:`M_\\odot`.
186
            a (:obj:`float`): scale factor.
187

188
        Returns:
189
            (:obj:`float` or `array`): halo radius in units of
190
            :math:`{\\rm Mpc}` (physical, not comoving).
191
        """
192
        M_use = np.atleast_1d(M)
1✔
193
        Delta = self.get_Delta(cosmo, a)
1✔
194
        R = (M_use / (4.18879020479 * Delta *
1✔
195
                      cosmo.rho_x(a, self.rho_type)))**(1./3.)
196
        if np.ndim(M) == 0:
1✔
197
            return R[0]
1✔
198
        return R
1✔
199

200
    @classmethod
1✔
201
    def from_name(cls, name):
1✔
202
        """ Return mass definition subclass from name string.
203

204
        Args:
205
            name (:obj:`str`):
206
                a mass definition name (e.g. ``'200m'`` for
207
                :math:`\\Delta=200` times the matter density).
208

209
        Returns:
210
            :class:`MassDef` instance corresponding to the input name.
211
        """
212
        MassDefName = f"MassDef{name.capitalize()}"
1✔
213
        if MassDefName in globals():
1✔
214
            # MassDef is defined in one of the implementations below.
215
            return globals()[MassDefName]
1✔
216
        parser = {"c": "critical", "m": "matter"}
1✔
217
        if len(name) < 2 or name[-1] not in parser:
1✔
218
            # Bogus input - can't parse it.
219
            raise ValueError("Could not parse mass definition string.")
1✔
220
        Delta, rho_type = name[:-1], parser[name[-1]]
1✔
221
        return cls(Delta, rho_type)
1✔
222

223
    @classmethod
1✔
224
    def create_instance(cls, input_):
1✔
225
        if isinstance(input_, cls):
1✔
226
            return input_
1✔
227
        else:
228
            return cls.from_name(input_)
1✔
229

230
    @classmethod
1✔
231
    def from_specs(cls, mass_def=None, *,
1✔
232
                   mass_function=None, halo_bias=None, concentration=None):
233
        """Instantiate mass definition and halo model ingredients.
234

235
        Unspecified halo model ingredients are ignored. ``mass_def`` is always
236
        instantiated.
237

238
        Args:
239
            mass_def (:obj:`MassDef`, :obj:`str` or :obj:`None`):
240
                Mass definition. If a string, instantiate from its name.
241
                If `None`, obtain the one from the first specified halo model
242
                ingredient.
243
            mass_function (:class:`~pyccl.halos.halo_model_base.MassFunc` or :obj:`str`):
244
                Mass function subclass. Strings are auto-instantiated using
245
                ``mass_def``. ``None`` values are ignored.
246
            halo_bias (:class:`~pyccl.halos.halo_model_base.HaloBias` or :obj:`str`):
247
                Halo bias subclass. Strings are auto-instantiated using
248
                ``mass_def``. ``None`` values are ignored.
249
            concentration (:class:`~pyccl.halos.halo_model_base.Concentration` or :obj:`str`):
250
                Concentration subclass. Strings are auto-instantiated using
251
                ``mass_def``. ``None`` values are ignored.
252

253
        Returns:
254
            Tuple of up to 4 elements.
255

256
            - mass_def : :class:`MassDef`
257
            - mass_function : :class:`~pyccl.halos.halo_model_base.MassFunc`, if specified
258
            - halo_bias : :class:`~pyccl.halos.halo_model_base.HaloBias`, if specified
259
            - concentration : :class:`~pyccl.halos.halo_model_base.Concentration`, if specified
260
        """ # noqa
261
        values = mass_function, halo_bias, concentration
1✔
262
        idx = [value is not None for value in values]
1✔
263

264
        # Filter only the specified ones.
265
        values = np.array(values)[idx]
1✔
266
        names = np.array(["mass_function", "halo_bias", "concentration"])[idx]
1✔
267
        Types = np.array([MassFunc, HaloBias, Concentration])[idx]
1✔
268

269
        # Sanity check.
270
        if mass_def is None:
1✔
271
            for name, value in zip(names, values):
1✔
272
                if isinstance(value, str):
1✔
273
                    raise ValueError(f"Need mass_def if {name} is str.")
1✔
274

275
        # Instantiate mass_def.
276
        if mass_def is not None:
1✔
277
            mass_def = cls.create_instance(mass_def)  # instantiate directly
1✔
278
        else:
279
            mass_def = values[0].mass_def  # use the one in HMIngredients
×
280

281
        # Instantiate halo model ingredients.
282
        out = []
1✔
283
        for name, value, Type in zip(names, values, Types):
1✔
284
            instance = Type.create_instance(value, mass_def=mass_def)
1✔
285
            out.append(instance)
1✔
286

287
        # Check mass definition consistency.
288
        if out and set([x.mass_def for x in out]) != set([mass_def]):
1✔
289
            raise ValueError("Inconsistent mass definitions.")
1✔
290

291
        return mass_def, *out
1✔
292

293

294
MassDef200m = MassDef(200, "matter")
1✔
295
MassDef200c = MassDef(200, "critical")
1✔
296
MassDef500c = MassDef(500, "critical")
1✔
297
MassDefVir = MassDef("vir", "critical")
1✔
298
MassDefFof = MassDef("fof", "matter")
1✔
299

300

301
def mass_translator(*, mass_in, mass_out, concentration):
1✔
302
    """Translate between mass definitions, assuming an NFW profile.
303

304
    Returns a function that can be used to translate between halo
305
    masses according to two different definitions.
306

307
    Args:
308
        mass_in (:class:`MassDef` or :obj:`str`): mass definition of the
309
            input mass.
310
        mass_out (:class:`MassDef` or :obj:`str`): mass definition of the
311
            output mass.
312
        concentration (:class:`~pyccl.halos.halo_model_base.Concentration` or :obj:`str`):
313
            concentration-mass relation to use for the mass conversion. It must
314
            be calibrated for masses using the ``mass_in`` definition.
315

316
    Returns:
317
        Function that ranslates between two masses. The returned function
318
        ``f`` can be called as: ``f(cosmo, M, a)``, where
319
        ``cosmo`` is a :class:`~pyccl.cosmology.Cosmology` object, ``M``
320
        is a mass (or array of masses), and ``a`` is a scale factor.
321

322
    """ # noqa
323

324
    mass_in = MassDef.create_instance(mass_in)
1✔
325
    mass_out = MassDef.create_instance(mass_out)
1✔
326
    concentration = Concentration.create_instance(concentration,
1✔
327
                                                  mass_def=mass_in)
328
    if concentration.mass_def != mass_in:
1✔
329
        raise ValueError("mass_def of concentration doesn't match mass_in")
1✔
330

331
    def translate(cosmo, M, a):
1✔
332
        if mass_in == mass_out:
1✔
333
            return M
1✔
334

335
        c_in = concentration(cosmo, M, a)
1✔
336
        Om_in = cosmo.omega_x(a, mass_in.rho_type)
1✔
337
        D_in = mass_in.get_Delta(cosmo, a) * Om_in
1✔
338
        R_in = mass_in.get_radius(cosmo, M, a)
1✔
339

340
        Om_out = cosmo.omega_x(a, mass_out.rho_type)
1✔
341
        D_out = mass_out.get_Delta(cosmo, a) * Om_out
1✔
342
        c_out = convert_concentration(
1✔
343
            cosmo, c_old=c_in, Delta_old=D_in, Delta_new=D_out)
344
        R_out = R_in * c_out/c_in
1✔
345
        return mass_out.get_mass(cosmo, R_out, a)
1✔
346

347
    return translate
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc