LFKit logo Conditional luminosity functions#

This page shows how to evaluate conditional luminosity functions with LFKit’s public API. A conditional luminosity function describes the abundance of galaxies as a function of absolute magnitude while allowing the model to depend on another variable.

A conditional luminosity function has the form \(\Phi(M \mid x)\), where \(M\) is absolute magnitude and \(x\) is the conditioning variable. The quantity

\[\Phi(M \mid x)\,dM\]

is the number density of galaxies with absolute magnitudes between \(M\) and \(M+dM\) at fixed \(x\). The conditioning variable is generic: it can represent redshift, halo mass, environment, galaxy type, richness, stellar mass, or another quantity.

LFKit exposes conditional luminosity functions through lfkit.ConditionalLuminosityFunction. Each constructor returns a lfkit.LuminosityFunction object, so the resulting model can be evaluated with lf.phi and integrated with the usual lf.integrals namespace.

The examples below use redshift as the conditioning variable because this is the most common use case for luminosity function evolution. In that case, \(\Phi(M \mid z)\) describes how the magnitude distribution of galaxies changes with redshift. The same API can be used with any other conditioning variable by replacing z with the desired quantity.

The examples include:

  • a conditional Schechter luminosity function,

  • a conditional Schechter model using LFKit parameter models,

  • a lognormal component,

  • a modified Schechter-like component,

  • a two-component lognormal plus modified-Schechter model,

  • integrated number densities and component fractions,

  • a halo-mass conditional example,

  • selection-limited number densities.

The number-density units follow the normalization supplied to the luminosity function. For example, if the amplitudes are supplied in \({\rm Mpc}^{-3}\,{\rm mag}^{-1}\), then \(\Phi(M \mid z)\) has units of \({\rm Mpc}^{-3}\,{\rm mag}^{-1}\).

Conditional Schechter luminosity function#

A conditional Schechter luminosity function is a Schechter model whose parameters depend on an external variable. Instead of using one fixed normalization, characteristic magnitude, and faint-end slope, the model can let one or more of these quantities vary with the condition.

This example uses redshift as the condition. The normalization \(\phi_\star(z)\) and characteristic magnitude \(M_\star(z)\) evolve with redshift, while the faint-end slope \(\alpha\) is kept fixed. Each curve is therefore a Schechter luminosity function at one redshift. Comparing the curves shows how the abundance and bright-end turnover shift as the population evolves.

import numpy as np
import matplotlib.pyplot as plt
import cmasher as cmr

from lfkit import ConditionalLuminosityFunction

LABEL_SIZE = 15
TICK_SIZE = 13
TITLE_SIZE = 17
LEGEND_SIZE = 15

absolute_mag = np.linspace(-24.0, -14.0, 500)
redshifts = [0.1, 0.6, 1.1]

colors = cmr.take_cmap_colors(
    "cmr.guppy",
    len(redshifts),
    cmap_range=(0.0, 0.2),
)

lf = ConditionalLuminosityFunction.schechter(
    phi_star=lambda z: 1.0e-3 * (1.0 + z) ** 0.8,
    m_star=lambda z: -20.5 - 0.7 * (z - 0.1),
    alpha=-1.1,
)

fig, ax = plt.subplots(figsize=(7.0, 5.0))

for z_value, color in zip(redshifts, colors):
    z = np.full_like(absolute_mag, z_value)
    phi = lf.phi(absolute_mag, z)

    ax.plot(
        absolute_mag,
        phi,
        lw=3,
        color=color,
        label=rf"$z={z_value}$",
    )

ax.set_yscale("log")
ax.set_ylim(1.0e-8, 1.0e-1)
ax.invert_xaxis()
ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE)
ax.set_ylabel(
    r"$\Phi(M \mid z)$ [$\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1}$]",
    fontsize=LABEL_SIZE,
)
ax.set_title("Conditional Schechter luminosity function", fontsize=TITLE_SIZE)
ax.tick_params(axis="both", labelsize=TICK_SIZE)
ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best")

plt.tight_layout()

(png)

../_images/conditional_luminosity_function-1.png

Conditional Schechter surface#

The same conditional Schechter model can be evaluated over the full magnitude-redshift plane rather than at a few selected redshifts.

The filled colour scale shows \(\log_{10}\Phi(M \mid z)\). The contours mark constant abundance levels. This view is useful for checking the global behaviour of the model: smooth contours indicate that the parameter evolution produces a smooth luminosity function, while abrupt features would signal a problem with the chosen parameterization or redshift dependence.

import numpy as np
import matplotlib.pyplot as plt
import cmasher as cmr

from lfkit import ConditionalLuminosityFunction

LABEL_SIZE = 15
TICK_SIZE = 13
TITLE_SIZE = 17

absolute_mag = np.linspace(-24.0, -16.0, 220)
redshift = np.linspace(0.0, 1.5, 180)

mag_grid, z_grid = np.meshgrid(absolute_mag, redshift)

lf = ConditionalLuminosityFunction.schechter(
    phi_star=lambda z: 1.0e-3 * (1.0 + z) ** 0.8,
    m_star=lambda z: -20.5 - 0.7 * (z - 0.1),
    alpha=-1.1,
)

phi = np.clip(lf.phi(mag_grid, z_grid), 1.0e-8, 1.0e-1)
log_phi = np.log10(phi)

fig, ax = plt.subplots(figsize=(7.2, 5.0))

mesh = ax.pcolormesh(
    absolute_mag,
    redshift,
    log_phi,
    shading="auto",
    cmap="cmr.guppy",
)

contour_levels = [-5.0, -4.0, -3.0, -2.0]
contours = ax.contour(
    absolute_mag,
    redshift,
    log_phi,
    levels=contour_levels,
    colors="white",
    linewidths=1.5,
    linestyles="solid",
)
ax.clabel(contours, inline=True, fontsize=TICK_SIZE, fmt=r"$10^{%.0f}$")

ax.invert_xaxis()
ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE)
ax.set_ylabel("Redshift $z$", fontsize=LABEL_SIZE)
ax.set_title("Conditional Schechter LF surface", fontsize=TITLE_SIZE)
ax.tick_params(axis="both", labelsize=TICK_SIZE)

cbar = fig.colorbar(mesh, ax=ax)
cbar.set_label(
    r"$\log_{10}\Phi(M \mid z)$ "
    r"[$\log_{10}(\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1})$]",
    fontsize=LABEL_SIZE,
)
cbar.ax.tick_params(labelsize=TICK_SIZE)

plt.tight_layout()

(png)

../_images/conditional_luminosity_function-2.png

Conditional Schechter model with LFKit parameter models#

LFKit can also build conditional Schechter models from its registered parameter models. This is useful when the desired evolution follows one of the standard forms already available in LFKit, rather than being written manually as a callable.

Here, the normalization and characteristic magnitude evolve with the conditioning variable, while the faint-end slope is constant. The result is equivalent in spirit to the previous example, but the parameter evolution is defined through named LFKit models. This makes the model easier to reproduce, configure, and document.

import numpy as np
import matplotlib.pyplot as plt
import cmasher as cmr

from lfkit import ConditionalLuminosityFunction

LABEL_SIZE = 15
TICK_SIZE = 13
TITLE_SIZE = 17
LEGEND_SIZE = 15

absolute_mag = np.linspace(-24.0, -14.0, 500)
redshifts = [0.1, 0.6, 1.1]

colors = cmr.take_cmap_colors(
    "cmr.guppy",
    len(redshifts),
    cmap_range=(0.0, 0.2),
)

lf = ConditionalLuminosityFunction.evolving_schechter(
    phi_model="linear_p",
    phi_kwargs={"phi_0_star": 1.0e-3, "p": 0.7},
    m_star_model="linear_q",
    m_star_kwargs={"m_0_star": -20.5, "q": 0.8, "z_ref": 0.1},
    alpha_model="constant",
    alpha_kwargs={"alpha": -1.1},
)

fig, ax = plt.subplots(figsize=(7.0, 5.0))

for z_value, color in zip(redshifts, colors):
    z = np.full_like(absolute_mag, z_value)
    phi = lf.phi(absolute_mag, z)

    ax.plot(
        absolute_mag,
        phi,
        lw=3,
        color=color,
        label=rf"$z={z_value}$",
    )

ax.set_yscale("log")
ax.set_ylim(1.0e-8, 1.0e-1)
ax.invert_xaxis()
ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE)
ax.set_ylabel(
    r"$\Phi(M \mid z)$ [$\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1}$]",
    fontsize=LABEL_SIZE,
)
ax.set_title("Conditional evolving Schechter model", fontsize=TITLE_SIZE)
ax.tick_params(axis="both", labelsize=TICK_SIZE)
ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best")

plt.tight_layout()

(png)

../_images/conditional_luminosity_function-3.png

Lognormal conditional component#

A lognormal conditional component describes a population concentrated around a characteristic luminosity at fixed condition. In magnitude space, this appears as a relatively narrow feature centred on a mean absolute magnitude.

This example lets the mean absolute magnitude become brighter with redshift, while the scatter is fixed. The peak therefore shifts to brighter magnitudes as redshift increases, but the width of the component remains similar. This kind of component is useful for representing a population with a preferred luminosity scale rather than a broad Schechter-like faint-end tail.

import numpy as np
import matplotlib.pyplot as plt
import cmasher as cmr

from lfkit import ConditionalLuminosityFunction

LABEL_SIZE = 15
TICK_SIZE = 13
TITLE_SIZE = 17
LEGEND_SIZE = 15

absolute_mag = np.linspace(-24.0, -16.0, 500)
redshifts = [0.1, 0.6, 1.1]

colors = cmr.take_cmap_colors(
    "cmr.guppy",
    len(redshifts),
    cmap_range=(0.0, 0.2),
)

lf = ConditionalLuminosityFunction.lognormal(
    mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1),
    sigma_log_luminosity=0.18,
    amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4,
)

fig, ax = plt.subplots(figsize=(7.0, 5.0))

for z_value, color in zip(redshifts, colors):
    z = np.full_like(absolute_mag, z_value)
    phi = lf.phi(absolute_mag, z)

    ax.plot(
        absolute_mag,
        phi,
        lw=3,
        color=color,
        label=rf"$z={z_value}$",
    )

ax.set_yscale("log")
ax.set_ylim(1.0e-8, 1.0e-1)
ax.invert_xaxis()
ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE)
ax.set_ylabel(
    r"$\Phi_{\rm lognormal}(M \mid z)$ "
    r"[$\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1}$]",
    fontsize=LABEL_SIZE,
)
ax.set_title("Lognormal conditional LF component", fontsize=TITLE_SIZE)
ax.tick_params(axis="both", labelsize=TICK_SIZE)
ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best")

plt.tight_layout()

(png)

../_images/conditional_luminosity_function-4.png

Modified Schechter conditional component#

The modified Schechter component is Schechter-like at the faint end but uses a different cutoff at high luminosity. Instead of the standard exponential cutoff, it has a broader cutoff controlled by the luminosity ratio.

This example lets the normalization, characteristic magnitude, and faint-end slope vary with redshift. The model therefore changes both in amplitude and in shape. This is useful when a standard Schechter function is too restrictive, especially for populations whose bright end falls off more gradually.

import numpy as np
import matplotlib.pyplot as plt
import cmasher as cmr

from lfkit import ConditionalLuminosityFunction

LABEL_SIZE = 15
TICK_SIZE = 13
TITLE_SIZE = 17
LEGEND_SIZE = 15

absolute_mag = np.linspace(-24.0, -14.0, 500)
redshifts = [0.1, 0.6, 1.1]

colors = cmr.take_cmap_colors(
    "cmr.guppy",
    len(redshifts),
    cmap_range=(0.0, 0.2),
)

lf = ConditionalLuminosityFunction.modified_schechter(
    phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5,
    m_star=lambda z: -19.9 - 0.5 * (z - 0.1),
    alpha=lambda z: -1.05 - 0.10 * z,
)

fig, ax = plt.subplots(figsize=(7.0, 5.0))

for z_value, color in zip(redshifts, colors):
    z = np.full_like(absolute_mag, z_value)
    phi = lf.phi(absolute_mag, z)

    ax.plot(
        absolute_mag,
        phi,
        lw=3,
        color=color,
        label=rf"$z={z_value}$",
    )

ax.set_yscale("log")
ax.set_ylim(1.0e-8, 1.0e-1)
ax.invert_xaxis()
ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE)
ax.set_ylabel(
    r"$\Phi_{\rm modSch}(M \mid z)$ "
    r"[$\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1}$]",
    fontsize=LABEL_SIZE,
)
ax.set_title("Modified Schechter conditional LF component", fontsize=TITLE_SIZE)
ax.tick_params(axis="both", labelsize=TICK_SIZE)
ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best")

plt.tight_layout()

(png)

../_images/conditional_luminosity_function-5.png

Standard, modified, and lognormal component shapes#

It is useful to compare the component shapes at fixed condition before combining them into more complicated models. This separates differences in functional form from differences caused by redshift or halo-mass evolution.

The standard Schechter form has a power-law faint end and an exponential bright-end cutoff. The modified Schechter component has a broader bright-end shape. The lognormal component is localized around a characteristic luminosity. Together, these examples show the kinds of galaxy populations each component is best suited to describe.

import numpy as np
import matplotlib.pyplot as plt
import cmasher as cmr

from lfkit import ConditionalLuminosityFunction

LABEL_SIZE = 15
TICK_SIZE = 13
TITLE_SIZE = 17
LEGEND_SIZE = 15

colors_blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0))
colors_red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))
colors_all = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 1.0))

absolute_mag = np.linspace(-24.0, -14.0, 600)
z_value = 0.6
z = np.full_like(absolute_mag, z_value)

schechter_lf = ConditionalLuminosityFunction.schechter(
    phi_star=1.0e-3,
    m_star=-20.5,
    alpha=-1.1,
)

modified_lf = ConditionalLuminosityFunction.modified_schechter(
    phi_star=1.0e-3,
    m_star=-20.5,
    alpha=-1.1,
)

lognormal_lf = ConditionalLuminosityFunction.lognormal(
    mean_absolute_mag=-20.5,
    sigma_log_luminosity=0.20,
    amplitude=1.0e-3,
)

phi_schechter = schechter_lf.phi(absolute_mag, z)
phi_modified = modified_lf.phi(absolute_mag, z)
phi_lognormal = lognormal_lf.phi(absolute_mag, z)

fig, ax = plt.subplots(figsize=(7.0, 5.0))

ax.plot(
    absolute_mag,
    phi_schechter,
    lw=3,
    color=colors_blue[1],
    label="Standard Schechter",
)
ax.plot(
    absolute_mag,
    phi_modified,
    lw=3,
    color=colors_red[1],
    label="Modified Schechter",
)
ax.plot(
    absolute_mag,
    phi_lognormal,
    lw=3,
    color=colors_all[1],
    label="Lognormal",
)

ax.set_yscale("log")
ax.set_ylim(1.0e-8, 1.0e-1)
ax.invert_xaxis()
ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE)
ax.set_ylabel(
    r"$\Phi(M \mid z=0.6)$ "
    r"[$\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1}$]",
    fontsize=LABEL_SIZE,
)
ax.set_title("Conditional LF component shapes", fontsize=TITLE_SIZE)
ax.tick_params(axis="both", labelsize=TICK_SIZE)
ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best")

plt.tight_layout()

(png)

../_images/conditional_luminosity_function-6.png

Two-component conditional LF#

The lognormal and modified Schechter components can be combined into a two-component conditional luminosity function. This is useful when one component is intended to describe a relatively narrow population and the other describes a broader luminosity distribution.

This plot separates the lognormal component, the modified Schechter component, and their sum at a fixed redshift. The comparison shows which component dominates different magnitude ranges. In this example, the lognormal component is concentrated near its characteristic magnitude, while the modified Schechter component contributes over a wider range of magnitudes.

import numpy as np
import matplotlib.pyplot as plt
import cmasher as cmr

from lfkit import ConditionalLuminosityFunction

LABEL_SIZE = 15
TICK_SIZE = 13
TITLE_SIZE = 17
LEGEND_SIZE = 15

colors_blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0))
colors_red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))
colors_all = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 1.0))

absolute_mag = np.linspace(-24.0, -14.0, 500)
z_value = 0.6
z = np.full_like(absolute_mag, z_value)

lognormal_lf = ConditionalLuminosityFunction.lognormal(
    mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1),
    sigma_log_luminosity=0.18,
    amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4,
)

modified_lf = ConditionalLuminosityFunction.modified_schechter(
    phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5,
    m_star=lambda z: -19.9 - 0.5 * (z - 0.1),
    alpha=lambda z: -1.05 - 0.10 * z,
)

total_lf = ConditionalLuminosityFunction.two_component(
    lognormal_mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1),
    lognormal_sigma_log_luminosity=0.18,
    lognormal_amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4,
    modified_phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5,
    modified_m_star=lambda z: -19.9 - 0.5 * (z - 0.1),
    modified_alpha=lambda z: -1.05 - 0.10 * z,
)

lognormal_phi = lognormal_lf.phi(absolute_mag, z)
modified_phi = modified_lf.phi(absolute_mag, z)
total_phi = total_lf.phi(absolute_mag, z)

fig, ax = plt.subplots(figsize=(7.0, 5.0))

ax.plot(
    absolute_mag,
    lognormal_phi,
    lw=3,
    color=colors_blue[1],
    label="Lognormal component",
)
ax.plot(
    absolute_mag,
    modified_phi,
    lw=3,
    color=colors_red[1],
    label="Modified Schechter component",
)
ax.plot(
    absolute_mag,
    total_phi,
    lw=3,
    color=colors_all[1],
    label="Two-component total",
)

ax.set_yscale("log")
ax.set_ylim(1.0e-8, 1.0e-1)
ax.invert_xaxis()
ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE)
ax.set_ylabel(
    r"$\Phi(M \mid z)$ [$\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1}$]",
    fontsize=LABEL_SIZE,
)
ax.set_title(r"Two-component conditional LF at $z=0.6$", fontsize=TITLE_SIZE)
ax.tick_params(axis="both", labelsize=TICK_SIZE)
ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best")

plt.tight_layout()

(png)

../_images/conditional_luminosity_function-7.png

Two-component evolution#

The two-component conditional luminosity function can also be evaluated at several redshifts. This shows how the total population changes when both components depend on the conditioning variable.

Each curve is the sum of the lognormal and modified Schechter components at one redshift. Changes in the curves reflect the combined evolution of the component amplitudes, characteristic magnitudes, and faint-end behaviour. This plot is a compact way to check whether the full conditional model evolves in the expected direction.

import numpy as np
import matplotlib.pyplot as plt
import cmasher as cmr

from lfkit import ConditionalLuminosityFunction

LABEL_SIZE = 15
TICK_SIZE = 13
TITLE_SIZE = 17
LEGEND_SIZE = 15

absolute_mag = np.linspace(-24.0, -14.0, 500)
redshifts = [0.1, 0.6, 1.1]

colors = cmr.take_cmap_colors(
    "cmr.guppy",
    len(redshifts),
    cmap_range=(0.0, 0.2),
)

lf = ConditionalLuminosityFunction.two_component(
    lognormal_mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1),
    lognormal_sigma_log_luminosity=0.18,
    lognormal_amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4,
    modified_phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5,
    modified_m_star=lambda z: -19.9 - 0.5 * (z - 0.1),
    modified_alpha=lambda z: -1.05 - 0.10 * z,
)

fig, ax = plt.subplots(figsize=(7.0, 5.0))

for z_value, color in zip(redshifts, colors):
    z = np.full_like(absolute_mag, z_value)
    phi = lf.phi(absolute_mag, z)

    ax.plot(
        absolute_mag,
        phi,
        lw=3,
        color=color,
        label=rf"$z={z_value}$",
    )

ax.set_yscale("log")
ax.set_ylim(1.0e-8, 1.0e-1)
ax.invert_xaxis()
ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE)
ax.set_ylabel(
    r"$\Phi_{\rm total}(M \mid z)$ "
    r"[$\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1}$]",
    fontsize=LABEL_SIZE,
)
ax.set_title("Two-component conditional LF", fontsize=TITLE_SIZE)
ax.tick_params(axis="both", labelsize=TICK_SIZE)
ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best")

plt.tight_layout()

(png)

../_images/conditional_luminosity_function-8.png

Integrated conditional number density#

A conditional luminosity function can be integrated over absolute magnitude at each value of the conditioning variable. The result is an integrated number density,

\[n(x) = \int_{M_{\rm bright}}^{M_{\rm faint}} \Phi(M \mid x)\,\mathrm{d}M.\]

For redshift as the conditioning variable, this gives the number density of galaxies inside the chosen magnitude range as a function of redshift. Because conditional constructors return normal LFKit luminosity function objects, the same lf.integrals namespace can be used here.

This example integrates the lognormal component, the modified Schechter component, and the full two-component model. The total number density is the sum of the two components, while the individual curves show how much each component contributes.

import numpy as np
import matplotlib.pyplot as plt
import cmasher as cmr

from lfkit import ConditionalLuminosityFunction

LABEL_SIZE = 15
TICK_SIZE = 13
TITLE_SIZE = 17
LEGEND_SIZE = 15

colors_blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0))
colors_red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))
colors_all = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 1.0))

redshift = np.linspace(0.05, 1.5, 180)

lognormal_lf = ConditionalLuminosityFunction.lognormal(
    mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1),
    sigma_log_luminosity=0.18,
    amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4,
)

modified_lf = ConditionalLuminosityFunction.modified_schechter(
    phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5,
    m_star=lambda z: -19.9 - 0.5 * (z - 0.1),
    alpha=lambda z: -1.05 - 0.10 * z,
)

total_lf = ConditionalLuminosityFunction.two_component(
    lognormal_mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1),
    lognormal_sigma_log_luminosity=0.18,
    lognormal_amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4,
    modified_phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5,
    modified_m_star=lambda z: -19.9 - 0.5 * (z - 0.1),
    modified_alpha=lambda z: -1.05 - 0.10 * z,
)

n_lognormal = lognormal_lf.integrals.number_density(
    redshift,
    m_bright=-24.0,
    m_faint=-14.0,
    n_m=800,
)

n_modified = modified_lf.integrals.number_density(
    redshift,
    m_bright=-24.0,
    m_faint=-14.0,
    n_m=800,
)

n_total = total_lf.integrals.number_density(
    redshift,
    m_bright=-24.0,
    m_faint=-14.0,
    n_m=800,
)

fig, ax = plt.subplots(figsize=(7.0, 5.0))

ax.plot(
    redshift,
    n_lognormal,
    lw=3,
    color=colors_blue[1],
    label="Lognormal component",
)
ax.plot(
    redshift,
    n_modified,
    lw=3,
    color=colors_red[1],
    label="Modified Schechter component",
)
ax.plot(
    redshift,
    n_total,
    lw=3,
    color=colors_all[1],
    label="Two-component total",
)

ax.set_yscale("log")
ax.set_ylim(1.0e-4, 1.0e-1)
ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE)
ax.set_ylabel(r"Integrated number density [$\mathrm{Mpc}^{-3}$]", fontsize=LABEL_SIZE)
ax.set_title(r"Integrated conditional LF over $-24 \leq M \leq -14$", fontsize=TITLE_SIZE)
ax.tick_params(axis="both", labelsize=TICK_SIZE)
ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best")

plt.tight_layout()

(png)

../_images/conditional_luminosity_function-9.png

Component fractions#

The relative contribution of each component can be summarized as a fraction of the integrated two-component luminosity function. For components \(n_1(z)\) and \(n_2(z)\), the corresponding fractions are

\[f_1(z) = \frac{n_1(z)}{n_1(z) + n_2(z)}, \qquad f_2(z) = \frac{n_2(z)}{n_1(z) + n_2(z)}.\]

This example computes the integrated lognormal and modified Schechter components with lf.integrals.number_density. The resulting fractions show whether the selected population is dominated by the narrow lognormal component, the broader modified Schechter component, or a mixture of both. This is often easier to interpret than comparing raw number densities alone.

import numpy as np
import matplotlib.pyplot as plt
import cmasher as cmr

from lfkit import ConditionalLuminosityFunction

LABEL_SIZE = 15
TICK_SIZE = 13
TITLE_SIZE = 17
LEGEND_SIZE = 15

colors_blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0))
colors_red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))

redshift = np.linspace(0.05, 1.5, 180)

lognormal_lf = ConditionalLuminosityFunction.lognormal(
    mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1),
    sigma_log_luminosity=0.18,
    amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4,
)

modified_lf = ConditionalLuminosityFunction.modified_schechter(
    phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5,
    m_star=lambda z: -19.9 - 0.5 * (z - 0.1),
    alpha=lambda z: -1.05 - 0.10 * z,
)

n_lognormal = lognormal_lf.integrals.number_density(
    redshift,
    m_bright=-24.0,
    m_faint=-14.0,
    n_m=800,
)

n_modified = modified_lf.integrals.number_density(
    redshift,
    m_bright=-24.0,
    m_faint=-14.0,
    n_m=800,
)

n_total = n_lognormal + n_modified

lognormal_fraction = n_lognormal / n_total
modified_fraction = n_modified / n_total

fig, ax = plt.subplots(figsize=(7.0, 5.0))

ax.plot(
    redshift,
    lognormal_fraction,
    lw=3,
    color=colors_blue[1],
    label="Lognormal fraction",
)
ax.plot(
    redshift,
    modified_fraction,
    lw=3,
    color=colors_red[1],
    label="Modified Schechter fraction",
)

ax.set_ylim(-0.05, 1.05)
ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE)
ax.set_ylabel("Fraction of integrated LF", fontsize=LABEL_SIZE)
ax.set_title(r"Component fractions over $-24 \leq M \leq -14$", fontsize=TITLE_SIZE)
ax.tick_params(axis="both", labelsize=TICK_SIZE)
ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="center right")

plt.tight_layout()

(png)

../_images/conditional_luminosity_function-10.png

Two-component LF surface#

The full two-component conditional luminosity function can be shown as a surface in the magnitude-redshift plane. This combines the component structure and the redshift evolution in a single diagnostic plot.

The filled colour scale shows \(\log_{10}\Phi_{\rm total}(M \mid z)\). The contours mark constant abundance levels. This view is useful for checking whether the narrow component, broad component, and redshift dependence combine smoothly across the full domain. It also makes it easier to identify where the model predicts the largest abundance of galaxies.

import numpy as np
import matplotlib.pyplot as plt
import cmasher as cmr

from lfkit import ConditionalLuminosityFunction

LABEL_SIZE = 15
TICK_SIZE = 13
TITLE_SIZE = 17

absolute_mag = np.linspace(-24.0, -14.0, 220)
redshift = np.linspace(0.0, 1.5, 180)

mag_grid, z_grid = np.meshgrid(absolute_mag, redshift)

lf = ConditionalLuminosityFunction.two_component(
    lognormal_mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1),
    lognormal_sigma_log_luminosity=0.18,
    lognormal_amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4,
    modified_phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5,
    modified_m_star=lambda z: -19.9 - 0.5 * (z - 0.1),
    modified_alpha=lambda z: -1.05 - 0.10 * z,
)

phi = np.clip(lf.phi(mag_grid, z_grid), 1.0e-8, 1.0e-1)
log_phi = np.log10(phi)

fig, ax = plt.subplots(figsize=(7.2, 5.0))

mesh = ax.pcolormesh(
    absolute_mag,
    redshift,
    log_phi,
    shading="auto",
    cmap="cmr.guppy",
)

contour_levels = [-5.0, -4.0, -3.0, -2.0]
contours = ax.contour(
    absolute_mag,
    redshift,
    log_phi,
    levels=contour_levels,
    colors="white",
    linewidths=1.5,
    linestyles="solid",
)
ax.clabel(contours, inline=True, fontsize=TICK_SIZE, fmt=r"$10^{%.0f}$")

ax.invert_xaxis()
ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE)
ax.set_ylabel("Redshift $z$", fontsize=LABEL_SIZE)
ax.set_title("Two-component conditional LF surface", fontsize=TITLE_SIZE)
ax.tick_params(axis="both", labelsize=TICK_SIZE)

cbar = fig.colorbar(mesh, ax=ax)
cbar.set_label(
    r"$\log_{10}\Phi_{\rm total}(M \mid z)$ "
    r"[$\log_{10}(\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1})$]",
    fontsize=LABEL_SIZE,
)
cbar.ax.tick_params(labelsize=TICK_SIZE)

plt.tight_layout()

(png)

../_images/conditional_luminosity_function-11.png

Halo-mass conditional luminosity function#

The conditioning variable does not need to be redshift. In halo-model applications, a conditional luminosity function is often written as \(\Phi(M \mid M_h)\), where \(M_h\) is halo mass. This describes the luminosity distribution of galaxies hosted by halos of a given mass.

This example uses log halo mass as the conditioning variable. The lognormal mean magnitude becomes brighter in more massive halos, and the amplitude also increases with halo mass. The interpretation is that more massive halos host brighter and more abundant galaxies in this toy model.

import numpy as np
import matplotlib.pyplot as plt
import cmasher as cmr

from lfkit import ConditionalLuminosityFunction

LABEL_SIZE = 15
TICK_SIZE = 13
TITLE_SIZE = 17
LEGEND_SIZE = 15

absolute_mag = np.linspace(-24.0, -16.0, 600)
log_halo_masses = [11.5, 12.0, 12.5, 13.0]

colors = cmr.take_cmap_colors(
    "cmr.guppy",
    len(log_halo_masses),
    cmap_range=(0.0, 0.2),
)

lf = ConditionalLuminosityFunction.lognormal(
    mean_absolute_mag=lambda log_mh: -20.0 - 0.8 * (log_mh - 12.0),
    sigma_log_luminosity=0.18,
    amplitude=lambda log_mh: 5.0e-4 * 10.0 ** (0.3 * (log_mh - 12.0)),
)

fig, ax = plt.subplots(figsize=(7.0, 5.0))

for log_mh, color in zip(log_halo_masses, colors):
    condition = np.full_like(absolute_mag, log_mh)
    phi = lf.phi(absolute_mag, condition)

    ax.plot(
        absolute_mag,
        phi,
        lw=3,
        color=color,
        label=rf"$\log_{{10}} M_h={log_mh}$",
    )

ax.set_yscale("log")
ax.set_ylim(1.0e-8, 1.0e-1)
ax.invert_xaxis()
ax.set_xlabel("Absolute magnitude $M$", fontsize=LABEL_SIZE)
ax.set_ylabel(
    r"$\Phi(M \mid M_h)$ [$\mathrm{Mpc}^{-3}\,\mathrm{mag}^{-1}$]",
    fontsize=LABEL_SIZE,
)
ax.set_title("Halo-mass conditional lognormal LF", fontsize=TITLE_SIZE)
ax.tick_params(axis="both", labelsize=TICK_SIZE)
ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best")

plt.tight_layout()

(png)

../_images/conditional_luminosity_function-12.png

Mean luminosity ratio from a conditional luminosity function#

Weighted integrals can be used to compute luminosity-weighted summary statistics of a conditional luminosity function. Instead of only counting galaxies, the integral can weight each magnitude by a physically meaningful quantity.

For example, the mean luminosity ratio relative to a reference magnitude is

\[\left\langle \frac{L}{L_{\rm ref}} \right\rangle(x) = \frac{ \int (L/L_{\rm ref}) \Phi(M \mid x)\,\mathrm{d}M }{ \int \Phi(M \mid x)\,\mathrm{d}M }.\]

This example uses the lf.integrals namespace on a two-component conditional luminosity function. The numerator is a luminosity-weighted integral and the denominator is the number density. Their ratio gives the average luminosity of the selected population relative to the chosen reference luminosity.

import numpy as np
import matplotlib.pyplot as plt
import cmasher as cmr

from lfkit import ConditionalLuminosityFunction

LABEL_SIZE = 15
TICK_SIZE = 13
TITLE_SIZE = 17

redshift = np.linspace(0.05, 1.5, 180)
reference_mag = -20.5

lf = ConditionalLuminosityFunction.two_component(
    lognormal_mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1),
    lognormal_sigma_log_luminosity=0.18,
    lognormal_amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4,
    modified_phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5,
    modified_m_star=lambda z: -19.9 - 0.5 * (z - 0.1),
    modified_alpha=lambda z: -1.05 - 0.10 * z,
)

number_density = lf.integrals.number_density(
    redshift,
    m_bright=-24.0,
    m_faint=-14.0,
    n_m=800,
)

weighted_luminosity_ratio = lf.integrals.weighted(
    redshift,
    weight_fn=lambda absolute_mag, condition: 10.0 ** (
        -0.4 * (absolute_mag - reference_mag)
    ),
    m_bright=-24.0,
    m_faint=-14.0,
    n_m=800,
)

mean_luminosity_ratio = weighted_luminosity_ratio / number_density

fig, ax = plt.subplots(figsize=(7.0, 5.0))

ax.plot(
    redshift,
    mean_luminosity_ratio,
    lw=3,
    color=cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))[1],
)

ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE)
ax.set_ylabel(r"Mean luminosity ratio $\langle L/L_{\rm ref} \rangle$", fontsize=LABEL_SIZE)
ax.set_title("Mean luminosity ratio of the conditional LF", fontsize=TITLE_SIZE)
ax.tick_params(axis="both", labelsize=TICK_SIZE)

plt.tight_layout()

(png)

../_images/conditional_luminosity_function-13.png

Selection-limited conditional number density#

The LFKit API can integrate a conditional luminosity function over finite magnitude bounds. This is useful for survey-like selections where only galaxies brighter than a limiting absolute magnitude contribute to the observed sample.

Here, the limiting absolute magnitude becomes brighter with redshift. The example compares the full number density over a fixed reference magnitude range with the number density brighter than the redshift-dependent limit. The gap between the two curves shows how much of the luminosity function is excluded by the selection.

import numpy as np
import matplotlib.pyplot as plt
import cmasher as cmr

from lfkit import ConditionalLuminosityFunction

LABEL_SIZE = 15
TICK_SIZE = 13
TITLE_SIZE = 17
LEGEND_SIZE = 15

redshift = np.linspace(0.05, 1.5, 180)

lf = ConditionalLuminosityFunction.two_component(
    lognormal_mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1),
    lognormal_sigma_log_luminosity=0.18,
    lognormal_amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4,
    modified_phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5,
    modified_m_star=lambda z: -19.9 - 0.5 * (z - 0.1),
    modified_alpha=lambda z: -1.05 - 0.10 * z,
)

limiting_mag = -18.5 - 1.2 * redshift

n_total = lf.integrals.number_density(
    redshift,
    m_bright=-24.0,
    m_faint=-14.0,
    n_m=800,
)

n_selected = lf.integrals.number_density(
    redshift,
    m_bright=-24.0,
    m_faint=limiting_mag,
    n_m=800,
)

colors_blue = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.8, 1.0))
colors_red = cmr.take_cmap_colors("cmr.guppy", 3, cmap_range=(0.0, 0.2))

fig, ax = plt.subplots(figsize=(7.0, 5.0))

ax.plot(
    redshift,
    n_total,
    lw=3,
    color=colors_blue[1],
    label="Full magnitude range",
)
ax.plot(
    redshift,
    n_selected,
    lw=3,
    color=colors_red[1],
    label="Brighter than limit",
)

ax.set_yscale("log")
ax.set_ylim(1.0e-4, 1.0e-1)
ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE)
ax.set_ylabel(
    r"Integrated number density [$\mathrm{Mpc}^{-3}$]",
    fontsize=LABEL_SIZE,
)
ax.set_title("Selection-limited conditional number density", fontsize=TITLE_SIZE)
ax.tick_params(axis="both", labelsize=TICK_SIZE)
ax.legend(frameon=True, fontsize=LEGEND_SIZE, loc="best")

plt.tight_layout()

(png)

../_images/conditional_luminosity_function-14.png

Selection fraction#

The selected fraction is the ratio between the number density brighter than the redshift-dependent limiting magnitude and the number density over the full reference magnitude range,

\[f_{\rm selected}(z) = \frac{ n(M_{\rm bright} \leq M \leq M_{\rm lim}(z)) }{ n(M_{\rm bright} \leq M \leq M_{\rm faint}) }.\]

This diagnostic compresses the selection effect into a number between zero and one. Values close to one mean that most of the reference luminosity function is included by the selection. Smaller values mean that the limiting magnitude removes a larger fraction of the population.

import numpy as np
import matplotlib.pyplot as plt
import cmasher as cmr

from lfkit import ConditionalLuminosityFunction

LABEL_SIZE = 15
TICK_SIZE = 13
TITLE_SIZE = 17

redshift = np.linspace(0.05, 1.5, 180)

lf = ConditionalLuminosityFunction.two_component(
    lognormal_mean_absolute_mag=lambda z: -20.8 - 0.6 * (z - 0.1),
    lognormal_sigma_log_luminosity=0.18,
    lognormal_amplitude=lambda z: 8.0e-4 * (1.0 + z) ** 0.4,
    modified_phi_star=lambda z: 1.2e-3 * (1.0 + z) ** 0.5,
    modified_m_star=lambda z: -19.9 - 0.5 * (z - 0.1),
    modified_alpha=lambda z: -1.05 - 0.10 * z,
)

limiting_mag = -18.5 - 1.2 * redshift

n_total = lf.integrals.number_density(
    redshift,
    m_bright=-24.0,
    m_faint=-14.0,
    n_m=800,
)

n_selected = lf.integrals.number_density(
    redshift,
    m_bright=-24.0,
    m_faint=limiting_mag,
    n_m=800,
)

selected_fraction = n_selected / n_total

color = 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, selected_fraction, lw=3, color=color)

ax.set_ylim(-0.05, 1.05)
ax.set_xlabel("Redshift $z$", fontsize=LABEL_SIZE)
ax.set_ylabel("Selected fraction", fontsize=LABEL_SIZE)
ax.set_title("Fraction brighter than the limiting magnitude", fontsize=TITLE_SIZE)
ax.tick_params(axis="both", labelsize=TICK_SIZE)

plt.tight_layout()

(png)

../_images/conditional_luminosity_function-15.png