r"""
Luminosity function modeller (:mod:`~horizonground.lumfunc_modeller`)
===========================================================================
Provide models of the redshift-dependent tracer luminosity function
:math:`\varPhi(m, z)` (for absolute magnitude :math:`m`) or
:math:`\varPhi(\lg{L}, z)` (for base-10 logarithm of the intrinsic
luminosity :math:`L`), from which the comoving number density below/above
some brightness threshold :math:`\bar{m}` or :math:`\bar{L}`,
.. math::
\bar{n}(z; <\!\bar{m}) = \int_{-\infty}^{\bar{m}}
\operatorname{d}\!m\, \varPhi(m, z)
\quad \mathrm{or} \quad
\bar{n}(z; >\!\lg\bar{L}) = \int^{\infty}_{\lg\bar{L}}
\operatorname{d}\!\lg{L}\, \varPhi(\lg{L}, z) \,,
can be predicted. The corresponding evolution bias is
.. math::
b_\mathrm{e}(z) = - (1 + z)
\frac{\partial \ln\bar{n}(z)}{\partial z}
and magnification bias is
.. math::
s(z) = \frac{1}{\ln10}
\frac{\varPhi(\bar{m},z)}{\bar{n}(z; <\!\bar{m})}
\quad \mathrm{or} \quad
s(z) = \frac{2}{5\ln10}
\frac{\varPhi(\lg\bar{L},z)}{\bar{n}(z; >\!\lg\bar{L})}
\,,
We also offer a simple :math:`K`-correction formula taken from ref. [1]_.
.. [1] Croom S. M. et al., 2009. **MNRAS** 399(4), 1755--1772.
[arXiv: `0907.2727 <https://arxiv.org/abs/0907.2727>`_]
.. autosummary::
LumFuncModeller
konstante_correction
Quasar (QSO) luminosity function
---------------------------------------------------------------------------
**Pure luminosity evolution model (PLE)**
The quasar luminosity function in the pure luminosity evolution (PLE)
model is a double power law
.. math::
\varPhi(m, z) = \frac{\varPhi_\ast}{
10^{0.4 (\alpha + 1) [m - m_\ast(z)]}
+ 10^{0.4 (\beta + 1) [m - m_\ast(z)]}
}
where :math:`m` is the absolute magnitude, :math:`z` is the redshift, and
the slope parameters :math:`\alpha, \beta` describe the power laws on the
bright and faint ends respectively, but their values differ below and
above the pivot redshift :math:`z_\mathrm{p}`. :math:`m_\ast` is the
break magnitude at which the luminosity function evaluates to
:math:`\varPhi_\ast`,
.. math::
m_\ast(z) = m_\ast(z_\mathrm{p}) - \frac{5}{2} \left[
k_1 (z - z_\mathrm{p}) + k_2 (z - z_\mathrm{p})^2
\right] \,,
where :math:`k_1, k_2` are the redshift-evolution parameters whose values
also differ below and above the pivot redshift [2]_.
This is a parametric model with 10 parameters: :math:`\lg\varPhi_\ast`,
:math:`m_\ast(z_\mathrm{p})`, :math:`(\alpha, \beta, k_1, k_2)_\mathrm{l}`
for :math:`z < z_\mathrm{p}` and :math:`(\alpha, \beta, k_1,
k_2)_\mathrm{h}` for :math:`z > z_\mathrm{p}`. Due to the exchange
symmetry between :math:`\alpha` and :math:`\beta` in the double power law,
the model constraint :math:`\alpha < \beta` is imposed for both above
and below the pivot redshift.
**Hybrid evolution model (PLE+LEDE)**
This model is the same as the PLE model below the pivot redshift, but
above that the luminosity evolution--density evolution (LEDE) model is
adopted where the luminosity function normalisation, break magnitude and
bright-end power law index have different redshift evolutions
.. math::
\begin{align*}
\lg\varPhi_\ast &= \lg\varPhi_\ast(z_\mathrm{p}) \
+ c_{1\mathrm{a}} (z - z_\mathrm{p})
+ c_{1\mathrm{b}} (z - z_\mathrm{p})^2 \,, \\
m_\ast(z) &= m_\ast(z_\mathrm{p}) + c_2 (z - z_\mathrm{p}) \,, \\
\alpha(z) &= \alpha(z_\mathrm{p}) + c_3 (z - z_\mathrm{p}) \,,
\end{align*}
and continuity across redshift is imposed by requiring the same
:math:`\lg\varPhi_\ast(z_\mathrm{p})` and :math:`m_\ast(z_\mathrm{p})` for
two models [2]_.
The hybrid model still has 10 overall parameters: the PLE model retains 6
low-redshift parameters and the high-redshift LEDE model has 8 parameters,
with the substitutions of :math:`\lg\varPhi_\ast(0)` for
:math:`\lg\varPhi_\ast`, :math:`m_\ast(0)` for
:math:`m_\ast(z_\mathrm{p})`, :math:`c_2` for :math:`k_1, k_2` and the
addition of :math:`c_{1\mathrm{a}}, c_{1\mathrm{b}}` and :math:`c_3`.
As before, due to the exchange symmetry between :math:`\alpha` and
:math:`\beta`, the low-redshift PLE model constraint :math:`\alpha < \beta`
is imposed.
.. [2] Palanque-Delabrouille N. et al., 2016. **A&A** 587, A41.
[arXiv: `1509.05607 <https://arxiv.org/abs/1509.05607>`_]
.. autosummary::
quasar_PLE_lumfunc
quasar_PLE_model_constraint
quasar_hybrid_lumfunc
quasar_hybrid_model_constraint
H |alpha| -emitter luminosity function
---------------------------------------------------------------------------
**Schechter function model**
The H |alpha| -emitter luminosity function in the Schechter function model
takes the form of a gamma function
.. math::
\varPhi(L, z) \operatorname{d}\!L = \underbrace{
\ln10 \, \varPhi_\ast(z) y(z)^{\alpha+1} \mathrm{e}^{-y(z)}
}_{\varPhi(\lg{L}, z)} \operatorname{d}\!\lg{L}
where :math:`\alpha` is the faint-end slope parameter,
.. math::
\begin{align*}
\varPhi_\ast(z) &=
\begin{cases}
\varPhi_{\ast0} (1 + z)^\epsilon \,,
\quad z \leqslant z_\mathrm{b} \,; \\
\varPhi_{\ast0} (1 + z_\mathrm{b})^{2\epsilon}
(1 + z)^{-\epsilon} \,, \quad z > z_\mathrm{b} \,,
\end{cases} \\
y(z) &= \frac{L}{L_{\ast0}} (1 + z)^{-\delta} \,,
\end{align*}
are the redshift-dependent characteristic comoving number density and
relative luminosity of the H |alpha| -emitters, :math:`\epsilon, \delta`
are the redshift-evolution indices, and :math:`z_\mathrm{b}` is the break
magnitude [3]_.
This is a parametric model with 6 parameters: :math:`\alpha, \epsilon,
\delta`, :math:`z_\mathrm{b}`, :math:`m_{\ast0}` and
:math:`\varPhi_{\ast0}`.
.. [3] Pozzetti L. et al., 2016. **A&A** 590, A3.
[arXiv: `1603.01453 <https://arxiv.org/abs/1603.01453>`_]
.. autosummary::
alpha_emitter_schechter_lumfunc
|
.. |alpha| unicode:: U+03B1
:trim:
"""
from inspect import signature
import numpy as np
from astropy import units
from scipy.integrate import quad
from scipy.misc import derivative
[docs]def konstante_correction(redshift, normalisation_redshift=2., index=-0.5):
r"""Subtractive Konstante correction (*K*-correction) to apparent
magnitude for correcting bandpass redshifting.
Notes
-----
This implements the subtractive *K*-correction :math:`K(z) - K(z_\ast)`
normalised to redshift :math:`z_\ast`, where
.. math::
K(z) = - \frac{5}{2} (1 + \alpha_\nu) \lg(1 + z)
and :math:`\alpha_\nu` is the power-law index.
Parameters
----------
redshift : float
Redshift.
normalisation_redshift : float, optional
Normalisation redshift (default is 2.).
index : float, optional
:math:`K`-correction power-law index (default is -0.5).
Returns
-------
correction_magnitude : float
The magnitude correction that needs to be subtracted from the
observed magnitude.
"""
return - 5./2. * (1 + index) \
* np.log10((1 + redshift) / (1 + normalisation_redshift))
[docs]def quasar_PLE_lumfunc(magnitude, redshift, *, base10_log=True,
redshift_pivot=2.2, **model_parameters):
r"""Evaluate the pure luminosity evolution (PLE) model for the quasar
luminosity function at the given absolute magnitude and redshift.
Parameters
----------
magnitude : float
Quasar absolute magnitude.
redshift : float
Quasar redshift.
base10_log : bool, optional
If `True` (default), return the base-10 logarithmic value.
redshift_pivot : float, optional
Pivot redshift.
**model_parameters
PLE model parameters. Must be passed with the following
parameter names:
``r'\lg\Phi_\ast'``, ``r'm_\ast(z_\mathrm{p})'``,
``r'\alpha_\mathrm{{l}}'``, ``r'\beta_\mathrm{{l}}'``,
``r'k_{{1\mathrm{{l}}}}'``, ``r'k_{{2\mathrm{{l}}}}'``,
``r'\alpha_\mathrm{{h}}'``, ``r'\beta_\mathrm{{h}}'``,
``r'k_{{1\mathrm{{h}}}}'``, ``r'k_{{2\mathrm{{h}}}}'``.
Returns
-------
lumfunc_value : float
Predicted qausar luminosity function value (in inverse cubic Mpc
per unit magnitude). Base-10 logarithmic value is returned if
`base10_log` is `True`.
Notes
-----
This function is not vectorised. Please use `numpy.vectorize` or
`functools` if you need vectorisation.
"""
if not isinstance(model_parameters, dict):
raise TypeError("Model parameters must be passed as a dictionary.")
m, z, z_p = magnitude, redshift, redshift_pivot
# Determine the redshift end for setting parameters.
subscript = r'\mathrm{{{}}}'.format('l') if z <= z_p \
else r'\mathrm{{{}}}'.format('h')
alpha = model_parameters[r'\alpha_{}'.format(subscript)]
beta = model_parameters[r'\beta_{}'.format(subscript)]
k_1 = model_parameters[r'k_{{1{}}}'.format(subscript)]
k_2 = model_parameters[r'k_{{2{}}}'.format(subscript)]
lg_Phi_star = model_parameters[r'\lg\Phi_\ast']
m_star_at_z_p = model_parameters[r'm_\ast(z_\mathrm{p})']
# Evaluate the model prediction.
magnitude_deviation_exponent = \
(m - m_star_at_z_p) + 2.5 * (k_1 * (z - z_p) + k_2 * (z - z_p) ** 2)
ln_faint_power_law = \
np.log(10) * (0.4 * (alpha + 1) * magnitude_deviation_exponent)
ln_bright_power_law = \
np.log(10) * (0.4 * (beta + 1) * magnitude_deviation_exponent)
ln_denominator = np.logaddexp(ln_faint_power_law, ln_bright_power_law)
if base10_log:
lumfunc_value = lg_Phi_star - ln_denominator / np.log(10)
else:
Phi_star = 10 ** lg_Phi_star
lumfunc_value = Phi_star / np.exp(ln_denominator)
return lumfunc_value
[docs]def quasar_PLE_model_constraint(**model_parameters):
"""Check whether the pure luminosity evolution (PLE) model constraint
is satisfied.
Parameters
----------
**model_parameters
PLE model parameters. See :func:`quasar_PLE_lumfunc` for required
parameters.
Returns
-------
bool
Whether or not the model constraint is satisfied.
"""
return (
model_parameters[r'\alpha_\mathrm{l}']
< model_parameters[r'\beta_\mathrm{l}']
) and (
model_parameters[r'\alpha_\mathrm{h}']
< model_parameters[r'\beta_\mathrm{h}']
)
[docs]def quasar_hybrid_lumfunc(magnitude, redshift, *, base10_log=True,
redshift_pivot=2.2, **model_parameters):
r"""Evaluate the hybrid model (pure luminosity evolution and luminosity
evolution--density evolution, 'PLE+LEDE') for the quasar luminosity
function at the given absolute magnitude and redshift.
Parameters
----------
magnitude : float
Quasar absolute magnitude.
redshift : float
Quasar redshift.
base10_log : bool, optional
If `True` (default), return the base-10 logarithmic value.
redshift_pivot : float, optional
Pivot redshift.
**model_parameters
Hybrid model parameters. Must be passed with the following
parameter names: ``r'\lg\Phi_\ast(0)'``, ``r'm_\ast(0)'``,
``r'\alpha'``, ``r'\beta'``, ``r'k_1'``, ``r'k_2'``,
``r'c_{{1\mathrm{{a}}}}'``, ``r'c_{{1\mathrm{{b}}}}'``,
``r'c_2'``, ``r'k_3'``.
Returns
-------
lumfunc_value : float
Predicted qausar luminosity function value (in inverse cubic Mpc
per unit magnitude). Base-10 logarithmic value is returned if
`base10_log` is `True`.
Notes
-----
This function is not vectorised. Please use `numpy.vectorize` or
`functools` if you need vectorisation.
"""
if not isinstance(model_parameters, dict):
raise TypeError("Model parameters must be passed as a dictionary.")
m, z, z_p = magnitude, redshift, redshift_pivot
# Low redshift PLE model.
lg_Phi_star_0 = model_parameters[r'\lg\Phi_\ast(0)']
m_star_0 = model_parameters[r'm_\ast(0)']
alpha = model_parameters[r'\alpha']
beta = model_parameters[r'\beta']
k_1 = model_parameters[r'k_1']
k_2 = model_parameters[r'k_2']
if z <= z_p:
magnitude_deviation_exponent = \
(m - m_star_0) + 2.5 * (k_1 * z + k_2 * z ** 2)
ln_faint_power_law = \
np.log(10) * (0.4 * (alpha + 1) * magnitude_deviation_exponent)
ln_bright_power_law = \
np.log(10) * (0.4 * (beta + 1) * magnitude_deviation_exponent)
ln_denominator = np.logaddexp(ln_faint_power_law, ln_bright_power_law)
if base10_log:
lumfunc_value = lg_Phi_star_0 - ln_denominator / np.log(10)
else:
Phi_star = 10 ** lg_Phi_star_0
lumfunc_value = Phi_star / np.exp(ln_denominator)
return lumfunc_value
# High redshift LEDE model.
c_1a = model_parameters[r'c_{1\mathrm{a}}']
c_1b = model_parameters[r'c_{1\mathrm{b}}']
c_2 = model_parameters[r'c_2']
c_3 = model_parameters[r'c_3']
lg_Phi_star_at_z_p = lg_Phi_star_0
m_star_at_z_p = m_star_0 - 2.5 * (k_1 * z_p + k_2 * z_p ** 2)
lg_Phi_star = lg_Phi_star_at_z_p + c_1a * (z - z_p) + c_1b * (z - z_p) ** 2
magnitude_deviation_exponent = (m - m_star_at_z_p) + c_2 * (z - z_p)
alpha = alpha + c_3 * (z - z_p)
ln_faint_power_law = \
np.log(10) * (0.4 * (alpha + 1) * magnitude_deviation_exponent)
ln_bright_power_law = \
np.log(10) * (0.4 * (beta + 1) * magnitude_deviation_exponent)
ln_denominator = np.logaddexp(ln_faint_power_law, ln_bright_power_law)
if base10_log:
lumfunc_value = lg_Phi_star - ln_denominator / np.log(10)
else:
Phi_star = 10 ** lg_Phi_star
lumfunc_value = Phi_star / np.exp(ln_denominator)
return lumfunc_value
[docs]def quasar_hybrid_model_constraint(**model_parameters):
r"""Check whether the hybrid model constraint is satisfied.
Parameters
----------
**model_parameters
Hybrid model parameters. See :func:`quasar_hybrid_lumfunc` for
required parameters.
Returns
-------
bool
Whether or not the model constraint is satisfied.
"""
return model_parameters[r'\alpha'] < model_parameters[r'\beta']
[docs]def alpha_emitter_schechter_lumfunc(luminosity, redshift, *, base10_log=True,
**model_parameters):
r"""Evaluate the Schechter model for the H |alpha| -emitter
luminosity function at the given intrinsic luminosity and redshift.
Parameters
----------
luminosity : float
H |alpha| -emitter luminosity in dex (base-10 logarithm).
redshift : float
H |alpha| -emitter redshift.
base10_log : bool, optional
If `True` (default), return the base-10 logarithmic value.
**model_parameters
Schechter model parameters. Must be passed with the following
parameter names: ``r'\lg\Phi_{\ast0}'``, ``r'\lg{L_{\ast0}}'``,
``r'z_\mathrm{b}'``, ``r'\alpha'``, ``r'\delta'``, ``r'\epsilon'``.
Returns
-------
lumfunc_value : float
Predicted H |alpha| -emitter luminosity function value (in inverse
cubic Mpc per flux dex). Base-10 logarithmic value is returned if
`base10_log` is `True`.
Notes
-----
This function is not vectorised. Please use `numpy.vectorize` or
`functools` if you need vectorisation.
"""
lg_L, z = luminosity, redshift
lg_Phi_star0 = model_parameters[r'\lg\Phi_{\ast0}']
lg_L_star0 = model_parameters[r'\lg{L_{\ast0}}']
z_b = model_parameters[r'z_\mathrm{b}']
alpha = model_parameters[r'\alpha']
delta = model_parameters[r'\delta']
epsilon = model_parameters[r'\epsilon']
# Evaluate the model prediction.
if z <= z_b:
lg_Phi_star = lg_Phi_star0 + epsilon * np.log10(1 + z)
else:
lg_Phi_star = lg_Phi_star0 \
+ 2 * epsilon * np.log10(1 + z_b) \
- epsilon * np.log10(1 + z)
lg_y = lg_L - lg_L_star0 - delta * np.log10(1 + z)
if base10_log:
lumfunc_value = np.log10(np.log(10)) + lg_Phi_star \
+ (alpha + 1) * lg_y - 10 ** lg_y * np.log10(np.e)
else:
lumfunc_value = np.log(10) * 10 ** lg_Phi_star \
* 10 ** (lg_y * (alpha + 1)) * np.exp(- 10 ** lg_y)
return lumfunc_value
[docs]class LumFuncModeller:
r"""Luminosity function modeller predicting the comoving number
density, evolution bias and magnification bias for a given brightness
threshold.
Parameters
----------
model_lumfunc : callable
A parametric luminosity function model as a function of brightness
and redshift (in that order) in units of inverse cubic Mpc. If it
returns the luminosity function value in base-10 logarithm,
`exponentiation` should be set to `True`; if it accepts a boolean
parameter 'base10_log', this will be overriden to `False`.
model_parameters : dict
Model parameters passed to `model_lumfunc` as keyword arguments.
brightness_variable : {'luminosity', 'magnitude'}, str
Brightness variable of `model_lumfunc`, either 'luminosity' (in
dex) or 'magnitude'.
threshold_value : float
Brightness threshold value for `brightness_variable`. If
`brightness_variable` is 'luminosity', this is interpreted as a
flux limit and converted to an intrinsic luminosity value at the
tracer redshift using the luminosity distance.
normalise_threshold : bool, optional
If `True` (default is `False`), `threshold_value` is converted
from a flux limit to a luminosity value at `normalisation_redshift`
using the luminosity distance if `brightness_variable` is
'luminosity', or converted from an apparent magnitude limit to
an absolute magnitude value at `normalisation_redshift` using the
distance modulus if `brightness_variable` is 'magnitude'.
normalisation_redshift : float or None, optional
If not `None` (default) and `normalise_threshold` is `True`, this
redshift is used convert flux or apparent magnitude limit to an
intrinsic luminosity or absolute magnitude threshold.
cosmology : :class:`astropy.cosmology.core.Cosmology` or None, optional
Background cosmological model used to calculate the luminosity
distance or distance modulus (default is `None`).
exponentiation : bool, optional
If `True` (default is `False`), this assumes `model_lumfunc` only
returns the base-10 logarithmic luminosity function and
exponentiate its returned value to a power of 10.
Attributes
----------
luminosity_function : callable
Luminosity function of brightness and redshift variables only
(in that order) (in inverse cubic :math:`\mathrm{Mpc}/h` per unit
luminosity).
brightness_threshold : callable
Intrinsic luminosity or absoloute magnitude threshold, depending
on the input argument of `brightness_variable`, as a function
of redshift.
cosmology : :class:`astropy.cosmology.core.Cosmology`
Background cosmological model.
attrs : dict
Model parameters and luminosity variables stored in a dictionary.
Notes
-----
:attr:`luminosity_function` is a vectorised function.
"""
luminosity_bound = {
'luminosity': 100.,
'magnitude': -50.,
}
r"""float: Finite luminosity upper bound for numerical integration.
If :attr:`brightness_variable` is 'luminosity', the bound is given as
a base-10 logarithmic flux value in erg/s; else if 'magnitude',
the bound is dimensionless.
"""
redshift_stepsize = 0.001
r"""float: Redshift step size for numerical differentiation.
"""
def __init__(self, model_lumfunc, model_parameters, brightness_variable,
threshold_value, normalise_threshold=False,
normalisation_redshift=None, cosmology=None,
exponentiation=False):
self.attrs = {
'model_parameters': model_parameters,
'brightness_variable': brightness_variable,
}
self.cosmology = cosmology
if 'base10_log' in signature(model_lumfunc).parameters:
model_parameters.update({'base10_log': False})
# pylint: disable=unnecessary-lambda
self.luminosity_function = np.vectorize(
lambda lum, z: model_lumfunc(lum, z, **model_parameters)
)
elif exponentiation:
self.luminosity_function = np.vectorize(
lambda lum, z: 10 ** model_lumfunc(lum, z, **model_parameters)
)
else:
# pylint: disable=unnecessary-lambda
self.luminosity_function = np.vectorize(
lambda lum, z: model_lumfunc(lum, z, **model_parameters)
)
if brightness_variable == 'luminosity':
# pylint: disable=no-member
self.brightness_threshold = lambda z: np.log10(
4 * np.pi * threshold_value
* self.cosmology.luminosity_distance(z).to(units.cm).value ** 2
)
self._brightness_threshold = \
self.brightness_threshold(normalisation_redshift)
elif brightness_variable == 'magnitude':
self.brightness_threshold = lambda z: \
threshold_value - self.cosmology.distmod(z).value \
if normalise_threshold else threshold_value
self._brightness_threshold = \
self.brightness_threshold(normalisation_redshift) \
if normalise_threshold else threshold_value
[docs] @classmethod
def from_parameter_file(cls, parameter_file, **kwargs):
"""Instantiate the modeller class with model parameter values
loaded from a file.
Parameters
----------
parameter_file : *str or* :class:`pathlib.Path`
Path of the model parameter file.
**kwargs
Parameters other than `model_parameters` passed to the
modeller class as keyword arguments.
"""
with open(parameter_file, 'r') as pfile:
parameters = tuple(
map(
lambda var_name: var_name.strip(),
pfile.readline().strip("#").strip("\n").split(",")
)
)
estimates = tuple(map(float, pfile.readline().split(",")))
model_parameters = dict(zip(parameters, estimates))
return cls(model_parameters=model_parameters, **kwargs)
[docs] def comoving_number_density(self, redshift):
r"""Return the comoving number density :math:`\bar{n}(z)`
above the luminosity threshold at a given redshift.
Parameters
----------
redshift : float
Redshift.
Returns
-------
float
Comoving number density (in inverse cubic
:math:`\mathrm{Mpc}/h`).
"""
_comoving_number_density = np.abs(
quad(
self.luminosity_function,
self._brightness_threshold,
self.luminosity_bound[self.attrs['brightness_variable']],
args=(redshift,)
)[0]
) / self.cosmology.h ** 3
return _comoving_number_density
[docs] def evolution_bias(self, redshift):
r"""Return the evolution bias :math:`b_\mathrm{e}(z)` at a
given redshift.
Parameters
----------
redshift : float
Redshift.
Returns
-------
float
Evolution bias.
"""
_evolution_bias = - (1 + redshift) * derivative(
self.comoving_number_density, redshift, dx=self.redshift_stepsize
) / self.comoving_number_density(redshift)
return _evolution_bias
[docs] def magnification_bias(self, redshift):
r"""Return the magnification bias :math:`s(z)` at a given redshift.
Parameters
----------
redshift : float
Redshift.
Returns
-------
float
Magnification bias.
"""
if self.attrs['brightness_variable'] == 'luminosity':
prefactor = 2./5. / np.log(10)
elif self.attrs['brightness_variable'] == 'magnitude':
prefactor = 1 / np.log(10)
_magnification_bias = prefactor * self.luminosity_function(
self._brightness_threshold, redshift
) / self.comoving_number_density(redshift)
return _magnification_bias