Source code for brutus.priors.stellar

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Stellar priors for Bayesian parameter estimation.

This module provides log-prior functions for stellar properties including
the initial mass function (IMF) and luminosity functions. These priors
are used in Bayesian inference of stellar parameters to incorporate
physical constraints from stellar populations.

Functions
---------
logp_imf : Initial mass function prior
    Kroupa-like broken power-law IMF
logp_ps1_luminosity_function : Luminosity function prior
    Pan-STARRS 1 r-band luminosity function

See Also
--------
brutus.analysis.individual.BruteForce : Uses these priors for stellar fitting
brutus.priors.galactic : Galactic structure priors
brutus.priors.extinction : Extinction priors

Notes
-----
These priors provide physically-motivated probability distributions for
stellar parameters:

- **IMF priors** weight stellar masses according to population statistics,
  ensuring realistic mass distributions in Bayesian fits

- **Luminosity function priors** weight absolute magnitudes according to
  observed stellar populations, useful when fitting distance and extinction

The priors are normalized and return log-probabilities suitable for direct
use in MCMC or nested sampling codes.

Examples
--------
Basic IMF prior usage:

>>> import numpy as np
>>> from brutus.priors.stellar import logp_imf
>>>
>>> # Evaluate IMF prior for solar-mass star
>>> masses = np.array([1.0])
>>> log_prior = logp_imf(masses)
>>> print(f"Log-prior for 1 solar mass: {log_prior[0]:.3f}")
>>>
>>> # Binary system with 1.0 + 0.5 solar mass components
>>> log_prior_binary = logp_imf(masses, mgrid2=np.array([0.5]))
>>> print(f"Binary log-prior: {log_prior_binary[0]:.3f}")

Luminosity function prior:

>>> from brutus.priors.stellar import logp_ps1_luminosity_function
>>>
>>> # Absolute r-band magnitude for main sequence star
>>> Mr = np.array([5.0])
>>> log_prior = logp_ps1_luminosity_function(Mr)
>>> print(f"Log-prior for Mr=5: {log_prior[0]:.3f}")
"""

import numpy as np

__all__ = ["logp_imf", "logp_ps1_luminosity_function"]


def _powerlaw_mass_integral(m_lo, m_hi, alpha):
    """Integral of ``M**(-alpha) dM`` from ``m_lo`` to ``m_hi``.

    Uses the logarithmic form ``ln(m_hi / m_lo)`` when ``alpha`` is (near) 1,
    where the usual ``(m**(1 - alpha)) / (1 - alpha)`` expression is singular
    (0/0). This keeps the IMF normalization finite and correct for the
    physically meaningful flat-in-log slope ``alpha == 1`` (and tames the
    catastrophic cancellation that occurs for slopes very close to 1).
    """
    if np.isclose(alpha, 1.0):
        return np.log(m_hi / m_lo)
    return (m_hi ** (1.0 - alpha) - m_lo ** (1.0 - alpha)) / (1.0 - alpha)


[docs] def logp_imf( mgrid, alpha_low=1.3, alpha_high=2.3, mass_break=0.5, mgrid2=None, mass_min=0.08, mass_max=100.0, ): r""" Log-prior for a Kroupa-like broken initial mass function. Implements a broken power-law IMF with separate slopes for low and high stellar masses, following Kroupa (2001). Supports binary systems with a secondary mass component. Parameters ---------- mgrid : array_like Grid of initial stellar masses in solar units. Must be > 0. alpha_low : float, optional Power-law slope for low-mass stars (M ≤ mass_break). Default is 1.3 (Kroupa 2001). alpha_high : float, optional Power-law slope for high-mass stars (M > mass_break). Default is 2.3 (Kroupa 2001). mass_break : float, optional Transition mass between low and high mass regimes in solar units. Default is 0.5. mgrid2 : array_like, optional Grid of secondary stellar masses for binary systems in solar units. If provided, computes joint prior for binary system. mass_min : float, optional Minimum mass for normalization (hydrogen burning limit). Default is 0.08 solar masses. mass_max : float, optional Maximum mass for normalization. Default is 100.0 solar masses. Returns ------- logp : array_like Normalized log-prior probability density for the input mass grid(s). Returns -inf for masses below mass_min or above mass_max. See Also -------- logp_ps1_luminosity_function : Alternative luminosity-based prior brutus.analysis.individual.BruteForce : Uses IMF priors for fitting Notes ----- The IMF follows the form: .. math:: \\xi(M) \\propto M^{-\\alpha} where α = α_low for M ≤ M_break and α = α_high for M > M_break. For binary systems, assumes independent sampling from the same IMF for both components. References ---------- Kroupa, P. (2001), MNRAS, 322, 231 """ mgrid = np.asarray(mgrid) # Initialize log-prior with -inf for invalid masses logp = np.full_like(mgrid, -np.inf, dtype=float) # Valid mass range valid_mass = (mgrid >= mass_min) & (mgrid <= mass_max) # Low-mass regime: M ≤ mass_break low_mass = valid_mass & (mgrid <= mass_break) logp[low_mass] = -alpha_low * np.log(mgrid[low_mass]) # High-mass regime: M > mass_break high_mass = valid_mass & (mgrid > mass_break) logp[high_mass] = -alpha_high * np.log(mgrid[high_mass]) + ( alpha_high - alpha_low ) * np.log(mass_break) # Compute normalization factor over [mass_min, mass_max] # Low-mass regime: mass_min to min(mass_break, mass_max) if mass_max >= mass_break: # Both regimes present norm_low = _powerlaw_mass_integral(mass_min, mass_break, alpha_low) # High-mass regime: mass_break to mass_max # The high-mass PDF includes continuity factor: mass_break^(alpha_high - alpha_low) continuity_factor = mass_break ** (alpha_high - alpha_low) norm_high = continuity_factor * _powerlaw_mass_integral( mass_break, mass_max, alpha_high ) norm = norm_low + norm_high else: # Only low-mass regime present norm = _powerlaw_mass_integral(mass_min, mass_max, alpha_low) # Handle binary component if provided if mgrid2 is not None: mgrid2 = np.asarray(mgrid2) # Compute prior for secondary logp2 = np.full_like(mgrid2, -np.inf, dtype=float) valid_mass2 = (mgrid2 >= mass_min) & (mgrid2 <= mass_max) low_mass2 = valid_mass2 & (mgrid2 <= mass_break) high_mass2 = valid_mass2 & (mgrid2 > mass_break) logp2[low_mass2] = -alpha_low * np.log(mgrid2[low_mass2]) logp2[high_mass2] = -alpha_high * np.log(mgrid2[high_mass2]) + ( alpha_high - alpha_low ) * np.log(mass_break) # Combined log-prior for binary system logp = logp + logp2 # Updated normalization for binary (independent sampling) norm = norm**2 # Apply normalization return logp - np.log(norm)
# Global interpolator for PS1 luminosity function (loaded on first use) _ps1_lf_interpolator = None
[docs] def logp_ps1_luminosity_function(Mr): """ Pan-STARRS 1 r-band luminosity function log-prior. Implements the stellar luminosity function derived from Pan-STARRS 1 observations, specifically calibrated for use with Bayestar stellar evolutionary models and dust maps. Parameters ---------- Mr : array_like Absolute r-band magnitudes in the Pan-STARRS 1 photometric system. Returns ------- logp : array_like Log-prior probability density for the given absolute magnitudes. Interpolates from empirical Pan-STARRS 1 luminosity function. See Also -------- logp_imf : Alternative mass-based IMF prior brutus.analysis.individual.BruteForce : Uses luminosity priors for fitting Notes ----- This prior is designed for integration with: - Bayestar stellar evolutionary tracks - Bayestar 3D dust extinction maps - Pan-STARRS 1 photometric system The luminosity function is based on empirical measurements from the Pan-STARRS 1 survey and provides realistic stellar population weights for Bayesian inference. References ---------- Green et al. (2015) - 3D Dust Mapping with Pan-STARRS 1 Green et al. (2018) - Bayestar dust maps """ global _ps1_lf_interpolator # Load data file on first use if _ps1_lf_interpolator is None: import os from scipy.interpolate import interp1d # Get path to data file module_dir = os.path.dirname(os.path.abspath(__file__)) data_path = os.path.join(module_dir, "PSMrLF_lnprior.dat") # Load PS1 luminosity function data grid_Mr, grid_lnp = np.loadtxt(data_path).T # Create interpolator with extrapolation _ps1_lf_interpolator = interp1d( grid_Mr, grid_lnp, fill_value="extrapolate", kind="linear" ) # Evaluate log-prior return _ps1_lf_interpolator(Mr)