From d271ae3068e1b816e9392bf838adbc1470cfe6ea Mon Sep 17 00:00:00 2001 From: "niko.sarcevic" Date: Fri, 15 May 2026 13:09:53 -0400 Subject: [PATCH 1/6] updated docs to match new api --- ...it.api.conditional_luminosity_function.rst | 7 + docs/api/lfkit.api.luminosity_function.rst | 7 + docs/api/lfkit.api.rst | 2 + docs/api/lfkit.utils.integrators.rst | 7 + docs/api/lfkit.utils.rst | 1 + docs/examples/api_overview.rst | 553 +++++++ ..._examples.rst => catalog_completeness.rst} | 44 +- ...st => conditional_luminosity_function.rst} | 522 +++---- docs/examples/index.rst | 81 +- .../examples/luminosity_function_examples.rst | 959 ------------ docs/examples/luminosity_function_models.rst | 626 ++++++++ docs/examples/magnitude_integrals.rst | 385 +++++ docs/examples/magnitudes_and_luminosities.rst | 211 +++ docs/examples/model_registry.rst | 63 + docs/examples/redshift_density.rst | 246 +++ src/lfkit/api/_clf_models.py | 0 src/lfkit/api/_completeness.py | 0 src/lfkit/api/_expose.py | 0 src/lfkit/api/_integrals.py | 0 src/lfkit/api/_lf_param_models.py | 77 + src/lfkit/api/_luminosities.py | 0 src/lfkit/api/_magnitudes.py | 0 src/lfkit/api/_redshift_density.py | 0 .../api/conditional_luminosity_function.py | 0 src/lfkit/api/lumfunc.py | 1109 -------------- src/lfkit/api/luminosity_function.py | 0 ...est_api_conditional_luminosity_function.py | 0 tests/test_api_lumfunc.py | 1329 ----------------- tests/test_api_luminosity_function.py | 0 29 files changed, 2438 insertions(+), 3791 deletions(-) create mode 100644 docs/api/lfkit.api.conditional_luminosity_function.rst create mode 100644 docs/api/lfkit.api.luminosity_function.rst create mode 100644 docs/api/lfkit.utils.integrators.rst create mode 100644 docs/examples/api_overview.rst rename docs/examples/{catalog_completeness_examples.rst => catalog_completeness.rst} (95%) rename docs/examples/{conditional_luminosity_function_examples.rst => conditional_luminosity_function.rst} (67%) delete mode 100644 docs/examples/luminosity_function_examples.rst create mode 100644 docs/examples/luminosity_function_models.rst create mode 100644 docs/examples/magnitude_integrals.rst create mode 100644 docs/examples/magnitudes_and_luminosities.rst create mode 100644 docs/examples/model_registry.rst create mode 100644 docs/examples/redshift_density.rst create mode 100644 src/lfkit/api/_clf_models.py create mode 100644 src/lfkit/api/_completeness.py create mode 100644 src/lfkit/api/_expose.py create mode 100644 src/lfkit/api/_integrals.py create mode 100644 src/lfkit/api/_lf_param_models.py create mode 100644 src/lfkit/api/_luminosities.py create mode 100644 src/lfkit/api/_magnitudes.py create mode 100644 src/lfkit/api/_redshift_density.py create mode 100644 src/lfkit/api/conditional_luminosity_function.py delete mode 100644 src/lfkit/api/lumfunc.py create mode 100644 src/lfkit/api/luminosity_function.py create mode 100644 tests/test_api_conditional_luminosity_function.py delete mode 100644 tests/test_api_lumfunc.py create mode 100644 tests/test_api_luminosity_function.py diff --git a/docs/api/lfkit.api.conditional_luminosity_function.rst b/docs/api/lfkit.api.conditional_luminosity_function.rst new file mode 100644 index 0000000..9512c14 --- /dev/null +++ b/docs/api/lfkit.api.conditional_luminosity_function.rst @@ -0,0 +1,7 @@ +lfkit.api.conditional\_luminosity\_function module +================================================== + +.. automodule:: lfkit.api.conditional_luminosity_function + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/lfkit.api.luminosity_function.rst b/docs/api/lfkit.api.luminosity_function.rst new file mode 100644 index 0000000..12b0e08 --- /dev/null +++ b/docs/api/lfkit.api.luminosity_function.rst @@ -0,0 +1,7 @@ +lfkit.api.luminosity\_function module +===================================== + +.. automodule:: lfkit.api.luminosity_function + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/lfkit.api.rst b/docs/api/lfkit.api.rst index 46d56b0..2d8f565 100644 --- a/docs/api/lfkit.api.rst +++ b/docs/api/lfkit.api.rst @@ -7,8 +7,10 @@ Submodules .. toctree:: :maxdepth: 2 + lfkit.api.conditional_luminosity_function lfkit.api.corrections lfkit.api.lumfunc + lfkit.api.luminosity_function Module contents --------------- diff --git a/docs/api/lfkit.utils.integrators.rst b/docs/api/lfkit.utils.integrators.rst new file mode 100644 index 0000000..11454f1 --- /dev/null +++ b/docs/api/lfkit.utils.integrators.rst @@ -0,0 +1,7 @@ +lfkit.utils.integrators module +============================== + +.. automodule:: lfkit.utils.integrators + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/lfkit.utils.rst b/docs/api/lfkit.utils.rst index 6e06a20..854d50c 100644 --- a/docs/api/lfkit.utils.rst +++ b/docs/api/lfkit.utils.rst @@ -9,6 +9,7 @@ Submodules lfkit.utils.download_poggianti97_data lfkit.utils.evaluators + lfkit.utils.integrators lfkit.utils.interpolation lfkit.utils.io lfkit.utils.types diff --git a/docs/examples/api_overview.rst b/docs/examples/api_overview.rst new file mode 100644 index 0000000..a87b752 --- /dev/null +++ b/docs/examples/api_overview.rst @@ -0,0 +1,553 @@ +.. |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_examples.rst b/docs/examples/catalog_completeness.rst similarity index 95% rename from docs/examples/catalog_completeness_examples.rst rename to docs/examples/catalog_completeness.rst index 307a96e..4adab81 100644 --- a/docs/examples/catalog_completeness_examples.rst +++ b/docs/examples/catalog_completeness.rst @@ -15,11 +15,10 @@ redshift only brighter galaxies remain above the catalog limit. All examples below are executable via ``.. plot::``. -The examples intentionally use ``corrections=None``. This means that no -:math:`K`-correction or evolution correction is applied. Users can pass their -own correction model through the ``corrections`` argument, for example a -:class:`lfkit.Corrections` object or any compatible correction callable used by -the LFKit magnitude-conversion methods. +The examples do not apply :math:`K`-corrections or evolution corrections. Users +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. For example, if :math:`\phi_*` is given in comoving :math:`{\rm Mpc}^{-3}`, then @@ -81,11 +80,10 @@ higher on the plot. alpha_kwargs={"alpha": -1.1}, ) - m_limit = lf.absolute_magnitude_limit( + m_limit = lf.completeness.absolute_magnitude_limit( cosmo, z, m_lim=24.5, - corrections=None, ) fig, ax = plt.subplots(figsize=(7.0, 5.0)) @@ -158,28 +156,26 @@ population described by the luminosity function, or only the bright tail of it. m_bright = -24.0 m_faint = -14.0 - n_total = lf.integrated_number_density( + n_total = lf.integrals.number_density( z, m_bright=m_bright, m_faint=m_faint, ) - n_obs = lf.observed_number_density( + n_obs = lf.completeness.observed_number_density( cosmo, z, m_lim=24.5, m_bright=m_bright, m_faint=m_faint, - corrections=None, ) - n_miss = lf.missing_number_density( + n_miss = lf.completeness.missing_number_density( cosmo, z, m_lim=24.5, m_bright=m_bright, m_faint=m_faint, - corrections=None, ) fig, ax = plt.subplots(figsize=(7.0, 5.0)) @@ -253,22 +249,20 @@ catalog contains only a small fraction of the intrinsic galaxy population. m_bright = -24.0 m_faint = -14.0 - f_obs = lf.catalog_completeness( + f_obs = lf.completeness.catalog_fraction( cosmo, z, m_lim=24.5, m_bright=m_bright, m_faint=m_faint, - corrections=None, ) - f_miss = lf.out_of_catalog_fraction( + f_miss = lf.completeness.out_of_catalog_fraction( cosmo, z, m_lim=24.5, m_bright=m_bright, m_faint=m_faint, - corrections=None, ) fig, ax = plt.subplots(figsize=(7.0, 5.0)) @@ -342,13 +336,12 @@ incomplete. fig, ax = plt.subplots(figsize=(7.0, 5.0)) for m_lim, color in zip(limits, colors_blue): - f_obs = lf.catalog_completeness( + f_obs = lf.completeness.catalog_fraction( cosmo, z, m_lim=m_lim, m_bright=-24.0, m_faint=-14.0, - corrections=None, ) ax.plot(z, f_obs, lw=3, color=color, label=rf"$m_{{\rm lim}}={m_lim}$") @@ -421,11 +414,10 @@ boundary. fig, ax = plt.subplots(figsize=(7.0, 5.0)) for m_lim, color in zip(limits, colors_blue): - m_limit = lf.absolute_magnitude_limit( + m_limit = lf.completeness.absolute_magnitude_limit( cosmo, z, m_lim=m_lim, - corrections=None, ) ax.plot(z, m_limit, lw=3, color=color, label=rf"$m_{{\rm lim}}={m_lim}$") @@ -497,13 +489,12 @@ about a much fainter underlying population. fig, ax = plt.subplots(figsize=(7.0, 5.0)) for m_faint, color in zip(faint_limits, colors_red): - f_obs = lf.catalog_completeness( + f_obs = lf.completeness.catalog_fraction( cosmo, z, m_lim=24.5, m_bright=-24.0, m_faint=m_faint, - corrections=None, ) ax.plot( z, @@ -589,13 +580,12 @@ redshift range is safe for a magnitude-limited sample. completeness = [] for m_lim in limits: - f_obs = lf.catalog_completeness( + f_obs = lf.completeness.catalog_fraction( cosmo, z, m_lim=m_lim, m_bright=-24.0, m_faint=-14.0, - corrections=None, ) completeness.append(f_obs) @@ -714,11 +704,10 @@ The lower panel shows the residual relative to the reference cosmology, m_limits = {} for label, cosmo in cosmologies.items(): - m_limits[label] = lf.absolute_magnitude_limit( + m_limits[label] = lf.completeness.absolute_magnitude_limit( cosmo, z, m_lim=24.5, - corrections=None, ) reference_label = r"$\Omega_{\rm m}=0.30,\ h=0.70$" @@ -828,13 +817,12 @@ The lower panel shows the residual relative to the reference cosmology, completeness = {} for label, cosmo in cosmologies.items(): - completeness[label] = lf.catalog_completeness( + completeness[label] = lf.completeness.catalog_fraction( cosmo, z, m_lim=24.5, m_bright=-24.0, m_faint=-14.0, - corrections=None, ) reference_label = r"$\Omega_{\rm m}=0.30,\ h=0.70$" diff --git a/docs/examples/conditional_luminosity_function_examples.rst b/docs/examples/conditional_luminosity_function.rst similarity index 67% rename from docs/examples/conditional_luminosity_function_examples.rst rename to docs/examples/conditional_luminosity_function.rst index f01be1c..32d49e4 100644 --- a/docs/examples/conditional_luminosity_function_examples.rst +++ b/docs/examples/conditional_luminosity_function.rst @@ -5,17 +5,23 @@ |lfkitlogo| Conditional luminosity functions ============================================ -This page shows how to evaluate conditional luminosity functions with LFKit. +This page shows how to evaluate conditional luminosity functions with LFKit's +public API. 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. +LFKit exposes conditional luminosity functions through +:class:`lfkit.ConditionalLuminosityFunction`. Each constructor returns a +:class:`lfkit.LuminosityFunction` object, so the resulting model can be +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 functions can be -used with any other conditioning variable by replacing ``z`` with the desired -quantity. +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 include: @@ -24,7 +30,9 @@ The examples include: * a lognormal component, * a modified Schechter-like component, * a two-component lognormal plus modified-Schechter model, -* integrated number densities and component fractions. +* integrated number densities and component fractions, +* a halo-mass conditional example, +* selection-limited number densities. The number-density units follow the normalization supplied to the luminosity function. For example, if the amplitudes are supplied in @@ -51,9 +59,7 @@ conditioning variable. import matplotlib.pyplot as plt import cmasher as cmr - from lfkit.photometry.conditional_lf_models import ( - conditional_schechter, - ) + from lfkit import ConditionalLuminosityFunction LABEL_SIZE = 15 TICK_SIZE = 13 @@ -65,18 +71,17 @@ conditioning variable. absolute_mag = np.linspace(-24.0, -14.0, 500) redshifts = [0.1, 0.6, 1.1] + 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), + alpha=-1.1, + ) + fig, ax = plt.subplots(figsize=(7.0, 5.0)) for z_value, color in zip(redshifts, colors): z = np.full_like(absolute_mag, z_value) - - phi = conditional_schechter( - absolute_mag, - z, - phi_star=lambda z: 1.0e-3 * (1.0 + z) ** 0.8, - m_star=lambda z: -20.5 - 0.7 * (z - 0.1), - alpha=-1.1, - ) + phi = lf.phi(absolute_mag, z) ax.plot( absolute_mag, @@ -96,6 +101,7 @@ conditioning variable. ax.set_title("Conditional Schechter luminosity function", fontsize=TITLE_SIZE) ax.tick_params(axis="both", labelsize=TICK_SIZE) ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() @@ -116,9 +122,7 @@ the conditional model behaves smoothly across the region where it will be used. import matplotlib.pyplot as plt import cmasher as cmr - from lfkit.photometry.conditional_lf_models import ( - conditional_schechter, - ) + from lfkit import ConditionalLuminosityFunction LABEL_SIZE = 15 TICK_SIZE = 13 @@ -129,14 +133,13 @@ the conditional model behaves smoothly across the region where it will be used. mag_grid, z_grid = np.meshgrid(absolute_mag, redshift) - phi = conditional_schechter( - mag_grid, - z_grid, + 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), alpha=-1.1, ) + phi = lf.phi(mag_grid, z_grid) log_phi = np.log10(phi) fig, ax = plt.subplots(figsize=(7.2, 5.0)) @@ -195,9 +198,7 @@ conditioning variable, while the faint-end slope is constant. import matplotlib.pyplot as plt import cmasher as cmr - from lfkit.photometry.conditional_lf_models import ( - conditional_schechter_evolving, - ) + from lfkit import ConditionalLuminosityFunction LABEL_SIZE = 15 TICK_SIZE = 13 @@ -209,21 +210,20 @@ conditioning variable, while the faint-end slope is constant. absolute_mag = np.linspace(-24.0, -14.0, 500) redshifts = [0.1, 0.6, 1.1] + lf = ConditionalLuminosityFunction.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}, + ) + fig, ax = plt.subplots(figsize=(7.0, 5.0)) for z_value, color in zip(redshifts, colors): z = np.full_like(absolute_mag, z_value) - - phi = conditional_schechter_evolving( - absolute_mag, - z, - 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}, - ) + phi = lf.phi(absolute_mag, z) ax.plot( absolute_mag, @@ -243,6 +243,7 @@ conditioning variable, while the faint-end slope is constant. ax.set_title("Conditional evolving Schechter model", fontsize=TITLE_SIZE) ax.tick_params(axis="both", labelsize=TICK_SIZE) ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() @@ -264,9 +265,7 @@ width. import matplotlib.pyplot as plt import cmasher as cmr - from lfkit.photometry.conditional_lf_models import ( - lognormal_conditional_lf, - ) + from lfkit import ConditionalLuminosityFunction LABEL_SIZE = 15 TICK_SIZE = 13 @@ -278,18 +277,17 @@ width. absolute_mag = np.linspace(-24.0, -16.0, 500) redshifts = [0.1, 0.6, 1.1] + lf = ConditionalLuminosityFunction.lognormal( + mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), + sigma_log_luminosity=0.18, + amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4, + ) + fig, ax = plt.subplots(figsize=(7.0, 5.0)) for z_value, color in zip(redshifts, colors): z = np.full_like(absolute_mag, z_value) - - phi = lognormal_conditional_lf( - absolute_mag, - z, - mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), - sigma_log_luminosity=0.18, - amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4, - ) + phi = lf.phi(absolute_mag, z) ax.plot( absolute_mag, @@ -310,6 +308,7 @@ width. ax.set_title("Lognormal conditional LF component", fontsize=TITLE_SIZE) ax.tick_params(axis="both", labelsize=TICK_SIZE) ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() @@ -328,9 +327,7 @@ range of faint magnitudes. import matplotlib.pyplot as plt import cmasher as cmr - from lfkit.photometry.conditional_lf_models import ( - modified_schechter_conditional_lf, - ) + from lfkit import ConditionalLuminosityFunction LABEL_SIZE = 15 TICK_SIZE = 13 @@ -342,18 +339,17 @@ range of faint magnitudes. absolute_mag = np.linspace(-24.0, -14.0, 500) redshifts = [0.1, 0.6, 1.1] + 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), + alpha=lambda z: -1.05 - 0.10 * z, + ) + fig, ax = plt.subplots(figsize=(7.0, 5.0)) for z_value, color in zip(redshifts, colors): z = np.full_like(absolute_mag, z_value) - - phi = modified_schechter_conditional_lf( - absolute_mag, - z, - phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5, - m_star=lambda z: -19.9 - 0.5 * (z - 0.1), - alpha=lambda z: -1.05 - 0.10 * z, - ) + phi = lf.phi(absolute_mag, z) ax.plot( absolute_mag, @@ -374,6 +370,7 @@ range of faint magnitudes. ax.set_title("Modified Schechter conditional LF component", fontsize=TITLE_SIZE) ax.tick_params(axis="both", labelsize=TICK_SIZE) ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() @@ -382,9 +379,9 @@ 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, making the -bright-end suppression sharper. The lognormal component is localized around a -mean luminosity and is useful for narrow populations. +modified Schechter component uses a squared exponential cutoff. The lognormal +component is localized around a mean luminosity and is useful for narrow +populations. .. plot:: :include-source: True @@ -394,11 +391,7 @@ mean luminosity and is useful for narrow populations. import matplotlib.pyplot as plt import cmasher as cmr - from lfkit.photometry.conditional_lf_models import ( - conditional_schechter, - lognormal_conditional_lf, - modified_schechter_conditional_lf, - ) + from lfkit import ConditionalLuminosityFunction LABEL_SIZE = 15 TICK_SIZE = 13 @@ -411,30 +404,28 @@ mean luminosity and is useful for narrow populations. z_value = 0.6 z = np.full_like(absolute_mag, z_value) - phi_schechter = conditional_schechter( - absolute_mag, - z, + schechter_lf = ConditionalLuminosityFunction.schechter( phi_star=1.0e-3, m_star=-20.5, alpha=-1.1, ) - phi_modified = modified_schechter_conditional_lf( - absolute_mag, - z, + modified_lf = ConditionalLuminosityFunction.modified_schechter( phi_star=1.0e-3, m_star=-20.5, alpha=-1.1, ) - phi_lognormal = lognormal_conditional_lf( - absolute_mag, - z, + lognormal_lf = ConditionalLuminosityFunction.lognormal( mean_absolute_mag=-20.5, sigma_log_luminosity=0.20, amplitude=1.0e-3, ) + phi_schechter = schechter_lf.phi(absolute_mag, z) + phi_modified = modified_lf.phi(absolute_mag, z) + phi_lognormal = lognormal_lf.phi(absolute_mag, z) + fig, ax = plt.subplots(figsize=(7.0, 5.0)) ax.plot( @@ -492,11 +483,7 @@ dominates different magnitude ranges. import matplotlib.pyplot as plt import cmasher as cmr - from lfkit.photometry.conditional_lf_models import ( - lognormal_conditional_lf, - modified_schechter_conditional_lf, - two_component_conditional_lf, - ) + from lfkit import ConditionalLuminosityFunction LABEL_SIZE = 15 TICK_SIZE = 13 @@ -511,25 +498,19 @@ dominates different magnitude ranges. z_value = 0.6 z = np.full_like(absolute_mag, z_value) - lognormal_phi = lognormal_conditional_lf( - absolute_mag, - z, + lognormal_lf = ConditionalLuminosityFunction.lognormal( mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), sigma_log_luminosity=0.18, amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4, ) - modified_phi = modified_schechter_conditional_lf( - absolute_mag, - z, + modified_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), alpha=lambda z: -1.05 - 0.10 * z, ) - total_phi = two_component_conditional_lf( - absolute_mag, - z, + total_lf = ConditionalLuminosityFunction.two_component( lognormal_mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), lognormal_sigma_log_luminosity=0.18, lognormal_amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4, @@ -538,6 +519,10 @@ dominates different magnitude ranges. modified_alpha=lambda z: -1.05 - 0.10 * z, ) + lognormal_phi = lognormal_lf.phi(absolute_mag, z) + modified_phi = modified_lf.phi(absolute_mag, z) + total_phi = total_lf.phi(absolute_mag, z) + fig, ax = plt.subplots(figsize=(7.0, 5.0)) ax.plot( @@ -572,6 +557,7 @@ dominates different magnitude ranges. ax.set_title(r"Two-component conditional LF at $z=0.6$", fontsize=TITLE_SIZE) ax.tick_params(axis="both", labelsize=TICK_SIZE) ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() @@ -590,9 +576,7 @@ components depend on the conditioning variable. import matplotlib.pyplot as plt import cmasher as cmr - from lfkit.photometry.conditional_lf_models import ( - two_component_conditional_lf, - ) + from lfkit import ConditionalLuminosityFunction LABEL_SIZE = 15 TICK_SIZE = 13 @@ -604,21 +588,20 @@ components depend on the conditioning variable. absolute_mag = np.linspace(-24.0, -14.0, 500) redshifts = [0.1, 0.6, 1.1] + lf = ConditionalLuminosityFunction.two_component( + lognormal_mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), + lognormal_sigma_log_luminosity=0.18, + lognormal_amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4, + modified_phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5, + modified_m_star=lambda z: -19.9 - 0.5 * (z - 0.1), + modified_alpha=lambda z: -1.05 - 0.10 * z, + ) + fig, ax = plt.subplots(figsize=(7.0, 5.0)) for z_value, color in zip(redshifts, colors): z = np.full_like(absolute_mag, z_value) - - phi = two_component_conditional_lf( - absolute_mag, - z, - lognormal_mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), - lognormal_sigma_log_luminosity=0.18, - lognormal_amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4, - modified_phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5, - modified_m_star=lambda z: -19.9 - 0.5 * (z - 0.1), - modified_alpha=lambda z: -1.05 - 0.10 * z, - ) + phi = lf.phi(absolute_mag, z) ax.plot( absolute_mag, @@ -639,6 +622,7 @@ components depend on the conditioning variable. ax.set_title("Two-component conditional LF", fontsize=TITLE_SIZE) ax.tick_params(axis="both", labelsize=TICK_SIZE) ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() @@ -648,10 +632,9 @@ Integrated conditional number density A conditional luminosity function can be integrated over absolute magnitude at each value of the conditioning variable. -This example uses LFKit's conditional luminosity-function integration helper to -integrate the lognormal component, the modified Schechter component, and the -two-component total over a fixed absolute-magnitude range. The result shows how -the selected number density changes with redshift. +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. .. plot:: :include-source: True @@ -661,14 +644,7 @@ the selected number density changes with redshift. import matplotlib.pyplot as plt import cmasher as cmr - from lfkit.photometry.conditional_lf_integrals import ( - integrate_conditional_luminosity_function, - ) - from lfkit.photometry.conditional_lf_models import ( - lognormal_conditional_lf, - modified_schechter_conditional_lf, - two_component_conditional_lf, - ) + from lfkit import ConditionalLuminosityFunction LABEL_SIZE = 15 TICK_SIZE = 13 @@ -680,50 +656,47 @@ the selected number density changes with redshift. total_color = 0.5 * (np.array(colors_blue[1]) + np.array(colors_red[1])) redshift = np.linspace(0.05, 1.5, 180) - absolute_mag = np.linspace(-24.0, -14.0, 800) - _, z_grid = np.meshgrid(absolute_mag, redshift) + lognormal_lf = ConditionalLuminosityFunction.lognormal( + mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), + sigma_log_luminosity=0.18, + amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4, + ) - n_lognormal = integrate_conditional_luminosity_function( - absolute_mag=absolute_mag, - condition=z_grid, - conditional_lf=lambda absolute_mag, condition: lognormal_conditional_lf( - absolute_mag, - condition, - mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), - sigma_log_luminosity=0.18, - amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4, - ), - axis=1, + modified_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), + alpha=lambda z: -1.05 - 0.10 * z, ) - n_modified = integrate_conditional_luminosity_function( - absolute_mag=absolute_mag, - condition=z_grid, - conditional_lf=lambda absolute_mag, condition: modified_schechter_conditional_lf( - absolute_mag, - condition, - phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5, - m_star=lambda z: -19.9 - 0.5 * (z - 0.1), - alpha=lambda z: -1.05 - 0.10 * z, - ), - axis=1, + total_lf = ConditionalLuminosityFunction.two_component( + lognormal_mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), + lognormal_sigma_log_luminosity=0.18, + lognormal_amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4, + modified_phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5, + modified_m_star=lambda z: -19.9 - 0.5 * (z - 0.1), + modified_alpha=lambda z: -1.05 - 0.10 * z, ) - n_total = integrate_conditional_luminosity_function( - absolute_mag=absolute_mag, - condition=z_grid, - conditional_lf=lambda absolute_mag, condition: two_component_conditional_lf( - absolute_mag, - condition, - lognormal_mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), - lognormal_sigma_log_luminosity=0.18, - lognormal_amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4, - modified_phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5, - modified_m_star=lambda z: -19.9 - 0.5 * (z - 0.1), - modified_alpha=lambda z: -1.05 - 0.10 * z, - ), - axis=1, + n_lognormal = lognormal_lf.integrals.number_density( + redshift, + m_bright=-24.0, + m_faint=-14.0, + n_m=800, + ) + + n_modified = modified_lf.integrals.number_density( + redshift, + m_bright=-24.0, + m_faint=-14.0, + n_m=800, + ) + + n_total = total_lf.integrals.number_density( + redshift, + m_bright=-24.0, + m_faint=-14.0, + n_m=800, ) fig, ax = plt.subplots(figsize=(7.0, 5.0)) @@ -756,6 +729,7 @@ the selected number density changes with redshift. ax.set_title(r"Integrated conditional LF over $-24 \leq M \leq -14$", fontsize=TITLE_SIZE) ax.tick_params(axis="both", labelsize=TICK_SIZE) ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() @@ -765,10 +739,10 @@ Component fractions The relative contribution of each component can be summarized as a fraction of the integrated two-component luminosity function. -This example uses LFKit's conditional luminosity-function integration helper to -compute the integrated lognormal and modified Schechter components. 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. +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. .. plot:: :include-source: True @@ -778,13 +752,7 @@ the lognormal component, the modified Schechter component, or a mixture of both. import matplotlib.pyplot as plt import cmasher as cmr - from lfkit.photometry.conditional_lf_integrals import ( - integrate_conditional_luminosity_function, - ) - from lfkit.photometry.conditional_lf_models import ( - lognormal_conditional_lf, - modified_schechter_conditional_lf, - ) + from lfkit import ConditionalLuminosityFunction LABEL_SIZE = 15 TICK_SIZE = 13 @@ -795,34 +763,31 @@ the lognormal component, the modified Schechter component, or a mixture of both. colors_red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.03, 0.26)) redshift = np.linspace(0.05, 1.5, 180) - absolute_mag = np.linspace(-24.0, -14.0, 800) - _, z_grid = np.meshgrid(absolute_mag, redshift) + lognormal_lf = ConditionalLuminosityFunction.lognormal( + mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), + sigma_log_luminosity=0.18, + amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4, + ) - n_lognormal = integrate_conditional_luminosity_function( - absolute_mag=absolute_mag, - condition=z_grid, - conditional_lf=lambda absolute_mag, condition: lognormal_conditional_lf( - absolute_mag, - condition, - mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), - sigma_log_luminosity=0.18, - amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4, - ), - axis=1, + modified_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), + alpha=lambda z: -1.05 - 0.10 * z, ) - n_modified = integrate_conditional_luminosity_function( - absolute_mag=absolute_mag, - condition=z_grid, - conditional_lf=lambda absolute_mag, condition: modified_schechter_conditional_lf( - absolute_mag, - condition, - phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5, - m_star=lambda z: -19.9 - 0.5 * (z - 0.1), - alpha=lambda z: -1.05 - 0.10 * z, - ), - axis=1, + n_lognormal = lognormal_lf.integrals.number_density( + redshift, + m_bright=-24.0, + m_faint=-14.0, + n_m=800, + ) + + n_modified = modified_lf.integrals.number_density( + redshift, + m_bright=-24.0, + m_faint=-14.0, + n_m=800, ) n_total = n_lognormal + n_modified @@ -853,11 +818,12 @@ the lognormal component, the modified Schechter component, or a mixture of both. ax.set_title(r"Component fractions over $-24 \leq M \leq -14$", fontsize=TITLE_SIZE) ax.tick_params(axis="both", labelsize=TICK_SIZE) ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="center right") + plt.tight_layout() Two-component LF surface ------------------------------------------ +------------------------ The full two-component conditional luminosity function can be shown as a surface in the magnitude-redshift plane. @@ -875,9 +841,7 @@ smoothly. import matplotlib.pyplot as plt import cmasher as cmr - from lfkit.photometry.conditional_lf_models import ( - two_component_conditional_lf, - ) + from lfkit import ConditionalLuminosityFunction LABEL_SIZE = 15 TICK_SIZE = 13 @@ -888,9 +852,7 @@ smoothly. mag_grid, z_grid = np.meshgrid(absolute_mag, redshift) - phi = two_component_conditional_lf( - mag_grid, - z_grid, + lf = ConditionalLuminosityFunction.two_component( lognormal_mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), lognormal_sigma_log_luminosity=0.18, lognormal_amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4, @@ -899,6 +861,7 @@ smoothly. modified_alpha=lambda z: -1.05 - 0.10 * z, ) + phi = lf.phi(mag_grid, z_grid) log_phi = np.log10(phi) fig, ax = plt.subplots(figsize=(7.2, 5.0)) @@ -957,7 +920,7 @@ lognormal mean magnitude become brighter in more massive halos. import matplotlib.pyplot as plt import cmasher as cmr - from lfkit.photometry.conditional_lf_models import lognormal_conditional_lf + from lfkit import ConditionalLuminosityFunction LABEL_SIZE = 15 TICK_SIZE = 13 @@ -969,18 +932,17 @@ lognormal mean magnitude become brighter in more massive halos. absolute_mag = np.linspace(-24.0, -16.0, 600) log_halo_masses = [11.5, 12.0, 12.5, 13.0] + lf = ConditionalLuminosityFunction.lognormal( + mean_absolute_mag=lambda log_mh: -20.0 - 0.8 * (log_mh - 12.0), + sigma_log_luminosity=0.18, + amplitude=lambda log_mh: 5.0e-4 * 10.0 ** (0.3 * (log_mh - 12.0)), + ) + fig, ax = plt.subplots(figsize=(7.0, 5.0)) for log_mh, color in zip(log_halo_masses, colors): condition = np.full_like(absolute_mag, log_mh) - - phi = lognormal_conditional_lf( - absolute_mag, - condition, - mean_absolute_mag=lambda log_mh: -20.0 - 0.8 * (log_mh - 12.0), - sigma_log_luminosity=0.18, - amplitude=lambda log_mh: 5.0e-4 * 10.0 ** (0.3 * (log_mh - 12.0)), - ) + phi = lf.phi(absolute_mag, condition) ax.plot( absolute_mag, @@ -1004,14 +966,18 @@ lognormal mean magnitude become brighter in more massive halos. plt.tight_layout() -Mean magnitude from a conditional luminosity function ------------------------------------------------------ +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 summary statistics of a conditional -luminosity function. For example, the mean absolute magnitude at fixed condition -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:`\langle M \rangle(x) = \int M \Phi(M \mid x)\,dM / \int \Phi(M \mid x)\,dM`. +This example uses the ``lf.integrals`` namespace on a two-component conditional +luminosity function. .. plot:: :include-source: True @@ -1021,69 +987,55 @@ is import matplotlib.pyplot as plt import cmasher as cmr - from lfkit.photometry.conditional_lf_integrals import ( - integrate_conditional_luminosity_function, - integrate_weighted_conditional_luminosity_function, - ) - from lfkit.photometry.conditional_lf_models import two_component_conditional_lf + from lfkit import ConditionalLuminosityFunction LABEL_SIZE = 15 TICK_SIZE = 13 TITLE_SIZE = 17 redshift = np.linspace(0.05, 1.5, 180) - absolute_mag = np.linspace(-24.0, -14.0, 800) + reference_mag = -20.5 - mag_grid, z_grid = np.meshgrid(absolute_mag, redshift) + lf = ConditionalLuminosityFunction.two_component( + lognormal_mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), + lognormal_sigma_log_luminosity=0.18, + lognormal_amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4, + modified_phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5, + modified_m_star=lambda z: -19.9 - 0.5 * (z - 0.1), + modified_alpha=lambda z: -1.05 - 0.10 * z, + ) - number_density = integrate_conditional_luminosity_function( - absolute_mag=absolute_mag, - condition=z_grid, - conditional_lf=lambda absolute_mag, condition: two_component_conditional_lf( - absolute_mag, - condition, - lognormal_mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), - lognormal_sigma_log_luminosity=0.18, - lognormal_amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4, - modified_phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5, - modified_m_star=lambda z: -19.9 - 0.5 * (z - 0.1), - modified_alpha=lambda z: -1.05 - 0.10 * z, - ), - axis=1, + number_density = lf.integrals.number_density( + redshift, + m_bright=-24.0, + m_faint=-14.0, + n_m=800, ) - weighted_magnitude = integrate_weighted_conditional_luminosity_function( - absolute_mag=absolute_mag, - condition=z_grid, - conditional_lf=lambda absolute_mag, condition: two_component_conditional_lf( - absolute_mag, - condition, - lognormal_mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), - lognormal_sigma_log_luminosity=0.18, - lognormal_amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4, - modified_phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5, - modified_m_star=lambda z: -19.9 - 0.5 * (z - 0.1), - modified_alpha=lambda z: -1.05 - 0.10 * z, + weighted_luminosity_ratio = lf.integrals.weighted( + redshift, + weight_fn=lambda absolute_mag, condition: 10.0 ** ( + -0.4 * (absolute_mag - reference_mag) ), - weight=lambda absolute_mag, condition: absolute_mag, - axis=1, + m_bright=-24.0, + m_faint=-14.0, + n_m=800, ) - mean_magnitude = weighted_magnitude / number_density + mean_luminosity_ratio = weighted_luminosity_ratio / number_density fig, ax = plt.subplots(figsize=(7.0, 5.0)) ax.plot( redshift, - mean_magnitude, + mean_luminosity_ratio, lw=3, color=cmr.take_cmap_colors("cmr.guppy", 1, cmap_range=(0.7, 0.9))[0], ) - ax.invert_yaxis() ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) - ax.set_ylabel(r"Mean absolute magnitude $\langle M \rangle$", fontsize=LABEL_SIZE) - ax.set_title("Mean magnitude of the conditional LF", fontsize=TITLE_SIZE) + ax.set_ylabel(r"Mean luminosity ratio $\langle L/L_{\rm ref} \rangle$", fontsize=LABEL_SIZE) + ax.set_title("Mean luminosity ratio of the conditional LF", fontsize=TITLE_SIZE) ax.tick_params(axis="both", labelsize=TICK_SIZE) plt.tight_layout() @@ -1092,10 +1044,9 @@ is Selection-limited conditional number density -------------------------------------------- -Instead of integrating over a fixed absolute-magnitude range by hand, LFKit can -integrate a luminosity function callable 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. +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. Here, the limiting absolute magnitude becomes brighter with redshift. The example compares the full number density over a fixed magnitude range with the @@ -1109,8 +1060,7 @@ number density brighter than the redshift-dependent limit. import matplotlib.pyplot as plt import cmasher as cmr - from lfkit.photometry.conditional_lf_models import two_component_conditional_lf - from lfkit.photometry.lf_integrals import integrated_number_density + from lfkit import ConditionalLuminosityFunction LABEL_SIZE = 15 TICK_SIZE = 13 @@ -1119,31 +1069,26 @@ number density brighter than the redshift-dependent limit. redshift = np.linspace(0.05, 1.5, 180) - def lf(absolute_mag, z): - return two_component_conditional_lf( - absolute_mag, - z, - lognormal_mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), - lognormal_sigma_log_luminosity=0.18, - lognormal_amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4, - modified_phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5, - modified_m_star=lambda z: -19.9 - 0.5 * (z - 0.1), - modified_alpha=lambda z: -1.05 - 0.10 * z, - ) + lf = ConditionalLuminosityFunction.two_component( + lognormal_mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), + lognormal_sigma_log_luminosity=0.18, + lognormal_amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4, + modified_phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5, + modified_m_star=lambda z: -19.9 - 0.5 * (z - 0.1), + modified_alpha=lambda z: -1.05 - 0.10 * z, + ) limiting_mag = -18.5 - 1.2 * redshift - n_total = integrated_number_density( + n_total = lf.integrals.number_density( redshift, - lf, m_bright=-24.0, m_faint=-14.0, n_m=800, ) - n_selected = integrated_number_density( + n_selected = lf.integrals.number_density( redshift, - lf, m_bright=-24.0, m_faint=limiting_mag, n_m=800, @@ -1196,8 +1141,7 @@ reference magnitude range. import matplotlib.pyplot as plt import cmasher as cmr - from lfkit.photometry.conditional_lf_models import two_component_conditional_lf - from lfkit.photometry.lf_integrals import integrated_number_density + from lfkit import ConditionalLuminosityFunction LABEL_SIZE = 15 TICK_SIZE = 13 @@ -1205,31 +1149,26 @@ reference magnitude range. redshift = np.linspace(0.05, 1.5, 180) - def lf(absolute_mag, z): - return two_component_conditional_lf( - absolute_mag, - z, - lognormal_mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), - lognormal_sigma_log_luminosity=0.18, - lognormal_amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4, - modified_phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5, - modified_m_star=lambda z: -19.9 - 0.5 * (z - 0.1), - modified_alpha=lambda z: -1.05 - 0.10 * z, - ) + lf = ConditionalLuminosityFunction.two_component( + lognormal_mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1), + lognormal_sigma_log_luminosity=0.18, + lognormal_amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4, + modified_phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5, + modified_m_star=lambda z: -19.9 - 0.5 * (z - 0.1), + modified_alpha=lambda z: -1.05 - 0.10 * z, + ) limiting_mag = -18.5 - 1.2 * redshift - n_total = integrated_number_density( + n_total = lf.integrals.number_density( redshift, - lf, m_bright=-24.0, m_faint=-14.0, n_m=800, ) - n_selected = integrated_number_density( + n_selected = lf.integrals.number_density( redshift, - lf, m_bright=-24.0, m_faint=limiting_mag, n_m=800, @@ -1241,12 +1180,7 @@ reference magnitude range. fig, ax = plt.subplots(figsize=(7.0, 5.0)) - ax.plot( - redshift, - selected_fraction, - lw=3, - color=color, - ) + ax.plot(redshift, selected_fraction, lw=3, color=color) ax.set_ylim(-0.05, 1.05) ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) diff --git a/docs/examples/index.rst b/docs/examples/index.rst index 01ac140..e35998a 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -5,82 +5,19 @@ |lfkitlogo| Examples ==================== -Working, executable examples for using LFKit. - -.. grid:: 2 - :gutter: 2 - - .. grid-item-card:: - :link: kcorrect_examples - :link-type: doc - :shadow: md - - **kcorrect examples** - ^^^ - Build :math:`k(z)` with the kcorrect backend from a single rest-frame - color anchor, compare anchors, and explore output-band choices. - - +++ - *Backends:* kcorrect - - .. grid-item-card:: - :link: poggianti_examples - :link-type: doc - :shadow: md - - **Poggianti (1997) examples** - ^^^ - Evaluate :math:`k(z)`, :math:`e(z)`, and :math:`k(z)-e(z)` from the - Poggianti tabulations; compare galaxy types and bands. - - +++ - *Backends:* Poggianti (1997) - - .. grid-item-card:: - :link: luminosity_function_examples - :link-type: doc - :shadow: md - - **Luminosity-function examples** - ^^^ - Build standard, evolving, and double Schechter luminosity functions, - evaluate :math:`\phi(M, z)`, and compare model behaviour. - - +++ - *API:* LuminosityFunction - - .. grid-item-card:: - :link: conditional_luminosity_function_examples - :link-type: doc - :shadow: md - - **Conditional luminosity function examples** - ^^^ - Build conditional luminosity functions, including redshift-dependent - Schechter models and central/satellite components. - - +++ - *API:* LuminosityFunction - - .. grid-item-card:: - :link: catalog_completeness_examples - :link-type: doc - :shadow: md - - **Catalog-completeness examples** - ^^^ - Compute observed and missing number densities for a magnitude-limited - catalog and visualize completeness as a function of redshift. - - +++ - *API:* LuminosityFunction .. toctree:: :maxdepth: 1 :hidden: + api_overview + luminosity_function_models + magnitude_integrals + magnitudes_and_luminosities + redshift_density + catalog_completeness + conditional_luminosity_function + model_registry kcorrect_examples poggianti_examples - luminosity_function_examples - conditional_luminosity_function_examples - catalog_completeness_examples + diff --git a/docs/examples/luminosity_function_examples.rst b/docs/examples/luminosity_function_examples.rst deleted file mode 100644 index b8f6e3e..0000000 --- a/docs/examples/luminosity_function_examples.rst +++ /dev/null @@ -1,959 +0,0 @@ -.. |lfkitlogo| image:: /_static/logos/lfkit_logo-icon.png - :alt: LFKit logo - :width: 50px - -|lfkitlogo| Luminosity function examples -======================================== - -This page provides executable examples showing how to use -:class:`lfkit.LuminosityFunction` to construct, evaluate, and compare -luminosity function models. - -Luminosity functions describe how common galaxies are as a function of their -absolute magnitude. In these examples, brighter galaxies appear on the left -because astronomical magnitudes decrease for brighter objects. - -The luminosity function normalization sets the units. For example, if -:math:`\phi_\star` is supplied in comoving :math:`{\rm Mpc}^{-3}\,{\rm mag}^{-1}`, -then :math:`\phi(M, z)` has units of -:math:`{\rm Mpc}^{-3}\,{\rm mag}^{-1}`, and magnitude-integrated number -densities have units of :math:`{\rm Mpc}^{-3}`. - -The examples that connect apparent and absolute magnitude use -``corrections=None``. This means that no :math:`K`-correction or evolution -correction is applied. Users can pass their own correction model through the -``corrections`` argument, for example a :class:`lfkit.Corrections` object or any -compatible correction callable used by the LFKit magnitude-conversion methods. - -All examples below are executable via ``.. plot::``. - - -Standard Schechter luminosity function --------------------------------------- - -A standard Schechter luminosity function has fixed parameters -:math:`\phi_\star`, :math:`M_\star`, and :math:`\alpha`. - -This plot shows the basic shape of a Schechter luminosity function as a -function of absolute magnitude. The function decreases rapidly at the bright end -and rises toward the faint end, reflecting the usual picture that very luminous -galaxies are rare while faint galaxies are more common. - -The y-axis is shown on a logarithmic scale because luminosity functions often -span several orders of magnitude. This makes both the bright-end cutoff and the -faint-end behaviour visible in the same figure. - -.. plot:: - :include-source: True - :width: 520 - - import numpy as np - import matplotlib.pyplot as plt - import cmasher as cmr - - from lfkit import LuminosityFunction - - LABEL_SIZE = 15 - TICK_SIZE = 13 - TITLE_SIZE = 17 - LEGEND_SIZE = 15 - - colors_blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.72, 0.96)) - - absolute_mag = np.linspace(-24.0, -14.0, 500) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.1, - ) - - phi = lf.phi(absolute_mag) - - fig, ax = plt.subplots(figsize=(7.0, 5.0)) - ax.plot(absolute_mag, phi, lw=3, color=colors_blue[1]) - ax.set_yscale("log") - ax.invert_xaxis() - ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE) - ax.set_ylabel(r"$\phi(M)$ [$\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1}$]", fontsize=LABEL_SIZE) - ax.set_title("Standard Schechter luminosity function", fontsize=TITLE_SIZE) - ax.tick_params(axis="both", labelsize=TICK_SIZE) - plt.tight_layout() - - -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. - -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. - -.. plot:: - :include-source: True - :width: 520 - - import numpy as np - import matplotlib.pyplot as plt - import cmasher as cmr - - from lfkit import LuminosityFunction - - LABEL_SIZE = 15 - TICK_SIZE = 13 - TITLE_SIZE = 17 - LEGEND_SIZE = 15 - - colors_red = 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] - - fig, ax = plt.subplots(figsize=(7.0, 5.0)) - - for alpha, color in zip(alphas, colors_red): - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=alpha, - ) - phi = lf.phi(absolute_mag) - ax.plot(absolute_mag, phi, lw=3, color=color, label=rf"$\alpha={alpha}$") - - ax.set_yscale("log") - ax.invert_xaxis() - ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE) - ax.set_ylabel(r"$\phi(M)$ [$\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1}$]", fontsize=LABEL_SIZE) - ax.set_title("Effect of the faint-end slope", fontsize=TITLE_SIZE) - ax.tick_params(axis="both", labelsize=TICK_SIZE) - ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") - plt.tight_layout() - - -Evolving Schechter luminosity function --------------------------------------- - -An evolving Schechter model allows the luminosity function parameters to vary -with redshift. - -This plot compares the luminosity function at several redshifts. Instead of -using one fixed curve for all epochs, the evolving model allows the amplitude -and characteristic magnitude to change with redshift. - -This kind of plot is useful for visualizing galaxy evolution in a compact way. -Changes in normalization alter the overall abundance, while shifts in -:math:`M_\star` move the turnover of the luminosity function toward brighter or -fainter magnitudes. - -.. plot:: - :include-source: True - :width: 520 - - import numpy as np - import matplotlib.pyplot as plt - import cmasher as cmr - - from lfkit import LuminosityFunction - - LABEL_SIZE = 15 - TICK_SIZE = 13 - TITLE_SIZE = 17 - LEGEND_SIZE = 15 - - colors_blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.72, 0.96)) - - absolute_mag = np.linspace(-24.0, -14.0, 500) - - lf = LuminosityFunction.evolving_schechter( - phi_model="linear_p", - phi_kwargs={"phi_0_star": 1.0e-3, "p": 0.8}, - m_star_model="linear_q", - m_star_kwargs={"m_0_star": -20.5, "q": 1.0, "z_ref": 0.1}, - alpha_model="constant", - alpha_kwargs={"alpha": -1.1}, - ) - - redshifts = [0.1, 0.5, 1.0] - - fig, ax = plt.subplots(figsize=(7.0, 5.0)) - - for z_value, color in zip(redshifts, colors_blue): - z = np.full_like(absolute_mag, z_value) - phi = lf.phi(absolute_mag, z) - ax.plot(absolute_mag, phi, lw=3, color=color, label=rf"$z={z_value}$") - - ax.set_yscale("log") - ax.invert_xaxis() - ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE) - ax.set_ylabel(r"$\phi(M, z)$ [$\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1}$]", fontsize=LABEL_SIZE) - ax.set_title("Evolving Schechter luminosity function", fontsize=TITLE_SIZE) - ax.tick_params(axis="both", labelsize=TICK_SIZE) - ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") - plt.tight_layout() - - -Inspecting evolving parameters ------------------------------- - -The evolving luminosity function parameters can also be evaluated directly. - -This plot shows the redshift evolution of the parameters that define the -luminosity function. Instead of plotting :math:`\phi(M, z)` itself, it shows how -:math:`\phi_\star`, :math:`M_\star`, and :math:`\alpha` change with redshift. - -This is useful when checking whether an evolving model behaves as expected -before using it in a larger calculation. For example, it can help verify that -the normalization evolves smoothly, that :math:`M_\star` shifts in the intended -direction, and that the faint-end slope remains in a sensible range. - -.. plot:: - :include-source: True - :width: 620 - - import numpy as np - import matplotlib.pyplot as plt - import cmasher as cmr - - from lfkit import LuminosityFunction - - LABEL_SIZE = 15 - TICK_SIZE = 13 - 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)) - - z = np.linspace(0.0, 1.5, 300) - - lf = LuminosityFunction.evolving_schechter( - phi_model="linear_p", - phi_kwargs={"phi_0_star": 1.0e-3, "p": 0.8}, - m_star_model="linear_q", - m_star_kwargs={"m_0_star": -20.5, "q": 1.0, "z_ref": 0.1}, - alpha_model="linear", - alpha_kwargs={"alpha_0": -1.1, "alpha_1": -0.1, "z_ref": 0.1}, - ) - - phi_star, m_star, alpha = lf.parameters(z) - - fig, axes = plt.subplots( - 3, - 1, - figsize=(7.0, 7.2), - sharex=True, - constrained_layout=True, - ) - - axes[0].plot(z, phi_star, lw=3, color=colors_blue[1]) - axes[0].set_ylabel(r"$\phi_\star$", fontsize=LABEL_SIZE) - axes[0].ticklabel_format(axis="y", style="sci", scilimits=(0, 0)) - axes[0].set_title("Evolving LF parameters", fontsize=TITLE_SIZE) - - axes[1].plot(z, m_star, lw=3, color=colors_red[1]) - axes[1].set_ylabel(r"$M_\star$", fontsize=LABEL_SIZE) - - axes[2].plot(z, alpha, lw=3, color=colors_blue[2]) - axes[2].set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) - axes[2].set_ylabel(r"$\alpha$", fontsize=LABEL_SIZE) - - for ax in axes: - ax.tick_params(axis="both", labelsize=TICK_SIZE) - - plt.tight_layout() - - -Double Schechter luminosity function ------------------------------------- - -A double Schechter-style model can be used when the luminosity function needs -additional structure beyond the standard Schechter form. - -This plot compares a standard Schechter model with a double-Schechter-style -model. The double model adds extra flexibility around the faint end, where a -single power-law slope may not describe the full galaxy population well. - -This type of comparison is useful when testing whether a simple one-component -model is sufficient. If the two curves differ strongly at faint magnitudes, -integrated quantities that depend on faint galaxies may also differ. - -.. plot:: - :include-source: True - :width: 520 - - import numpy as np - import matplotlib.pyplot as plt - import cmasher as cmr - - from lfkit import LuminosityFunction - - LABEL_SIZE = 15 - TICK_SIZE = 13 - 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)) - - absolute_mag = np.linspace(-24.0, -14.0, 500) - - standard = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.1, - ) - - double = LuminosityFunction.double_schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.0, - beta=-1.5, - m_transition=-18.0, - ) - - fig, ax = plt.subplots(figsize=(7.0, 5.0)) - - ax.plot( - absolute_mag, - standard.phi(absolute_mag), - lw=3, - color=colors_blue[1], - label="Standard Schechter", - ) - ax.plot( - absolute_mag, - double.phi(absolute_mag), - lw=3, - color=colors_red[1], - label="Double Schechter", - ) - - ax.set_yscale("log") - ax.invert_xaxis() - ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE) - ax.set_ylabel(r"$\phi(M)$ [$\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1}$]", fontsize=LABEL_SIZE) - ax.set_title("Standard and double Schechter models", fontsize=TITLE_SIZE) - ax.tick_params(axis="both", labelsize=TICK_SIZE) - ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") - plt.tight_layout() - - -Integrated number density -------------------------- - -A luminosity function can be integrated over magnitude to estimate the number -density of galaxies brighter than a chosen absolute-magnitude limit. - -This plot shows how the cumulative number density changes as progressively -fainter galaxies are included. At very bright limits, only rare luminous -galaxies contribute. As the magnitude limit becomes fainter, more galaxies are -included and the integrated number density increases. - -This is one of the most common ways a luminosity function enters survey -calculations. Instead of using the value of :math:`\phi(M)` at one magnitude, -the model is integrated over the part of the galaxy population that the sample -selects. - -.. plot:: - :include-source: True - :width: 520 - - import numpy as np - import matplotlib.pyplot as plt - import cmasher as cmr - - from lfkit import LuminosityFunction - - LABEL_SIZE = 15 - TICK_SIZE = 13 - TITLE_SIZE = 17 - LEGEND_SIZE = 15 - - colors_blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.72, 0.96)) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.1, - ) - - magnitude_limits = np.linspace(-23.0, -15.0, 120) - - number_density = lf.integrated_number_density( - z=0.0, - m_bright=-25.0, - m_faint=magnitude_limits, - n_m=800, - ) - - fig, ax = plt.subplots(figsize=(7.0, 5.0)) - ax.plot(magnitude_limits, number_density, lw=3, color=colors_blue[1]) - ax.set_yscale("log") - ax.invert_xaxis() - ax.set_xlabel(r"Faint absolute-magnitude limit $M_{\rm lim}$", fontsize=LABEL_SIZE) - ax.set_ylabel(r"$n(M < M_{\rm lim})$ [$\mathrm{Mpc}^{-3}$]", fontsize=LABEL_SIZE) - ax.set_title("Integrated number density", fontsize=TITLE_SIZE) - ax.tick_params(axis="both", labelsize=TICK_SIZE) - plt.tight_layout() - - -Magnitude-redshift luminosity function surface ----------------------------------------------- - -For an evolving luminosity function, the abundance depends on both absolute -magnitude and redshift. - -This plot shows the luminosity function amplitude across the -magnitude-redshift plane. The horizontal direction shows which galaxies are -bright or faint, while the vertical direction shows how the model changes with -redshift. - -The filled colour scale shows :math:`\log_{10}\phi(M, z)`. The white contours -mark constant :math:`\log_{10}\phi(M, z)` levels at -5, -4, -3, and -2. These -contours make it easier to see where equal-abundance regions sit in the -magnitude-redshift plane. - -This view is helpful for checking the full two-dimensional behaviour of an -evolving model. It makes it easier to see whether the bright end, faint end, and -redshift evolution combine smoothly across the range of interest. - -.. plot:: - :include-source: True - :width: 560 - - import numpy as np - import matplotlib.pyplot as plt - import cmasher as cmr - - from lfkit import LuminosityFunction - - LABEL_SIZE = 15 - TICK_SIZE = 13 - TITLE_SIZE = 17 - LEGEND_SIZE = 15 - - absolute_mag = np.linspace(-24.0, -18.0, 220) - redshift = np.linspace(0.0, 1.5, 180) - - mag_grid, z_grid = np.meshgrid(absolute_mag, redshift) - - lf = LuminosityFunction.evolving_schechter( - phi_model="linear_p", - phi_kwargs={"phi_0_star": 1.0e-3, "p": 0.8}, - m_star_model="linear_q", - m_star_kwargs={"m_0_star": -20.5, "q": 1.0, "z_ref": 0.1}, - alpha_model="constant", - alpha_kwargs={"alpha": -1.1}, - ) - - phi = lf.phi(mag_grid, z_grid) - 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", - ) - - contour_levels = [-5.0, -4.0, -3.0, -2.0] - contours = ax.contour( - absolute_mag, - redshift, - log_phi, - levels=contour_levels, - colors="white", - linewidths=1.2, - ) - ax.clabel(contours, inline=True, fontsize=TICK_SIZE, fmt=r"$10^{%.0f}$") - - ax.invert_xaxis() - ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE) - ax.set_ylabel("Redshift $z$", fontsize=LABEL_SIZE) - ax.set_title("Evolving luminosity function", fontsize=TITLE_SIZE) - ax.tick_params(axis="both", labelsize=TICK_SIZE) - - cbar = fig.colorbar(mesh, ax=ax) - cbar.set_label( - r"$\log_{10}\phi(M, z)$ [$\log_{10}(\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1})$]", - fontsize=LABEL_SIZE, - ) - cbar.ax.tick_params(labelsize=TICK_SIZE) - - plt.tight_layout() - - -Magnitude-limited LF-weighted redshift trend --------------------------------------------- - -A survey magnitude limit selects different parts of the luminosity function at -different redshifts. - -This example shows LF-weighted redshift trends for several fixed -apparent-magnitude limits. The absolute-magnitude limit is computed from the -cosmology-dependent distance modulus. At lower redshift, a flux-limited sample -can include relatively faint galaxies. At higher redshift, the same -apparent-magnitude limit corresponds to a brighter absolute-magnitude cut, so -only intrinsically brighter galaxies remain in the selected sample. - -The result is not intended to be a full survey :math:`n(z)`, because it does not -include the survey volume element. Instead, it shows the luminosity function -selection factor that later enters magnitude-limited redshift-distribution -calculations. - -.. plot:: - :include-source: True - :width: 620 - - import numpy as np - import matplotlib.pyplot as plt - import cmasher as cmr - import pyccl as ccl - - from lfkit import LuminosityFunction - - LABEL_SIZE = 15 - TICK_SIZE = 13 - TITLE_SIZE = 17 - LEGEND_SIZE = 15 - - colors_blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.72, 0.96)) - - cosmo = ccl.Cosmology( - Omega_c=0.25, - Omega_b=0.05, - h=0.7, - sigma8=0.8, - n_s=0.96, - transfer_function="bbks", - matter_power_spectrum="linear", - ) - - lf = LuminosityFunction.evolving_schechter( - phi_model="linear_p", - phi_kwargs={"phi_0_star": 1.0e-3, "p": 0.6}, - 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}, - ) - - redshift = np.linspace(0.05, 1.5, 160) - apparent_limits = [23.5, 24.5, 25.5] - - fig, ax = plt.subplots(figsize=(7.0, 5.0)) - - for m_lim, color in zip(apparent_limits, colors_blue): - m_limit = lf.absolute_magnitude_limit( - cosmo, - redshift, - m_lim=m_lim, - corrections=None, - ) - - lf_selection = lf.integrated_number_density( - z=redshift, - m_bright=-25.0, - m_faint=m_limit, - n_m=700, - ) - lf_selection /= np.trapezoid(lf_selection, redshift) - - ax.plot( - redshift, - lf_selection, - lw=3, - color=color, - label=rf"$m_{{\rm lim}}={m_lim}$", - ) - - ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) - ax.set_ylabel("LF selection", fontsize=LABEL_SIZE) - ax.set_title("Magnitude-limited LF selection", fontsize=TITLE_SIZE) - ax.tick_params(axis="both", labelsize=TICK_SIZE) - ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") - plt.tight_layout() - - -Cosmology dependence of the absolute-magnitude limit ----------------------------------------------------- - -The apparent-to-absolute magnitude conversion depends on cosmology through the -luminosity distance and distance modulus. - -This plot compares the absolute-magnitude limit implied by the same apparent -magnitude cut in several cosmologies. The luminosity function is not used in -this plot; the comparison isolates the selection boundary itself. - -Users can replace the entries in the ``cosmologies`` dictionary with any -:class:`pyccl.Cosmology` objects relevant to their own analysis. - -.. plot:: - :include-source: True - :width: 620 - - import numpy as np - import matplotlib.pyplot as plt - import cmasher as cmr - import pyccl as ccl - - from lfkit import LuminosityFunction - - LABEL_SIZE = 15 - TICK_SIZE = 13 - 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, - Omega_b=0.05, - h=0.70, - sigma8=0.8, - n_s=0.96, - transfer_function="bbks", - matter_power_spectrum="linear", - ), - r"$\Omega_{\rm m}=0.30,\ h=0.70$": ccl.Cosmology( - Omega_c=0.25, - Omega_b=0.05, - h=0.70, - sigma8=0.8, - n_s=0.96, - transfer_function="bbks", - matter_power_spectrum="linear", - ), - r"$\Omega_{\rm m}=0.35,\ h=0.70$": ccl.Cosmology( - Omega_c=0.30, - Omega_b=0.05, - h=0.70, - sigma8=0.8, - n_s=0.96, - transfer_function="bbks", - matter_power_spectrum="linear", - ), - } - - z = np.linspace(0.05, 1.5, 250) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.1, - ) - - m_limits = {} - - for label, cosmo in cosmologies.items(): - m_limits[label] = lf.absolute_magnitude_limit( - cosmo, - z, - m_lim=24.5, - corrections=None, - ) - - reference_label = r"$\Omega_{\rm m}=0.30,\ h=0.70$" - reference = m_limits[reference_label] - - 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): - ax_top.plot(z, m_limit, lw=3, color=color, label=label) - ax_bottom.plot(z, m_limit - reference, lw=2.5, color=color) - - ax_top.invert_yaxis() - ax_top.set_ylabel(r"$M_{\rm lim}(z)$", fontsize=LABEL_SIZE) - ax_top.set_title(r"Cosmology dependence of $M_{\rm lim}(z)$", fontsize=TITLE_SIZE) - ax_top.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") - - ax_bottom.axhline(0.0, lw=1.0, color="0.3") - ax_bottom.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) - ax_bottom.set_ylabel(r"$\Delta M_{\rm lim}$", fontsize=LABEL_SIZE) - - for ax in (ax_top, ax_bottom): - ax.tick_params(axis="both", labelsize=TICK_SIZE) - - plt.tight_layout() - - -Cosmology dependence of LF selection ------------------------------------- - -The LF-weighted selection factor also depends on cosmology because the same -apparent-magnitude cut maps to a different absolute-magnitude limit. - -This example keeps the luminosity function and apparent-magnitude limit fixed, -then changes only the cosmology. The curves are normalized to unit integral over -redshift, so the comparison emphasizes changes in shape rather than absolute -normalization. - -The lower panel shows the residual relative to the reference cosmology, -:math:`\Omega_{\rm m}=0.30,\ h=0.70`. - -This is still not a full survey :math:`n(z)`, because it does not include the -cosmological volume element. It is the luminosity function selection factor -alone. - -.. plot:: - :include-source: True - :width: 620 - - import numpy as np - import matplotlib.pyplot as plt - import cmasher as cmr - import pyccl as ccl - - from lfkit import LuminosityFunction - - LABEL_SIZE = 15 - TICK_SIZE = 13 - 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, - Omega_b=0.05, - h=0.70, - sigma8=0.8, - n_s=0.96, - transfer_function="bbks", - matter_power_spectrum="linear", - ), - r"$\Omega_{\rm m}=0.30,\ h=0.70$": ccl.Cosmology( - Omega_c=0.25, - Omega_b=0.05, - h=0.70, - sigma8=0.8, - n_s=0.96, - transfer_function="bbks", - matter_power_spectrum="linear", - ), - r"$\Omega_{\rm m}=0.35,\ h=0.70$": ccl.Cosmology( - Omega_c=0.30, - Omega_b=0.05, - h=0.70, - sigma8=0.8, - n_s=0.96, - transfer_function="bbks", - matter_power_spectrum="linear", - ), - } - - redshift = np.linspace(0.05, 1.5, 160) - - lf = LuminosityFunction.evolving_schechter( - phi_model="linear_p", - phi_kwargs={"phi_0_star": 1.0e-3, "p": 0.6}, - 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}, - ) - - selections = {} - - for label, cosmo in cosmologies.items(): - m_limit = lf.absolute_magnitude_limit( - cosmo, - redshift, - m_lim=24.5, - corrections=None, - ) - - lf_selection = lf.integrated_number_density( - z=redshift, - m_bright=-25.0, - m_faint=m_limit, - n_m=700, - ) - lf_selection /= np.trapezoid(lf_selection, redshift) - - selections[label] = lf_selection - - reference_label = r"$\Omega_{\rm m}=0.30,\ h=0.70$" - reference = selections[reference_label] - - 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, lf_selection), color in zip(selections.items(), colors_red): - ax_top.plot(redshift, lf_selection, lw=3, color=color, label=label) - ax_bottom.plot(redshift, lf_selection - reference, lw=2.5, color=color) - - ax_top.set_ylabel("LF selection", fontsize=LABEL_SIZE) - ax_top.set_title(r"Cosmology dependence of LF selection", fontsize=TITLE_SIZE) - ax_top.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") - - ax_bottom.axhline(0.0, lw=1.0, color="0.3") - ax_bottom.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) - ax_bottom.set_ylabel(r"$\Delta$ selection", fontsize=LABEL_SIZE) - - for ax in (ax_top, ax_bottom): - ax.tick_params(axis="both", labelsize=TICK_SIZE) - - plt.tight_layout() - - -Cosmology and volume-weighted LF redshift trend ------------------------------------------------ - -A full LF-based redshift trend for a magnitude-limited sample should include -both the luminosity function selection and the cosmological volume element. - -This example multiplies the magnitude-integrated luminosity function by a -simple comoving-volume weight per steradian, :math:`\chi^2(z) / H(z)`, up to an -overall constant. The result is closer to the ingredient used in LF-dependent -:math:`n(z)` construction. - -The curves are normalized to unit integral over redshift, so the comparison -shows how cosmology changes the shape of the redshift trend. The lower panel -shows the residual relative to the reference cosmology, -:math:`\Omega_{\rm m}=0.30,\ h=0.70`. - -The absolute normalization depends on survey area, LF normalization, and the -exact volume convention used by the calling analysis. - -.. plot:: - :include-source: True - :width: 620 - - import numpy as np - import matplotlib.pyplot as plt - import cmasher as cmr - import pyccl as ccl - - from lfkit import LuminosityFunction - - LABEL_SIZE = 15 - TICK_SIZE = 13 - 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, - Omega_b=0.05, - h=0.70, - sigma8=0.8, - n_s=0.96, - transfer_function="bbks", - matter_power_spectrum="linear", - ), - r"$\Omega_{\rm m}=0.30,\ h=0.70$": ccl.Cosmology( - Omega_c=0.25, - Omega_b=0.05, - h=0.70, - sigma8=0.8, - n_s=0.96, - transfer_function="bbks", - matter_power_spectrum="linear", - ), - r"$\Omega_{\rm m}=0.35,\ h=0.70$": ccl.Cosmology( - Omega_c=0.30, - Omega_b=0.05, - h=0.70, - sigma8=0.8, - n_s=0.96, - transfer_function="bbks", - matter_power_spectrum="linear", - ), - } - - redshift = np.linspace(0.05, 1.5, 160) - - lf = LuminosityFunction.evolving_schechter( - phi_model="linear_p", - phi_kwargs={"phi_0_star": 1.0e-3, "p": 0.6}, - 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}, - ) - - trends = {} - - for label, cosmo in cosmologies.items(): - scale_factor = 1.0 / (1.0 + redshift) - - chi = ccl.comoving_radial_distance(cosmo, scale_factor) - h_over_h0 = ccl.h_over_h0(cosmo, scale_factor) - - volume_weight = chi**2 / h_over_h0 - - m_limit = lf.absolute_magnitude_limit( - cosmo, - redshift, - m_lim=24.5, - corrections=None, - ) - - lf_selection = lf.integrated_number_density( - z=redshift, - m_bright=-25.0, - m_faint=m_limit, - n_m=700, - ) - - weighted_trend = volume_weight * lf_selection - weighted_trend /= np.trapezoid(weighted_trend, redshift) - - trends[label] = weighted_trend - - reference_label = r"$\Omega_{\rm m}=0.30,\ h=0.70$" - reference = trends[reference_label] - - 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, weighted_trend), color in zip(trends.items(), colors_red): - ax_top.plot(redshift, weighted_trend, lw=3, color=color, label=label) - ax_bottom.plot(redshift, weighted_trend - reference, lw=2.5, color=color) - - ax_top.set_ylabel("Volume-weighted LF trend", fontsize=LABEL_SIZE) - ax_top.set_title(r"Cosmology dependence with volume weighting", fontsize=TITLE_SIZE) - ax_top.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") - - ax_bottom.axhline(0.0, lw=1.0, color="0.3") - ax_bottom.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) - ax_bottom.set_ylabel(r"$\Delta$ trend", fontsize=LABEL_SIZE) - - for ax in (ax_top, ax_bottom): - ax.tick_params(axis="both", labelsize=TICK_SIZE) - - plt.tight_layout() diff --git a/docs/examples/luminosity_function_models.rst b/docs/examples/luminosity_function_models.rst new file mode 100644 index 0000000..6e6c554 --- /dev/null +++ b/docs/examples/luminosity_function_models.rst @@ -0,0 +1,626 @@ +.. |lfkitlogo| image:: /_static/logos/lfkit_logo-icon.png + :alt: LFKit logo + :width: 50px + +|lfkitlogo| Luminosity function models +====================================== + +This page introduces the luminosity function models exposed by +:class:`lfkit.LuminosityFunction`. + +The examples focus on constructing, evaluating, visualizing, and comparing +luminosity function models. Magnitude integrals, completeness calculations, +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 +object stores the chosen model and evaluates it through +:meth:`lfkit.LuminosityFunction.phi`. + +The number-density units follow the normalization supplied to the luminosity +function. For example, if ``phi_star`` is supplied in +:math:`{\rm Mpc}^{-3}\,{\rm mag}^{-1}`, then :math:`\Phi(M)` has units of +:math:`{\rm Mpc}^{-3}\,{\rm mag}^{-1}`. + + +Schechter-family models +----------------------- + +The Schechter family is the main luminosity function model family currently +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. + + +Standard Schechter luminosity function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A Schechter luminosity function can be created with +:meth:`lfkit.LuminosityFunction.schechter`. The returned object evaluates +:math:`\Phi(M)` through :meth:`lfkit.LuminosityFunction.phi`. + +.. plot:: + :include-source: True + :width: 520 + + import numpy as np + import matplotlib.pyplot as plt + import cmasher as cmr + + from lfkit import LuminosityFunction + + LABEL_SIZE = 15 + TICK_SIZE = 13 + TITLE_SIZE = 17 + + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + absolute_mag = np.linspace(-24.0, -14.0, 500) + phi = lf.phi(absolute_mag) + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + ax.plot( + absolute_mag, + phi, + lw=3, + color=cmr.take_cmap_colors("cmr.guppy", 1, cmap_range=(0.72, 0.9))[0], + ) + + ax.set_yscale("log") + ax.invert_xaxis() + ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE) + ax.set_ylabel( + r"$\Phi(M)$ [$\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1}$]", + fontsize=LABEL_SIZE, + ) + ax.set_title("Schechter luminosity function", fontsize=TITLE_SIZE) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + plt.tight_layout() + + +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 +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. + +.. plot:: + :include-source: True + :width: 560 + + import numpy as np + import matplotlib.pyplot as plt + import cmasher as cmr + + from lfkit import LuminosityFunction + + LABEL_SIZE = 15 + TICK_SIZE = 13 + TITLE_SIZE = 17 + + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + 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) + + fig, ax = plt.subplots(figsize=(7.2, 5.0)) + ax.plot( + absolute_mag, + phi, + lw=3, + color=cmr.take_cmap_colors("cmr.guppy", 1, cmap_range=(0.72, 0.9))[0], + ) + + secax = ax.secondary_xaxis( + "top", + functions=(absolute_to_apparent, apparent_to_absolute), + ) + + ax.set_yscale("log") + ax.invert_xaxis() + ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE) + ax.set_ylabel( + r"$\Phi(M)$ [$\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1}$]", + fontsize=LABEL_SIZE, + ) + ax.set_title( + "Schechter luminosity function with apparent-magnitude axis", + fontsize=TITLE_SIZE, + ) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + + secax.set_xlabel("Apparent magnitude $m$", fontsize=LABEL_SIZE) + secax.tick_params(axis="x", labelsize=TICK_SIZE) + + plt.tight_layout() + + + +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. + +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. + +.. plot:: + :include-source: True + :width: 520 + + import numpy as np + import matplotlib.pyplot as plt + import cmasher as cmr + + from lfkit import LuminosityFunction + + LABEL_SIZE = 15 + TICK_SIZE = 13 + 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] + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + + for alpha, color in zip(alphas, colors): + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=alpha, + ) + ax.plot( + absolute_mag, + lf.phi(absolute_mag), + lw=3, + color=color, + label=rf"$\alpha={alpha}$", + ) + + ax.set_yscale("log") + ax.invert_xaxis() + ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE) + ax.set_ylabel( + r"$\Phi(M)$ [$\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1}$]", + fontsize=LABEL_SIZE, + ) + ax.set_title("Effect of the faint-end slope", fontsize=TITLE_SIZE) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() + + +Double Schechter luminosity function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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. + +.. plot:: + :include-source: True + :width: 520 + + import numpy as np + import matplotlib.pyplot as plt + import cmasher as cmr + + from lfkit import LuminosityFunction + + LABEL_SIZE = 15 + TICK_SIZE = 13 + TITLE_SIZE = 17 + LEGEND_SIZE = 15 + + single = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + double = LuminosityFunction.double_schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + beta=-1.5, + m_transition=-19.5, + ) + + absolute_mag = np.linspace(-24.0, -14.0, 500) + colors = cmr.take_cmap_colors("cmr.guppy", 2, cmap_range=(0.15, 0.85)) + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + ax.plot( + absolute_mag, + single.phi(absolute_mag), + lw=3, + color=colors[0], + label="Schechter", + ) + ax.plot( + absolute_mag, + double.phi(absolute_mag), + lw=3, + color=colors[1], + label="Double Schechter", + ) + + ax.set_yscale("log") + ax.invert_xaxis() + ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE) + ax.set_ylabel( + r"$\Phi(M)$ [$\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1}$]", + fontsize=LABEL_SIZE, + ) + ax.set_title("Schechter and double-Schechter models", fontsize=TITLE_SIZE) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() + + +Evolving Schechter luminosity function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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. + +.. plot:: + :include-source: True + :width: 520 + + import numpy as np + import matplotlib.pyplot as plt + import cmasher as cmr + + from lfkit import LuminosityFunction + + 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}, + ) + + 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)) + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + + for z_value, color in zip(redshifts, colors): + phi = lf.phi(absolute_mag, z_value) + ax.plot( + absolute_mag, + phi, + lw=3, + color=color, + label=rf"$z={z_value}$", + ) + + ax.set_yscale("log") + ax.invert_xaxis() + ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE) + ax.set_ylabel( + r"$\Phi(M, z)$ [$\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1}$]", + fontsize=LABEL_SIZE, + ) + ax.set_title("Evolving Schechter luminosity function", fontsize=TITLE_SIZE) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() + + +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 +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. + +.. plot:: + :include-source: True + :width: 560 + + import numpy as np + import matplotlib.pyplot as plt + import cmasher as cmr + + from lfkit import LuminosityFunction + + 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}, + ) + + absolute_mag = np.linspace(-24.0, -14.0, 500) + redshifts = [0.1, 0.6, 1.1] + + reference_redshift = 0.6 + luminosity_distance_mpc = { + 0.1: 460.0, + 0.6: 3500.0, + 1.1: 7600.0, + } + 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, + ) + + 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): + phi = lf.phi(absolute_mag, z_value) + ax.plot( + absolute_mag, + phi, + lw=3, + color=color, + label=rf"$z={z_value}$", + ) + + secax = ax.secondary_xaxis( + "top", + functions=(absolute_to_apparent, apparent_to_absolute), + ) + + ax.set_yscale("log") + ax.invert_xaxis() + ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE) + ax.set_ylabel( + r"$\Phi(M, z)$ [$\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1}$]", + fontsize=LABEL_SIZE, + ) + ax.set_title( + "Evolving Schechter luminosity function with apparent-magnitude axis", + fontsize=TITLE_SIZE, + ) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + + secax.set_xlabel( + rf"Apparent magnitude $m$ at $z={reference_redshift}$", + fontsize=LABEL_SIZE, + ) + secax.tick_params(axis="x", labelsize=TICK_SIZE) + + plt.tight_layout() + + +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. + +.. plot:: + :include-source: True + :width: 520 + + import numpy as np + import matplotlib.pyplot as plt + import cmasher as cmr + + from lfkit import LuminosityFunction + + 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}, + ) + + 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)) + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + ax.plot( + redshift, + phi_star / 1.0e-3, + lw=3, + color=colors[0], + label=r"$\phi_*/10^{-3}$", + ) + ax.plot( + redshift, + m_star, + lw=3, + color=colors[1], + label=r"$M_*$", + ) + ax.plot( + redshift, + alpha, + lw=3, + color=colors[2], + label=r"$\alpha$", + ) + + 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() + + +Evolving Schechter surface +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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. + +.. plot:: + :include-source: True + :width: 560 + + import numpy as np + import matplotlib.pyplot as plt + + from lfkit import LuminosityFunction + + LABEL_SIZE = 15 + TICK_SIZE = 13 + TITLE_SIZE = 17 + + 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}, + ) + + absolute_mag = np.linspace(-24.0, -16.0, 220) + redshift = np.linspace(0.0, 1.5, 180) + mag_grid, z_grid = np.meshgrid(absolute_mag, redshift) + + phi = lf.phi(mag_grid, z_grid) + 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", + ) + + contour_levels = [-5.0, -4.0, -3.0, -2.0] + contours = ax.contour( + absolute_mag, + redshift, + log_phi, + levels=contour_levels, + colors="white", + linewidths=1.2, + ) + ax.clabel(contours, inline=True, fontsize=TICK_SIZE, fmt=r"$10^{%.0f}$") + + ax.invert_xaxis() + ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE) + ax.set_ylabel("Redshift $z$", fontsize=LABEL_SIZE) + ax.set_title("Evolving Schechter LF surface", fontsize=TITLE_SIZE) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + + cbar = fig.colorbar(mesh, ax=ax) + cbar.set_label( + r"$\log_{10}\Phi(M, z)$ " + r"[$\log_{10}(\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1})$]", + fontsize=LABEL_SIZE, + ) + cbar.ax.tick_params(labelsize=TICK_SIZE) + + plt.tight_layout() + + +Other luminosity function parametrizations +------------------------------------------ + +Additional luminosity function parametrizations can be added here as they are +implemented in the public API. + +Examples may include Saunders or modified-Schechter models, double-power-law +forms, lognormal-inspired parametrizations, or other survey-specific luminosity +function models. This section is intentionally kept as a placeholder so the +page can grow beyond the Schechter family without mixing all models under one +flat heading structure. + + +Available models +---------------- + +The API can report the registered luminosity function models and parameter +models. This is useful for examples, validation, and interactive exploration. + +.. code-block:: python + + from lfkit import LuminosityFunction + + LuminosityFunction.available_models() + LuminosityFunction.available_from_m_models() + LuminosityFunction.available_parameter_models() diff --git a/docs/examples/magnitude_integrals.rst b/docs/examples/magnitude_integrals.rst new file mode 100644 index 0000000..e2a5659 --- /dev/null +++ b/docs/examples/magnitude_integrals.rst @@ -0,0 +1,385 @@ +.. |lfkitlogo| image:: /_static/logos/lfkit_logo-icon.png + :alt: LFKit logo + :width: 50px + +|lfkitlogo| Luminosity-function magnitude integrals +=================================================== + +This page shows how to integrate a bound +:class:`lfkit.LuminosityFunction` over absolute magnitude. + +The examples use the ``lf.integrals`` namespace. These methods insert the +luminosity function callable internally, so users only provide the redshift +values, magnitude limits, and any optional weighting functions. + +Magnitude integrals are useful when a luminosity function is used to predict +number densities, luminosity-weighted summaries, or selected fractions over a +finite magnitude range. + +The number-density units follow the normalization supplied to the luminosity +function. For example, if ``phi_star`` is supplied in +:math:`{\rm Mpc}^{-3}\,{\rm mag}^{-1}`, then magnitude-integrated number +densities have units of :math:`{\rm Mpc}^{-3}`. + + +Integrated number density +------------------------- + +The integrated number density is the luminosity function integrated over a +finite absolute-magnitude range. + +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. + +.. plot:: + :include-source: True + :width: 520 + + import numpy as np + import matplotlib.pyplot as plt + import cmasher as cmr + + from lfkit import LuminosityFunction + + 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}, + ) + + redshift = np.linspace(0.05, 1.5, 180) + + n_bright = lf.integrals.number_density( + redshift, + m_bright=-24.0, + m_faint=-20.0, + n_m=800, + ) + + n_total = lf.integrals.number_density( + redshift, + m_bright=-24.0, + m_faint=-16.0, + n_m=800, + ) + + colors = cmr.take_cmap_colors("cmr.guppy", 2, cmap_range=(0.2, 0.9)) + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + ax.plot( + redshift, + n_bright, + lw=3, + color=colors[0], + label=r"$-24 \leq M \leq -20$", + ) + ax.plot( + redshift, + n_total, + lw=3, + color=colors[1], + label=r"$-24 \leq M \leq -16$", + ) + + ax.set_yscale("log") + 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) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() + + +Cumulative number density +------------------------- + +The number density can also be viewed as a cumulative function of the faint +absolute-magnitude limit. + +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. + +.. plot:: + :include-source: True + :width: 520 + + import numpy as np + import matplotlib.pyplot as plt + import cmasher as cmr + + from lfkit import LuminosityFunction + + LABEL_SIZE = 15 + TICK_SIZE = 13 + TITLE_SIZE = 17 + + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + magnitude_limits = np.linspace(-23.0, -15.0, 120) + + number_density = lf.integrals.number_density( + 0.0, + m_bright=-25.0, + m_faint=magnitude_limits, + n_m=800, + ) + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + ax.plot( + magnitude_limits, + number_density, + lw=3, + color=cmr.take_cmap_colors("cmr.guppy", 1, cmap_range=(0.72, 0.9))[0], + ) + + ax.set_yscale("log") + ax.invert_xaxis() + ax.set_xlabel( + r"Faint absolute-magnitude limit $M_{\rm faint}$", + fontsize=LABEL_SIZE, + ) + ax.set_ylabel( + r"$n(M < M_{\rm faint})$ [$\mathrm{Mpc}^{-3}$]", + fontsize=LABEL_SIZE, + ) + ax.set_title("Cumulative LF number density", fontsize=TITLE_SIZE) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + plt.tight_layout() + + +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. + +The curves below are normalized by their first redshift value to emphasize the +relative redshift trend rather than the absolute normalization. + +.. plot:: + :include-source: True + :width: 520 + + import numpy as np + import matplotlib.pyplot as plt + import cmasher as cmr + + from lfkit import LuminosityFunction + + 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}, + ) + + redshift = np.linspace(0.05, 1.5, 180) + + 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, + ) + + colors = cmr.take_cmap_colors("cmr.guppy", 2, cmap_range=(0.2, 0.9)) + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + ax.plot( + redshift, + luminosity_density / luminosity_density[0], + lw=3, + color=colors[0], + label="Luminosity density", + ) + ax.plot( + redshift, + mean_luminosity / mean_luminosity[0], + lw=3, + color=colors[1], + label="Mean luminosity", + ) + + ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) + ax.set_ylabel("Value relative to first redshift bin", fontsize=LABEL_SIZE) + ax.set_title("Luminosity-weighted LF summaries", fontsize=TITLE_SIZE) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() + + +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. + +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. + +.. plot:: + :include-source: True + :width: 520 + + import numpy as np + import matplotlib.pyplot as plt + import cmasher as cmr + + from lfkit import LuminosityFunction + + 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}, + ) + + 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)) + + n_total = lf.integrals.number_density( + redshift, + m_bright=-24.0, + m_faint=-14.0, + n_m=800, + ) + + n_selected = lf.integrals.selection_weighted_number_density( + redshift, + selection_fn=soft_selection, + m_bright=-24.0, + m_faint=-14.0, + n_m=800, + ) + + colors = cmr.take_cmap_colors("cmr.guppy", 2, cmap_range=(0.2, 0.9)) + + 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_selected, + lw=3, + color=colors[1], + label="Selection weighted", + ) + + ax.set_yscale("log") + 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) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() + + +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. + +.. plot:: + :include-source: True + :width: 520 + + import numpy as np + import matplotlib.pyplot as plt + import cmasher as cmr + + from lfkit import LuminosityFunction + + LABEL_SIZE = 15 + TICK_SIZE = 13 + TITLE_SIZE = 17 + + 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}, + ) + + 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)) + + n_total = lf.integrals.number_density( + redshift, + m_bright=-24.0, + m_faint=-14.0, + n_m=800, + ) + + 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 = n_selected / n_total + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + ax.plot( + redshift, + selected_fraction, + lw=3, + color=cmr.take_cmap_colors("cmr.guppy", 1, cmap_range=(0.72, 0.9))[0], + ) + + ax.set_ylim(-0.05, 1.05) + 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.tick_params(axis="both", labelsize=TICK_SIZE) + plt.tight_layout() diff --git a/docs/examples/magnitudes_and_luminosities.rst b/docs/examples/magnitudes_and_luminosities.rst new file mode 100644 index 0000000..544d698 --- /dev/null +++ b/docs/examples/magnitudes_and_luminosities.rst @@ -0,0 +1,211 @@ +.. |lfkitlogo| image:: /_static/logos/lfkit_logo-icon.png + :alt: LFKit logo + :width: 50px + +|lfkitlogo| Magnitudes and luminosities +======================================= + +This page shows magnitude and luminosity helper examples for +:class:`lfkit.LuminosityFunction`. + +The examples use the ``lf.magnitudes`` and ``lf.luminosities`` namespaces, plus +:meth:`lfkit.LuminosityFunction.phi_from_m` for evaluating a luminosity function +from apparent magnitude. + +These helpers are useful when a workflow needs to connect apparent magnitude, +absolute magnitude, luminosity distance, and luminosity ratios before applying +luminosity function calculations. + +Completeness calculations and survey magnitude limits are covered on a separate +page. + + +LF from apparent magnitude +-------------------------- + +For models with apparent-magnitude support, +:meth:`lfkit.LuminosityFunction.phi_from_m` converts apparent magnitude to +absolute magnitude and evaluates the luminosity function. + +This is useful when the natural input is an observed apparent magnitude rather +than an intrinsic absolute magnitude. + +.. plot:: + :include-source: True + :width: 520 + + import numpy as np + import matplotlib.pyplot as plt + import cmasher as cmr + import pyccl as ccl + + from lfkit import LuminosityFunction + + LABEL_SIZE = 15 + TICK_SIZE = 13 + TITLE_SIZE = 17 + LEGEND_SIZE = 15 + + cosmo = ccl.Cosmology( + Omega_c=0.25, + Omega_b=0.05, + h=0.7, + sigma8=0.8, + n_s=0.96, + transfer_function="bbks", + matter_power_spectrum="linear", + ) + + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + 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)) + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + + for z_value, color in zip(redshifts, colors): + phi = lf.phi_from_m( + cosmo, + z_value, + apparent_mag, + h=0.7, + ) + ax.plot(apparent_mag, phi, lw=3, color=color, label=rf"$z={z_value}$") + + ax.set_yscale("log") + ax.set_xlabel("Apparent magnitude $m$", fontsize=LABEL_SIZE) + ax.set_ylabel( + r"$\Phi[m \rightarrow M(m,z)]$ [$\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1}$]", + fontsize=LABEL_SIZE, + ) + ax.set_title("LF evaluated from apparent magnitude", fontsize=TITLE_SIZE) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() + + +Magnitude conversions +--------------------- + +The ``magnitudes`` namespace provides helpers for converting between apparent +and absolute magnitude. + +The luminosity-distance versions are useful when a workflow already has a +distance array from another cosmology backend. + +.. plot:: + :include-source: True + :width: 520 + + import numpy as np + import matplotlib.pyplot as plt + import cmasher as cmr + + from lfkit import LuminosityFunction + + LABEL_SIZE = 15 + TICK_SIZE = 13 + TITLE_SIZE = 17 + LEGEND_SIZE = 15 + + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + luminosity_distance_mpc = np.linspace(100.0, 5000.0, 300) + + absolute_mag = lf.magnitudes.absolute_from_luminosity_distance( + 24.0, + luminosity_distance_mpc, + ) + + apparent_mag = lf.magnitudes.apparent_from_luminosity_distance( + -20.5, + luminosity_distance_mpc, + ) + + colors = cmr.take_cmap_colors("cmr.guppy", 2, cmap_range=(0.2, 0.9)) + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + ax.plot( + luminosity_distance_mpc, + absolute_mag, + lw=3, + color=colors[0], + label=r"$M(m=24, d_L)$", + ) + ax.plot( + luminosity_distance_mpc, + apparent_mag, + lw=3, + color=colors[1], + label=r"$m(M=-20.5, d_L)$", + ) + + ax.invert_yaxis() + ax.set_xlabel(r"Luminosity distance $d_L$ [Mpc]", fontsize=LABEL_SIZE) + ax.set_ylabel("Magnitude", fontsize=LABEL_SIZE) + ax.set_title("Magnitude conversions", fontsize=TITLE_SIZE) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() + + +Luminosity ratio from magnitudes +-------------------------------- + +The ``luminosities`` namespace exposes helper functions for magnitude and +luminosity-ratio calculations. + +This example converts absolute magnitude into the luminosity ratio +:math:`L/L_*`. Brighter galaxies have larger luminosity ratios, so the curve +increases toward more negative absolute magnitudes. + +.. plot:: + :include-source: True + :width: 520 + + import numpy as np + import matplotlib.pyplot as plt + import cmasher as cmr + + from lfkit import LuminosityFunction + + LABEL_SIZE = 15 + TICK_SIZE = 13 + TITLE_SIZE = 17 + + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + absolute_mag = np.linspace(-24.0, -16.0, 300) + luminosity_ratio = lf.luminosities.ratio_from_magnitudes( + absolute_mag, + -20.5, + ) + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + ax.plot( + absolute_mag, + luminosity_ratio, + lw=3, + color=cmr.take_cmap_colors("cmr.guppy", 1, cmap_range=(0.72, 0.9))[0], + ) + + ax.set_yscale("log") + ax.invert_xaxis() + ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE) + ax.set_ylabel(r"Luminosity ratio $L/L_*$", fontsize=LABEL_SIZE) + ax.set_title("Luminosity ratio from magnitudes", fontsize=TITLE_SIZE) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + plt.tight_layout() diff --git a/docs/examples/model_registry.rst b/docs/examples/model_registry.rst new file mode 100644 index 0000000..1e7fe4a --- /dev/null +++ b/docs/examples/model_registry.rst @@ -0,0 +1,63 @@ +.. |lfkitlogo| image:: /_static/logos/lfkit_logo-icon.png + :alt: LFKit logo + :width: 50px + +|lfkitlogo| Available luminosity function models +================================================ + +LFKit exposes registered model names so users can inspect which luminosity +functions and redshift-dependent parameter models are available from the public +API. + +This is useful in notebooks, examples, and tests because it lets users discover +valid model names without looking through the implementation modules. + + +Available luminosity function models +------------------------------------ + +.. code-block:: python + + from lfkit import LuminosityFunction + + LuminosityFunction.available_models() + + +Available apparent-magnitude models +----------------------------------- + +.. code-block:: python + + from lfkit import LuminosityFunction + + LuminosityFunction.available_from_m_models() + + +Available parameter models +-------------------------- + +.. code-block:: python + + from lfkit import LuminosityFunction + + LuminosityFunction.available_parameter_models() + + +Typical use +----------- + +The registered names can be passed to constructors such as +:meth:`lfkit.LuminosityFunction.evolving_schechter`. + +.. code-block:: python + + from lfkit import LuminosityFunction + + 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}, + ) diff --git a/docs/examples/redshift_density.rst b/docs/examples/redshift_density.rst new file mode 100644 index 0000000..8778e3f --- /dev/null +++ b/docs/examples/redshift_density.rst @@ -0,0 +1,246 @@ +.. |lfkitlogo| image:: /_static/logos/lfkit_logo-icon.png + :alt: LFKit logo + :width: 50px + +|lfkitlogo| LF-weighted redshift density +======================================== + +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 +weight. + +This is not required to be a complete survey :math:`n(z)` by itself. It is the +LF-dependent ingredient that can be combined with survey geometry, volume +weights, or tomography code. + +All examples below are executable via ``.. plot::``. + + +Magnitude-limited LF redshift density +------------------------------------- + +The integrated version computes the luminosity function number density selected +by an apparent-magnitude limit. + +.. plot:: + :include-source: True + :width: 520 + + import numpy as np + import matplotlib.pyplot as plt + import cmasher as cmr + + from lfkit import LuminosityFunction + + 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}, + ) + + redshift = np.linspace(0.05, 1.5, 180) + + def luminosity_distance_mpc(z): + 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)) + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + + for m_lim, color in zip(limits, colors): + number_density = lf.redshift_density.integrated_number_density( + redshift, + m_lim=m_lim, + m_bright=-24.0, + luminosity_distance_mpc_fn=luminosity_distance_mpc, + n_m=800, + ) + + number_density /= np.trapezoid(number_density, redshift) + + ax.plot( + redshift, + number_density, + lw=3, + color=color, + label=rf"$m_{{\rm lim}}={m_lim}$", + ) + + ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) + ax.set_ylabel("Normalized LF selection", fontsize=LABEL_SIZE) + ax.set_title("Magnitude-limited LF redshift density", fontsize=TITLE_SIZE) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() + + +LF-weighted redshift trend +-------------------------- + +The weighted version multiplies the magnitude-integrated luminosity function by +a user-provided redshift or volume weight. + +.. plot:: + :include-source: True + :width: 520 + + import numpy as np + import matplotlib.pyplot as plt + import cmasher as cmr + + from lfkit import LuminosityFunction + + 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}, + ) + + redshift = np.linspace(0.05, 1.5, 180) + + def luminosity_distance_mpc(z): + return 3000.0 * z * (1.0 + 0.5 * z) + + def volume_weight(z): + return z**2 * np.exp(-z / 0.5) + + 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, + ) + + 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, + ) + + colors = cmr.take_cmap_colors("cmr.guppy", 2, cmap_range=(0.2, 0.9)) + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + ax.plot( + redshift, + number_density / np.max(number_density), + lw=3, + color=colors[0], + label="Magnitude-limited LF integral", + ) + ax.plot( + redshift, + weighted_density / np.max(weighted_density), + lw=3, + color=colors[1], + label="LF-weighted redshift density", + ) + + ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) + ax.set_ylabel("Normalized density", fontsize=LABEL_SIZE) + ax.set_title("LF redshift density with volume weighting", fontsize=TITLE_SIZE) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() + + +Cosmology-style volume weighting +-------------------------------- + +A common survey ingredient is a volume-like weight. The example below uses a +simple callable to keep the redshift-density API independent of any specific +cosmology backend. + +.. plot:: + :include-source: True + :width: 520 + + import numpy as np + import matplotlib.pyplot as plt + import cmasher as cmr + + from lfkit import LuminosityFunction + + 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.6}, + 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}, + ) + + redshift = np.linspace(0.05, 1.8, 220) + + def luminosity_distance_mpc(z): + return 3000.0 * z * (1.0 + 0.4 * z) + + def low_z_volume_weight(z): + return z**2 + + def tapered_volume_weight(z): + return z**2 * np.exp(-z / 0.8) + + low_z_trend = lf.redshift_density.weighted( + redshift, + m_lim=24.5, + m_bright=-24.0, + luminosity_distance_mpc_fn=luminosity_distance_mpc, + volume_weight_fn=low_z_volume_weight, + n_m=800, + ) + + tapered_trend = lf.redshift_density.weighted( + redshift, + m_lim=24.5, + m_bright=-24.0, + luminosity_distance_mpc_fn=luminosity_distance_mpc, + volume_weight_fn=tapered_volume_weight, + n_m=800, + ) + + 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)) + + 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.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) + ax.set_ylabel("Normalized weighted trend", fontsize=LABEL_SIZE) + ax.set_title("Effect of redshift weighting", fontsize=TITLE_SIZE) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() diff --git a/src/lfkit/api/_clf_models.py b/src/lfkit/api/_clf_models.py new file mode 100644 index 0000000..e69de29 diff --git a/src/lfkit/api/_completeness.py b/src/lfkit/api/_completeness.py new file mode 100644 index 0000000..e69de29 diff --git a/src/lfkit/api/_expose.py b/src/lfkit/api/_expose.py new file mode 100644 index 0000000..e69de29 diff --git a/src/lfkit/api/_integrals.py b/src/lfkit/api/_integrals.py new file mode 100644 index 0000000..e69de29 diff --git a/src/lfkit/api/_lf_param_models.py b/src/lfkit/api/_lf_param_models.py new file mode 100644 index 0000000..d80c8bc --- /dev/null +++ b/src/lfkit/api/_lf_param_models.py @@ -0,0 +1,77 @@ +"""Luminosity-function model registries used by the public API.""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Any, TypedDict + +from lfkit.photometry.conditional_lf_models import ( + conditional_schechter, + conditional_schechter_double, + conditional_schechter_evolving, + lognormal_conditional_lf, + modified_schechter_conditional_lf, + two_component_conditional_lf, +) +from lfkit.photometry.luminosity_function import ( + schechter, + schechter_double, + schechter_double_from_m, + schechter_evolving, + schechter_evolving_from_m, + schechter_from_m, +) + + +class LFModelSpec(TypedDict): + """Description of a luminosity-function model exposed by the API.""" + + function: Callable[..., Any] + requires_z: bool + + +LF_MODELS: dict[str, LFModelSpec] = { + "schechter": { + "function": schechter, + "requires_z": False, + }, + "evolving_schechter": { + "function": schechter_evolving, + "requires_z": True, + }, + "double_schechter": { + "function": schechter_double, + "requires_z": False, + }, + "conditional_schechter": { + "function": conditional_schechter, + "requires_z": True, + }, + "conditional_evolving_schechter": { + "function": conditional_schechter_evolving, + "requires_z": True, + }, + "conditional_double_schechter": { + "function": conditional_schechter_double, + "requires_z": True, + }, + "central_lognormal_conditional": { + "function": lognormal_conditional_lf, + "requires_z": True, + }, + "satellite_modified_schechter_conditional": { + "function": modified_schechter_conditional_lf, + "requires_z": True, + }, + "central_satellite_conditional": { + "function": two_component_conditional_lf, + "requires_z": True, + }, +} + + +LF_FROM_M_MODELS: dict[str, Callable[..., Any]] = { + "schechter": schechter_from_m, + "evolving_schechter": schechter_evolving_from_m, + "double_schechter": schechter_double_from_m, +} diff --git a/src/lfkit/api/_luminosities.py b/src/lfkit/api/_luminosities.py new file mode 100644 index 0000000..e69de29 diff --git a/src/lfkit/api/_magnitudes.py b/src/lfkit/api/_magnitudes.py new file mode 100644 index 0000000..e69de29 diff --git a/src/lfkit/api/_redshift_density.py b/src/lfkit/api/_redshift_density.py new file mode 100644 index 0000000..e69de29 diff --git a/src/lfkit/api/conditional_luminosity_function.py b/src/lfkit/api/conditional_luminosity_function.py new file mode 100644 index 0000000..e69de29 diff --git a/src/lfkit/api/lumfunc.py b/src/lfkit/api/lumfunc.py deleted file mode 100644 index 8f35741..0000000 --- a/src/lfkit/api/lumfunc.py +++ /dev/null @@ -1,1109 +0,0 @@ -r"""Public luminosity function interface. - -This module provides the user-facing :class:`LuminosityFunction` API for -evaluating luminosity functions in absolute- or apparent-magnitude space. - -The class wraps the lower-level LFKit photometry functions behind a small, -stable interface. Users can construct standard, evolving, or double Schechter -models, evaluate :math:`\phi(M, z)`, evaluate from apparent magnitudes, and -compute integrated, observed, and missing number densities for -magnitude-limited catalog selections. - -File reading is intentionally not handled here. Catalog-derived LF parameters, -magnitude limits, or correction models should be loaded elsewhere and passed -into this API as scalars, arrays, or correction objects. -""" - -from __future__ import annotations - -from collections.abc import Callable, Mapping -from typing import TYPE_CHECKING - -import numpy as np - -from lfkit.photometry.luminosity_function import ( - schechter, - schechter_evolving, - schechter_double, - schechter_from_m, - schechter_evolving_from_m, - schechter_double_from_m, -) -from lfkit.photometry.conditional_lf_models import ( - conditional_schechter, - conditional_schechter_double, - conditional_schechter_evolving, - lognormal_conditional_lf, - modified_schechter_conditional_lf, - two_component_conditional_lf, -) -from lfkit.photometry.magnitudes import ( - absolute_magnitude, - absolute_magnitude_from_luminosity_distance, - apparent_magnitude, - apparent_magnitude_from_luminosity_distance, -) -from lfkit.photometry.lf_integrals import ( - cumulative_number_density, - integrated_luminosity_density, - integrated_number_density, - lf_weighted_integral, - mean_luminosity, - selection_weighted_number_density, -) -from lfkit.photometry.lf_parameter_models import ( - available_lf_parameter_models, - evaluate_lf_parameters, - register_alpha_model, - register_m_star_model, - register_phi_star_model, -) -from lfkit.photometry.lf_redshift_density import ( - lf_integrated_number_density, - lf_weighted_redshift_density, -) -from lfkit.photometry.catalog_completeness import ( - absolute_magnitude_limit, - catalog_completeness_fraction, - out_of_catalog_fraction, - observed_number_density, - missing_number_density, -) -from lfkit.utils.types import ( - Cosmology, - FloatArray, - FloatInput, - ParameterModel, - ParameterValue, - ConditionalParameter, -) - -if TYPE_CHECKING: - from lfkit.api.corrections import Corrections -else: - Corrections = object - - -__all__ = ["LuminosityFunction"] - - -class LuminosityFunction: - """User-facing wrapper for luminosity function evaluation.""" - - def __init__( - self, - *, - model: str, - parameters: Mapping[str, object], - meta: Mapping[str, object] | None = None, - ) -> None: - """Store a luminosity function model and its parameters. - - Args: - 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. - """ - self.model = str(model) - self.parameters_dict = dict(parameters) - self.meta = {} if meta is None else dict(meta) - - def absolute_magnitude( - self, - cosmo_obj: Cosmology, - z: FloatInput, - apparent_mag: FloatInput, - *, - h: float | None = None, - corrections: Corrections | None = None, - ) -> FloatArray: - """Convert apparent magnitude to absolute magnitude. - - Args: - cosmo_obj: Cosmology object used for distance-modulus conversion. - z: Redshift values. - apparent_mag: Apparent magnitude values. - h: Optional reduced Hubble parameter used in the magnitude conversion. - corrections: Optional object providing k-correction and e-correction values. - - Returns: - Absolute magnitudes using the LFKit convention - ``M = m - mu - K + E``. - """ - k_corr, e_corr = self._correction_values(corrections, z) - - return absolute_magnitude( - cosmo_obj, - z, - apparent_mag, - h=h, - k_correction=k_corr, - e_correction=e_corr, - ) - - def apparent_magnitude( - self, - cosmo_obj: Cosmology, - z: FloatInput, - absolute_mag: FloatInput, - *, - h: float | None = None, - corrections: Corrections | None = None, - ) -> FloatArray: - """Convert absolute magnitude to apparent magnitude. - - Args: - cosmo_obj: Cosmology object used for distance-modulus conversion. - z: Redshift values. - absolute_mag: Absolute magnitude values. - h: Optional reduced Hubble parameter used in the magnitude conversion. - corrections: Optional object providing k-correction and e-correction values. - - Returns: - Apparent magnitudes using the LFKit convention - ``m = M + mu + K - E``. - """ - k_corr, e_corr = self._correction_values(corrections, z) - - return apparent_magnitude( - cosmo_obj, - z, - absolute_mag, - h=h, - k_correction=k_corr, - e_correction=e_corr, - ) - - def absolute_magnitude_from_luminosity_distance( - self, - apparent_mag: FloatInput, - luminosity_distance_mpc: FloatInput, - *, - z: FloatInput, - corrections: Corrections | None = None, - ) -> FloatArray: - """Convert apparent magnitude to absolute magnitude from luminosity distance. - - Args: - apparent_mag: Apparent magnitude values. - luminosity_distance_mpc: Luminosity distance values in Mpc. - corrections: Optional object providing k-correction and e-correction values. - z: Redshift values where corrections are evaluated. - - Returns: - Absolute magnitudes using ``M = m - mu - K + E``. - """ - k_corr, e_corr = self._correction_values(corrections, z) - - return absolute_magnitude_from_luminosity_distance( - apparent_mag, - luminosity_distance_mpc, - k_correction=k_corr, - e_correction=e_corr, - ) - - def apparent_magnitude_from_luminosity_distance( - self, - absolute_mag: FloatInput, - luminosity_distance_mpc: FloatInput, - *, - z: FloatInput, - corrections: Corrections | None = None, - ) -> FloatArray: - """Convert absolute magnitude to apparent magnitude from luminosity distance. - - Args: - absolute_mag: Absolute magnitude values. - luminosity_distance_mpc: Luminosity distance values in Mpc. - z: Redshift values where corrections are evaluated. - corrections: Optional object providing k-correction and e-correction values. - - Returns: - Apparent magnitudes using ``m = M + mu + K - E``. - """ - k_corr, e_corr = self._correction_values(corrections, z) - - return apparent_magnitude_from_luminosity_distance( - absolute_mag, - luminosity_distance_mpc, - k_correction=k_corr, - e_correction=e_corr, - ) - - def lf_integrated_number_density( - self, - z: FloatInput, - *, - m_lim: float, - m_bright: float, - n_m: int = 512, - luminosity_distance_mpc_fn: Callable[[FloatArray], FloatArray], - corrections: Corrections | None = None, - ) -> FloatArray: - """Return LF-integrated number density for an apparent-magnitude limit. - - This integrates the luminosity function from ``m_bright`` to the - absolute-magnitude limit implied by ``m_lim`` and the supplied - luminosity-distance callable. - - Args: - z: Redshift values. - m_lim: Apparent-magnitude limit of the catalog. - m_bright: Bright absolute-magnitude integration bound. - n_m: Number of magnitude-grid points used in the integration. - luminosity_distance_mpc_fn: Callable returning luminosity distance - in Mpc as a function of redshift. - corrections: Optional object providing k-correction and - e-correction values. - - Returns: - LF-integrated number density evaluated at redshift. - """ - k_corr, e_corr = self._correction_values(corrections, z) - - return lf_integrated_number_density( - z, - self._as_callable(), - m_lim=m_lim, - m_bright=m_bright, - n_m=n_m, - luminosity_distance_mpc_fn=luminosity_distance_mpc_fn, - k_correction=k_corr, - evolution_correction=e_corr, - ) - - def lf_weighted_redshift_density( - self, - z: FloatInput, - *, - m_lim: float, - m_bright: float, - n_m: int = 512, - luminosity_distance_mpc_fn: Callable[[FloatArray], FloatArray], - volume_weight_fn: Callable[[FloatArray], FloatArray], - corrections: Corrections | None = None, - normalize: bool = True, - ) -> FloatArray: - """Return an LF-weighted redshift-density curve. - - This computes an LF-selected redshift distribution by integrating the - luminosity function up to the apparent-magnitude limit and multiplying - by a supplied redshift or volume weight. - - Args: - z: Redshift values. - m_lim: Apparent-magnitude limit of the catalog. - m_bright: Bright absolute-magnitude integration bound. - n_m: Number of magnitude-grid points used in the integration. - luminosity_distance_mpc_fn: Callable returning luminosity distance - in Mpc as a function of redshift. - volume_weight_fn: Callable returning the redshift or volume weight. - corrections: Optional object providing k-correction and - e-correction values. - normalize: If True, normalize the returned curve to integrate to - one over redshift. - - Returns: - LF-weighted redshift-density curve. - """ - k_corr, e_corr = self._correction_values(corrections, z) - - return lf_weighted_redshift_density( - z, - self._as_callable(), - m_lim=m_lim, - m_bright=m_bright, - n_m=n_m, - luminosity_distance_mpc_fn=luminosity_distance_mpc_fn, - volume_weight_fn=volume_weight_fn, - k_correction=k_corr, - evolution_correction=e_corr, - normalize=normalize, - ) - - @classmethod - def schechter( - cls, - *, - phi_star: ParameterValue, - m_star: ParameterValue, - alpha: ParameterValue, - ) -> "LuminosityFunction": - """Create a standard Schechter luminosity function. - - Args: - phi_star: Normalization of the luminosity function. - m_star: Characteristic absolute magnitude. - alpha: Faint-end slope. - - Returns: - Luminosity-function API object using the standard Schechter model. - """ - return cls( - model="schechter", - parameters={ - "phi_star": phi_star, - "m_star": m_star, - "alpha": alpha, - }, - ) - - @classmethod - def evolving_schechter( - cls, - *, - phi_model: str = "linear_p", - phi_kwargs: Mapping[str, ParameterValue] | None = None, - m_star_model: str = "linear_q", - m_star_kwargs: Mapping[str, ParameterValue] | None = None, - alpha_model: str = "constant", - alpha_kwargs: Mapping[str, ParameterValue] | None = None, - ) -> "LuminosityFunction": - """Create a redshift-evolving Schechter luminosity function. - - Args: - phi_model: Parameter model used for the normalization evolution. - phi_kwargs: Keyword arguments for the normalization model. - m_star_model: Parameter model used for characteristic-magnitude evolution. - m_star_kwargs: Keyword arguments for the characteristic-magnitude model. - alpha_model: Parameter model used for faint-end-slope evolution. - alpha_kwargs: Keyword arguments for the faint-end-slope model. - - Returns: - Luminosity-function API object using an evolving Schechter model. - """ - return cls( - model="evolving_schechter", - parameters={ - "phi_model": phi_model, - "phi_kwargs": {} if phi_kwargs is None else dict(phi_kwargs), - "m_star_model": m_star_model, - "m_star_kwargs": {} if m_star_kwargs is None else dict(m_star_kwargs), - "alpha_model": alpha_model, - "alpha_kwargs": {} if alpha_kwargs is None else dict(alpha_kwargs), - }, - ) - - @classmethod - def double_schechter( - cls, - *, - phi_star: ParameterValue, - m_star: ParameterValue, - alpha: float, - beta: float, - m_transition: ParameterValue, - ) -> "LuminosityFunction": - """Create a double-power-law Schechter luminosity function. - - Args: - phi_star: Normalization of the luminosity function. - m_star: Characteristic absolute magnitude. - alpha: Bright-end or main Schechter slope. - beta: Additional slope controlling the second power-law component. - m_transition: Transition magnitude for the second component. - - Returns: - Luminosity-function API object using the double Schechter model. - """ - return cls( - model="double_schechter", - parameters={ - "phi_star": phi_star, - "m_star": m_star, - "alpha": alpha, - "beta": beta, - "m_transition": m_transition, - }, - ) - - def phi( - self, - absolute_mag: FloatInput, - z: FloatInput | None = None, - ) -> FloatArray: - """Evaluate the luminosity function in absolute-magnitude space. - - Args: - absolute_mag: Absolute magnitude values where the LF is evaluated. - z: Redshift values. Required for evolving and conditional models. - - Returns: - Luminosity-function values evaluated at the input magnitudes. - """ - if self.model == "schechter": - return schechter( - np.asarray(absolute_mag, dtype=float), - **self.parameters_dict, - ) - - if self.model == "evolving_schechter": - if z is None: - raise ValueError("z is required for an evolving luminosity function.") - - return schechter_evolving( - np.asarray(absolute_mag, dtype=float), - np.asarray(z, dtype=float), - **self.parameters_dict, - ) - - if self.model == "double_schechter": - return schechter_double( - np.asarray(absolute_mag, dtype=float), - **self.parameters_dict, - ) - - if self.model == "conditional_schechter": - if z is None: - raise ValueError("z is required for a conditional luminosity function.") - - return conditional_schechter( - np.asarray(absolute_mag, dtype=float), - np.asarray(z, dtype=float), - **self.parameters_dict, - ) - - if self.model == "conditional_evolving_schechter": - if z is None: - raise ValueError("z is required for a conditional luminosity function.") - - return conditional_schechter_evolving( - np.asarray(absolute_mag, dtype=float), - np.asarray(z, dtype=float), - **self.parameters_dict, - ) - - if self.model == "conditional_double_schechter": - if z is None: - raise ValueError("z is required for a conditional luminosity function.") - - return conditional_schechter_double( - np.asarray(absolute_mag, dtype=float), - np.asarray(z, dtype=float), - **self.parameters_dict, - ) - - if self.model == "central_lognormal_conditional": - if z is None: - raise ValueError("z is required for a conditional luminosity function.") - - return lognormal_conditional_lf( - np.asarray(absolute_mag, dtype=float), - np.asarray(z, dtype=float), - **self.parameters_dict, - ) - - if self.model == "satellite_modified_schechter_conditional": - if z is None: - raise ValueError("z is required for a conditional luminosity function.") - - return modified_schechter_conditional_lf( - np.asarray(absolute_mag, dtype=float), - np.asarray(z, dtype=float), - **self.parameters_dict, - ) - - if self.model == "central_satellite_conditional": - if z is None: - raise ValueError("z is required for a conditional luminosity function.") - - return two_component_conditional_lf( - np.asarray(absolute_mag, dtype=float), - np.asarray(z, dtype=float), - **self.parameters_dict, - ) - - raise ValueError(f"Unsupported luminosity function model '{self.model}'.") - - def phi_from_m( - self, - cosmo_obj: Cosmology, - z: FloatInput, - apparent_mag: FloatInput, - *, - h: float | None = None, - corrections: Corrections | None = None, - ) -> FloatArray: - """Evaluate the luminosity function from apparent magnitudes. - - Apparent magnitudes are converted to absolute magnitudes using the - supplied cosmology, optional reduced Hubble parameter, and optional - k- and e-correction model. - - Args: - cosmo_obj: Cosmology object used for distance-modulus conversion. - z: Redshift values. - apparent_mag: Apparent magnitude values. - h: Optional reduced Hubble parameter used in the magnitude conversion. - corrections: Optional object providing k-correction and e-correction values. - - Returns: - Luminosity-function values evaluated from apparent magnitudes. - """ - k_corr, e_corr = self._correction_values(corrections, z) - - if self.model == "schechter": - return schechter_from_m( - cosmo_obj, - np.asarray(z, dtype=float), - np.asarray(apparent_mag, dtype=float), - h=h, - k_correction=k_corr, - e_correction=e_corr, - **self.parameters_dict, - ) - - if self.model == "evolving_schechter": - return schechter_evolving_from_m( - cosmo_obj, - np.asarray(z, dtype=float), - np.asarray(apparent_mag, dtype=float), - h=h, - k_correction=k_corr, - e_correction=e_corr, - **self.parameters_dict, - ) - - if self.model == "double_schechter": - return schechter_double_from_m( - cosmo_obj, - np.asarray(z, dtype=float), - np.asarray(apparent_mag, dtype=float), - h=h, - k_correction=k_corr, - e_correction=e_corr, - **self.parameters_dict, - ) - - raise ValueError(f"Unsupported luminosity function model '{self.model}'.") - - def parameters( - self, - z: FloatInput, - ) -> tuple[FloatArray, FloatArray, FloatArray]: - """Evaluate evolving Schechter parameters at redshift. - - Args: - z: Redshift values where the evolving LF parameters are evaluated. - - Returns: - Tuple containing ``phi_star(z)``, ``m_star(z)``, and ``alpha(z)``. - """ - if self.model != "evolving_schechter": - raise ValueError("parameters(z) is only defined for evolving_schechter.") - - return evaluate_lf_parameters( - np.asarray(z, dtype=float), - **self.parameters_dict, - ) - - def absolute_magnitude_limit( - self, - cosmo_obj: Cosmology, - z: FloatInput, - *, - m_lim: float, - h: float | None = None, - corrections: Corrections | None = None, - ) -> FloatArray: - """Return the absolute-magnitude limit of a catalog apparent-magnitude cut. - - Args: - cosmo_obj: Cosmology object used for distance-modulus conversion. - z: Redshift values. - m_lim: Apparent-magnitude limit of the catalog. - h: Optional reduced Hubble parameter used in the magnitude conversion. - corrections: Optional object providing k-correction and e-correction values. - - Returns: - Absolute-magnitude limits using the LFKit convention - ``M_lim = m_lim - mu - K + E``. - """ - k_corr, e_corr = self._correction_values(corrections, z) - - return absolute_magnitude_limit( - cosmo_obj, - z, - m_lim=m_lim, - h=h, - k_correction=k_corr, - e_correction=e_corr, - ) - - @classmethod - def conditional_schechter( - cls, - *, - phi_star: ConditionalParameter, - m_star: ConditionalParameter, - alpha: ConditionalParameter, - ) -> "LuminosityFunction": - """Create a conditional Schechter luminosity function.""" - return cls( - model="conditional_schechter", - parameters={ - "phi_star": phi_star, - "m_star": m_star, - "alpha": alpha, - }, - ) - - @classmethod - def conditional_evolving_schechter( - cls, - *, - phi_model: str = "linear_p", - phi_kwargs: Mapping[str, ParameterValue] | None = None, - m_star_model: str = "linear_q", - m_star_kwargs: Mapping[str, ParameterValue] | None = None, - alpha_model: str = "constant", - alpha_kwargs: Mapping[str, ParameterValue] | None = None, - ) -> "LuminosityFunction": - """Create a conditional Schechter LF using LF parameter models.""" - return cls( - model="conditional_evolving_schechter", - parameters={ - "phi_model": phi_model, - "phi_kwargs": {} if phi_kwargs is None else dict(phi_kwargs), - "m_star_model": m_star_model, - "m_star_kwargs": {} if m_star_kwargs is None else dict(m_star_kwargs), - "alpha_model": alpha_model, - "alpha_kwargs": {} if alpha_kwargs is None else dict(alpha_kwargs), - }, - ) - - @classmethod - def conditional_double_schechter( - cls, - *, - phi_star: ConditionalParameter, - m_star: ConditionalParameter, - alpha: float, - beta: float, - m_transition: ConditionalParameter, - ) -> "LuminosityFunction": - """Create a conditional double-power-law Schechter LF.""" - return cls( - model="conditional_double_schechter", - parameters={ - "phi_star": phi_star, - "m_star": m_star, - "alpha": alpha, - "beta": beta, - "m_transition": m_transition, - }, - ) - - @classmethod - def central_lognormal_conditional( - cls, - *, - mean_absolute_mag: ConditionalParameter, - sigma_log_luminosity: ConditionalParameter, - amplitude: ConditionalParameter = 1.0, - ) -> "LuminosityFunction": - """Create a central-galaxy lognormal conditional LF.""" - return cls( - model="central_lognormal_conditional", - parameters={ - "mean_absolute_mag": mean_absolute_mag, - "sigma_log_luminosity": sigma_log_luminosity, - "amplitude": amplitude, - }, - ) - - @classmethod - def satellite_modified_schechter_conditional( - cls, - *, - phi_star: ConditionalParameter, - m_star: ConditionalParameter, - alpha: ConditionalParameter, - ) -> "LuminosityFunction": - """Create a satellite modified-Schechter conditional LF.""" - return cls( - model="satellite_modified_schechter_conditional", - parameters={ - "phi_star": phi_star, - "m_star": m_star, - "alpha": alpha, - }, - ) - - @classmethod - def central_satellite_conditional( - cls, - *, - central_mean_absolute_mag: ConditionalParameter, - central_sigma_log_luminosity: ConditionalParameter, - satellite_phi_star: ConditionalParameter, - satellite_alpha: ConditionalParameter, - central_amplitude: ConditionalParameter = 1.0, - satellite_m_star: ConditionalParameter | None = None, - satellite_luminosity_fraction: ConditionalParameter = 0.562, - ) -> "LuminosityFunction": - """Create a central-plus-satellite conditional LF.""" - return cls( - model="central_satellite_conditional", - parameters={ - "lognormal_mean_absolute_mag": central_mean_absolute_mag, - "lognormal_sigma_log_luminosity": central_sigma_log_luminosity, - "lognormal_amplitude": central_amplitude, - "modified_phi_star": satellite_phi_star, - "modified_alpha": satellite_alpha, - "modified_m_star": satellite_m_star, - "modified_luminosity_fraction": satellite_luminosity_fraction, - }, - ) - - @staticmethod - def available_parameter_models() -> dict[str, list[str]]: - """Return available LF parameter evolution models.""" - return available_lf_parameter_models() - - @staticmethod - def register_phi_star_model( - name: str, - model: ParameterModel, - *, - overwrite: bool = False, - ) -> None: - """Register a phi_star evolution model.""" - register_phi_star_model(name, model, overwrite=overwrite) - - @staticmethod - def register_m_star_model( - name: str, - model: ParameterModel, - *, - overwrite: bool = False, - ) -> None: - """Register an M_star evolution model.""" - register_m_star_model(name, model, overwrite=overwrite) - - @staticmethod - def register_alpha_model( - name: str, - model: ParameterModel, - *, - overwrite: bool = False, - ) -> None: - """Register an alpha evolution model.""" - register_alpha_model(name, model, overwrite=overwrite) - - def integrated_number_density( - self, - z: FloatInput, - *, - m_bright: ParameterValue, - m_faint: ParameterValue, - n_m: int = 512, - ) -> FloatArray: - """Integrate the LF over an absolute-magnitude range. - - Args: - z: Redshift values. - m_bright: Bright absolute-magnitude integration limit. - m_faint: Faint absolute-magnitude integration limit. - n_m: Number of magnitude-grid points used in the integration. - - Returns: - Number density integrated over the requested magnitude range. - """ - return integrated_number_density( - z, - self._as_callable(), - m_bright=m_bright, - m_faint=m_faint, - n_m=n_m, - ) - - def lf_weighted_integral( - self, - z: FloatInput, - *, - m_bright: ParameterValue, - m_faint: ParameterValue, - weight_fn: Callable[[FloatArray, FloatArray], FloatArray], - n_m: int = 512, - ) -> FloatArray: - """Integrate the LF with a user-supplied magnitude-redshift weight.""" - return lf_weighted_integral( - z, - self._as_callable(), - m_bright=m_bright, - m_faint=m_faint, - weight_fn=weight_fn, - n_m=n_m, - ) - - def selection_weighted_number_density( - self, - z: FloatInput, - *, - m_bright: ParameterValue, - m_faint: ParameterValue, - selection_fn: Callable[[FloatArray, FloatArray], FloatArray], - n_m: int = 512, - ) -> FloatArray: - """Return LF number density weighted by a selection function.""" - return selection_weighted_number_density( - z, - self._as_callable(), - m_bright=m_bright, - m_faint=m_faint, - selection_fn=selection_fn, - n_m=n_m, - ) - - def integrated_luminosity_density( - self, - z: FloatInput, - *, - m_bright: ParameterValue, - m_faint: ParameterValue, - m_reference: float = 0.0, - n_m: int = 512, - ) -> FloatArray: - """Return luminosity density over an absolute-magnitude range.""" - return integrated_luminosity_density( - z, - self._as_callable(), - m_bright=m_bright, - m_faint=m_faint, - m_reference=m_reference, - n_m=n_m, - ) - - def mean_luminosity( - self, - z: FloatInput, - *, - m_bright: ParameterValue, - m_faint: ParameterValue, - m_reference: float = 0.0, - n_m: int = 512, - ) -> FloatArray: - """Return mean luminosity over an absolute-magnitude range.""" - return mean_luminosity( - z, - self._as_callable(), - m_bright=m_bright, - m_faint=m_faint, - m_reference=m_reference, - n_m=n_m, - ) - - def cumulative_number_density( - self, - z: FloatInput, - *, - m_threshold: ParameterValue, - m_bright: ParameterValue, - m_faint: ParameterValue, - brighter_than: bool = True, - n_m: int = 512, - ) -> FloatArray: - """Return cumulative LF number density around a magnitude threshold.""" - return cumulative_number_density( - z, - self._as_callable(), - m_threshold=m_threshold, - m_bright=m_bright, - m_faint=m_faint, - brighter_than=brighter_than, - n_m=n_m, - ) - - def observed_number_density( - self, - cosmo_obj: Cosmology, - z: FloatInput, - *, - m_lim: float, - m_bright: float, - m_faint: float, - n_m: int = 512, - h: float | None = None, - corrections: Corrections | None = None, - ) -> FloatArray: - """Return the LF number density observable in a magnitude-limited catalog. - - Args: - cosmo_obj: Cosmology object used for apparent-to-absolute conversion. - z: Redshift values. - m_lim: Apparent-magnitude limit of the catalog. - m_bright: Bright absolute-magnitude integration limit. - m_faint: Faint absolute-magnitude integration limit. - n_m: Number of magnitude-grid points used in the integration. - h: Optional reduced Hubble parameter used in the magnitude conversion. - corrections: Optional object providing k-correction and e-correction values. - - Returns: - Number density brighter than the catalog magnitude limit. - """ - k_corr, e_corr = self._correction_values(corrections, z) - - return observed_number_density( - cosmo_obj, - z, - self._as_callable(), - m_lim=m_lim, - m_bright=m_bright, - m_faint=m_faint, - n_m=n_m, - h=h, - k_correction=k_corr, - e_correction=e_corr, - ) - - def missing_number_density( - self, - cosmo_obj: Cosmology, - z: FloatInput, - *, - m_lim: float, - m_bright: float, - m_faint: float, - n_m: int = 512, - h: float | None = None, - corrections: Corrections | None = None, - ) -> FloatArray: - """Return the LF number density missing from a magnitude-limited catalog. - - Args: - cosmo_obj: Cosmology object used for apparent-to-absolute conversion. - z: Redshift values. - m_lim: Apparent-magnitude limit of the catalog. - m_bright: Bright absolute-magnitude integration limit. - m_faint: Faint absolute-magnitude integration limit. - n_m: Number of magnitude-grid points used in the integration. - h: Optional reduced Hubble parameter used in the magnitude conversion. - corrections: Optional object providing k-correction and e-correction values. - - Returns: - Number density fainter than the catalog magnitude limit but inside - the requested absolute-magnitude range. - """ - k_corr, e_corr = self._correction_values(corrections, z) - - return missing_number_density( - cosmo_obj, - z, - self._as_callable(), - m_lim=m_lim, - m_bright=m_bright, - m_faint=m_faint, - n_m=n_m, - h=h, - k_correction=k_corr, - e_correction=e_corr, - ) - - def catalog_completeness( - self, - cosmo_obj: Cosmology, - z: FloatInput, - *, - m_lim: float, - m_bright: float, - m_faint: float, - n_m: int = 512, - h: float | None = None, - corrections: Corrections | None = None, - ) -> FloatArray: - """Return the observed LF fraction in a magnitude-limited catalog. - - Args: - cosmo_obj: Cosmology object used for apparent-to-absolute conversion. - z: Redshift values. - m_lim: Apparent-magnitude limit of the catalog. - m_bright: Bright absolute-magnitude integration limit. - m_faint: Faint absolute-magnitude integration limit. - n_m: Number of magnitude-grid points used in the integration. - h: Optional reduced Hubble parameter used in the magnitude conversion. - corrections: Optional object providing k-correction and e-correction values. - - Returns: - Fraction of the LF number density observable in the catalog. - """ - k_corr, e_corr = self._correction_values(corrections, z) - - return catalog_completeness_fraction( - cosmo_obj, - z, - self._as_callable(), - m_lim=m_lim, - m_bright=m_bright, - m_faint=m_faint, - n_m=n_m, - h=h, - k_correction=k_corr, - e_correction=e_corr, - ) - - def out_of_catalog_fraction( - self, - cosmo_obj: Cosmology, - z: FloatInput, - *, - m_lim: float, - m_bright: float, - m_faint: float, - n_m: int = 512, - h: float | None = None, - corrections: Corrections | None = None, - ) -> FloatArray: - """Return the missing LF fraction for a magnitude-limited catalog. - - Args: - cosmo_obj: Cosmology object used for apparent-to-absolute conversion. - z: Redshift values. - m_lim: Apparent-magnitude limit of the catalog. - m_bright: Bright absolute-magnitude integration limit. - m_faint: Faint absolute-magnitude integration limit. - n_m: Number of magnitude-grid points used in the integration. - h: Optional reduced Hubble parameter used in the magnitude conversion. - corrections: Optional object providing k-correction and e-correction values. - - Returns: - Fraction of the LF number density missing from the catalog. - """ - k_corr, e_corr = self._correction_values(corrections, z) - - return out_of_catalog_fraction( - cosmo_obj, - z, - self._as_callable(), - m_lim=m_lim, - m_bright=m_bright, - m_faint=m_faint, - n_m=n_m, - h=h, - k_correction=k_corr, - e_correction=e_corr, - ) - - def _as_callable(self): - """Return this object as an ``lf(M, z)`` callable.""" - return lambda absolute_mag, z: self.phi(absolute_mag, z) - - @staticmethod - def _correction_values( - corrections: Corrections | None, - z: FloatInput, - ) -> tuple[FloatArray | None, FloatArray | None]: - """Evaluate optional correction values at redshift. - - Args: - corrections: Optional correction object with ``k(z)`` and ``e(z)`` methods. - z: Redshift values where corrections are evaluated. - - Returns: - Tuple of k-correction and e-correction arrays, or ``None`` values - when no correction object is supplied. - """ - if corrections is None: - return None, None - - return corrections.k(z), corrections.e(z) diff --git a/src/lfkit/api/luminosity_function.py b/src/lfkit/api/luminosity_function.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_api_conditional_luminosity_function.py b/tests/test_api_conditional_luminosity_function.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_api_lumfunc.py b/tests/test_api_lumfunc.py deleted file mode 100644 index 0abcd37..0000000 --- a/tests/test_api_lumfunc.py +++ /dev/null @@ -1,1329 +0,0 @@ -"""Unit tests for ``lfkit.api.lumfunc.py``.""" - -from __future__ import annotations - -from typing import cast - -import numpy as np -import pytest - -import lfkit.api.lumfunc as lf_api -from lfkit.api.corrections import Corrections -from lfkit.api.lumfunc import LuminosityFunction - - -class DummyCorrections: - """Small correction object used to test public API forwarding.""" - - def k(self, z): - """Tests that k-corrections can be evaluated at z.""" - return np.asarray(z, dtype=float) + 1.0 - - def e(self, z): - """Tests that e-corrections can be evaluated at z.""" - return np.asarray(z, dtype=float) - 1.0 - - -def test_schechter_constructor_stores_model_and_parameters(): - """Tests that the Schechter constructor stores the expected public state.""" - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - ) - - assert lf.model == "schechter" - assert lf.parameters_dict == { - "phi_star": 1.0e-3, - "m_star": -20.5, - "alpha": -1.2, - } - assert lf.meta == {} - - -def test_constructor_copies_parameter_and_metadata_mappings(): - """Tests that input mappings are copied rather than stored by reference.""" - parameters = {"phi_star": 1.0e-3, "m_star": -20.5, "alpha": -1.2} - meta = {"survey": "test"} - - lf = LuminosityFunction( - model="schechter", - parameters=parameters, - meta=meta, - ) - - parameters["phi_star"] = 9.0 - meta["survey"] = "changed" - - assert lf.parameters_dict["phi_star"] == 1.0e-3 - assert lf.meta["survey"] == "test" - - -def test_phi_dispatches_schechter_model(monkeypatch): - """Tests that phi dispatches to the standard Schechter implementation.""" - absolute_mag = np.array([-21.0, -20.0]) - - def fake_schechter(mag, *, phi_star, m_star, alpha): - assert np.allclose(mag, absolute_mag) - assert phi_star == 1.0e-3 - assert m_star == -20.5 - assert alpha == -1.2 - return np.array([1.0, 2.0]) - - monkeypatch.setattr(lf_api, "schechter", fake_schechter) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - ) - - result = lf.phi(absolute_mag) - - assert np.allclose(result, [1.0, 2.0]) - - -def test_phi_requires_redshift_for_evolving_schechter(): - """Tests that evolving Schechter evaluation requires redshift input.""" - lf = LuminosityFunction.evolving_schechter() - - with pytest.raises(ValueError, match="z is required"): - lf.phi(np.array([-21.0, -20.0])) - - -def test_phi_dispatches_evolving_schechter_model(monkeypatch): - """Tests that phi dispatches to the evolving Schechter implementation.""" - absolute_mag = np.array([-21.0, -20.0]) - z = np.array([0.2, 0.8]) - - def fake_schechter_evolving( - mag, - redshift, - *, - phi_model, - phi_kwargs, - m_star_model, - m_star_kwargs, - alpha_model, - alpha_kwargs, - ): - assert np.allclose(mag, absolute_mag) - assert np.allclose(redshift, z) - assert phi_model == "constant" - assert phi_kwargs == {"value": 1.0e-3} - assert m_star_model == "constant" - assert m_star_kwargs == {"value": -20.5} - assert alpha_model == "constant" - assert alpha_kwargs == {"value": -1.2} - return np.array([3.0, 4.0]) - - monkeypatch.setattr(lf_api, "schechter_evolving", fake_schechter_evolving) - - lf = LuminosityFunction.evolving_schechter( - phi_model="constant", - phi_kwargs={"value": 1.0e-3}, - m_star_model="constant", - m_star_kwargs={"value": -20.5}, - alpha_model="constant", - alpha_kwargs={"value": -1.2}, - ) - - result = lf.phi(absolute_mag, z) - - assert np.allclose(result, [3.0, 4.0]) - - -def test_phi_dispatches_double_schechter_model(monkeypatch): - """Tests that phi dispatches to the double Schechter implementation.""" - absolute_mag = np.array([-21.0, -20.0]) - - def fake_schechter_double( - mag, - *, - phi_star, - m_star, - alpha, - beta, - m_transition, - ): - assert np.allclose(mag, absolute_mag) - assert phi_star == 1.0e-3 - assert m_star == -20.5 - assert alpha == -1.2 - assert beta == -2.0 - assert m_transition == -19.0 - return np.array([5.0, 6.0]) - - monkeypatch.setattr(lf_api, "schechter_double", fake_schechter_double) - - lf = LuminosityFunction.double_schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - beta=-2.0, - m_transition=-19.0, - ) - - result = lf.phi(absolute_mag) - - assert np.allclose(result, [5.0, 6.0]) - - -def test_phi_raises_for_unsupported_model(): - """Tests that unsupported luminosity function models fail clearly.""" - lf = LuminosityFunction(model="bad_model", parameters={}) - - with pytest.raises(ValueError, match="Unsupported luminosity function model"): - lf.phi(np.array([-21.0, -20.0])) - - -def test_phi_from_m_forwards_corrections_to_schechter_from_m(monkeypatch): - """Tests that phi_from_m forwards correction arrays to magnitude evaluation.""" - cosmo_obj = object() - z = np.array([0.1, 0.5]) - apparent_mag = np.array([22.0, 24.0]) - corrections = cast(Corrections, DummyCorrections()) - - def fake_schechter_from_m( - received_cosmo, - received_z, - received_apparent_mag, - *, - h, - k_correction, - e_correction, - phi_star, - m_star, - alpha, - ): - assert received_cosmo is cosmo_obj - assert np.allclose(received_z, z) - assert np.allclose(received_apparent_mag, apparent_mag) - assert h == 0.7 - assert np.allclose(k_correction, [1.1, 1.5]) - assert np.allclose(e_correction, [-0.9, -0.5]) - assert phi_star == 1.0e-3 - assert m_star == -20.5 - assert alpha == -1.2 - return np.array([7.0, 8.0]) - - monkeypatch.setattr(lf_api, "schechter_from_m", fake_schechter_from_m) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - ) - - result = lf.phi_from_m( - cosmo_obj, - z, - apparent_mag, - h=0.7, - corrections=corrections, - ) - - assert np.allclose(result, [7.0, 8.0]) - - -def test_phi_from_m_passes_none_corrections_when_not_supplied(monkeypatch): - """Tests that phi_from_m uses None corrections when no correction object is given.""" - cosmo_obj = object() - z = np.array([0.1, 0.5]) - apparent_mag = np.array([22.0, 24.0]) - - def fake_schechter_from_m( - received_cosmo, - received_z, - received_apparent_mag, - *, - h, - k_correction, - e_correction, - phi_star, - m_star, - alpha, - ): - assert received_cosmo is cosmo_obj - assert np.allclose(received_z, z) - assert np.allclose(received_apparent_mag, apparent_mag) - assert h is None - assert k_correction is None - assert e_correction is None - return np.array([1.0, 1.0]) - - monkeypatch.setattr(lf_api, "schechter_from_m", fake_schechter_from_m) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - ) - - result = lf.phi_from_m(cosmo_obj, z, apparent_mag) - - assert np.allclose(result, [1.0, 1.0]) - - -def test_parameters_only_work_for_evolving_schechter(monkeypatch): - """Tests that parameters delegates only for evolving Schechter models.""" - z = np.array([0.1, 0.5]) - - def fake_evaluate_lf_parameters(redshift, **kwargs): - assert np.allclose(redshift, z) - assert kwargs["phi_model"] == "constant" - return ( - np.array([1.0e-3, 1.0e-3]), - np.array([-20.5, -20.5]), - np.array([-1.2, -1.2]), - ) - - monkeypatch.setattr(lf_api, "evaluate_lf_parameters", fake_evaluate_lf_parameters) - - lf = LuminosityFunction.evolving_schechter( - phi_model="constant", - phi_kwargs={"value": 1.0e-3}, - m_star_model="constant", - m_star_kwargs={"value": -20.5}, - alpha_model="constant", - alpha_kwargs={"value": -1.2}, - ) - - phi_star, m_star, alpha = lf.parameters(z) - - assert np.allclose(phi_star, [1.0e-3, 1.0e-3]) - assert np.allclose(m_star, [-20.5, -20.5]) - assert np.allclose(alpha, [-1.2, -1.2]) - - -def test_parameters_raise_for_non_evolving_schechter(): - """Tests that parameters raises for non-evolving luminosity functions.""" - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - ) - - with pytest.raises(ValueError, match="only defined for evolving_schechter"): - lf.parameters(np.array([0.1, 0.5])) - - -def test_integrated_number_density_uses_api_callable(monkeypatch): - """Tests that integrated number density evaluates the public LF callable.""" - z = np.array([0.1, 0.5]) - - def fake_integrated_number_density(redshift, lf_callable, *, m_bright, m_faint, n_m): - mag = np.array([-21.0, -20.0]) - assert np.allclose(redshift, z) - assert m_bright == -24.0 - assert m_faint == -18.0 - assert n_m == 32 - assert np.allclose(lf_callable(mag, z), [2.0, 2.0]) - return np.array([10.0, 20.0]) - - monkeypatch.setattr( - lf_api, - "integrated_number_density", - fake_integrated_number_density, - ) - - lf = LuminosityFunction( - model="schechter", - parameters={"phi_star": 1.0e-3, "m_star": -20.5, "alpha": -1.2}, - ) - monkeypatch.setattr( - lf, - "phi", - lambda absolute_mag, z=None: np.full_like(absolute_mag, 2.0), - ) - - result = lf.integrated_number_density( - z, - m_bright=-24.0, - m_faint=-18.0, - n_m=32, - ) - - assert np.allclose(result, [10.0, 20.0]) - - -def test_catalog_completeness_forwards_corrections(monkeypatch): - """Tests that catalog completeness forwards correction arrays.""" - cosmo_obj = object() - z = np.array([0.1, 0.5]) - corrections = cast(Corrections, DummyCorrections()) - - def fake_catalog_completeness_fraction( - received_cosmo, - received_z, - lf_callable, - *, - m_lim, - m_bright, - m_faint, - n_m, - h, - k_correction, - e_correction, - ): - assert received_cosmo is cosmo_obj - assert np.allclose(received_z, z) - assert m_lim == 24.5 - assert m_bright == -24.0 - assert m_faint == -18.0 - assert n_m == 64 - assert h == 0.7 - assert np.allclose(k_correction, [1.1, 1.5]) - assert np.allclose(e_correction, [-0.9, -0.5]) - assert np.allclose(lf_callable(np.array([-21.0]), np.array([0.1])), [3.0]) - return np.array([0.8, 0.6]) - - monkeypatch.setattr( - lf_api, - "catalog_completeness_fraction", - fake_catalog_completeness_fraction, - ) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - ) - monkeypatch.setattr( - lf, - "phi", - lambda absolute_mag, z=None: np.full_like(absolute_mag, 3.0), - ) - - result = lf.catalog_completeness( - cosmo_obj, - z, - m_lim=24.5, - m_bright=-24.0, - m_faint=-18.0, - n_m=64, - h=0.7, - corrections=corrections, - ) - - assert np.allclose(result, [0.8, 0.6]) - - -def test_observed_and_missing_number_density_are_consistent(monkeypatch): - """Tests that observed and missing wrappers forward matching inputs.""" - cosmo_obj = object() - z = np.array([0.1, 0.5]) - - def fake_observed_number_density( - received_cosmo, - received_z, - lf_callable, - *, - m_lim, - m_bright, - m_faint, - n_m, - h, - k_correction, - e_correction, - ): - assert received_cosmo is cosmo_obj - assert np.allclose(received_z, z) - assert m_lim == 24.5 - assert m_bright == -24.0 - assert m_faint == -18.0 - assert n_m == 128 - assert h is None - assert k_correction is None - assert e_correction is None - return np.array([4.0, 6.0]) - - def fake_missing_number_density( - received_cosmo, - received_z, - lf_callable, - *, - m_lim, - m_bright, - m_faint, - n_m, - h, - k_correction, - e_correction, - ): - assert received_cosmo is cosmo_obj - assert np.allclose(received_z, z) - assert m_lim == 24.5 - assert m_bright == -24.0 - assert m_faint == -18.0 - assert n_m == 128 - assert h is None - assert k_correction is None - assert e_correction is None - return np.array([1.0, 2.0]) - - monkeypatch.setattr(lf_api, "observed_number_density", fake_observed_number_density) - monkeypatch.setattr(lf_api, "missing_number_density", fake_missing_number_density) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - ) - - observed = lf.observed_number_density( - cosmo_obj, - z, - m_lim=24.5, - m_bright=-24.0, - m_faint=-18.0, - n_m=128, - ) - missing = lf.missing_number_density( - cosmo_obj, - z, - m_lim=24.5, - m_bright=-24.0, - m_faint=-18.0, - n_m=128, - ) - - assert np.allclose(observed, [4.0, 6.0]) - assert np.allclose(missing, [1.0, 2.0]) - - -def test_catalog_and_out_of_catalog_fractions_sum_to_one(monkeypatch): - """Tests that catalog and out-of-catalog wrappers preserve fraction semantics.""" - cosmo_obj = object() - z = np.array([0.1, 0.5]) - - monkeypatch.setattr( - lf_api, - "catalog_completeness_fraction", - lambda *args, **kwargs: np.array([0.75, 0.25]), - ) - monkeypatch.setattr( - lf_api, - "out_of_catalog_fraction", - lambda *args, **kwargs: np.array([0.25, 0.75]), - ) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - ) - - completeness = lf.catalog_completeness( - cosmo_obj, - z, - m_lim=24.5, - m_bright=-24.0, - m_faint=-18.0, - ) - missing = lf.out_of_catalog_fraction( - cosmo_obj, - z, - m_lim=24.5, - m_bright=-24.0, - m_faint=-18.0, - ) - - assert np.allclose(completeness + missing, 1.0) - - -def test_absolute_magnitude_forwards_corrections(monkeypatch): - """Tests that absolute_magnitude forwards correction arrays.""" - cosmo_obj = object() - z = np.array([0.1, 0.5]) - apparent_mag = np.array([22.0, 24.0]) - corrections = cast(Corrections, DummyCorrections()) - - def fake_absolute_magnitude( - received_cosmo, - received_z, - received_apparent_mag, - *, - h, - k_correction, - e_correction, - ): - assert received_cosmo is cosmo_obj - assert np.allclose(received_z, z) - assert np.allclose(received_apparent_mag, apparent_mag) - assert h == 0.7 - assert np.allclose(k_correction, [1.1, 1.5]) - assert np.allclose(e_correction, [-0.9, -0.5]) - return np.array([-19.0, -20.0]) - - monkeypatch.setattr(lf_api, "absolute_magnitude", fake_absolute_magnitude) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - ) - - result = lf.absolute_magnitude( - cosmo_obj, - z, - apparent_mag, - h=0.7, - corrections=corrections, - ) - - assert np.allclose(result, [-19.0, -20.0]) - - -def test_apparent_magnitude_forwards_corrections(monkeypatch): - """Tests that apparent_magnitude forwards correction arrays.""" - cosmo_obj = object() - z = np.array([0.1, 0.5]) - absolute_mag = np.array([-19.0, -20.0]) - corrections = cast(Corrections, DummyCorrections()) - - def fake_apparent_magnitude( - received_cosmo, - received_z, - received_absolute_mag, - *, - h, - k_correction, - e_correction, - ): - assert received_cosmo is cosmo_obj - assert np.allclose(received_z, z) - assert np.allclose(received_absolute_mag, absolute_mag) - assert h == 0.7 - assert np.allclose(k_correction, [1.1, 1.5]) - assert np.allclose(e_correction, [-0.9, -0.5]) - return np.array([22.0, 24.0]) - - monkeypatch.setattr(lf_api, "apparent_magnitude", fake_apparent_magnitude) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - ) - - result = lf.apparent_magnitude( - cosmo_obj, - z, - absolute_mag, - h=0.7, - corrections=corrections, - ) - - assert np.allclose(result, [22.0, 24.0]) - - -def test_absolute_magnitude_from_luminosity_distance_forwards_corrections(monkeypatch): - """Tests that absolute magnitude from distance forwards correction arrays.""" - z = np.array([0.1, 0.5]) - apparent_mag = np.array([22.0, 24.0]) - luminosity_distance_mpc = np.array([500.0, 1500.0]) - corrections = cast(Corrections, DummyCorrections()) - - def fake_absolute_magnitude_from_luminosity_distance( - received_apparent_mag, - received_luminosity_distance_mpc, - *, - k_correction, - e_correction, - ): - assert np.allclose(received_apparent_mag, apparent_mag) - assert np.allclose(received_luminosity_distance_mpc, luminosity_distance_mpc) - assert np.allclose(k_correction, [1.1, 1.5]) - assert np.allclose(e_correction, [-0.9, -0.5]) - return np.array([-18.0, -21.0]) - - monkeypatch.setattr( - lf_api, - "absolute_magnitude_from_luminosity_distance", - fake_absolute_magnitude_from_luminosity_distance, - ) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - ) - - result = lf.absolute_magnitude_from_luminosity_distance( - apparent_mag, - luminosity_distance_mpc, - z=z, - corrections=corrections, - ) - - assert np.allclose(result, [-18.0, -21.0]) - - -def test_apparent_magnitude_from_luminosity_distance_forwards_corrections(monkeypatch): - """Tests that apparent magnitude from distance forwards correction arrays.""" - z = np.array([0.1, 0.5]) - absolute_mag = np.array([-18.0, -21.0]) - luminosity_distance_mpc = np.array([500.0, 1500.0]) - corrections = cast(Corrections, DummyCorrections()) - - def fake_apparent_magnitude_from_luminosity_distance( - received_absolute_mag, - received_luminosity_distance_mpc, - *, - k_correction, - e_correction, - ): - assert np.allclose(received_absolute_mag, absolute_mag) - assert np.allclose(received_luminosity_distance_mpc, luminosity_distance_mpc) - assert np.allclose(k_correction, [1.1, 1.5]) - assert np.allclose(e_correction, [-0.9, -0.5]) - return np.array([22.0, 24.0]) - - monkeypatch.setattr( - lf_api, - "apparent_magnitude_from_luminosity_distance", - fake_apparent_magnitude_from_luminosity_distance, - ) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - ) - - result = lf.apparent_magnitude_from_luminosity_distance( - absolute_mag, - luminosity_distance_mpc, - z=z, - corrections=corrections, - ) - - assert np.allclose(result, [22.0, 24.0]) - - -def test_absolute_magnitude_limit_forwards_corrections(monkeypatch): - """Tests that absolute_magnitude_limit forwards correction arrays.""" - cosmo_obj = object() - z = np.array([0.1, 0.5]) - corrections = cast(Corrections, DummyCorrections()) - - def fake_absolute_magnitude_limit( - received_cosmo, - received_z, - *, - m_lim, - h, - k_correction, - e_correction, - ): - assert received_cosmo is cosmo_obj - assert np.allclose(received_z, z) - assert m_lim == 24.5 - assert h == 0.7 - assert np.allclose(k_correction, [1.1, 1.5]) - assert np.allclose(e_correction, [-0.9, -0.5]) - return np.array([-18.0, -20.0]) - - monkeypatch.setattr(lf_api, "absolute_magnitude_limit", fake_absolute_magnitude_limit) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - ) - - result = lf.absolute_magnitude_limit( - cosmo_obj, - z, - m_lim=24.5, - h=0.7, - corrections=corrections, - ) - - assert np.allclose(result, [-18.0, -20.0]) - - -def test_phi_from_m_dispatches_evolving_schechter_from_m(monkeypatch): - """Tests that phi_from_m dispatches to evolving Schechter from apparent magnitude.""" - cosmo_obj = object() - z = np.array([0.1, 0.5]) - apparent_mag = np.array([22.0, 24.0]) - - def fake_schechter_evolving_from_m( - received_cosmo, - received_z, - received_apparent_mag, - *, - h, - k_correction, - e_correction, - phi_model, - phi_kwargs, - m_star_model, - m_star_kwargs, - alpha_model, - alpha_kwargs, - ): - assert received_cosmo is cosmo_obj - assert np.allclose(received_z, z) - assert np.allclose(received_apparent_mag, apparent_mag) - assert h is None - assert k_correction is None - assert e_correction is None - assert phi_model == "constant" - assert phi_kwargs == {"value": 1.0e-3} - assert m_star_model == "constant" - assert m_star_kwargs == {"value": -20.5} - assert alpha_model == "constant" - assert alpha_kwargs == {"value": -1.2} - return np.array([9.0, 10.0]) - - monkeypatch.setattr( - lf_api, - "schechter_evolving_from_m", - fake_schechter_evolving_from_m, - ) - - lf = LuminosityFunction.evolving_schechter( - phi_model="constant", - phi_kwargs={"value": 1.0e-3}, - m_star_model="constant", - m_star_kwargs={"value": -20.5}, - alpha_model="constant", - alpha_kwargs={"value": -1.2}, - ) - - result = lf.phi_from_m(cosmo_obj, z, apparent_mag) - - assert np.allclose(result, [9.0, 10.0]) - - -def test_phi_from_m_dispatches_double_schechter_from_m(monkeypatch): - """Tests that phi_from_m dispatches to double Schechter from apparent magnitude.""" - cosmo_obj = object() - z = np.array([0.1, 0.5]) - apparent_mag = np.array([22.0, 24.0]) - - def fake_schechter_double_from_m( - received_cosmo, - received_z, - received_apparent_mag, - *, - h, - k_correction, - e_correction, - phi_star, - m_star, - alpha, - beta, - m_transition, - ): - assert received_cosmo is cosmo_obj - assert np.allclose(received_z, z) - assert np.allclose(received_apparent_mag, apparent_mag) - assert h is None - assert k_correction is None - assert e_correction is None - assert phi_star == 1.0e-3 - assert m_star == -20.5 - assert alpha == -1.2 - assert beta == -2.0 - assert m_transition == -19.0 - return np.array([11.0, 12.0]) - - monkeypatch.setattr( - lf_api, - "schechter_double_from_m", - fake_schechter_double_from_m, - ) - - lf = LuminosityFunction.double_schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - beta=-2.0, - m_transition=-19.0, - ) - - result = lf.phi_from_m(cosmo_obj, z, apparent_mag) - - assert np.allclose(result, [11.0, 12.0]) - - -def test_phi_from_m_raises_for_unsupported_model(): - """Tests that phi_from_m rejects unsupported LF models.""" - lf = LuminosityFunction(model="bad_model", parameters={}) - - with pytest.raises(ValueError, match="Unsupported luminosity function model"): - lf.phi_from_m(object(), np.array([0.1]), np.array([22.0])) - - -def test_lf_weighted_integral_forwards_to_module_function(monkeypatch): - """Tests that lf_weighted_integral forwards inputs to the module function.""" - z = np.array([0.1, 0.5]) - - def weight_fn(absolute_mag, redshift): - return np.ones_like(absolute_mag, dtype=float) - - def fake_lf_weighted_integral( - received_z, - lf_callable, - *, - m_bright, - m_faint, - weight_fn: object, - n_m, - ): - mag = np.array([-21.0, -20.0]) - assert np.allclose(received_z, z) - assert m_bright == -24.0 - assert m_faint == -18.0 - assert n_m == 64 - assert np.allclose(lf_callable(mag, z), [2.0, 2.0]) - return np.array([3.0, 4.0]) - - monkeypatch.setattr(lf_api, "lf_weighted_integral", fake_lf_weighted_integral) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - ) - monkeypatch.setattr( - lf, - "phi", - lambda absolute_mag, z=None: np.full_like(absolute_mag, 2.0), - ) - - result = lf.lf_weighted_integral( - z, - m_bright=-24.0, - m_faint=-18.0, - weight_fn=weight_fn, - n_m=64, - ) - - assert np.allclose(result, [3.0, 4.0]) - - -def test_selection_weighted_number_density_forwards_to_module_function(monkeypatch): - """Tests that selection_weighted_number_density forwards inputs.""" - z = np.array([0.1, 0.5]) - - def selection_fn(absolute_mag, redshift): - return np.ones_like(absolute_mag, dtype=float) - - def fake_selection_weighted_number_density( - received_z, - lf_callable, - *, - m_bright, - m_faint, - selection_fn: object, - n_m, - ): - mag = np.array([-21.0, -20.0]) - assert np.allclose(received_z, z) - assert m_bright == -24.0 - assert m_faint == -18.0 - assert n_m == 64 - assert np.allclose(lf_callable(mag, z), [2.0, 2.0]) - return np.array([5.0, 6.0]) - - monkeypatch.setattr( - lf_api, - "selection_weighted_number_density", - fake_selection_weighted_number_density, - ) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - ) - monkeypatch.setattr( - lf, - "phi", - lambda absolute_mag, z=None: np.full_like(absolute_mag, 2.0), - ) - - result = lf.selection_weighted_number_density( - z, - m_bright=-24.0, - m_faint=-18.0, - selection_fn=selection_fn, - n_m=64, - ) - - assert np.allclose(result, [5.0, 6.0]) - - -def test_integrated_luminosity_density_forwards_to_module_function(monkeypatch): - """Tests that integrated_luminosity_density forwards inputs.""" - z = np.array([0.1, 0.5]) - - def fake_integrated_luminosity_density( - received_z, - lf_callable, - *, - m_bright, - m_faint, - m_reference, - n_m, - ): - mag = np.array([-21.0, -20.0]) - assert np.allclose(received_z, z) - assert m_bright == -24.0 - assert m_faint == -18.0 - assert m_reference == -20.0 - assert n_m == 64 - assert np.allclose(lf_callable(mag, z), [2.0, 2.0]) - return np.array([7.0, 8.0]) - - monkeypatch.setattr( - lf_api, - "integrated_luminosity_density", - fake_integrated_luminosity_density, - ) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - ) - monkeypatch.setattr( - lf, - "phi", - lambda absolute_mag, z=None: np.full_like(absolute_mag, 2.0), - ) - - result = lf.integrated_luminosity_density( - z, - m_bright=-24.0, - m_faint=-18.0, - m_reference=-20.0, - n_m=64, - ) - - assert np.allclose(result, [7.0, 8.0]) - - -def test_mean_luminosity_forwards_to_module_function(monkeypatch): - """Tests that mean_luminosity forwards inputs.""" - z = np.array([0.1, 0.5]) - - def fake_mean_luminosity( - received_z, - lf_callable, - *, - m_bright, - m_faint, - m_reference, - n_m, - ): - mag = np.array([-21.0, -20.0]) - assert np.allclose(received_z, z) - assert m_bright == -24.0 - assert m_faint == -18.0 - assert m_reference == -20.0 - assert n_m == 64 - assert np.allclose(lf_callable(mag, z), [2.0, 2.0]) - return np.array([9.0, 10.0]) - - monkeypatch.setattr(lf_api, "mean_luminosity", fake_mean_luminosity) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - ) - monkeypatch.setattr( - lf, - "phi", - lambda absolute_mag, z=None: np.full_like(absolute_mag, 2.0), - ) - - result = lf.mean_luminosity( - z, - m_bright=-24.0, - m_faint=-18.0, - m_reference=-20.0, - n_m=64, - ) - - assert np.allclose(result, [9.0, 10.0]) - - -def test_cumulative_number_density_forwards_to_module_function(monkeypatch): - """Tests that cumulative_number_density forwards inputs.""" - z = np.array([0.1, 0.5]) - - def fake_cumulative_number_density( - received_z, - lf_callable, - *, - m_threshold, - m_bright, - m_faint, - brighter_than, - n_m, - ): - mag = np.array([-21.0, -20.0]) - assert np.allclose(received_z, z) - assert m_threshold == -20.0 - assert m_bright == -24.0 - assert m_faint == -18.0 - assert brighter_than is False - assert n_m == 64 - assert np.allclose(lf_callable(mag, z), [2.0, 2.0]) - return np.array([11.0, 12.0]) - - monkeypatch.setattr( - lf_api, - "cumulative_number_density", - fake_cumulative_number_density, - ) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - ) - monkeypatch.setattr( - lf, - "phi", - lambda absolute_mag, z=None: np.full_like(absolute_mag, 2.0), - ) - - result = lf.cumulative_number_density( - z, - m_threshold=-20.0, - m_bright=-24.0, - m_faint=-18.0, - brighter_than=False, - n_m=64, - ) - - assert np.allclose(result, [11.0, 12.0]) - - -def test_lf_integrated_number_density_forwards_corrections(monkeypatch): - """Tests that apparent-limit LF number density forwards corrections.""" - z = np.array([0.1, 0.5]) - corrections = cast(Corrections, DummyCorrections()) - - def luminosity_distance_mpc_fn(redshift): - return 1000.0 * np.asarray(redshift, dtype=float) - - def fake_lf_integrated_number_density( - received_z, - lf_callable, - *, - m_lim, - m_bright, - n_m, - luminosity_distance_mpc_fn, - k_correction, - evolution_correction, - ): - mag = np.array([-21.0, -20.0]) - assert np.allclose(received_z, z) - assert m_lim == 24.5 - assert m_bright == -24.0 - assert n_m == 64 - assert np.allclose(luminosity_distance_mpc_fn(z), [100.0, 500.0]) - assert np.allclose(k_correction, [1.1, 1.5]) - assert np.allclose(evolution_correction, [-0.9, -0.5]) - assert np.allclose(lf_callable(mag, z), [2.0, 2.0]) - return np.array([13.0, 14.0]) - - monkeypatch.setattr( - lf_api, - "lf_integrated_number_density", - fake_lf_integrated_number_density, - ) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - ) - monkeypatch.setattr( - lf, - "phi", - lambda absolute_mag, z=None: np.full_like(absolute_mag, 2.0), - ) - - result = lf.lf_integrated_number_density( - z, - m_lim=24.5, - m_bright=-24.0, - n_m=64, - luminosity_distance_mpc_fn=luminosity_distance_mpc_fn, - corrections=corrections, - ) - - assert np.allclose(result, [13.0, 14.0]) - - -def test_lf_weighted_redshift_density_forwards_corrections(monkeypatch): - """Tests that LF-weighted redshift density forwards corrections.""" - z = np.array([0.1, 0.5]) - corrections = cast(Corrections, DummyCorrections()) - - def luminosity_distance_mpc_fn(redshift): - return 1000.0 * np.asarray(redshift, dtype=float) - - def volume_weight_fn(redshift): - return 2.0 * np.asarray(redshift, dtype=float) - - def fake_lf_weighted_redshift_density( - received_z, - lf_callable, - *, - m_lim, - m_bright, - n_m, - luminosity_distance_mpc_fn, - volume_weight_fn, - k_correction, - evolution_correction, - normalize, - ): - mag = np.array([-21.0, -20.0]) - assert np.allclose(received_z, z) - assert m_lim == 24.5 - assert m_bright == -24.0 - assert n_m == 64 - assert np.allclose(luminosity_distance_mpc_fn(z), [100.0, 500.0]) - assert np.allclose(volume_weight_fn(z), [0.2, 1.0]) - assert np.allclose(k_correction, [1.1, 1.5]) - assert np.allclose(evolution_correction, [-0.9, -0.5]) - assert normalize is False - assert np.allclose(lf_callable(mag, z), [2.0, 2.0]) - return np.array([15.0, 16.0]) - - monkeypatch.setattr( - lf_api, - "lf_weighted_redshift_density", - fake_lf_weighted_redshift_density, - ) - - lf = LuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.2, - ) - monkeypatch.setattr( - lf, - "phi", - lambda absolute_mag, z=None: np.full_like(absolute_mag, 2.0), - ) - - result = lf.lf_weighted_redshift_density( - z, - m_lim=24.5, - m_bright=-24.0, - n_m=64, - luminosity_distance_mpc_fn=luminosity_distance_mpc_fn, - volume_weight_fn=volume_weight_fn, - corrections=corrections, - normalize=False, - ) - - assert np.allclose(result, [15.0, 16.0]) - - -def test_available_parameter_models_delegates_to_module_function(monkeypatch): - """Tests that available_parameter_models delegates to the registry helper.""" - expected = { - "phi_star": ["constant", "linear_p"], - "m_star": ["constant", "linear_q"], - "alpha": ["constant"], - } - - monkeypatch.setattr( - lf_api, - "available_lf_parameter_models", - lambda: expected, - ) - - result = LuminosityFunction.available_parameter_models() - - assert result == expected - - -def test_register_phi_star_model_delegates_to_module_function(monkeypatch): - """Tests that register_phi_star_model delegates to the registry helper.""" - captured = {} - - def model(z, *, value): - return np.full_like(np.asarray(z, dtype=float), value) - - def fake_register(name, received_model, *, overwrite): - captured["name"] = name - captured["model"] = received_model - captured["overwrite"] = overwrite - - monkeypatch.setattr(lf_api, "register_phi_star_model", fake_register) - - LuminosityFunction.register_phi_star_model( - "test_phi", - model, - overwrite=True, - ) - - assert captured["name"] == "test_phi" - assert captured["model"] is model - assert captured["overwrite"] is True - - -def test_register_m_star_model_delegates_to_module_function(monkeypatch): - """Tests that register_m_star_model delegates to the registry helper.""" - captured = {} - - def model(z, *, value): - return np.full_like(np.asarray(z, dtype=float), value) - - def fake_register(name, received_model, *, overwrite): - captured["name"] = name - captured["model"] = received_model - captured["overwrite"] = overwrite - - monkeypatch.setattr(lf_api, "register_m_star_model", fake_register) - - LuminosityFunction.register_m_star_model( - "test_m_star", - model, - overwrite=True, - ) - - assert captured["name"] == "test_m_star" - assert captured["model"] is model - assert captured["overwrite"] is True - - -def test_register_alpha_model_delegates_to_module_function(monkeypatch): - """Tests that register_alpha_model delegates to the registry helper.""" - captured = {} - - def model(z, *, value): - return np.full_like(np.asarray(z, dtype=float), value) - - def fake_register(name, received_model, *, overwrite): - captured["name"] = name - captured["model"] = received_model - captured["overwrite"] = overwrite - - monkeypatch.setattr(lf_api, "register_alpha_model", fake_register) - - LuminosityFunction.register_alpha_model( - "test_alpha", - model, - overwrite=True, - ) - - assert captured["name"] == "test_alpha" - assert captured["model"] is model - assert captured["overwrite"] is True diff --git a/tests/test_api_luminosity_function.py b/tests/test_api_luminosity_function.py new file mode 100644 index 0000000..e69de29 From b398929499676179648efd53c9bd964c6f31d8f4 Mon Sep 17 00:00:00 2001 From: "niko.sarcevic" Date: Fri, 15 May 2026 13:10:19 -0400 Subject: [PATCH 2/6] updated api registry --- src/lfkit/api/_clf_models.py | 23 ++ src/lfkit/api/_completeness.py | 51 +++ src/lfkit/api/_expose.py | 45 +++ src/lfkit/api/_integrals.py | 53 +++ src/lfkit/api/_lf_param_models.py | 30 +- src/lfkit/api/_luminosities.py | 36 ++ src/lfkit/api/_magnitudes.py | 27 ++ src/lfkit/api/_redshift_density.py | 39 ++ .../api/conditional_luminosity_function.py | 161 ++++++++ src/lfkit/api/luminosity_function.py | 370 ++++++++++++++++++ 10 files changed, 820 insertions(+), 15 deletions(-) diff --git a/src/lfkit/api/_clf_models.py b/src/lfkit/api/_clf_models.py index e69de29..4924c6c 100644 --- a/src/lfkit/api/_clf_models.py +++ b/src/lfkit/api/_clf_models.py @@ -0,0 +1,23 @@ +"""User-facing conditional luminosity-function model API namespace.""" + +from __future__ import annotations + +from lfkit.photometry.conditional_lf_models import ( + conditional_schechter, + conditional_double_schechter, + conditional_evolving_schechter, + lognormal_conditional_lf, + modified_schechter_conditional_lf, + two_component_conditional_lf, +) + + +class LFConditionalModelsAPI: + """Grouped API for evaluating conditional luminosity-function models.""" + + schechter = staticmethod(conditional_schechter) + evolving_schechter = staticmethod(conditional_evolving_schechter) + double_schechter = staticmethod(conditional_double_schechter) + lognormal = staticmethod(lognormal_conditional_lf) + modified_schechter = staticmethod(modified_schechter_conditional_lf) + two_component = staticmethod(two_component_conditional_lf) diff --git a/src/lfkit/api/_completeness.py b/src/lfkit/api/_completeness.py index e69de29..3fabc49 100644 --- a/src/lfkit/api/_completeness.py +++ b/src/lfkit/api/_completeness.py @@ -0,0 +1,51 @@ +"""User-facing catalog-completeness API namespace.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from lfkit.api._expose import expose_lf_function +from lfkit.photometry.catalog_completeness import ( + absolute_magnitude_limit, + catalog_completeness_fraction, + missing_number_density, + observed_number_density, + out_of_catalog_fraction, +) + +if TYPE_CHECKING: + from lfkit.api.luminosity_function import LuminosityFunction + + +class LFCompletenessAPI: + """Grouped API for catalog-completeness calculations. + + Args: + lf: Parent luminosity-function object. + """ + + def __init__(self, lf: LuminosityFunction) -> None: + self.lf = lf + + +_COMPLETENESS_METHODS = { + "observed_number_density": observed_number_density, + "missing_number_density": missing_number_density, + "catalog_fraction": catalog_completeness_fraction, + "out_of_catalog_fraction": out_of_catalog_fraction, +} + + +for method_name, function in _COMPLETENESS_METHODS.items(): + setattr( + LFCompletenessAPI, + method_name, + expose_lf_function( + function, + lf_arg_position=None, + lf_arg_name="lf", + ), + ) + + +LFCompletenessAPI.absolute_magnitude_limit = staticmethod(absolute_magnitude_limit) diff --git a/src/lfkit/api/_expose.py b/src/lfkit/api/_expose.py index e69de29..7b10104 100644 --- a/src/lfkit/api/_expose.py +++ b/src/lfkit/api/_expose.py @@ -0,0 +1,45 @@ +"""Helpers for exposing low-level functions through API namespaces.""" + +from __future__ import annotations + +from collections.abc import Callable +from functools import wraps +from typing import Any + + +def expose_lf_function( + function: Callable[..., Any], + *, + lf_arg_position: int | None = 1, + lf_arg_name: str | None = None, +) -> Callable[..., Any]: + """Expose a low-level LF function as a bound API method. + + Args: + function: Low-level function to expose. + lf_arg_position: Positional location where the LF callable is inserted. + Use this when the low-level function expects ``lf_callable`` as a + positional argument. + lf_arg_name: Keyword name that receives the LF callable. Use this when + the low-level function expects a named LF callable argument. + + Returns: + Bound method that injects ``self.lf._as_callable()``. + """ + + @wraps(function) + def method(self, *args, **kwargs): + lf_callable = self.lf._as_callable() + + if lf_arg_name is not None: + kwargs[lf_arg_name] = lf_callable + return function(*args, **kwargs) + + if lf_arg_position is None: + return function(*args, **kwargs) + + args_list = list(args) + args_list.insert(lf_arg_position, lf_callable) + return function(*args_list, **kwargs) + + return method diff --git a/src/lfkit/api/_integrals.py b/src/lfkit/api/_integrals.py index e69de29..0a040ae 100644 --- a/src/lfkit/api/_integrals.py +++ b/src/lfkit/api/_integrals.py @@ -0,0 +1,53 @@ +"""User-facing luminosity-function integral API namespace.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from lfkit.api._expose import expose_lf_function +from lfkit.photometry.lf_integrals import ( + cumulative_number_density, + integrated_luminosity_density, + integrated_number_density, + lf_weighted_integral, + magnitude_window_number_density, + mean_luminosity, + selection_weighted_number_density, +) + +if TYPE_CHECKING: + from lfkit.api.luminosity_function import LuminosityFunction + + +class LFIntegralsAPI: + """Grouped API for luminosity-function integrals. + + Args: + lf: Parent luminosity-function object. + """ + + def __init__(self, lf: LuminosityFunction) -> None: + self.lf = lf + + +_INTEGRAL_METHODS = { + "number_density": integrated_number_density, + "weighted": lf_weighted_integral, + "selection_weighted_number_density": selection_weighted_number_density, + "luminosity_density": integrated_luminosity_density, + "mean_luminosity": mean_luminosity, + "cumulative_number_density": cumulative_number_density, + "magnitude_window_number_density": magnitude_window_number_density, +} + + +for method_name, function in _INTEGRAL_METHODS.items(): + setattr( + LFIntegralsAPI, + method_name, + expose_lf_function( + function, + lf_arg_position=None, + lf_arg_name="lf", + ), + ) diff --git a/src/lfkit/api/_lf_param_models.py b/src/lfkit/api/_lf_param_models.py index d80c8bc..464a1c8 100644 --- a/src/lfkit/api/_lf_param_models.py +++ b/src/lfkit/api/_lf_param_models.py @@ -7,18 +7,18 @@ from lfkit.photometry.conditional_lf_models import ( conditional_schechter, - conditional_schechter_double, - conditional_schechter_evolving, + conditional_double_schechter, + conditional_evolving_schechter, lognormal_conditional_lf, modified_schechter_conditional_lf, two_component_conditional_lf, ) from lfkit.photometry.luminosity_function import ( schechter, - schechter_double, - schechter_double_from_m, - schechter_evolving, - schechter_evolving_from_m, + double_schechter, + double_schechter_from_m, + evolving_schechter, + evolving_schechter_from_m, schechter_from_m, ) @@ -36,11 +36,11 @@ class LFModelSpec(TypedDict): "requires_z": False, }, "evolving_schechter": { - "function": schechter_evolving, + "function": evolving_schechter, "requires_z": True, }, "double_schechter": { - "function": schechter_double, + "function": double_schechter, "requires_z": False, }, "conditional_schechter": { @@ -48,22 +48,22 @@ class LFModelSpec(TypedDict): "requires_z": True, }, "conditional_evolving_schechter": { - "function": conditional_schechter_evolving, + "function": conditional_evolving_schechter, "requires_z": True, }, "conditional_double_schechter": { - "function": conditional_schechter_double, + "function": conditional_double_schechter, "requires_z": True, }, - "central_lognormal_conditional": { + "lognormal_conditional_lf": { "function": lognormal_conditional_lf, "requires_z": True, }, - "satellite_modified_schechter_conditional": { + "modified_schechter_conditional_lf": { "function": modified_schechter_conditional_lf, "requires_z": True, }, - "central_satellite_conditional": { + "two_component_conditional_lf": { "function": two_component_conditional_lf, "requires_z": True, }, @@ -72,6 +72,6 @@ class LFModelSpec(TypedDict): LF_FROM_M_MODELS: dict[str, Callable[..., Any]] = { "schechter": schechter_from_m, - "evolving_schechter": schechter_evolving_from_m, - "double_schechter": schechter_double_from_m, + "evolving_schechter": evolving_schechter_from_m, + "double_schechter": double_schechter_from_m, } diff --git a/src/lfkit/api/_luminosities.py b/src/lfkit/api/_luminosities.py index e69de29..efb7824 100644 --- a/src/lfkit/api/_luminosities.py +++ b/src/lfkit/api/_luminosities.py @@ -0,0 +1,36 @@ +"""User-facing luminosity and magnitude conversion API namespace.""" + +from __future__ import annotations + +from lfkit.photometry.luminosities import ( + luminosity_from_magnitude, + luminosity_ratio, + luminosity_ratio_from_magnitudes, + luminosity_weight_from_magnitude, + magnitude_difference_from_luminosity_ratio, + sample_schechter_luminosity, + schechter_cumulative_number_density_luminosity, + schechter_luminosity_density, + schechter_mean_luminosity, + schechter_selection_function, +) + + +class LFLuminositiesAPI: + """Grouped API for luminosity, magnitude, and Schechter-luminosity helpers.""" + + ratio = staticmethod(luminosity_ratio) + ratio_from_magnitudes = staticmethod(luminosity_ratio_from_magnitudes) + magnitude_difference_from_ratio = staticmethod( + magnitude_difference_from_luminosity_ratio + ) + weight_from_magnitude = staticmethod(luminosity_weight_from_magnitude) + from_magnitude = staticmethod(luminosity_from_magnitude) + + schechter_cumulative_number_density = staticmethod( + schechter_cumulative_number_density_luminosity + ) + schechter_luminosity_density = staticmethod(schechter_luminosity_density) + schechter_mean_luminosity = staticmethod(schechter_mean_luminosity) + sample_schechter = staticmethod(sample_schechter_luminosity) + schechter_selection = staticmethod(schechter_selection_function) diff --git a/src/lfkit/api/_magnitudes.py b/src/lfkit/api/_magnitudes.py index e69de29..0190687 100644 --- a/src/lfkit/api/_magnitudes.py +++ b/src/lfkit/api/_magnitudes.py @@ -0,0 +1,27 @@ +"""User-facing magnitude conversion API namespace.""" + +from __future__ import annotations + +from lfkit.photometry.magnitudes import ( + absolute_magnitude, + absolute_magnitude_from_luminosity_distance, + apparent_magnitude, + apparent_magnitude_from_luminosity_distance, + total_magnitude_correction, +) + + +class LFMagnitudesAPI: + """Grouped API for apparent- and absolute-magnitude conversions.""" + + correction = staticmethod(total_magnitude_correction) + + absolute = staticmethod(absolute_magnitude) + absolute_from_luminosity_distance = staticmethod( + absolute_magnitude_from_luminosity_distance + ) + + apparent = staticmethod(apparent_magnitude) + apparent_from_luminosity_distance = staticmethod( + apparent_magnitude_from_luminosity_distance + ) diff --git a/src/lfkit/api/_redshift_density.py b/src/lfkit/api/_redshift_density.py index e69de29..d732756 100644 --- a/src/lfkit/api/_redshift_density.py +++ b/src/lfkit/api/_redshift_density.py @@ -0,0 +1,39 @@ +"""User-facing LF redshift-density API namespace.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from lfkit.api._expose import expose_lf_function +from lfkit.photometry.lf_redshift_density import ( + lf_integrated_number_density, + lf_weighted_redshift_density, +) + +if TYPE_CHECKING: + from lfkit.api.luminosity_function import LuminosityFunction + + +class LFRedshiftDensityAPI: + """Grouped API for LF-weighted redshift-density calculations. + + Args: + lf: Parent luminosity-function object. + """ + + def __init__(self, lf: LuminosityFunction) -> None: + self.lf = lf + + +_REDSHIFT_DENSITY_METHODS = { + "integrated_number_density": lf_integrated_number_density, + "weighted": lf_weighted_redshift_density, +} + + +for method_name, function in _REDSHIFT_DENSITY_METHODS.items(): + setattr( + LFRedshiftDensityAPI, + method_name, + expose_lf_function(function, lf_arg_position=1), + ) diff --git a/src/lfkit/api/conditional_luminosity_function.py b/src/lfkit/api/conditional_luminosity_function.py index e69de29..90bc7f1 100644 --- a/src/lfkit/api/conditional_luminosity_function.py +++ b/src/lfkit/api/conditional_luminosity_function.py @@ -0,0 +1,161 @@ +"""Public conditional luminosity-function constructors.""" + +from __future__ import annotations + +from collections.abc import Mapping + +from lfkit.api.luminosity_function import LuminosityFunction +from lfkit.utils.types import ConditionalParameter, ParameterValue + + +__all__ = ["ConditionalLuminosityFunction"] + + +def _make_conditional_lf( + *, + model: str, + parameters: Mapping[str, object], + meta: Mapping[str, object] | None, +) -> LuminosityFunction: + """Create a LuminosityFunction backed by a conditional LF model.""" + return LuminosityFunction( + model=model, + parameters=parameters, + meta=meta, + ) + + +class ConditionalLuminosityFunction: + """Factory namespace for conditional luminosity-function models.""" + + @staticmethod + def schechter( + *, + phi_star: ConditionalParameter, + m_star: ConditionalParameter, + alpha: ConditionalParameter, + meta: Mapping[str, object] | None = None, + ) -> LuminosityFunction: + """Create a conditional Schechter luminosity function.""" + return _make_conditional_lf( + model="conditional_schechter", + parameters={ + "phi_star": phi_star, + "m_star": m_star, + "alpha": alpha, + }, + meta=meta, + ) + + @staticmethod + def evolving_schechter( + *, + phi_model: str = "linear_p", + phi_kwargs: Mapping[str, ParameterValue] | None = None, + m_star_model: str = "linear_q", + m_star_kwargs: Mapping[str, ParameterValue] | None = None, + alpha_model: str = "constant", + alpha_kwargs: Mapping[str, ParameterValue] | None = None, + meta: Mapping[str, object] | None = None, + ) -> LuminosityFunction: + """Create a conditional evolving Schechter luminosity function.""" + return _make_conditional_lf( + model="conditional_evolving_schechter", + parameters={ + "phi_model": phi_model, + "phi_kwargs": {} if phi_kwargs is None else dict(phi_kwargs), + "m_star_model": m_star_model, + "m_star_kwargs": {} if m_star_kwargs is None else dict(m_star_kwargs), + "alpha_model": alpha_model, + "alpha_kwargs": {} if alpha_kwargs is None else dict(alpha_kwargs), + }, + meta=meta, + ) + + @staticmethod + def double_schechter( + *, + phi_star: ConditionalParameter, + m_star: ConditionalParameter, + alpha: float, + beta: float, + m_transition: ConditionalParameter, + meta: Mapping[str, object] | None = None, + ) -> LuminosityFunction: + """Create a conditional double-power-law Schechter luminosity function.""" + return _make_conditional_lf( + model="conditional_double_schechter", + parameters={ + "phi_star": phi_star, + "m_star": m_star, + "alpha": alpha, + "beta": beta, + "m_transition": m_transition, + }, + meta=meta, + ) + + @staticmethod + def lognormal( + *, + mean_absolute_mag: ConditionalParameter, + sigma_log_luminosity: ConditionalParameter, + amplitude: ConditionalParameter = 1.0, + meta: Mapping[str, object] | None = None, + ) -> LuminosityFunction: + """Create a lognormal conditional luminosity function.""" + return _make_conditional_lf( + model="lognormal_conditional_lf", + parameters={ + "mean_absolute_mag": mean_absolute_mag, + "sigma_log_luminosity": sigma_log_luminosity, + "amplitude": amplitude, + }, + meta=meta, + ) + + @staticmethod + def modified_schechter( + *, + phi_star: ConditionalParameter, + m_star: ConditionalParameter, + alpha: ConditionalParameter, + meta: Mapping[str, object] | None = None, + ) -> LuminosityFunction: + """Create a modified Schechter conditional luminosity function.""" + return _make_conditional_lf( + model="modified_schechter_conditional_lf", + parameters={ + "phi_star": phi_star, + "m_star": m_star, + "alpha": alpha, + }, + meta=meta, + ) + + @staticmethod + def two_component( + *, + lognormal_mean_absolute_mag: ConditionalParameter, + lognormal_sigma_log_luminosity: ConditionalParameter, + modified_phi_star: ConditionalParameter, + modified_alpha: ConditionalParameter, + lognormal_amplitude: ConditionalParameter = 1.0, + modified_m_star: ConditionalParameter | None = None, + modified_luminosity_fraction: ConditionalParameter = 0.562, + meta: Mapping[str, object] | None = None, + ) -> LuminosityFunction: + """Create a two-component conditional luminosity function.""" + return _make_conditional_lf( + model="two_component_conditional_lf", + parameters={ + "lognormal_mean_absolute_mag": lognormal_mean_absolute_mag, + "lognormal_sigma_log_luminosity": lognormal_sigma_log_luminosity, + "lognormal_amplitude": lognormal_amplitude, + "modified_phi_star": modified_phi_star, + "modified_alpha": modified_alpha, + "modified_m_star": modified_m_star, + "modified_luminosity_fraction": modified_luminosity_fraction, + }, + meta=meta, + ) diff --git a/src/lfkit/api/luminosity_function.py b/src/lfkit/api/luminosity_function.py index e69de29..545282c 100644 --- a/src/lfkit/api/luminosity_function.py +++ b/src/lfkit/api/luminosity_function.py @@ -0,0 +1,370 @@ +r"""Public luminosity-function interface. + +This module provides the user-facing :class:`LuminosityFunction` API for +evaluating luminosity functions in absolute- or apparent-magnitude space. + +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. +""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING + +import numpy as np + +from lfkit.api._lf_param_models import LF_FROM_M_MODELS, LF_MODELS +from lfkit.api._completeness import LFCompletenessAPI +from lfkit.api._integrals import LFIntegralsAPI +from lfkit.api._luminosities import LFLuminositiesAPI +from lfkit.api._magnitudes import LFMagnitudesAPI +from lfkit.photometry.lf_parameter_models import ( + available_lf_parameter_models, + evaluate_lf_parameters, + register_alpha_model, + register_m_star_model, + register_phi_star_model, +) +from lfkit.api._redshift_density import LFRedshiftDensityAPI +from lfkit.utils.types import ( + Cosmology, + FloatArray, + FloatInput, + ParameterModel, + ParameterValue, +) + +if TYPE_CHECKING: + from lfkit.api.corrections import Corrections +else: + Corrections = object + + +__all__ = ["LuminosityFunction"] + + +class LuminosityFunction: + """User-facing wrapper for luminosity-function evaluation. + + Args: + 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. + """ + + def __init__( + self, + *, + model: str, + parameters: Mapping[str, object], + meta: Mapping[str, object] | None = None, + ) -> None: + self.model = str(model) + self.parameters_dict = dict(parameters) + self.meta = {} if meta is None else dict(meta) + + self.integrals = LFIntegralsAPI(self) + self.redshift_density = LFRedshiftDensityAPI(self) + self.completeness = LFCompletenessAPI(self) + self.luminosities = LFLuminositiesAPI() + self.magnitudes = LFMagnitudesAPI() + + @classmethod + def schechter( + cls, + *, + phi_star: ParameterValue, + m_star: ParameterValue, + alpha: ParameterValue, + meta: Mapping[str, object] | None = None, + ) -> LuminosityFunction: + """Create a standard Schechter luminosity function. + + Args: + phi_star: Normalization of the luminosity function. + m_star: Characteristic absolute magnitude. + alpha: Faint-end slope. + meta: Optional metadata describing the LF source or calibration. + + Returns: + Luminosity-function API object using the standard Schechter model. + """ + return cls( + model="schechter", + parameters={ + "phi_star": phi_star, + "m_star": m_star, + "alpha": alpha, + }, + meta=meta, + ) + + @classmethod + def evolving_schechter( + cls, + *, + phi_model: str = "linear_p", + phi_kwargs: Mapping[str, ParameterValue] | None = None, + m_star_model: str = "linear_q", + m_star_kwargs: Mapping[str, ParameterValue] | None = None, + alpha_model: str = "constant", + alpha_kwargs: Mapping[str, ParameterValue] | None = None, + meta: Mapping[str, object] | None = None, + ) -> LuminosityFunction: + """Create a redshift-evolving Schechter luminosity function. + + Args: + phi_model: Parameter model used for the normalization evolution. + phi_kwargs: Keyword arguments for the normalization model. + m_star_model: Parameter model used for characteristic-magnitude evolution. + m_star_kwargs: Keyword arguments for the characteristic-magnitude model. + alpha_model: Parameter model used for faint-end-slope evolution. + alpha_kwargs: Keyword arguments for the faint-end-slope model. + meta: Optional metadata describing the LF source or calibration. + + Returns: + Luminosity-function API object using an evolving Schechter model. + """ + return cls( + model="evolving_schechter", + parameters={ + "phi_model": phi_model, + "phi_kwargs": {} if phi_kwargs is None else dict(phi_kwargs), + "m_star_model": m_star_model, + "m_star_kwargs": {} if m_star_kwargs is None else dict(m_star_kwargs), + "alpha_model": alpha_model, + "alpha_kwargs": {} if alpha_kwargs is None else dict(alpha_kwargs), + }, + meta=meta, + ) + + @classmethod + def double_schechter( + cls, + *, + phi_star: ParameterValue, + m_star: ParameterValue, + alpha: float, + beta: float, + m_transition: ParameterValue, + meta: Mapping[str, object] | None = None, + ) -> LuminosityFunction: + """Create a double-power-law Schechter luminosity function. + + Args: + phi_star: Normalization of the luminosity function. + m_star: Characteristic absolute magnitude. + alpha: Bright-end or main Schechter slope. + beta: Additional slope controlling the second power-law component. + m_transition: Transition magnitude for the second component. + meta: Optional metadata describing the LF source or calibration. + + Returns: + Luminosity-function API object using the double Schechter model. + """ + return cls( + model="double_schechter", + parameters={ + "phi_star": phi_star, + "m_star": m_star, + "alpha": alpha, + "beta": beta, + "m_transition": m_transition, + }, + meta=meta, + ) + + def phi( + self, + absolute_mag: FloatInput, + z: FloatInput | None = None, + ) -> FloatArray: + """Evaluate the luminosity function in absolute-magnitude space. + + Args: + absolute_mag: Absolute magnitude values where the LF is evaluated. + z: Redshift or conditional-coordinate values. Required for evolving + and conditional models. + + Returns: + Luminosity-function values evaluated at the input magnitudes. + """ + try: + model_spec = LF_MODELS[self.model] + except KeyError as exc: + raise ValueError( + f"Unsupported luminosity function model '{self.model}'." + ) from exc + + absolute_mag_arr = np.asarray(absolute_mag, dtype=float) + + if model_spec["requires_z"]: + if z is None: + raise ValueError( + f"z is required for luminosity function model '{self.model}'." + ) + + return model_spec["function"]( + absolute_mag_arr, + np.asarray(z, dtype=float), + **self.parameters_dict, + ) + + return model_spec["function"]( + absolute_mag_arr, + **self.parameters_dict, + ) + + def phi_from_m( + self, + cosmo_obj: Cosmology, + z: FloatInput, + apparent_mag: FloatInput, + *, + h: float | None = None, + corrections: Corrections | None = None, + ) -> FloatArray: + """Evaluate the luminosity function from apparent magnitudes. + + Apparent magnitudes are converted to absolute magnitudes using the + supplied cosmology, optional reduced Hubble parameter, and optional + k- and e-correction model. + + Args: + cosmo_obj: Cosmology object used for distance-modulus conversion. + z: Redshift values. + apparent_mag: Apparent magnitude values. + h: Optional reduced Hubble parameter used in the magnitude conversion. + corrections: Optional object providing k-correction and e-correction values. + + Returns: + Luminosity-function values evaluated from apparent magnitudes. + """ + try: + function = LF_FROM_M_MODELS[self.model] + except KeyError as exc: + raise ValueError( + f"phi_from_m is not defined for luminosity function model " + f"'{self.model}'." + ) from exc + + k_corr, e_corr = self._correction_values(corrections, z) + + return function( + cosmo_obj, + np.asarray(z, dtype=float), + np.asarray(apparent_mag, dtype=float), + h=h, + k_correction=k_corr, + e_correction=e_corr, + **self.parameters_dict, + ) + + def parameters( + self, + z: FloatInput, + ) -> tuple[FloatArray, FloatArray, FloatArray]: + """Evaluate evolving Schechter parameters at redshift. + + Args: + z: Redshift values where the evolving LF parameters are evaluated. + + Returns: + Tuple containing ``phi_star(z)``, ``m_star(z)``, and ``alpha(z)``. + """ + if self.model != "evolving_schechter": + raise ValueError("parameters(z) is only defined for evolving_schechter.") + + return evaluate_lf_parameters( + np.asarray(z, dtype=float), + **self.parameters_dict, + ) + + def _as_callable(self): + """Return this object as an ``lf(M, z)`` callable.""" + return lambda absolute_mag, z: self.phi(absolute_mag, z) + + @staticmethod + def available_models() -> list[str]: + """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 sorted(LF_FROM_M_MODELS) + + @staticmethod + def available_parameter_models() -> dict[str, list[str]]: + """Return available LF parameter evolution models.""" + return available_lf_parameter_models() + + @staticmethod + def register_phi_star_model( + name: str, + model: ParameterModel, + *, + overwrite: bool = False, + ) -> None: + """Register a phi-star evolution model. + + Args: + name: Name used to identify the model. + model: Callable evaluating ``phi_star(z)``. + overwrite: If True, replace an existing model with the same name. + """ + register_phi_star_model(name, model, overwrite=overwrite) + + @staticmethod + def register_m_star_model( + name: str, + model: ParameterModel, + *, + overwrite: bool = False, + ) -> None: + """Register an M-star evolution model. + + Args: + name: Name used to identify the model. + model: Callable evaluating ``M_star(z)``. + overwrite: If True, replace an existing model with the same name. + """ + register_m_star_model(name, model, overwrite=overwrite) + + @staticmethod + def register_alpha_model( + name: str, + model: ParameterModel, + *, + overwrite: bool = False, + ) -> None: + """Register an alpha evolution model. + + Args: + name: Name used to identify the model. + model: Callable evaluating ``alpha(z)``. + overwrite: If True, replace an existing model with the same name. + """ + register_alpha_model(name, model, overwrite=overwrite) + + @staticmethod + def _correction_values( + corrections: Corrections | None, + z: FloatInput, + ) -> tuple[FloatArray | None, FloatArray | None]: + """Evaluate optional correction values at redshift. + + Args: + corrections: Optional correction object with ``k(z)`` and ``e(z)`` methods. + z: Redshift values where corrections are evaluated. + + Returns: + Tuple of k-correction and e-correction arrays, or ``None`` values + when no correction object is supplied. + """ + if corrections is None: + return None, None + + return corrections.k(z), corrections.e(z) From 750c36ae960be27555ed2f8b8e69e255f764fec4 Mon Sep 17 00:00:00 2001 From: "niko.sarcevic" Date: Fri, 15 May 2026 13:10:36 -0400 Subject: [PATCH 3/6] udpated tests to match the new api --- ...est_api_conditional_luminosity_function.py | 192 ++++++++ tests/test_api_luminosity_function.py | 421 ++++++++++++++++++ .../test_photometry_conditional_lf_models.py | 26 +- tests/test_photometry_luminosity_function.py | 40 +- 4 files changed, 646 insertions(+), 33 deletions(-) diff --git a/tests/test_api_conditional_luminosity_function.py b/tests/test_api_conditional_luminosity_function.py index e69de29..ef990a7 100644 --- a/tests/test_api_conditional_luminosity_function.py +++ b/tests/test_api_conditional_luminosity_function.py @@ -0,0 +1,192 @@ +"""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. +They intentionally avoid testing conditional LF physics, which is covered by +the lower-level photometry tests. +""" + +from __future__ import annotations + +from lfkit.api.conditional_luminosity_function import ConditionalLuminosityFunction +from lfkit.api.luminosity_function import LuminosityFunction + + +def test_conditional_schechter_constructor_delegates_to_luminosity_function(): + lf = ConditionalLuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + meta={"source": "test"}, + ) + + assert isinstance(lf, LuminosityFunction) + assert lf.model == "conditional_schechter" + assert lf.parameters_dict == { + "phi_star": 1.0e-3, + "m_star": -20.5, + "alpha": -1.1, + } + assert lf.meta == {"source": "test"} + + +def test_conditional_evolving_schechter_constructor_delegates_to_luminosity_function(): + lf = ConditionalLuminosityFunction.evolving_schechter( + phi_model="linear_p", + phi_kwargs={"phi_star0": 1.0e-3, "p": 0.2}, + m_star_model="linear_q", + m_star_kwargs={"m_star0": -20.5, "q": 0.5}, + alpha_model="constant", + alpha_kwargs={"value": -1.1}, + meta={"source": "test"}, + ) + + assert isinstance(lf, LuminosityFunction) + assert lf.model == "conditional_evolving_schechter" + assert lf.parameters_dict == { + "phi_model": "linear_p", + "phi_kwargs": {"phi_star0": 1.0e-3, "p": 0.2}, + "m_star_model": "linear_q", + "m_star_kwargs": {"m_star0": -20.5, "q": 0.5}, + "alpha_model": "constant", + "alpha_kwargs": {"value": -1.1}, + } + assert lf.meta == {"source": "test"} + + +def test_conditional_evolving_schechter_constructor_uses_empty_default_kwargs(): + lf = ConditionalLuminosityFunction.evolving_schechter() + + assert isinstance(lf, LuminosityFunction) + assert lf.model == "conditional_evolving_schechter" + assert lf.parameters_dict == { + "phi_model": "linear_p", + "phi_kwargs": {}, + "m_star_model": "linear_q", + "m_star_kwargs": {}, + "alpha_model": "constant", + "alpha_kwargs": {}, + } + assert lf.meta == {} + + +def test_conditional_double_schechter_constructor_delegates_to_luminosity_function(): + lf = ConditionalLuminosityFunction.double_schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + beta=-1.5, + m_transition=-19.5, + meta={"source": "test"}, + ) + + assert isinstance(lf, LuminosityFunction) + assert lf.model == "conditional_double_schechter" + assert lf.parameters_dict == { + "phi_star": 1.0e-3, + "m_star": -20.5, + "alpha": -1.1, + "beta": -1.5, + "m_transition": -19.5, + } + assert lf.meta == {"source": "test"} + + +def test_lognormal_constructor_delegates_to_luminosity_function(): + lf = ConditionalLuminosityFunction.lognormal( + mean_absolute_mag=-20.5, + sigma_log_luminosity=0.2, + amplitude=2.0, + meta={"source": "test"}, + ) + + assert isinstance(lf, LuminosityFunction) + assert lf.model == "lognormal_conditional_lf" + assert lf.parameters_dict == { + "mean_absolute_mag": -20.5, + "sigma_log_luminosity": 0.2, + "amplitude": 2.0, + } + assert lf.meta == {"source": "test"} + + +def test_lognormal_constructor_uses_default_amplitude(): + lf = ConditionalLuminosityFunction.lognormal( + mean_absolute_mag=-20.5, + sigma_log_luminosity=0.2, + ) + + assert isinstance(lf, LuminosityFunction) + assert lf.model == "lognormal_conditional_lf" + assert lf.parameters_dict == { + "mean_absolute_mag": -20.5, + "sigma_log_luminosity": 0.2, + "amplitude": 1.0, + } + assert lf.meta == {} + + +def test_modified_schechter_constructor_delegates_to_luminosity_function(): + lf = ConditionalLuminosityFunction.modified_schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + meta={"source": "test"}, + ) + + assert isinstance(lf, LuminosityFunction) + assert lf.model == "modified_schechter_conditional_lf" + assert lf.parameters_dict == { + "phi_star": 1.0e-3, + "m_star": -20.5, + "alpha": -1.1, + } + assert lf.meta == {"source": "test"} + + +def test_two_component_constructor_delegates_to_luminosity_function(): + lf = ConditionalLuminosityFunction.two_component( + lognormal_mean_absolute_mag=-21.0, + lognormal_sigma_log_luminosity=0.2, + lognormal_amplitude=2.0, + modified_phi_star=1.0e-3, + modified_alpha=-1.1, + modified_m_star=-20.0, + modified_luminosity_fraction=0.6, + meta={"source": "test"}, + ) + + assert isinstance(lf, LuminosityFunction) + assert lf.model == "two_component_conditional_lf" + assert lf.parameters_dict == { + "lognormal_mean_absolute_mag": -21.0, + "lognormal_sigma_log_luminosity": 0.2, + "lognormal_amplitude": 2.0, + "modified_phi_star": 1.0e-3, + "modified_alpha": -1.1, + "modified_m_star": -20.0, + "modified_luminosity_fraction": 0.6, + } + assert lf.meta == {"source": "test"} + + +def test_two_component_constructor_uses_default_optional_parameters(): + lf = ConditionalLuminosityFunction.two_component( + lognormal_mean_absolute_mag=-21.0, + lognormal_sigma_log_luminosity=0.2, + modified_phi_star=1.0e-3, + modified_alpha=-1.1, + ) + + assert isinstance(lf, LuminosityFunction) + assert lf.model == "two_component_conditional_lf" + assert lf.parameters_dict == { + "lognormal_mean_absolute_mag": -21.0, + "lognormal_sigma_log_luminosity": 0.2, + "lognormal_amplitude": 1.0, + "modified_phi_star": 1.0e-3, + "modified_alpha": -1.1, + "modified_m_star": None, + "modified_luminosity_fraction": 0.562, + } + assert lf.meta == {} diff --git a/tests/test_api_luminosity_function.py b/tests/test_api_luminosity_function.py index e69de29..f314b27 100644 --- a/tests/test_api_luminosity_function.py +++ b/tests/test_api_luminosity_function.py @@ -0,0 +1,421 @@ +"""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 +physics, which is covered by the lower-level photometry tests. +""" + +from __future__ import annotations + +import numpy as np +import pytest + +import pyccl as ccl + +from lfkit.api._expose import expose_lf_function +from lfkit.api.luminosity_function import LuminosityFunction + + +def make_test_cosmology(): + return ccl.Cosmology( + Omega_c=0.25, + Omega_b=0.05, + h=0.7, + sigma8=0.8, + n_s=0.96, + transfer_function="bbks", + matter_power_spectrum="linear", + ) + + +def test_luminosity_function_initializes_grouped_api_namespaces(): + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + assert hasattr(lf, "integrals") + assert hasattr(lf, "redshift_density") + assert hasattr(lf, "completeness") + assert hasattr(lf, "luminosities") + assert hasattr(lf, "magnitudes") + + +def test_luminosity_function_does_not_initialize_conditional_models_namespace(): + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + assert not hasattr(lf, "conditional_models") + + +def test_expose_lf_function_injects_lf_callable_by_position(): + calls = {} + + def low_level(x, lf_callable, z, *, scale=1.0): + calls["x"] = x + calls["z"] = z + calls["scale"] = scale + calls["lf_value"] = lf_callable(x, z) + return scale * calls["lf_value"] + + class Parent: + def _as_callable(self): + return lambda absolute_mag, redshift: absolute_mag + redshift + + class API: + def __init__(self): + self.lf = Parent() + + API.method = expose_lf_function(low_level, lf_arg_position=1) + + api = API() + result = api.method(2.0, 3.0, scale=4.0) + + assert result == 20.0 + assert calls["x"] == 2.0 + assert calls["z"] == 3.0 + assert calls["scale"] == 4.0 + assert calls["lf_value"] == 5.0 + + +def test_expose_lf_function_injects_lf_callable_by_keyword(): + calls = {} + + def low_level(x, z, *, lf_callable): + calls["lf_value"] = lf_callable(x, z) + return calls["lf_value"] + + class Parent: + def _as_callable(self): + return lambda absolute_mag, redshift: absolute_mag * redshift + + class API: + def __init__(self): + self.lf = Parent() + + API.method = expose_lf_function( + low_level, + lf_arg_position=None, + lf_arg_name="lf_callable", + ) + + api = API() + result = api.method(2.0, 3.0) + + assert result == 6.0 + assert calls["lf_value"] == 6.0 + + +def test_phi_evaluates_schechter_model(): + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + result = lf.phi(-20.0) + + assert np.asarray(result).shape == () + assert np.isfinite(result) + + +def test_evolving_schechter_constructor_stores_model_and_parameter(): + lf = LuminosityFunction.evolving_schechter( + phi_model="constant", + phi_kwargs={"phi_star": 1.0e-3}, + m_star_model="constant", + m_star_kwargs={"m_star": -20.5}, + alpha_model="constant", + alpha_kwargs={"alpha": -1.1}, + meta={"source": "test"}, + ) + + assert lf.model == "evolving_schechter" + assert lf.parameters_dict == { + "phi_model": "constant", + "phi_kwargs": {"phi_star": 1.0e-3}, + "m_star_model": "constant", + "m_star_kwargs": {"m_star": -20.5}, + "alpha_model": "constant", + "alpha_kwargs": {"alpha": -1.1}, + } + assert lf.meta == {"source": "test"} + + +def test_phi_evaluates_double_schechter_model(): + lf = LuminosityFunction.double_schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + beta=-1.5, + m_transition=-19.5, + ) + + result = lf.phi(-20.0) + + assert np.asarray(result).shape == () + assert np.isfinite(result) + + +def test_phi_from_m_evaluates_supported_model(): + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + cosmo = make_test_cosmology() + + result = lf.phi_from_m( + cosmo, + 0.5, + 24.0, + h=0.7, + ) + + assert np.asarray(result).shape == () + assert np.isfinite(result) + + +def test_phi_requires_redshift_for_evolving_model(): + lf = LuminosityFunction.evolving_schechter() + + with pytest.raises(ValueError, match="z is required"): + lf.phi(-20.0) + + +def test_parameters_raises_for_non_evolving_model(): + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + with pytest.raises(ValueError, match="only defined for evolving_schechter"): + lf.parameters(0.5) + + +def test_integrals_namespace_delegates_to_bound_lf_callable(): + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + result = lf.integrals.number_density( + 0.5, + m_bright=-24.0, + m_faint=-18.0, + n_m=32, + ) + + assert np.asarray(result).shape == () + assert np.isfinite(result) + + +def test_completeness_namespace_delegates_to_bound_lf_callable(): + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + cosmo = make_test_cosmology() + + result = lf.completeness.catalog_fraction( + cosmo, + 0.5, + m_lim=24.0, + m_bright=-24.0, + m_faint=-16.0, + n_m=32, + h=0.7, + ) + + assert np.asarray(result).shape == () + assert np.isfinite(result) + assert 0.0 <= float(result) <= 1.0 + + +def test_completeness_absolute_magnitude_limit_is_static_helper(): + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + cosmo = make_test_cosmology() + + result = lf.completeness.absolute_magnitude_limit( + cosmo, + 0.5, + m_lim=24.0, + h=0.7, + ) + + assert np.asarray(result).shape == () + assert np.isfinite(result) + + +def test_magnitude_namespace_static_helpers_are_available(): + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + result = lf.magnitudes.absolute_from_luminosity_distance( + 24.0, + 1000.0, + ) + + assert np.asarray(result).shape == () + assert np.isfinite(result) + + +def test_luminosity_namespace_static_helpers_are_available(): + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + result = lf.luminosities.ratio_from_magnitudes(-21.0, -20.0) + + assert np.asarray(result).shape == () + assert np.isfinite(result) + + +def test_unsupported_model_raises_clear_error(): + lf = LuminosityFunction( + model="not_a_model", + parameters={}, + ) + + with pytest.raises(ValueError, match="Unsupported luminosity function model"): + lf.phi(-20.0, 0.5) + + +def test_unsupported_phi_from_m_model_raises_clear_error(): + lf = LuminosityFunction( + model="not_a_model", + parameters={}, + ) + cosmo = make_test_cosmology() + + with pytest.raises(ValueError, match="phi_from_m is not defined"): + lf.phi_from_m(cosmo, 0.5, 24.0, h=0.7) + + +def test_available_model_helpers_return_public_model_names(): + assert "schechter" in LuminosityFunction.available_models() + assert "evolving_schechter" in LuminosityFunction.available_models() + assert "schechter" in LuminosityFunction.available_from_m_models() + + +def test_available_parameter_models_returns_grouped_registry_names(): + models = LuminosityFunction.available_parameter_models() + + assert "phi_star" in models + assert "m_star" in models + assert "alpha" in models + + +def test_integrals_namespace_exposes_expected_methods(): + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + expected = [ + "number_density", + "weighted", + "selection_weighted_number_density", + "luminosity_density", + "mean_luminosity", + "cumulative_number_density", + "magnitude_window_number_density", + ] + + for name in expected: + assert callable(getattr(lf.integrals, name)) + + +def test_redshift_density_namespace_exposes_expected_methods(): + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + expected = [ + "integrated_number_density", + "weighted", + ] + + for name in expected: + assert callable(getattr(lf.redshift_density, name)) + + +def test_completeness_namespace_exposes_expected_methods(): + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + expected = [ + "observed_number_density", + "missing_number_density", + "catalog_fraction", + "out_of_catalog_fraction", + "absolute_magnitude_limit", + ] + + for name in expected: + assert callable(getattr(lf.completeness, name)) + + +def test_magnitudes_namespace_exposes_expected_methods(): + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + expected = [ + "correction", + "absolute", + "absolute_from_luminosity_distance", + "apparent", + "apparent_from_luminosity_distance", + ] + + for name in expected: + assert callable(getattr(lf.magnitudes, name)) + + +def test_luminosities_namespace_exposes_expected_methods(): + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + expected = [ + "ratio", + "ratio_from_magnitudes", + "magnitude_difference_from_ratio", + "weight_from_magnitude", + "from_magnitude", + "schechter_cumulative_number_density", + "schechter_luminosity_density", + "schechter_mean_luminosity", + "sample_schechter", + "schechter_selection", + ] + + for name in expected: + assert callable(getattr(lf.luminosities, name)) diff --git a/tests/test_photometry_conditional_lf_models.py b/tests/test_photometry_conditional_lf_models.py index c4dd2d0..e157d26 100644 --- a/tests/test_photometry_conditional_lf_models.py +++ b/tests/test_photometry_conditional_lf_models.py @@ -5,8 +5,8 @@ from lfkit.photometry.conditional_lf_models import ( conditional_schechter, - conditional_schechter_double, - conditional_schechter_evolving, + conditional_double_schechter, + conditional_evolving_schechter, lognormal_conditional_lf, modified_schechter_conditional_lf, two_component_conditional_lf, @@ -15,7 +15,7 @@ luminosity_ratio, magnitude_difference_from_luminosity_ratio, ) -from lfkit.photometry.luminosity_function import schechter, schechter_double +from lfkit.photometry.luminosity_function import schechter, double_schechter def test_conditional_schechter_matches_schechter_for_scalar_parameters() -> None: @@ -94,13 +94,13 @@ def test_conditional_schechter_rejects_non_finite_callable_parameter() -> None: ) -def test_conditional_schechter_evolving_matches_explicit_parameter_models() -> None: +def test_conditional_evolving_schechter_matches_explicit_parameter_models() -> None: """Tests the conditional evolving Schechter wrapper with simple models.""" absolute_mag = np.array([-22.0, -21.0, -20.0]) condition = np.array([0.0, 1.0, 2.0]) - result = conditional_schechter_evolving( + result = conditional_evolving_schechter( absolute_mag=absolute_mag, condition=condition, phi_model="constant", @@ -122,11 +122,11 @@ def test_conditional_schechter_evolving_matches_explicit_parameter_models() -> N assert result.dtype == np.float64 -def test_conditional_schechter_evolving_rejects_unknown_model() -> None: +def test_conditional_evolving_schechter_rejects_unknown_model() -> None: """Tests that unknown LF parameter models are rejected.""" with pytest.raises(ValueError): - conditional_schechter_evolving( + conditional_evolving_schechter( absolute_mag=[-22.0, -21.0, -20.0], condition=[0.0, 1.0, 2.0], phi_model="not_a_model", @@ -138,13 +138,13 @@ def test_conditional_schechter_evolving_rejects_unknown_model() -> None: ) -def test_conditional_schechter_double_matches_double_schechter() -> None: +def test_conditional_double_schechter_matches_double_schechter() -> None: """Tests that the conditional double-Schechter wrapper matches the model.""" absolute_mag = np.array([-22.0, -21.0, -20.0]) condition = np.array([0.0, 1.0, 2.0]) - result = conditional_schechter_double( + result = conditional_double_schechter( absolute_mag=absolute_mag, condition=condition, phi_star=1.0e-3, @@ -154,7 +154,7 @@ def test_conditional_schechter_double_matches_double_schechter() -> None: m_transition=-19.5, ) - expected = schechter_double( + expected = double_schechter( absolute_mag, phi_star=1.0e-3, m_star=-21.0, @@ -167,13 +167,13 @@ def test_conditional_schechter_double_matches_double_schechter() -> None: assert result.dtype == np.float64 -def test_conditional_schechter_double_accepts_callable_parameters() -> None: +def test_conditional_double_schechter_accepts_callable_parameters() -> None: """Tests callable parameters for the conditional double-Schechter model.""" absolute_mag = np.array([-22.0, -21.0, -20.0]) condition = np.array([0.0, 1.0, 2.0]) - result = conditional_schechter_double( + result = conditional_double_schechter( absolute_mag=absolute_mag, condition=condition, phi_star=lambda x: 1.0e-3 * (1.0 + x), @@ -183,7 +183,7 @@ def test_conditional_schechter_double_accepts_callable_parameters() -> None: m_transition=lambda x: -19.5 - 0.2 * x, ) - expected = schechter_double( + expected = double_schechter( absolute_mag, phi_star=np.array([1.0e-3, 2.0e-3, 3.0e-3]), m_star=np.array([-21.0, -21.1, -21.2]), diff --git a/tests/test_photometry_luminosity_function.py b/tests/test_photometry_luminosity_function.py index 5784c9e..01a1e0f 100644 --- a/tests/test_photometry_luminosity_function.py +++ b/tests/test_photometry_luminosity_function.py @@ -6,8 +6,8 @@ from lfkit.photometry.luminosities import luminosity_ratio from lfkit.photometry.luminosity_function import ( schechter, - schechter_evolving, - schechter_double, + evolving_schechter, + double_schechter, schechter_cumulative, schechter_cumulative_evolving, ) @@ -35,8 +35,8 @@ def test_schechter_negative_phi_star_error(): schechter(M, phi_star=-1.0, m_star=-20.0, alpha=-1.0) -def test_schechter_evolving_matches_constant_case(): - """Tests that schechter_evolving reduces to schechter for constant models.""" +def test_evolving_schechter_matches_constant_case(): + """Tests that evolving_schechter reduces to schechter for constant models.""" M = np.array([-20.0]) z = np.array([0.5]) @@ -47,7 +47,7 @@ def test_schechter_evolving_matches_constant_case(): alpha=-1.0, ) - phi2 = schechter_evolving( + phi2 = evolving_schechter( M, z, phi_model="constant", @@ -61,20 +61,20 @@ def test_schechter_evolving_matches_constant_case(): assert np.allclose(phi1, phi2) -def test_schechter_evolving_invalid_model(): - """Tests that schechter_evolving raises for invalid model names.""" +def test_evolving_schechter_invalid_model(): + """Tests that evolving_schechter raises for invalid model names.""" with pytest.raises(ValueError): - schechter_evolving( + evolving_schechter( [-20.0], [0.5], phi_model="invalid", ) -def test_schechter_double_positive(): - """Tests that schechter_double returns finite positive values.""" +def test_double_schechter_positive(): + """Tests that double_schechter returns finite positive values.""" M = np.linspace(-22, -18, 10) - phi = schechter_double( + phi = double_schechter( M, phi_star=1e-3, m_star=-20.0, @@ -86,10 +86,10 @@ def test_schechter_double_positive(): assert np.all(phi >= 0) -def test_schechter_double_invalid_alpha(): - """Tests that schechter_double raises for non-finite alpha.""" +def test_double_schechter_invalid_alpha(): + """Tests that double_schechter raises for non-finite alpha.""" with pytest.raises(ValueError): - schechter_double( + double_schechter( [-20.0], phi_star=1e-3, m_star=-20.0, @@ -220,11 +220,11 @@ def test_schechter_cumulative_evolving_matches_constant(): assert np.allclose(n1, n2) -def test_schechter_double_transition_effect(): - """Tests that schechter_double changes slope across transition magnitude.""" +def test_double_schechter_transition_effect(): + """Tests that double_schechter changes slope across transition magnitude.""" M = np.array([-19.0, -17.0]) - phi = schechter_double( + phi = double_schechter( M, phi_star=1e-3, m_star=-20.0, @@ -244,10 +244,10 @@ def test_schechter_extreme_magnitudes_finite(): assert np.all(np.isfinite(phi)) -def test_schechter_evolving_missing_kwargs(): - """Tests that schechter_evolving raises if required kwargs are missing.""" +def test_evolving_schechter_missing_kwargs(): + """Tests that evolving_schechter raises if required kwargs are missing.""" with pytest.raises(TypeError): - schechter_evolving( + evolving_schechter( [-20.0], [0.5], phi_model="linear_p", # requires phi_0_star and p From 678e2bde15abeb1fbe5260d4434267bc9aa5915e Mon Sep 17 00:00:00 2001 From: "niko.sarcevic" Date: Fri, 15 May 2026 13:11:10 -0400 Subject: [PATCH 4/6] udpated lf and clf to match the new api --- src/lfkit/photometry/conditional_lf_models.py | 14 ++++++------ src/lfkit/photometry/luminosity_function.py | 22 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/lfkit/photometry/conditional_lf_models.py b/src/lfkit/photometry/conditional_lf_models.py index 2cc1eaa..ccbb8b4 100644 --- a/src/lfkit/photometry/conditional_lf_models.py +++ b/src/lfkit/photometry/conditional_lf_models.py @@ -24,7 +24,7 @@ luminosity_ratio, magnitude_difference_from_luminosity_ratio, ) -from lfkit.photometry.luminosity_function import schechter, schechter_double +from lfkit.photometry.luminosity_function import schechter, double_schechter from lfkit.utils.types import ( ConditionalParameter, FloatArray, @@ -36,8 +36,8 @@ __all__ = [ "conditional_schechter", - "conditional_schechter_evolving", - "conditional_schechter_double", + "conditional_evolving_schechter", + "conditional_double_schechter", "lognormal_conditional_lf", "modified_schechter_conditional_lf", "two_component_conditional_lf", @@ -89,7 +89,7 @@ def conditional_schechter( ) -def conditional_schechter_evolving( +def conditional_evolving_schechter( absolute_mag: FloatInput, condition: FloatInput, *, @@ -102,7 +102,7 @@ def conditional_schechter_evolving( ) -> FloatArray: """Evaluate a conditional Schechter LF using LFKit parameter models. - This is the conditional LF analogue of ``schechter_evolving``. The + This is the conditional LF analogue of ``evolving_schechter``. The conditioning variable is passed to LFKit's registered parameter models. Args: @@ -141,7 +141,7 @@ def conditional_schechter_evolving( ) -def conditional_schechter_double( +def conditional_double_schechter( absolute_mag: FloatInput, condition: FloatInput, *, @@ -170,7 +170,7 @@ def conditional_schechter_double( """ condition_arr = validate_array(condition, name="condition") - return schechter_double( + return double_schechter( absolute_mag, phi_star=_evaluate_conditional_parameter( phi_star, diff --git a/src/lfkit/photometry/luminosity_function.py b/src/lfkit/photometry/luminosity_function.py index 2565573..f13dc98 100644 --- a/src/lfkit/photometry/luminosity_function.py +++ b/src/lfkit/photometry/luminosity_function.py @@ -1,4 +1,4 @@ -"""Luminosity function utilities for LFKit. +r"""Luminosity function utilities for LFKit. This module provides simple standalone functions for evaluating common galaxy luminosity function parameterization. @@ -63,11 +63,11 @@ __all__ = [ "schechter", - "schechter_evolving", - "schechter_double", + "evolving_schechter", + "double_schechter", "schechter_from_m", - "schechter_evolving_from_m", - "schechter_double_from_m", + "evolving_schechter_from_m", + "double_schechter_from_m", "schechter_cumulative", "schechter_cumulative_evolving", ] @@ -124,7 +124,7 @@ def schechter( return np.asarray(phi, dtype=float) -def schechter_evolving( +def evolving_schechter( absolute_mag: FloatInput, z: FloatInput, *, @@ -187,7 +187,7 @@ def schechter_evolving( ) -def schechter_double( +def double_schechter( absolute_mag: FloatInput, *, phi_star: ParameterValue, @@ -351,7 +351,7 @@ def schechter_from_m( ) -def schechter_evolving_from_m( +def evolving_schechter_from_m( cosmo_obj: Cosmology, z: FloatInput, apparent_mag: FloatInput, @@ -439,7 +439,7 @@ def schechter_evolving_from_m( abs_mag = validate_array(abs_mag, name="abs_mag") - return schechter_evolving( + return evolving_schechter( abs_mag, z, phi_model=phi_model, @@ -451,7 +451,7 @@ def schechter_evolving_from_m( ) -def schechter_double_from_m( +def double_schechter_from_m( cosmo_obj: Cosmology, z: FloatInput, apparent_mag: FloatInput, @@ -524,7 +524,7 @@ def schechter_double_from_m( abs_mag = validate_array(abs_mag, name="abs_mag") - return schechter_double( + return double_schechter( abs_mag, phi_star=phi_star, m_star=m_star, From 9129f262faa3fed3f3a6516d9b0cb75a8d038c5c Mon Sep 17 00:00:00 2001 From: "niko.sarcevic" Date: Fri, 15 May 2026 13:11:21 -0400 Subject: [PATCH 5/6] udpated main ini --- src/lfkit/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lfkit/__init__.py b/src/lfkit/__init__.py index 3927633..9c1135b 100644 --- a/src/lfkit/__init__.py +++ b/src/lfkit/__init__.py @@ -3,7 +3,8 @@ from __future__ import annotations from lfkit.api.corrections import Corrections -from lfkit.api.lumfunc import LuminosityFunction +from lfkit.api.luminosity_function import LuminosityFunction +from lfkit.api.conditional_luminosity_function import ConditionalLuminosityFunction try: from lfkit._version import version as __version__ @@ -14,6 +15,7 @@ __all__ = [ "Corrections", "LuminosityFunction", + "ConditionalLuminosityFunction", ] __author__ = """Niko Sarcevic and collaborators.""" From 638d8149d6770bebf4f6cf3fc88d91b91c0605b2 Mon Sep 17 00:00:00 2001 From: "niko.sarcevic" Date: Fri, 15 May 2026 13:17:37 -0400 Subject: [PATCH 6/6] removed old docs --- docs/api/lfkit.api.lumfunc.rst | 7 ------- docs/api/lfkit.api.rst | 1 - 2 files changed, 8 deletions(-) delete mode 100644 docs/api/lfkit.api.lumfunc.rst diff --git a/docs/api/lfkit.api.lumfunc.rst b/docs/api/lfkit.api.lumfunc.rst deleted file mode 100644 index c331f8a..0000000 --- a/docs/api/lfkit.api.lumfunc.rst +++ /dev/null @@ -1,7 +0,0 @@ -lfkit.api.lumfunc module -======================== - -.. automodule:: lfkit.api.lumfunc - :members: - :show-inheritance: - :undoc-members: diff --git a/docs/api/lfkit.api.rst b/docs/api/lfkit.api.rst index 2d8f565..fa06294 100644 --- a/docs/api/lfkit.api.rst +++ b/docs/api/lfkit.api.rst @@ -9,7 +9,6 @@ Submodules lfkit.api.conditional_luminosity_function lfkit.api.corrections - lfkit.api.lumfunc lfkit.api.luminosity_function Module contents