From 327eacff4f910a782225f0fd6ad9db3de2558274 Mon Sep 17 00:00:00 2001 From: "niko.sarcevic" Date: Sat, 20 Jun 2026 14:35:30 -0400 Subject: [PATCH 1/3] added lf frac functionality; updated validators --- src/lfkit/api/_namespaces.py | 70 +++- src/lfkit/api/luminosity_function.py | 2 + src/lfkit/luminosity_functions/fractions.py | 393 ++++++++++++++++++++ src/lfkit/utils/validators.py | 15 +- tests/test_lumfuncs_fractions.py | 0 5 files changed, 471 insertions(+), 9 deletions(-) create mode 100644 src/lfkit/luminosity_functions/fractions.py create mode 100644 tests/test_lumfuncs_fractions.py diff --git a/src/lfkit/api/_namespaces.py b/src/lfkit/api/_namespaces.py index cb3b340b..b9335498 100644 --- a/src/lfkit/api/_namespaces.py +++ b/src/lfkit/api/_namespaces.py @@ -6,7 +6,10 @@ from functools import wraps from typing import Any +import numpy as np + from lfkit.luminosity_functions import completeness as lf_completeness +from lfkit.luminosity_functions import fractions as lf_fractions from lfkit.luminosity_functions import integrals as lf_integrals from lfkit.luminosity_functions import redshift_density as lf_redshift_density from lfkit.photometry import luminosities as photo_luminosities @@ -49,6 +52,38 @@ def __init__(self, lf) -> None: self.lf = lf +class LFFractionsAPI: + """Grouped API for luminosity function population fractions. + + Args: + lf: Luminosity function object whose callable form is used as the + numerator luminosity function. + """ + + def __init__(self, lf) -> None: + self.lf = lf + + def blue_fraction( + self, + z, + total_lf, + *, + m_bright, + m_faint, + n_m=512, + ): + """Return one minus the parent luminosity function fraction.""" + red_fraction = self.red_fraction( + z, + total_lf, + m_bright=m_bright, + m_faint=m_faint, + n_m=n_m, + ) + + return np.asarray(1.0 - red_fraction, dtype=float) + + class LFMagnitudesAPI: """Grouped API for apparent magnitude and absolute magnitude conversions.""" @@ -82,6 +117,12 @@ class LFLuminositiesAPI: "module": photo_luminosities, "bound_to_lf": False, }, + LFFractionsAPI: { + "module": lf_fractions, + "bound_to_lf": True, + "lf_arg_position": 1, + "coerce_lf_args": {2}, + }, } @@ -90,6 +131,7 @@ def expose_lf_function( *, lf_arg_position: int | None = None, lf_arg_name: str | None = None, + coerce_lf_args: set[int] | None = None, ) -> Callable[..., Any]: """Expose a low level luminosity function helper as a bound API method. @@ -100,6 +142,8 @@ def expose_lf_function( argument is inserted. lf_arg_name: Keyword name used to pass the luminosity function callable. If provided, this takes priority over ``lf_arg_position``. + coerce_lf_args: Optional positional argument indices that should be + converted from luminosity function objects to callables. Returns: Bound method that injects ``self.lf._as_callable()`` before calling @@ -114,11 +158,16 @@ def method(self, *args, **kwargs): 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) + + if lf_arg_position is not None: + args_list.insert(lf_arg_position, lf_callable) + + if coerce_lf_args is not None: + for index in coerce_lf_args: + if index < len(args_list): + args_list[index] = _as_lf_callable(args_list[index]) + return function(*args_list, **kwargs) return method @@ -164,6 +213,9 @@ def _attach_api_methods() -> None: for function_name, function in _public_functions(module).items(): method_name = _method_name(module, function_name) + if method_name in api_cls.__dict__: + continue + if not bound_to_lf or function_name in static_functions: setattr(api_cls, method_name, staticmethod(function)) continue @@ -175,8 +227,18 @@ def _attach_api_methods() -> None: function, lf_arg_position=spec.get("lf_arg_position"), lf_arg_name=spec.get("lf_arg_name"), + coerce_lf_args=spec.get("coerce_lf_args"), ), ) _attach_api_methods() + + +def _as_lf_callable(lf): + """Return a luminosity function object or callable as ``lf(M, z)``.""" + + if hasattr(lf, "_as_callable"): + return lf._as_callable() + + return lf diff --git a/src/lfkit/api/luminosity_function.py b/src/lfkit/api/luminosity_function.py index 31d6442d..d31c6f81 100644 --- a/src/lfkit/api/luminosity_function.py +++ b/src/lfkit/api/luminosity_function.py @@ -9,6 +9,7 @@ from lfkit.api._namespaces import ( LFCompletenessAPI, + LFFractionsAPI, LFIntegralsAPI, LFLuminositiesAPI, LFMagnitudesAPI, @@ -77,6 +78,7 @@ def __init__( self.integrals = LFIntegralsAPI(self) self.redshift_density = LFRedshiftDensityAPI(self) self.completeness = LFCompletenessAPI(self) + self.fractions = LFFractionsAPI(self) self.luminosities = LFLuminositiesAPI() self.magnitudes = LFMagnitudesAPI() diff --git a/src/lfkit/luminosity_functions/fractions.py b/src/lfkit/luminosity_functions/fractions.py new file mode 100644 index 00000000..22900e4d --- /dev/null +++ b/src/lfkit/luminosity_functions/fractions.py @@ -0,0 +1,393 @@ +r"""Luminosity function population fractions. + +This module provides helpers for computing population fractions from +luminosity functions. These functions are thin wrappers around the generic +luminosity function integration utilities. +""" + +from __future__ import annotations + +import numpy as np + +from lfkit.luminosity_functions.integrals import integrated_number_density +from lfkit.utils.integrators import safe_divide +from lfkit.utils.types import FloatArray, FloatInput, LuminosityFunction +from lfkit.utils.validators import validate_magnitude_range + +__all__ = [ + "fraction_from_luminosity_functions", + "complement_fraction_from_luminosity_functions", + "red_fraction_from_luminosity_functions", + "blue_fraction_from_luminosity_functions", + "red_blue_fractions_from_luminosity_functions", + "population_densities_from_luminosity_functions", +] + +__api_aliases__ = { + "fraction_from_luminosity_functions": "fraction", + "complement_fraction_from_luminosity_functions": "complement_fraction", + "red_fraction_from_luminosity_functions": "red_fraction", + "blue_fraction_from_luminosity_functions": "blue_fraction", + "red_blue_fractions_from_luminosity_functions": "red_blue", + "population_densities_from_luminosity_functions": "population_densities", +} + + +def fraction_from_luminosity_functions( + z: FloatInput, + numerator_lf: LuminosityFunction, + denominator_lf: LuminosityFunction, + *, + m_bright: FloatInput, + m_faint: FloatInput, + n_m: int = 512, +) -> FloatArray: + r"""Return the LF number density fraction between two luminosity functions. + + This computes + + .. math:: + + f(z) = + \frac{ + \int_{M_{\mathrm{bright}}}^{M_{\mathrm{faint}}} + \phi_{\mathrm{num}}(M,z)\,dM + }{ + \int_{M_{\mathrm{bright}}}^{M_{\mathrm{faint}}} + \phi_{\mathrm{den}}(M,z)\,dM + }. + + Args: + z: Redshift value or array-like of redshift values. + numerator_lf: Numerator luminosity function callable with signature + ``lf(M, z)``. + denominator_lf: Denominator luminosity function callable with signature + ``lf(M, z)``. + m_bright: Bright absolute magnitude bound. May be scalar or array-like. + m_faint: Faint absolute magnitude bound. May be scalar or array-like. + n_m: Number of magnitude-grid points used for each integral. + + Returns: + NumPy array of LF number density fractions. Entries are zero where the + denominator number density is zero. + + Raises: + ValueError: If redshift values are negative, if magnitude bounds are + invalid, or if either luminosity function returns invalid values. + """ + validate_magnitude_range(m_bright=m_bright, m_faint=m_faint) + + numerator_density = integrated_number_density( + z, + numerator_lf, + m_bright=m_bright, + m_faint=m_faint, + n_m=n_m, + ) + denominator_density = integrated_number_density( + z, + denominator_lf, + m_bright=m_bright, + m_faint=m_faint, + n_m=n_m, + ) + + return np.asarray( + safe_divide(numerator_density, denominator_density), + dtype=float, + ) + + +def complement_fraction_from_luminosity_functions( + z: FloatInput, + numerator_lf: LuminosityFunction, + denominator_lf: LuminosityFunction, + *, + m_bright: FloatInput, + m_faint: FloatInput, + n_m: int = 512, +) -> FloatArray: + r"""Return one minus the LF number density fraction. + + This computes + + .. math:: + + f_{\mathrm{comp}}(z) = 1 - f(z), + + where + + .. math:: + + f(z) = + \frac{ + \int_{M_{\mathrm{bright}}}^{M_{\mathrm{faint}}} + \phi_{\mathrm{num}}(M,z)\,dM + }{ + \int_{M_{\mathrm{bright}}}^{M_{\mathrm{faint}}} + \phi_{\mathrm{den}}(M,z)\,dM + }. + + Args: + z: Redshift value or array-like of redshift values. + numerator_lf: Numerator luminosity function callable with signature + ``lf(M, z)``. + denominator_lf: Denominator luminosity function callable with signature + ``lf(M, z)``. + m_bright: Bright absolute magnitude bound. May be scalar or array-like. + m_faint: Faint absolute magnitude bound. May be scalar or array-like. + n_m: Number of magnitude-grid points used for each integral. + + Returns: + NumPy array containing ``1 - numerator / denominator``. + """ + fraction = fraction_from_luminosity_functions( + z, + numerator_lf, + denominator_lf, + m_bright=m_bright, + m_faint=m_faint, + n_m=n_m, + ) + + return np.asarray(1.0 - fraction, dtype=float) + + +def red_fraction_from_luminosity_functions( + z: FloatInput, + red_lf: LuminosityFunction, + total_lf: LuminosityFunction, + *, + m_bright: FloatInput, + m_faint: FloatInput, + n_m: int = 512, +) -> FloatArray: + r"""Return the red fraction from red and total luminosity functions. + + This computes + + .. math:: + + f_{\mathrm{red}}(z) = + \frac{ + \int_{M_{\mathrm{bright}}}^{M_{\mathrm{faint}}} + \phi_{\mathrm{red}}(M,z)\,dM + }{ + \int_{M_{\mathrm{bright}}}^{M_{\mathrm{faint}}} + \phi_{\mathrm{tot}}(M,z)\,dM + }. + + Args: + z: Redshift value or array-like of redshift values. + red_lf: Red galaxy luminosity function callable with signature + ``lf(M, z)``. + total_lf: Total luminosity function callable with signature + ``lf(M, z)``. + m_bright: Bright absolute magnitude bound. May be scalar or array-like. + m_faint: Faint absolute magnitude bound. May be scalar or array-like. + n_m: Number of magnitude-grid points used for each integral. + + Returns: + NumPy array of red fractions. Entries are zero where the total number + density is zero. + """ + return fraction_from_luminosity_functions( + z, + red_lf, + total_lf, + m_bright=m_bright, + m_faint=m_faint, + n_m=n_m, + ) + + +def blue_fraction_from_luminosity_functions( + z: FloatInput, + blue_lf: LuminosityFunction, + total_lf: LuminosityFunction, + *, + m_bright: FloatInput, + m_faint: FloatInput, + n_m: int = 512, +) -> FloatArray: + r"""Return the blue fraction from blue and total luminosity functions. + + This computes + + .. math:: + + f_{\mathrm{blue}}(z) = + \frac{ + \int_{M_{\mathrm{bright}}}^{M_{\mathrm{faint}}} + \phi_{\mathrm{blue}}(M,z)\,dM + }{ + \int_{M_{\mathrm{bright}}}^{M_{\mathrm{faint}}} + \phi_{\mathrm{tot}}(M,z)\,dM + }. + + Args: + z: Redshift value or array-like of redshift values. + blue_lf: Blue galaxy luminosity function callable with signature + ``lf(M, z)``. + total_lf: Total luminosity function callable with signature + ``lf(M, z)``. + m_bright: Bright absolute magnitude bound. May be scalar or array-like. + m_faint: Faint absolute magnitude bound. May be scalar or array-like. + n_m: Number of magnitude-grid points used for each integral. + + Returns: + NumPy array of blue fractions. Entries are zero where the total number + density is zero. + """ + return fraction_from_luminosity_functions( + z, + blue_lf, + total_lf, + m_bright=m_bright, + m_faint=m_faint, + n_m=n_m, + ) + + +def red_blue_fractions_from_luminosity_functions( + z: FloatInput, + red_lf: LuminosityFunction, + blue_lf: LuminosityFunction, + *, + m_bright: FloatInput, + m_faint: FloatInput, + n_m: int = 512, +) -> tuple[FloatArray, FloatArray]: + r"""Return red and blue fractions from red and blue luminosity functions. + + This computes + + .. math:: + + f_{\mathrm{red}}(z) = + \frac{n_{\mathrm{red}}(z)} + {n_{\mathrm{red}}(z) + n_{\mathrm{blue}}(z)}, + + and + + .. math:: + + f_{\mathrm{blue}}(z) = + \frac{n_{\mathrm{blue}}(z)} + {n_{\mathrm{red}}(z) + n_{\mathrm{blue}}(z)}, + + where + + .. math:: + + n_{\mathrm{red}}(z) = + \int_{M_{\mathrm{bright}}}^{M_{\mathrm{faint}}} + \phi_{\mathrm{red}}(M,z)\,dM, + + and similarly for the blue luminosity function. + + Args: + z: Redshift value or array-like of redshift values. + red_lf: Red galaxy luminosity function callable with signature + ``lf(M, z)``. + blue_lf: Blue galaxy luminosity function callable with signature + ``lf(M, z)``. + m_bright: Bright absolute magnitude bound. May be scalar or array-like. + m_faint: Faint absolute magnitude bound. May be scalar or array-like. + n_m: Number of magnitude-grid points used for each integral. + + Returns: + Tuple containing ``(red_fraction, blue_fraction)``. Entries are zero + where the combined red plus blue number density is zero. + + Raises: + ValueError: If redshift values are negative, if magnitude bounds are + invalid, or if either luminosity function returns invalid values. + """ + red_density, blue_density, total_density = ( + population_densities_from_luminosity_functions( + z, + red_lf, + blue_lf, + m_bright=m_bright, + m_faint=m_faint, + n_m=n_m, + ) + ) + + red_fraction = safe_divide(red_density, total_density) + blue_fraction = safe_divide(blue_density, total_density) + + return ( + np.asarray(red_fraction, dtype=float), + np.asarray(blue_fraction, dtype=float), + ) + + +def population_densities_from_luminosity_functions( + z: FloatInput, + first_lf: LuminosityFunction, + second_lf: LuminosityFunction, + *, + m_bright: FloatInput, + m_faint: FloatInput, + n_m: int = 512, +) -> tuple[FloatArray, FloatArray, FloatArray]: + r"""Return two population densities and their sum. + + This computes + + .. math:: + + n_1(z) = + \int_{M_{\mathrm{bright}}}^{M_{\mathrm{faint}}} + \phi_1(M,z)\,dM, + + .. math:: + + n_2(z) = + \int_{M_{\mathrm{bright}}}^{M_{\mathrm{faint}}} + \phi_2(M,z)\,dM, + + and + + .. math:: + + n_{\mathrm{tot}}(z) = n_1(z) + n_2(z). + + Args: + z: Redshift value or array-like of redshift values. + first_lf: First population luminosity function callable with signature + ``lf(M, z)``. + second_lf: Second population luminosity function callable with + signature ``lf(M, z)``. + m_bright: Bright absolute magnitude bound. May be scalar or array-like. + m_faint: Faint absolute magnitude bound. May be scalar or array-like. + n_m: Number of magnitude-grid points used for each integral. + + Returns: + Tuple containing ``(first_density, second_density, total_density)``. + """ + validate_magnitude_range(m_bright=m_bright, m_faint=m_faint) + + first_density = integrated_number_density( + z, + first_lf, + m_bright=m_bright, + m_faint=m_faint, + n_m=n_m, + ) + second_density = integrated_number_density( + z, + second_lf, + m_bright=m_bright, + m_faint=m_faint, + n_m=n_m, + ) + total_density = first_density + second_density + + return ( + np.asarray(first_density, dtype=float), + np.asarray(second_density, dtype=float), + np.asarray(total_density, dtype=float), + ) diff --git a/src/lfkit/utils/validators.py b/src/lfkit/utils/validators.py index b86bc1e6..90d9d670 100644 --- a/src/lfkit/utils/validators.py +++ b/src/lfkit/utils/validators.py @@ -54,17 +54,22 @@ def validate_luminosity_distance( def validate_magnitude_range( *, - m_bright: float, - m_faint: float, + m_bright: FloatInput, + m_faint: FloatInput, ) -> None: """Validate bright and faint magnitude bounds.""" - if not np.isfinite(m_bright): + m_bright_arr, m_faint_arr = np.broadcast_arrays( + np.asarray(m_bright, dtype=float), + np.asarray(m_faint, dtype=float), + ) + + if not np.all(np.isfinite(m_bright_arr)): raise ValueError("m_bright must be finite.") - if not np.isfinite(m_faint): + if not np.all(np.isfinite(m_faint_arr)): raise ValueError("m_faint must be finite.") - if m_faint <= m_bright: + if np.any(m_faint_arr <= m_bright_arr): raise ValueError("m_faint must be larger than m_bright.") diff --git a/tests/test_lumfuncs_fractions.py b/tests/test_lumfuncs_fractions.py new file mode 100644 index 00000000..e69de29b From a8d56f7eb66f25a44db3a5d755bb348af94f1422 Mon Sep 17 00:00:00 2001 From: "niko.sarcevic" Date: Sat, 20 Jun 2026 14:35:52 -0400 Subject: [PATCH 2/3] added corresponding tests for lf fracs --- tests/test_api_luminosity_function.py | 125 ++++++ tests/test_api_namespaces.py | 142 +++++++ tests/test_lumfuncs_fractions.py | 546 ++++++++++++++++++++++++++ tests/test_lumfuncs_integrals.py | 2 +- tests/test_utils_validators.py | 40 ++ 5 files changed, 854 insertions(+), 1 deletion(-) diff --git a/tests/test_api_luminosity_function.py b/tests/test_api_luminosity_function.py index 494cf2d9..10a72f8f 100644 --- a/tests/test_api_luminosity_function.py +++ b/tests/test_api_luminosity_function.py @@ -49,6 +49,7 @@ def test_luminosity_function_initializes_grouped_api_namespaces(): assert hasattr(lf, "integrals") assert hasattr(lf, "redshift_density") assert hasattr(lf, "completeness") + assert hasattr(lf, "fractions") assert hasattr(lf, "luminosities") assert hasattr(lf, "magnitudes") @@ -898,3 +899,127 @@ def test_available_models_includes_extended_model_families() -> None: for name in expected: assert name in models + + +def _make_registered_lf(model_name): + """Return a luminosity function instance for one registered model.""" + model_spec = LF_MODELS[model_name] + + return getattr(LuminosityFunction, model_name)( + **_minimal_parameter_payload(model_name, model_spec.function) + ) + + +@pytest.mark.parametrize("model_name", sorted(LF_MODELS)) +def test_fractions_namespace_delegates_to_bound_lf_callable(model_name): + """Tests that fractions delegate to registered LF callables.""" + red_lf = _make_registered_lf(model_name) + all_lf = _make_registered_lf(model_name) + + result = red_lf.fractions.fraction( + 0.5, + all_lf, + m_bright=-24.0, + m_faint=-18.0, + n_m=32, + ) + + assert np.asarray(result).shape == () + assert np.isfinite(result) + assert 0.0 <= float(result) <= 1.0 + assert np.allclose(result, 1.0) + + +@pytest.mark.parametrize("model_name", sorted(LF_MODELS)) +def test_fractions_namespace_accepts_callable_denominator_lf(model_name): + """Tests that fractions accept callable denominator LF models.""" + red_lf = _make_registered_lf(model_name) + + def all_lf(absolute_mag, redshift): + return 2.0 * red_lf.phi(absolute_mag, z=redshift) + + result = red_lf.fractions.fraction( + 0.5, + all_lf, + m_bright=-24.0, + m_faint=-18.0, + n_m=32, + ) + + assert np.asarray(result).shape == () + assert np.isfinite(result) + assert 0.0 <= float(result) <= 1.0 + assert np.allclose(result, 0.5) + + +def test_fractions_namespace_exposes_expected_methods(): + """Tests that fractions namespace exposes expected methods.""" + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + expected_methods = [ + "fraction", + "red_fraction", + "blue_fraction", + ] + + for name in expected_methods: + assert callable(getattr(lf.fractions, name)) + + +@pytest.mark.parametrize("model_name", sorted(LF_MODELS)) +def test_luminosity_function_fractions_namespace_uses_parent_lf(model_name): + """Tests that fractions namespace stores the parent LF.""" + lf = _make_registered_lf(model_name) + + assert lf.fractions.lf is lf + + +@pytest.mark.parametrize("model_name", sorted(LF_MODELS)) +def test_blue_fraction_is_complement_of_red_fraction(model_name): + """Tests that blue fraction is the complement of red fraction.""" + model_spec = LF_MODELS[model_name] + red_lf = getattr(LuminosityFunction, model_name)( + **_minimal_parameter_payload(model_name, model_spec.function) + ) + all_lf = getattr(LuminosityFunction, model_name)( + **_minimal_parameter_payload(model_name, model_spec.function) + ) + + red_fraction = red_lf.fractions.red_fraction( + 0.5, + all_lf, + m_bright=-24.0, + m_faint=-18.0, + n_m=32, + ) + blue_fraction = red_lf.fractions.blue_fraction( + 0.5, + all_lf, + m_bright=-24.0, + m_faint=-18.0, + n_m=32, + ) + + assert np.allclose(blue_fraction, 1.0 - red_fraction) + + +def test_fractions_namespace_rejects_invalid_magnitude_range() -> None: + """Tests that the fractions namespace validates magnitude bounds.""" + lf = LuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + ) + + with pytest.raises(ValueError, match="m_faint must be larger than m_bright"): + lf.fractions.fraction( + 0.5, + lf, + m_bright=-18.0, + m_faint=-24.0, + n_m=32, + ) diff --git a/tests/test_api_namespaces.py b/tests/test_api_namespaces.py index 6cd2ebd4..09840422 100644 --- a/tests/test_api_namespaces.py +++ b/tests/test_api_namespaces.py @@ -8,10 +8,12 @@ from lfkit.api._namespaces import ( LFCompletenessAPI, + LFFractionsAPI, LFIntegralsAPI, LFLuminositiesAPI, LFMagnitudesAPI, LFRedshiftDensityAPI, + _as_lf_callable, _method_name, _public_functions, expose_lf_function, @@ -280,3 +282,143 @@ def test_luminosity_namespace_has_expected_methods(): for name in expected_methods: assert callable(getattr(LFLuminositiesAPI, name)) + + +def test_fractions_namespace_stores_parent_lf(): + """Tests that fractions namespace stores the parent LF.""" + lf = DummyLF() + api = LFFractionsAPI(lf) + + assert api.lf is lf + + +def test_as_lf_callable_converts_lf_object_to_callable(): + """Tests that LF objects are converted to luminosity function callables.""" + lf = DummyLF() + + result = _as_lf_callable(lf) + + assert callable(result) + assert result(2.0, 0.5) == 3.0 + + +def test_as_lf_callable_preserves_plain_callable(): + """Tests that plain callables are returned unchanged.""" + + def lf_callable(absolute_mag, redshift=None): + return np.asarray(absolute_mag) + 2.0 + + result = _as_lf_callable(lf_callable) + + assert result is lf_callable + assert result(2.0, 0.5) == 4.0 + + +def test_expose_lf_function_coerces_lf_args_from_lf_objects(): + """Tests that selected LF positional arguments are coerced to callables.""" + calls = {} + + def low_level(x, numerator_lf, denominator_lf, z): + calls["numerator_value"] = numerator_lf(x, z) + calls["denominator_value"] = denominator_lf(x, z) + return calls["numerator_value"] / calls["denominator_value"] + + class DenominatorLF: + """Minimal denominator LF object.""" + + def _as_callable(self): + """Return a simple denominator luminosity function callable.""" + return lambda absolute_mag, redshift=None: np.asarray(absolute_mag) + 5.0 + + method = expose_lf_function( + low_level, + lf_arg_position=1, + coerce_lf_args={2}, + ) + api = DummyBoundAPI() + + result = method(api, 2.0, DenominatorLF(), 0.5) + + assert result == 3.0 / 7.0 + assert calls["numerator_value"] == 3.0 + assert calls["denominator_value"] == 7.0 + + +def test_expose_lf_function_coerces_lf_args_from_plain_callables(): + """Tests that selected callable LF arguments remain usable.""" + + def low_level(x, numerator_lf, denominator_lf, z): + return numerator_lf(x, z) / denominator_lf(x, z) + + def denominator_lf(absolute_mag, redshift=None): + return np.asarray(absolute_mag) + 6.0 + + method = expose_lf_function( + low_level, + lf_arg_position=1, + coerce_lf_args={2}, + ) + api = DummyBoundAPI() + + result = method(api, 2.0, denominator_lf, 0.5) + + assert result == 3.0 / 8.0 + + +def test_expose_lf_function_ignores_missing_coerce_indices(): + """Tests that out of range LF coercion indices are ignored.""" + + def low_level(x, lf_callable): + return lf_callable(x) + + method = expose_lf_function( + low_level, + lf_arg_position=1, + coerce_lf_args={10}, + ) + api = DummyBoundAPI() + + result = method(api, 2.0) + + assert result == 3.0 + + +def test_fractions_namespace_has_expected_methods(): + """Tests that fractions namespace exposes expected methods.""" + expected_methods = [ + "fraction", + "red_fraction", + "blue_fraction", + ] + + for name in expected_methods: + assert callable(getattr(LFFractionsAPI, name)) + + +def test_fractions_namespace_blue_fraction_uses_red_fraction_complement() -> None: + """Tests that the custom blue fraction method returns one minus red fraction.""" + + class DummyFractionsAPI(LFFractionsAPI): + def red_fraction( + self, + z, + total_lf, + *, + m_bright, + m_faint, + n_m=512, + ): + return np.asarray([0.25, 0.5, 0.75]) + + api = DummyFractionsAPI(DummyLF()) + + result = api.blue_fraction( + np.array([0.1, 0.5, 1.0]), + DummyLF(), + m_bright=-24.0, + m_faint=-18.0, + n_m=32, + ) + + np.testing.assert_allclose(result, np.array([0.75, 0.5, 0.25])) + assert result.dtype == float diff --git a/tests/test_lumfuncs_fractions.py b/tests/test_lumfuncs_fractions.py index e69de29b..f5b2e8d4 100644 --- a/tests/test_lumfuncs_fractions.py +++ b/tests/test_lumfuncs_fractions.py @@ -0,0 +1,546 @@ +"""Unit tests for the ``lfkit.luminosity_functions.fractions``.""" + +from __future__ import annotations + +import pytest + +import numpy as np + +from lfkit.luminosity_functions.fractions import ( + blue_fraction_from_luminosity_functions, + complement_fraction_from_luminosity_functions, + fraction_from_luminosity_functions, + population_densities_from_luminosity_functions, + red_blue_fractions_from_luminosity_functions, + red_fraction_from_luminosity_functions, +) + + +def test_fraction_from_luminosity_functions_returns_constant_ratio() -> None: + """Tests that LF fractions recover a constant normalization ratio.""" + + def total_lf(absolute_mag, z): + return np.ones_like(np.broadcast_arrays(absolute_mag, z)[0], dtype=float) + + def red_lf(absolute_mag, z): + return 0.35 * np.ones_like( + np.broadcast_arrays(absolute_mag, z)[0], + dtype=float, + ) + + z = np.array([0.1, 0.5, 1.0]) + + frac = fraction_from_luminosity_functions( + z, + red_lf, + total_lf, + m_bright=-23.0, + m_faint=-18.0, + n_m=128, + ) + + np.testing.assert_allclose(frac, 0.35, rtol=1.0e-12, atol=1.0e-12) + + +def test_red_fraction_from_luminosity_functions_returns_expected_ratio() -> None: + """Tests that the red fraction helper returns the expected LF ratio.""" + + def total_lf(absolute_mag, z): + return np.ones_like(np.broadcast_arrays(absolute_mag, z)[0], dtype=float) + + def red_lf(absolute_mag, z): + return 0.6 * np.ones_like( + np.broadcast_arrays(absolute_mag, z)[0], + dtype=float, + ) + + z = np.array([0.2, 0.4, 0.8]) + + frac = red_fraction_from_luminosity_functions( + z, + red_lf, + total_lf, + m_bright=-24.0, + m_faint=-19.0, + n_m=128, + ) + + np.testing.assert_allclose(frac, 0.6, rtol=1.0e-12, atol=1.0e-12) + + +def test_blue_fraction_from_luminosity_functions_returns_expected_ratio() -> None: + """Tests that the blue fraction helper returns the expected LF ratio.""" + + def total_lf(absolute_mag, z): + return np.ones_like(np.broadcast_arrays(absolute_mag, z)[0], dtype=float) + + def blue_lf(absolute_mag, z): + return 0.4 * np.ones_like( + np.broadcast_arrays(absolute_mag, z)[0], + dtype=float, + ) + + z = np.array([0.2, 0.4, 0.8]) + + frac = blue_fraction_from_luminosity_functions( + z, + blue_lf, + total_lf, + m_bright=-24.0, + m_faint=-19.0, + n_m=128, + ) + + np.testing.assert_allclose(frac, 0.4, rtol=1.0e-12, atol=1.0e-12) + + +def test_fraction_from_luminosity_functions_returns_zero_for_zero_denominator() -> None: + """Tests that zero denominator density gives zero fraction.""" + + def numerator_lf(absolute_mag, z): + return np.ones_like(np.broadcast_arrays(absolute_mag, z)[0], dtype=float) + + def denominator_lf(absolute_mag, z): + return np.zeros_like(np.broadcast_arrays(absolute_mag, z)[0], dtype=float) + + z = np.array([0.1, 0.5]) + + frac = fraction_from_luminosity_functions( + z, + numerator_lf, + denominator_lf, + m_bright=-23.0, + m_faint=-18.0, + n_m=128, + ) + + np.testing.assert_allclose(frac, np.zeros_like(z), rtol=0.0, atol=0.0) + + +def test_red_fraction_from_luminosity_functions_accepts_scalar_redshift() -> None: + """Tests that scalar redshift input is accepted.""" + + def total_lf(absolute_mag, z): + return np.ones_like(np.broadcast_arrays(absolute_mag, z)[0], dtype=float) + + def red_lf(absolute_mag, z): + return 0.25 * np.ones_like( + np.broadcast_arrays(absolute_mag, z)[0], + dtype=float, + ) + + frac = red_fraction_from_luminosity_functions( + 0.5, + red_lf, + total_lf, + m_bright=-23.0, + m_faint=-18.0, + n_m=128, + ) + + np.testing.assert_allclose(frac, 0.25, rtol=1.0e-12, atol=1.0e-12) + + +def test_red_blue_fractions_from_luminosity_functions_return_expected_ratios() -> None: + """Tests that red and blue LF fractions use red plus blue as the denominator.""" + + def red_lf(absolute_mag, z): + return 0.25 * np.ones_like( + np.broadcast_arrays(absolute_mag, z)[0], + dtype=float, + ) + + def blue_lf(absolute_mag, z): + return 0.75 * np.ones_like( + np.broadcast_arrays(absolute_mag, z)[0], + dtype=float, + ) + + z = np.array([0.1, 0.5, 1.0]) + + red_fraction, blue_fraction = red_blue_fractions_from_luminosity_functions( + z, + red_lf, + blue_lf, + m_bright=-23.0, + m_faint=-18.0, + n_m=128, + ) + + np.testing.assert_allclose(red_fraction, 0.25, rtol=1.0e-12, atol=1.0e-12) + np.testing.assert_allclose(blue_fraction, 0.75, rtol=1.0e-12, atol=1.0e-12) + + +def test_red_blue_fractions_from_luminosity_functions_zero_for_zero_total() -> None: + """Tests that red and blue fractions are zero when red plus blue is zero.""" + + def red_lf(absolute_mag, z): + return np.zeros_like(np.broadcast_arrays(absolute_mag, z)[0], dtype=float) + + def blue_lf(absolute_mag, z): + return np.zeros_like(np.broadcast_arrays(absolute_mag, z)[0], dtype=float) + + z = np.array([0.1, 0.5]) + + red_fraction, blue_fraction = red_blue_fractions_from_luminosity_functions( + z, + red_lf, + blue_lf, + m_bright=-23.0, + m_faint=-18.0, + n_m=128, + ) + + np.testing.assert_allclose(red_fraction, np.zeros_like(z), rtol=0.0, atol=0.0) + np.testing.assert_allclose(blue_fraction, np.zeros_like(z), rtol=0.0, atol=0.0) + + +def test_complement_fraction_from_luminosity_functions_returns_expected_value() -> None: + """Tests that complement fractions return one minus the LF ratio.""" + + def total_lf(absolute_mag, z): + return np.ones_like(np.broadcast_arrays(absolute_mag, z)[0], dtype=float) + + def red_lf(absolute_mag, z): + return 0.6 * np.ones_like( + np.broadcast_arrays(absolute_mag, z)[0], + dtype=float, + ) + + z = np.array([0.2, 0.4, 0.8]) + + frac = complement_fraction_from_luminosity_functions( + z, + red_lf, + total_lf, + m_bright=-24.0, + m_faint=-19.0, + n_m=128, + ) + + np.testing.assert_allclose(frac, 0.4, rtol=1.0e-12, atol=1.0e-12) + + +def test_population_densities_from_luminosity_functions_returns_expected_values() -> None: + """Tests that population densities return both densities and their sum.""" + + def red_lf(absolute_mag, z): + return 0.25 * np.ones_like( + np.broadcast_arrays(absolute_mag, z)[0], + dtype=float, + ) + + def blue_lf(absolute_mag, z): + return 0.75 * np.ones_like( + np.broadcast_arrays(absolute_mag, z)[0], + dtype=float, + ) + + z = np.array([0.1, 0.5, 1.0]) + + red_density, blue_density, total_density = population_densities_from_luminosity_functions( + z, + red_lf, + blue_lf, + m_bright=-23.0, + m_faint=-18.0, + n_m=128, + ) + + expected_width = 5.0 + + np.testing.assert_allclose(red_density, 0.25 * expected_width) + np.testing.assert_allclose(blue_density, 0.75 * expected_width) + np.testing.assert_allclose(total_density, expected_width) + + +def test_fraction_from_luminosity_functions_tracks_redshift_dependence() -> None: + """Tests that LF fractions can vary with redshift.""" + + def numerator_lf(absolute_mag, z): + absolute_mag, z = np.broadcast_arrays(absolute_mag, z) + return (0.2 + 0.1 * z) * np.ones_like(absolute_mag, dtype=float) + + def denominator_lf(absolute_mag, z): + absolute_mag, z = np.broadcast_arrays(absolute_mag, z) + return np.ones_like(absolute_mag, dtype=float) + + z = np.array([0.0, 0.5, 1.0]) + + frac = fraction_from_luminosity_functions( + z, + numerator_lf, + denominator_lf, + m_bright=-23.0, + m_faint=-18.0, + n_m=128, + ) + + expected = 0.2 + 0.1 * z + + np.testing.assert_allclose(frac, expected, rtol=1.0e-12, atol=1.0e-12) + + +def test_fraction_from_luminosity_functions_tracks_magnitude_limit_dependence() -> None: + """Tests that LF fractions respond to the chosen magnitude range.""" + + def numerator_lf(absolute_mag, z): + absolute_mag, z = np.broadcast_arrays(absolute_mag, z) + return absolute_mag + 24.0 + + def denominator_lf(absolute_mag, z): + absolute_mag, z = np.broadcast_arrays(absolute_mag, z) + return np.ones_like(absolute_mag, dtype=float) + + z = 0.5 + + bright_selection = fraction_from_luminosity_functions( + z, + numerator_lf, + denominator_lf, + m_bright=-23.0, + m_faint=-21.0, + n_m=512, + ) + + faint_selection = fraction_from_luminosity_functions( + z, + numerator_lf, + denominator_lf, + m_bright=-21.0, + m_faint=-18.0, + n_m=512, + ) + + assert faint_selection > bright_selection + + np.testing.assert_allclose( + bright_selection, + 2.0, + rtol=1.0e-4, + atol=1.0e-4, + ) + np.testing.assert_allclose( + faint_selection, + 4.5, + rtol=1.0e-4, + atol=1.0e-4, + ) + + +def test_complement_fraction_matches_one_minus_fraction() -> None: + """Tests that complement fractions are the complement of the LF ratio.""" + + def total_lf(absolute_mag, z): + return np.ones_like(np.broadcast_arrays(absolute_mag, z)[0], dtype=float) + + def selected_lf(absolute_mag, z): + absolute_mag, z = np.broadcast_arrays(absolute_mag, z) + return (0.3 + 0.2 * z) * np.ones_like(absolute_mag, dtype=float) + + z = np.array([0.0, 0.5, 1.0]) + + fraction = fraction_from_luminosity_functions( + z, + selected_lf, + total_lf, + m_bright=-23.0, + m_faint=-18.0, + n_m=128, + ) + + complement = complement_fraction_from_luminosity_functions( + z, + selected_lf, + total_lf, + m_bright=-23.0, + m_faint=-18.0, + n_m=128, + ) + + np.testing.assert_allclose( + complement, + 1.0 - fraction, + rtol=1.0e-12, + atol=1.0e-12, + ) + + +def test_red_blue_fractions_sum_to_one_for_nonzero_total() -> None: + """Tests that red and blue fractions sum to one when total density is nonzero.""" + + def red_lf(absolute_mag, z): + absolute_mag, z = np.broadcast_arrays(absolute_mag, z) + return (0.2 + 0.1 * z) * np.ones_like(absolute_mag, dtype=float) + + def blue_lf(absolute_mag, z): + absolute_mag, z = np.broadcast_arrays(absolute_mag, z) + return (0.8 - 0.1 * z) * np.ones_like(absolute_mag, dtype=float) + + z = np.array([0.0, 0.5, 1.0]) + + red_fraction, blue_fraction = red_blue_fractions_from_luminosity_functions( + z, + red_lf, + blue_lf, + m_bright=-23.0, + m_faint=-18.0, + n_m=128, + ) + + np.testing.assert_allclose( + red_fraction + blue_fraction, + np.ones_like(z), + rtol=1.0e-12, + atol=1.0e-12, + ) + + +def test_population_densities_are_consistent_with_fraction() -> None: + """Tests that density ratios agree with the fraction helper.""" + + def red_lf(absolute_mag, z): + absolute_mag, z = np.broadcast_arrays(absolute_mag, z) + return (0.2 + 0.1 * z) * np.ones_like(absolute_mag, dtype=float) + + def total_lf(absolute_mag, z): + return np.ones_like(np.broadcast_arrays(absolute_mag, z)[0], dtype=float) + + z = np.array([0.0, 0.5, 1.0]) + + fraction = fraction_from_luminosity_functions( + z, + red_lf, + total_lf, + m_bright=-23.0, + m_faint=-18.0, + n_m=128, + ) + + red_density, total_density, combined_density = ( + population_densities_from_luminosity_functions( + z, + red_lf, + total_lf, + m_bright=-23.0, + m_faint=-18.0, + n_m=128, + ) + ) + + np.testing.assert_allclose( + red_density / total_density, + fraction, + rtol=1.0e-12, + atol=1.0e-12, + ) + np.testing.assert_allclose( + combined_density, + red_density + total_density, + rtol=1.0e-12, + atol=1.0e-12, + ) + + +def test_fraction_from_luminosity_functions_returns_finite_array() -> None: + """Tests that array inputs return finite fraction values.""" + + def numerator_lf(absolute_mag, z): + absolute_mag, z = np.broadcast_arrays(absolute_mag, z) + return (0.4 + 0.05 * z) * np.ones_like(absolute_mag, dtype=float) + + def denominator_lf(absolute_mag, z): + absolute_mag, z = np.broadcast_arrays(absolute_mag, z) + return (1.0 + 0.1 * z) * np.ones_like(absolute_mag, dtype=float) + + z = np.linspace(0.0, 1.0, 5) + + fraction = fraction_from_luminosity_functions( + z, + numerator_lf, + denominator_lf, + m_bright=-23.0, + m_faint=-18.0, + n_m=128, + ) + + assert fraction.shape == z.shape + assert np.all(np.isfinite(fraction)) + + +def test_fraction_from_luminosity_functions_rejects_invalid_magnitude_range() -> None: + """Tests that the bright magnitude bound must be smaller than the faint bound.""" + + def lf(absolute_mag, z): + return np.ones_like(np.broadcast_arrays(absolute_mag, z)[0], dtype=float) + + with pytest.raises(ValueError, match="m_faint must be larger than m_bright"): + fraction_from_luminosity_functions( + 0.5, + lf, + lf, + m_bright=-18.0, + m_faint=-23.0, + n_m=128, + ) + + +def test_fraction_from_luminosity_functions_rejects_equal_magnitude_bounds() -> None: + """Tests that equal bright and faint magnitude bounds are rejected.""" + + def lf(absolute_mag, z): + return np.ones_like(np.broadcast_arrays(absolute_mag, z)[0], dtype=float) + + with pytest.raises(ValueError, match="m_faint must be larger than m_bright"): + fraction_from_luminosity_functions( + 0.5, + lf, + lf, + m_bright=-20.0, + m_faint=-20.0, + n_m=128, + ) + + +def test_fraction_from_luminosity_functions_rejects_nonfinite_magnitude_bounds() -> None: + """Tests that non finite magnitude bounds are rejected.""" + + def lf(absolute_mag, z): + return np.ones_like(np.broadcast_arrays(absolute_mag, z)[0], dtype=float) + + with pytest.raises(ValueError, match="m_bright must be finite"): + fraction_from_luminosity_functions( + 0.5, + lf, + lf, + m_bright=np.nan, + m_faint=-18.0, + n_m=128, + ) + + with pytest.raises(ValueError, match="m_faint must be finite"): + fraction_from_luminosity_functions( + 0.5, + lf, + lf, + m_bright=-23.0, + m_faint=np.inf, + n_m=128, + ) + + +def test_population_densities_from_luminosity_functions_rejects_invalid_magnitude_range() -> None: + """Tests that population densities validate magnitude bounds.""" + + def lf(absolute_mag, z): + return np.ones_like(np.broadcast_arrays(absolute_mag, z)[0], dtype=float) + + with pytest.raises(ValueError, match="m_faint must be larger than m_bright"): + population_densities_from_luminosity_functions( + 0.5, + lf, + lf, + m_bright=-18.0, + m_faint=-23.0, + n_m=128, + ) diff --git a/tests/test_lumfuncs_integrals.py b/tests/test_lumfuncs_integrals.py index ed9127e3..8c4876b6 100644 --- a/tests/test_lumfuncs_integrals.py +++ b/tests/test_lumfuncs_integrals.py @@ -1,4 +1,4 @@ -"""Unit tests for the ``lfkit.photometry.lf_integrals``.""" +"""Unit tests for the ``lfkit.luminosity_functions.lf_integrals``.""" import numpy as np import pytest diff --git a/tests/test_utils_validators.py b/tests/test_utils_validators.py index ca6abfd9..2edc5793 100644 --- a/tests/test_utils_validators.py +++ b/tests/test_utils_validators.py @@ -661,3 +661,43 @@ def test_validate_2d_binned_grid_rejects_negative_y_edges_when_disallowed() -> N values_name="counts", allow_negative_y_edges=False, ) + + +def test_validate_magnitude_range_accepts_array_bounds() -> None: + """Tests that array magnitude bounds are accepted.""" + validate_magnitude_range( + m_bright=np.array([-24.0, -23.0, -22.0]), + m_faint=np.array([-20.0, -19.0, -18.0]), + ) + + +def test_validate_magnitude_range_accepts_broadcast_bounds() -> None: + """Tests that scalar and array magnitude bounds can be broadcast.""" + validate_magnitude_range( + m_bright=-24.0, + m_faint=np.array([-22.0, -20.0, -18.0]), + ) + + +def test_validate_magnitude_range_rejects_array_reversed_bounds() -> None: + """Tests that any invalid array magnitude pair is rejected.""" + with pytest.raises(ValueError, match="m_faint must be larger than m_bright"): + validate_magnitude_range( + m_bright=np.array([-24.0, -18.0, -22.0]), + m_faint=np.array([-20.0, -23.0, -18.0]), + ) + + +def test_validate_magnitude_range_rejects_array_nonfinite_bounds() -> None: + """Tests that non finite array magnitude bounds are rejected.""" + with pytest.raises(ValueError, match="m_bright must be finite"): + validate_magnitude_range( + m_bright=np.array([-24.0, np.nan]), + m_faint=np.array([-20.0, -18.0]), + ) + + with pytest.raises(ValueError, match="m_faint must be finite"): + validate_magnitude_range( + m_bright=np.array([-24.0, -23.0]), + m_faint=np.array([-20.0, np.inf]), + ) From 261d0eb41954466eb96c130d3e4b6d5512c45430 Mon Sep 17 00:00:00 2001 From: "niko.sarcevic" Date: Sat, 20 Jun 2026 14:36:10 -0400 Subject: [PATCH 3/3] added lf fracs to docs examples --- ...it.api.conditional_luminosity_function.rst | 12 + docs/api/generated/lfkit.api.corrections.rst | 12 + .../lfkit.api.luminosity_function.rst | 12 + docs/api/generated/lfkit.api.rst | 15 + .../lfkit.corrections.color_anchors.rst | 12 + .../generated/lfkit.corrections.filters.rst | 17 + .../lfkit.corrections.kcorrect_backend.rst | 12 + .../lfkit.corrections.kcorrect_from_color.rst | 12 + .../lfkit.corrections.kcorrect_grids.rst | 14 + .../lfkit.corrections.poggianti1997.rst | 21 + .../generated/lfkit.corrections.responses.rst | 16 + docs/api/generated/lfkit.corrections.rst | 19 + docs/api/generated/lfkit.cosmo.cosmology.rst | 17 + docs/api/generated/lfkit.cosmo.rst | 13 + ...fkit.luminosity_functions.completeness.rst | 16 + ...nosity_functions.conditional_integrals.rst | 14 + ...uminosity_functions.conditional_models.rst | 12 + .../lfkit.luminosity_functions.fractions.rst | 17 + .../lfkit.luminosity_functions.integrals.rst | 21 + ....luminosity_functions.models.composite.rst | 13 + ...fkit.luminosity_functions.models.gamma.rst | 13 + ...t.luminosity_functions.models.gaussian.rst | 13 + ....luminosity_functions.models.modifiers.rst | 12 + ...nosity_functions.models.non_parametric.rst | 17 + ....luminosity_functions.models.power_law.rst | 15 + .../lfkit.luminosity_functions.models.rst | 20 + ...t.luminosity_functions.models.saunders.rst | 15 + ....luminosity_functions.models.schechter.rst | 22 + ....luminosity_functions.parameter_models.rst | 23 + ....luminosity_functions.redshift_density.rst | 13 + .../lfkit.luminosity_functions.registry.rst | 24 + .../generated/lfkit.luminosity_functions.rst | 21 + .../lfkit.photometry.luminosities.rst | 16 + .../generated/lfkit.photometry.magnitudes.rst | 16 + docs/api/generated/lfkit.photometry.rst | 14 + docs/api/generated/lfkit.rst | 18 + .../lfkit.utils.download_poggianti97_data.rst | 12 + docs/api/generated/lfkit.utils.evaluators.rst | 16 + .../api/generated/lfkit.utils.integrators.rst | 14 + .../generated/lfkit.utils.interpolation.rst | 15 + docs/api/generated/lfkit.utils.io.rst | 18 + docs/api/generated/lfkit.utils.rst | 20 + docs/api/generated/lfkit.utils.types.rst | 6 + docs/api/generated/lfkit.utils.units.rst | 17 + docs/api/generated/lfkit.utils.validators.rst | 19 + docs/examples/fractions.rst | 815 ++++++++++++++++++ docs/examples/index.rst | 1 + 47 files changed, 1522 insertions(+) create mode 100644 docs/api/generated/lfkit.api.conditional_luminosity_function.rst create mode 100644 docs/api/generated/lfkit.api.corrections.rst create mode 100644 docs/api/generated/lfkit.api.luminosity_function.rst create mode 100644 docs/api/generated/lfkit.api.rst create mode 100644 docs/api/generated/lfkit.corrections.color_anchors.rst create mode 100644 docs/api/generated/lfkit.corrections.filters.rst create mode 100644 docs/api/generated/lfkit.corrections.kcorrect_backend.rst create mode 100644 docs/api/generated/lfkit.corrections.kcorrect_from_color.rst create mode 100644 docs/api/generated/lfkit.corrections.kcorrect_grids.rst create mode 100644 docs/api/generated/lfkit.corrections.poggianti1997.rst create mode 100644 docs/api/generated/lfkit.corrections.responses.rst create mode 100644 docs/api/generated/lfkit.corrections.rst create mode 100644 docs/api/generated/lfkit.cosmo.cosmology.rst create mode 100644 docs/api/generated/lfkit.cosmo.rst create mode 100644 docs/api/generated/lfkit.luminosity_functions.completeness.rst create mode 100644 docs/api/generated/lfkit.luminosity_functions.conditional_integrals.rst create mode 100644 docs/api/generated/lfkit.luminosity_functions.conditional_models.rst create mode 100644 docs/api/generated/lfkit.luminosity_functions.fractions.rst create mode 100644 docs/api/generated/lfkit.luminosity_functions.integrals.rst create mode 100644 docs/api/generated/lfkit.luminosity_functions.models.composite.rst create mode 100644 docs/api/generated/lfkit.luminosity_functions.models.gamma.rst create mode 100644 docs/api/generated/lfkit.luminosity_functions.models.gaussian.rst create mode 100644 docs/api/generated/lfkit.luminosity_functions.models.modifiers.rst create mode 100644 docs/api/generated/lfkit.luminosity_functions.models.non_parametric.rst create mode 100644 docs/api/generated/lfkit.luminosity_functions.models.power_law.rst create mode 100644 docs/api/generated/lfkit.luminosity_functions.models.rst create mode 100644 docs/api/generated/lfkit.luminosity_functions.models.saunders.rst create mode 100644 docs/api/generated/lfkit.luminosity_functions.models.schechter.rst create mode 100644 docs/api/generated/lfkit.luminosity_functions.parameter_models.rst create mode 100644 docs/api/generated/lfkit.luminosity_functions.redshift_density.rst create mode 100644 docs/api/generated/lfkit.luminosity_functions.registry.rst create mode 100644 docs/api/generated/lfkit.luminosity_functions.rst create mode 100644 docs/api/generated/lfkit.photometry.luminosities.rst create mode 100644 docs/api/generated/lfkit.photometry.magnitudes.rst create mode 100644 docs/api/generated/lfkit.photometry.rst create mode 100644 docs/api/generated/lfkit.rst create mode 100644 docs/api/generated/lfkit.utils.download_poggianti97_data.rst create mode 100644 docs/api/generated/lfkit.utils.evaluators.rst create mode 100644 docs/api/generated/lfkit.utils.integrators.rst create mode 100644 docs/api/generated/lfkit.utils.interpolation.rst create mode 100644 docs/api/generated/lfkit.utils.io.rst create mode 100644 docs/api/generated/lfkit.utils.rst create mode 100644 docs/api/generated/lfkit.utils.types.rst create mode 100644 docs/api/generated/lfkit.utils.units.rst create mode 100644 docs/api/generated/lfkit.utils.validators.rst create mode 100644 docs/examples/fractions.rst diff --git a/docs/api/generated/lfkit.api.conditional_luminosity_function.rst b/docs/api/generated/lfkit.api.conditional_luminosity_function.rst new file mode 100644 index 00000000..5f3c06a4 --- /dev/null +++ b/docs/api/generated/lfkit.api.conditional_luminosity_function.rst @@ -0,0 +1,12 @@ +lfkit.api.conditional\_luminosity\_function +=========================================== + +.. automodule:: lfkit.api.conditional_luminosity_function + + + .. rubric:: Classes + + .. autosummary:: + + ConditionalLuminosityFunction + \ No newline at end of file diff --git a/docs/api/generated/lfkit.api.corrections.rst b/docs/api/generated/lfkit.api.corrections.rst new file mode 100644 index 00000000..e1319fac --- /dev/null +++ b/docs/api/generated/lfkit.api.corrections.rst @@ -0,0 +1,12 @@ +lfkit.api.corrections +===================== + +.. automodule:: lfkit.api.corrections + + + .. rubric:: Classes + + .. autosummary:: + + Corrections + \ No newline at end of file diff --git a/docs/api/generated/lfkit.api.luminosity_function.rst b/docs/api/generated/lfkit.api.luminosity_function.rst new file mode 100644 index 00000000..542b1bb5 --- /dev/null +++ b/docs/api/generated/lfkit.api.luminosity_function.rst @@ -0,0 +1,12 @@ +lfkit.api.luminosity\_function +============================== + +.. automodule:: lfkit.api.luminosity_function + + + .. rubric:: Classes + + .. autosummary:: + + LuminosityFunction + \ No newline at end of file diff --git a/docs/api/generated/lfkit.api.rst b/docs/api/generated/lfkit.api.rst new file mode 100644 index 00000000..002dfc8a --- /dev/null +++ b/docs/api/generated/lfkit.api.rst @@ -0,0 +1,15 @@ +lfkit.api +========= + +.. automodule:: lfkit.api + + +.. rubric:: Modules + +.. autosummary:: + :toctree: + :recursive: + + conditional_luminosity_function + corrections + luminosity_function diff --git a/docs/api/generated/lfkit.corrections.color_anchors.rst b/docs/api/generated/lfkit.corrections.color_anchors.rst new file mode 100644 index 00000000..891f0865 --- /dev/null +++ b/docs/api/generated/lfkit.corrections.color_anchors.rst @@ -0,0 +1,12 @@ +lfkit.corrections.color\_anchors +================================ + +.. automodule:: lfkit.corrections.color_anchors + + + .. rubric:: Functions + + .. autosummary:: + + fit_coeffs_from_bandcolor + \ No newline at end of file diff --git a/docs/api/generated/lfkit.corrections.filters.rst b/docs/api/generated/lfkit.corrections.filters.rst new file mode 100644 index 00000000..d1d70450 --- /dev/null +++ b/docs/api/generated/lfkit.corrections.filters.rst @@ -0,0 +1,17 @@ +lfkit.corrections.filters +========================= + +.. automodule:: lfkit.corrections.filters + + + .. rubric:: Functions + + .. autosummary:: + + list_supported + make_response_map + normalize_band + normalize_filterset + resolve_response_name + validate_coverage + \ No newline at end of file diff --git a/docs/api/generated/lfkit.corrections.kcorrect_backend.rst b/docs/api/generated/lfkit.corrections.kcorrect_backend.rst new file mode 100644 index 00000000..9a8c0a6e --- /dev/null +++ b/docs/api/generated/lfkit.corrections.kcorrect_backend.rst @@ -0,0 +1,12 @@ +lfkit.corrections.kcorrect\_backend +=================================== + +.. automodule:: lfkit.corrections.kcorrect_backend + + + .. rubric:: Functions + + .. autosummary:: + + build_kcorrect + \ No newline at end of file diff --git a/docs/api/generated/lfkit.corrections.kcorrect_from_color.rst b/docs/api/generated/lfkit.corrections.kcorrect_from_color.rst new file mode 100644 index 00000000..43531a9f --- /dev/null +++ b/docs/api/generated/lfkit.corrections.kcorrect_from_color.rst @@ -0,0 +1,12 @@ +lfkit.corrections.kcorrect\_from\_color +======================================= + +.. automodule:: lfkit.corrections.kcorrect_from_color + + + .. rubric:: Functions + + .. autosummary:: + + kcorrect_from_bandcolor + \ No newline at end of file diff --git a/docs/api/generated/lfkit.corrections.kcorrect_grids.rst b/docs/api/generated/lfkit.corrections.kcorrect_grids.rst new file mode 100644 index 00000000..1e5e7ca8 --- /dev/null +++ b/docs/api/generated/lfkit.corrections.kcorrect_grids.rst @@ -0,0 +1,14 @@ +lfkit.corrections.kcorrect\_grids +================================= + +.. automodule:: lfkit.corrections.kcorrect_grids + + + .. rubric:: Functions + + .. autosummary:: + + build_kcorr_grid_package + compute_k_table + kcorr_interpolators + \ No newline at end of file diff --git a/docs/api/generated/lfkit.corrections.poggianti1997.rst b/docs/api/generated/lfkit.corrections.poggianti1997.rst new file mode 100644 index 00000000..5d77e221 --- /dev/null +++ b/docs/api/generated/lfkit.corrections.poggianti1997.rst @@ -0,0 +1,21 @@ +lfkit.corrections.poggianti1997 +=============================== + +.. automodule:: lfkit.corrections.poggianti1997 + + + .. rubric:: Functions + + .. autosummary:: + + available_pairs + describe_poggianti1997_available + extract_sed_spectrum + load_poggianti1997_tables + make_ecorr_interpolator + make_kcorr_interpolator + poggianti1997_lookback_time_gyr + poggianti1997_time_since_bb_gyr + poggianti1997_to_accelerating_redshift + z_from_lookback_time + \ No newline at end of file diff --git a/docs/api/generated/lfkit.corrections.responses.rst b/docs/api/generated/lfkit.corrections.responses.rst new file mode 100644 index 00000000..93fe638a --- /dev/null +++ b/docs/api/generated/lfkit.corrections.responses.rst @@ -0,0 +1,16 @@ +lfkit.corrections.responses +=========================== + +.. automodule:: lfkit.corrections.responses + + + .. rubric:: Functions + + .. autosummary:: + + discover_response_dir_auto + kcorrect_supports_response_dir + list_available_responses + require_responses + write_kcorrect_response + \ No newline at end of file diff --git a/docs/api/generated/lfkit.corrections.rst b/docs/api/generated/lfkit.corrections.rst new file mode 100644 index 00000000..cd579d56 --- /dev/null +++ b/docs/api/generated/lfkit.corrections.rst @@ -0,0 +1,19 @@ +lfkit.corrections +================= + +.. automodule:: lfkit.corrections + + +.. rubric:: Modules + +.. autosummary:: + :toctree: + :recursive: + + color_anchors + filters + kcorrect_backend + kcorrect_from_color + kcorrect_grids + poggianti1997 + responses diff --git a/docs/api/generated/lfkit.cosmo.cosmology.rst b/docs/api/generated/lfkit.cosmo.cosmology.rst new file mode 100644 index 00000000..d544eef4 --- /dev/null +++ b/docs/api/generated/lfkit.cosmo.cosmology.rst @@ -0,0 +1,17 @@ +lfkit.cosmo.cosmology +===================== + +.. automodule:: lfkit.cosmo.cosmology + + + .. rubric:: Functions + + .. autosummary:: + + comoving_distance_mpc + cosmo_object + differential_comoving_volume + distance_modulus + lookback_time_gyr + luminosity_distance_mpc + \ No newline at end of file diff --git a/docs/api/generated/lfkit.cosmo.rst b/docs/api/generated/lfkit.cosmo.rst new file mode 100644 index 00000000..e731c99c --- /dev/null +++ b/docs/api/generated/lfkit.cosmo.rst @@ -0,0 +1,13 @@ +lfkit.cosmo +=========== + +.. automodule:: lfkit.cosmo + + +.. rubric:: Modules + +.. autosummary:: + :toctree: + :recursive: + + cosmology diff --git a/docs/api/generated/lfkit.luminosity_functions.completeness.rst b/docs/api/generated/lfkit.luminosity_functions.completeness.rst new file mode 100644 index 00000000..73fa550b --- /dev/null +++ b/docs/api/generated/lfkit.luminosity_functions.completeness.rst @@ -0,0 +1,16 @@ +lfkit.luminosity\_functions.completeness +======================================== + +.. automodule:: lfkit.luminosity_functions.completeness + + + .. rubric:: Functions + + .. autosummary:: + + absolute_magnitude_limit + catalog_fraction + missing_number_density + observed_number_density + out_of_catalog_fraction + \ No newline at end of file diff --git a/docs/api/generated/lfkit.luminosity_functions.conditional_integrals.rst b/docs/api/generated/lfkit.luminosity_functions.conditional_integrals.rst new file mode 100644 index 00000000..365e4c4d --- /dev/null +++ b/docs/api/generated/lfkit.luminosity_functions.conditional_integrals.rst @@ -0,0 +1,14 @@ +lfkit.luminosity\_functions.conditional\_integrals +================================================== + +.. automodule:: lfkit.luminosity_functions.conditional_integrals + + + .. rubric:: Functions + + .. autosummary:: + + evaluate_conditional_luminosity_function + integrate_conditional_luminosity_function + integrate_weighted_conditional_luminosity_function + \ No newline at end of file diff --git a/docs/api/generated/lfkit.luminosity_functions.conditional_models.rst b/docs/api/generated/lfkit.luminosity_functions.conditional_models.rst new file mode 100644 index 00000000..a0dd91e9 --- /dev/null +++ b/docs/api/generated/lfkit.luminosity_functions.conditional_models.rst @@ -0,0 +1,12 @@ +lfkit.luminosity\_functions.conditional\_models +=============================================== + +.. automodule:: lfkit.luminosity_functions.conditional_models + + + .. rubric:: Functions + + .. autosummary:: + + conditionalize_lf_model + \ No newline at end of file diff --git a/docs/api/generated/lfkit.luminosity_functions.fractions.rst b/docs/api/generated/lfkit.luminosity_functions.fractions.rst new file mode 100644 index 00000000..508de46b --- /dev/null +++ b/docs/api/generated/lfkit.luminosity_functions.fractions.rst @@ -0,0 +1,17 @@ +lfkit.luminosity\_functions.fractions +===================================== + +.. automodule:: lfkit.luminosity_functions.fractions + + + .. rubric:: Functions + + .. autosummary:: + + blue_fraction_from_luminosity_functions + complement_fraction_from_luminosity_functions + fraction_from_luminosity_functions + population_densities_from_luminosity_functions + red_blue_fractions_from_luminosity_functions + red_fraction_from_luminosity_functions + \ No newline at end of file diff --git a/docs/api/generated/lfkit.luminosity_functions.integrals.rst b/docs/api/generated/lfkit.luminosity_functions.integrals.rst new file mode 100644 index 00000000..f87da20c --- /dev/null +++ b/docs/api/generated/lfkit.luminosity_functions.integrals.rst @@ -0,0 +1,21 @@ +lfkit.luminosity\_functions.integrals +===================================== + +.. automodule:: lfkit.luminosity_functions.integrals + + + .. rubric:: Functions + + .. autosummary:: + + cumulative_number_density + cumulative_selection_function + integrated_luminosity_density + integrated_number_density + lf_weighted_integral + luminosity_weight + magnitude_window_number_density + mean_luminosity + selection_fraction + selection_weighted_number_density + \ No newline at end of file diff --git a/docs/api/generated/lfkit.luminosity_functions.models.composite.rst b/docs/api/generated/lfkit.luminosity_functions.models.composite.rst new file mode 100644 index 00000000..e2694b06 --- /dev/null +++ b/docs/api/generated/lfkit.luminosity_functions.models.composite.rst @@ -0,0 +1,13 @@ +lfkit.luminosity\_functions.models.composite +============================================ + +.. automodule:: lfkit.luminosity_functions.models.composite + + + .. rubric:: Functions + + .. autosummary:: + + additive_lf + two_component_lf + \ No newline at end of file diff --git a/docs/api/generated/lfkit.luminosity_functions.models.gamma.rst b/docs/api/generated/lfkit.luminosity_functions.models.gamma.rst new file mode 100644 index 00000000..d2a75cda --- /dev/null +++ b/docs/api/generated/lfkit.luminosity_functions.models.gamma.rst @@ -0,0 +1,13 @@ +lfkit.luminosity\_functions.models.gamma +======================================== + +.. automodule:: lfkit.luminosity_functions.models.gamma + + + .. rubric:: Functions + + .. autosummary:: + + gamma_lf + generalized_gamma_lf + \ No newline at end of file diff --git a/docs/api/generated/lfkit.luminosity_functions.models.gaussian.rst b/docs/api/generated/lfkit.luminosity_functions.models.gaussian.rst new file mode 100644 index 00000000..a7f3b86a --- /dev/null +++ b/docs/api/generated/lfkit.luminosity_functions.models.gaussian.rst @@ -0,0 +1,13 @@ +lfkit.luminosity\_functions.models.gaussian +=========================================== + +.. automodule:: lfkit.luminosity_functions.models.gaussian + + + .. rubric:: Functions + + .. autosummary:: + + gaussian_lf + lognormal_lf + \ No newline at end of file diff --git a/docs/api/generated/lfkit.luminosity_functions.models.modifiers.rst b/docs/api/generated/lfkit.luminosity_functions.models.modifiers.rst new file mode 100644 index 00000000..ed38ada8 --- /dev/null +++ b/docs/api/generated/lfkit.luminosity_functions.models.modifiers.rst @@ -0,0 +1,12 @@ +lfkit.luminosity\_functions.models.modifiers +============================================ + +.. automodule:: lfkit.luminosity_functions.models.modifiers + + + .. rubric:: Functions + + .. autosummary:: + + apply_luminosity_cutoff + \ No newline at end of file diff --git a/docs/api/generated/lfkit.luminosity_functions.models.non_parametric.rst b/docs/api/generated/lfkit.luminosity_functions.models.non_parametric.rst new file mode 100644 index 00000000..ad46bdd5 --- /dev/null +++ b/docs/api/generated/lfkit.luminosity_functions.models.non_parametric.rst @@ -0,0 +1,17 @@ +lfkit.luminosity\_functions.models.non\_parametric +================================================== + +.. automodule:: lfkit.luminosity_functions.models.non_parametric + + + .. rubric:: Functions + + .. autosummary:: + + binned_lf + distance_binned_lf + distance_tabulated_lf + redshift_binned_lf + redshift_tabulated_lf + tabulated_lf + \ No newline at end of file diff --git a/docs/api/generated/lfkit.luminosity_functions.models.power_law.rst b/docs/api/generated/lfkit.luminosity_functions.models.power_law.rst new file mode 100644 index 00000000..279379ff --- /dev/null +++ b/docs/api/generated/lfkit.luminosity_functions.models.power_law.rst @@ -0,0 +1,15 @@ +lfkit.luminosity\_functions.models.power\_law +============================================= + +.. automodule:: lfkit.luminosity_functions.models.power_law + + + .. rubric:: Functions + + .. autosummary:: + + broken_power_law_lf + double_power_law_lf + log_power_law_lf + power_law_lf + \ No newline at end of file diff --git a/docs/api/generated/lfkit.luminosity_functions.models.rst b/docs/api/generated/lfkit.luminosity_functions.models.rst new file mode 100644 index 00000000..87fec274 --- /dev/null +++ b/docs/api/generated/lfkit.luminosity_functions.models.rst @@ -0,0 +1,20 @@ +lfkit.luminosity\_functions.models +================================== + +.. automodule:: lfkit.luminosity_functions.models + + +.. rubric:: Modules + +.. autosummary:: + :toctree: + :recursive: + + composite + gamma + gaussian + modifiers + non_parametric + power_law + saunders + schechter diff --git a/docs/api/generated/lfkit.luminosity_functions.models.saunders.rst b/docs/api/generated/lfkit.luminosity_functions.models.saunders.rst new file mode 100644 index 00000000..402ff67d --- /dev/null +++ b/docs/api/generated/lfkit.luminosity_functions.models.saunders.rst @@ -0,0 +1,15 @@ +lfkit.luminosity\_functions.models.saunders +=========================================== + +.. automodule:: lfkit.luminosity_functions.models.saunders + + + .. rubric:: Functions + + .. autosummary:: + + double_saunders_lf + evolving_saunders_lf + generalized_saunders_lf + saunders_lf + \ No newline at end of file diff --git a/docs/api/generated/lfkit.luminosity_functions.models.schechter.rst b/docs/api/generated/lfkit.luminosity_functions.models.schechter.rst new file mode 100644 index 00000000..d199d845 --- /dev/null +++ b/docs/api/generated/lfkit.luminosity_functions.models.schechter.rst @@ -0,0 +1,22 @@ +lfkit.luminosity\_functions.models.schechter +============================================ + +.. automodule:: lfkit.luminosity_functions.models.schechter + + + .. rubric:: Functions + + .. autosummary:: + + double_schechter + double_schechter_from_m + evolving_schechter + evolving_schechter_from_m + modified_schechter + multi_schechter + schechter + schechter_cumulative + schechter_cumulative_evolving + schechter_from_m + truncated_schechter + \ No newline at end of file diff --git a/docs/api/generated/lfkit.luminosity_functions.parameter_models.rst b/docs/api/generated/lfkit.luminosity_functions.parameter_models.rst new file mode 100644 index 00000000..c4c9c60d --- /dev/null +++ b/docs/api/generated/lfkit.luminosity_functions.parameter_models.rst @@ -0,0 +1,23 @@ +lfkit.luminosity\_functions.parameter\_models +============================================= + +.. automodule:: lfkit.luminosity_functions.parameter_models + + + .. rubric:: Functions + + .. autosummary:: + + alpha_constant + alpha_linear + available_lf_parameter_models + evaluate_lf_parameters + get_parameter_model + m_star_constant + m_star_linear_q + phi_star_constant + phi_star_linear_p + register_alpha_model + register_m_star_model + register_phi_star_model + \ No newline at end of file diff --git a/docs/api/generated/lfkit.luminosity_functions.redshift_density.rst b/docs/api/generated/lfkit.luminosity_functions.redshift_density.rst new file mode 100644 index 00000000..892a0634 --- /dev/null +++ b/docs/api/generated/lfkit.luminosity_functions.redshift_density.rst @@ -0,0 +1,13 @@ +lfkit.luminosity\_functions.redshift\_density +============================================= + +.. automodule:: lfkit.luminosity_functions.redshift_density + + + .. rubric:: Functions + + .. autosummary:: + + lf_integrated_number_density + lf_weighted_redshift_density + \ No newline at end of file diff --git a/docs/api/generated/lfkit.luminosity_functions.registry.rst b/docs/api/generated/lfkit.luminosity_functions.registry.rst new file mode 100644 index 00000000..b2e394e0 --- /dev/null +++ b/docs/api/generated/lfkit.luminosity_functions.registry.rst @@ -0,0 +1,24 @@ +lfkit.luminosity\_functions.registry +==================================== + +.. automodule:: lfkit.luminosity_functions.registry + + + .. rubric:: Functions + + .. autosummary:: + + available_conditional_lf_models + available_lf_from_m_models + available_lf_models + discover_lf_models + get_conditional_lf_model + get_lf_from_m_model + get_lf_model + + .. rubric:: Classes + + .. autosummary:: + + LFModel + \ No newline at end of file diff --git a/docs/api/generated/lfkit.luminosity_functions.rst b/docs/api/generated/lfkit.luminosity_functions.rst new file mode 100644 index 00000000..97c6b50f --- /dev/null +++ b/docs/api/generated/lfkit.luminosity_functions.rst @@ -0,0 +1,21 @@ +lfkit.luminosity\_functions +=========================== + +.. automodule:: lfkit.luminosity_functions + + +.. rubric:: Modules + +.. autosummary:: + :toctree: + :recursive: + + completeness + conditional_integrals + conditional_models + fractions + integrals + models + parameter_models + redshift_density + registry diff --git a/docs/api/generated/lfkit.photometry.luminosities.rst b/docs/api/generated/lfkit.photometry.luminosities.rst new file mode 100644 index 00000000..25cd3bc4 --- /dev/null +++ b/docs/api/generated/lfkit.photometry.luminosities.rst @@ -0,0 +1,16 @@ +lfkit.photometry.luminosities +============================= + +.. automodule:: lfkit.photometry.luminosities + + + .. rubric:: Functions + + .. autosummary:: + + luminosity_from_magnitude + luminosity_ratio + luminosity_ratio_from_magnitudes + luminosity_weight_from_magnitude + magnitude_difference_from_luminosity_ratio + \ No newline at end of file diff --git a/docs/api/generated/lfkit.photometry.magnitudes.rst b/docs/api/generated/lfkit.photometry.magnitudes.rst new file mode 100644 index 00000000..022bbc7f --- /dev/null +++ b/docs/api/generated/lfkit.photometry.magnitudes.rst @@ -0,0 +1,16 @@ +lfkit.photometry.magnitudes +=========================== + +.. automodule:: lfkit.photometry.magnitudes + + + .. rubric:: Functions + + .. autosummary:: + + absolute_magnitude + absolute_magnitude_from_luminosity_distance + apparent_magnitude + apparent_magnitude_from_luminosity_distance + total_magnitude_correction + \ No newline at end of file diff --git a/docs/api/generated/lfkit.photometry.rst b/docs/api/generated/lfkit.photometry.rst new file mode 100644 index 00000000..60f5cd38 --- /dev/null +++ b/docs/api/generated/lfkit.photometry.rst @@ -0,0 +1,14 @@ +lfkit.photometry +================ + +.. automodule:: lfkit.photometry + + +.. rubric:: Modules + +.. autosummary:: + :toctree: + :recursive: + + luminosities + magnitudes diff --git a/docs/api/generated/lfkit.rst b/docs/api/generated/lfkit.rst new file mode 100644 index 00000000..1563afa1 --- /dev/null +++ b/docs/api/generated/lfkit.rst @@ -0,0 +1,18 @@ +lfkit +===== + +.. automodule:: lfkit + + +.. rubric:: Modules + +.. autosummary:: + :toctree: + :recursive: + + api + corrections + cosmo + luminosity_functions + photometry + utils diff --git a/docs/api/generated/lfkit.utils.download_poggianti97_data.rst b/docs/api/generated/lfkit.utils.download_poggianti97_data.rst new file mode 100644 index 00000000..ae2f4b1c --- /dev/null +++ b/docs/api/generated/lfkit.utils.download_poggianti97_data.rst @@ -0,0 +1,12 @@ +lfkit.utils.download\_poggianti97\_data +======================================= + +.. automodule:: lfkit.utils.download_poggianti97_data + + + .. rubric:: Functions + + .. autosummary:: + + main + \ No newline at end of file diff --git a/docs/api/generated/lfkit.utils.evaluators.rst b/docs/api/generated/lfkit.utils.evaluators.rst new file mode 100644 index 00000000..36f78e42 --- /dev/null +++ b/docs/api/generated/lfkit.utils.evaluators.rst @@ -0,0 +1,16 @@ +lfkit.utils.evaluators +====================== + +.. automodule:: lfkit.utils.evaluators + + + .. rubric:: Functions + + .. autosummary:: + + evaluate_lf_on_grid + evaluate_non_negative_redshift_callable + evaluate_optional_redshift_callable + evaluate_positive_redshift_callable + evaluate_weight_on_grid + \ No newline at end of file diff --git a/docs/api/generated/lfkit.utils.integrators.rst b/docs/api/generated/lfkit.utils.integrators.rst new file mode 100644 index 00000000..d91a3135 --- /dev/null +++ b/docs/api/generated/lfkit.utils.integrators.rst @@ -0,0 +1,14 @@ +lfkit.utils.integrators +======================= + +.. automodule:: lfkit.utils.integrators + + + .. rubric:: Functions + + .. autosummary:: + + integrate_between_variable_bounds + safe_divide + safe_power10 + \ No newline at end of file diff --git a/docs/api/generated/lfkit.utils.interpolation.rst b/docs/api/generated/lfkit.utils.interpolation.rst new file mode 100644 index 00000000..7387cb57 --- /dev/null +++ b/docs/api/generated/lfkit.utils.interpolation.rst @@ -0,0 +1,15 @@ +lfkit.utils.interpolation +========================= + +.. automodule:: lfkit.utils.interpolation + + + .. rubric:: Functions + + .. autosummary:: + + as_1d_finite_grid + build_1d_interpolator + linear_interp_extrap + prep_strictly_increasing_xy + \ No newline at end of file diff --git a/docs/api/generated/lfkit.utils.io.rst b/docs/api/generated/lfkit.utils.io.rst new file mode 100644 index 00000000..0fe2d9bb --- /dev/null +++ b/docs/api/generated/lfkit.utils.io.rst @@ -0,0 +1,18 @@ +lfkit.utils.io +============== + +.. automodule:: lfkit.utils.io + + + .. rubric:: Functions + + .. autosummary:: + + available_from_table + available_pairs + extract_series + load_kcorr_package + load_vizier_csv + resolve_packaged_csv + save_kcorr_package + \ No newline at end of file diff --git a/docs/api/generated/lfkit.utils.rst b/docs/api/generated/lfkit.utils.rst new file mode 100644 index 00000000..9330b50b --- /dev/null +++ b/docs/api/generated/lfkit.utils.rst @@ -0,0 +1,20 @@ +lfkit.utils +=========== + +.. automodule:: lfkit.utils + + +.. rubric:: Modules + +.. autosummary:: + :toctree: + :recursive: + + download_poggianti97_data + evaluators + integrators + interpolation + io + types + units + validators diff --git a/docs/api/generated/lfkit.utils.types.rst b/docs/api/generated/lfkit.utils.types.rst new file mode 100644 index 00000000..494d5733 --- /dev/null +++ b/docs/api/generated/lfkit.utils.types.rst @@ -0,0 +1,6 @@ +lfkit.utils.types +================= + +.. automodule:: lfkit.utils.types + + \ No newline at end of file diff --git a/docs/api/generated/lfkit.utils.units.rst b/docs/api/generated/lfkit.utils.units.rst new file mode 100644 index 00000000..b009dca8 --- /dev/null +++ b/docs/api/generated/lfkit.utils.units.rst @@ -0,0 +1,17 @@ +lfkit.utils.units +================= + +.. automodule:: lfkit.utils.units + + + .. rubric:: Functions + + .. autosummary:: + + h0_km_s_mpc_to_gyr_inv + km_per_mpc + mag_to_maggies + magerr_to_ivar_maggies + maggies_to_mag + sec_per_gyr + \ No newline at end of file diff --git a/docs/api/generated/lfkit.utils.validators.rst b/docs/api/generated/lfkit.utils.validators.rst new file mode 100644 index 00000000..5e6fcaa8 --- /dev/null +++ b/docs/api/generated/lfkit.utils.validators.rst @@ -0,0 +1,19 @@ +lfkit.utils.validators +====================== + +.. automodule:: lfkit.utils.validators + + + .. rubric:: Functions + + .. autosummary:: + + validate_2d_binned_grid + validate_2d_tabulated_grid + validate_array + validate_binned_grid + validate_luminosity_distance + validate_magnitude_range + validate_strictly_increasing_1d + validate_tabulated_grid + \ No newline at end of file diff --git a/docs/examples/fractions.rst b/docs/examples/fractions.rst new file mode 100644 index 00000000..ad7db3e0 --- /dev/null +++ b/docs/examples/fractions.rst @@ -0,0 +1,815 @@ +.. |lfkitlogo| image:: /_static/logos/lfkit_logo-icon.png + :alt: LFKit logo + :width: 50px + +|lfkitlogo| Luminosity function fractions +========================================= + +This page shows how to compute population fractions from luminosity functions. + +The ``fractions`` namespace is useful when one luminosity function describes a +subsample and another luminosity function describes the full sample. For example, +the numerator could be a red galaxy luminosity function and the denominator could +be the total galaxy luminosity function. The resulting fraction is an integrated +quantity: both luminosity functions are integrated over the same absolute +magnitude range, and the ratio of those integrals is returned. + +This is useful for comparing how the relative abundance of a population changes +with redshift, magnitude selection, sample depth, or model choice. + +In the examples below, we often use ``red`` and ``blue`` luminosity functions as +a simple cosmology motivated population split. In this context, red samples are +often used as a rough proxy for elliptical or early type galaxies, while blue +samples are often used as a rough proxy for spiral or late type galaxies. This +is a simplified classification, but it is common in galaxy evolution and +cosmology applications. + +The same functions can be used for any pair of galaxy populations, not only red +and blue samples. For example, the numerator could describe star forming +galaxies, quenched galaxies, centrals, satellites, emission line galaxies, or any +other selected subsample. In each case, the denominator should describe the full +population relative to which the fraction is meant to be interpreted. + +The denominator can be another ``LuminosityFunction`` instance or a callable. A +callable denominator should accept absolute magnitude and redshift as positional +arguments: + +.. code-block:: python + + def denominator_lf(absolute_mag, redshift): + return phi_total + +All examples below are executable via ``.. plot::``. + + +Red fraction from two luminosity functions +------------------------------------------ + +The most direct use is to bind the numerator luminosity function and pass the +denominator luminosity function to ``fraction``. + +In this example, the numerator is a red galaxy luminosity function and the +denominator is a total galaxy luminosity function. The plotted curve shows how +the integrated red fraction evolves with redshift for a fixed absolute magnitude +range. This is the typical use case when the sample selection is fixed and one +wants to study population evolution. + +.. 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 + + red_lf = LuminosityFunction.evolving_schechter( + phi_model="linear_p", + phi_kwargs={"phi_0_star": 5.0e-4, "p": -0.2}, + m_star_model="linear_q", + m_star_kwargs={"m_0_star": -20.4, "q": 0.5, "z_ref": 0.1}, + alpha_model="constant", + alpha_kwargs={"alpha": -0.8}, + ) + + all_lf = LuminosityFunction.evolving_schechter( + phi_model="linear_p", + phi_kwargs={"phi_0_star": 1.2e-3, "p": 0.4}, + m_star_model="linear_q", + m_star_kwargs={"m_0_star": -20.6, "q": 0.7, "z_ref": 0.1}, + alpha_model="constant", + alpha_kwargs={"alpha": -1.1}, + ) + + redshift = np.linspace(0.05, 1.4, 120) + + red_fraction = np.array( + [ + red_lf.fractions.fraction( + z, + all_lf, + m_bright=-24.0, + m_faint=-18.0, + n_m=600, + ) + for z in redshift + ] + ) + + red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))[1] + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + ax.plot( + redshift, + red_fraction, + lw=3, + color=red, + label="Red fraction", + ) + + ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) + ax.set_ylabel("LF fraction", fontsize=LABEL_SIZE) + ax.set_title("Fraction from red and total luminosity functions", fontsize=TITLE_SIZE) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + ax.set_ylim(0.0, 1.0) + ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() + + +Red and blue fractions +---------------------- + +The ``red_fraction`` and ``blue_fraction`` helpers are convenience methods for +the common color split case. The blue fraction is the complement of the red +fraction with respect to the denominator luminosity function. The total fraction +is shown as a reference line at one. + +This plot is a useful sanity check for two population fractions. If the red +fraction increases, the blue fraction decreases by the same amount because the +blue fraction is defined as ``1 - red_fraction``. This is appropriate when the +denominator represents the full sample and the numerator represents one +subsample. + +.. 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 + + red_lf = LuminosityFunction.evolving_schechter( + phi_model="linear_p", + phi_kwargs={"phi_0_star": 3.5e-4, "p": -0.4}, + m_star_model="linear_q", + m_star_kwargs={"m_0_star": -20.3, "q": 0.4, "z_ref": 0.1}, + alpha_model="constant", + alpha_kwargs={"alpha": -0.7}, + ) + + all_lf = LuminosityFunction.evolving_schechter( + phi_model="linear_p", + phi_kwargs={"phi_0_star": 1.4e-3, "p": 0.3}, + m_star_model="linear_q", + m_star_kwargs={"m_0_star": -20.7, "q": 0.7, "z_ref": 0.1}, + alpha_model="constant", + alpha_kwargs={"alpha": -1.1}, + ) + + redshift = np.linspace(0.05, 1.4, 120) + + red_fraction = np.array( + [ + red_lf.fractions.red_fraction( + z, + all_lf, + m_bright=-24.0, + m_faint=-18.0, + n_m=600, + ) + for z in redshift + ] + ) + + blue_fraction = np.array( + [ + red_lf.fractions.blue_fraction( + z, + all_lf, + m_bright=-24.0, + m_faint=-18.0, + n_m=600, + ) + for z in redshift + ] + ) + + red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))[1] + blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0))[1] + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + ax.plot( + redshift, + blue_fraction, + lw=3, + color=blue, + label="Blue fraction", + ) + ax.plot( + redshift, + red_fraction, + lw=3, + color=red, + label="Red fraction", + ) + ax.axhline( + 1.0, + color="k", + lw=2, + ls="--", + alpha=0.6, + label="Total reference", + ) + + ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) + ax.set_ylabel("LF fraction", fontsize=LABEL_SIZE) + ax.set_title("Red and blue LF fractions", fontsize=TITLE_SIZE) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + ax.set_ylim(0.0, 1.08) + ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() + + +Complement fraction +------------------- + +Sometimes one luminosity function describes a selected population and another +describes the full population. In that case the complement fraction is useful for +the population not described by the numerator. For example, if the numerator is +the red luminosity function, then the complement is the blue fraction with +respect to the total luminosity function. + +This is useful when only one subsample luminosity function is available. Instead +of explicitly modeling the second subsample, the complement gives the remaining +fraction implied by the chosen numerator and denominator models. + +.. 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 + + red_lf = LuminosityFunction.evolving_schechter( + phi_model="linear_p", + phi_kwargs={"phi_0_star": 3.5e-4, "p": -0.4}, + m_star_model="linear_q", + m_star_kwargs={"m_0_star": -20.3, "q": 0.4, "z_ref": 0.1}, + alpha_model="constant", + alpha_kwargs={"alpha": -0.7}, + ) + + all_lf = LuminosityFunction.evolving_schechter( + phi_model="linear_p", + phi_kwargs={"phi_0_star": 1.4e-3, "p": 0.3}, + m_star_model="linear_q", + m_star_kwargs={"m_0_star": -20.7, "q": 0.7, "z_ref": 0.1}, + alpha_model="constant", + alpha_kwargs={"alpha": -1.1}, + ) + + redshift = np.linspace(0.05, 1.4, 120) + + red_fraction = np.array( + [ + red_lf.fractions.red_fraction( + z, + all_lf, + m_bright=-24.0, + m_faint=-18.0, + n_m=600, + ) + for z in redshift + ] + ) + + complement_fraction = np.array( + [ + red_lf.fractions.complement_fraction( + z, + all_lf, + m_bright=-24.0, + m_faint=-18.0, + n_m=600, + ) + for z in redshift + ] + ) + + red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))[1] + blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0))[1] + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + ax.plot( + redshift, + red_fraction, + lw=3, + color=red, + label="Red fraction", + ) + ax.plot( + redshift, + complement_fraction, + lw=3, + color=blue, + label="Complement fraction", + ) + + ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) + ax.set_ylabel("LF fraction", fontsize=LABEL_SIZE) + ax.set_title("Complement of a luminosity function fraction", fontsize=TITLE_SIZE) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + ax.set_ylim(0.0, 1.0) + ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() + + +Magnitude range dependence +-------------------------- + +Fractions depend on the absolute magnitude range used in the integral. This is +useful when comparing bright, intermediate, and faint galaxy selections. In this +example both the numerator and denominator luminosity functions evolve with +redshift, so the fraction changes with redshift as well as with magnitude range. + +Different magnitude ranges can give different population fractions because the +relative shape of the numerator and denominator luminosity functions is not +constant with magnitude. This plot is useful for checking whether a population +fraction is driven mostly by bright galaxies, faint galaxies, or the full +selected magnitude interval. + +.. 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 + + red_lf = LuminosityFunction.evolving_schechter( + phi_model="linear_p", + phi_kwargs={"phi_0_star": 3.5e-4, "p": -0.6}, + m_star_model="linear_q", + m_star_kwargs={"m_0_star": -20.2, "q": 0.2, "z_ref": 0.1}, + alpha_model="constant", + alpha_kwargs={"alpha": -0.6}, + ) + + all_lf = LuminosityFunction.evolving_schechter( + phi_model="linear_p", + phi_kwargs={"phi_0_star": 1.4e-3, "p": 0.5}, + m_star_model="linear_q", + m_star_kwargs={"m_0_star": -20.8, "q": 1.0, "z_ref": 0.1}, + alpha_model="constant", + alpha_kwargs={"alpha": -1.25}, + ) + + redshift = np.linspace(0.05, 1.4, 120) + + magnitude_windows = [ + (-24.0, -21.0, "Bright"), + (-21.0, -19.0, "Intermediate"), + (-19.0, -17.0, "Faint"), + ] + + colors = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2)) + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + + for (m_bright, m_faint, label), color in zip( + magnitude_windows, + colors, + strict=True, + ): + fraction = np.array( + [ + red_lf.fractions.fraction( + z, + all_lf, + m_bright=m_bright, + m_faint=m_faint, + n_m=600, + ) + for z in redshift + ] + ) + + ax.plot( + redshift, + fraction, + lw=3, + color=color, + label=label, + ) + + ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) + ax.set_ylabel("LF fraction", fontsize=LABEL_SIZE) + ax.set_title("Magnitude dependent LF fraction evolution", fontsize=TITLE_SIZE) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() + + +Fraction as a function of faint magnitude limit +----------------------------------------------- + +The fraction can also be evaluated as a function of the faint absolute magnitude +limit at fixed redshift. This is useful for checking how strongly a population +fraction depends on sample depth. + +In this example, the bright limit is held fixed while the faint limit is varied. +Moving to fainter magnitude limits adds fainter galaxies to both the numerator +and denominator integrals. If the curves change strongly with ``M_faint``, then +the inferred population fraction is sensitive to the survey depth or luminosity +cut. If the curves are nearly flat, then the fraction is mostly set by the bright +part of the luminosity functions over the plotted 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 + + red_lf = LuminosityFunction.evolving_schechter( + phi_model="linear_p", + phi_kwargs={"phi_0_star": 4.0e-4, "p": -0.3}, + m_star_model="linear_q", + m_star_kwargs={"m_0_star": -20.4, "q": 0.4, "z_ref": 0.1}, + alpha_model="constant", + alpha_kwargs={"alpha": -0.7}, + ) + + all_lf = LuminosityFunction.evolving_schechter( + phi_model="linear_p", + phi_kwargs={"phi_0_star": 1.4e-3, "p": 0.3}, + m_star_model="linear_q", + m_star_kwargs={"m_0_star": -20.7, "q": 0.7, "z_ref": 0.1}, + alpha_model="constant", + alpha_kwargs={"alpha": -1.2}, + ) + + faint_limits = np.linspace(-22.5, -17.0, 120) + redshifts = [0.2, 0.6, 1.0] + + colors = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2)) + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + + for z, color in zip(redshifts, colors, strict=True): + fraction = np.array( + [ + red_lf.fractions.red_fraction( + z, + all_lf, + m_bright=-24.0, + m_faint=m_faint, + n_m=600, + ) + for m_faint in faint_limits + ] + ) + + ax.plot( + faint_limits, + fraction, + lw=3, + color=color, + label=rf"$z={z}$", + ) + + ax.set_xlabel(r"Faint absolute magnitude limit $M_{\rm faint}$", fontsize=LABEL_SIZE) + ax.set_ylabel("Red fraction", fontsize=LABEL_SIZE) + ax.set_title("Red fraction dependence on sample depth", fontsize=TITLE_SIZE) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() + + +Population densities +-------------------- + +The ``population_densities`` helper returns the integrated number densities for +two luminosity functions and their sum. This is useful for checking whether two +subpopulations reconstruct the total population over the chosen magnitude range. + +Unlike the fraction helpers, this function returns the integrated densities +themselves rather than their ratio. This makes it useful for diagnostics: one can +check whether the red and blue densities have sensible amplitudes, whether their +sum evolves smoothly, and whether the assumed subpopulation luminosity functions +are consistent with the expected total abundance. + +.. 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 + + red_lf = LuminosityFunction.evolving_schechter( + phi_model="linear_p", + phi_kwargs={"phi_0_star": 3.5e-4, "p": -0.4}, + m_star_model="linear_q", + m_star_kwargs={"m_0_star": -20.3, "q": 0.4, "z_ref": 0.1}, + alpha_model="constant", + alpha_kwargs={"alpha": -0.7}, + ) + + blue_lf = LuminosityFunction.evolving_schechter( + phi_model="linear_p", + phi_kwargs={"phi_0_star": 8.0e-4, "p": 0.5}, + m_star_model="linear_q", + m_star_kwargs={"m_0_star": -20.8, "q": 0.9, "z_ref": 0.1}, + alpha_model="constant", + alpha_kwargs={"alpha": -1.25}, + ) + + redshift = np.linspace(0.05, 1.4, 120) + + red_density = [] + blue_density = [] + total_density = [] + + for z in redshift: + red_n, blue_n, total_n = red_lf.fractions.population_densities( + z, + blue_lf, + m_bright=-24.0, + m_faint=-18.0, + n_m=600, + ) + + red_density.append(red_n) + blue_density.append(blue_n) + total_density.append(total_n) + + red_density = np.asarray(red_density) + blue_density = np.asarray(blue_density) + total_density = np.asarray(total_density) + + red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))[1] + blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0))[1] + total = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.2, 0.8))[1] + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + ax.plot( + redshift, + total_density, + lw=3, + color=total, + label="Red plus blue density", + ) + ax.plot( + redshift, + blue_density, + lw=3, + color=blue, + label="Blue density", + ) + ax.plot( + redshift, + red_density, + lw=3, + color=red, + label="Red density", + ) + + ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) + ax.set_ylabel(r"Integrated number density", fontsize=LABEL_SIZE) + ax.set_title("Population densities from luminosity functions", fontsize=TITLE_SIZE) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() + + +Numerator and denominator integrals +----------------------------------- + +The fraction is the ratio of two luminosity function integrals over the same +absolute magnitude range. The numerator is the bound luminosity function and the +denominator is the luminosity function passed to ``fraction``. + +This diagnostic plot shows the two luminosity functions before integration. The +shaded region marks the magnitude range used in the fraction calculation. It is +often helpful to inspect this plot when a fraction looks surprising, because the +answer may be driven by the relative normalization, faint end slope, or the +chosen magnitude limits. + +.. 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 + + red_lf = LuminosityFunction.evolving_schechter( + phi_model="linear_p", + phi_kwargs={"phi_0_star": 4.0e-4, "p": -0.3}, + m_star_model="linear_q", + m_star_kwargs={"m_0_star": -20.4, "q": 0.4, "z_ref": 0.1}, + alpha_model="constant", + alpha_kwargs={"alpha": -0.7}, + ) + + all_lf = LuminosityFunction.evolving_schechter( + phi_model="linear_p", + phi_kwargs={"phi_0_star": 1.4e-3, "p": 0.3}, + m_star_model="linear_q", + m_star_kwargs={"m_0_star": -20.7, "q": 0.7, "z_ref": 0.1}, + alpha_model="constant", + alpha_kwargs={"alpha": -1.2}, + ) + + z = 0.6 + absolute_mag = np.linspace(-24.5, -16.5, 400) + m_bright = -24.0 + m_faint = -18.0 + + red_phi = red_lf.phi(absolute_mag, z=z) + all_phi = all_lf.phi(absolute_mag, z=z) + + mask = (absolute_mag >= m_bright) & (absolute_mag <= m_faint) + + red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))[1] + blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0))[1] + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + ax.plot( + absolute_mag, + all_phi, + lw=3, + color=blue, + label="Denominator LF", + ) + ax.plot( + absolute_mag, + red_phi, + lw=3, + color=red, + label="Numerator LF", + ) + + ax.fill_between( + absolute_mag[mask], + 0.0, + all_phi[mask], + color=blue, + alpha=0.18, + linewidth=0.0, + ) + ax.fill_between( + absolute_mag[mask], + 0.0, + red_phi[mask], + color=red, + alpha=0.35, + linewidth=0.0, + ) + + fraction = red_lf.fractions.red_fraction( + z, + all_lf, + m_bright=m_bright, + m_faint=m_faint, + n_m=800, + ) + + ax.set_xlabel(r"Absolute magnitude $M$", fontsize=LABEL_SIZE) + ax.set_ylabel(r"$\phi(M, z)$", fontsize=LABEL_SIZE) + ax.set_title(rf"Fraction as a ratio of LF integrals, $f={fraction:.2f}$", fontsize=TITLE_SIZE) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + ax.set_yscale("log") + ax.invert_xaxis() + ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() + + +Callable denominator luminosity function +---------------------------------------- + +The denominator can also be any callable with signature +``denominator_lf(absolute_mag, redshift)``. This is useful when the total +luminosity function is produced by a custom model, interpolation, or simulation +output. + +This makes the fraction API flexible: the numerator can be an LFKit +``LuminosityFunction`` while the denominator can come from external data or a +user defined model. The only requirement is that the callable returns the +denominator luminosity function evaluated at the requested absolute magnitudes +and 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 + LEGEND_SIZE = 15 + + red_lf = LuminosityFunction.saunders( + phi_star=6.0e-4, + m_star=-20.4, + alpha=-0.9, + sigma=0.6, + ) + + def all_lf(absolute_mag, redshift): + return 2.0 * red_lf.phi(absolute_mag, z=redshift) + + redshift = np.linspace(0.05, 1.4, 120) + + red_fraction = np.array( + [ + red_lf.fractions.fraction( + z, + all_lf, + m_bright=-24.0, + m_faint=-18.0, + n_m=600, + ) + for z in redshift + ] + ) + + red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))[1] + + fig, ax = plt.subplots(figsize=(7.0, 5.0)) + ax.plot( + redshift, + red_fraction, + lw=3, + color=red, + label="Callable denominator", + ) + + ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE) + ax.set_ylabel("LF fraction", fontsize=LABEL_SIZE) + ax.set_title("Fraction with a callable denominator LF", fontsize=TITLE_SIZE) + ax.tick_params(axis="both", labelsize=TICK_SIZE) + ax.set_ylim(0.0, 1.0) + ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best") + plt.tight_layout() + + +.. note:: + + Fractions are computed from integrated luminosity functions. They are + therefore sensitive to the absolute magnitude limits, the LF model choice, + and the redshift dependence of both the numerator and denominator models. + The denominator luminosity function should represent the full population + corresponding to the numerator sample. If this is not true, the returned + value is still a ratio of integrals, but it should not be interpreted as a + physical population fraction. + + The red and blue examples on this page are intended as simple cosmology + motivated examples. They should not be interpreted as a complete physical + classification of galaxy morphology or evolution. \ No newline at end of file diff --git a/docs/examples/index.rst b/docs/examples/index.rst index ccc55683..516138df 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -23,6 +23,7 @@ redshift-density trends, and magnitude or luminosity conversions. magnitude_integrals magnitudes_and_luminosities redshift_density + fractions catalog_completeness kcorrect_examples poggianti_examples