diff --git a/CITATION.cff b/CITATION.cff index 5397545..88711b7 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -10,8 +10,8 @@ authors: email: "nikolina.sarcevic@gmail.com" orcid: "https://orcid.org/0000-0001-7301-6415" -version: "0.1.0" -date-released: 2026-03-01 +version: "0.2.0" +date-released: 2026-05-15 repository-code: "https://github.com/cosmo-hub/lfkit" url: "https://github.com/cosmo-hub/lfkit" diff --git a/docs/about/corr_overview.rst b/docs/about/corrections_overview.rst similarity index 100% rename from docs/about/corr_overview.rst rename to docs/about/corrections_overview.rst diff --git a/docs/about/index.rst b/docs/about/index.rst index a2ace1e..945352a 100644 --- a/docs/about/index.rst +++ b/docs/about/index.rst @@ -21,7 +21,7 @@ Core components :gutter: 3 .. grid-item-card:: - :link: corr_overview + :link: corrections_overview :link-type: doc :shadow: md @@ -33,7 +33,7 @@ Core components filter response curves and spectral energy distributions (SEDs). .. grid-item-card:: - :link: lf_overview + :link: photometry_overview :link-type: doc :shadow: md @@ -49,5 +49,5 @@ Core components :maxdepth: 1 :hidden: - corr_overview - lf_overview \ No newline at end of file + corrections_overview + photometry_overview \ No newline at end of file diff --git a/docs/about/lf_overview.rst b/docs/about/lf_overview.rst deleted file mode 100644 index 7adf3c9..0000000 --- a/docs/about/lf_overview.rst +++ /dev/null @@ -1,541 +0,0 @@ -.. |lfkitlogo| image:: /_static/logos/lfkit_logo.png - :alt: LFKit logo black - :width: 50px - -|lfkitlogo| Luminosity Functions -================================ - -LFKit provides a public luminosity function interface through -:class:`~lfkit.api.lumfunc.LuminosityFunction`. - -For most users, the recommended import is: - -.. code-block:: python - - from lfkit import LuminosityFunction - -The ``LuminosityFunction`` object defines and evaluates luminosity function -models in rest-frame absolute-magnitude space. It can also evaluate luminosity -functions from apparent magnitudes, convert between apparent and absolute -magnitudes, and compute number-density quantities for magnitude-limited catalog -selections. - -File reading is intentionally not handled by this API. Catalog-derived -luminosity function parameters, magnitude limits, or correction models should -be loaded elsewhere and passed in as scalars, arrays, or correction objects. - - -Magnitude-space luminosity functions ------------------------------------- - -LFKit luminosity functions are evaluated in rest-frame absolute magnitude -:math:`M`. - -The standard magnitude-space Schechter luminosity function is - -.. math:: - - \phi(M) = - 0.4 \ln(10) \, \phi_\star \, - x^{\alpha + 1} \exp(-x), - -where - -.. math:: - - x = 10^{-0.4(M - M_\star)}. - -Here: - -- :math:`\phi_\star` is the normalization, -- :math:`M_\star` is the characteristic magnitude, -- :math:`\alpha` is the faint-end slope. - -By convention, more negative magnitudes are brighter. - - -Standard Schechter model ------------------------- - -Use :meth:`~lfkit.api.lumfunc.LuminosityFunction.schechter` to construct a -Schechter luminosity function with fixed parameters. - -.. code-block:: python - - from lfkit import LuminosityFunction - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.1, - ) - - phi = lf.phi(absolute_mag, z) - -The returned ``phi`` values are number densities per magnitude evaluated at the -input absolute magnitudes. - - -Evolving Schechter model ------------------------- - -Use :meth:`~lfkit.api.lumfunc.LuminosityFunction.evolving_schechter` to build a -Schechter luminosity function with redshift-dependent parameters. - -The evolving model evaluates - -.. math:: - - \phi_\star(z), \quad M_\star(z), \quad \alpha(z), - -and then evaluates the Schechter function at each redshift. - -.. code-block:: python - - from lfkit import LuminosityFunction - - lf = LuminosityFunction.evolving_schechter( - phi_model="linear_p", - phi_kwargs={"phi_0_star": 1.0e-3, "p": 1.0}, - m_star_model="linear_q", - m_star_kwargs={"m_0_star": -20.5, "q": 1.2, "z_ref": 0.1}, - alpha_model="constant", - alpha_kwargs={"alpha": -1.1}, - ) - - phi = lf.phi(absolute_mag, z) - -You can also evaluate the evolving parameters directly: - -.. code-block:: python - - phi_star, m_star, alpha = lf.parameters(z) - - -Double Schechter model ----------------------- - -Use :meth:`~lfkit.api.lumfunc.LuminosityFunction.double_schechter` to build a -double-power-law Schechter-style luminosity function. - -.. code-block:: python - - from lfkit import LuminosityFunction - - lf = LuminosityFunction.double_schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.0, - beta=-1.5, - m_transition=-18.0, - ) - - phi = lf.phi(absolute_mag, z) - -This model is useful when an additional slope or transition is needed beyond -the standard Schechter form. - - -Built-in parameter evolution models ------------------------------------ - -Redshift-dependent luminosity function parameters are handled by parameter -evolution models. - -The built-in options are: - -.. list-table:: - :header-rows: 1 - :widths: 25 25 50 - - * - Parameter - - Model name - - Form - * - ``phi_star`` - - ``constant`` - - :math:`\phi_\star(z) = \phi_\star` - * - ``phi_star`` - - ``linear_p`` - - :math:`\phi_\star(z) = \phi_{0,\star} 10^{0.4 p z}` - * - ``M_star`` - - ``constant`` - - :math:`M_\star(z) = M_\star` - * - ``M_star`` - - ``linear_q`` - - :math:`M_\star(z) = M_{0,\star} - q(z - z_{\rm ref})` - * - ``alpha`` - - ``constant`` - - :math:`\alpha(z) = \alpha` - * - ``alpha`` - - ``linear`` - - :math:`\alpha(z) = \alpha_0 + \alpha_1(z - z_{\rm ref})` - -To inspect the available models: - -.. code-block:: python - - models = LuminosityFunction.available_parameter_models() - -Custom parameter evolution models can be registered through: - -.. code-block:: python - - LuminosityFunction.register_phi_star_model(name, model) - LuminosityFunction.register_m_star_model(name, model) - LuminosityFunction.register_alpha_model(name, model) - -Each registered model should accept redshift values as its first argument and -return NumPy-compatible parameter values. - - -Evaluating from apparent magnitudes ------------------------------------ - -The luminosity function object can evaluate from apparent magnitudes. - -In this case, LFKit first converts apparent magnitude :math:`m` into absolute -magnitude :math:`M` using - -.. math:: - - M = m - \mu(z) - K(z) + E(z), - -and then evaluates :math:`\phi(M, z)`. - -.. code-block:: python - - from lfkit import Corrections, LuminosityFunction - - corr = Corrections.poggianti( - band="r", - gal_type="E", - ) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.1, - ) - - phi = lf.phi_from_m( - cosmo, - z, - apparent_mag, - corrections=corr, - ) - -The ``corrections`` argument is optional. If omitted, LFKit evaluates the -conversion without k- or e-corrections. - - -Magnitude conversion helpers ----------------------------- - -The same object can convert between apparent and absolute magnitudes using the -LFKit magnitude convention. - -.. code-block:: python - - absolute_mag = lf.absolute_magnitude( - cosmo, - z, - apparent_mag, - corrections=corr, - ) - - apparent_mag = lf.apparent_magnitude( - cosmo, - z, - absolute_mag, - corrections=corr, - ) - -The absolute-magnitude limit corresponding to an apparent-magnitude catalog cut -can be computed with: - -.. code-block:: python - - m_limit = lf.absolute_magnitude_limit( - cosmo, - z, - m_lim=24.5, - corrections=corr, - ) - - -Integrated number density -------------------------- - -Use :meth:`~lfkit.api.lumfunc.LuminosityFunction.integrated_number_density` to -integrate the luminosity function over an absolute-magnitude range. - -.. code-block:: python - - n_total = lf.integrated_number_density( - z, - m_bright=-24.0, - m_faint=-14.0, - ) - -This computes the number density inside the requested absolute-magnitude -interval. - - -LF-weighted redshift density ----------------------------- - -Use :meth:`~lfkit.api.lumfunc.LuminosityFunction.lf_redshift_density` to build -an LF-weighted redshift trend for a magnitude-limited sample. - -This computes the magnitude-integrated luminosity function as a function of -redshift. Optionally, it can also include a redshift-dependent volume weight -and normalize the result over the supplied redshift grid. - -.. code-block:: python - - density = lf.lf_redshift_density( - cosmo, - z, - m_lim=24.5, - m_bright=-24.0, - corrections=corr, - normalize=True, - ) - -This is useful for constructing LF-dependent redshift trends. -Without a volume weight, the result is the luminosity function -selection factor only, not a full survey redshift distribution. - -A volume-weighted trend can be computed by passing a callable: - -.. code-block:: python - - import pyccl as ccl - - def volume_weight_fn(z): - a = 1.0 / (1.0 + z) - chi = ccl.comoving_radial_distance(cosmo, a) - h_over_h0 = ccl.h_over_h0(cosmo, a) - return chi**2 / h_over_h0 - - density = lf.lf_redshift_density( - cosmo, - z, - m_lim=24.5, - m_bright=-24.0, - corrections=corr, - volume_weight_fn=volume_weight_fn, - normalize=True, - ) - - -Magnitude-limited catalog completeness --------------------------------------- - -A luminosity function can be split into observed and missing components for a -magnitude-limited catalog. - -The core idea is: - -1. convert the apparent catalog limit :math:`m_{\rm lim}` into an - absolute-magnitude limit :math:`M_{\rm lim}(z)`, -2. integrate the luminosity function over a finite absolute-magnitude range, -3. split the population into observed and missing pieces. - -The limiting absolute magnitude is - -.. math:: - - M_{\rm lim}(z) = m_{\rm lim} - \mu(z) - K(z) + E(z). - - -Observed number density -^^^^^^^^^^^^^^^^^^^^^^^ - -The observed, or in-catalog, number density is - -.. math:: - - n_{\rm obs}(z) = - \int_{M_{\rm bright}}^{\min[M_{\rm lim}(z), M_{\rm faint}]} - \phi(M, z) \, dM. - -Use: - -.. code-block:: python - - n_obs = lf.observed_number_density( - cosmo, - z, - m_lim=24.5, - m_bright=-24.0, - m_faint=-14.0, - corrections=corr, - ) - - -Missing number density -^^^^^^^^^^^^^^^^^^^^^^ - -The missing, or out-of-catalog, number density is - -.. math:: - - n_{\rm miss}(z) = - \int_{\max[M_{\rm lim}(z), M_{\rm bright}]}^{M_{\rm faint}} - \phi(M, z) \, dM. - -Use: - -.. code-block:: python - - n_miss = lf.missing_number_density( - cosmo, - z, - m_lim=24.5, - m_bright=-24.0, - m_faint=-14.0, - corrections=corr, - ) - - -Completeness fractions -^^^^^^^^^^^^^^^^^^^^^^ - -The catalog completeness fraction is - -.. math:: - - f_{\rm obs}(z) = - \frac{n_{\rm obs}(z)} - {n_{\rm obs}(z) + n_{\rm miss}(z)}. - -The out-of-catalog fraction is - -.. math:: - - f_{\rm miss}(z) = 1 - f_{\rm obs}(z). - -Use: - -.. code-block:: python - - f_obs = lf.catalog_completeness( - cosmo, - z, - m_lim=24.5, - m_bright=-24.0, - m_faint=-14.0, - corrections=corr, - ) - - f_miss = lf.out_of_catalog_fraction( - cosmo, - z, - m_lim=24.5, - m_bright=-24.0, - m_faint=-14.0, - corrections=corr, - ) - - -Avoiding double-counted evolution ---------------------------------- - -There are two different places where luminosity evolution can enter an -analysis: - -1. the apparent-to-absolute magnitude conversion through :math:`E(z)`, -2. the luminosity function model through redshift evolution of :math:`M_\star(z)`. - -For this reason, be careful when using an evolving Schechter model with -non-zero ``linear_q`` evolution together with an explicit evolution correction. - -This is not always wrong, but the two definitions should be intentionally -separated. - - -Typical workflow ----------------- - -A typical luminosity function workflow is: - -1. define a cosmology, -2. define a luminosity function model, -3. evaluate :math:`\phi(M, z)` directly or :math:`\phi(m, z)` from apparent - magnitudes, -4. compute integrated number densities, LF-weighted redshift densities, or - observed/missing number densities for magnitude-limited selections. - -For example: - -.. code-block:: python - - from lfkit import Corrections, LuminosityFunction - - corr = Corrections.poggianti( - band="r", - gal_type="E", - ) - - lf = LuminosityFunction.evolving_schechter( - phi_model="linear_p", - phi_kwargs={"phi_0_star": 1.0e-3, "p": 1.0}, - m_star_model="linear_q", - m_star_kwargs={"m_0_star": -20.5, "q": 1.2, "z_ref": 0.1}, - alpha_model="constant", - alpha_kwargs={"alpha": -1.1}, - ) - - phi = lf.phi_from_m( - cosmo, - z, - apparent_mag, - corrections=corr, - ) - - n_obs = lf.observed_number_density( - cosmo, - z, - m_lim=24.5, - m_bright=-24.0, - m_faint=-14.0, - corrections=corr, - ) - - density = lf.lf_redshift_density( - cosmo, - z, - m_lim=24.5, - m_bright=-24.0, - corrections=corr, - normalize=True, - ) - - -Lower-level functions ---------------------- - -The high-level API is recommended for most scripts, examples, notebooks, and -downstream package interfaces. - -The lower-level functions in ``lfkit.photometry`` are useful when: - -- testing individual mathematical pieces, -- adding new luminosity function models, -- adding new parameter evolution models, -- debugging the magnitude convention, -- building new public API objects, -- integrating LFKit into specialized workflows. - - -What LFKit does not do here ---------------------------- - -LFKit does not read survey catalogs, apply angular masks, or model survey-area -incompleteness in this layer. - -For magnitude-limited catalog applications, LFKit models the part of the -selection function that comes from the apparent-magnitude limit. Other effects, -such as unobserved sky area, angular masks, spectroscopic targeting, blending, -or color cuts, should be handled by the calling analysis code. diff --git a/docs/about/photometry_overview.rst b/docs/about/photometry_overview.rst new file mode 100644 index 0000000..7d680bb --- /dev/null +++ b/docs/about/photometry_overview.rst @@ -0,0 +1,421 @@ +.. |lfkitlogo| image:: /_static/logos/lfkit_logo.png + :alt: LFKit logo black + :width: 50px + +|lfkitlogo| Photometry Overview +=============================== + +A luminosity function describes the abundance of galaxies as a function of +intrinsic brightness. It is a number-density distribution: it tells us how many +galaxies exist per unit volume and per luminosity or magnitude interval. If it +is normalized by the total number density, it can also be interpreted as a +probability density over luminosity or magnitude. + +In luminosity units, the luminosity function is often written as +:math:`\Phi(L, z)`, where :math:`L` is luminosity and :math:`z` is redshift. +The quantity + +.. math:: + + \Phi(L, z)\,\mathrm{d}L + +is the comoving number density of galaxies with luminosities between +:math:`L` and :math:`L+\mathrm{d}L` at redshift :math:`z`. + +Equivalently, in absolute magnitude space, the luminosity function is written as +:math:`\Phi(M, z)`, where :math:`M` is absolute magnitude. In this case, + +.. math:: + + \Phi(M, z)\,\mathrm{d}M + +is the comoving number density of galaxies with absolute magnitudes between +:math:`M` and :math:`M+\mathrm{d}M`. + +Luminosity and absolute magnitude are intrinsic quantities. Surveys instead +measure fluxes, which are usually reported as apparent magnitudes :math:`m`. +Therefore, using a luminosity function with observed data usually requires +converting between apparent magnitude and absolute magnitude using a luminosity +distance, and sometimes additional photometric corrections. + +For worked examples of the LFKit public API, see the dedicated example pages: + +- :doc:`../examples/luminosity_function_models` +- :doc:`../examples/magnitudes_and_luminosities` +- :doc:`../examples/magnitude_integrals` +- :doc:`../examples/redshift_density` +- :doc:`../examples/catalog_completeness` + + +Luminosity and magnitude +------------------------ + +Luminosity :math:`L` is the total energy emitted by a galaxy per unit time. +Flux :math:`F` is the energy received by an observer per unit area and per unit +time. The two are related by the luminosity distance :math:`d_L`: + +.. math:: + + F = \frac{L}{4\pi d_L^2}. + +This is why luminosity is not directly observed. It is inferred from the +measured flux once a distance has been specified. + +Astronomy often uses magnitudes instead of luminosities. Apparent magnitude +:math:`m` describes how bright an object appears to the observer. Absolute +magnitude :math:`M` describes the intrinsic brightness of the object, defined as +the apparent magnitude it would have at a standard reference distance. + +By convention, more negative magnitudes are brighter. A galaxy with +:math:`M=-22` is brighter than a galaxy with :math:`M=-18`. + +Luminosity and absolute magnitude are related logarithmically. Relative to a +reference luminosity :math:`L_0`, the absolute magnitude can be written as + +.. math:: + + M = M_0 - 2.5 \log_{10}\left(\frac{L}{L_0}\right), + +or equivalently, + +.. math:: + + \frac{L}{L_0} + = + 10^{-0.4(M - M_0)}. + +Here :math:`M_0` is the magnitude corresponding to the reference luminosity +:math:`L_0`. This relation is why brighter objects have smaller, more negative +magnitudes. + +The conversion between apparent and absolute magnitude can be written as + +.. math:: + + M = m - \mu(z) - K(z) + E(z), + +where: + +- :math:`M` is absolute magnitude, +- :math:`m` is apparent magnitude, +- :math:`\mu(z)` is the distance modulus, +- :math:`K(z)` is the k-correction, +- :math:`E(z)` is the evolution correction, +- :math:`z` is redshift. + +The distance modulus encodes the effect of distance. The k-correction accounts +for observing a redshifted galaxy spectrum through a fixed bandpass. The +evolution correction accounts for intrinsic luminosity evolution of the galaxy +population, depending on the convention adopted in the analysis. + + +Magnitude-space luminosity functions +------------------------------------ + +LFKit works primarily in rest-frame absolute magnitude space. This is a natural +choice for galaxy luminosity functions because absolute magnitude is an +intrinsic brightness variable. + +A magnitude-space luminosity function :math:`\Phi(M, z)` gives the number +density of galaxies per unit magnitude. If :math:`\Phi(M, z)` has units of +:math:`{\rm Mpc}^{-3}\,{\rm mag}^{-1}`, then integrating it over a finite +absolute magnitude interval gives a number density in +:math:`{\rm Mpc}^{-3}`: + +.. math:: + + n(z) = + \int_{M_{\rm bright}}^{M_{\rm faint}} + \Phi(M, z)\,\mathrm{d}M. + +Here: + +- :math:`n(z)` is the integrated number density at redshift :math:`z`, +- :math:`M_{\rm bright}` is the bright absolute magnitude limit, +- :math:`M_{\rm faint}` is the faint absolute magnitude limit, +- :math:`\Phi(M, z)` is the luminosity function per unit magnitude. + +Because brighter galaxies have more negative magnitudes, +:math:`M_{\rm bright}` is usually more negative than :math:`M_{\rm faint}`. + + +The Schechter luminosity function +--------------------------------- + +A common model for galaxy luminosity functions is the Schechter function. In +luminosity space, it is usually written as + +.. math:: + + \Phi(L)\,\mathrm{d}L = + \phi_\star + \left(\frac{L}{L_\star}\right)^\alpha + \exp\left(-\frac{L}{L_\star}\right) + \frac{\mathrm{d}L}{L_\star}. + +Here: + +- :math:`L` is galaxy luminosity, +- :math:`L_\star` is the characteristic luminosity, +- :math:`\phi_\star` is the normalization, +- :math:`\alpha` is the faint-end slope. + +The Schechter form combines two behaviours. At low luminosities, the model is +approximately a power law controlled by :math:`\alpha`. At high luminosities, +the exponential term suppresses the abundance of very bright galaxies. + + +Schechter function in magnitude space +------------------------------------- + +The magnitude-space form follows from the luminosity-space form by using the +luminosity ratio + +.. math:: + + \frac{L}{L_\star} + = + 10^{-0.4(M - M_\star)}. + +The change of variables from luminosity to magnitude also introduces the factor +:math:`0.4\ln(10)`. + +In absolute magnitude space, the Schechter luminosity function can be written as + +.. math:: + + \Phi(M) = + 0.4 \ln(10) \, \phi_\star \, + x^{\alpha + 1} \exp(-x), + +with + +.. math:: + + x = 10^{-0.4(M - M_\star)}. + +Here: + +- :math:`M` is absolute magnitude, +- :math:`M_\star` is the characteristic absolute magnitude, +- :math:`\phi_\star` is the normalization, +- :math:`\alpha` is the faint-end slope, +- :math:`x` is the luminosity ratio :math:`L/L_\star` written in magnitude form. + +The parameter :math:`M_\star` marks the transition between the power-law part of +the luminosity function and the exponential bright-end cutoff. The parameter +:math:`\alpha` controls how rapidly the abundance rises toward fainter +magnitudes. More negative values of :math:`\alpha` produce a steeper faint end. + +The normalization :math:`\phi_\star` sets the overall abundance scale. If +:math:`\phi_\star` is supplied in :math:`{\rm Mpc}^{-3}`, then +:math:`\Phi(M)` is usually interpreted as a number density per magnitude, +:math:`{\rm Mpc}^{-3}\,{\rm mag}^{-1}`. + + +Redshift evolution +------------------ + +Galaxy populations evolve with redshift, so luminosity function parameters are +often allowed to depend on :math:`z`. A redshift-dependent Schechter model can +be written schematically as + +.. math:: + + \Phi(M, z) = + \Phi\left(M \mid \phi_\star(z), M_\star(z), \alpha(z)\right). + +Here: + +- :math:`\phi_\star(z)` describes evolution in the overall normalization, +- :math:`M_\star(z)` describes evolution in the characteristic magnitude, +- :math:`\alpha(z)` describes evolution in the faint-end slope. + +Changing :math:`\phi_\star(z)` changes the total abundance scale. Changing +:math:`M_\star(z)` shifts the characteristic magnitude where the luminosity +function turns over. Changing :math:`\alpha(z)` mainly changes the relative +abundance of faint galaxies. + +Different analyses use different parameterizations for this evolution. For +example, one may use a constant parameter, a linear trend with redshift, or a +survey-specific empirical model. The important point is that the luminosity +function model and the photometric evolution correction should be defined +consistently. + + +Apparent magnitude limits +------------------------- + +Observed catalogs are often selected by an apparent magnitude limit +:math:`m_{\rm lim}`. A luminosity function, however, is usually evaluated in +absolute magnitude space. The corresponding absolute magnitude limit is + +.. math:: + + M_{\rm lim}(z) + = + m_{\rm lim} - \mu(z) - K(z) + E(z). + +Here: + +- :math:`M_{\rm lim}(z)` is the redshift-dependent absolute magnitude limit, +- :math:`m_{\rm lim}` is the apparent magnitude limit of the catalog, +- :math:`\mu(z)` is the distance modulus, +- :math:`K(z)` is the k-correction, +- :math:`E(z)` is the evolution correction. + +The dependence on :math:`z` is important. The same apparent magnitude limit +corresponds to different intrinsic luminosities at different redshifts. At +higher redshift, a fixed apparent magnitude cut usually selects only brighter +galaxies. + +This is the basic reason magnitude-limited samples become increasingly +incomplete for faint galaxies at larger distances. + + +Number-density integrals +------------------------ + +Integrating a luminosity function over magnitude gives the number density of +galaxies inside a chosen magnitude range: + +.. math:: + + n(z) = + \int_{M_{\rm bright}}^{M_{\rm faint}} + \Phi(M, z)\,\mathrm{d}M. + +This quantity is useful when the luminosity function is used to predict the +abundance of a galaxy sample. Changing the integration limits changes the +population being counted. A brighter cut selects only luminous galaxies, while a +fainter cut includes more of the faint galaxy population. + +For a magnitude-limited catalog, the observed number density can be written as + +.. math:: + + n_{\rm obs}(z) = + \int_{M_{\rm bright}}^{\min[M_{\rm lim}(z), M_{\rm faint}]} + \Phi(M, z)\,\mathrm{d}M. + +The missing, or out-of-catalog, number density can be written as + +.. math:: + + n_{\rm miss}(z) = + \int_{\max[M_{\rm lim}(z), M_{\rm bright}]}^{M_{\rm faint}} + \Phi(M, z)\,\mathrm{d}M. + +Here: + +- :math:`n_{\rm obs}(z)` is the number density above the catalog selection, +- :math:`n_{\rm miss}(z)` is the number density below the catalog selection, +- :math:`M_{\rm lim}(z)` is the absolute magnitude limit implied by the apparent + magnitude cut. + +These definitions split the same reference luminosity function into the part +that is observable and the part that is missed by the magnitude limit. + + +Completeness fractions +---------------------- + +The catalog completeness fraction is the fraction of the reference population +that is retained by the magnitude limit: + +.. math:: + + f_{\rm obs}(z) = + \frac{n_{\rm obs}(z)} + {n_{\rm obs}(z) + n_{\rm miss}(z)}. + +The missing fraction is + +.. math:: + + f_{\rm miss}(z) = 1 - f_{\rm obs}(z). + +Here: + +- :math:`f_{\rm obs}(z)` is the observed or in-catalog fraction, +- :math:`f_{\rm miss}(z)` is the missing or out-of-catalog fraction. + +These fractions only describe the selection caused by the apparent magnitude +limit. Other survey effects, such as masks, blending, targeting, color cuts, or +spectroscopic failures, are separate selection effects and should be modeled +elsewhere. + + +LF-weighted redshift trends +--------------------------- + +A luminosity function can also be used to build a redshift-dependent selection +trend. For a magnitude-limited sample, one common ingredient is the +magnitude-integrated luminosity function as a function of redshift: + +.. math:: + + S(z) = + \int_{M_{\rm bright}}^{M_{\rm lim}(z)} + \Phi(M, z)\,\mathrm{d}M. + +Here: + +- :math:`S(z)` is the luminosity function selection factor, +- :math:`M_{\rm bright}` is the bright integration limit, +- :math:`M_{\rm lim}(z)` is the redshift-dependent faint limit implied by the + apparent magnitude cut. + +This selection factor describes how much of the luminosity function is retained +at each redshift. It is not by itself a full survey redshift distribution. To +build a redshift distribution, it is often combined with a volume factor or +another redshift-dependent weight: + +.. math:: + + n(z) \propto S(z)\,W(z), + +where :math:`W(z)` is a chosen redshift or volume weight. + +The exact form of :math:`W(z)` depends on the analysis. For example, it may +represent the comoving volume element, an input parent population, or another +survey-specific weighting function. + + +Luminosity evolution and double counting +---------------------------------------- + +Luminosity evolution can enter an analysis in more than one place. It may appear +in the photometric conversion through an evolution correction :math:`E(z)`, or +it may appear directly in the luminosity function through a redshift-dependent +parameter such as :math:`M_\star(z)`. + +These two choices are not automatically equivalent. Using both at the same time +can be correct if the conventions are defined carefully, but it can also double +count evolution if both terms describe the same physical effect. + +A useful rule is to keep the roles separate: + +- :math:`E(z)` belongs to the apparent-to-absolute magnitude conversion, +- :math:`M_\star(z)`, :math:`\phi_\star(z)`, and :math:`\alpha(z)` belong to the + luminosity function model. + +The analysis should define which part of the evolution is handled by the +photometric correction and which part is handled by the luminosity function +parameterization. + + +What LFKit models +----------------- + +LFKit focuses on the luminosity function side of these calculations. In this +layer, the relevant ingredients are intrinsic luminosity or magnitude, +redshift-dependent luminosity function parameters, apparent-to-absolute +magnitude conversions, and number-density integrals. + +LFKit does not model every survey selection effect. Angular masks, survey area, +blending, targeting, spectroscopic success rates, and other catalog-level +effects should be handled by the calling analysis code. + +The theory described here is implemented in the public LFKit interface and shown +with executable examples in the example pages. \ No newline at end of file diff --git a/docs/citation.rst b/docs/citation.rst new file mode 100644 index 0000000..4e6f8cd --- /dev/null +++ b/docs/citation.rst @@ -0,0 +1,46 @@ +.. |lfkitlogo| image:: /_static/logos/lfkit_logo.png + :alt: LFKit logo black + :width: 50px + + +|lfkitlogo| Citation +==================== + +If you use LFKit in your work, please cite it using the citation metadata +provided with the repository. + +How to cite LFKit +----------------- + +The recommended citation is stored in the project repository as +``CITATION.cff``. GitHub uses this file to generate citation information +automatically. + +Current citation metadata: + +.. code-block:: yaml + + cff-version: 1.2.0 + message: > + If you use LFKit in your research, cite it using the metadata below. + + title: "LFKit: Luminosity functions and photometric corrections toolkit" + + authors: + - family-names: "Šarčević" + given-names: "Nikolina" + email: "nikolina.sarcevic@gmail.com" + orcid: "https://orcid.org/0000-0001-7301-6415" + + version: "0.2.0" + date-released: 2026-05-15 + + repository-code: "https://github.com/cosmology-kit/lfkit" + url: "https://github.com/cosmology-kit/lfkit" + + license: MIT + + abstract: > + LFKit is a modular Python toolkit for luminosity function modeling, + k-corrections, e-corrections, and photometric response handling, + designed for cosmological analyses. \ No newline at end of file diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 0000000..a716862 --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1,128 @@ +.. _contributing: + +.. |lfkitlogo| image:: /_static/logos/lfkit_logo.png + :alt: LFKit logo black + :width: 50px + + +|lfkitlogo| Contributing +======================== + +Contributions in any shape or form are appreciated. +Below are some minimal guidelines to get started. + +Development of ``lfkit`` is organised around `the Github repository `__. +Contributing usually requires `setting up an account `__ on Github. +No worries, it is free of charge! + +When submitting contributions, please write in clear and correct English using full sentences. +Be concise and avoid unnecessarily formulaic descriptions. + +*********************** +Submitting bugs or code +*********************** + +Submitting a bug report or feature request can be done by `opening an issue on Github `__. +In the case of a bug report please make sure to + +- describe the expected behaviour, +- describe the actual behaviour, +- the version(s) of ``lfkit`` that produce the bug, +- any specific environment details. + +In the case of a feature request, please make sure to + +- describe the feature, +- describe why the feature is useful. + +Submitting a code contribution can be done by `opening a pull request on Github `__. +In the pull request, please make sure of the following: + +- The description of the pull request contains a high-level overview of what is implemented in the contribution. +- The description describes the reason for the addition. + Specifically, it describes what problem it is supposed to solve. + If the pull request resolves a bug or implements a feature for which an issue exists, make sure to refer to this. +- The ``lfkit`` workflows complete successfully. + The workflows will activate when a pull request is created, but can also be run locally on your computer as described below. + +*************************** +Running ``lfkit`` workflows +*************************** + +``lfkit`` uses `tox `__ to run its workflows. +For development, clone the repository and install the development dependencies:: + + git clone https://github.com/cosmology-kit/lfkit.git + cd lfkit + pip install -e ".[dev]" + +All workflows can be run consecutively by calling tox from the project root directory:: + + tox + +Specific workflows can also be run in isolation. +The following workflows are provided: + +======= +Linting +======= + +Code for ``lfkit`` must comply with `PEP-8 `__. +Comments and docstrings must be compatible with `Google-style comments and docstrings `__. + +The linting workflow can be run using:: + + tox -e lint + +Note that tox uses `ruff `__ as the actual linter. +Options can be passed to `ruff check` by supplying them as command-line arguments to tox. +For example, to address fixable linting errors, use:: + + tox -e lint -- --fix + +============= +Documentation +============= + +Documentation is written in `reStructuredText `__. +Code documentation is generated from docstrings, and the documentation can be built locally with:: + + tox -e docs + +The new documentation will be placed in the ``docs/_build`` directory. +Newly created rST files may need to be added to the appropriate table of contents files by hand. + +Note that ``tox -e docs`` will use the ``html`` builder of sphinx-build. +A different builder can be selected by passing it as a command-line argument to tox. +For example, to run the doctest build:: + + tox -e docs -- doctest + +A list of supported options can be found on the `Builders `__ page of Sphinx. + +The entire documentation for the head of the main branch and all release tags can be generated using:: + + tox -e docs-releases + +======= +Testing +======= + +Contributions that contain new code must include tests in the appropriate files in the ``tests`` directory. +The test suite can be run locally with:: + + tox -m test + +Note that this will attempt to run the test suite for all supported Python versions. +It will automatically skip versions which are not locally available. + +If the test suite must be run for a specific version of Python, that specific environment can be called. +For example, to test against Python 3.13:: + + tox -e py313 + +For development it is sometimes useful to run a single test, as this is much faster than running the entire test suite. +To do so, add it as a command-line argument separated from the ``tox`` invocation by a ``--``. +For example:: + + tox -m test -- tests/test_luminosity_function.py diff --git a/docs/examples/api_overview.rst b/docs/examples/api_overview.rst deleted file mode 100644 index a87b752..0000000 --- a/docs/examples/api_overview.rst +++ /dev/null @@ -1,553 +0,0 @@ -.. |lfkitlogo| image:: /_static/logos/lfkit_logo-icon.png - :alt: LFKit logo - :width: 50px - -|lfkitlogo| API overview -======================== - -This page gives a high-level overview of how LFKit's public API is organized. - -LFKit is built around a small number of user-facing entry points. The goal is -that most users should not need to import low-level functions from -``lfkit.photometry`` directly. Instead, they can start from the public API -objects and use grouped namespaces for related calculations. - -The main API areas are: - -* luminosity function models, -* conditional luminosity function models, -* luminosity function integrals, -* magnitude-limited completeness, -* LF-weighted redshift-density calculations, -* magnitude and luminosity conversions, -* photometric corrections. - -The detailed examples are split across separate pages. This page is only a map -of the public API and the intended workflow. - - -Main entry points ------------------ - -The most important public objects are imported from :mod:`lfkit`: - -.. code-block:: python - - from lfkit import LuminosityFunction - from lfkit import Corrections - -If conditional luminosity functions are exposed through a separate public class, -the intended import should be: - -.. code-block:: python - - from lfkit import ConditionalLuminosityFunction - -The public API is organized so that users first create a model object and then -call grouped methods from that object. - -For example, a standard luminosity function workflow starts with: - -.. code-block:: python - - from lfkit import LuminosityFunction - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.1, - ) - - phi = lf.phi(-20.0) - -The object ``lf`` stores the chosen luminosity function model and exposes -namespaces for common calculations. - - -Luminosity-function API ------------------------ - -The :class:`lfkit.LuminosityFunction` object is the main interface for ordinary -luminosity function models. - -It provides constructors for supported LF parameterizations, for example: - -.. code-block:: python - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.1, - ) - - lf = LuminosityFunction.double_schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.1, - beta=-1.5, - m_transition=-19.5, - ) - - lf = LuminosityFunction.evolving_schechter( - phi_model="linear_p", - phi_kwargs={"phi_0_star": 1.0e-3, "p": 0.7}, - m_star_model="linear_q", - m_star_kwargs={"m_0_star": -20.5, "q": 0.8, "z_ref": 0.1}, - alpha_model="constant", - alpha_kwargs={"alpha": -1.1}, - ) - -Once created, the object can evaluate the luminosity function: - -.. code-block:: python - - phi = lf.phi(absolute_mag) - -For evolving models, pass redshift as well: - -.. code-block:: python - - phi = lf.phi(absolute_mag, redshift) - -The same object can also expose the redshift-dependent parameters: - -.. code-block:: python - - phi_star, m_star, alpha = lf.parameters(redshift) - - -Grouped namespaces ------------------- - -A :class:`lfkit.LuminosityFunction` object groups related functionality into -small namespaces. - -The main namespaces are: - -.. list-table:: - :header-rows: 1 - :widths: 25 55 20 - - * - Namespace - - Purpose - - Example - * - ``lf.integrals`` - - Magnitude integrals of the bound luminosity function. - - ``lf.integrals.number_density(...)`` - * - ``lf.redshift_density`` - - Magnitude-limited and volume-weighted redshift-density calculations. - - ``lf.redshift_density.weighted(...)`` - * - ``lf.completeness`` - - Magnitude-limited catalog completeness and missing fractions. - - ``lf.completeness.catalog_fraction(...)`` - * - ``lf.magnitudes`` - - Apparent/absolute magnitude and distance-modulus helpers. - - ``lf.magnitudes.absolute_from_luminosity_distance(...)`` - * - ``lf.luminosities`` - - Luminosity-ratio and Schechter helper functions. - - ``lf.luminosities.ratio_from_magnitudes(...)`` - -This keeps the public API readable. Users can discover functionality from the -model object without needing to remember which low-level module contains each -calculation. - - -Magnitude integrals -------------------- - -The ``integrals`` namespace evaluates integrals over absolute magnitude for the -luminosity function stored in ``lf``. - -Typical methods include number density, luminosity density, mean luminosity, -and selection-weighted number density. - -.. code-block:: python - - number_density = lf.integrals.number_density( - redshift, - m_bright=-24.0, - m_faint=-16.0, - n_m=800, - ) - - luminosity_density = lf.integrals.luminosity_density( - redshift, - m_bright=-24.0, - m_faint=-16.0, - n_m=800, - ) - - mean_luminosity = lf.integrals.mean_luminosity( - redshift, - m_bright=-24.0, - m_faint=-16.0, - n_m=800, - ) - -Selection weights can be supplied through a callable: - -.. code-block:: python - - def selection_fn(absolute_mag, z): - limiting_mag = -18.5 - 1.2 * z - width = 0.35 - return 1.0 / (1.0 + np.exp((absolute_mag - limiting_mag) / width)) - - selected_density = lf.integrals.selection_weighted_number_density( - redshift, - selection_fn=selection_fn, - m_bright=-24.0, - m_faint=-14.0, - n_m=800, - ) - -The luminosity function callable is inserted internally by the API, so users -only provide the redshift grid, magnitude bounds, and optional selection -function. - - -Completeness calculations -------------------------- - -The ``completeness`` namespace handles magnitude-limited catalog calculations. - -These methods are useful when a survey has an apparent-magnitude limit -``m_lim`` and the user wants to know which part of the intrinsic luminosity -function is visible at each redshift. - -Typical quantities are: - -.. code-block:: python - - observed = lf.completeness.observed_number_density( - cosmo, - redshift, - m_lim=24.0, - m_bright=-24.0, - m_faint=-16.0, - n_m=800, - h=0.7, - ) - - missing = lf.completeness.missing_number_density( - cosmo, - redshift, - m_lim=24.0, - m_bright=-24.0, - m_faint=-16.0, - n_m=800, - h=0.7, - ) - - catalog_fraction = lf.completeness.catalog_fraction( - cosmo, - redshift, - m_lim=24.0, - m_bright=-24.0, - m_faint=-16.0, - n_m=800, - h=0.7, - ) - - out_of_catalog_fraction = lf.completeness.out_of_catalog_fraction( - cosmo, - redshift, - m_lim=24.0, - m_bright=-24.0, - m_faint=-16.0, - n_m=800, - h=0.7, - ) - -The same namespace also exposes the absolute-magnitude limit implied by an -apparent-magnitude cut: - -.. code-block:: python - - m_limit = lf.completeness.absolute_magnitude_limit( - cosmo, - redshift, - m_lim=24.0, - h=0.7, - ) - -This is often the first diagnostic to check before interpreting completeness -fractions. - - -Redshift-density calculations ------------------------------ - -The ``redshift_density`` namespace is for building LF-weighted redshift trends. - -These methods are useful when LFKit is used as an ingredient in survey -forecasting or tomography construction. - -A magnitude-limited number density can be computed with: - -.. code-block:: python - - number_density = lf.redshift_density.integrated_number_density( - redshift, - m_lim=24.0, - m_bright=-24.0, - luminosity_distance_mpc_fn=luminosity_distance_mpc, - n_m=800, - ) - -A volume-weighted redshift trend can be computed with: - -.. code-block:: python - - weighted_density = lf.redshift_density.weighted( - redshift, - m_lim=24.0, - m_bright=-24.0, - luminosity_distance_mpc_fn=luminosity_distance_mpc, - volume_weight_fn=volume_weight, - n_m=800, - ) - -Here ``luminosity_distance_mpc_fn`` and ``volume_weight_fn`` are callables -supplied by the user or by another cosmology package. - -This design keeps LFKit independent of one specific cosmology backend for these -generic redshift-density utilities. - - -Magnitude and luminosity helpers --------------------------------- - -The ``magnitudes`` namespace provides public helpers for converting between -apparent magnitude, absolute magnitude, and luminosity distance. - -For example: - -.. code-block:: python - - absolute_mag = lf.magnitudes.absolute_from_luminosity_distance( - apparent_mag, - luminosity_distance_mpc, - ) - - apparent_mag = lf.magnitudes.apparent_from_luminosity_distance( - absolute_mag, - luminosity_distance_mpc, - ) - -The ``luminosities`` namespace provides luminosity-ratio helpers: - -.. code-block:: python - - luminosity_ratio = lf.luminosities.ratio_from_magnitudes( - absolute_mag, - m_star, - ) - -These helpers are useful for diagnostics, selection functions, and examples -where the user wants to inspect the magnitude-luminosity mapping directly. - - -Conditional luminosity function API ------------------------------------ - -Conditional luminosity functions describe luminosity distributions conditioned -on another variable, usually halo mass. - -They should be kept conceptually separate from ordinary luminosity functions. -A conditional luminosity function object should be responsible for CLF models, -while :class:`lfkit.LuminosityFunction` remains responsible for ordinary LF -models. - -A typical CLF workflow should look like: - -.. code-block:: python - - clf = ConditionalLuminosityFunction.schechter( - phi_star=1.0, - l_star=1.0e10, - alpha=-1.1, - ) - - phi = clf.phi(luminosity, halo_mass) - -or, for magnitude-based CLF models: - -.. code-block:: python - - phi = clf.phi(absolute_mag, halo_mass) - -The detailed CLF examples should live on a separate page. This overview page -only records the architectural boundary: - -* ordinary LF models belong to ``LuminosityFunction``, -* conditional LF models belong to ``ConditionalLuminosityFunction``, -* shared numerical helpers should stay in lower-level utility modules, -* the public API should avoid duplicating the low-level model code. - - -Photometric corrections ------------------------ - -Photometric corrections are exposed separately through :class:`lfkit.Corrections`. - -This keeps corrections independent from the luminosity function model itself. -Users can construct or evaluate corrections and then pass them into magnitude, -completeness, or redshift-density calculations when needed. - -For example, correction callables can be passed into LF calculations that need -k-corrections or evolution corrections: - -.. code-block:: python - - number_density = lf.redshift_density.integrated_number_density( - redshift, - m_lim=24.0, - m_bright=-24.0, - luminosity_distance_mpc_fn=luminosity_distance_mpc, - k_correction_fn=k_correction, - e_correction_fn=e_correction, - n_m=800, - ) - -The sign convention used by the magnitude helpers is: - -.. math:: - - M = m - \mu - K + E, - -and equivalently, - -.. math:: - - m = M + \mu + K - E. - -This means that corrections can be supplied without hard-coding one correction -backend into the luminosity function API. - - -Available models ----------------- - -The API can report which models are registered. - -For luminosity function models: - -.. code-block:: python - - from lfkit import LuminosityFunction - - LuminosityFunction.available_models() - LuminosityFunction.available_from_m_models() - LuminosityFunction.available_parameter_models() - -For conditional luminosity function models, the matching API should be: - -.. code-block:: python - - from lfkit import ConditionalLuminosityFunction - - ConditionalLuminosityFunction.available_models() - -These discovery methods are useful in examples, notebooks, and validation -scripts. - - -Recommended example-page split ------------------------------- - -The detailed examples should stay split by topic rather than collected into one -large page. - -A useful organization is: - -.. list-table:: - :header-rows: 1 - :widths: 28 52 - - * - Page - - Contents - * - ``api_overview`` - - High-level organization of the public API. - * - ``luminosity_function_examples`` - - Basic LF models, model comparison, evolving parameters, and LF surfaces. - * - ``conditional_luminosity_function_examples`` - - CLF models and halo-mass-dependent luminosity distributions. - * - ``magnitude_integrals`` - - Number density, luminosity density, mean luminosity, and selection-weighted integrals. - * - ``magnitudes_and_luminosities`` - - Magnitude conversions, luminosity-distance helpers, and luminosity-ratio helpers. - * - ``catalog_completeness_examples`` - - Observed/missing number densities and catalog fractions. - * - ``redshift_density`` - - Magnitude-limited and volume-weighted LF redshift trends. - * - ``kcorrect_examples`` - - Examples using the kcorrect backend. - * - ``poggianti_examples`` - - Examples using Poggianti correction tables. - * - ``model_registry`` - - Registered models and how to inspect them. - -This keeps each page small enough to read and maintain. - - -Which API should I use? ------------------------ - -Use :class:`lfkit.LuminosityFunction` when you want to evaluate or integrate an -ordinary luminosity function: - -.. code-block:: python - - lf = LuminosityFunction.schechter(...) - phi = lf.phi(...) - number_density = lf.integrals.number_density(...) - -Use the ``completeness`` namespace when a survey apparent-magnitude limit is -part of the calculation: - -.. code-block:: python - - fraction = lf.completeness.catalog_fraction(...) - -Use the ``redshift_density`` namespace when constructing an LF-weighted -redshift trend: - -.. code-block:: python - - nz = lf.redshift_density.weighted(...) - -Use the ``magnitudes`` and ``luminosities`` namespaces for conversions and -diagnostics: - -.. code-block:: python - - m_abs = lf.magnitudes.absolute_from_luminosity_distance(...) - l_ratio = lf.luminosities.ratio_from_magnitudes(...) - -Use :class:`lfkit.Corrections` when constructing or evaluating photometric -corrections. - -Use ``ConditionalLuminosityFunction`` when the model is conditional on halo mass -or another external variable. - - -Design principle ----------------- - -The public API should be thin and user-facing. - -Low-level modules should contain the numerical implementation. Public API -classes should organize those functions into discoverable workflows without -duplicating the underlying model code. - -In practice, this means: - -* model constructors store the selected model and parameters, -* bound namespaces inject the stored LF callable automatically, -* correction callables are passed explicitly where needed, -* low-level functions remain available for specialist use, -* examples should use the public API wherever possible. - -This keeps the docs readable while preserving the flexibility of the underlying -photometry modules. diff --git a/docs/examples/catalog_completeness.rst b/docs/examples/catalog_completeness.rst index 4adab81..9710bb0 100644 --- a/docs/examples/catalog_completeness.rst +++ b/docs/examples/catalog_completeness.rst @@ -20,7 +20,7 @@ can pass correction values explicitly through the ``k_correction`` and ``e_correction`` keyword arguments of the magnitude-limit and completeness methods. -The number-density units follow the normalization of the luminosity function. +The number density units follow the normalization of the luminosity function. For example, if :math:`\phi_*` is given in comoving :math:`{\rm Mpc}^{-3}`, then the integrated number densities are also in :math:`{\rm Mpc}^{-3}`. Fractions are dimensionless and are always defined relative to the chosen intrinsic @@ -57,7 +57,7 @@ higher on the plot. TITLE_SIZE = 17 LEGEND_SIZE = 15 - colors_blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.72, 0.96)) + red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))[1] cosmo = ccl.Cosmology( Omega_c=0.25, @@ -87,10 +87,10 @@ higher on the plot. ) fig, ax = plt.subplots(figsize=(7.0, 5.0)) - ax.plot(z, m_limit, lw=3, color=colors_blue[1]) + ax.plot(z, m_limit, lw=3, color=red) ax.invert_yaxis() ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) - ax.set_ylabel(r"Absolute-magnitude limit $M_{\rm lim}(z)$", fontsize=LABEL_SIZE) + ax.set_ylabel(r"Absolute magnitude limit $M_{\rm lim}(z)$", fontsize=LABEL_SIZE) ax.set_title(r"Catalog limit for $m_{\rm lim}=24.5$", fontsize=TITLE_SIZE) ax.tick_params(axis="both", labelsize=TICK_SIZE) plt.tight_layout() @@ -128,9 +128,9 @@ population described by the luminosity function, or only the bright tail of it. TITLE_SIZE = 17 LEGEND_SIZE = 15 - colors_blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.72, 0.96)) - colors_red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.03, 0.26)) - c_mid = 0.5 * (np.array(colors_blue[1]) + np.array(colors_red[1])) + c_red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))[1] + c_blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0))[1] + c_mid = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 1.0))[1] cosmo = ccl.Cosmology( Omega_c=0.25, @@ -180,8 +180,8 @@ population described by the luminosity function, or only the bright tail of it. fig, ax = plt.subplots(figsize=(7.0, 5.0)) ax.plot(z, n_total, lw=3, color=c_mid, label="Total LF integral") - ax.plot(z, n_obs, lw=3, color=colors_blue[1], label=r"Observed: $M < M_{\rm lim}(z)$") - ax.plot(z, n_miss, lw=3, color=colors_red[1], label=r"Missing: $M_{\rm lim}(z) < M < M_{\rm faint}$") + ax.plot(z, n_obs, lw=3, color=c_blue, label=r"Observed: $M < M_{\rm lim}(z)$") + ax.plot(z, n_miss, lw=3, color=c_red, label=r"Missing: $M_{\rm lim}(z) < M < M_{\rm faint}$") ax.set_yscale("log") ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) ax.set_ylabel(r"Comoving number density $n(z)$ [$\mathrm{Mpc}^{-3}$]", fontsize=LABEL_SIZE) @@ -222,8 +222,8 @@ catalog contains only a small fraction of the intrinsic galaxy population. TITLE_SIZE = 17 LEGEND_SIZE = 15 - colors_blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.72, 0.96)) - colors_red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.03, 0.26)) + red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))[1] + blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0))[1] cosmo = ccl.Cosmology( Omega_c=0.25, @@ -266,8 +266,8 @@ catalog contains only a small fraction of the intrinsic galaxy population. ) fig, ax = plt.subplots(figsize=(7.0, 5.0)) - ax.plot(z, f_obs, lw=3, color=colors_blue[1], label="Observable LF fraction") - ax.plot(z, f_miss, lw=3, color=colors_red[1], label="Missing LF fraction") + ax.plot(z, f_obs, lw=3, color=blue, label="Observable LF fraction") + ax.plot(z, f_miss, lw=3, color=red, label="Missing LF fraction") ax.set_ylim(-0.05, 1.05) ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) ax.set_ylabel("Fraction of chosen LF integral", fontsize=LABEL_SIZE) @@ -308,8 +308,6 @@ incomplete. TITLE_SIZE = 17 LEGEND_SIZE = 15 - colors_blue = cmr.take_cmap_colors("cmr.guppy", 4, cmap_range=(0.66, 0.98)) - cosmo = ccl.Cosmology( Omega_c=0.25, Omega_b=0.05, @@ -332,10 +330,15 @@ incomplete. ) limits = [22.5, 23.5, 24.5, 25.5] + colors = cmr.take_cmap_colors( + "cmr.guppy", + len(limits), + cmap_range=(0.0, 0.2), + ) fig, ax = plt.subplots(figsize=(7.0, 5.0)) - for m_lim, color in zip(limits, colors_blue): + for m_lim, color in zip(limits, colors): f_obs = lf.completeness.catalog_fraction( cosmo, z, @@ -354,7 +357,7 @@ incomplete. plt.tight_layout() -Absolute-magnitude limits for different depths +Absolute magnitude limits for different depths ---------------------------------------------- The apparent magnitude limit can also be shown directly as an absolute magnitude @@ -386,8 +389,6 @@ boundary. TITLE_SIZE = 17 LEGEND_SIZE = 15 - colors_blue = cmr.take_cmap_colors("cmr.guppy", 4, cmap_range=(0.66, 0.98)) - cosmo = ccl.Cosmology( Omega_c=0.25, Omega_b=0.05, @@ -410,10 +411,15 @@ boundary. ) limits = [22.5, 23.5, 24.5, 25.5] + colors = cmr.take_cmap_colors( + "cmr.guppy", + len(limits), + cmap_range=(0.0, 0.2), + ) fig, ax = plt.subplots(figsize=(7.0, 5.0)) - for m_lim, color in zip(limits, colors_blue): + for m_lim, color in zip(limits, colors): m_limit = lf.completeness.absolute_magnitude_limit( cosmo, z, @@ -423,7 +429,7 @@ boundary. ax.invert_yaxis() ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) - ax.set_ylabel(r"Absolute-magnitude limit $M_{\rm lim}(z)$", fontsize=LABEL_SIZE) + ax.set_ylabel(r"Absolute magnitude limit $M_{\rm lim}(z)$", fontsize=LABEL_SIZE) ax.set_title("Catalog selection boundary", fontsize=TITLE_SIZE) ax.tick_params(axis="both", labelsize=TICK_SIZE) ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") @@ -461,8 +467,6 @@ about a much fainter underlying population. TITLE_SIZE = 17 LEGEND_SIZE = 15 - colors_red = cmr.take_cmap_colors("cmr.guppy", 4, cmap_range=(0.03, 0.30)) - cosmo = ccl.Cosmology( Omega_c=0.25, Omega_b=0.05, @@ -485,10 +489,15 @@ about a much fainter underlying population. ) faint_limits = [-17.0, -16.0, -15.0, -14.0] + colors = cmr.take_cmap_colors( + "cmr.guppy", + len(faint_limits), + cmap_range=(0.0, 0.2), + ) fig, ax = plt.subplots(figsize=(7.0, 5.0)) - for m_faint, color in zip(faint_limits, colors_red): + for m_faint, color in zip(faint_limits, colors): f_obs = lf.completeness.catalog_fraction( cosmo, z, @@ -610,12 +619,12 @@ redshift range is safe for a magnitude-limited sample. completeness, levels=contour_levels, colors="white", - linewidths=1.2, + linewidths=1.5, ) ax.clabel(contours, inline=True, fontsize=TICK_SIZE, fmt="%.1f") ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) - ax.set_ylabel(r"Apparent-magnitude limit $m_{\rm lim}$", fontsize=LABEL_SIZE) + ax.set_ylabel(r"Apparent magnitude limit $m_{\rm lim}$", fontsize=LABEL_SIZE) ax.set_title(r"Completeness over $-24 \leq M \leq -14$", fontsize=TITLE_SIZE) ax.tick_params(axis="both", labelsize=TICK_SIZE) @@ -658,8 +667,6 @@ The lower panel shows the residual relative to the reference cosmology, TITLE_SIZE = 17 LEGEND_SIZE = 15 - colors_red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.03, 0.26)) - cosmologies = { r"$\Omega_{\rm m}=0.25,\ h=0.70$": ccl.Cosmology( Omega_c=0.20, @@ -713,16 +720,21 @@ The lower panel shows the residual relative to the reference cosmology, reference_label = r"$\Omega_{\rm m}=0.30,\ h=0.70$" reference = m_limits[reference_label] + colors = cmr.take_cmap_colors( + "cmr.guppy", + len(cosmologies), + cmap_range=(0.0, 0.2), + ) + fig, (ax_top, ax_bottom) = plt.subplots( 2, 1, figsize=(7.0, 6.2), sharex=True, gridspec_kw={"height_ratios": [3, 1]}, - constrained_layout=True, ) - for (label, m_limit), color in zip(m_limits.items(), colors_red): + for (label, m_limit), color in zip(m_limits.items(), colors): ax_top.plot(z, m_limit, lw=3, color=color, label=label) ax_bottom.plot(z, m_limit - reference, lw=2.5, color=color) @@ -771,8 +783,6 @@ The lower panel shows the residual relative to the reference cosmology, TITLE_SIZE = 17 LEGEND_SIZE = 15 - colors_red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.03, 0.26)) - cosmologies = { r"$\Omega_{\rm m}=0.25,\ h=0.70$": ccl.Cosmology( Omega_c=0.20, @@ -803,6 +813,12 @@ The lower panel shows the residual relative to the reference cosmology, ), } + colors = cmr.take_cmap_colors( + "cmr.guppy", + len(cosmologies), + cmap_range=(0.0, 0.2), + ) + z = np.linspace(0.05, 1.2, 250) lf = LuminosityFunction.evolving_schechter( @@ -834,10 +850,9 @@ The lower panel shows the residual relative to the reference cosmology, figsize=(7.0, 6.2), sharex=True, gridspec_kw={"height_ratios": [3, 1]}, - constrained_layout=True, ) - for (label, f_obs), color in zip(completeness.items(), colors_red): + for (label, f_obs), color in zip(completeness.items(), colors): ax_top.plot(z, f_obs, lw=3, color=color, label=label) ax_bottom.plot(z, f_obs - reference, lw=2.5, color=color) diff --git a/docs/examples/conditional_luminosity_function.rst b/docs/examples/conditional_luminosity_function.rst index 32d49e4..02b389f 100644 --- a/docs/examples/conditional_luminosity_function.rst +++ b/docs/examples/conditional_luminosity_function.rst @@ -6,12 +6,22 @@ ============================================ This page shows how to evaluate conditional luminosity functions with LFKit's -public API. +public API. A conditional luminosity function describes the abundance of +galaxies as a function of absolute magnitude while allowing the model to depend +on another variable. A conditional luminosity function has the form :math:`\Phi(M \mid x)`, where -:math:`M` is absolute magnitude and :math:`x` is an external conditioning -variable. The conditioning variable is generic: it can represent redshift, halo -mass, environment, galaxy type, richness, stellar mass, or another quantity. +:math:`M` is absolute magnitude and :math:`x` is the conditioning variable. The +quantity + +.. math:: + + \Phi(M \mid x)\,dM + +is the number density of galaxies with absolute magnitudes between :math:`M` +and :math:`M+dM` at fixed :math:`x`. The conditioning variable is generic: it +can represent redshift, halo mass, environment, galaxy type, richness, stellar +mass, or another quantity. LFKit exposes conditional luminosity functions through :class:`lfkit.ConditionalLuminosityFunction`. Each constructor returns a @@ -19,9 +29,11 @@ LFKit exposes conditional luminosity functions through evaluated with ``lf.phi`` and integrated with the usual ``lf.integrals`` namespace. -The examples below use redshift as the conditioning variable because it is a -natural choice for luminosity function evolution. The same API can be used with -any other conditioning variable by replacing ``z`` with the desired quantity. +The examples below use redshift as the conditioning variable because this is +the most common use case for luminosity function evolution. In that case, +:math:`\Phi(M \mid z)` describes how the magnitude distribution of galaxies +changes with redshift. The same API can be used with any other conditioning +variable by replacing ``z`` with the desired quantity. The examples include: @@ -43,13 +55,17 @@ of :math:`{\rm Mpc}^{-3}\,{\rm mag}^{-1}`. Conditional Schechter luminosity function ----------------------------------------- -A conditional Schechter luminosity function allows the Schechter parameters to -depend on an external variable. +A conditional Schechter luminosity function is a Schechter model whose +parameters depend on an external variable. Instead of using one fixed +normalization, characteristic magnitude, and faint-end slope, the model can let +one or more of these quantities vary with the condition. -This example makes the normalization and characteristic magnitude depend on -redshift. The faint-end slope is kept fixed. At each redshift, the model is -still a Schechter luminosity function, but the curve evolves smoothly with the -conditioning variable. +This example uses redshift as the condition. The normalization +:math:`\phi_\star(z)` and characteristic magnitude :math:`M_\star(z)` evolve +with redshift, while the faint-end slope :math:`\alpha` is kept fixed. Each +curve is therefore a Schechter luminosity function at one redshift. Comparing +the curves shows how the abundance and bright-end turnover shift as the +population evolves. .. plot:: :include-source: True @@ -66,11 +82,15 @@ conditioning variable. TITLE_SIZE = 17 LEGEND_SIZE = 15 - colors = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.72, 0.96)) - absolute_mag = np.linspace(-24.0, -14.0, 500) redshifts = [0.1, 0.6, 1.1] + colors = cmr.take_cmap_colors( + "cmr.guppy", + len(redshifts), + cmap_range=(0.0, 0.2), + ) + lf = ConditionalLuminosityFunction.schechter( phi_star=lambda z: 1.0e-3 * (1.0 + z) ** 0.8, m_star=lambda z: -20.5 - 0.7 * (z - 0.1), @@ -92,6 +112,7 @@ conditioning variable. ) ax.set_yscale("log") + ax.set_ylim(1.0e-8, 1.0e-1) ax.invert_xaxis() ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE) ax.set_ylabel( @@ -108,11 +129,14 @@ conditioning variable. Conditional Schechter surface ----------------------------- -The same model can be shown across the full magnitude-redshift plane. +The same conditional Schechter model can be evaluated over the full +magnitude-redshift plane rather than at a few selected redshifts. The filled colour scale shows :math:`\log_{10}\Phi(M \mid z)`. The contours -mark constant abundance levels. This is a useful diagnostic for checking that -the conditional model behaves smoothly across the region where it will be used. +mark constant abundance levels. This view is useful for checking the global +behaviour of the model: smooth contours indicate that the parameter evolution +produces a smooth luminosity function, while abrupt features would signal a +problem with the chosen parameterization or redshift dependence. .. plot:: :include-source: True @@ -139,7 +163,7 @@ the conditional model behaves smoothly across the region where it will be used. alpha=-1.1, ) - phi = lf.phi(mag_grid, z_grid) + phi = np.clip(lf.phi(mag_grid, z_grid), 1.0e-8, 1.0e-1) log_phi = np.log10(phi) fig, ax = plt.subplots(figsize=(7.2, 5.0)) @@ -159,7 +183,8 @@ the conditional model behaves smoothly across the region where it will be used. log_phi, levels=contour_levels, colors="white", - linewidths=1.2, + linewidths=1.5, + linestyles="solid", ) ax.clabel(contours, inline=True, fontsize=TICK_SIZE, fmt=r"$10^{%.0f}$") @@ -183,12 +208,16 @@ the conditional model behaves smoothly across the region where it will be used. Conditional Schechter model with LFKit parameter models ------------------------------------------------------- -LFKit can also evaluate conditional Schechter models using its registered -parameter models. This is useful when the desired dependence follows one of the -standard LFKit parameterizations. +LFKit can also build conditional Schechter models from its registered parameter +models. This is useful when the desired evolution follows one of the standard +forms already available in LFKit, rather than being written manually as a +callable. Here, the normalization and characteristic magnitude evolve with the -conditioning variable, while the faint-end slope is constant. +conditioning variable, while the faint-end slope is constant. The result is +equivalent in spirit to the previous example, but the parameter evolution is +defined through named LFKit models. This makes the model easier to reproduce, +configure, and document. .. plot:: :include-source: True @@ -205,11 +234,15 @@ conditioning variable, while the faint-end slope is constant. TITLE_SIZE = 17 LEGEND_SIZE = 15 - colors = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.03, 0.26)) - absolute_mag = np.linspace(-24.0, -14.0, 500) redshifts = [0.1, 0.6, 1.1] + colors = cmr.take_cmap_colors( + "cmr.guppy", + len(redshifts), + cmap_range=(0.0, 0.2), + ) + lf = ConditionalLuminosityFunction.evolving_schechter( phi_model="linear_p", phi_kwargs={"phi_0_star": 1.0e-3, "p": 0.7}, @@ -234,6 +267,7 @@ conditioning variable, while the faint-end slope is constant. ) ax.set_yscale("log") + ax.set_ylim(1.0e-8, 1.0e-1) ax.invert_xaxis() ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE) ax.set_ylabel( @@ -250,12 +284,15 @@ conditioning variable, while the faint-end slope is constant. Lognormal conditional component ------------------------------- -A narrow lognormal component can represent a population concentrated around a -characteristic luminosity at fixed condition. +A lognormal conditional component describes a population concentrated around a +characteristic luminosity at fixed condition. In magnitude space, this appears +as a relatively narrow feature centred on a mean absolute magnitude. -This example uses a mean absolute magnitude that becomes brighter with -redshift. The scatter is fixed, so the peak shifts while retaining a similar -width. +This example lets the mean absolute magnitude become brighter with redshift, +while the scatter is fixed. The peak therefore shifts to brighter magnitudes as +redshift increases, but the width of the component remains similar. This kind +of component is useful for representing a population with a preferred luminosity +scale rather than a broad Schechter-like faint-end tail. .. plot:: :include-source: True @@ -272,11 +309,15 @@ width. TITLE_SIZE = 17 LEGEND_SIZE = 15 - colors = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.72, 0.96)) - absolute_mag = np.linspace(-24.0, -16.0, 500) redshifts = [0.1, 0.6, 1.1] + colors = cmr.take_cmap_colors( + "cmr.guppy", + len(redshifts), + cmap_range=(0.0, 0.2), + ) + lf = ConditionalLuminosityFunction.lognormal( mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), sigma_log_luminosity=0.18, @@ -298,6 +339,7 @@ width. ) ax.set_yscale("log") + ax.set_ylim(1.0e-8, 1.0e-1) ax.invert_xaxis() ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE) ax.set_ylabel( @@ -315,9 +357,14 @@ width. Modified Schechter conditional component ---------------------------------------- -The modified Schechter component uses a squared exponential cutoff in luminosity -ratio. It is broader than the lognormal component and contributes over a wider -range of faint magnitudes. +The modified Schechter component is Schechter-like at the faint end but uses a +different cutoff at high luminosity. Instead of the standard exponential cutoff, +it has a broader cutoff controlled by the luminosity ratio. + +This example lets the normalization, characteristic magnitude, and faint-end +slope vary with redshift. The model therefore changes both in amplitude and in +shape. This is useful when a standard Schechter function is too restrictive, +especially for populations whose bright end falls off more gradually. .. plot:: :include-source: True @@ -334,11 +381,15 @@ range of faint magnitudes. TITLE_SIZE = 17 LEGEND_SIZE = 15 - colors = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.03, 0.26)) - absolute_mag = np.linspace(-24.0, -14.0, 500) redshifts = [0.1, 0.6, 1.1] + colors = cmr.take_cmap_colors( + "cmr.guppy", + len(redshifts), + cmap_range=(0.0, 0.2), + ) + lf = ConditionalLuminosityFunction.modified_schechter( phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5, m_star=lambda z: -19.9 - 0.5 * (z - 0.1), @@ -360,6 +411,7 @@ range of faint magnitudes. ) ax.set_yscale("log") + ax.set_ylim(1.0e-8, 1.0e-1) ax.invert_xaxis() ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE) ax.set_ylabel( @@ -377,11 +429,15 @@ range of faint magnitudes. Standard, modified, and lognormal component shapes -------------------------------------------------- -It is useful to compare the component shapes at fixed condition. The standard -Schechter form has the usual exponential cutoff in luminosity ratio. The -modified Schechter component uses a squared exponential cutoff. The lognormal -component is localized around a mean luminosity and is useful for narrow -populations. +It is useful to compare the component shapes at fixed condition before combining +them into more complicated models. This separates differences in functional +form from differences caused by redshift or halo-mass evolution. + +The standard Schechter form has a power-law faint end and an exponential +bright-end cutoff. The modified Schechter component has a broader bright-end +shape. The lognormal component is localized around a characteristic luminosity. +Together, these examples show the kinds of galaxy populations each component is +best suited to describe. .. plot:: :include-source: True @@ -398,7 +454,9 @@ populations. TITLE_SIZE = 17 LEGEND_SIZE = 15 - colors = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.08, 0.92)) + colors_blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0)) + colors_red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2)) + colors_all = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 1.0)) absolute_mag = np.linspace(-24.0, -14.0, 600) z_value = 0.6 @@ -432,25 +490,26 @@ populations. absolute_mag, phi_schechter, lw=3, - color=colors[0], + color=colors_blue[1], label="Standard Schechter", ) ax.plot( absolute_mag, phi_modified, lw=3, - color=colors[1], + color=colors_red[1], label="Modified Schechter", ) ax.plot( absolute_mag, phi_lognormal, lw=3, - color=colors[2], + color=colors_all[1], label="Lognormal", ) ax.set_yscale("log") + ax.set_ylim(1.0e-8, 1.0e-1) ax.invert_xaxis() ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE) ax.set_ylabel( @@ -465,15 +524,19 @@ populations. plt.tight_layout() -Two-component conditional luminosity function +Two-component conditional LF --------------------------------------------- The lognormal and modified Schechter components can be combined into a -two-component conditional luminosity function. +two-component conditional luminosity function. This is useful when one component +is intended to describe a relatively narrow population and the other describes a +broader luminosity distribution. This plot separates the lognormal component, the modified Schechter component, -and their sum at a fixed redshift. This is a useful way to check which component -dominates different magnitude ranges. +and their sum at a fixed redshift. The comparison shows which component +dominates different magnitude ranges. In this example, the lognormal component +is concentrated near its characteristic magnitude, while the modified Schechter +component contributes over a wider range of magnitudes. .. plot:: :include-source: True @@ -490,9 +553,9 @@ dominates different magnitude ranges. TITLE_SIZE = 17 LEGEND_SIZE = 15 - colors_blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.72, 0.96)) - colors_red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.03, 0.26)) - total_color = 0.5 * (np.array(colors_blue[1]) + np.array(colors_red[1])) + colors_blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0)) + colors_red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2)) + colors_all = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 1.0)) absolute_mag = np.linspace(-24.0, -14.0, 500) z_value = 0.6 @@ -543,11 +606,12 @@ dominates different magnitude ranges. absolute_mag, total_phi, lw=3, - color=total_color, + color=colors_all[1], label="Two-component total", ) ax.set_yscale("log") + ax.set_ylim(1.0e-8, 1.0e-1) ax.invert_xaxis() ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE) ax.set_ylabel( @@ -564,10 +628,16 @@ dominates different magnitude ranges. Two-component evolution ----------------------- -The two-component conditional luminosity function can be evaluated across -several redshifts. This example shows how the full model changes when both +The two-component conditional luminosity function can also be evaluated at +several redshifts. This shows how the total population changes when both components depend on the conditioning variable. +Each curve is the sum of the lognormal and modified Schechter components at one +redshift. Changes in the curves reflect the combined evolution of the component +amplitudes, characteristic magnitudes, and faint-end behaviour. This plot is a +compact way to check whether the full conditional model evolves in the expected +direction. + .. plot:: :include-source: True :width: 520 @@ -583,11 +653,15 @@ components depend on the conditioning variable. TITLE_SIZE = 17 LEGEND_SIZE = 15 - colors = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.72, 0.96)) - absolute_mag = np.linspace(-24.0, -14.0, 500) redshifts = [0.1, 0.6, 1.1] + colors = cmr.take_cmap_colors( + "cmr.guppy", + len(redshifts), + cmap_range=(0.0, 0.2), + ) + lf = ConditionalLuminosityFunction.two_component( lognormal_mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), lognormal_sigma_log_luminosity=0.18, @@ -612,6 +686,7 @@ components depend on the conditioning variable. ) ax.set_yscale("log") + ax.set_ylim(1.0e-8, 1.0e-1) ax.invert_xaxis() ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE) ax.set_ylabel( @@ -630,11 +705,22 @@ Integrated conditional number density ------------------------------------- A conditional luminosity function can be integrated over absolute magnitude at -each value of the conditioning variable. +each value of the conditioning variable. The result is an integrated number +density, + +.. math:: -Because conditional constructors return normal LFKit luminosity function -objects, the same ``lf.integrals`` namespace can be used here. The result shows -how the selected number density changes with redshift. + n(x) = \int_{M_{\rm bright}}^{M_{\rm faint}} \Phi(M \mid x)\,\mathrm{d}M. + +For redshift as the conditioning variable, this gives the number density of +galaxies inside the chosen magnitude range as a function of redshift. Because +conditional constructors return normal LFKit luminosity function objects, the +same ``lf.integrals`` namespace can be used here. + +This example integrates the lognormal component, the modified Schechter +component, and the full two-component model. The total number density is the sum +of the two components, while the individual curves show how much each component +contributes. .. plot:: :include-source: True @@ -651,9 +737,9 @@ how the selected number density changes with redshift. TITLE_SIZE = 17 LEGEND_SIZE = 15 - colors_blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.72, 0.96)) - colors_red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.03, 0.26)) - total_color = 0.5 * (np.array(colors_blue[1]) + np.array(colors_red[1])) + colors_blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0)) + colors_red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2)) + colors_all = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 1.0)) redshift = np.linspace(0.05, 1.5, 180) @@ -719,11 +805,12 @@ how the selected number density changes with redshift. redshift, n_total, lw=3, - color=total_color, + color=colors_all[1], label="Two-component total", ) ax.set_yscale("log") + ax.set_ylim(1.0e-4, 1.0e-1) ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) ax.set_ylabel(r"Integrated number density [$\mathrm{Mpc}^{-3}$]", fontsize=LABEL_SIZE) ax.set_title(r"Integrated conditional LF over $-24 \leq M \leq -14$", fontsize=TITLE_SIZE) @@ -737,12 +824,19 @@ Component fractions ------------------- The relative contribution of each component can be summarized as a fraction of -the integrated two-component luminosity function. +the integrated two-component luminosity function. For components +:math:`n_1(z)` and :math:`n_2(z)`, the corresponding fractions are + +.. math:: + + f_1(z) = \frac{n_1(z)}{n_1(z) + n_2(z)}, \qquad + f_2(z) = \frac{n_2(z)}{n_1(z) + n_2(z)}. This example computes the integrated lognormal and modified Schechter -components with ``lf.integrals.number_density``. This is a compact diagnostic -for checking whether the selected population is dominated by the lognormal -component, the modified Schechter component, or a mixture of both. +components with ``lf.integrals.number_density``. The resulting fractions show +whether the selected population is dominated by the narrow lognormal component, +the broader modified Schechter component, or a mixture of both. This is often +easier to interpret than comparing raw number densities alone. .. plot:: :include-source: True @@ -759,8 +853,8 @@ component, the modified Schechter component, or a mixture of both. TITLE_SIZE = 17 LEGEND_SIZE = 15 - colors_blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.72, 0.96)) - colors_red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.03, 0.26)) + colors_blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0)) + colors_red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2)) redshift = np.linspace(0.05, 1.5, 180) @@ -826,12 +920,14 @@ Two-component LF surface ------------------------ The full two-component conditional luminosity function can be shown as a -surface in the magnitude-redshift plane. +surface in the magnitude-redshift plane. This combines the component structure +and the redshift evolution in a single diagnostic plot. The filled colour scale shows :math:`\log_{10}\Phi_{\rm total}(M \mid z)`. The contours mark constant abundance levels. This view is useful for checking whether the narrow component, broad component, and redshift dependence combine -smoothly. +smoothly across the full domain. It also makes it easier to identify where the +model predicts the largest abundance of galaxies. .. plot:: :include-source: True @@ -861,7 +957,7 @@ smoothly. modified_alpha=lambda z: -1.05 - 0.10 * z, ) - phi = lf.phi(mag_grid, z_grid) + phi = np.clip(lf.phi(mag_grid, z_grid), 1.0e-8, 1.0e-1) log_phi = np.log10(phi) fig, ax = plt.subplots(figsize=(7.2, 5.0)) @@ -881,7 +977,8 @@ smoothly. log_phi, levels=contour_levels, colors="white", - linewidths=1.2, + linewidths=1.5, + linestyles="solid", ) ax.clabel(contours, inline=True, fontsize=TICK_SIZE, fmt=r"$10^{%.0f}$") @@ -907,10 +1004,13 @@ Halo-mass conditional luminosity function The conditioning variable does not need to be redshift. In halo-model applications, a conditional luminosity function is often written as -:math:`\Phi(M \mid M_h)`, where :math:`M_h` is halo mass. +:math:`\Phi(M \mid M_h)`, where :math:`M_h` is halo mass. This describes the +luminosity distribution of galaxies hosted by halos of a given mass. -This example uses log halo mass as the conditioning variable and lets the -lognormal mean magnitude become brighter in more massive halos. +This example uses log halo mass as the conditioning variable. The lognormal +mean magnitude becomes brighter in more massive halos, and the amplitude also +increases with halo mass. The interpretation is that more massive halos host +brighter and more abundant galaxies in this toy model. .. plot:: :include-source: True @@ -927,11 +1027,15 @@ lognormal mean magnitude become brighter in more massive halos. TITLE_SIZE = 17 LEGEND_SIZE = 15 - colors = cmr.take_cmap_colors("cmr.guppy", 4, cmap_range=(0.08, 0.92)) - absolute_mag = np.linspace(-24.0, -16.0, 600) log_halo_masses = [11.5, 12.0, 12.5, 13.0] + colors = cmr.take_cmap_colors( + "cmr.guppy", + len(log_halo_masses), + cmap_range=(0.0, 0.2), + ) + lf = ConditionalLuminosityFunction.lognormal( mean_absolute_mag=lambda log_mh: -20.0 - 0.8 * (log_mh - 12.0), sigma_log_luminosity=0.18, @@ -953,6 +1057,7 @@ lognormal mean magnitude become brighter in more massive halos. ) ax.set_yscale("log") + ax.set_ylim(1.0e-8, 1.0e-1) ax.invert_xaxis() ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE) ax.set_ylabel( @@ -969,15 +1074,27 @@ lognormal mean magnitude become brighter in more massive halos. Mean luminosity ratio from a conditional luminosity function ------------------------------------------------------------ -Weighted integrals can be used to compute positive luminosity-weighted summary -statistics of a conditional luminosity function. For example, the mean -luminosity ratio relative to a reference magnitude is +Weighted integrals can be used to compute luminosity-weighted summary +statistics of a conditional luminosity function. Instead of only counting +galaxies, the integral can weight each magnitude by a physically meaningful +quantity. + +For example, the mean luminosity ratio relative to a reference magnitude is -:math:`\langle L/L_{\rm ref} \rangle(x) = -\int (L/L_{\rm ref}) \Phi(M \mid x)\,dM / \int \Phi(M \mid x)\,dM`. +.. math:: + + \left\langle \frac{L}{L_{\rm ref}} \right\rangle(x) + = + \frac{ + \int (L/L_{\rm ref}) \Phi(M \mid x)\,\mathrm{d}M + }{ + \int \Phi(M \mid x)\,\mathrm{d}M + }. This example uses the ``lf.integrals`` namespace on a two-component conditional -luminosity function. +luminosity function. The numerator is a luminosity-weighted integral and the +denominator is the number density. Their ratio gives the average luminosity of +the selected population relative to the chosen reference luminosity. .. plot:: :include-source: True @@ -1030,7 +1147,7 @@ luminosity function. redshift, mean_luminosity_ratio, lw=3, - color=cmr.take_cmap_colors("cmr.guppy", 1, cmap_range=(0.7, 0.9))[0], + color=cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))[1], ) ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) @@ -1046,11 +1163,13 @@ Selection-limited conditional number density The LFKit API can integrate a conditional luminosity function over finite magnitude bounds. This is useful for survey-like selections where only galaxies -brighter than a limiting absolute magnitude contribute to the selected sample. +brighter than a limiting absolute magnitude contribute to the observed sample. Here, the limiting absolute magnitude becomes brighter with redshift. The -example compares the full number density over a fixed magnitude range with the -number density brighter than the redshift-dependent limit. +example compares the full number density over a fixed reference magnitude range +with the number density brighter than the redshift-dependent limit. The gap +between the two curves shows how much of the luminosity function is excluded by +the selection. .. plot:: :include-source: True @@ -1094,7 +1213,8 @@ number density brighter than the redshift-dependent limit. n_m=800, ) - colors = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.12, 0.9)) + colors_blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0)) + colors_red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2)) fig, ax = plt.subplots(figsize=(7.0, 5.0)) @@ -1102,18 +1222,19 @@ number density brighter than the redshift-dependent limit. redshift, n_total, lw=3, - color=colors[0], + color=colors_blue[1], label="Full magnitude range", ) ax.plot( redshift, n_selected, lw=3, - color=colors[2], + color=colors_red[1], label="Brighter than limit", ) ax.set_yscale("log") + ax.set_ylim(1.0e-4, 1.0e-1) ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) ax.set_ylabel( r"Integrated number density [$\mathrm{Mpc}^{-3}$]", @@ -1131,7 +1252,22 @@ Selection fraction The selected fraction is the ratio between the number density brighter than the redshift-dependent limiting magnitude and the number density over the full -reference magnitude range. +reference magnitude range, + +.. math:: + + f_{\rm selected}(z) + = + \frac{ + n(M_{\rm bright} \leq M \leq M_{\rm lim}(z)) + }{ + n(M_{\rm bright} \leq M \leq M_{\rm faint}) + }. + +This diagnostic compresses the selection effect into a number between zero and +one. Values close to one mean that most of the reference luminosity function is +included by the selection. Smaller values mean that the limiting magnitude +removes a larger fraction of the population. .. plot:: :include-source: True @@ -1176,7 +1312,7 @@ reference magnitude range. selected_fraction = n_selected / n_total - color = cmr.take_cmap_colors("cmr.guppy", 1, cmap_range=(0.72, 0.9))[0] + color = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))[1] fig, ax = plt.subplots(figsize=(7.0, 5.0)) diff --git a/docs/examples/index.rst b/docs/examples/index.rst index e35998a..1485fca 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -5,12 +5,19 @@ |lfkitlogo| Examples ==================== +This page gives a high-level overview of LFKit's public API, how the main +example pages fit together, and which page to use for each type of calculation. +Detailed examples are provided on the topic-specific pages listed below. + +LFKit is organized around a small number of user-facing entry points. Most +workflows start by creating a luminosity function model and then using grouped +methods attached to that model for integrals, completeness calculations, +redshift-density trends, and magnitude or luminosity conversions. .. toctree:: :maxdepth: 1 :hidden: - api_overview luminosity_function_models magnitude_integrals magnitudes_and_luminosities @@ -21,3 +28,549 @@ kcorrect_examples poggianti_examples + +Start here +---------- + +For ordinary luminosity functions, start with :class:`lfkit.LuminosityFunction`: + +.. code-block:: python + + from lfkit import LuminosityFunction + + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + phi = lf.phi(-20.0) + +The object ``lf`` represents the chosen luminosity function model. Once the +model is created, the same object can be used for direct evaluation, magnitude +integrals, survey completeness calculations, redshift-density calculations, and +diagnostic conversions. + +Photometric corrections are exposed separately through :class:`lfkit.Corrections`: + +.. code-block:: python + + from lfkit import Corrections + +Correction functions can be passed into calculations that need k-corrections, +evolution corrections, or other magnitude corrections. + + +Main API areas +-------------- + +The main public API areas are: + +* luminosity function models, +* conditional luminosity function models, +* luminosity function integrals, +* magnitude-limited completeness, +* LF-weighted redshift-density calculations, +* magnitude and luminosity conversions, +* photometric corrections, +* model discovery utilities. + + +Which tool do I need? +--------------------- + +.. list-table:: + :header-rows: 1 + :widths: 28 42 30 + + * - Task + - Use + - Example page + * - Evaluate or compare luminosity function models + - :class:`lfkit.LuminosityFunction` + - :doc:`luminosity function models ` + * - Integrate a luminosity function over absolute magnitude + - ``lf.integrals`` + - :doc:`magnitude_integrals` + * - Convert between apparent magnitude, absolute magnitude, and luminosity distance + - ``lf.magnitudes`` + - :doc:`magnitudes_and_luminosities` + * - Work with luminosity ratios and Schechter luminosity variables + - ``lf.luminosities`` + - :doc:`magnitudes_and_luminosities` + * - Estimate observed, missing, and out-of-catalog fractions + - ``lf.completeness`` + - :doc:`catalog completeness ` + * - Build magnitude-limited or volume-weighted redshift trends + - ``lf.redshift_density`` + - :doc:`redshift_density` + * - Work with conditional luminosity functions + - ``ConditionalLuminosityFunction`` + - :doc:`conditional luminosity function ` + * - Use k-corrections from the kcorrect backend + - :class:`lfkit.Corrections` + - :doc:`kcorrect_examples` + * - Use Poggianti correction tables + - :class:`lfkit.Corrections` + - :doc:`poggianti_examples` + * - Inspect available model names + - ``available_models()`` methods + - :doc:`model_registry` + + +Basic workflow +-------------- + +A typical LFKit workflow has three steps: + +1. choose a luminosity function model, +2. evaluate it directly or pass it to one of the grouped calculation namespaces, +3. add survey limits, correction functions, or cosmology-dependent distances + when the calculation needs them. + +For example: + +.. code-block:: python + + from lfkit import LuminosityFunction + + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + phi = lf.phi(-20.0) + + number_density = lf.integrals.number_density( + redshift=0.5, + m_bright=-24.0, + m_faint=-16.0, + ) + + catalog_fraction = lf.completeness.catalog_fraction( + cosmo, + redshift=0.5, + m_lim=24.0, + m_bright=-24.0, + m_faint=-16.0, + h=0.7, + ) + +The first line evaluates the luminosity function at a single absolute +magnitude. The integral example computes the number density over a finite +absolute magnitude range. The completeness example adds an apparent magnitude +limit and asks what fraction of the intrinsic luminosity function is visible in +a survey. + + +Grouped methods +--------------- + +A luminosity function object exposes grouped methods for common calculations: + +.. list-table:: + :header-rows: 1 + :widths: 25 55 20 + + * - Namespace + - Purpose + - Example + * - ``lf.integrals`` + - Integrals over absolute magnitude. + - ``lf.integrals.number_density(...)`` + * - ``lf.completeness`` + - Magnitude-limited catalog fractions and missing fractions. + - ``lf.completeness.catalog_fraction(...)`` + * - ``lf.redshift_density`` + - LF-weighted redshift trends and magnitude-limited number densities. + - ``lf.redshift_density.weighted(...)`` + * - ``lf.magnitudes`` + - Apparent/absolute magnitude and luminosity-distance conversions. + - ``lf.magnitudes.absolute_from_luminosity_distance(...)`` + * - ``lf.luminosities`` + - Luminosity ratios and related helpers. + - ``lf.luminosities.ratio_from_magnitudes(...)`` + +This grouping is meant to make notebook workflows easier to read. Start from +the model object, then choose the namespace that matches the calculation. + + +Example pages +------------- + +The examples are split by topic so that each page stays focused. + +.. grid:: 2 + :gutter: 2 + + .. grid-item-card:: + :link: luminosity_function_models + :link-type: doc + :shadow: md + + **Luminosity function models** + ^^^ + Build, evaluate, and compare ordinary luminosity function models, + including evolving models and model surfaces. + + +++ + *API:* ``LuminosityFunction`` + + .. grid-item-card:: + :link: magnitude_integrals + :link-type: doc + :shadow: md + + **Magnitude integrals** + ^^^ + Compute number density, luminosity density, mean luminosity, and + selection-weighted integrals over absolute magnitude. + + +++ + *API:* ``lf.integrals`` + + .. grid-item-card:: + :link: magnitudes_and_luminosities + :link-type: doc + :shadow: md + + **Magnitudes and luminosities** + ^^^ + Convert between apparent magnitude, absolute magnitude, luminosity + distance, luminosity ratios, and Schechter luminosity variables. + + +++ + *API:* ``lf.magnitudes`` and ``lf.luminosities`` + + .. grid-item-card:: + :link: redshift_density + :link-type: doc + :shadow: md + + **Redshift density** + ^^^ + Build magnitude-limited and volume-weighted LF redshift trends for survey, + tomography, and forecasting workflows. + + +++ + *API:* ``lf.redshift_density`` + + .. grid-item-card:: + :link: catalog_completeness + :link-type: doc + :shadow: md + + **Catalog completeness** + ^^^ + Estimate observed, missing, and out-of-catalog fractions from apparent + magnitude limits. + + +++ + *API:* ``lf.completeness`` + + .. grid-item-card:: + :link: conditional_luminosity_function + :link-type: doc + :shadow: md + + **Conditional luminosity functions** + ^^^ + Work with luminosity distributions conditioned on another variable, such + as halo mass. + + +++ + *API:* ``ConditionalLuminosityFunction`` + + .. grid-item-card:: + :link: model_registry + :link-type: doc + :shadow: md + + **Model registry** + ^^^ + Inspect available model names and model-discovery helpers for ordinary + and conditional luminosity functions. + + +++ + *API:* ``available_models()`` + + .. grid-item-card:: + :link: kcorrect_examples + :link-type: doc + :shadow: md + + **kcorrect examples** + ^^^ + Build k-corrections with the kcorrect backend and pass correction + callables into LFKit calculations. + + +++ + *API:* ``Corrections`` + + .. grid-item-card:: + :link: poggianti_examples + :link-type: doc + :shadow: md + + **Poggianti examples** + ^^^ + Evaluate Poggianti tabulated k-corrections and evolution corrections for + galaxy-type-dependent workflows. + + +++ + *API:* ``Corrections`` + + +Luminosity function models +-------------------------- + +Use :class:`lfkit.LuminosityFunction` for ordinary luminosity function models. + +For a standard Schechter model: + +.. code-block:: python + + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + phi = lf.phi(absolute_mag) + +For an evolving model, pass redshift when evaluating the model: + +.. code-block:: python + + phi = lf.phi(absolute_mag, redshift) + +The luminosity function examples page shows how to compare models, evaluate +evolving parameters, and visualize luminosity function behavior: + +:doc:`luminosity function models ` + + +Magnitude integrals +------------------- + +Use ``lf.integrals`` when you want to integrate a luminosity function over an +absolute magnitude range. + +For example: + +.. code-block:: python + + number_density = lf.integrals.number_density( + redshift=0.5, + m_bright=-24.0, + m_faint=-16.0, + n_m=800, + ) + +Other useful quantities include luminosity density, mean luminosity, and +selection-weighted number density. + +See the dedicated page for complete examples: + +:doc:`magnitude_integrals` + + +Completeness calculations +------------------------- + +Use ``lf.completeness`` when a survey apparent magnitude limit is part of the +calculation. + +For example: + +.. code-block:: python + + catalog_fraction = lf.completeness.catalog_fraction( + cosmo, + redshift=0.5, + m_lim=24.0, + m_bright=-24.0, + m_faint=-16.0, + h=0.7, + ) + +This answers a common survey question: given an apparent magnitude limit, what +fraction of the intrinsic luminosity function is visible at a given redshift? + +The same namespace can also be used to compute observed number densities, +missing number densities, out-of-catalog fractions, and the absolute magnitude +limit implied by an apparent magnitude cut. + +See the dedicated page for complete examples: + +:doc:`catalog completeness ` + + +Redshift-density calculations +----------------------------- + +Use ``lf.redshift_density`` when you want to build an LF-weighted redshift trend. + +For example: + +.. code-block:: python + + weighted_density = lf.redshift_density.weighted( + redshift, + m_lim=24.0, + m_bright=-24.0, + luminosity_distance_mpc_fn=luminosity_distance_mpc, + volume_weight_fn=volume_weight, + n_m=800, + ) + +This is useful when LFKit is used as part of a survey, tomography, or forecasting +workflow. + +See the dedicated page for complete examples: + +:doc:`redshift_density` + + +Magnitude and luminosity conversions +------------------------------------ + +Use ``lf.magnitudes`` for apparent magnitude, absolute magnitude, and +luminosity-distance conversions. + +For example: + +.. code-block:: python + + absolute_mag = lf.magnitudes.absolute_from_luminosity_distance( + apparent_mag, + luminosity_distance_mpc, + ) + +Use ``lf.luminosities`` for luminosity ratios and related luminosity variables: + +.. code-block:: python + + luminosity_ratio = lf.luminosities.ratio_from_magnitudes( + absolute_mag, + m_star, + ) + +These helpers are especially useful for diagnostics, selection functions, and +plots that compare magnitude and luminosity conventions. + +See the dedicated page for complete examples: + +:doc:`magnitudes_and_luminosities` + + +Conditional luminosity functions +-------------------------------- + +Use ``ConditionalLuminosityFunction`` when the luminosity distribution is +conditioned on another variable, such as halo mass. + +A typical workflow looks like: + +.. code-block:: python + + from lfkit import ConditionalLuminosityFunction + + clf = ConditionalLuminosityFunction.schechter( + phi_star=1.0, + l_star=1.0e10, + alpha=-1.1, + ) + + phi = clf.phi(luminosity, halo_mass) + +Conditional luminosity function examples are kept on a separate page because +they answer a different modeling question from ordinary luminosity functions. + +See the dedicated page for complete examples: + +:doc:`conditional luminosity function ` + + +Photometric corrections +----------------------- + +Use :class:`lfkit.Corrections` when you want to construct or evaluate +photometric corrections. + +Correction callables can be passed into calculations that need k-corrections or +evolution corrections. For example: + +.. code-block:: python + + number_density = lf.redshift_density.integrated_number_density( + redshift, + m_lim=24.0, + m_bright=-24.0, + luminosity_distance_mpc_fn=luminosity_distance_mpc, + k_correction_fn=k_correction, + e_correction_fn=e_correction, + n_m=800, + ) + +The magnitude convention used by LFKit is: + +.. math:: + + M = m - \mu - K + E, + +or equivalently, + +.. math:: + + m = M + \mu + K - E. + +See the correction-specific pages for complete examples: + +:doc:`kcorrect_examples` + +:doc:`poggianti_examples` + + +Model discovery +--------------- + +LFKit provides methods for inspecting available model names. These are useful in +notebooks, examples, and validation scripts. + +For ordinary luminosity functions: + +.. code-block:: python + + from lfkit import LuminosityFunction + + LuminosityFunction.available_models() + LuminosityFunction.available_from_m_models() + LuminosityFunction.available_parameter_models() + +For conditional luminosity functions: + +.. code-block:: python + + from lfkit import ConditionalLuminosityFunction + + ConditionalLuminosityFunction.available_models() + +See the dedicated page for complete examples: + +:doc:`model_registry` + + +Next steps +---------- + +If you are new to LFKit, start with the luminosity function examples page and +then move to the calculation that matches your use case: + +* use :doc:`magnitude_integrals` for intrinsic LF integrals, +* use :doc:`catalog completeness ` for survey magnitude limits, +* use :doc:`redshift_density` for LF-weighted redshift trends, +* use :doc:`magnitudes_and_luminosities` for conversions and diagnostics, +* use :doc:`kcorrect_examples` or :doc:`poggianti_examples` when corrections are + needed. \ No newline at end of file diff --git a/docs/examples/luminosity_function_models.rst b/docs/examples/luminosity_function_models.rst index 6e6c554..32c3b2c 100644 --- a/docs/examples/luminosity_function_models.rst +++ b/docs/examples/luminosity_function_models.rst @@ -6,11 +6,12 @@ ====================================== This page introduces the luminosity function models exposed by -:class:`lfkit.LuminosityFunction`. +:class:`lfkit.LuminosityFunction`. These models describe the abundance of +galaxies as a function of magnitude, usually written as :math:`\Phi(M)`. The examples focus on constructing, evaluating, visualizing, and comparing luminosity function models. Magnitude integrals, completeness calculations, -apparent-magnitude limits, redshift-density weighting, and conditional +apparent magnitude limits, redshift-density weighting, and conditional luminosity functions are covered on separate pages. The API is centered on :class:`lfkit.LuminosityFunction`. A luminosity function @@ -23,7 +24,7 @@ function. For example, if ``phi_star`` is supplied in :math:`{\rm Mpc}^{-3}\,{\rm mag}^{-1}`. -Schechter-family models +Schechter family models ----------------------- The Schechter family is the main luminosity function model family currently @@ -31,8 +32,10 @@ exposed by LFKit. It includes the standard Schechter model, double-Schechter variants, and redshift-evolving Schechter models. These models are useful for describing galaxy luminosity functions with a -power-law faint end and an exponential bright-end cutoff. The examples below -show how to construct, evaluate, compare, and inspect Schechter-family models. +power-law faint end and an exponential bright-end cutoff. The faint end controls +the abundance of low-luminosity galaxies, while the bright-end cutoff suppresses +very luminous galaxies. The examples below show how to construct, evaluate, +compare, and inspect Schechter-family models. Standard Schechter luminosity function @@ -42,6 +45,10 @@ A Schechter luminosity function can be created with :meth:`lfkit.LuminosityFunction.schechter`. The returned object evaluates :math:`\Phi(M)` through :meth:`lfkit.LuminosityFunction.phi`. +This example shows the basic shape of the model in absolute magnitude. The +curve rises toward fainter magnitudes because of the power-law faint end, while +the abundance drops rapidly at the bright end because of the exponential cutoff. + .. plot:: :include-source: True :width: 520 @@ -70,7 +77,7 @@ A Schechter luminosity function can be created with absolute_mag, phi, lw=3, - color=cmr.take_cmap_colors("cmr.guppy", 1, cmap_range=(0.72, 0.9))[0], + color=cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0., 0.2))[1], ) ax.set_yscale("log") @@ -85,15 +92,17 @@ A Schechter luminosity function can be created with plt.tight_layout() -Standard Schechter luminosity function with apparent-magnitude axis +Standard Schechter luminosity function with apparent magnitude axis ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The Schechter luminosity function is evaluated in absolute magnitude. A -secondary x-axis can show the corresponding apparent magnitude at a fixed +secondary *x*-axis can show the corresponding apparent magnitude at a fixed luminosity distance using the LFKit magnitude converters. -This keeps the model-native absolute-magnitude axis while also showing where -the same magnitude range would appear observationally. +This keeps the model-native absolute magnitude axis while also showing where +the same magnitude range would appear observationally. The apparent magnitude +axis is only a reference conversion for the chosen luminosity distance; changing +that distance would shift the upper axis. .. plot:: :include-source: True @@ -117,18 +126,6 @@ the same magnitude range would appear observationally. luminosity_distance_mpc = 3500.0 - def absolute_to_apparent(absolute_mag): - return lf.magnitudes.apparent_from_luminosity_distance( - absolute_mag, - luminosity_distance_mpc, - ) - - def apparent_to_absolute(apparent_mag): - return lf.magnitudes.absolute_from_luminosity_distance( - apparent_mag, - luminosity_distance_mpc, - ) - absolute_mag = np.linspace(-24.0, -14.0, 500) phi = lf.phi(absolute_mag) @@ -137,12 +134,21 @@ the same magnitude range would appear observationally. absolute_mag, phi, lw=3, - color=cmr.take_cmap_colors("cmr.guppy", 1, cmap_range=(0.72, 0.9))[0], + color=cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))[1], ) secax = ax.secondary_xaxis( "top", - functions=(absolute_to_apparent, apparent_to_absolute), + functions=( + lambda absolute_mag: lf.magnitudes.apparent_from_luminosity_distance( + absolute_mag, + luminosity_distance_mpc, + ), + lambda apparent_mag: lf.magnitudes.absolute_from_luminosity_distance( + apparent_mag, + luminosity_distance_mpc, + ), + ), ) ax.set_yscale("log") @@ -153,8 +159,9 @@ the same magnitude range would appear observationally. fontsize=LABEL_SIZE, ) ax.set_title( - "Schechter luminosity function with apparent-magnitude axis", + "Schechter luminosity function", fontsize=TITLE_SIZE, + pad=0.5, ) ax.tick_params(axis="both", labelsize=TICK_SIZE) @@ -169,16 +176,14 @@ Comparing Schechter slopes ~~~~~~~~~~~~~~~~~~~~~~~~~~ Changing :math:`\alpha` modifies the faint-end behaviour of the luminosity -function. - -This comparison shows how the faint-end slope changes the abundance of faint -galaxies while keeping the other Schechter parameters fixed. More negative -values of :math:`\alpha` produce a steeper rise toward faint magnitudes. +function. More negative values of :math:`\alpha` produce a steeper rise toward +faint magnitudes, while less negative values give a shallower faint-end +population. -This is useful because the faint-end slope often controls how strongly low -luminosity galaxies contribute to integrated quantities, such as number density -or luminosity density. Even if the bright end is almost unchanged, the total -abundance can change noticeably when the faint end is modified. +This comparison keeps the other Schechter parameters fixed so that the effect +of :math:`\alpha` is isolated. This is useful because the faint-end slope often +controls how strongly low-luminosity galaxies contribute to integrated +quantities such as number density or luminosity density. .. plot:: :include-source: True @@ -195,10 +200,14 @@ abundance can change noticeably when the faint end is modified. TITLE_SIZE = 17 LEGEND_SIZE = 15 - colors = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.03, 0.26)) - absolute_mag = np.linspace(-24.0, -14.0, 500) - alphas = [-0.8, -1.1, -1.4] + alphas = [-0.5, -0.75, -1.0, -1.25, -1.5] + + colors = cmr.take_cmap_colors( + "cmr.guppy", + len(alphas), + cmap_range=(0.0, 0.2) + ) fig, ax = plt.subplots(figsize=(7.0, 5.0)) @@ -236,6 +245,10 @@ The API also exposes a double-Schechter constructor. This is useful for models that need extra flexibility at the faint end while retaining a Schechter-like bright-end cutoff. +The double-Schechter form adds a second faint-end component. This can represent +a luminosity function whose faint population cannot be captured well by one +single Schechter slope. + .. plot:: :include-source: True :width: 520 @@ -266,7 +279,7 @@ bright-end cutoff. ) absolute_mag = np.linspace(-24.0, -14.0, 500) - colors = cmr.take_cmap_colors("cmr.guppy", 2, cmap_range=(0.15, 0.85)) + colors = cmr.take_cmap_colors("cmr.guppy", 2, cmap_range=(0.0, 0.2)) fig, ax = plt.subplots(figsize=(7.0, 5.0)) ax.plot( @@ -304,6 +317,11 @@ An evolving Schechter luminosity function lets the Schechter parameters depend on redshift through LFKit's registered parameter models. This is useful when the same LF object should evaluate :math:`\Phi(M, z)` at many redshifts. +The curves below show how the predicted luminosity function changes when the +normalization, characteristic magnitude, or slope evolve with redshift. This is +the model form used when the galaxy population is not assumed to be fixed across +cosmic time. + .. plot:: :include-source: True :width: 520 @@ -330,7 +348,11 @@ same LF object should evaluate :math:`\Phi(M, z)` at many redshifts. absolute_mag = np.linspace(-24.0, -14.0, 500) redshifts = [0.1, 0.6, 1.1] - colors = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.03, 0.26)) + colors = cmr.take_cmap_colors( + "cmr.guppy", + len(redshifts), + cmap_range=(0.0, 0.2) + ) fig, ax = plt.subplots(figsize=(7.0, 5.0)) @@ -357,17 +379,18 @@ same LF object should evaluate :math:`\Phi(M, z)` at many redshifts. plt.tight_layout() -Evolving Schechter luminosity function with apparent-magnitude axis +Evolving Schechter luminosity function with apparent magnitude axis ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The evolving Schechter model is evaluated as :math:`\Phi(M, z)`. A secondary -x-axis can show the apparent magnitude corresponding to the absolute-magnitude +*x*-axis can show the apparent magnitude corresponding to the absolute magnitude range at a chosen reference luminosity distance. Here, the curves are evaluated at several redshifts, while the upper apparent magnitude axis is defined for the reference redshift :math:`z=0.6`. This keeps the bottom axis model-native and avoids mixing several different -distance-redshift mappings into one top axis. +distance-redshift mappings into one top axis. The top axis should therefore be +read as a reference guide, not as a separate conversion for every curve. .. plot:: :include-source: True @@ -404,20 +427,12 @@ distance-redshift mappings into one top axis. } reference_luminosity_distance_mpc = luminosity_distance_mpc[reference_redshift] - def absolute_to_apparent(absolute_mag): - return lf.magnitudes.apparent_from_luminosity_distance( - absolute_mag, - reference_luminosity_distance_mpc, + colors = cmr.take_cmap_colors( + "cmr.guppy", + len(redshifts), + cmap_range=(0.0, 0.2) ) - def apparent_to_absolute(apparent_mag): - return lf.magnitudes.absolute_from_luminosity_distance( - apparent_mag, - reference_luminosity_distance_mpc, - ) - - colors = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.03, 0.26)) - fig, ax = plt.subplots(figsize=(7.2, 5.0)) for z_value, color in zip(redshifts, colors): @@ -432,7 +447,16 @@ distance-redshift mappings into one top axis. secax = ax.secondary_xaxis( "top", - functions=(absolute_to_apparent, apparent_to_absolute), + functions=( + lambda absolute_mag: lf.magnitudes.apparent_from_luminosity_distance( + absolute_mag, + reference_luminosity_distance_mpc, + ), + lambda apparent_mag: lf.magnitudes.absolute_from_luminosity_distance( + apparent_mag, + reference_luminosity_distance_mpc, + ), + ), ) ax.set_yscale("log") @@ -443,8 +467,9 @@ distance-redshift mappings into one top axis. fontsize=LABEL_SIZE, ) ax.set_title( - "Evolving Schechter luminosity function with apparent-magnitude axis", + "Evolving Schechter luminosity function", fontsize=TITLE_SIZE, + pad=0.5, ) ax.tick_params(axis="both", labelsize=TICK_SIZE) ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") @@ -463,12 +488,18 @@ Inspecting evolving parameters For evolving models, :meth:`lfkit.LuminosityFunction.parameters` evaluates the registered parameter models at the requested redshift. This is useful for -checking the physical behaviour before using the LF in number-density or -selection calculations. +checking the model behaviour before using the LF in number-density or selection +calculations. + +Here all three Schechter parameters evolve with redshift, including the +faint-end slope :math:`\alpha(z)`. This diagnostic separates the ingredients of +the luminosity function: :math:`\phi_*` controls the normalization, +:math:`M_*` sets the characteristic magnitude, and :math:`\alpha` controls the +faint-end slope. .. plot:: :include-source: True - :width: 520 + :width: 560 import numpy as np import matplotlib.pyplot as plt @@ -479,49 +510,57 @@ selection calculations. LABEL_SIZE = 15 TICK_SIZE = 13 TITLE_SIZE = 17 - LEGEND_SIZE = 15 lf = LuminosityFunction.evolving_schechter( phi_model="linear_p", phi_kwargs={"phi_0_star": 1.0e-3, "p": 0.7}, m_star_model="linear_q", m_star_kwargs={"m_0_star": -20.5, "q": 0.8, "z_ref": 0.1}, - alpha_model="constant", - alpha_kwargs={"alpha": -1.1}, + alpha_model="linear", + alpha_kwargs={"alpha_0": -1.0, "alpha_1": -0.25, "z_ref": 0.1}, ) redshift = np.linspace(0.0, 1.5, 200) phi_star, m_star, alpha = lf.parameters(redshift) - colors = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.1, 0.9)) + colors = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2)) - fig, ax = plt.subplots(figsize=(7.0, 5.0)) - ax.plot( + fig, axes = plt.subplots( + nrows=3, + ncols=1, + figsize=(7.0, 8.0), + sharex=True, + ) + + axes[0].plot( redshift, phi_star / 1.0e-3, lw=3, color=colors[0], - label=r"$\phi_*/10^{-3}$", ) - ax.plot( + axes[0].set_ylabel(r"$\phi_*/10^{-3}$", fontsize=LABEL_SIZE) + + axes[1].plot( redshift, m_star, lw=3, color=colors[1], - label=r"$M_*$", ) - ax.plot( + axes[1].set_ylabel(r"$M_*$", fontsize=LABEL_SIZE) + + axes[2].plot( redshift, alpha, lw=3, color=colors[2], - label=r"$\alpha$", ) + axes[2].set_ylabel(r"$\alpha(z)$", fontsize=LABEL_SIZE) + axes[2].set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) + + axes[0].set_title("Evolving Schechter parameters", fontsize=TITLE_SIZE) + + for axis in axes: + axis.tick_params(axis="both", labelsize=TICK_SIZE) - ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) - ax.set_ylabel("Parameter value", fontsize=LABEL_SIZE) - ax.set_title("Evolving Schechter parameters", fontsize=TITLE_SIZE) - ax.tick_params(axis="both", labelsize=TICK_SIZE) - ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") plt.tight_layout() @@ -532,12 +571,18 @@ The same evolving model can be shown over the full magnitude-redshift plane. The filled colour scale shows :math:`\log_{10}\Phi(M, z)`, while contours mark constant abundance levels. +This view is useful for seeing the joint magnitude and redshift dependence in +one panel. Horizontal changes show how the luminosity function varies with +magnitude, while vertical changes show how the evolving parameters modify the +model with redshift. + .. plot:: :include-source: True :width: 560 import numpy as np import matplotlib.pyplot as plt + import cmasher as cmr from lfkit import LuminosityFunction @@ -562,12 +607,13 @@ constant abundance levels. log_phi = np.log10(phi) fig, ax = plt.subplots(figsize=(7.2, 5.0)) + mesh = ax.pcolormesh( absolute_mag, redshift, log_phi, shading="auto", - cmap="cmr.guppy", + cmap=cmr.get_sub_cmap('cmr.guppy_r', 0.0, 1) ) contour_levels = [-5.0, -4.0, -3.0, -2.0] diff --git a/docs/examples/magnitude_integrals.rst b/docs/examples/magnitude_integrals.rst index e2a5659..ca60a30 100644 --- a/docs/examples/magnitude_integrals.rst +++ b/docs/examples/magnitude_integrals.rst @@ -2,7 +2,7 @@ :alt: LFKit logo :width: 50px -|lfkitlogo| Luminosity-function magnitude integrals +|lfkitlogo| Luminosity function magnitude integrals =================================================== This page shows how to integrate a bound @@ -26,11 +26,14 @@ Integrated number density ------------------------- The integrated number density is the luminosity function integrated over a -finite absolute-magnitude range. +finite absolute magnitude range. It gives the abundance of galaxies retained by +a chosen magnitude selection. This example compares a bright sample to a broader sample that also includes fainter galaxies. The broader magnitude range gives a larger number density -because more galaxies are included in the integral. +because more of the faint end of the luminosity function is included. In +practice, these magnitude limits can be changed to match a survey cut, a +science sample definition, or a test range used for model comparisons. .. plot:: :include-source: True @@ -39,6 +42,7 @@ because more galaxies are included in the integral. import numpy as np import matplotlib.pyplot as plt import cmasher as cmr + from matplotlib.ticker import LogLocator, LogFormatterMathtext from lfkit import LuminosityFunction @@ -72,25 +76,32 @@ because more galaxies are included in the integral. n_m=800, ) - colors = cmr.take_cmap_colors("cmr.guppy", 2, cmap_range=(0.2, 0.9)) + red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))[1] + blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0))[1] fig, ax = plt.subplots(figsize=(7.0, 5.0)) ax.plot( redshift, n_bright, lw=3, - color=colors[0], + color=red, label=r"$-24 \leq M \leq -20$", ) ax.plot( redshift, n_total, lw=3, - color=colors[1], + color=blue, label=r"$-24 \leq M \leq -16$", ) ax.set_yscale("log") + ax.yaxis.set_major_locator(LogLocator(base=10.0, numticks=5)) + ax.yaxis.set_major_formatter(LogFormatterMathtext(base=10.0)) + ax.yaxis.set_minor_locator( + LogLocator(base=10.0, subs=np.arange(2, 10) * 0.1, numticks=12) + ) + ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) ax.set_ylabel(r"Number density [$\mathrm{Mpc}^{-3}$]", fontsize=LABEL_SIZE) ax.set_title("Integrated LF number density", fontsize=TITLE_SIZE) @@ -103,11 +114,13 @@ Cumulative number density ------------------------- The number density can also be viewed as a cumulative function of the faint -absolute-magnitude limit. +absolute magnitude limit. Here the bright limit is held fixed, while the faint +limit is moved across the luminosity function. This diagnostic is useful for checking how much faint galaxies contribute to the total abundance. As the faint limit moves to less negative magnitudes, more -of the luminosity function is included. +galaxies are included and the cumulative number density increases. The shape of +this curve shows where most of the abundance is coming from in magnitude space. .. plot:: :include-source: True @@ -143,13 +156,13 @@ of the luminosity function is included. magnitude_limits, number_density, lw=3, - color=cmr.take_cmap_colors("cmr.guppy", 1, cmap_range=(0.72, 0.9))[0], + color=cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))[1], ) ax.set_yscale("log") ax.invert_xaxis() ax.set_xlabel( - r"Faint absolute-magnitude limit $M_{\rm faint}$", + r"Faint absolute magnitude limit $M_{\rm faint}$", fontsize=LABEL_SIZE, ) ax.set_ylabel( @@ -165,10 +178,14 @@ Luminosity density and mean luminosity -------------------------------------- The same namespace can compute luminosity-weighted summaries such as luminosity -density and mean luminosity over a selected magnitude range. +density and mean luminosity over a selected magnitude range. These quantities +weight the luminosity function by luminosity before integrating, so they are +more sensitive to the bright end than the number density alone. The curves below are normalized by their first redshift value to emphasize the -relative redshift trend rather than the absolute normalization. +relative redshift trend rather than the absolute normalization. This makes it +easier to compare how the luminosity density and mean luminosity evolve across +redshift for the same underlying luminosity function model. .. plot:: :include-source: True @@ -210,21 +227,22 @@ relative redshift trend rather than the absolute normalization. n_m=800, ) - colors = cmr.take_cmap_colors("cmr.guppy", 2, cmap_range=(0.2, 0.9)) + red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))[1] + blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0))[1] fig, ax = plt.subplots(figsize=(7.0, 5.0)) ax.plot( redshift, luminosity_density / luminosity_density[0], lw=3, - color=colors[0], + color=red, label="Luminosity density", ) ax.plot( redshift, mean_luminosity / mean_luminosity[0], lw=3, - color=colors[1], + color=blue, label="Mean luminosity", ) @@ -240,11 +258,15 @@ Selection-weighted number density --------------------------------- Selection weights can be applied directly through the integrals API. This is -useful when a sample is not selected by a hard magnitude cut alone. +useful when a sample is not selected by a hard magnitude cut alone, but instead +has a gradual completeness, targeting, or selection probability. -This example uses a smooth selection function in absolute magnitude. The -selected number density is lower than the total number density because the -selection downweights part of the magnitude range. +This example uses a smooth user-defined selection function in absolute +magnitude. The function used here is only one possible choice: users can replace +it with any callable that returns a selection weight for absolute magnitude and +redshift. The selected number density is lower than the total number density +because the selection downweights part of the magnitude range instead of +retaining every galaxy equally. .. plot:: :include-source: True @@ -253,6 +275,7 @@ selection downweights part of the magnitude range. import numpy as np import matplotlib.pyplot as plt import cmasher as cmr + from matplotlib.ticker import LogLocator, LogFormatterMathtext from lfkit import LuminosityFunction @@ -292,19 +315,26 @@ selection downweights part of the magnitude range. n_m=800, ) - colors = cmr.take_cmap_colors("cmr.guppy", 2, cmap_range=(0.2, 0.9)) + red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))[1] + blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0))[1] fig, ax = plt.subplots(figsize=(7.0, 5.0)) - ax.plot(redshift, n_total, lw=3, color=colors[0], label="Total") + ax.plot(redshift, n_total, lw=3, color=red, label="Total") ax.plot( redshift, n_selected, lw=3, - color=colors[1], + color=blue, label="Selection weighted", ) ax.set_yscale("log") + ax.yaxis.set_major_locator(LogLocator(base=10.0, numticks=5)) + ax.yaxis.set_major_formatter(LogFormatterMathtext(base=10.0)) + ax.yaxis.set_minor_locator( + LogLocator(base=10.0, subs=np.arange(2, 10) * 0.1, numticks=12) + ) + ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) ax.set_ylabel(r"Number density [$\mathrm{Mpc}^{-3}$]", fontsize=LABEL_SIZE) ax.set_title("Selection-weighted LF number density", fontsize=TITLE_SIZE) @@ -317,14 +347,33 @@ Selection fraction ------------------ The selected fraction is the ratio between the selection-weighted number density -and the total number density over the same reference magnitude range. - -This diagnostic is useful for checking how strongly a soft selection function -changes the effective sample abundance as a function of redshift. +and the total number density over the same reference magnitude range. It is a +dimensionless diagnostic of how much of the available luminosity function remains +after applying a selection function. + +This diagnostic is useful for checking how strongly a soft selection changes the +effective sample abundance as a function of redshift. A value close to one means +that most of the luminosity function within the reference magnitude range is +retained, while a value close to zero means that the selection removes most of +the available population. + +The surface below repeats the calculation for several values of the selection +threshold. For each threshold, the code defines a smooth user-controlled +selection function and computes the retained fraction relative to the same total +number density. Brighter thresholds retain a smaller fraction of the luminosity +function, while fainter thresholds retain more of the reference magnitude range. +The contours mark constant retained fractions. + +In this example the effective limiting magnitude becomes brighter with +redshift, so the retained fraction generally decreases toward larger redshift at +fixed threshold. Moving to fainter threshold values increases the retained +fraction because more of the faint end of the luminosity function passes the +selection. Users can replace the threshold model, redshift dependence, or width +of the soft transition to represent a different survey selection. .. plot:: :include-source: True - :width: 520 + :width: 560 import numpy as np import matplotlib.pyplot as plt @@ -346,11 +395,7 @@ changes the effective sample abundance as a function of redshift. ) redshift = np.linspace(0.05, 1.5, 180) - - def soft_selection(absolute_mag, z): - limiting_mag = -18.5 - 1.2 * z - width = 0.35 - return 1.0 / (1.0 + np.exp((absolute_mag - limiting_mag) / width)) + selection_thresholds = np.linspace(-22.0, -15.5, 160) n_total = lf.integrals.number_density( redshift, @@ -359,27 +404,70 @@ changes the effective sample abundance as a function of redshift. n_m=800, ) - n_selected = lf.integrals.selection_weighted_number_density( + selected_fraction = [] + + for threshold in selection_thresholds: + + def soft_selection(absolute_mag, z, threshold=threshold): + limiting_mag = threshold - 1.2 * z + width = 0.35 + return 1.0 / (1.0 + np.exp((absolute_mag - limiting_mag) / width)) + + n_selected = lf.integrals.selection_weighted_number_density( + redshift, + selection_fn=soft_selection, + m_bright=-24.0, + m_faint=-14.0, + n_m=800, + ) + + selected_fraction.append(n_selected / n_total) + + selected_fraction = np.asarray(selected_fraction) + + fig, ax = plt.subplots(figsize=(7.2, 5.0)) + + mesh = ax.pcolormesh( redshift, - selection_fn=soft_selection, - m_bright=-24.0, - m_faint=-14.0, - n_m=800, + selection_thresholds, + selected_fraction, + shading="auto", + cmap=cmr.get_sub_cmap("cmr.guppy_r", 0.0, 1.0), + vmin=0.0, + vmax=1.0, ) - selected_fraction = n_selected / n_total + contour_levels = [0.1, 0.25, 0.5, 0.75, 0.9] - fig, ax = plt.subplots(figsize=(7.0, 5.0)) - ax.plot( + contours = ax.contour( redshift, + selection_thresholds, selected_fraction, - lw=3, - color=cmr.take_cmap_colors("cmr.guppy", 1, cmap_range=(0.72, 0.9))[0], + levels=contour_levels, + colors="white", + linewidths=1.1, + alpha=0.95, + ) + + ax.clabel( + contours, + inline=True, + fontsize=TICK_SIZE - 1, + fmt="%.2f", ) - ax.set_ylim(-0.05, 1.05) + ax.invert_yaxis() ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) - ax.set_ylabel("Selected fraction", fontsize=LABEL_SIZE) - ax.set_title("Fraction retained by the soft selection", fontsize=TITLE_SIZE) + ax.set_ylabel(r"Selection threshold $M_0$", fontsize=LABEL_SIZE) + ax.set_title( + "Fraction retained by the soft selection", + fontsize=TITLE_SIZE, + pad=0.5, + ) ax.tick_params(axis="both", labelsize=TICK_SIZE) + + cbar = fig.colorbar(mesh, ax=ax) + cbar.set_label("Selected fraction", fontsize=LABEL_SIZE) + cbar.ax.tick_params(labelsize=TICK_SIZE) + plt.tight_layout() diff --git a/docs/examples/magnitudes_and_luminosities.rst b/docs/examples/magnitudes_and_luminosities.rst index 544d698..f4cabdf 100644 --- a/docs/examples/magnitudes_and_luminosities.rst +++ b/docs/examples/magnitudes_and_luminosities.rst @@ -23,7 +23,7 @@ page. LF from apparent magnitude -------------------------- -For models with apparent-magnitude support, +For models with apparent magnitude support, :meth:`lfkit.LuminosityFunction.phi_from_m` converts apparent magnitude to absolute magnitude and evaluates the luminosity function. @@ -64,7 +64,11 @@ than an intrinsic absolute magnitude. apparent_mag = np.linspace(18.0, 26.0, 500) redshifts = [0.3, 0.6, 1.0] - colors = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.08, 0.9)) + colors = cmr.take_cmap_colors( + "cmr.guppy", + len(redshifts), + cmap_range=(0.0, 0.2), + ) fig, ax = plt.subplots(figsize=(7.0, 5.0)) @@ -131,21 +135,22 @@ distance array from another cosmology backend. luminosity_distance_mpc, ) - colors = cmr.take_cmap_colors("cmr.guppy", 2, cmap_range=(0.2, 0.9)) + red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))[1] + blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0))[1] fig, ax = plt.subplots(figsize=(7.0, 5.0)) ax.plot( luminosity_distance_mpc, absolute_mag, lw=3, - color=colors[0], + color=red, label=r"$M(m=24, d_L)$", ) ax.plot( luminosity_distance_mpc, apparent_mag, lw=3, - color=colors[1], + color=blue, label=r"$m(M=-20.5, d_L)$", ) @@ -199,7 +204,7 @@ increases toward more negative absolute magnitudes. absolute_mag, luminosity_ratio, lw=3, - color=cmr.take_cmap_colors("cmr.guppy", 1, cmap_range=(0.72, 0.9))[0], + color=cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))[1], ) ax.set_yscale("log") diff --git a/docs/examples/model_registry.rst b/docs/examples/model_registry.rst index 1e7fe4a..7a76ad0 100644 --- a/docs/examples/model_registry.rst +++ b/docs/examples/model_registry.rst @@ -23,7 +23,7 @@ Available luminosity function models LuminosityFunction.available_models() -Available apparent-magnitude models +Available apparent magnitude models ----------------------------------- .. code-block:: python diff --git a/docs/examples/redshift_density.rst b/docs/examples/redshift_density.rst index 8778e3f..90bb28f 100644 --- a/docs/examples/redshift_density.rst +++ b/docs/examples/redshift_density.rst @@ -9,9 +9,9 @@ This page shows how to convert a luminosity function into a redshift-dependent selection or weighting factor. The ``redshift_density`` namespace is useful when constructing LF-dependent -redshift trends for survey forecasting. It combines an apparent-magnitude limit -with a luminosity-distance callable, integrates the luminosity function over the -visible absolute-magnitude range, and can optionally apply a redshift or volume +redshift trends for survey forecasting. It combines an apparent magnitude limit +with a luminosity distance callable, integrates the luminosity function over the +visible absolute magnitude range, and can optionally apply a redshift or volume weight. This is not required to be a complete survey :math:`n(z)` by itself. It is the @@ -25,7 +25,7 @@ Magnitude-limited LF redshift density ------------------------------------- The integrated version computes the luminosity function number density selected -by an apparent-magnitude limit. +by an apparent magnitude limit. .. plot:: :include-source: True @@ -57,7 +57,7 @@ by an apparent-magnitude limit. return 3000.0 * z * (1.0 + 0.5 * z) limits = [23.5, 24.5, 25.5] - colors = cmr.take_cmap_colors("cmr.guppy", len(limits), cmap_range=(0.08, 0.9)) + colors = cmr.take_cmap_colors("cmr.guppy", len(limits), cmap_range=(0.0, 0.2)) fig, ax = plt.subplots(figsize=(7.0, 5.0)) @@ -143,21 +143,22 @@ a user-provided redshift or volume weight. n_m=800, ) - colors = cmr.take_cmap_colors("cmr.guppy", 2, cmap_range=(0.2, 0.9)) + red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))[1] + blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0))[1] fig, ax = plt.subplots(figsize=(7.0, 5.0)) ax.plot( redshift, number_density / np.max(number_density), lw=3, - color=colors[0], + color=red, label="Magnitude-limited LF integral", ) ax.plot( redshift, weighted_density / np.max(weighted_density), lw=3, - color=colors[1], + color=blue, label="LF-weighted redshift density", ) @@ -232,11 +233,12 @@ cosmology backend. low_z_trend /= np.trapezoid(low_z_trend, redshift) tapered_trend /= np.trapezoid(tapered_trend, redshift) - colors = cmr.take_cmap_colors("cmr.guppy", 2, cmap_range=(0.2, 0.9)) + red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))[1] + blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0))[1] fig, ax = plt.subplots(figsize=(7.0, 5.0)) - ax.plot(redshift, low_z_trend, lw=3, color=colors[0], label=r"$w(z)=z^2$") - ax.plot(redshift, tapered_trend, lw=3, color=colors[1], label=r"$w(z)=z^2 e^{-z/0.8}$") + ax.plot(redshift, low_z_trend, lw=3, color=red, label=r"$w(z)=z^2$") + ax.plot(redshift, tapered_trend, lw=3, color=blue, label=r"$w(z)=z^2 e^{-z/0.8}$") ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) ax.set_ylabel("Normalized weighted trend", fontsize=LABEL_SIZE) diff --git a/docs/index.rst b/docs/index.rst index d08b85a..6fac43d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,25 +5,25 @@ |lfkitlogo| LFKit ================= -**LFKit** is a toolkit for modelling galaxy luminosity functions, -photometric corrections, and magnitude-limited catalog completeness. - -It provides a clean interface for building theoretical luminosity functions, -including redshift-dependent parameter models, and for connecting apparent -magnitude limits to observable and missing galaxy number densities. - -LFKit is designed to be science-use-case agnostic: the same luminosity function -machinery can be used in photometric-redshift modelling, intrinsic-alignment -modelling, cluster science, GW-cosmology catalog completeness, or any other -analysis that needs luminosity function-based number densities. +**LFKit** is a Python toolkit for modelling galaxy luminosity functions, +photometric corrections, magnitude conversions, and magnitude-limited catalog +selection. + +It provides a modular interface for turning luminosity function models into +number densities, completeness fractions, LF-weighted redshift trends, and +observable or missing catalog populations. The same machinery can be used in +photometric redshift modelling, intrinsic alignment modelling, cluster science, +GW-cosmology catalog completeness, survey forecasting, or any analysis that +connects galaxy luminosities, magnitudes, redshift evolution, and observed +catalog limits. Getting started --------------- -Use the examples section for runnable workflows with plots, or the API -reference for detailed documentation of the public classes and functions. +Start with the theory overview for the main conventions and definitions, or use +the examples section for executable workflows with plots. -.. grid:: 3 +.. grid:: 2 :gutter: 2 .. grid-item-card:: @@ -31,9 +31,10 @@ reference for detailed documentation of the public classes and functions. :link-type: doc :shadow: md - **About** + **Theory and overview** ^^^ - Overview of the package, scope, and design choices. + Core concepts, conventions, and package scope, including luminosity + functions, photometry, corrections, and catalog selection. .. grid-item-card:: :link: examples/index @@ -42,23 +43,19 @@ reference for detailed documentation of the public classes and functions. **Examples** ^^^ - Runnable examples for luminosity functions, corrections, and catalog - completeness. + Runnable examples for luminosity function models, magnitude conversions, + integrals, corrections, redshift trends, and catalog completeness. - .. grid-item-card:: - :link: api/index - :link-type: doc - :shadow: md - - **API reference** - ^^^ - Public classes, functions, and modules. +Documentation +------------- .. toctree:: - :maxdepth: 2 - :caption: Documentation - :hidden: + :maxdepth: 1 + installation about/index examples/index - api/index \ No newline at end of file + api/index + citation + contributing + license diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..afae5b2 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,47 @@ +.. |lfkitlogo| image:: /_static/logos/lfkit_logo.png + :alt: LFKit logo black + :width: 50px + + +|lfkitlogo| Installation +======================== + +LFKit can be installed from PyPI. The PyPI distribution name is ``py-lfkit``:: + + pip3 install py-lfkit + +LFKit can additionally be installed from source. +To create an editable installation, clone the repository and run:: + + git clone https://github.com/cosmology-kit/lfkit.git + cd lfkit + pip install -e . + +Alternatively, install directly from GitHub with ``pip``:: + + pip install git+https://github.com/cosmology-kit/lfkit.git + + +Development installation +------------------------ + +For development work, clone the repository and install LFKit in editable mode +with the development dependencies:: + + git clone https://github.com/cosmology-kit/lfkit.git + cd lfkit + pip install -e ".[dev]" + +This allows local changes to the source code to be picked up without +reinstalling the package. + + +Optional dependencies +--------------------- + +Some LFKit examples use optional third-party packages for cosmology calculations, +photometric corrections, or plotting. Install these separately when needed by a +specific example page. + +For example, if an example uses CCL, install CCL following the instructions for +your system and Python environment before running that example. diff --git a/docs/license.rst b/docs/license.rst new file mode 100644 index 0000000..6e7bb0e --- /dev/null +++ b/docs/license.rst @@ -0,0 +1,11 @@ +.. |lfkitlogo| image:: /_static/logos/lfkit_logo.png + :alt: LFKit logo black + :width: 50px + + +|lfkitlogo| License +=================== + +MIT License © 2026 Nikolina Šarčević. + +The full license text is available in the project repository as ``LICENSE``. diff --git a/src/lfkit/api/_clf_models.py b/src/lfkit/api/_clf_models.py index 4924c6c..8bd77c2 100644 --- a/src/lfkit/api/_clf_models.py +++ b/src/lfkit/api/_clf_models.py @@ -1,4 +1,4 @@ -"""User-facing conditional luminosity-function model API namespace.""" +"""User-facing conditional luminosity function model API namespace.""" from __future__ import annotations @@ -13,7 +13,7 @@ class LFConditionalModelsAPI: - """Grouped API for evaluating conditional luminosity-function models.""" + """Grouped API for evaluating conditional luminosity function models.""" schechter = staticmethod(conditional_schechter) evolving_schechter = staticmethod(conditional_evolving_schechter) diff --git a/src/lfkit/api/_completeness.py b/src/lfkit/api/_completeness.py index 3fabc49..40f6cc9 100644 --- a/src/lfkit/api/_completeness.py +++ b/src/lfkit/api/_completeness.py @@ -21,7 +21,7 @@ class LFCompletenessAPI: """Grouped API for catalog-completeness calculations. Args: - lf: Parent luminosity-function object. + lf: Parent luminosity function object. """ def __init__(self, lf: LuminosityFunction) -> None: diff --git a/src/lfkit/api/_integrals.py b/src/lfkit/api/_integrals.py index 0a040ae..03f4e07 100644 --- a/src/lfkit/api/_integrals.py +++ b/src/lfkit/api/_integrals.py @@ -1,4 +1,4 @@ -"""User-facing luminosity-function integral API namespace.""" +"""User-facing luminosity function integral API namespace.""" from __future__ import annotations @@ -20,10 +20,10 @@ class LFIntegralsAPI: - """Grouped API for luminosity-function integrals. + """Grouped API for luminosity function integrals. Args: - lf: Parent luminosity-function object. + lf: Parent luminosity function object. """ def __init__(self, lf: LuminosityFunction) -> None: diff --git a/src/lfkit/api/_lf_param_models.py b/src/lfkit/api/_lf_param_models.py index 464a1c8..29b19b6 100644 --- a/src/lfkit/api/_lf_param_models.py +++ b/src/lfkit/api/_lf_param_models.py @@ -24,7 +24,7 @@ class LFModelSpec(TypedDict): - """Description of a luminosity-function model exposed by the API.""" + """Description of a luminosity function model exposed by the API.""" function: Callable[..., Any] requires_z: bool diff --git a/src/lfkit/api/_magnitudes.py b/src/lfkit/api/_magnitudes.py index 0190687..a8d9632 100644 --- a/src/lfkit/api/_magnitudes.py +++ b/src/lfkit/api/_magnitudes.py @@ -12,7 +12,7 @@ class LFMagnitudesAPI: - """Grouped API for apparent- and absolute-magnitude conversions.""" + """Grouped API for apparent- and absolute magnitude conversions.""" correction = staticmethod(total_magnitude_correction) diff --git a/src/lfkit/api/_redshift_density.py b/src/lfkit/api/_redshift_density.py index d732756..6b6ce01 100644 --- a/src/lfkit/api/_redshift_density.py +++ b/src/lfkit/api/_redshift_density.py @@ -18,7 +18,7 @@ class LFRedshiftDensityAPI: """Grouped API for LF-weighted redshift-density calculations. Args: - lf: Parent luminosity-function object. + lf: Parent luminosity function object. """ def __init__(self, lf: LuminosityFunction) -> None: diff --git a/src/lfkit/api/conditional_luminosity_function.py b/src/lfkit/api/conditional_luminosity_function.py index 90bc7f1..07c65d9 100644 --- a/src/lfkit/api/conditional_luminosity_function.py +++ b/src/lfkit/api/conditional_luminosity_function.py @@ -1,4 +1,4 @@ -"""Public conditional luminosity-function constructors.""" +"""Public conditional luminosity function constructors.""" from __future__ import annotations @@ -26,7 +26,7 @@ def _make_conditional_lf( class ConditionalLuminosityFunction: - """Factory namespace for conditional luminosity-function models.""" + """Factory namespace for conditional luminosity function models.""" @staticmethod def schechter( diff --git a/src/lfkit/api/luminosity_function.py b/src/lfkit/api/luminosity_function.py index 545282c..0aa5fa9 100644 --- a/src/lfkit/api/luminosity_function.py +++ b/src/lfkit/api/luminosity_function.py @@ -1,9 +1,9 @@ -r"""Public luminosity-function interface. +r"""Public luminosity function interface. This module provides the user-facing :class:`LuminosityFunction` API for -evaluating luminosity functions in absolute- or apparent-magnitude space. +evaluating luminosity functions in absolute- or apparent magnitude space. -The class stores luminosity-function model state and exposes grouped API +The class stores luminosity function model state and exposes grouped API namespaces for related calculations. Low-level numerical and photometric work remains in the function-based ``lfkit.photometry`` modules. """ @@ -46,10 +46,10 @@ class LuminosityFunction: - """User-facing wrapper for luminosity-function evaluation. + """User-facing wrapper for luminosity function evaluation. Args: - model: Name of the luminosity-function model. + model: Name of the luminosity function model. parameters: Model parameters passed to the underlying LF function. meta: Optional metadata describing the LF source or calibration. """ @@ -181,7 +181,7 @@ def phi( absolute_mag: FloatInput, z: FloatInput | None = None, ) -> FloatArray: - """Evaluate the luminosity function in absolute-magnitude space. + """Evaluate the luminosity function in absolute magnitude space. Args: absolute_mag: Absolute magnitude values where the LF is evaluated. @@ -288,12 +288,12 @@ def _as_callable(self): @staticmethod def available_models() -> list[str]: - """Return luminosity-function model names available through the API.""" + """Return luminosity function model names available through the API.""" return sorted(LF_MODELS) @staticmethod def available_from_m_models() -> list[str]: - """Return models that support apparent-magnitude evaluation.""" + """Return models that support apparent magnitude evaluation.""" return sorted(LF_FROM_M_MODELS) @staticmethod diff --git a/src/lfkit/photometry/catalog_completeness.py b/src/lfkit/photometry/catalog_completeness.py index 1acece3..223b1e8 100644 --- a/src/lfkit/photometry/catalog_completeness.py +++ b/src/lfkit/photometry/catalog_completeness.py @@ -5,7 +5,7 @@ for applications that need an out-of-catalog correction, such as galaxy-catalog priors for gravitational-wave cosmology. -The utilities convert an apparent magnitude limit into an absolute-magnitude +The utilities convert an apparent magnitude limit into an absolute magnitude limit and call the generic LF integration helpers to return number densities or fractions. @@ -53,7 +53,7 @@ def absolute_magnitude_limit( k_correction: FloatInput | None = None, e_correction: FloatInput | None = None, ) -> FloatArray: - r"""Return the absolute-magnitude limit of an apparent-magnitude catalog cut. + r"""Return the absolute magnitude limit of an apparent magnitude catalog cut. This converts an apparent magnitude limit into the corresponding limiting absolute magnitude at each redshift, @@ -124,8 +124,8 @@ def observed_number_density( z: Redshift value or array-like of redshift values. lf: Luminosity-function callable with signature ``lf(M, z)``. m_lim: Apparent magnitude limit of the catalog. - m_bright: Bright absolute-magnitude bound of the LF model. - m_faint: Faint absolute-magnitude bound of the LF model. + m_bright: Bright absolute magnitude bound of the LF model. + m_faint: Faint absolute magnitude bound of the LF model. n_m: Number of magnitude-grid points used for the integral. h: Optional dimensionless Hubble parameter used in the distance-modulus convention. @@ -187,8 +187,8 @@ def missing_number_density( z: Redshift value or array-like of redshift values. lf: Luminosity-function callable with signature ``lf(M, z)``. m_lim: Apparent magnitude limit of the catalog. - m_bright: Bright absolute-magnitude bound of the LF model. - m_faint: Faint absolute-magnitude bound of the LF model. + m_bright: Bright absolute magnitude bound of the LF model. + m_faint: Faint absolute magnitude bound of the LF model. n_m: Number of magnitude-grid points used for the integral. h: Optional dimensionless Hubble parameter used in the distance-modulus convention. @@ -249,8 +249,8 @@ def catalog_completeness_fraction( z: Redshift value or array-like of redshift values. lf: Luminosity-function callable with signature ``lf(M, z)``. m_lim: Apparent magnitude limit of the catalog. - m_bright: Bright absolute-magnitude bound of the LF model. - m_faint: Faint absolute-magnitude bound of the LF model. + m_bright: Bright absolute magnitude bound of the LF model. + m_faint: Faint absolute magnitude bound of the LF model. n_m: Number of magnitude-grid points used for the integral. h: Optional dimensionless Hubble parameter used in the distance-modulus convention. @@ -315,8 +315,8 @@ def out_of_catalog_fraction( z: Redshift value or array-like of redshift values. lf: Luminosity-function callable with signature ``lf(M, z)``. m_lim: Apparent magnitude limit of the catalog. - m_bright: Bright absolute-magnitude bound of the LF model. - m_faint: Faint absolute-magnitude bound of the LF model. + m_bright: Bright absolute magnitude bound of the LF model. + m_faint: Faint absolute magnitude bound of the LF model. n_m: Number of magnitude-grid points used for the integral. h: Optional dimensionless Hubble parameter used in the distance-modulus convention. diff --git a/src/lfkit/photometry/lf_integrals.py b/src/lfkit/photometry/lf_integrals.py index aaefe3f..c230986 100644 --- a/src/lfkit/photometry/lf_integrals.py +++ b/src/lfkit/photometry/lf_integrals.py @@ -1,7 +1,7 @@ r"""Luminosity-function integration utilities. This module provides generic numerical integrals of luminosity function -callables over finite absolute-magnitude ranges. +callables over finite absolute magnitude ranges. The core API accepts a luminosity function callable with signature @@ -66,8 +66,8 @@ def integrated_number_density( Args: z: Redshift value or array-like of redshift values. lf: Luminosity-function callable with signature ``lf(M, z)``. - m_bright: Bright absolute-magnitude bound. May be scalar or array-like. - m_faint: Faint absolute-magnitude bound. May be scalar or array-like. + m_bright: Bright absolute magnitude bound. May be scalar or array-like. + m_faint: Faint absolute magnitude bound. May be scalar or array-like. n_m: Number of magnitude-grid points used for the integral. Returns: @@ -103,8 +103,8 @@ def lf_weighted_integral( Args: z: Redshift value or array-like of redshift values. lf: Luminosity-function callable with signature ``lf(M, z)``. - m_bright: Bright absolute-magnitude bound. May be scalar or array-like. - m_faint: Faint absolute-magnitude bound. May be scalar or array-like. + m_bright: Bright absolute magnitude bound. May be scalar or array-like. + m_faint: Faint absolute magnitude bound. May be scalar or array-like. weight_fn: Weight callable with signature ``weight_fn(M, z)``. Its return values must be broadcastable to the magnitude-redshift grid. n_m: Number of magnitude-grid points used for the integral. @@ -144,8 +144,8 @@ def selection_weighted_number_density( Args: z: Redshift value or array-like of redshift values. lf: Luminosity-function callable with signature ``lf(M, z)``. - m_bright: Bright absolute-magnitude bound. May be scalar or array-like. - m_faint: Faint absolute-magnitude bound. May be scalar or array-like. + m_bright: Bright absolute magnitude bound. May be scalar or array-like. + m_faint: Faint absolute magnitude bound. May be scalar or array-like. selection_fn: Selection callable with signature ``selection_fn(M, z)``. Values should usually lie between 0 and 1, although this function only requires finite non-negative values. @@ -193,8 +193,8 @@ def integrated_luminosity_density( Args: z: Redshift value or array-like of redshift values. lf: Luminosity-function callable with signature ``lf(M, z)``. - m_bright: Bright absolute-magnitude bound. May be scalar or array-like. - m_faint: Faint absolute-magnitude bound. May be scalar or array-like. + m_bright: Bright absolute magnitude bound. May be scalar or array-like. + m_faint: Faint absolute magnitude bound. May be scalar or array-like. m_reference: Reference absolute magnitude defining the luminosity unit. n_m: Number of magnitude-grid points used for the integral. @@ -247,8 +247,8 @@ def mean_luminosity( Args: z: Redshift value or array-like of redshift values. lf: Luminosity-function callable with signature ``lf(M, z)``. - m_bright: Bright absolute-magnitude bound. May be scalar or array-like. - m_faint: Faint absolute-magnitude bound. May be scalar or array-like. + m_bright: Bright absolute magnitude bound. May be scalar or array-like. + m_faint: Faint absolute magnitude bound. May be scalar or array-like. m_reference: Reference absolute magnitude defining the luminosity unit. n_m: Number of magnitude-grid points used for the integral. @@ -306,9 +306,9 @@ def cumulative_number_density( Args: z: Redshift value or array-like of redshift values. lf: Luminosity-function callable with signature ``lf(M, z)``. - m_threshold: Absolute-magnitude threshold. May be scalar or array-like. - m_bright: Bright absolute-magnitude bound. May be scalar or array-like. - m_faint: Faint absolute-magnitude bound. May be scalar or array-like. + m_threshold: Absolute magnitude threshold. May be scalar or array-like. + m_bright: Bright absolute magnitude bound. May be scalar or array-like. + m_faint: Faint absolute magnitude bound. May be scalar or array-like. brighter_than: If True, integrate galaxies brighter than the threshold. If False, integrate galaxies fainter than the threshold. n_m: Number of magnitude-grid points used for the integral. @@ -359,13 +359,13 @@ def magnitude_window_number_density( ) -> FloatArray: r"""Return LF number density inside a magnitude-selection window. - This integrates a luminosity function over a finite absolute-magnitude + This integrates a luminosity function over a finite absolute magnitude range. The bright and faint limits may be supplied directly as absolute magnitudes, converted from apparent magnitudes, or supplied as a mixture of both. Magnitudes are ordered so that more negative values are brighter. Apparent - magnitude limits are converted to absolute-magnitude limits at each + magnitude limits are converted to absolute magnitude limits at each redshift before integration. This helper is science-use-case agnostic. It only defines the LF integral @@ -375,12 +375,12 @@ def magnitude_window_number_density( Args: z: Redshift value or array-like of redshift values. lf: Luminosity-function callable with signature ``lf(M, z)``. - m_bright: Bright absolute-magnitude bound. - m_faint: Faint absolute-magnitude bound. - apparent_m_bright: Bright apparent-magnitude bound. - apparent_m_faint: Faint apparent-magnitude bound. + m_bright: Bright absolute magnitude bound. + m_faint: Faint absolute magnitude bound. + apparent_m_bright: Bright apparent magnitude bound. + apparent_m_faint: Faint apparent magnitude bound. luminosity_distance_mpc_fn: Callable returning luminosity distance in - Mpc. Required when either apparent-magnitude bound is supplied. + Mpc. Required when either apparent magnitude bound is supplied. k_correction_fn: Optional K-correction callable evaluated at ``z``. e_correction_fn: Optional E-correction callable evaluated at ``z``. n_m: Number of magnitude-grid points used for the integral. @@ -433,7 +433,7 @@ def _resolve_magnitude_window_bound( e_correction_fn: Callable[[FloatArray], FloatArray] | None, bound_name: str, ) -> FloatArray: - r"""Return an absolute-magnitude bound for a magnitude window.""" + r"""Return an absolute magnitude bound for a magnitude window.""" if absolute_mag is None and apparent_mag is None: raise ValueError( f"Must provide either m_{bound_name} or apparent_m_{bound_name}." diff --git a/src/lfkit/photometry/lf_redshift_density.py b/src/lfkit/photometry/lf_redshift_density.py index 759bf92..1695a10 100644 --- a/src/lfkit/photometry/lf_redshift_density.py +++ b/src/lfkit/photometry/lf_redshift_density.py @@ -7,7 +7,7 @@ n_lf(z) = int phi(M, z) dM -over the observable absolute-magnitude range implied by an apparent-magnitude +over the observable absolute magnitude range implied by an apparent magnitude limit. A second helper multiplies this LF-integrated density by a user-supplied redshift or volume weight. @@ -62,7 +62,7 @@ def lf_integrated_number_density( \int_{M_{\mathrm{bright}}}^{M_{\mathrm{lim}}(z)} \phi(M, z)\,dM, - where ``M_lim(z)`` is the absolute-magnitude limit implied by the apparent + where ``M_lim(z)`` is the absolute magnitude limit implied by the apparent magnitude cut ``m_lim``. The magnitude conversion follows @@ -79,7 +79,7 @@ def lf_integrated_number_density( z: Redshift value or array-like of redshift values. lf: Luminosity-function callable with signature ``lf(M, z)``. m_lim: Apparent magnitude limit of the catalog. - m_bright: Bright absolute-magnitude integration bound. + m_bright: Bright absolute magnitude integration bound. n_m: Number of magnitude-grid points used for the integral. luminosity_distance_mpc_fn: Callable returning luminosity distance in Mpc as a function of redshift. @@ -164,7 +164,7 @@ def lf_weighted_redshift_density( z: Redshift value or array-like of redshift values. lf: Luminosity-function callable with signature ``lf(M, z)``. m_lim: Apparent magnitude limit of the catalog. - m_bright: Bright absolute-magnitude integration bound. + m_bright: Bright absolute magnitude integration bound. n_m: Number of magnitude-grid points used for the integral. luminosity_distance_mpc_fn: Callable returning luminosity distance in Mpc as a function of redshift. diff --git a/src/lfkit/photometry/luminosity_function.py b/src/lfkit/photometry/luminosity_function.py index f13dc98..ed193e1 100644 --- a/src/lfkit/photometry/luminosity_function.py +++ b/src/lfkit/photometry/luminosity_function.py @@ -6,7 +6,7 @@ All luminosity functions in this module are defined in rest-frame absolute magnitude space. Functions with names ending in ``_from_m`` are convenience wrappers that accept apparent magnitudes, convert them to absolute -magnitudes, and then evaluate the luminosity function in absolute-magnitude +magnitudes, and then evaluate the luminosity function in absolute magnitude space. Implemented models: diff --git a/src/lfkit/utils/integrators.py b/src/lfkit/utils/integrators.py index 5af7362..cd8f976 100644 --- a/src/lfkit/utils/integrators.py +++ b/src/lfkit/utils/integrators.py @@ -2,7 +2,7 @@ This module provides small reusable helpers for integrating tabulated values over fixed or variable finite bounds. These helpers do not encode any -luminosity-function, photometry, or cosmology assumptions. +luminosity function, photometry, or cosmology assumptions. """ from __future__ import annotations diff --git a/tests/test_api_conditional_luminosity_function.py b/tests/test_api_conditional_luminosity_function.py index ef990a7..46b853a 100644 --- a/tests/test_api_conditional_luminosity_function.py +++ b/tests/test_api_conditional_luminosity_function.py @@ -1,4 +1,4 @@ -"""Smoke tests for conditional luminosity-function API constructors. +"""Smoke tests for conditional luminosity function API constructors. These tests check that the public conditional LF factory methods create LuminosityFunction objects with the expected model names and parameter payloads. diff --git a/tests/test_api_luminosity_function.py b/tests/test_api_luminosity_function.py index f314b27..d5d0235 100644 --- a/tests/test_api_luminosity_function.py +++ b/tests/test_api_luminosity_function.py @@ -1,7 +1,7 @@ """Smoke tests for user-facing API delegation. These tests check that the public API namespaces are wired to the expected -low-level functions. They intentionally avoid testing luminosity-function +low-level functions. They intentionally avoid testing luminosity function physics, which is covered by the lower-level photometry tests. """ diff --git a/tests/test_photometry_completeness_fake_catalog.py b/tests/test_photometry_completeness_fake_catalog.py index fa068b7..271bfe8 100644 --- a/tests/test_photometry_completeness_fake_catalog.py +++ b/tests/test_photometry_completeness_fake_catalog.py @@ -124,7 +124,7 @@ def test_observed_and_missing_fractions_sum_to_one() -> None: def test_deeper_catalog_limit_increases_completeness() -> None: - """Tests that a fainter apparent-magnitude limit increases completeness.""" + """Tests that a fainter apparent magnitude limit increases completeness.""" catalog = load_fake_catalog() cosmo = make_cosmology() @@ -154,7 +154,7 @@ def test_deeper_catalog_limit_increases_completeness() -> None: def test_deeper_catalog_limit_decreases_missing_density() -> None: - """Tests that a fainter apparent-magnitude limit misses no more galaxies.""" + """Tests that a fainter apparent magnitude limit misses no more galaxies.""" catalog = load_fake_catalog() cosmo = make_cosmology() @@ -183,7 +183,7 @@ def test_deeper_catalog_limit_decreases_missing_density() -> None: def test_deeper_catalog_limit_increases_observed_density() -> None: - """Tests that a fainter apparent-magnitude limit observes no fewer galaxies.""" + """Tests that a fainter apparent magnitude limit observes no fewer galaxies.""" catalog = load_fake_catalog() cosmo = make_cosmology() diff --git a/tests/test_photometry_lf_integrals.py b/tests/test_photometry_lf_integrals.py index 48cf312..e32a09d 100644 --- a/tests/test_photometry_lf_integrals.py +++ b/tests/test_photometry_lf_integrals.py @@ -491,7 +491,7 @@ def test_cumulative_number_density_rejects_negative_redshift() -> None: def test_magnitude_window_number_density_uses_absolute_bounds() -> None: - """Tests magnitude-window density with direct absolute-magnitude bounds.""" + """Tests magnitude-window density with direct absolute magnitude bounds.""" result = li.magnitude_window_number_density( [0.1, 0.2], constant_lf, @@ -564,7 +564,7 @@ def test_magnitude_window_number_density_rejects_duplicate_bright_bounds() -> No def test_magnitude_window_density_requires_distance_for_apparent_bounds() -> None: - """Tests that apparent-magnitude bounds require a distance callable.""" + """Tests that apparent magnitude bounds require a distance callable.""" with pytest.raises( ValueError, match="luminosity_distance_mpc_fn is required", @@ -578,7 +578,7 @@ def test_magnitude_window_density_requires_distance_for_apparent_bounds() -> Non def test_magnitude_window_number_density_applies_k_and_e_corrections() -> None: - """Tests apparent-magnitude conversion with K- and E-corrections.""" + """Tests apparent magnitude conversion with K- and E-corrections.""" def luminosity_distance_mpc_fn(z: np.ndarray) -> np.ndarray: """Return a constant luminosity distance.""" diff --git a/tests/test_photometry_lf_redshift_density.py b/tests/test_photometry_lf_redshift_density.py index 35a55e3..1ec1330 100644 --- a/tests/test_photometry_lf_redshift_density.py +++ b/tests/test_photometry_lf_redshift_density.py @@ -52,7 +52,7 @@ def expected_absolute_magnitude_limit( k_correction: float | np.ndarray = 0.0, evolution_correction: float | np.ndarray = 0.0, ) -> np.ndarray: - """Return the expected absolute-magnitude limit for test inputs.""" + """Return the expected absolute magnitude limit for test inputs.""" return ( m_lim - 5.0 * np.log10(luminosity_distance_mpc) @@ -63,7 +63,7 @@ def expected_absolute_magnitude_limit( def test_lf_integrated_number_density_integrates_to_absolute_magnitude_limit() -> None: - """Tests LF integration to the apparent-magnitude-implied absolute limit.""" + """Tests LF integration to the apparent magnitude-implied absolute limit.""" result = lfrd.lf_integrated_number_density( [0.1, 0.2], constant_lf, @@ -124,7 +124,7 @@ def test_lf_integrated_number_density_uses_luminosity_distance_function() -> Non def test_lf_integrated_number_density_applies_k_correction() -> None: - """Tests that k-corrections shift the absolute-magnitude limit.""" + """Tests that k-corrections shift the absolute magnitude limit.""" result = lfrd.lf_integrated_number_density( [0.1, 0.2], constant_lf, @@ -139,7 +139,7 @@ def test_lf_integrated_number_density_applies_k_correction() -> None: def test_lf_integrated_number_density_applies_evolution_correction() -> None: - """Tests that evolution corrections shift the absolute-magnitude limit.""" + """Tests that evolution corrections shift the absolute magnitude limit.""" result = lfrd.lf_integrated_number_density( [0.1, 0.2], constant_lf,