Source code for uvex_transients.models.supernovae.IIp

r"""SED model for Type IIP supernovae."""

from typing import ClassVar

import numpy as np
from astropy import units as u

from uvex_transients.models._constants import SECONDS_PER_DAY
from uvex_transients.models._typing import CGSParameterValue, FloatArray
from uvex_transients.models._util_functions import _log_sigmoid
from uvex_transients.models._utils import to_cgs_value
from uvex_transients.models.core.base import SpectralModel
from uvex_transients.models.core.parameters import Parameter
from uvex_transients.models.core.priors import ConstantPrior, NormalPrior, UniformPrior
from uvex_transients.models.spectra.thermal import BlackbodySpectrum

__all__ = ["TypeIIPExcessSED", "TypeIIPSED"]

#: :math:`^{56}\mathrm{Co}` decay e-folding time (half-life 77.236 d / ln 2), in days. The
#: radioactive tail's decay rate is fixed to this physical constant rather than sampled.
_TAU_CO_DAYS = 77.236 / np.log(2.0)

#: Numerical floor on L_bol, in erg/s. Well before t0 (where L_bol is already ~0 from the S_0(t)
#: switch) the bolometric light curve is unconstrained and can underflow toward 0 in float64,
#: which would otherwise propagate to -inf magnitudes downstream.
_L_BOL_FLOOR_CGS = 1e35

#: Numerical floor on the photospheric temperature, in K. Same rationale as `_L_BOL_FLOOR_CGS`:
#: well before t0, the doubly-broken power law's early t^alpha_r branch can underflow toward 0 K,
#: which drives the blackbody flux to exactly 0 and -inf magnitudes.
_T_FLOOR_K = 100.0


# ======================================== #
# Model                                    #
# ======================================== #
[docs] class TypeIIPSED(SpectralModel): r""" Type IIP supernova SED: two shared-onset exponentials and a switched radioactive tail. With a cooling blackbody photosphere. .. math:: L_\mathrm{bol}(t) = S_0(t)\,\bigl[1-S_P(t)\bigr] \Bigl[L_\mathrm{pk}\,e^{-(t-t_0)/\tau_\mathrm{cool}} + L_p\Bigr] + S_P(t)\, L_\mathrm{Co}\, e^{-(t-t_P)/\tau_\mathrm{Co}}, with two logistic switches .. math:: S_0(t) = \frac{1}{1+e^{-(t-t_0)/\tau_\mathrm{rise}}}, \qquad S_P(t) = \frac{1}{1+e^{-(t-t_P)/\tau_\mathrm{drop}}}. :math:`S_0` turns the photospheric emission on at the explosion/rise epoch :math:`t_0`; :math:`S_P` turns it back off -- and the radioactive tail on -- at the plateau-end epoch :math:`t_P`. Well before :math:`t_0`, :math:`L_\mathrm{bol} \to 0`. Well after the rise but before the plateau ends, :math:`L_\mathrm{bol} \simeq L_\mathrm{pk}\,e^{-(t-t_0)/\tau_\mathrm{cool}} + L_p`: an early cooling-phase decline from :math:`L_\mathrm{pk}` onto a constant plateau :math:`L_p`. After :math:`t_P`, :math:`L_\mathrm{bol} \to L_\mathrm{Co}\, e^{-(t-t_P)/\tau_\mathrm{Co}}`, a pure radioactive-tail exponential. A deliberately minimum-complexity stand-in for the classic three-phase Type IIP morphology (shock-cooling decline, hydrogen-recombination plateau, radioactive tail): two logistic switches, two exponentials, three luminosity scales. The photospheric temperature reuses the light curve's own :math:`t_0` and :math:`t_P` and is a smooth, doubly-broken power law with three regimes -- an early regime, a cooling-phase regime around :math:`t_0`, and a recombination/plateau regime around :math:`t_P` -- .. math:: T(t) = T_0\, \left(\frac{t}{t_0}\right)^{\alpha_r} \left(\frac{1 + (t/t_0)^{s_0}}{2}\right)^{\frac{\alpha_c - \alpha_r}{s_0}} \left(\frac{1 + (t/\sqrt{t_0 t_P})^{s_1}}{2}\right)^{-\frac{\alpha_c - \alpha_p}{s_1}}, (:math:`t` in days). Both bracketed terms are normalized to equal 1 exactly at their own break (:math:`t=t_0`, :math:`t=\sqrt{t_0 t_P}`), so :math:`T(t_0) \approx T_0` (up to a small correction from the second bracket, negligible when :math:`t_0 \ll t_P`). For :math:`t \ll t_0`, :math:`T(t) \to T_0\, (t/t_0)^{\alpha_r}`; between the two breaks (:math:`t_0 \ll t \ll \sqrt{t_0 t_P}`), :math:`T(t) \propto t^{\alpha_c}`; for :math:`t \gg \sqrt{t_0 t_P}` (approaching and past :math:`t_P`), :math:`T(t) \propto t^{\alpha_p}`. With :math:`\alpha_r > 0 > \alpha_c > \alpha_p`, the temperature rises through the early, pre-:math:`t_0` region (where :math:`L_\mathrm{bol} \approx 0` anyway, so this region is not otherwise constrained), peaks near :math:`t_0`, then declines -- fast through the cooling regime, and much more slowly (nearly flat) through the recombination/plateau regime. :math:`s_0`/:math:`s_1` set the sharpness of the two breaks (smooth, unlike the light curve's logistic switches, but the same role); the second break has no epoch of its own, so it is centered on the geometric mean of :math:`t_0` and :math:`t_P`. .. rubric:: Parameters .. list-table:: :header-rows: 1 :widths: 22 78 * - Parameter - Symbol * - ``t0`` - :math:`t_0` * - ``tau_rise`` - :math:`\tau_\mathrm{rise}` * - ``L_pk`` - :math:`L_\mathrm{pk}` * - ``tau_cool`` - :math:`\tau_\mathrm{cool}` * - ``L_p`` - :math:`L_p` * - ``t_P`` - :math:`t_P` * - ``tau_drop`` - :math:`\tau_\mathrm{drop}` * - ``L_Co`` - :math:`L_\mathrm{Co}` * - ``tau_Co`` - :math:`\tau_\mathrm{Co}` * - ``T_0`` - :math:`T_0` * - ``alpha_r`` - :math:`\alpha_r` * - ``alpha_c`` - :math:`\alpha_c` * - ``alpha_p`` - :math:`\alpha_p` * - ``s_0`` - :math:`s_0` * - ``s_1`` - :math:`s_1` Notes ----- ``tau_drop``/``tau_Co`` are fixed shape constants. Light-curve priors (``t0`` through ``tau_Co``) come from a :func:`scipy.optimize.curve_fit` to SN 1999em and SN 2003hn; temperature-law priors (``T_0`` through ``s_1``) are hand-tuned against the aggregate Type IIP photospheric-temperature dataset in ``test_data/transients`` (SN 1999em, SN 2003hn, SN 2012aw, SN 2012A, SN 2008in), following :footcite:t:`dallora2014` and :footcite:t:`faran2018`. """ _DEFAULT_PARAMETERS: ClassVar[dict[str, Parameter]] = { "t0": Parameter( prior=UniformPrior(lower=4, upper=20), scale=1.0 * u.day, description="Explosion/rise reference epoch.", latex=r"t_0", ), "tau_rise": Parameter( prior=NormalPrior(mean=0, sigma=0.2), scale=1.0 * u.day, transform="log10", description="Rise timescale.", latex=r"\tau_\mathrm{rise}", ), "L_pk": Parameter( prior=NormalPrior(mean=np.log10(3e42), sigma=0.3), scale=1.0 * u.erg / u.s, transform="log10", description="Early cooling-phase peak luminosity scale.", latex=r"L_\mathrm{pk}", ), "tau_cool": Parameter( prior=UniformPrior(lower=4, upper=10), scale=1 * u.day, description="Early cooling-phase decay timescale.", latex=r"\tau_\mathrm{cool}", ), "L_p": Parameter( prior=NormalPrior(mean=42.1, sigma=0.2), scale=1.0 * u.erg / u.s, transform="log10", description="Plateau luminosity.", latex=r"L_p", ), "t_P": Parameter( prior=UniformPrior(lower=50, upper=150), scale=1 * u.day, description="Plateau-end / radioactive-tail-onset epoch.", latex=r"t_P", ), "tau_drop": Parameter( prior=ConstantPrior(value=10.5), scale=1 * u.day, description="Plateau-end transition width. Fixed.", latex=r"\tau_\mathrm{drop}", ), "L_Co": Parameter( prior=NormalPrior(mean=41, sigma=0.3), scale=1.0 * u.erg / u.s, transform="log10", description="Radioactive-tail luminosity at t_P.", latex=r"L_\mathrm{Co}", ), "tau_Co": Parameter( prior=ConstantPrior(value=1.0), scale=_TAU_CO_DAYS * u.day, description="Radioactive-tail decay timescale. Fixed to the 56Co decay e-folding time.", latex=r"\tau_\mathrm{Co}", ), "T_0": Parameter( prior=NormalPrior(mean=4.05, sigma=0.1), scale=1.0 * u.K, transform="log10", description="Doubly-broken power-law temperature normalization; T(t) ~ T_0 at t = t0 (~11200 K).", latex=r"T_0", ), "alpha_r": Parameter( prior=NormalPrior(mean=2, sigma=0.2), scale=1, description="Early-regime (t << t0) temperature power-law index; T rises toward t0.", latex=r"\alpha_r", ), "alpha_c": Parameter( prior=NormalPrior(mean=-0.45, sigma=0.1), scale=1, description="Cooling-regime (t0 << t << sqrt(t0 t_P)) temperature power-law index; T declines.", latex=r"\alpha_c", ), "alpha_p": Parameter( prior=NormalPrior(mean=-0.1, sigma=0.01), scale=1, description="Plateau-regime (t >> sqrt(t0 t_P)) temperature power-law index; T declines slowly.", latex=r"\alpha_p", ), "s_0": Parameter( prior=UniformPrior(lower=10, upper=20), scale=1, description="Sharpness of the break at t0 (early -> cooling regime).", latex=r"s_0", ), "s_1": Parameter( prior=UniformPrior(lower=10, upper=50), scale=1, description="Sharpness of the break at sqrt(t0 t_P) (cooling -> plateau regime).", latex=r"s_1", ), } # -------------------------------------- # # Cooling Law: T(t) # # -------------------------------------- # @classmethod def _temperature_cgs( cls, t: FloatArray, *, t0: CGSParameterValue, t_P: CGSParameterValue, T_0: CGSParameterValue, alpha_r: CGSParameterValue, alpha_c: CGSParameterValue, alpha_p: CGSParameterValue, s_0: CGSParameterValue, s_1: CGSParameterValue, **_ignored: CGSParameterValue, ) -> FloatArray: r""" Doubly-broken power-law photospheric temperature; see the class docstring. Reuses the light curve's own ``t0``/``t_P``. Evaluated in days -- ``t``, ``t0``, ``t_P`` arrive in cgs (seconds), converted back here since (unlike the rest of this model) the formula is not scale-invariant to the day/second choice. Floored at ``_T_FLOOR_K`` -- well before ``t0`` (unconstrained; ``L_bol`` is already ~0 there) the early branch can otherwise underflow toward 0 K. Parameters ---------- t : numpy.ndarray Time since explosion, in seconds. t0, t_P, T_0, alpha_r, alpha_c, alpha_p, s_0, s_1 : float or numpy.ndarray This model's parameter values, in cgs units; see the class docstring. **_ignored Any other model parameter values, ignored. Returns ------- numpy.ndarray :math:`T(t)`, in Kelvin. """ t_day = t / SECONDS_PER_DAY t0_day = t0 / SECONDS_PER_DAY tP_day = t_P / SECONDS_PER_DAY t_break2 = np.sqrt(t0_day * tP_day) with np.errstate(divide="ignore", invalid="ignore"): early_to_cooling = ((1.0 + (t_day / t0_day) ** s_0) / 2) ** ((alpha_c - alpha_r) / s_0) cooling_to_plateau = ((1.0 + (t_day / t_break2) ** s_1) / 2) ** (-(alpha_c - alpha_p) / s_1) temperature = T_0 * (t_day / t0_day) ** alpha_r * early_to_cooling * cooling_to_plateau return np.maximum(temperature, _T_FLOOR_K)
[docs] @classmethod def temperature(cls, t: u.Quantity, **parameters: CGSParameterValue) -> u.Quantity: r""" :math:`T(t)` in Kelvin. Parameters ---------- t : ~astropy.units.Quantity Time since explosion. **parameters This model's parameter values. See :meth:`eval_log_cgs`. Returns ------- ~astropy.units.Quantity :math:`T(t)`, in Kelvin. """ cgs_parameters: dict[str, CGSParameterValue] = {name: to_cgs_value(value) for name, value in parameters.items()} return cls._temperature_cgs(t.cgs.value, **cgs_parameters) * u.K
# -------------------------------------- # # Bolometric Luminosity: L_bol(t) # # -------------------------------------- # @classmethod def _eval_bolometric(cls, t: FloatArray, **parameters: CGSParameterValue) -> FloatArray: r""" :math:`\ln L_\mathrm{bol}(t)`; see the class docstring. Floored at ``_L_BOL_FLOOR_CGS`` -- well before ``t0``/well after ``t_P`` this is already ~0 and otherwise unconstrained, and can underflow toward 0 in float64. Parameters ---------- t : numpy.ndarray Time since explosion, in seconds. **parameters This model's parameter values, in cgs units. Returns ------- numpy.ndarray The natural log of :math:`L_\mathrm{bol}(t)`, in erg/s. """ t0 = parameters["t0"] t_P = parameters["t_P"] x0 = (t - t0) / parameters["tau_rise"] xP = (t - t_P) / parameters["tau_drop"] ln_S0 = _log_sigmoid(x0) ln_SP = _log_sigmoid(xP) ln_1mSP = _log_sigmoid(-xP) # L_pk * exp(-(t-t0)/tau_cool) + L_p, computed stably in log-space. ln_photospheric = np.logaddexp( np.log(parameters["L_pk"]) - (t - t0) / parameters["tau_cool"], np.log(parameters["L_p"]), ) ln_term_photospheric = ln_S0 + ln_1mSP + ln_photospheric ln_term_radioactive = ln_SP + np.log(parameters["L_Co"]) - (t - t_P) / parameters["tau_Co"] ln_L_bol = np.logaddexp(ln_term_photospheric, ln_term_radioactive) return np.maximum(ln_L_bol, np.log(_L_BOL_FLOOR_CGS)) # -------------------------------------- # # Normalized Spectral Shape: S(nu, t) # # -------------------------------------- # @classmethod def _eval_spectrum(cls, nu: FloatArray, t: FloatArray, **parameters: CGSParameterValue) -> FloatArray: r""" :math:`\log S(\nu, T(t))`, delegated to :class:`BlackbodySpectrum`. Evaluated at this ``t``'s own cooling-law temperature. Parameters ---------- nu : numpy.ndarray Frequency, in Hz. t : numpy.ndarray Time since explosion, in seconds. **parameters This model's parameter values, in cgs units. Returns ------- numpy.ndarray The natural log of the normalized spectral shape, in 1/Hz. """ temperature = cls._temperature_cgs(t, **parameters) return BlackbodySpectrum._eval(nu, temperature=temperature) # -------------------------------------- # # Spectral Luminosity: L_nu(nu, t) # # -------------------------------------- # @classmethod def _eval(cls, nu: FloatArray, t: FloatArray, **parameters: CGSParameterValue) -> FloatArray: r""" :math:`\log L_\nu(\nu, t) = \log L_\mathrm{bol}(t) + \log S(\nu, T(t))`. Parameters ---------- nu : numpy.ndarray Frequency, in Hz. t : numpy.ndarray Time since explosion, in seconds. **parameters This model's parameter values, in cgs units. Returns ------- numpy.ndarray The natural log of :math:`L_\nu(\nu, t)`, in erg/s/Hz. """ return cls._eval_bolometric(t, **parameters) + cls._eval_spectrum(nu, t, **parameters)
# ======================================== # # Model: Early-Interacting Excess # # ======================================== #
[docs] class TypeIIPExcessSED(TypeIIPSED): r""" Early-interacting, IXF/GGI-like Type IIP supernova SED. Same light curve and temperature functional forms as :class:`TypeIIPSED` -- only the parameter priors differ, shifted toward the brighter, hotter, faster early cooling phase attributed to shock breakout through and/or collisional heating of close circumstellar material, rather than the smoother early decline of ordinary Type IIP SNe. Priors are hand-tuned against SN 2023ixf :footcite:p:`hsu2025` and SN 2024ggi :footcite:p:`chen2024`, the only two objects of this kind currently in ``test_data/transients``. """ _DEFAULT_PARAMETERS: ClassVar[dict[str, Parameter]] = { "t0": Parameter( prior=UniformPrior(lower=2, upper=5), scale=1.0 * u.day, description="Explosion/rise reference epoch.", latex=r"t_0", ), "tau_rise": Parameter( prior=NormalPrior(mean=-0.5, sigma=0.2), scale=1.0 * u.day, transform="log10", description="Rise timescale.", latex=r"\tau_\mathrm{rise}", ), "L_pk": Parameter( prior=NormalPrior(mean=np.log10(2e43), sigma=0.25), scale=1.0 * u.erg / u.s, transform="log10", description="Early cooling-phase peak luminosity scale.", latex=r"L_\mathrm{pk}", ), "tau_cool": Parameter( prior=UniformPrior(lower=4, upper=15), scale=1 * u.day, description="Early cooling-phase decay timescale.", latex=r"\tau_\mathrm{cool}", ), "L_p": Parameter( prior=NormalPrior(mean=42.2, sigma=0.2), scale=1.0 * u.erg / u.s, transform="log10", description="Plateau luminosity.", latex=r"L_p", ), "t_P": Parameter( prior=UniformPrior(lower=50, upper=150), scale=1 * u.day, description="Plateau-end / radioactive-tail-onset epoch.", latex=r"t_P", ), "tau_drop": Parameter( prior=ConstantPrior(value=10.5), scale=1 * u.day, description="Plateau-end transition width. Fixed.", latex=r"\tau_\mathrm{drop}", ), "L_Co": Parameter( prior=NormalPrior(mean=41.5, sigma=0.3), scale=1.0 * u.erg / u.s, transform="log10", description="Radioactive-tail luminosity at t_P.", latex=r"L_\mathrm{Co}", ), "tau_Co": Parameter( prior=ConstantPrior(value=1.0), scale=_TAU_CO_DAYS * u.day, description="Radioactive-tail decay timescale. Fixed to the 56Co decay e-folding time.", latex=r"\tau_\mathrm{Co}", ), "T_0": Parameter( prior=NormalPrior(mean=4.3, sigma=0.1), scale=1.0 * u.K, transform="log10", description="Doubly-broken power-law temperature normalization; T(t) ~ T_0 at t = t0 (~11200 K).", latex=r"T_0", ), "alpha_r": Parameter( prior=NormalPrior(mean=1, sigma=0.2), scale=1, description="Early-regime (t << t0) temperature power-law index; T rises toward t0.", latex=r"\alpha_r", ), "alpha_c": Parameter( prior=NormalPrior(mean=-0.5, sigma=0.1), scale=1, description="Cooling-regime (t0 << t << sqrt(t0 t_P)) temperature power-law index; T declines.", latex=r"\alpha_c", ), "alpha_p": Parameter( prior=NormalPrior(mean=-0.1, sigma=0.01), scale=1, description="Plateau-regime (t >> sqrt(t0 t_P)) temperature power-law index; T declines slowly.", latex=r"\alpha_p", ), "s_0": Parameter( prior=UniformPrior(lower=10, upper=20), scale=1, description="Sharpness of the break at t0 (early -> cooling regime).", latex=r"s_0", ), "s_1": Parameter( prior=UniformPrior(lower=10, upper=50), scale=1, description="Sharpness of the break at sqrt(t0 t_P) (cooling -> plateau regime).", latex=r"s_1", ), }