r"""Tidal disruption event SED with an early decline and a late-time disk plateau.
Following :footcite:t:`2025arXiv250303811A`.
"""
from typing import ClassVar
import numpy as np
from astropy import units as u
from uvex_transients.models._typing import CGSParameterValue, FloatArray
from uvex_transients.models.core.base import SpectralModel
from uvex_transients.models.core.parameters import Parameter
from uvex_transients.models.core.priors import NormalPrior, UniformPrior
from uvex_transients.models.lightcurves.generic import GREDLightcurve
from uvex_transients.models.spectra.thermal import BlackbodySpectrum
__all__ = ["AlushStoneTDESED"]
[docs]
class AlushStoneTDESED(SpectralModel):
r"""
A Gaussian-rise/exponential-decline photosphere followed by a magnetized-disk plateau.
.. math::
L_\nu(\nu, t) = L_\mathrm{bol}^\mathrm{early}(t) \cdot S(\nu, T)
+ L_\mathrm{bol}^\mathrm{plat}(t) \cdot S(\nu, T_\mathrm{p}),
where :math:`S(\nu, T)` is :class:`~uvex_transients.models.spectra.thermal.BlackbodySpectrum`'s
normalized shape, :math:`L_\mathrm{bol}^\mathrm{early}(t)` is
:class:`~uvex_transients.models.lightcurves.generic.GREDLightcurve`
(peaking at :math:`t_\mathrm{peak} = 5\sigma`, that class's own convention), and
.. math::
L_\mathrm{bol}^\mathrm{plat}(t) = L_\mathrm{p}
\left(1 + \frac{t - t_\mathrm{peak}}{\tau_\mathrm{p}}\right)^{-\alpha_\mathrm{p}}.
The plateau branch is referenced to the same :math:`t_\mathrm{peak}` as the early
branch (the same convention `GaussianRisePowerLawLightcurve` uses for its power-law
tail): :math:`L_\mathrm{bol}^\mathrm{plat}(t_\mathrm{peak}) = L_\mathrm{p}` exactly,
and it smoothly softens from a flat plateau (for :math:`t - t_\mathrm{peak} \ll
\tau_\mathrm{p}`) to a :math:`t^{-\alpha_\mathrm{p}}` power-law decline (for
:math:`t - t_\mathrm{peak} \gg \tau_\mathrm{p}`) -- exactly the family of
theory-agnostic plateau shapes fit to late-time TDE disks by
:footcite:t:`2025arXiv251024696A`. The two components are summed in linear space
(:func:`numpy.logaddexp` on their logs) rather than switched between, since a real
disk plateau does not sharply replace the fading early-time emission.
Physically, the early branch is the same reprocessed/photospheric emission
:class:`~uvex_transients.models.tdes.van_velzen.VanVelzenTDESED` models, while the
plateau is the late-time, UV-bright emission from a magnetized accretion disk that
:footcite:t:`2025arXiv250303811A` predict settles onto an asymptotic
:math:`L \propto t^{-5/6}` decline persisting for decades to centuries --
``plateau_decline``'s default prior is a wide, uninformative :math:`\mathrm{Uniform}(0, 2)`
that brackets that theoretical value rather than pinning it, since
:footcite:t:`2025arXiv251024696A` find real plateaus vary in how flat/evolving they are.
``temperature``/``sigma_rise``/``tau_decline`` reuse the same ZTF-sample-informed
defaults as :class:`~uvex_transients.models.tdes.van_velzen.VanVelzenTDESED` (see that
class's docstring); ``plateau_temperature``, ``plateau_amplitude``, and
``plateau_timescale`` do not yet have literature-calibrated defaults and use
order-of-magnitude fiducial scales instead -- narrow these once fit to real
late-time photometry.
.. rubric:: Parameters
The model parameters are summarized below.
.. list-table::
:header-rows: 1
:widths: 18 18 64
* - Parameter
- Symbol
- Description
* - ``amplitude``
- :math:`L_0`
- Peak bolometric luminosity of the early-time component,
:math:`L_0 = L_\mathrm{bol}^\mathrm{early}(t_\mathrm{peak})`.
:math:`\log_{10}(L_0/\mathrm{erg\,s^{-1}}) \sim \mathcal{N}(43.8, 0.3^2)`.
* - ``temperature``
- :math:`T`
- Early-time photospheric blackbody temperature.
:math:`\log_{10}(T/\mathrm{K}) \sim \mathcal{N}(4.3, 0.1^2)`.
* - ``sigma_rise``
- :math:`\sigma`
- Gaussian width of the pre-peak rise.
:math:`\log_{10}(\sigma/\mathrm{d}) \sim \mathcal{N}(0.91, 0.25^2)`.
* - ``tau_decline``
- :math:`\tau`
- Exponential decline timescale of the early-time component, after peak.
:math:`\log_{10}(\tau/\mathrm{d}) \sim \mathcal{N}(1.8, 0.2^2)`.
* - ``plateau_temperature``
- :math:`T_\mathrm{p}`
- Blackbody temperature of the late-time disk plateau.
:math:`\log_{10}(T_\mathrm{p}/\mathrm{K}) \sim \mathcal{N}(4.0, 0.3^2)`.
* - ``plateau_amplitude``
- :math:`L_\mathrm{p}`
- Plateau bolometric luminosity at :math:`t_\mathrm{peak}`,
:math:`L_\mathrm{p} = L_\mathrm{bol}^\mathrm{plat}(t_\mathrm{peak})`.
:math:`\log_{10}(L_\mathrm{p}/\mathrm{erg\,s^{-1}}) \sim \mathcal{N}(41.5, 0.2^2)`.
* - ``plateau_timescale``
- :math:`\tau_\mathrm{p}`
- Timescale over which the plateau softens into its power-law decline.
:math:`\log_{10}(\tau_\mathrm{p}/\mathrm{d}) \sim \mathcal{N}(2.3, 0.3^2)`.
* - ``plateau_decline``
- :math:`\alpha_\mathrm{p}`
- Late-time power-law decline index, :math:`L \propto t^{-\alpha_\mathrm{p}}`
for :math:`t - t_\mathrm{peak} \gg \tau_\mathrm{p}`.
:math:`\alpha_\mathrm{p} \sim \mathrm{Uniform}(0, 2)`, a wide prior bracketing the
magnetized-disk prediction :math:`\alpha_\mathrm{p}=5/6` of
:footcite:t:`2025arXiv250303811A`.
References
----------
.. footbibliography::
"""
_DEFAULT_PARAMETERS: ClassVar[dict[str, Parameter]] = {
"amplitude": Parameter(
prior=NormalPrior(mean=43.8, sigma=0.3),
scale=1.0 * u.erg / u.s,
transform="log10",
description="Peak bolometric luminosity, L_0 = L_bol(t_peak). log10(L_0/[erg/s]) ~ N(43.8, 0.3^2).",
latex=r"L_0",
),
"temperature": Parameter(
prior=NormalPrior(mean=4.3, sigma=0.1),
scale=1.0 * u.K,
transform="log10",
description="Photospheric blackbody temperature. log10(T/K) ~ N(4.3, 0.1^2).",
latex=r"T",
),
"sigma_rise": Parameter(
prior=NormalPrior(mean=0.91, sigma=0.25),
scale=1.0 * u.day,
transform="log10",
description="Gaussian width of the pre-peak rise. log10(sigma/day) ~ N(0.91, 0.25^2).",
latex=r"\sigma",
),
"tau_decline": Parameter(
prior=NormalPrior(mean=1.8, sigma=0.2),
scale=1.0 * u.day,
transform="log10",
description="Exponential decline timescale after peak. log10(tau/day) ~ N(1.8, 0.2^2).",
latex=r"\tau",
),
"plateau_temperature": Parameter(
prior=NormalPrior(mean=4.0, sigma=0.3),
scale=1.0 * u.K,
transform="log10",
description="Blackbody temperature of the late-time disk plateau. log10(T_p/K) ~ N(4.0, 0.3^2).",
latex=r"T_\mathrm{p}",
),
"plateau_amplitude": Parameter(
prior=NormalPrior(mean=41.5, sigma=0.2),
scale=1.0 * u.erg / u.s,
transform="log10",
description="Plateau bolometric luminosity, L_p = L_bol_plat(t_peak). log10(L_p/[erg/s]) ~ N(41.5, 0.2^2).",
latex=r"L_\mathrm{p}",
),
"plateau_timescale": Parameter(
prior=NormalPrior(mean=2.3, sigma=0.3),
scale=1.0 * u.day,
transform="log10",
description=(
"Timescale over which the plateau softens into a power-law decline. log10(tau_p/day) ~ N(2.3, 0.3^2)."
),
latex=r"\tau_\mathrm{p}",
),
"plateau_decline": Parameter(
prior=UniformPrior(lower=0, upper=2),
scale=1.0 * u.dimensionless_unscaled,
description=(
"Late-time power-law decline index, L ~ t^-alpha_p. Uniform(0, 2), bracketing "
"the magnetized-disk prediction alpha_p = 5/6."
),
latex=r"\alpha_\mathrm{p}",
),
}
# ============================================== #
# SED Methods #
# ============================================== #
@classmethod
def _eval(cls, nu: FloatArray, t: FloatArray, **parameters: CGSParameterValue) -> FloatArray:
r"""
Evaluate the natural log of :math:`L_\nu(\nu, t)`, in cgs units.
This is the one method every model must implement. ``nu``, ``t``,
and every parameter value are combined using plain NumPy
broadcasting -- no axes are inserted automatically. See the module
docstring for what that means in practice.
Parameters
----------
nu : numpy.ndarray
Frequency, in Hz.
t : numpy.ndarray
Time since explosion, in seconds. Always non-negative.
**parameters
This model's parameter values, in cgs units, broadcastable
against ``nu`` and ``t``.
Returns
-------
numpy.ndarray
The natural log of :math:`L_\nu(\nu, t)`, in erg/s/Hz.
"""
# Evaluate the early- and late-time spectral shapes using the
# blackbody function provided in the thermal spectrum module.
early_spec = BlackbodySpectrum._eval(nu, temperature=parameters["temperature"])
late_spec = BlackbodySpectrum._eval(nu, temperature=parameters["plateau_temperature"])
# Evaluate the two components' bolometric envelopes, both referenced
# to the same t_peak (GREDLightcurve's own convention -- see its
# docstring). The early envelope is a Gaussian rise / exponential
# decline in time, not frequency, so `t` (not `nu`) is what it's
# evaluated at.
t_peak = 5 * parameters["sigma_rise"]
early_amp = GREDLightcurve._eval(
t,
amplitude=parameters["amplitude"],
sigma_rise=parameters["sigma_rise"],
tau_decline=parameters["tau_decline"],
)
# The plateau softens from flat (near t_peak) to a t^-plateau_decline
# power law once t - t_peak >> plateau_timescale -- see the class
# docstring, which only defines this behavior for t >= t_peak (x >=
# 0). `(1+x)^-plateau_decline` is not just undefined but singular as
# x -> -1 from above (since 1+x -> 0+ with a negative exponent), so
# letting the branch run for -1 < x < 0 doesn't just risk a NaN at
# the boundary -- it produces a spurious blow-up in luminosity
# immediately after it, whenever `plateau_timescale` is smaller than
# `t_peak`. Restricting to x >= 0 keeps evaluation inside the domain
# the formula (and docstring) actually describe, where it's smooth
# and bounded by L_p; a plateau that hasn't reached t_peak yet
# contributes zero luminosity, i.e. -inf in log space.
x = (t - t_peak) / parameters["plateau_timescale"]
plateau_active = x >= 0.0
with np.errstate(divide="ignore", invalid="ignore"):
plateau_shape = -parameters["plateau_decline"] * np.log1p(np.where(plateau_active, x, 0.0))
late_amp = np.where(
plateau_active,
np.log(parameters["plateau_amplitude"]) + plateau_shape,
-np.inf,
)
# Combine each component's shape and envelope in log space (shape +
# envelope, since `early_spec`/`late_spec` are already normalized
# log-shapes -- see `BlackbodySpectrum`), then sum the two
# components themselves via logaddexp rather than a linear-space
# sum, since both are already logarithmic.
early_sed = early_spec + early_amp
late_sed = late_spec + late_amp
return np.logaddexp(early_sed, late_sed)