From 5e91495eea30ba792566dd1ca70752c4ddabfc18 Mon Sep 17 00:00:00 2001 From: "niko.sarcevic" Date: Mon, 15 Jun 2026 01:15:04 -0400 Subject: [PATCH 1/4] Generalize conditional LF conditioning inputs --- .../api/conditional_luminosity_function.py | 54 ++++++---- .../conditional_integrals.py | 99 +++++++++++-------- .../conditional_models.py | 46 ++++++--- 3 files changed, 129 insertions(+), 70 deletions(-) diff --git a/src/lfkit/api/conditional_luminosity_function.py b/src/lfkit/api/conditional_luminosity_function.py index 8a0a9776..c6cbb855 100644 --- a/src/lfkit/api/conditional_luminosity_function.py +++ b/src/lfkit/api/conditional_luminosity_function.py @@ -21,10 +21,10 @@ class ConditionalLuminosityFunction(LuminosityFunction): """User-facing wrapper for conditional luminosity function models. - A conditional luminosity function evaluates ``Phi(M | x)``, where ``M`` is - absolute magnitude and ``x`` is an external conditioning variable such as - redshift, halo mass, environment, richness, stellar mass, or another - model-specific quantity. + A conditional luminosity function evaluates ``Phi(M | x_1, x_2, ...)``, + where ``M`` is absolute magnitude and the ``x_i`` are external conditioning + variables such as redshift, halo mass, environment, richness, stellar mass, + or other model-specific quantities. Instances can be created either with the generic constructor or with automatically generated model constructors. @@ -33,34 +33,39 @@ class ConditionalLuminosityFunction(LuminosityFunction): def phi( self, absolute_mag: FloatInput, - condition: FloatInput | None = None, + *conditions: FloatInput, ) -> FloatArray: """Evaluate the conditional luminosity function. Args: absolute_mag: Absolute magnitude value or array. - condition: Conditioning variable value or array. The meaning of this - variable depends on the selected conditional luminosity function model. + *conditions: One or more conditioning variable values or arrays. The + meaning of each variable depends on the selected conditional + luminosity function model. Returns: Conditional luminosity function evaluated at ``absolute_mag`` and - ``condition``. + the supplied conditioning variables. Raises: - ValueError: If ``condition`` is not provided or the model is not registered - as a conditional luminosity function. + ValueError: If no conditioning variables are provided, or if the + model is not registered as a conditional luminosity function. """ model_spec = get_conditional_lf_model(self.model) - if condition is None: + if not conditions: raise ValueError( - f"condition is required for conditional luminosity function " - f"model '{self.model}'." + f"At least one conditioning variable is required for conditional " + f"luminosity function model '{self.model}'." ) + condition_arrays = tuple( + np.asarray(condition_value, dtype=float) for condition_value in conditions + ) + return model_spec.function( np.asarray(absolute_mag, dtype=float), - np.asarray(condition, dtype=float), + *condition_arrays, **self.parameters_dict, ) @@ -125,7 +130,8 @@ def constructor( Examples: >>> clf = ConditionalLuminosityFunction.{model_name}(...) - >>> phi = clf.phi(absolute_mag=-20.0, condition=0.5) + >>> phi = clf.phi(-20.0, 0.5) + >>> phi = clf.phi(-20.0, halo_mass, redshift) """ return constructor @@ -138,8 +144,8 @@ def _parameters_from_signature( ) -> dict[str, Any]: """Build stored parameters from a function signature and user values. - Independent variables such as ``absolute_mag`` and ``condition`` are not stored - as model parameters. They are supplied later when calling + Independent variables such as ``absolute_mag`` and conditioning variables are + not stored as model parameters. They are supplied later when calling :meth:`ConditionalLuminosityFunction.phi`. Args: @@ -156,8 +162,20 @@ def _parameters_from_signature( """ payload: dict[str, Any] = {} + independent_names = { + "absolute_mag", + "condition", + "conditions", + "z", + "redshift", + "x", + "halo_mass", + "environment", + "galaxy_type", + } + for name, parameter in signature.parameters.items(): - if name in {"absolute_mag", "condition", "z", "redshift", "x"}: + if name in independent_names: continue if parameter.kind in { diff --git a/src/lfkit/luminosity_functions/conditional_integrals.py b/src/lfkit/luminosity_functions/conditional_integrals.py index f1d9eb8c..2d367d22 100644 --- a/src/lfkit/luminosity_functions/conditional_integrals.py +++ b/src/lfkit/luminosity_functions/conditional_integrals.py @@ -1,12 +1,12 @@ """Conditional luminosity function integration utilities. This module provides numerical helpers for conditional luminosity functions of -the form ``Phi(M | x)``, where ``M`` is absolute magnitude and ``x`` is an -external conditioning variable. +the form ``Phi(M | x_1, x_2, ...)``, where ``M`` is absolute magnitude and the +``x_i`` are external conditioning variables. -The conditioning variable is intentionally generic. It may represent halo mass, -environment, galaxy type, richness, stellar mass, or any other quantity. This -module does not implement HOD or halo-model machinery. +The conditioning variables are intentionally generic. They may represent halo +mass, redshift, environment, galaxy type, richness, stellar mass, or any other +quantities. This module does not implement HOD or halo-model machinery. The goal is to support conditional luminosity function evaluation and integration while keeping halo model calculations outside LFKit. @@ -15,6 +15,7 @@ from __future__ import annotations from collections.abc import Callable +from typing import Any import numpy as np @@ -28,31 +29,46 @@ ] +def _validate_conditions(conditions: tuple[FloatInput, ...]) -> tuple[FloatArray, ...]: + """Return validated conditioning-variable arrays.""" + if not conditions: + raise ValueError("At least one conditioning variable is required.") + + return tuple( + validate_array(condition, name=f"condition_{i}") + for i, condition in enumerate(conditions) + ) + + def evaluate_conditional_luminosity_function( absolute_mag: FloatInput, - condition: FloatInput, - conditional_lf: Callable[[FloatArray, FloatArray], FloatArray], + *conditions: FloatInput, + conditional_lf: Callable[..., Any], ) -> FloatArray: """Evaluate a conditional luminosity function. Args: absolute_mag: Absolute magnitude values. - condition: Values of the conditioning variable. - conditional_lf: Callable returning ``Phi(M | x)`` for absolute - magnitude ``M`` and condition ``x``. + *conditions: Values of one or more conditioning variables. + conditional_lf: Callable returning ``Phi(M | x_1, x_2, ...)`` for + absolute magnitude ``M`` and the supplied conditioning variables. Returns: Conditional luminosity function values evaluated at the requested absolute magnitudes and conditioning values. Raises: - ValueError: If the inputs contain non-finite values, or if the evaluated - conditional luminosity function contains non-finite or negative values. + ValueError: If no conditioning variables are supplied, if the inputs + contain non-finite values, or if the evaluated conditional luminosity + function contains non-finite or negative values. """ absolute_mag_arr = validate_array(absolute_mag, name="absolute_mag") - condition_arr = validate_array(condition, name="condition") + condition_arrays = _validate_conditions(conditions) - phi = np.asarray(conditional_lf(absolute_mag_arr, condition_arr), dtype=float) + phi = np.asarray( + conditional_lf(absolute_mag_arr, *condition_arrays), + dtype=float, + ) if not np.all(np.isfinite(phi)): raise ValueError("conditional_lf returned NaN or infinite values.") @@ -67,32 +83,32 @@ def evaluate_conditional_luminosity_function( def integrate_conditional_luminosity_function( absolute_mag: FloatInput, - condition: FloatInput, - conditional_lf: Callable[[FloatArray, FloatArray], FloatArray], - *, + *conditions: FloatInput, + conditional_lf: Callable[..., Any], axis: int = -1, ) -> FloatArray: """Integrate a conditional luminosity function over absolute magnitude. Args: absolute_mag: Absolute magnitude grid. - condition: Values of the conditioning variable. - conditional_lf: Callable returning ``Phi(M | x)`` for absolute - magnitude ``M`` and condition ``x``. + *conditions: Values of one or more conditioning variables. + conditional_lf: Callable returning ``Phi(M | x_1, x_2, ...)`` for + absolute magnitude ``M`` and the supplied conditioning variables. axis: Axis corresponding to the absolute magnitude grid. Returns: Conditional luminosity function integrated over absolute magnitude. Raises: - ValueError: If the inputs contain non-finite values, or if the evaluated - conditional luminosity function contains non-finite or negative values. + ValueError: If no conditioning variables are supplied, if the inputs + contain non-finite values, or if the evaluated conditional luminosity + function contains non-finite or negative values. """ absolute_mag_arr = validate_array(absolute_mag, name="absolute_mag") phi = evaluate_conditional_luminosity_function( - absolute_mag=absolute_mag_arr, - condition=condition, + absolute_mag_arr, + *conditions, conditional_lf=conditional_lf, ) @@ -104,21 +120,20 @@ def integrate_conditional_luminosity_function( def integrate_weighted_conditional_luminosity_function( absolute_mag: FloatInput, - condition: FloatInput, - conditional_lf: Callable[[FloatArray, FloatArray], FloatArray], - weight: Callable[[FloatArray, FloatArray], FloatArray], - *, + *conditions: FloatInput, + conditional_lf: Callable[..., Any], + weight: Callable[..., Any], axis: int = -1, ) -> FloatArray: """Integrate a weighted conditional luminosity function. Args: absolute_mag: Absolute magnitude grid. - condition: Values of the conditioning variable. - conditional_lf: Callable returning ``Phi(M | x)`` for absolute - magnitude ``M`` and condition ``x``. - weight: Callable returning weights ``w(M, x)`` for absolute magnitude - ``M`` and condition ``x``. + *conditions: Values of one or more conditioning variables. + conditional_lf: Callable returning ``Phi(M | x_1, x_2, ...)`` for + absolute magnitude ``M`` and the supplied conditioning variables. + weight: Callable returning weights ``w(M, x_1, x_2, ...)`` for absolute + magnitude ``M`` and the supplied conditioning variables. axis: Axis corresponding to the absolute magnitude grid. Returns: @@ -126,20 +141,24 @@ def integrate_weighted_conditional_luminosity_function( magnitude. Raises: - ValueError: If the inputs contain non-finite values, if the evaluated - conditional luminosity function contains non-finite or negative values, - or if the weights contain non-finite values. + ValueError: If no conditioning variables are supplied, if the inputs + contain non-finite values, if the evaluated conditional luminosity + function contains non-finite or negative values, or if the weights + contain non-finite values. """ absolute_mag_arr = validate_array(absolute_mag, name="absolute_mag") - condition_arr = validate_array(condition, name="condition") + condition_arrays = _validate_conditions(conditions) phi = evaluate_conditional_luminosity_function( - absolute_mag=absolute_mag_arr, - condition=condition_arr, + absolute_mag_arr, + *condition_arrays, conditional_lf=conditional_lf, ) - weight_arr = np.asarray(weight(absolute_mag_arr, condition_arr), dtype=float) + weight_arr = np.asarray( + weight(absolute_mag_arr, *condition_arrays), + dtype=float, + ) if not np.all(np.isfinite(weight_arr)): raise ValueError("weight returned NaN or infinite values.") diff --git a/src/lfkit/luminosity_functions/conditional_models.py b/src/lfkit/luminosity_functions/conditional_models.py index 093ddfbd..362f8aff 100644 --- a/src/lfkit/luminosity_functions/conditional_models.py +++ b/src/lfkit/luminosity_functions/conditional_models.py @@ -3,10 +3,10 @@ This module provides generic conditional wrappers around LFKit luminosity function models. -A conditional luminosity function has the form ``Phi(M | x)``, where ``M`` is -absolute magnitude and ``x`` is an external conditioning variable. Callable -model parameters are evaluated at ``x`` before the wrapped luminosity function -is evaluated. +A conditional luminosity function has the form ``Phi(M | x_1, x_2, ...)``, +where ``M`` is absolute magnitude and the ``x_i`` are external conditioning +variables. Callable model parameters are evaluated at the supplied conditioning +variables before the wrapped luminosity function is evaluated. """ from __future__ import annotations @@ -27,42 +27,64 @@ def conditionalize_lf_model( ) -> Callable[..., FloatArray]: """Return a conditional version of a luminosity function model. - Callable keyword arguments are interpreted as parameter models and evaluated as - functions of ``condition``. Non-callable keyword arguments are passed through - unchanged. + Callable keyword arguments are interpreted as parameter models and evaluated + as functions of the supplied conditioning variables. Non-callable keyword + arguments are passed through unchanged. Args: lf_model: Luminosity function model to wrap. Returns: Conditional luminosity function model with signature - ``conditional_model(absolute_mag, condition, **kwargs)``. + ``conditional_model(absolute_mag, *conditions, **kwargs)``. """ @wraps(lf_model) def conditional_model( absolute_mag: FloatInput, - condition: FloatInput, + *conditions: FloatInput, **kwargs: Any, ) -> FloatArray: - condition_arr = validate_array(condition, name="condition") + absolute_mag_arr = validate_array(absolute_mag, name="absolute_mag") + condition_arrays = _validate_conditions(conditions) evaluated_kwargs: dict[str, Any] = {} for name, value in kwargs.items(): if callable(value): evaluated_kwargs[name] = validate_array( - value(condition_arr), + value(*condition_arrays), name=name, ) else: evaluated_kwargs[name] = value - phi = lf_model(absolute_mag, **evaluated_kwargs) + phi = lf_model(absolute_mag_arr, **evaluated_kwargs) return _validate_lf_output(phi, name=lf_model.__name__) return conditional_model +def _validate_conditions(conditions: tuple[FloatInput, ...]) -> tuple[FloatArray, ...]: + """Return validated conditioning variable arrays. + + Args: + conditions: Conditioning variable values. + + Returns: + Validated conditioning variable arrays. + + Raises: + ValueError: If no conditioning variables are supplied. + """ + if not conditions: + raise ValueError("At least one conditioning variable is required.") + + return tuple( + validate_array(condition, name=f"condition_{i}") + for i, condition in enumerate(conditions) + ) + + def _conditional_model_name(name: str) -> str: """Return the generated conditional wrapper name for a luminosity function model. From 8b6a8928fd23badf927c8c66ba68a064898807a6 Mon Sep 17 00:00:00 2001 From: "niko.sarcevic" Date: Mon, 15 Jun 2026 01:15:11 -0400 Subject: [PATCH 2/4] Expand conditional LF test coverage --- ...est_api_conditional_luminosity_function.py | 201 ++++++++++++------ tests/test_lumfuncs_conditional_integrals.py | 84 ++++---- tests/test_lumfuncs_conditional_models.py | 87 ++++---- 3 files changed, 224 insertions(+), 148 deletions(-) diff --git a/tests/test_api_conditional_luminosity_function.py b/tests/test_api_conditional_luminosity_function.py index 9311fd49..9f7c7cdb 100644 --- a/tests/test_api_conditional_luminosity_function.py +++ b/tests/test_api_conditional_luminosity_function.py @@ -10,7 +10,7 @@ from lfkit.luminosity_functions.registry import CONDITIONAL_LF_MODELS -def test_conditional_schechter_constructor_stores_parameters(): +def test_conditional_schechter_constructor_stores_parameters() -> None: """Tests that the conditional Schechter constructor stores parameters.""" lf = ConditionalLuminosityFunction.schechter( phi_star=1.0e-3, @@ -29,7 +29,7 @@ def test_conditional_schechter_constructor_stores_parameters(): assert lf.meta == {"source": "test"} -def test_conditional_double_schechter_constructor_stores_parameters(): +def test_conditional_double_schechter_constructor_stores_parameters() -> None: """Tests that the conditional double Schechter constructor stores parameters.""" lf = ConditionalLuminosityFunction.double_schechter( phi_star=1.0e-3, @@ -52,7 +52,7 @@ def test_conditional_double_schechter_constructor_stores_parameters(): assert lf.meta == {"source": "test"} -def test_lognormal_constructor_stores_parameters(): +def test_lognormal_constructor_stores_parameters() -> None: """Tests that the conditional lognormal constructor stores parameters.""" lf = ConditionalLuminosityFunction.lognormal( mean_absolute_mag=-20.5, @@ -71,7 +71,7 @@ def test_lognormal_constructor_stores_parameters(): assert lf.meta == {"source": "test"} -def test_lognormal_constructor_uses_default_amplitude(): +def test_lognormal_constructor_uses_default_amplitude() -> None: """Tests that the conditional lognormal constructor uses default amplitude.""" lf = ConditionalLuminosityFunction.lognormal( mean_absolute_mag=-20.5, @@ -89,11 +89,11 @@ def test_lognormal_constructor_uses_default_amplitude(): def test_luminosity_cutoff_modifier_is_not_registered() -> None: - """Tests that luminosity cutoff modifiers are not registered as conditional LF models.""" + """Tests that luminosity cutoff modifiers are not registered as conditional models.""" assert "apply_luminosity_cutoff" not in CONDITIONAL_LF_MODELS -def test_two_component_constructor_stores_parameters(): +def test_two_component_constructor_stores_parameters() -> None: """Tests that the conditional two-component constructor stores parameters.""" lf = ConditionalLuminosityFunction.two_component( lognormal_mean_absolute_mag=-21.0, @@ -120,7 +120,7 @@ def test_two_component_constructor_stores_parameters(): assert lf.meta == {"source": "test"} -def test_two_component_constructor_uses_default_optional_parameters(): +def test_two_component_constructor_uses_default_optional_parameters() -> None: """Tests that the two-component constructor uses optional defaults.""" lf = ConditionalLuminosityFunction.two_component( lognormal_mean_absolute_mag=-21.0, @@ -143,7 +143,7 @@ def test_two_component_constructor_uses_default_optional_parameters(): assert lf.meta == {} -def test_available_models_includes_expected_models(): +def test_available_models_includes_expected_models() -> None: """Tests that available models includes expected conditional models.""" models = ConditionalLuminosityFunction.available_models() @@ -154,60 +154,6 @@ def test_available_models_includes_expected_models(): assert models == sorted(models) -def test_phi_requires_condition(): - """Tests that conditional phi requires a condition.""" - lf = ConditionalLuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.1, - ) - - with pytest.raises(ValueError, match="condition is required"): - lf.phi(-20.0) - - -def test_phi_accepts_scalar_condition(): - """Tests that conditional phi accepts scalar conditions.""" - lf = ConditionalLuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.1, - ) - - phi = lf.phi(-20.0, condition=0.5) - - assert np.asarray(phi).shape == () - assert np.isfinite(phi) - - -def test_phi_accepts_array_condition(): - """Tests that conditional phi accepts array conditions.""" - lf = ConditionalLuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.1, - ) - - absolute_mag = np.array([-22.0, -21.0, -20.0]) - condition = np.array([0.1, 0.5, 1.0]) - - phi = lf.phi(absolute_mag, condition=condition) - - assert phi.shape == absolute_mag.shape - assert np.all(np.isfinite(phi)) - - -def test_constructor_rejects_unexpected_parameter(): - """Tests that constructors reject unexpected parameters.""" - with pytest.raises(TypeError, match="Unexpected parameter"): - ConditionalLuminosityFunction.schechter( - phi_star=1.0e-3, - m_star=-20.5, - alpha=-1.1, - bad_parameter=123, - ) - - @pytest.mark.parametrize( "name", [ @@ -228,3 +174,134 @@ def test_constructor_rejects_unexpected_parameter(): def test_available_models_includes_extended_model_families(name: str) -> None: """Tests that extended model families are available as conditional models.""" assert name in ConditionalLuminosityFunction.available_models() + + +def test_constructor_rejects_unexpected_parameter() -> None: + """Tests that constructors reject unexpected parameters.""" + with pytest.raises(TypeError, match="Unexpected parameter"): + ConditionalLuminosityFunction.schechter( + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, + bad_parameter=123, + ) + + +CONDITIONAL_MODEL_TEST_PARAMETERS = { + "schechter": { + "phi_star": 1.0e-3, + "m_star": -20.5, + "alpha": -1.1, + }, + "double_schechter": { + "phi_star": 1.0e-3, + "m_star": -20.5, + "alpha": -1.1, + "beta": -1.5, + "m_transition": -19.5, + }, + "lognormal": { + "mean_absolute_mag": -20.5, + "sigma_log_luminosity": 0.2, + "amplitude": 1.0, + }, + "two_component": { + "lognormal_mean_absolute_mag": -21.0, + "lognormal_sigma_log_luminosity": 0.2, + "lognormal_amplitude": 1.0, + "modified_phi_star": 1.0e-3, + "modified_alpha": -1.1, + "modified_m_star": -20.0, + "modified_luminosity_fraction": 0.562, + }, +} + + +@pytest.fixture(params=CONDITIONAL_MODEL_TEST_PARAMETERS) +def conditional_lf(request: pytest.FixtureRequest) -> LuminosityFunction: + """Return a conditional luminosity function for API tests.""" + name = str(request.param) + constructor = getattr(ConditionalLuminosityFunction, name) + return constructor(**CONDITIONAL_MODEL_TEST_PARAMETERS[name]) + + +def test_phi_requires_condition(conditional_lf: LuminosityFunction) -> None: + """Tests that conditional phi requires a conditioning variable.""" + with pytest.raises( + ValueError, + match="At least one conditioning variable is required", + ): + conditional_lf.phi(-20.0) + + +def test_phi_accepts_scalar_condition(conditional_lf: LuminosityFunction) -> None: + """Tests that conditional phi accepts scalar conditioning variables.""" + phi = conditional_lf.phi(-20.0, 0.5) + + assert np.asarray(phi).shape == () + assert np.isfinite(phi) + + +def test_phi_accepts_array_condition(conditional_lf: LuminosityFunction) -> None: + """Tests that conditional phi accepts array conditioning variables.""" + absolute_mag = np.array([-22.0, -21.0, -20.0]) + condition = np.array([0.1, 0.5, 1.0]) + + phi = conditional_lf.phi(absolute_mag, condition) + + assert phi.shape == absolute_mag.shape + assert np.all(np.isfinite(phi)) + + +def test_phi_accepts_two_conditions( + conditional_lf: LuminosityFunction, +) -> None: + """Tests that conditional phi accepts two conditioning variables.""" + absolute_mag = np.array([-22.0, -21.0, -20.0]) + first_condition = np.array([0.1, 0.5, 1.0]) + second_condition = np.array([1.0, 2.0, 3.0]) + + phi = conditional_lf.phi( + absolute_mag, + first_condition, + second_condition, + ) + + assert phi.shape == absolute_mag.shape + assert np.all(np.isfinite(phi)) + + +def test_phi_accepts_three_conditions( + conditional_lf: LuminosityFunction, +) -> None: + """Tests that conditional phi accepts three conditioning variables.""" + absolute_mag = np.array([-22.0, -21.0, -20.0]) + first_condition = np.array([0.1, 0.5, 1.0]) + second_condition = np.array([1.0, 2.0, 3.0]) + third_condition = np.array([10.0, 20.0, 30.0]) + + phi = conditional_lf.phi( + absolute_mag, + first_condition, + second_condition, + third_condition, + ) + + assert phi.shape == absolute_mag.shape + assert np.all(np.isfinite(phi)) + + +def test_phi_rejects_nonfinite_condition( + conditional_lf: LuminosityFunction, +) -> None: + """Tests that conditional phi rejects non-finite conditioning variables.""" + with pytest.raises(ValueError, match="NaN or infinite"): + conditional_lf.phi(-20.0, np.nan) + + +def test_phi_rejects_nonfinite_later_condition( + conditional_lf: LuminosityFunction, +) -> None: + """Tests that conditional phi rejects non-finite later conditioning variables.""" + with pytest.raises(ValueError, match="NaN or infinite"): + conditional_lf.phi(-20.0, 0.5, np.inf) diff --git a/tests/test_lumfuncs_conditional_integrals.py b/tests/test_lumfuncs_conditional_integrals.py index 48442114..86ea02b7 100644 --- a/tests/test_lumfuncs_conditional_integrals.py +++ b/tests/test_lumfuncs_conditional_integrals.py @@ -17,8 +17,8 @@ def conditional_lf(absolute_mag, condition): return condition * np.exp(-0.1 * absolute_mag) result = evaluate_conditional_luminosity_function( - absolute_mag=-20.0, - condition=2.0, + -20.0, + 2.0, conditional_lf=conditional_lf, ) @@ -40,8 +40,8 @@ def conditional_lf(absolute_mag, condition): return condition * (absolute_mag + 23.0) result = evaluate_conditional_luminosity_function( - absolute_mag=absolute_mag, - condition=condition, + absolute_mag, + condition, conditional_lf=conditional_lf, ) @@ -61,8 +61,8 @@ def conditional_lf(absolute_mag, condition): return condition * (absolute_mag + 23.0) result = evaluate_conditional_luminosity_function( - absolute_mag=absolute_mag, - condition=condition, + absolute_mag, + condition, conditional_lf=conditional_lf, ) @@ -85,8 +85,8 @@ def conditional_lf(absolute_mag, condition): with pytest.raises(ValueError, match="absolute_mag contains NaN or infinite values."): evaluate_conditional_luminosity_function( - absolute_mag=[-22.0, np.nan, -20.0], - condition=1.0, + [-22.0, np.nan, -20.0], + 1.0, conditional_lf=conditional_lf, ) @@ -97,10 +97,10 @@ def test_evaluate_conditional_luminosity_function_rejects_non_finite_condition() def conditional_lf(absolute_mag, condition): return np.ones_like(absolute_mag) - with pytest.raises(ValueError, match="condition contains NaN or infinite values."): + with pytest.raises(ValueError, match="condition_0 contains NaN or infinite values."): evaluate_conditional_luminosity_function( - absolute_mag=[-22.0, -21.0, -20.0], - condition=np.inf, + [-22.0, -21.0, -20.0], + np.inf, conditional_lf=conditional_lf, ) @@ -116,8 +116,8 @@ def conditional_lf(absolute_mag, condition): match="conditional_lf returned NaN or infinite values.", ): evaluate_conditional_luminosity_function( - absolute_mag=[-22.0, -21.0, -20.0], - condition=1.0, + [-22.0, -21.0, -20.0], + 1.0, conditional_lf=conditional_lf, ) @@ -133,8 +133,8 @@ def conditional_lf(absolute_mag, condition): match="conditional_lf returned negative values, which are not allowed.", ): evaluate_conditional_luminosity_function( - absolute_mag=[-22.0, -21.0, -20.0], - condition=1.0, + [-22.0, -21.0, -20.0], + 1.0, conditional_lf=conditional_lf, ) @@ -149,8 +149,8 @@ def conditional_lf(absolute_mag, condition): return condition * (absolute_mag + 23.0) result = integrate_conditional_luminosity_function( - absolute_mag=absolute_mag, - condition=condition, + absolute_mag, + condition, conditional_lf=conditional_lf, ) @@ -179,8 +179,8 @@ def conditional_lf(absolute_mag, condition): return (absolute_mag[:, None] + 23.0) * condition result = integrate_conditional_luminosity_function( - absolute_mag=absolute_mag, - condition=condition, + absolute_mag, + condition, conditional_lf=conditional_lf, axis=0, ) @@ -209,8 +209,8 @@ def conditional_lf(absolute_mag, condition): match="conditional_lf returned negative values, which are not allowed.", ): integrate_conditional_luminosity_function( - absolute_mag=[-22.0, -21.0, -20.0], - condition=1.0, + [-22.0, -21.0, -20.0], + 1.0, conditional_lf=conditional_lf, ) @@ -228,8 +228,8 @@ def weight(absolute_mag, condition): return absolute_mag + 24.0 result = integrate_weighted_conditional_luminosity_function( - absolute_mag=absolute_mag, - condition=condition, + absolute_mag, + condition, conditional_lf=conditional_lf, weight=weight, ) @@ -260,8 +260,8 @@ def weight(absolute_mag, condition): return absolute_mag[:, None] + 24.0 result = integrate_weighted_conditional_luminosity_function( - absolute_mag=absolute_mag, - condition=condition, + absolute_mag, + condition, conditional_lf=conditional_lf, weight=weight, axis=0, @@ -298,8 +298,8 @@ def weight(absolute_mag, condition): with pytest.raises(ValueError, match="weight returned NaN or infinite values."): integrate_weighted_conditional_luminosity_function( - absolute_mag=[-22.0, -21.0, -20.0], - condition=1.0, + [-22.0, -21.0, -20.0], + 1.0, conditional_lf=conditional_lf, weight=weight, ) @@ -317,8 +317,8 @@ def weight(absolute_mag, condition): return np.array([1.0, -2.0, 3.0]) result = integrate_weighted_conditional_luminosity_function( - absolute_mag=absolute_mag, - condition=1.0, + absolute_mag, + 1.0, conditional_lf=conditional_lf, weight=weight, ) @@ -343,8 +343,8 @@ def weight(absolute_mag, condition): match="conditional_lf returned negative values, which are not allowed.", ): integrate_weighted_conditional_luminosity_function( - absolute_mag=[-22.0, -21.0, -20.0], - condition=1.0, + [-22.0, -21.0, -20.0], + 1.0, conditional_lf=conditional_lf, weight=weight, ) @@ -357,8 +357,8 @@ def conditional_lf(absolute_mag, condition): return np.zeros_like(absolute_mag, dtype=float) result = evaluate_conditional_luminosity_function( - absolute_mag=[-22.0, -21.0, -20.0], - condition=1.0, + [-22.0, -21.0, -20.0], + 1.0, conditional_lf=conditional_lf, ) @@ -371,10 +371,10 @@ def test_evaluate_conditional_lf_rejects_non_finite_broadcasted_condition() -> N def conditional_lf(absolute_mag, condition): return np.ones((2, 3)) - with pytest.raises(ValueError, match="condition contains NaN or infinite values."): + with pytest.raises(ValueError, match="condition_0 contains NaN or infinite values."): evaluate_conditional_luminosity_function( - absolute_mag=np.array([-22.0, -21.0, -20.0]), - condition=np.array([[1.0], [np.nan]]), + np.array([-22.0, -21.0, -20.0]), + np.array([[1.0], [np.nan]]), conditional_lf=conditional_lf, ) @@ -388,8 +388,8 @@ def conditional_lf(absolute_mag, condition): return 2.0 * np.ones_like(absolute_mag) result = integrate_conditional_luminosity_function( - absolute_mag=absolute_mag, - condition=1.0, + absolute_mag, + 1.0, conditional_lf=conditional_lf, ) @@ -408,8 +408,8 @@ def weight(absolute_mag, condition): return 2.0 result = integrate_weighted_conditional_luminosity_function( - absolute_mag=absolute_mag, - condition=1.0, + absolute_mag, + 1.0, conditional_lf=conditional_lf, weight=weight, ) @@ -430,8 +430,8 @@ def weight(absolute_mag, condition): with pytest.raises(ValueError, match="weight returned NaN or infinite values."): integrate_weighted_conditional_luminosity_function( - absolute_mag=[-22.0, -21.0, -20.0], - condition=1.0, + [-22.0, -21.0, -20.0], + 1.0, conditional_lf=conditional_lf, weight=weight, ) diff --git a/tests/test_lumfuncs_conditional_models.py b/tests/test_lumfuncs_conditional_models.py index 39150202..55aab854 100644 --- a/tests/test_lumfuncs_conditional_models.py +++ b/tests/test_lumfuncs_conditional_models.py @@ -3,7 +3,6 @@ import numpy as np import pytest - from lfkit.luminosity_functions.conditional_models import ( __all__, conditionalize_lf_model, @@ -26,8 +25,8 @@ def test_conditional_schechter_matches_schechter_for_scalar_parameters() -> None condition = np.array([0.1, 0.2, 0.3]) result = conditional_schechter( - absolute_mag=absolute_mag, - condition=condition, + absolute_mag, + condition, phi_star=1.0e-3, m_star=-21.0, alpha=-1.1, @@ -51,8 +50,8 @@ def test_conditional_schechter_accepts_callable_parameters() -> None: condition = np.array([0.0, 1.0, 2.0]) result = conditional_schechter( - absolute_mag=absolute_mag, - condition=condition, + absolute_mag, + condition, phi_star=lambda x: 1.0e-3 * (1.0 + x), m_star=lambda x: -21.0 - 0.1 * x, alpha=lambda x: -1.0 - 0.05 * x, @@ -72,10 +71,10 @@ def test_conditional_schechter_accepts_callable_parameters() -> None: def test_conditional_schechter_rejects_non_finite_condition() -> None: """Tests that non-finite condition values are rejected.""" - with pytest.raises(ValueError, match="condition contains NaN or infinite values."): + with pytest.raises(ValueError, match="condition_0 contains NaN or infinite values."): conditional_schechter( - absolute_mag=[-22.0, -21.0, -20.0], - condition=[0.0, np.nan, 1.0], + [-22.0, -21.0, -20.0], + [0.0, np.nan, 1.0], phi_star=1.0e-3, m_star=-21.0, alpha=-1.1, @@ -87,8 +86,8 @@ def test_conditional_schechter_rejects_non_finite_callable_parameter() -> None: with pytest.raises(ValueError, match="phi_star contains NaN or infinite values."): conditional_schechter( - absolute_mag=[-22.0, -21.0, -20.0], - condition=[0.0, 1.0, 2.0], + [-22.0, -21.0, -20.0], + [0.0, 1.0, 2.0], phi_star=lambda x: np.array([1.0e-3, np.nan, 2.0e-3]), m_star=-21.0, alpha=-1.1, @@ -102,8 +101,8 @@ def test_conditional_double_schechter_matches_double_schechter() -> None: condition = np.array([0.0, 1.0, 2.0]) result = conditional_double_schechter( - absolute_mag=absolute_mag, - condition=condition, + absolute_mag, + condition, phi_star=1.0e-3, m_star=-21.0, alpha=-1.0, @@ -131,8 +130,8 @@ def test_conditional_double_schechter_accepts_callable_parameters() -> None: condition = np.array([0.0, 1.0, 2.0]) result = conditional_double_schechter( - absolute_mag=absolute_mag, - condition=condition, + absolute_mag, + condition, phi_star=lambda x: 1.0e-3 * (1.0 + x), m_star=lambda x: -21.0 - 0.1 * x, alpha=-1.0, @@ -163,8 +162,8 @@ def test_conditional_lognormal_lf_matches_expected_formula() -> None: amplitude = np.array([1.0, 2.0, 3.0]) result = conditional_lognormal_lf( - absolute_mag=absolute_mag, - condition=condition, + absolute_mag, + condition, mean_absolute_mag=mean_absolute_mag, sigma_log_luminosity=sigma_log_luminosity, amplitude=amplitude, @@ -189,8 +188,8 @@ def test_conditional_lognormal_lf_accepts_callable_parameters() -> None: condition = np.array([0.0, 1.0, 2.0]) result = conditional_lognormal_lf( - absolute_mag=absolute_mag, - condition=condition, + absolute_mag, + condition, mean_absolute_mag=lambda x: -21.0 - 0.1 * x, sigma_log_luminosity=lambda x: 0.2 + 0.01 * x, amplitude=lambda x: 1.0 + x, @@ -217,8 +216,8 @@ def test_conditional_lognormal_lf_rejects_zero_sigma() -> None: with pytest.raises(ValueError, match="sigma_log_luminosity must be positive."): conditional_lognormal_lf( - absolute_mag=[-22.0, -21.0, -20.0], - condition=[0.0, 1.0, 2.0], + [-22.0, -21.0, -20.0], + [0.0, 1.0, 2.0], mean_absolute_mag=-21.0, sigma_log_luminosity=0.0, amplitude=1.0, @@ -230,8 +229,8 @@ def test_conditional_lognormal_lf_rejects_negative_sigma() -> None: with pytest.raises(ValueError, match="sigma_log_luminosity must be positive."): conditional_lognormal_lf( - absolute_mag=[-22.0, -21.0, -20.0], - condition=[0.0, 1.0, 2.0], + [-22.0, -21.0, -20.0], + [0.0, 1.0, 2.0], mean_absolute_mag=-21.0, sigma_log_luminosity=-0.2, amplitude=1.0, @@ -243,8 +242,8 @@ def test_conditional_lognormal_lf_rejects_negative_amplitude() -> None: with pytest.raises(ValueError, match="amplitude must be non-negative."): conditional_lognormal_lf( - absolute_mag=[-22.0, -21.0, -20.0], - condition=[0.0, 1.0, 2.0], + [-22.0, -21.0, -20.0], + [0.0, 1.0, 2.0], mean_absolute_mag=-21.0, sigma_log_luminosity=0.2, amplitude=-1.0, @@ -258,8 +257,8 @@ def test_conditional_two_component_lf_equals_sum_with_explicit_modified_m_star() condition = np.array([0.0, 1.0, 2.0]) result = conditional_two_component_lf( - absolute_mag=absolute_mag, - condition=condition, + absolute_mag, + condition, lognormal_mean_absolute_mag=-21.0, lognormal_sigma_log_luminosity=0.2, modified_phi_star=1.0e-3, @@ -291,8 +290,8 @@ def test_conditional_two_component_lf_derives_modified_m_star() -> None: modified_luminosity_fraction = np.array([0.5, 0.6, 0.7]) result = conditional_two_component_lf( - absolute_mag=absolute_mag, - condition=condition, + absolute_mag, + condition, lognormal_mean_absolute_mag=lognormal_mean_absolute_mag, lognormal_sigma_log_luminosity=0.2, modified_phi_star=1.0e-3, @@ -325,8 +324,8 @@ def test_conditional_two_component_lf_rejects_zero_luminosity_fraction() -> None match="modified_luminosity_fraction must be positive.", ): conditional_two_component_lf( - absolute_mag=[-22.0, -21.0, -20.0], - condition=[0.0, 1.0, 2.0], + [-22.0, -21.0, -20.0], + [0.0, 1.0, 2.0], lognormal_mean_absolute_mag=-21.0, lognormal_sigma_log_luminosity=0.2, modified_phi_star=1.0e-3, @@ -345,8 +344,8 @@ def test_conditional_two_component_lf_rejects_negative_luminosity_fraction() -> match="modified_luminosity_fraction must be positive.", ): conditional_two_component_lf( - absolute_mag=[-22.0, -21.0, -20.0], - condition=[0.0, 1.0, 2.0], + [-22.0, -21.0, -20.0], + [0.0, 1.0, 2.0], lognormal_mean_absolute_mag=-21.0, lognormal_sigma_log_luminosity=0.2, modified_phi_star=1.0e-3, @@ -362,8 +361,8 @@ def test_conditional_two_component_lf_propagates_invalid_lognormal_component() - with pytest.raises(ValueError, match="sigma_log_luminosity must be positive."): conditional_two_component_lf( - absolute_mag=[-22.0, -21.0, -20.0], - condition=[0.0, 1.0, 2.0], + [-22.0, -21.0, -20.0], + [0.0, 1.0, 2.0], lognormal_mean_absolute_mag=-21.0, lognormal_sigma_log_luminosity=0.0, modified_phi_star=1.0e-3, @@ -378,8 +377,8 @@ def test_conditional_two_component_lf_propagates_invalid_modified_component() -> with pytest.raises(ValueError, match="phi_star must be non-negative."): conditional_two_component_lf( - absolute_mag=[-22.0, -21.0, -20.0], - condition=[0.0, 1.0, 2.0], + [-22.0, -21.0, -20.0], + [0.0, 1.0, 2.0], lognormal_mean_absolute_mag=-21.0, lognormal_sigma_log_luminosity=0.2, modified_phi_star=-1.0e-3, @@ -411,8 +410,8 @@ def toy_lf(absolute_mag, amplitude, offset): conditional_toy_lf = conditionalize_lf_model(toy_lf) result = conditional_toy_lf( - absolute_mag=absolute_mag, - condition=np.array([0.0, 1.0, 2.0]), + absolute_mag, + np.array([0.0, 1.0, 2.0]), amplitude=2.0, offset=3.0, ) @@ -434,8 +433,8 @@ def toy_lf(absolute_mag, amplitude): match="toy_lf returned negative values, which are not allowed.", ): conditional_toy_lf( - absolute_mag=[-22.0, -21.0, -20.0], - condition=[0.0, 1.0, 2.0], + [-22.0, -21.0, -20.0], + [0.0, 1.0, 2.0], amplitude=1.0, ) @@ -450,8 +449,8 @@ def toy_lf(absolute_mag, amplitude): with pytest.raises(ValueError, match="toy_lf contains NaN or infinite values."): conditional_toy_lf( - absolute_mag=[-22.0, -21.0, -20.0], - condition=[0.0, 1.0, 2.0], + [-22.0, -21.0, -20.0], + [0.0, 1.0, 2.0], amplitude=1.0, ) @@ -470,8 +469,8 @@ def test_conditional_schechter_accepts_scalar_condition_with_callable_parameter( """Tests callable parameter evaluation for scalar condition input.""" result = conditional_schechter( - absolute_mag=np.array([-22.0, -21.0, -20.0]), - condition=2.0, + np.array([-22.0, -21.0, -20.0]), + 2.0, phi_star=lambda x: 1.0e-3 * (1.0 + x), m_star=-21.0, alpha=-1.1, From ad165fade2b36f130ae0ca6f018dfcdf6a81e8ee Mon Sep 17 00:00:00 2001 From: "niko.sarcevic" Date: Mon, 15 Jun 2026 01:15:14 -0400 Subject: [PATCH 3/4] Update conditional LF documentation examples --- docs/examples/conditional_luminosity_function.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/examples/conditional_luminosity_function.rst b/docs/examples/conditional_luminosity_function.rst index 6f1e5181..d4ccec51 100644 --- a/docs/examples/conditional_luminosity_function.rst +++ b/docs/examples/conditional_luminosity_function.rst @@ -387,10 +387,12 @@ The model therefore changes both in amplitude and in shape. cmap_range=(0.0, 0.2), ) - lf = ConditionalLuminosityFunction.schechter( + lf = ConditionalLuminosityFunction.double_schechter( phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5, m_star=lambda z: -20.3 - 0.5 * (z - 0.1), alpha=lambda z: -1.15 - 0.10 * z, + beta=lambda z: -0.45 - 0.05 * z, + m_transition=lambda z: -19.0 - 0.3 * (z - 0.1), ) fig, ax = plt.subplots(figsize=(7.0, 5.0)) From bc7e79c97b308488b71b9bd5a7f2a8357391879d Mon Sep 17 00:00:00 2001 From: "niko.sarcevic" Date: Mon, 15 Jun 2026 01:16:39 -0400 Subject: [PATCH 4/4] improved schechter --- src/lfkit/luminosity_functions/models/schechter.py | 12 ++++++------ tests/test_lumfuncs_models_schechter.py | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/lfkit/luminosity_functions/models/schechter.py b/src/lfkit/luminosity_functions/models/schechter.py index 8d7ebd47..4b8dc151 100644 --- a/src/lfkit/luminosity_functions/models/schechter.py +++ b/src/lfkit/luminosity_functions/models/schechter.py @@ -213,8 +213,8 @@ def double_schechter( """ absolute_mag = validate_array(absolute_mag, name="absolute_mag") phi_star_arr = validate_array(phi_star, name="phi_star") - alpha = float(alpha) - beta = float(beta) + alpha_arr = validate_array(alpha, name="alpha") + beta_arr = validate_array(beta, name="beta") if np.any(phi_star_arr == 0): warnings.warn( @@ -225,10 +225,10 @@ def double_schechter( if np.any(phi_star_arr < 0): raise ValueError("phi_star must be non-negative.") - if not np.isfinite(alpha): + if np.any(~np.isfinite(alpha_arr)): raise ValueError("alpha must be finite.") - if not np.isfinite(beta): + if np.any(~np.isfinite(beta_arr)): raise ValueError("beta must be finite.") x = luminosity_ratio(absolute_mag, m_star) @@ -238,10 +238,10 @@ def double_schechter( x_t = np.clip(x_t, 1e-300, None) prefactor = 0.4 * np.log(10.0) * phi_star_arr - modifier = 1.0 + (x / x_t) ** beta + modifier = 1.0 + (x / x_t) ** beta_arr return np.asarray( - prefactor * x ** (alpha + 1.0) * np.exp(-x) * modifier, + prefactor * x ** (alpha_arr + 1.0) * np.exp(-x) * modifier, dtype=float, ) diff --git a/tests/test_lumfuncs_models_schechter.py b/tests/test_lumfuncs_models_schechter.py index f21deab1..dd752ff1 100644 --- a/tests/test_lumfuncs_models_schechter.py +++ b/tests/test_lumfuncs_models_schechter.py @@ -362,12 +362,12 @@ def test_double_schechter_rejects_negative_phi_star() -> None: def test_double_schechter_rejects_nonfinite_beta() -> None: """Tests that double_schechter rejects non-finite beta.""" - with pytest.raises(ValueError, match="beta must be finite"): + with pytest.raises(ValueError, match="beta contains NaN or infinite values"): double_schechter( - [-20.0], - phi_star=1e-3, - m_star=-20.0, - alpha=-1.0, + np.array([-20.0]), + phi_star=1.0e-3, + m_star=-20.5, + alpha=-1.1, beta=np.nan, m_transition=-18.0, )