Physics Models

The building blocks of tengri’s forward model: star formation history parameterisations, power spectral density functions, dust attenuation, stellar population synthesis, and filter handling.

Star Formation History

Analytic mean-SFH functional forms. Each takes a log-age grid and shape parameters, returning SFR in solar masses per year.

tengri.tsnorm(t_lookback: Array, log_peak_sfr: float, peak_lbt: float, width: float, skew: float, trunc: float) Array

Truncated skew-normal star formation history (Bellstedt+2020).

The most flexible smooth SFH model: a skewed Gaussian kernel multiplied by a normal CDF truncation factor that smoothly suppresses star formation at recent times (young lookback times).

Parameters:
  • t_lookback (array_like, shape (n_age,)) – Lookback time [yr].

  • log_peak_sfr (float) – log10 of peak SFR [Msun/yr].

  • peak_lbt (float) – Peak lookback time [yr].

  • width (float) – Gaussian width parameter [yr].

  • skew (float) – Skewness parameter [dimensionless]. 0 = symmetric, >0 skews toward older ages.

  • trunc (float) – Truncation sharpness [dimensionless]. Larger values produce sharper truncation. Typical range: 1-10.

Returns:

SFR at each lookback time [Msun/yr], non-negative.

Return type:

ndarray, shape (n_age,)

Notes

JIT-compatible: yes — uses jnp primitives and jax.lax.erfc.

Gradient-safe: yes — differentiable everywhere except at SFR=0 (where gradient is ill-defined but finite).

The SFH is:

\[\mathrm{SFR}(t) = 10^{\log_{\rm peak}} \, K(t) \, T(t)\]

where \(K(t)\) is the skewed Gaussian kernel and \(T(t)\) is the truncation factor:

\[K(t) = \exp\left( -\frac{Y(t)^2}{2} \right), \quad Y(t) = \frac{t - t_{\rm peak}}{w} \exp(\gamma \, \mathrm{arcsinh}(Y))\]
\[T(t) = \frac{1}{2} \mathrm{erfc}\left( \frac{t - t_{\rm peak}}{w \cdot f_{\rm trunc} \sqrt{2}} \right) # noqa: E501\]

where \(w\) is width [yr], \(\gamma\) is skewness [dimensionless], and \(f_{\rm trunc}\) is the truncation sharpness [dimensionless].

Approximation: The skewness is implemented via the Robotham+2020 formulation (arcsinh-based) rather than the classical Owen’s lambda transformation. This provides better numerical stability and JAX compatibility.

References

Examples

>>> import jax.numpy as jnp
>>> from tengri.components.stellar.sfh import truncated_skewnormal
>>> t = jnp.linspace(0.0, 13.7e9, 100)
>>> sfr = truncated_skewnormal(
...     t, log_peak_sfr=1.0, peak_lbt=5e9, width=2e9, skew=0.0, trunc=5.0
... )
>>> sfr.shape
(100,)
tengri.dpl(t_lookback: Array, alpha: float, beta: float, tau: float, log_peak_sfr: float) Array[source]

Double power law with log-peak SFR parameterization (canonical).

Registry-compatible wrapper around the Carnall+2018 double power law. Uses log10(peak_sfr) instead of linear normalization for consistency with other parametric SFH models in the registry.

Parameters:
  • t_lookback (array_like, shape (n_age,)) – Lookback time [yr].

  • alpha (float) – Falling slope exponent [dimensionless]. Typical range: 0.5-4.

  • beta (float) – Rising slope exponent [dimensionless]. Typical range: 0.3-3.

  • tau (float) – Turnover timescale [yr].

  • log_peak_sfr (float) – log10 of peak SFR [Msun/yr].

Returns:

SFR at each lookback time [Msun/yr].

Return type:

ndarray, shape (n_age,)

Notes

JIT-compatible: yes — all operations use jnp primitives.

This function wraps double_powerlaw() with norm = 10**log_peak_sfr. See double_powerlaw() for physics details.

Examples

>>> import jax.numpy as jnp
>>> from tengri import dpl
>>> t = jnp.logspace(7, 10.14, 64)
>>> sfr = dpl(t, alpha=1.5, beta=0.5, tau=3e9, log_peak_sfr=1.5)
>>> sfr.shape
(64,)
tengri.double_powerlaw(t_lookback: Array, alpha: float, beta: float, tau: float, norm: float) Array[source]

Double power law star formation history (Carnall+2018, BAGPIPES).

A flexible two-parameter SFH model that peaks near a characteristic timescale and can smoothly transition between rising and declining phases.

Parameters:
  • t_lookback (array_like, shape (n_age,)) – Lookback time [yr].

  • alpha (float) – Falling slope exponent [dimensionless]. Controls the decline from peak to present. Larger alpha = steeper decline. Typical range: 0.5-4.

  • beta (float) – Rising slope exponent [dimensionless]. Controls the rise from early times to peak. Larger beta = steeper rise. Typical range: 0.3-3.

  • tau (float) – Turnover timescale [yr]. Approximately when SFR peaks (in cosmic time).

  • norm (float) – Normalization factor [Msun/yr]. Note: this controls overall amplitude, not stellar mass. \(M_\star = \int \mathrm{SFR}(t) \, dt\) is derived.

Returns:

SFR at each lookback time [Msun/yr].

Return type:

ndarray, shape (n_age,)

Notes

JIT-compatible: yes — all operations use jnp primitives.

Gradient-safe: yes — differentiable everywhere for positive tau.

The double power law SFH is:

\[\mathrm{SFR}(t_{\rm cosmic}) = \frac{n}{(t_{\rm cosmic}/\tau)^\alpha + (t_{\rm cosmic}/\tau)^{-\beta}} # noqa: E501\]

where \(t_{\rm cosmic} = t_{\mathrm{H}} - t_{\rm lookback}\) is cosmic time since the Big Bang, \(t_{\mathrm{H}}\) is the age of the universe, \(\tau\) is the turnover timescale, and \(\alpha\), \(\beta\) are the falling and rising slopes [dimensionless].

In cosmic time: \(\alpha\) controls the declining phase (after peak), \(\beta\) controls the rising phase (before peak).

In lookback time (as plotted): \(\alpha\) controls the RIGHT side (early universe, large lookback), \(\beta\) controls the LEFT side (near present, small lookback).

References

Examples

>>> import jax.numpy as jnp
>>> from tengri import double_powerlaw
>>> t = jnp.linspace(1e6, 13.7e9, 100)
>>> sfr = double_powerlaw(t, alpha=1.5, beta=2.0, tau=3e9, norm=10.0)
>>> sfr.shape
(100,)
tengri.snorm(t_lookback: Array, log_peak_sfr: float, peak_lbt: float, width: float, skew: float) Array

Skew-normal SFH (Robotham+2020).

Like truncated_skewnormal but without truncation.

Parameters:
  • t_lookback (array_like, shape (n_age,)) – Lookback time [yr].

  • log_peak_sfr (float) – log10 of peak SFR [Msun/yr].

  • peak_lbt (float) – Peak lookback time [yr].

  • width (float) – Gaussian width [yr].

  • skew (float) – Skewness parameter [dimensionless]. 0 = symmetric, >0 skews toward older ages.

Returns:

SFR at each lookback time [Msun/yr], non-negative.

Return type:

ndarray, shape (n_age,)

Notes

JIT-compatible: yes — all operations use jnp primitives.

Examples

>>> import jax.numpy as jnp
>>> from tengri.components.stellar.sfh import skewnormal
>>> t = jnp.logspace(7, 10.14, 64)
>>> sfr = skewnormal(t, log_peak_sfr=1.5, peak_lbt=3e9, width=1e9, skew=1.0)
>>> sfr.shape
(64,)
tengri.norm(t_lookback: Array, log_peak_sfr: float, peak_lbt: float, width: float) Array

Gaussian (normal) SFH — skewnormal with skew=0.

Parameters:
  • t_lookback (array_like, shape (n_age,)) – Lookback time [yr].

  • log_peak_sfr (float) – log10 of peak SFR [Msun/yr].

  • peak_lbt (float) – Peak lookback time [yr].

  • width (float) – Gaussian width [yr].

Returns:

SFR at each lookback time [Msun/yr], non-negative.

Return type:

ndarray, shape (n_age,)

Notes

JIT-compatible: yes — all operations use jnp primitives.

Examples

>>> import jax.numpy as jnp
>>> from tengri.components.stellar.sfh import gaussian
>>> t = jnp.logspace(7, 10.14, 64)
>>> sfr = gaussian(t, log_peak_sfr=1.5, peak_lbt=3e9, width=1e9)
>>> sfr.shape
(64,)
tengri.lnorm(t_lookback: Array, log_peak_sfr: float, peak_lbt: float, width: float) Array

Log-normal SFH — Gaussian in log10(age) space.

The mode in log10(age) space is peak_lbt, but the peak in linear time is shifted: t_peak_linear = peak_lbt * 10^{-w^2 * ln(10)}. For w=0.3, this is a ~15% shift toward younger ages. peak_lbt is best interpreted as the median lookback time, not the linear peak.

Parameters:
  • t_lookback (array_like, shape (n_age,)) – Lookback time [yr].

  • log_peak_sfr (float) – log10 of peak SFR [Msun/yr].

  • peak_lbt (float) – Peak lookback time [yr]. Converted to log10 internally.

  • width (float) – Width in log10(age) space [dex].

Returns:

SFR at each lookback time [Msun/yr], non-negative.

Return type:

ndarray, shape (n_age,)

Notes

JIT-compatible: yes — all operations use jnp primitives.

Examples

>>> import jax.numpy as jnp
>>> from tengri.components.stellar.sfh import lognormal
>>> t = jnp.logspace(7, 10.14, 64)
>>> sfr = lognormal(t, log_peak_sfr=1.5, peak_lbt=3e9, width=0.3)
>>> sfr.shape
(64,)
tengri.delayed_tau(t_lookback: Array, tau: float, norm: float) Array[source]

Delayed-tau SFH: SFR(t) = norm * t * exp(-t/tau). Peaks at t=tau.

Parameters:
  • t_lookback (array_like, shape (n_age,)) – Lookback time [yr].

  • tau (float) – Timescale [yr].

  • norm (float) – Normalization factor [Msun/yr].

Returns:

SFR at each lookback time [Msun/yr].

Return type:

ndarray, shape (n_age,)

Notes

JIT-compatible: yes — uses jnp primitives.

Examples

>>> import jax.numpy as jnp
>>> from tengri import delayed_tau
>>> t = jnp.logspace(7, 10.14, 64)
>>> sfr = delayed_tau(t, tau=2e9, norm=10.0)
>>> sfr.shape
(64,)
tengri.triweight_burst(t_lookback: Array, log_tpeak_myr: float, log_tmax_myr: float) Array[source]

Triweight burst kernel in log-age space (Zacharegkas+2025).

A compact-support kernel for modeling starburst episodes. The kernel is smooth, has finite support in log-age space, and is designed for composition with smooth SFH models via mass-fraction mixing.

Parameters:
  • t_lookback (array_like, shape (n_age,)) – Lookback time [yr].

  • log_tpeak_myr (float) – log10 of burst peak time [Myr]. Center of the kernel in log-age space.

  • log_tmax_myr (float) – log10 of burst duration [Myr]. Controls the kernel half-width. The kernel has support roughly ±:math:3 imes 10^{log_{mathrm{tmax}}} Myr.

Returns:

Unnormalized burst shape [dimensionless], non-negative. Integrates to ~1.

Return type:

ndarray, shape (n_age,)

Notes

JIT-compatible: yes — all operations use jnp primitives.

This is a shape-only function (unitless, not in Msun/yr). The burst amplitude is set by the burst mixture fraction in the composition step.

The triweight kernel in normalized variable \(u = x/3\) is:

\[K(u) = \frac{35}{96} (1 - u^2)^3, \quad |u| < 1\]

where \(x\) is the normalized time offset from peak, defined as # noqa: E501 \((\log_{10}(t/\mathrm{Myr}) - \log_{10}(t_{\rm peak}/\mathrm{Myr})) / \log_{10}(t_{\rm max}/\mathrm{Myr})\).

The kernel has compact support (finite extent) and is \(C^{\infty}\) smooth. It is superior to Gaussian kernels for burst modeling because it avoids the extended low-level wings that affect neighboring age bins.

References

Examples

>>> import jax.numpy as jnp
>>> from tengri import triweight_burst
>>> t = jnp.linspace(1e6, 13.7e9, 100)
>>> kernel = triweight_burst(t, log_tpeak_myr=2.0, log_tmax_myr=1.0)
>>> kernel.shape
(100,)

SFH Registry

tengri.resolve_sfh(mean_sfh_type: str | list[str], bin_edges_gyr: object = None) tuple[object, dict[str, ParamDef], dict[str, tuple[str, float, float]], dict[str, Any]][source]

Resolve SFH specification to a composed function + params.

Parameters:
  • mean_sfh_type (str or list[str]) – Model name(s). E.g., "tsnorm" or ["tsnorm", "burst", "field"].

  • bin_edges_gyr (array-like, shape (n_bins+1,), optional) – Custom age bin edges [Gyr] for continuity and dirichlet models. When provided, overrides the default DEFAULT_BIN_EDGES_GYR. Use make_agebins_from_zred to generate redshift-appropriate edges. Ignored for non-nonparametric models. Default None (use model default).

Returns:

  • composed_fn (callable) – Pure JAX function: fn(t_lookback, **all_internal_kwargs) -> SFR [Msun/yr].

  • merged_params (dict[str, ParamDef]) – All fittable parameters across selected models.

  • merged_param_map (dict[str, tuple[str, float, float]]) – Public name -> (internal, scale, offset) for all params.

  • merged_settings (dict[str, Any]) – Non-fittable settings (e.g., sfh_field_ngrid).

Raises:
  • KeyError – If a model name is not in the registry.

  • ValueError – If composition constraints are violated (e.g., >1 burst, no smooth component).

Notes

JIT-compatible: yes — returns a JIT-compatible closure (composed_fn).

Composition rules:

  • Additive: smooth models summed. E.g., ["tsnorm", "dpl"] yields SFR_total = SFR_tsnorm + SFR_dpl.

  • Mixture (burst): mass-fraction weighted, replaces smooth. E.g., ["tsnorm", "burst"] yields SFR = (1-f)*SFR_tsnorm + f*burst_shape.

  • Modulator (field): multiplicative GP modulation. E.g., ["tsnorm", "field"] yields SFR = SFR_tsnorm * exp(gp_x - K_0/2).

Auto-swap: dense_basisdense_basis_pure if burst or field is present (to avoid SFR constraint interference with composition).

Examples

>>> from tengri import resolve_sfh
>>> fn, params, param_map, settings = resolve_sfh("dpl")
>>> "sfh_dpl_alpha" in params
True
>>> fn
<function ...>
tengri.SFH_REGISTRY = {'buat08': SFHRegistryEntry(callable=SFHModelSpec(name='buat08', fn=<function buat08>, params={'sfh_buat08_velocity_km_s': ParamDef(description='Rotational velocity (km/s), range [40, 360]', bound_check=<function <lambda>>, bound_error='must have 40 <= lo and hi <= 360', default=Uniform(80.0, 360.0))}, settings={}, internal_param_map={'sfh_buat08_velocity_km_s': ('velocity_km_s', 1.0, 0.0)}, composition_type='additive'), citation='Buat et al. 2008', status='production', short_doc='Velocity-parameterized SFH (Buat et al.)'), 'burst': SFHRegistryEntry(callable=SFHModelSpec(name='burst', fn=<function triweight_burst>, params={'sfh_burst_log_fburst': ParamDef(description='log10 burst mass fraction', bound_check=<function <lambda>>, bound_error='must have hi < 0 (fraction < 1)', default=Uniform(-3.0, -0.1)), 'sfh_burst_log_tpeak_myr': ParamDef(description='log10 burst peak time (Myr)', bound_check=<function <lambda>>, bound_error='', default=Uniform(0.0, 3.0)), 'sfh_burst_log_tmax_myr': ParamDef(description='log10 burst duration (Myr)', bound_check=<function <lambda>>, bound_error='', default=Uniform(1.0, 4.0))}, settings={}, internal_param_map={'sfh_burst_log_fburst': ('log_fburst', 1.0, 0.0), 'sfh_burst_log_tpeak_myr': ('log_tpeak_myr', 1.0, 0.0), 'sfh_burst_log_tmax_myr': ('log_tmax_myr', 1.0, 0.0)}, composition_type='mixture'), citation='', status='production', short_doc='Triweight burst kernel (Zacharegkas+2025)'), 'const': SFHRegistryEntry(callable=SFHModelSpec(name='const', fn=<function constant>, params={'sfh_const_log_sfr': ParamDef(description='log10 SFR', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 3.0)), 'sfh_const_start_gyr': ParamDef(description='Lookback to SF onset (Gyr): when did SF start?', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Fixed(14.0)), 'sfh_const_end_gyr': ParamDef(description='Lookback to SF cessation (Gyr): when did SF stop? (0 = ongoing)', bound_check=<function <lambda>>, bound_error='must have lo >= 0', default=Fixed(0.0))}, settings={}, internal_param_map={'sfh_const_log_sfr': ('log_sfr', 1.0, 0.0), 'sfh_const_start_gyr': ('end', 1000000000.0, 0.0), 'sfh_const_end_gyr': ('start', 1000000000.0, 0.0)}, composition_type='additive'), citation='', status='production', short_doc='Constant SFR'), 'const_exp': SFHRegistryEntry(callable=SFHModelSpec(name='const_exp', fn=<function constant_then_exponential>, params={'sfh_cexp_log_sfr': ParamDef(description='log10 constant SFR before quenching (Msun/yr)', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 3.0)), 'sfh_cexp_tau_gyr': ParamDef(description='Post-quench e-folding timescale (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.1, 10.0)), 'sfh_cexp_quench_gyr': ParamDef(description='Lookback time when quenching began (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.01, 10.0)), 'sfh_cexp_age_gyr': ParamDef(description='Galaxy age / lookback to formation (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 13.0))}, settings={}, internal_param_map={'sfh_cexp_log_sfr': ('log_sfr', 1.0, 0.0), 'sfh_cexp_tau_gyr': ('tau', 1000000000.0, 0.0), 'sfh_cexp_quench_gyr': ('quench_age', 1000000000.0, 0.0), 'sfh_cexp_age_gyr': ('age', 1000000000.0, 0.0)}, composition_type='additive'), citation='', status='production', short_doc='Constant SFR then exponential quenching'), 'constant_then_exponential': SFHRegistryEntry(callable=SFHModelSpec(name='const_exp', fn=<function constant_then_exponential>, params={'sfh_cexp_log_sfr': ParamDef(description='log10 constant SFR before quenching (Msun/yr)', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 3.0)), 'sfh_cexp_tau_gyr': ParamDef(description='Post-quench e-folding timescale (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.1, 10.0)), 'sfh_cexp_quench_gyr': ParamDef(description='Lookback time when quenching began (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.01, 10.0)), 'sfh_cexp_age_gyr': ParamDef(description='Galaxy age / lookback to formation (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 13.0))}, settings={}, internal_param_map={'sfh_cexp_log_sfr': ('log_sfr', 1.0, 0.0), 'sfh_cexp_tau_gyr': ('tau', 1000000000.0, 0.0), 'sfh_cexp_quench_gyr': ('quench_age', 1000000000.0, 0.0), 'sfh_cexp_age_gyr': ('age', 1000000000.0, 0.0)}, composition_type='additive'), citation='', status='production', short_doc='Constant SFR then exponential quenching'), 'continuity': SFHRegistryEntry(callable=SFHModelSpec(name='continuity', fn=<function continuity>, params={'sfh_cont_log_total_mass': ParamDef(description='log10 total stellar mass formed (Msun)', bound_check=<function <lambda>>, bound_error='', default=Uniform(8.0, 12.0)), 'sfh_cont_ratio_0': ParamDef(description='log10 SFR ratio bin 0/1', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 1.0)), 'sfh_cont_ratio_1': ParamDef(description='log10 SFR ratio bin 1/2', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 1.0)), 'sfh_cont_ratio_2': ParamDef(description='log10 SFR ratio bin 2/3', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 1.0)), 'sfh_cont_ratio_3': ParamDef(description='log10 SFR ratio bin 3/4', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 1.0)), 'sfh_cont_ratio_4': ParamDef(description='log10 SFR ratio bin 4/5', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 1.0)), 'sfh_cont_ratio_5': ParamDef(description='log10 SFR ratio bin 5/6', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 1.0))}, settings={}, internal_param_map={'sfh_cont_log_total_mass': ('log_total_mass', 1.0, 0.0), 'sfh_cont_ratio_0': ('ratio_0', 1.0, 0.0), 'sfh_cont_ratio_1': ('ratio_1', 1.0, 0.0), 'sfh_cont_ratio_2': ('ratio_2', 1.0, 0.0), 'sfh_cont_ratio_3': ('ratio_3', 1.0, 0.0), 'sfh_cont_ratio_4': ('ratio_4', 1.0, 0.0), 'sfh_cont_ratio_5': ('ratio_5', 1.0, 0.0)}, composition_type='additive'), citation='Leja et al. 2019 (ApJ 876, 39)', status='production', short_doc='Non-parametric piecewise continuity SFH (Leja+19)'), 'continuity_flex': SFHRegistryEntry(callable=SFHModelSpec(name='continuity_flex', fn=<function continuity_flex>, params={'sfh_cflex_log_total_mass': ParamDef(description='log10 total stellar mass formed (Msun)', bound_check=<function <lambda>>, bound_error='', default=Uniform(8.0, 12.0)), 'sfh_cflex_ratio_young': ParamDef(description='log10(SFR_young / SFR_flex[0])', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 1.0)), 'sfh_cflex_flex_0': ParamDef(description='log10 flex bin SFR ratio 0 (controls bin width)', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 1.0)), 'sfh_cflex_flex_1': ParamDef(description='log10 flex bin SFR ratio 1 (controls bin width)', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 1.0)), 'sfh_cflex_flex_2': ParamDef(description='log10 flex bin SFR ratio 2 (controls bin width)', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 1.0)), 'sfh_cflex_ratio_old': ParamDef(description='log10(SFR_old / SFR_flex[N])', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 1.0))}, settings={'sfh_cflex_n_flex': 3, 'sfh_cflex_anchor_gyr': [0.0316, 5.012, 13.7]}, internal_param_map={'sfh_cflex_log_total_mass': ('log_total_mass', 1.0, 0.0), 'sfh_cflex_ratio_young': ('ratio_young', 1.0, 0.0), 'sfh_cflex_flex_0': ('flex_0', 1.0, 0.0), 'sfh_cflex_flex_1': ('flex_1', 1.0, 0.0), 'sfh_cflex_flex_2': ('flex_2', 1.0, 0.0), 'sfh_cflex_ratio_old': ('ratio_old', 1.0, 0.0)}, composition_type='additive'), citation='', status='production', short_doc='Non-parametric continuity + flexible bin edges'), 'db': SFHRegistryEntry(callable=SFHModelSpec(name='dense_basis', fn=<function dense_basis>, params={'sfh_db_log_total_mass': ParamDef(description='log10 total stellar mass formed (Msun)', bound_check=<function <lambda>>, bound_error='', default=Uniform(8.0, 12.0)), 'sfh_db_log_sfr_inst': ParamDef(description='log10 instantaneous SFR at observation (Msun/yr)', bound_check=<function <lambda>>, bound_error='', default=Uniform(-2.0, 3.0)), 'sfh_db_tx_frac_0': ParamDef(description='Cosmic time fraction at 25% mass', bound_check=<function <dictcomp>.<lambda>>, bound_error='must be in [0, 1]', default=Uniform(0.05, 0.95)), 'sfh_db_tx_frac_1': ParamDef(description='Cosmic time fraction at 50% mass', bound_check=<function <dictcomp>.<lambda>>, bound_error='must be in [0, 1]', default=Uniform(0.05, 0.95)), 'sfh_db_tx_frac_2': ParamDef(description='Cosmic time fraction at 75% mass', bound_check=<function <dictcomp>.<lambda>>, bound_error='must be in [0, 1]', default=Uniform(0.05, 0.95))}, settings={'sfh_db_nparam': 3, 'sfh_db_age_universe_gyr': 13.47}, internal_param_map={'sfh_db_log_total_mass': ('log_total_mass', 1.0, 0.0), 'sfh_db_log_sfr_inst': ('log_sfr_inst', 1.0, 0.0), 'sfh_db_tx_frac_0': ('tx_frac_0', 1.0, 0.0), 'sfh_db_tx_frac_1': ('tx_frac_1', 1.0, 0.0), 'sfh_db_tx_frac_2': ('tx_frac_2', 1.0, 0.0)}, composition_type='additive'), citation='', status='production', short_doc='Dense-basis non-parametric SFH (Iyer+19)'), 'dbp': SFHRegistryEntry(callable=SFHModelSpec(name='dense_basis_pure', fn=<function dense_basis_pure>, params={'sfh_dbp_log_total_mass': ParamDef(description='log10 total stellar mass formed (Msun)', bound_check=<function <lambda>>, bound_error='', default=Uniform(8.0, 12.0)), 'sfh_dbp_tx_frac_0': ParamDef(description='Cosmic time fraction at 25% mass', bound_check=<function <dictcomp>.<lambda>>, bound_error='must be in [0, 1]', default=Uniform(0.05, 0.95)), 'sfh_dbp_tx_frac_1': ParamDef(description='Cosmic time fraction at 50% mass', bound_check=<function <dictcomp>.<lambda>>, bound_error='must be in [0, 1]', default=Uniform(0.05, 0.95)), 'sfh_dbp_tx_frac_2': ParamDef(description='Cosmic time fraction at 75% mass', bound_check=<function <dictcomp>.<lambda>>, bound_error='must be in [0, 1]', default=Uniform(0.05, 0.95))}, settings={'sfh_dbp_nparam': 3, 'sfh_dbp_age_universe_gyr': 13.47}, internal_param_map={'sfh_dbp_log_total_mass': ('log_total_mass', 1.0, 0.0), 'sfh_dbp_tx_frac_0': ('tx_frac_0', 1.0, 0.0), 'sfh_dbp_tx_frac_1': ('tx_frac_1', 1.0, 0.0), 'sfh_dbp_tx_frac_2': ('tx_frac_2', 1.0, 0.0)}, composition_type='additive'), citation='', status='production', short_doc='Dense-basis SFH without GP constraints'), 'delayed_bq': SFHRegistryEntry(callable=SFHModelSpec(name='delayed_bq', fn=<function delayed_bq>, params={'sfh_delayed_bq_tau_main_gyr': ParamDef(description='e-folding timescale of main component (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.1, 10.0)), 'sfh_delayed_bq_age_main_gyr': ParamDef(description='Galaxy age / lookback to formation (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 13.0)), 'sfh_delayed_bq_age_bq_gyr': ParamDef(description='Lookback time of burst/quench onset (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.01, 5.0)), 'sfh_delayed_bq_r_sfr': ParamDef(description='SFR ratio after/before burst/quench', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.01, 10.0))}, settings={}, internal_param_map={'sfh_delayed_bq_tau_main_gyr': ('tau_main_yr', 1000000000.0, 0.0), 'sfh_delayed_bq_age_main_gyr': ('age_main_yr', 1000000000.0, 0.0), 'sfh_delayed_bq_age_bq_gyr': ('age_bq_yr', 1000000000.0, 0.0), 'sfh_delayed_bq_r_sfr': ('r_sfr', 1.0, 0.0)}, composition_type='additive'), citation='', status='production', short_doc='Delayed tau with burst/quench (Ciesla et al.)'), 'dense_basis': SFHRegistryEntry(callable=SFHModelSpec(name='dense_basis', fn=<function dense_basis>, params={'sfh_db_log_total_mass': ParamDef(description='log10 total stellar mass formed (Msun)', bound_check=<function <lambda>>, bound_error='', default=Uniform(8.0, 12.0)), 'sfh_db_log_sfr_inst': ParamDef(description='log10 instantaneous SFR at observation (Msun/yr)', bound_check=<function <lambda>>, bound_error='', default=Uniform(-2.0, 3.0)), 'sfh_db_tx_frac_0': ParamDef(description='Cosmic time fraction at 25% mass', bound_check=<function <dictcomp>.<lambda>>, bound_error='must be in [0, 1]', default=Uniform(0.05, 0.95)), 'sfh_db_tx_frac_1': ParamDef(description='Cosmic time fraction at 50% mass', bound_check=<function <dictcomp>.<lambda>>, bound_error='must be in [0, 1]', default=Uniform(0.05, 0.95)), 'sfh_db_tx_frac_2': ParamDef(description='Cosmic time fraction at 75% mass', bound_check=<function <dictcomp>.<lambda>>, bound_error='must be in [0, 1]', default=Uniform(0.05, 0.95))}, settings={'sfh_db_nparam': 3, 'sfh_db_age_universe_gyr': 13.47}, internal_param_map={'sfh_db_log_total_mass': ('log_total_mass', 1.0, 0.0), 'sfh_db_log_sfr_inst': ('log_sfr_inst', 1.0, 0.0), 'sfh_db_tx_frac_0': ('tx_frac_0', 1.0, 0.0), 'sfh_db_tx_frac_1': ('tx_frac_1', 1.0, 0.0), 'sfh_db_tx_frac_2': ('tx_frac_2', 1.0, 0.0)}, composition_type='additive'), citation='', status='production', short_doc='Dense-basis non-parametric SFH (Iyer+19)'), 'dense_basis_pure': SFHRegistryEntry(callable=SFHModelSpec(name='dense_basis_pure', fn=<function dense_basis_pure>, params={'sfh_dbp_log_total_mass': ParamDef(description='log10 total stellar mass formed (Msun)', bound_check=<function <lambda>>, bound_error='', default=Uniform(8.0, 12.0)), 'sfh_dbp_tx_frac_0': ParamDef(description='Cosmic time fraction at 25% mass', bound_check=<function <dictcomp>.<lambda>>, bound_error='must be in [0, 1]', default=Uniform(0.05, 0.95)), 'sfh_dbp_tx_frac_1': ParamDef(description='Cosmic time fraction at 50% mass', bound_check=<function <dictcomp>.<lambda>>, bound_error='must be in [0, 1]', default=Uniform(0.05, 0.95)), 'sfh_dbp_tx_frac_2': ParamDef(description='Cosmic time fraction at 75% mass', bound_check=<function <dictcomp>.<lambda>>, bound_error='must be in [0, 1]', default=Uniform(0.05, 0.95))}, settings={'sfh_dbp_nparam': 3, 'sfh_dbp_age_universe_gyr': 13.47}, internal_param_map={'sfh_dbp_log_total_mass': ('log_total_mass', 1.0, 0.0), 'sfh_dbp_tx_frac_0': ('tx_frac_0', 1.0, 0.0), 'sfh_dbp_tx_frac_1': ('tx_frac_1', 1.0, 0.0), 'sfh_dbp_tx_frac_2': ('tx_frac_2', 1.0, 0.0)}, composition_type='additive'), citation='', status='production', short_doc='Dense-basis SFH without GP constraints'), 'dexp': SFHRegistryEntry(callable=SFHModelSpec(name='dexp', fn=<function delayed_exponential>, params={'sfh_dexp_log_peak_sfr': ParamDef(description='log10 peak SFR', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 3.0)), 'sfh_dexp_tau_gyr': ParamDef(description='Timescale (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.1, 10.0)), 'sfh_dexp_start_gyr': ParamDef(description='Start lookback (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo >= 0', default=Fixed(0.0))}, settings={}, internal_param_map={'sfh_dexp_log_peak_sfr': ('log_peak_sfr', 1.0, 0.0), 'sfh_dexp_tau_gyr': ('tau', 1000000000.0, 0.0), 'sfh_dexp_start_gyr': ('start', 1000000000.0, 0.0)}, composition_type='additive'), citation='', status='production', short_doc='Delayed exponential SFH'), 'dirichlet': SFHRegistryEntry(callable=SFHModelSpec(name='dirichlet', fn=<function dirichlet>, params={'sfh_dir_log_total_mass': ParamDef(description='log10 total stellar mass formed (Msun)', bound_check=<function <lambda>>, bound_error='', default=Uniform(8.0, 12.0)), 'sfh_dir_z_0': ParamDef(description='Dirichlet stick-breaking variable 0', bound_check=<function <dictcomp>.<lambda>>, bound_error='must be in [0, 1]', default=Uniform(0.01, 0.99)), 'sfh_dir_z_1': ParamDef(description='Dirichlet stick-breaking variable 1', bound_check=<function <dictcomp>.<lambda>>, bound_error='must be in [0, 1]', default=Uniform(0.01, 0.99)), 'sfh_dir_z_2': ParamDef(description='Dirichlet stick-breaking variable 2', bound_check=<function <dictcomp>.<lambda>>, bound_error='must be in [0, 1]', default=Uniform(0.01, 0.99)), 'sfh_dir_z_3': ParamDef(description='Dirichlet stick-breaking variable 3', bound_check=<function <dictcomp>.<lambda>>, bound_error='must be in [0, 1]', default=Uniform(0.01, 0.99)), 'sfh_dir_z_4': ParamDef(description='Dirichlet stick-breaking variable 4', bound_check=<function <dictcomp>.<lambda>>, bound_error='must be in [0, 1]', default=Uniform(0.01, 0.99)), 'sfh_dir_z_5': ParamDef(description='Dirichlet stick-breaking variable 5', bound_check=<function <dictcomp>.<lambda>>, bound_error='must be in [0, 1]', default=Uniform(0.01, 0.99))}, settings={}, internal_param_map={'sfh_dir_log_total_mass': ('log_total_mass', 1.0, 0.0), 'sfh_dir_z_0': ('z_frac_0', 1.0, 0.0), 'sfh_dir_z_1': ('z_frac_1', 1.0, 0.0), 'sfh_dir_z_2': ('z_frac_2', 1.0, 0.0), 'sfh_dir_z_3': ('z_frac_3', 1.0, 0.0), 'sfh_dir_z_4': ('z_frac_4', 1.0, 0.0), 'sfh_dir_z_5': ('z_frac_5', 1.0, 0.0)}, composition_type='additive'), citation='Leja et al. 2017 (ApJ 837, 170)', status='production', short_doc='Non-parametric Dirichlet SFH (Leja+17)'), 'dpl': SFHRegistryEntry(callable=SFHModelSpec(name='dpl', fn=<function dpl>, params={'sfh_dpl_alpha': ParamDef(description='DPL falling slope', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.1, 5.0)), 'sfh_dpl_beta': ParamDef(description='DPL rising slope', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.1, 3.0)), 'sfh_dpl_tau_gyr': ParamDef(description='DPL turnover time (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.1, 12.0)), 'sfh_dpl_log_peak_sfr': ParamDef(description='log10 peak SFR', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 3.0))}, settings={}, internal_param_map={'sfh_dpl_alpha': ('alpha', 1.0, 0.0), 'sfh_dpl_beta': ('beta', 1.0, 0.0), 'sfh_dpl_tau_gyr': ('tau', 1000000000.0, 0.0), 'sfh_dpl_log_peak_sfr': ('log_peak_sfr', 1.0, 0.0)}, composition_type='additive'), citation='Carnall et al. 2018 (MNRAS 480, 4379)', status='production', short_doc='Double power-law SFH'), 'exp': SFHRegistryEntry(callable=SFHModelSpec(name='exp', fn=<function exponential>, params={'sfh_exp_log_peak_sfr': ParamDef(description='log10 peak SFR', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 3.0)), 'sfh_exp_tau_gyr': ParamDef(description='e-folding timescale (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.1, 10.0)), 'sfh_exp_start_gyr': ParamDef(description='Start lookback (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo >= 0', default=Fixed(0.0))}, settings={}, internal_param_map={'sfh_exp_log_peak_sfr': ('log_peak_sfr', 1.0, 0.0), 'sfh_exp_tau_gyr': ('tau', 1000000000.0, 0.0), 'sfh_exp_start_gyr': ('start', 1000000000.0, 0.0)}, composition_type='additive'), citation='', status='production', short_doc='Exponential rise SFH'), 'field': SFHRegistryEntry(callable=SFHModelSpec(name='field', fn=<function _field_fn_placeholder>, params={'sfh_field_psd_sigma': ParamDef(description='PSD amplitude (dex)', bound_check=<function <lambda>>, bound_error='must have lo >= 0', default=Uniform(0.01, 1.0)), 'sfh_field_psd_tau_myr': ParamDef(description='PSD timescale (Myr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(10.0, 500.0))}, settings={'sfh_field_ngrid': 256, 'sfh_field_model': 'drw'}, internal_param_map={'sfh_field_psd_sigma': ('psd_sigma', 1.0, 0.0), 'sfh_field_psd_tau_myr': ('psd_tau_yr', 1000000.0, 0.0)}, composition_type='modulator'), citation='', status='production', short_doc='GP field modulator with DRW power spectrum'), 'gaussian_burst': SFHRegistryEntry(callable=SFHModelSpec(name='gaussian_burst', fn=<function gaussian_burst>, params={'sfh_gaussian_burst_amplitude': ParamDef(description='Peak burst SFR (Msun/yr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.01, 100.0)), 'sfh_gaussian_burst_t_peak_gyr': ParamDef(description='Burst peak age / lookback time (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.01, 13.0)), 'sfh_gaussian_burst_sigma_gyr': ParamDef(description='Gaussian width / standard deviation (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.01, 5.0))}, settings={}, internal_param_map={'sfh_gaussian_burst_amplitude': ('amplitude', 1.0, 0.0), 'sfh_gaussian_burst_t_peak_gyr': ('t_peak', 1000000000.0, 0.0), 'sfh_gaussian_burst_sigma_gyr': ('sigma', 1000000000.0, 0.0)}, composition_type='additive'), citation='Robotham et al. 2020 (arXiv:2002.06980) ProSpect', status='production', short_doc='Gaussian-in-age burst (Robotham+2020)'), 'gaussian_sfh': SFHRegistryEntry(callable=SFHModelSpec(name='norm', fn=<function gaussian>, params={'sfh_norm_log_peak_sfr': ParamDef(description='log10 peak SFR', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 3.0)), 'sfh_norm_peak_lbt_gyr': ParamDef(description='Peak lookback time (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 12.0)), 'sfh_norm_width_gyr': ParamDef(description='Gaussian width (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.2, 5.0))}, settings={}, internal_param_map={'sfh_norm_log_peak_sfr': ('log_peak_sfr', 1.0, 0.0), 'sfh_norm_peak_lbt_gyr': ('peak_lbt', 1000000000.0, 0.0), 'sfh_norm_width_gyr': ('width', 1000000000.0, 0.0)}, composition_type='additive'), citation='', status='production', short_doc='Gaussian SFH'), 'lnorm': SFHRegistryEntry(callable=SFHModelSpec(name='lnorm', fn=<function lognormal>, params={'sfh_lnorm_log_peak_sfr': ParamDef(description='log10 peak SFR', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 3.0)), 'sfh_lnorm_peak_lbt_gyr': ParamDef(description='Peak lookback time (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 12.0)), 'sfh_lnorm_width_gyr': ParamDef(description='Log-space width (dex)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.1, 2.0))}, settings={}, internal_param_map={'sfh_lnorm_log_peak_sfr': ('log_peak_sfr', 1.0, 0.0), 'sfh_lnorm_peak_lbt_gyr': ('peak_lbt', 1000000000.0, 0.0), 'sfh_lnorm_width_gyr': ('width', 1.0, 0.0)}, composition_type='additive'), citation='', status='production', short_doc='Log-normal SFH'), 'lognormal_sfh': SFHRegistryEntry(callable=SFHModelSpec(name='lnorm', fn=<function lognormal>, params={'sfh_lnorm_log_peak_sfr': ParamDef(description='log10 peak SFR', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 3.0)), 'sfh_lnorm_peak_lbt_gyr': ParamDef(description='Peak lookback time (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 12.0)), 'sfh_lnorm_width_gyr': ParamDef(description='Log-space width (dex)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.1, 2.0))}, settings={}, internal_param_map={'sfh_lnorm_log_peak_sfr': ('log_peak_sfr', 1.0, 0.0), 'sfh_lnorm_peak_lbt_gyr': ('peak_lbt', 1000000000.0, 0.0), 'sfh_lnorm_width_gyr': ('width', 1.0, 0.0)}, composition_type='additive'), citation='', status='production', short_doc='Log-normal SFH'), 'norm': SFHRegistryEntry(callable=SFHModelSpec(name='norm', fn=<function gaussian>, params={'sfh_norm_log_peak_sfr': ParamDef(description='log10 peak SFR', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 3.0)), 'sfh_norm_peak_lbt_gyr': ParamDef(description='Peak lookback time (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 12.0)), 'sfh_norm_width_gyr': ParamDef(description='Gaussian width (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.2, 5.0))}, settings={}, internal_param_map={'sfh_norm_log_peak_sfr': ('log_peak_sfr', 1.0, 0.0), 'sfh_norm_peak_lbt_gyr': ('peak_lbt', 1000000000.0, 0.0), 'sfh_norm_width_gyr': ('width', 1000000000.0, 0.0)}, composition_type='additive'), citation='', status='production', short_doc='Gaussian SFH'), 'periodic': SFHRegistryEntry(callable=SFHModelSpec(name='periodic', fn=<function periodic>, params={'sfh_periodic_delta_bursts_gyr': ParamDef(description='Spacing between burst onsets (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.01, 1.0)), 'sfh_periodic_tau_bursts_gyr': ParamDef(description='Duration/e-folding timescale of each burst (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.001, 0.5)), 'sfh_periodic_burst_type': ParamDef(description='Burst type: 0=exponential, 1=delayed, 2=rectangular', bound_check=<function <lambda>>, bound_error='must be 0, 1, or 2', default=Fixed(0.0)), 'sfh_periodic_age_gyr': ParamDef(description='Galaxy age / lookback to formation (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 13.0))}, settings={}, internal_param_map={'sfh_periodic_delta_bursts_gyr': ('delta_bursts_yr', 1000000000.0, 0.0), 'sfh_periodic_tau_bursts_gyr': ('tau_bursts_yr', 1000000000.0, 0.0), 'sfh_periodic_burst_type': ('burst_type', 1.0, 0.0), 'sfh_periodic_age_gyr': ('age_yr', 1000000000.0, 0.0)}, composition_type='additive'), citation='', status='production', short_doc='Periodic SF events (Ciesla et al.)'), 'psb': SFHRegistryEntry(callable=SFHModelSpec(name='psb', fn=<function psb_wild2020>, params={'sfh_psb_log_peak_sfr': ParamDef(description='log10 overall SFR normalization (Msun/yr)', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 3.0)), 'sfh_psb_age_gyr': ParamDef(description='Galaxy age / lookback to formation (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 13.0)), 'sfh_psb_tau_gyr': ParamDef(description='Old-component e-folding timescale (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.1, 10.0)), 'sfh_psb_burstage_gyr': ParamDef(description='Lookback time of burst onset (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.01, 5.0)), 'sfh_psb_alpha': ParamDef(description='DPL burst falling slope', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 5.0)), 'sfh_psb_beta': ParamDef(description='DPL burst rising slope', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 5.0)), 'sfh_psb_fburst': ParamDef(description='Burst mass fraction', bound_check=<function <lambda>>, bound_error='must be in [0, 1]', default=Uniform(0.01, 0.99))}, settings={}, internal_param_map={'sfh_psb_log_peak_sfr': ('log_peak_sfr', 1.0, 0.0), 'sfh_psb_age_gyr': ('age', 1000000000.0, 0.0), 'sfh_psb_tau_gyr': ('tau', 1000000000.0, 0.0), 'sfh_psb_burstage_gyr': ('burstage', 1000000000.0, 0.0), 'sfh_psb_alpha': ('alpha', 1.0, 0.0), 'sfh_psb_beta': ('beta', 1.0, 0.0), 'sfh_psb_fburst': ('fburst', 1.0, 0.0)}, composition_type='additive'), citation='Wild et al. 2020 (MNRAS 494, 529)', status='production', short_doc='Post-starburst SFH (Wild et al.)'), 'psb_wild2020': SFHRegistryEntry(callable=SFHModelSpec(name='psb', fn=<function psb_wild2020>, params={'sfh_psb_log_peak_sfr': ParamDef(description='log10 overall SFR normalization (Msun/yr)', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 3.0)), 'sfh_psb_age_gyr': ParamDef(description='Galaxy age / lookback to formation (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 13.0)), 'sfh_psb_tau_gyr': ParamDef(description='Old-component e-folding timescale (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.1, 10.0)), 'sfh_psb_burstage_gyr': ParamDef(description='Lookback time of burst onset (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.01, 5.0)), 'sfh_psb_alpha': ParamDef(description='DPL burst falling slope', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 5.0)), 'sfh_psb_beta': ParamDef(description='DPL burst rising slope', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 5.0)), 'sfh_psb_fburst': ParamDef(description='Burst mass fraction', bound_check=<function <lambda>>, bound_error='must be in [0, 1]', default=Uniform(0.01, 0.99))}, settings={}, internal_param_map={'sfh_psb_log_peak_sfr': ('log_peak_sfr', 1.0, 0.0), 'sfh_psb_age_gyr': ('age', 1000000000.0, 0.0), 'sfh_psb_tau_gyr': ('tau', 1000000000.0, 0.0), 'sfh_psb_burstage_gyr': ('burstage', 1000000000.0, 0.0), 'sfh_psb_alpha': ('alpha', 1.0, 0.0), 'sfh_psb_beta': ('beta', 1.0, 0.0), 'sfh_psb_fburst': ('fburst', 1.0, 0.0)}, composition_type='additive'), citation='Wild et al. 2020 (MNRAS 494, 529)', status='production', short_doc='Post-starburst SFH (Wild et al.)'), 'skewnormal_sfh': SFHRegistryEntry(callable=SFHModelSpec(name='snorm', fn=<function skewnormal>, params={'sfh_snorm_log_peak_sfr': ParamDef(description='log10 peak SFR', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 3.0)), 'sfh_snorm_peak_lbt_gyr': ParamDef(description='Peak lookback time (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 12.0)), 'sfh_snorm_width_gyr': ParamDef(description='Gaussian width (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.2, 5.0)), 'sfh_snorm_skew': ParamDef(description='Skewness', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 1.0))}, settings={}, internal_param_map={'sfh_snorm_log_peak_sfr': ('log_peak_sfr', 1.0, 0.0), 'sfh_snorm_peak_lbt_gyr': ('peak_lbt', 1000000000.0, 0.0), 'sfh_snorm_width_gyr': ('width', 1000000000.0, 0.0), 'sfh_snorm_skew': ('skew', 1.0, 0.0)}, composition_type='additive'), citation='Bellstedt et al. 2020 (arXiv:2005.11917)', status='production', short_doc='Skew-normal SFH (Bellstedt+2020)'), 'snorm': SFHRegistryEntry(callable=SFHModelSpec(name='snorm', fn=<function skewnormal>, params={'sfh_snorm_log_peak_sfr': ParamDef(description='log10 peak SFR', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 3.0)), 'sfh_snorm_peak_lbt_gyr': ParamDef(description='Peak lookback time (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 12.0)), 'sfh_snorm_width_gyr': ParamDef(description='Gaussian width (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.2, 5.0)), 'sfh_snorm_skew': ParamDef(description='Skewness', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 1.0))}, settings={}, internal_param_map={'sfh_snorm_log_peak_sfr': ('log_peak_sfr', 1.0, 0.0), 'sfh_snorm_peak_lbt_gyr': ('peak_lbt', 1000000000.0, 0.0), 'sfh_snorm_width_gyr': ('width', 1000000000.0, 0.0), 'sfh_snorm_skew': ('skew', 1.0, 0.0)}, composition_type='additive'), citation='Bellstedt et al. 2020 (arXiv:2005.11917)', status='production', short_doc='Skew-normal SFH (Bellstedt+2020)'), 'snorm_burst': SFHRegistryEntry(callable=SFHModelSpec(name='snorm_burst', fn=<function snorm_burst>, params={'sfh_snorm_burst_log_peak_sfr': ParamDef(description='log10 peak SFR of skew-normal component', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 3.0)), 'sfh_snorm_burst_peak_lbt_gyr': ParamDef(description='Peak lookback time (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 12.0)), 'sfh_snorm_burst_width_gyr': ParamDef(description='Gaussian width (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.2, 5.0)), 'sfh_snorm_burst_skew': ParamDef(description='Skewness', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 1.0)), 'sfh_snorm_burst_burst_sfr': ParamDef(description='Constant burst SFR amplitude (Msun/yr)', bound_check=<function <lambda>>, bound_error='must have lo >= 0', default=Fixed(0.0)), 'sfh_snorm_burst_burst_age_gyr': ParamDef(description='Burst lookback duration (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Fixed(0.1))}, settings={}, internal_param_map={'sfh_snorm_burst_log_peak_sfr': ('log_peak_sfr', 1.0, 0.0), 'sfh_snorm_burst_peak_lbt_gyr': ('peak_lbt', 1000000000.0, 0.0), 'sfh_snorm_burst_width_gyr': ('width', 1000000000.0, 0.0), 'sfh_snorm_burst_skew': ('skew', 1.0, 0.0), 'sfh_snorm_burst_burst_sfr': ('burst_sfr', 1.0, 0.0), 'sfh_snorm_burst_burst_age_gyr': ('burst_age', 1000000000.0, 0.0)}, composition_type='additive'), citation='Robotham et al. 2020 (arXiv:2002.06980) ProSpect', status='production', short_doc='Skew-normal + flat recent burst (ProSpect)'), 'snorm_burst_sfh': SFHRegistryEntry(callable=SFHModelSpec(name='snorm_burst', fn=<function snorm_burst>, params={'sfh_snorm_burst_log_peak_sfr': ParamDef(description='log10 peak SFR of skew-normal component', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 3.0)), 'sfh_snorm_burst_peak_lbt_gyr': ParamDef(description='Peak lookback time (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 12.0)), 'sfh_snorm_burst_width_gyr': ParamDef(description='Gaussian width (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.2, 5.0)), 'sfh_snorm_burst_skew': ParamDef(description='Skewness', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 1.0)), 'sfh_snorm_burst_burst_sfr': ParamDef(description='Constant burst SFR amplitude (Msun/yr)', bound_check=<function <lambda>>, bound_error='must have lo >= 0', default=Fixed(0.0)), 'sfh_snorm_burst_burst_age_gyr': ParamDef(description='Burst lookback duration (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Fixed(0.1))}, settings={}, internal_param_map={'sfh_snorm_burst_log_peak_sfr': ('log_peak_sfr', 1.0, 0.0), 'sfh_snorm_burst_peak_lbt_gyr': ('peak_lbt', 1000000000.0, 0.0), 'sfh_snorm_burst_width_gyr': ('width', 1000000000.0, 0.0), 'sfh_snorm_burst_skew': ('skew', 1.0, 0.0), 'sfh_snorm_burst_burst_sfr': ('burst_sfr', 1.0, 0.0), 'sfh_snorm_burst_burst_age_gyr': ('burst_age', 1000000000.0, 0.0)}, composition_type='additive'), citation='Robotham et al. 2020 (arXiv:2002.06980) ProSpect', status='production', short_doc='Skew-normal + flat recent burst (ProSpect)'), 'snorm_trunc_burst_sfh': SFHRegistryEntry(callable=SFHModelSpec(name='tsnorm_burst', fn=<function snorm_trunc_burst>, params={'sfh_tsnorm_burst_log_peak_sfr': ParamDef(description='log10 peak SFR of tsnorm component', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 3.0)), 'sfh_tsnorm_burst_peak_lbt_gyr': ParamDef(description='Peak lookback time (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 12.0)), 'sfh_tsnorm_burst_width_gyr': ParamDef(description='Gaussian width (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.2, 5.0)), 'sfh_tsnorm_burst_skew': ParamDef(description='Skewness', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 1.0)), 'sfh_tsnorm_burst_trunc': ParamDef(description='Truncation sharpness', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(1.0, 10.0)), 'sfh_tsnorm_burst_burst_sfr': ParamDef(description='Constant burst SFR amplitude (Msun/yr)', bound_check=<function <lambda>>, bound_error='must have lo >= 0', default=Fixed(0.0)), 'sfh_tsnorm_burst_burst_age_gyr': ParamDef(description='Burst lookback duration (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Fixed(0.1))}, settings={}, internal_param_map={'sfh_tsnorm_burst_log_peak_sfr': ('log_peak_sfr', 1.0, 0.0), 'sfh_tsnorm_burst_peak_lbt_gyr': ('peak_lbt', 1000000000.0, 0.0), 'sfh_tsnorm_burst_width_gyr': ('width', 1000000000.0, 0.0), 'sfh_tsnorm_burst_skew': ('skew', 1.0, 0.0), 'sfh_tsnorm_burst_trunc': ('trunc', 1.0, 0.0), 'sfh_tsnorm_burst_burst_sfr': ('burst_sfr', 1.0, 0.0), 'sfh_tsnorm_burst_burst_age_gyr': ('burst_age', 1000000000.0, 0.0)}, composition_type='additive'), citation='Robotham et al. 2020 (arXiv:2002.06980) ProSpect', status='production', short_doc='Truncated skew-normal + flat burst (ProSpect)'), 'table': SFHRegistryEntry(callable=SFHModelSpec(name='table', fn=<function _table_sfh_placeholder>, params={}, settings={}, internal_param_map={}, composition_type='additive'), citation='', status='production', short_doc='User-supplied tabulated SFH(t)'), 'tau': SFHRegistryEntry(callable=SFHModelSpec(name='tau', fn=<function declining_exponential>, params={'sfh_tau_log_peak_sfr': ParamDef(description='log10 peak SFR at formation (Msun/yr)', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 3.0)), 'sfh_tau_tau_gyr': ParamDef(description='e-folding timescale (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.1, 10.0)), 'sfh_tau_age_gyr': ParamDef(description='Galaxy age / lookback time of formation (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 13.0))}, settings={}, internal_param_map={'sfh_tau_log_peak_sfr': ('log_peak_sfr', 1.0, 0.0), 'sfh_tau_tau_gyr': ('tau', 1000000000.0, 0.0), 'sfh_tau_age_gyr': ('age', 1000000000.0, 0.0)}, composition_type='additive'), citation='', status='production', short_doc='Declining exponential SFH (FSPS/bagpipes)'), 'top_hat': SFHRegistryEntry(callable=SFHModelSpec(name='top_hat', fn=<function top_hat>, params={'sfh_top_hat_amplitude': ParamDef(description='Constant SFR inside window (Msun/yr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.01, 100.0)), 'sfh_top_hat_t_start_gyr': ParamDef(description='Older lookback boundary / SF onset (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.1, 13.0)), 'sfh_top_hat_t_end_gyr': ParamDef(description='Younger lookback boundary / SF cessation (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo >= 0', default=Uniform(0.0, 12.0)), 'sfh_top_hat_smooth_width_gyr': ParamDef(description='Sigmoid transition width (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Fixed(0.1))}, settings={}, internal_param_map={'sfh_top_hat_amplitude': ('amplitude', 1.0, 0.0), 'sfh_top_hat_t_start_gyr': ('t_start', 1000000000.0, 0.0), 'sfh_top_hat_t_end_gyr': ('t_end', 1000000000.0, 0.0), 'sfh_top_hat_smooth_width_gyr': ('smooth_width', 1000000000.0, 0.0)}, composition_type='additive'), citation='', status='production', short_doc='Constant-SFR window with smooth edges (top-hat)'), 'truncated_skewnormal_sfh': SFHRegistryEntry(callable=SFHModelSpec(name='tsnorm', fn=<function truncated_skewnormal>, params={'sfh_tsnorm_log_peak_sfr': ParamDef(description='log10 peak SFR', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 3.0)), 'sfh_tsnorm_peak_lbt_gyr': ParamDef(description='Peak lookback time (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 12.0)), 'sfh_tsnorm_width_gyr': ParamDef(description='Gaussian width (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.2, 5.0)), 'sfh_tsnorm_skew': ParamDef(description='Skewness', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 1.0)), 'sfh_tsnorm_trunc': ParamDef(description='Truncation sharpness', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(1.0, 10.0))}, settings={}, internal_param_map={'sfh_tsnorm_log_peak_sfr': ('log_peak_sfr', 1.0, 0.0), 'sfh_tsnorm_peak_lbt_gyr': ('peak_lbt', 1000000000.0, 0.0), 'sfh_tsnorm_width_gyr': ('width', 1000000000.0, 0.0), 'sfh_tsnorm_skew': ('skew', 1.0, 0.0), 'sfh_tsnorm_trunc': ('trunc', 1.0, 0.0)}, composition_type='additive'), citation='Bellstedt et al. 2020 (arXiv:2005.11917)', status='production', short_doc='Truncated skew-normal SFH (Bellstedt+2020)'), 'tsnorm': SFHRegistryEntry(callable=SFHModelSpec(name='tsnorm', fn=<function truncated_skewnormal>, params={'sfh_tsnorm_log_peak_sfr': ParamDef(description='log10 peak SFR', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 3.0)), 'sfh_tsnorm_peak_lbt_gyr': ParamDef(description='Peak lookback time (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 12.0)), 'sfh_tsnorm_width_gyr': ParamDef(description='Gaussian width (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.2, 5.0)), 'sfh_tsnorm_skew': ParamDef(description='Skewness', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 1.0)), 'sfh_tsnorm_trunc': ParamDef(description='Truncation sharpness', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(1.0, 10.0))}, settings={}, internal_param_map={'sfh_tsnorm_log_peak_sfr': ('log_peak_sfr', 1.0, 0.0), 'sfh_tsnorm_peak_lbt_gyr': ('peak_lbt', 1000000000.0, 0.0), 'sfh_tsnorm_width_gyr': ('width', 1000000000.0, 0.0), 'sfh_tsnorm_skew': ('skew', 1.0, 0.0), 'sfh_tsnorm_trunc': ('trunc', 1.0, 0.0)}, composition_type='additive'), citation='Bellstedt et al. 2020 (arXiv:2005.11917)', status='production', short_doc='Truncated skew-normal SFH (Bellstedt+2020)'), 'tsnorm_burst': SFHRegistryEntry(callable=SFHModelSpec(name='tsnorm_burst', fn=<function snorm_trunc_burst>, params={'sfh_tsnorm_burst_log_peak_sfr': ParamDef(description='log10 peak SFR of tsnorm component', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 3.0)), 'sfh_tsnorm_burst_peak_lbt_gyr': ParamDef(description='Peak lookback time (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.5, 12.0)), 'sfh_tsnorm_burst_width_gyr': ParamDef(description='Gaussian width (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(0.2, 5.0)), 'sfh_tsnorm_burst_skew': ParamDef(description='Skewness', bound_check=<function <lambda>>, bound_error='', default=Uniform(-1.0, 1.0)), 'sfh_tsnorm_burst_trunc': ParamDef(description='Truncation sharpness', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Uniform(1.0, 10.0)), 'sfh_tsnorm_burst_burst_sfr': ParamDef(description='Constant burst SFR amplitude (Msun/yr)', bound_check=<function <lambda>>, bound_error='must have lo >= 0', default=Fixed(0.0)), 'sfh_tsnorm_burst_burst_age_gyr': ParamDef(description='Burst lookback duration (Gyr)', bound_check=<function <lambda>>, bound_error='must have lo > 0', default=Fixed(0.1))}, settings={}, internal_param_map={'sfh_tsnorm_burst_log_peak_sfr': ('log_peak_sfr', 1.0, 0.0), 'sfh_tsnorm_burst_peak_lbt_gyr': ('peak_lbt', 1000000000.0, 0.0), 'sfh_tsnorm_burst_width_gyr': ('width', 1000000000.0, 0.0), 'sfh_tsnorm_burst_skew': ('skew', 1.0, 0.0), 'sfh_tsnorm_burst_trunc': ('trunc', 1.0, 0.0), 'sfh_tsnorm_burst_burst_sfr': ('burst_sfr', 1.0, 0.0), 'sfh_tsnorm_burst_burst_age_gyr': ('burst_age', 1000000000.0, 0.0)}, composition_type='additive'), citation='Robotham et al. 2020 (arXiv:2002.06980) ProSpect', status='production', short_doc='Truncated skew-normal + flat burst (ProSpect)')}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object’s

(key, value) pairs

dict(iterable) -> new dictionary initialized as if via:

d = {} for k, v in iterable:

d[k] = v

dict(**kwargs) -> new dictionary initialized with the name=value pairs

in the keyword argument list. For example: dict(one=1, two=2)

tengri.FIELD_MODEL_REGISTRY = {'drw': <function compute_sqrt_power_drw>}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object’s

(key, value) pairs

dict(iterable) -> new dictionary initialized as if via:

d = {} for k, v in iterable:

d[k] = v

dict(**kwargs) -> new dictionary initialized with the name=value pairs

in the keyword argument list. For example: dict(one=1, two=2)

Power Spectral Density

DRW (damped random walk) PSD models that govern the burstiness of star formation histories via Gaussian process priors.

tengri.psd_drw(omega: Array, psd_sigma: float, psd_tau_yr: float) Array[source]

Damped random walk (Lorentzian) power spectral density.

Parameters:
  • omega (array_like, shape (n_freq,)) – Angular frequency [rad/yr].

  • psd_sigma (float) – PSD amplitude (dimensionless).

  • psd_tau_yr (float) – Characteristic damping timescale [yr].

Returns:

Power spectral density at each frequency [dimensionless].

Return type:

ndarray, shape (n_freq,)

Notes

JIT-compatible: yes — all operations use jnp primitives.

The damped random walk (Lorentzian) power spectral density is:

\[\begin{split}P(\\omega) = \\sigma_{\rm PS}^2 \\,\tau_{\rm PS} / (1 + (\tau_{\rm PS} \\omega)^2)\end{split}\]

where \(\\sigma_{\rm PS}\) is the PSD amplitude [dimensionless], \(\tau_{\rm PS}\) is the damping timescale [yr], and \(\\omega\) is the angular frequency [rad/yr].

This is the primary PSD model used in tengri for stochastic SFH modeling.

References

Examples

>>> import jax.numpy as jnp
>>> from tengri import psd_drw
>>> omega = jnp.linspace(0.0, 2e-7, 128)  # angular frequencies [rad/yr]
>>> psd = psd_drw(omega, psd_sigma=1.0, psd_tau_yr=1e8)
>>> psd.shape
(128,)
>>> float(psd[0])  # peak power at omega=0
1e+08
tengri.drw_acf(delta_t: Array, psd_sigma: float, psd_tau_yr: float) Array[source]

Analytic autocorrelation function (autocovariance) for DRW.

Parameters:
  • delta_t (array_like, shape (n_lag,)) – Time lag [yr].

  • psd_sigma (float) – PSD amplitude (dimensionless).

  • psd_tau_yr (float) – Damping timescale [yr].

Returns:

Autocovariance at each lag [dimensionless].

Return type:

ndarray, shape (n_lag,)

Notes

JIT-compatible: yes — all operations use jnp primitives.

The autocovariance (autocorrelation function) of the DRW is:

\[\xi(\Delta t) = \frac{\sigma_{\rm PS}^2}{2} \exp\!\left( -\frac{|\Delta t|}{\tau_{\rm PS}} \right)\]

where \(\sigma_{\rm PS}\) is the PSD amplitude, \(\tau_{\rm PS}\) is the damping timescale [yr], and \(\Delta t\) is the time lag [yr].

Examples

>>> import jax.numpy as jnp
>>> from tengri import drw_acf
>>> lags = jnp.array([0.0, 1e7, 1e8, 1e9])  # time lags [yr]
>>> acf = drw_acf(lags, psd_sigma=1.0, psd_tau_yr=1e8)
>>> acf.shape
(4,)
>>> float(acf[0])  # variance at zero lag = sigma^2 / 2
0.5
tengri.drw_variance(psd_sigma: float) float[source]

Stationary variance of DRW.

psd_sigmafloat

PSD amplitude (dimensionless).

float

Stationary variance [dimensionless].

JIT-compatible: yes — scalar arithmetic.

The stationary variance of a DRW is:

\[\sigma_x^2 = \frac{\sigma_{\]

m PS}^2}{2}

where :math:`sigma_{

m PS}` is the PSD amplitude.

>>> from tengri import drw_variance
>>> float(drw_variance(psd_sigma=1.0))
0.5
>>> float(drw_variance(psd_sigma=2.0))
2.0
tengri.compute_sqrt_power_drw(n_points: int, d_log_age: float, psd_sigma: float, psd_tau_yr: float, log_age_ref: float = 8.0) Array[source]

Pre-compute DRW amplitude operator for log-age grid.

Converts the DRW PSD from physical frequency (rad/yr) to log-age frequency space (rad/dex) using the Jacobian correction for the change of variables from linear time \(t\) to log-time \(u = \log_{10} t\).

n_pointsint

Grid size (number of age samples).

d_log_agefloat

Grid spacing in dex [dimensionless].

psd_sigmafloat

DRW PSD amplitude [dimensionless].

psd_tau_yrfloat

DRW damping timescale [yr].

log_age_reffloat, optional

Reference log10(age/yr) for Jacobian correction. Default: 8.0 (100 Myr).

ndarray, shape (n_freq,)

Amplitude operator \(\sqrt{P_u(q) / \Delta u}\) at rfft frequencies in log-age space [dimensionless].

JIT-compatible: yes — all operations use jnp primitives.

The Jacobian correction for the change of variables from cosmic time \(t\) (in years) to log-age \(u = \log_{10}(t)\) is:

\[P_u(q) = P_t\left( \frac{q}{t_{\rm ref} \ln 10} \right) / (t_{\rm ref} \ln 10)\]

where :math:`t_{rm ref} = 10^{log_{

m age, ref}}` is a reference time,

\(q\) is the angular frequency in log-age space [rad/dex], and \(P_t\) is the PSD in physical frequency space.

The reference time scales out in the power spectrum but affects the mapping between physical and log-age frequencies. A typical choice is the midpoint of the age range (e.g., 100 Myr ~ 8 Gyr cosmic age).

>>> from tengri import compute_sqrt_power_drw, make_log_age_grid
>>> grid = make_log_age_grid(n_grid=64)
>>> d = float(grid[1] - grid[0])
>>> sp = compute_sqrt_power_drw(64, d, psd_sigma=1.0, psd_tau_yr=1e8)
>>> sp.shape
(33,)

GP Generation

Functions for generating Gaussian process realisations from PSD parameters and a latent vector.

tengri.compute_field_gp(xi: Array, psd_sigma: float, psd_tau_yr: float, n_grid: int, d_log_age: float, field_model: str = 'drw') tuple[Array, float][source]

Compute GP realization and lognormal correction for the field component.

Parameters:
  • xi (array, shape (n_grid,)) – Latent vector (xi ~ N(0, I)).

  • psd_sigma (float) – PSD amplitude (dex).

  • psd_tau_yr (float) – PSD timescale (yr).

  • n_grid (int) – Grid size.

  • d_log_age (float) – Grid spacing in dex.

  • field_model (str) – PSD model name. Default “drw”.

Returns:

  • gp_x (array, shape (n_grid,)) – GP realization on the log-age grid.

  • k0_half (float) – Lognormal correction: K(0)/2 = sigma_PS^2 / 4.

Notes

JIT-compatible: yes — uses gp_from_xi and PSD model functions from the field model registry.

The Gaussian process realization models burstiness via a correlated random field in log-space. The PSD model (e.g., “drw” for Damped Random Walk) controls temporal correlations. The lognormal correction k0_half accounts for the bias introduced when exponentiation is applied to the Gaussian latents.

Examples

>>> import jax.numpy as jnp
>>> from tengri import compute_field_gp, make_log_age_grid
>>> n = 64
>>> grid = make_log_age_grid(n)
>>> d = float(grid[1] - grid[0])
>>> xi = jnp.zeros(n)
>>> gp_x, k0_half = compute_field_gp(xi, psd_sigma=1.0, psd_tau_yr=1e8, n_grid=n, d_log_age=d)
>>> gp_x.shape
(64,)
tengri.generate_gp_fourier(key: Array, sqrt_power: Array, n_points: int) Array[source]

Stochastic GP realization for mock galaxy generation.

Draws a random standardized vector and maps it to a correlated GP field.

keyjax.random.PRNGKey

JAX random key for reproducibility.

sqrt_powerarray_like, shape (n_freq,)

Amplitude operator :math:`sqrt{P(omega) / d_{

m grid}}` at rfft frequencies

(pre-compute with psd_to_sqrt_power()). [dimensionless]

n_pointsint

Number of grid points.

ndarray, shape (n_points,)

GP realization on the log-age grid [dimensionless].

JIT-compatible: yes — uses jax.random.normal and gp_from_xi().

This function is the primary interface for generating mock SFHs with stochastic variability. The random draw is always independent; for reproducibility, pass the same PRNGKey.

gp_from_xi : Deterministic GP mapping (used internally). generate_gp_batch : Generate multiple independent realizations.

>>> import jax
>>> from tengri import generate_gp_fourier, make_log_age_grid, compute_sqrt_power_drw
>>> n = 64
>>> grid = make_log_age_grid(n)
>>> d = float(grid[1] - grid[0])
>>> sqrt_power = compute_sqrt_power_drw(n, d, psd_sigma=1.0, psd_tau_yr=1e8)
>>> key = jax.random.PRNGKey(0)
>>> sfh = generate_gp_fourier(key, sqrt_power, n)
>>> sfh.shape
(64,)
tengri.generate_gp_batch(key: Array, sqrt_power: Array, n_points: int, n_realizations: int) Array[source]

Batch of independent GP realizations via vectorization.

Generates multiple independent SFH realizations in parallel using vmap.

keyjax.random.PRNGKey

JAX random key (will be split into n_realizations independent keys).

sqrt_powerarray_like, shape (n_freq,)

Amplitude operator :math:`sqrt{P(omega) / d_{

m grid}}` at rfft frequencies.

[dimensionless]

n_pointsint

Number of grid points.

n_realizationsint

Number of independent realizations to generate.

ndarray, shape (n_realizations, n_points)

Batch of independent GP realizations [dimensionless].

JIT-compatible: yes — uses jax.vmap over generate_gp_fourier().

Each realization is independent with the specified PSD structure. This function is useful for generating mock catalogs or computing uncertainties via Monte Carlo sampling.

generate_gp_fourier : Single realization.

>>> import jax
>>> from tengri import generate_gp_batch, make_log_age_grid, compute_sqrt_power_drw
>>> n = 64
>>> grid = make_log_age_grid(n)
>>> d = float(grid[1] - grid[0])
>>> sqrt_power = compute_sqrt_power_drw(n, d, psd_sigma=1.0, psd_tau_yr=1e8)
>>> key = jax.random.PRNGKey(0)
>>> batch = generate_gp_batch(key, sqrt_power, n_points=n, n_realizations=10)
>>> batch.shape
(10, 64)
tengri.gp_from_xi(xi: Array, sqrt_power: Array, n_points: int) Array[source]

Deterministic GP realization from standardized latent vector.

Maps a standardized Gaussian random vector to a correlated GP field via Fourier-space multiplication with a spectral amplitude operator. This is the core function used during inference and mock generation.

xiarray_like, shape (n_points,)

Standardized latent vector \(\xi \sim \mathcal{N}(0, I)\) under the prior.

sqrt_powerarray_like, shape (n_freq,)

Amplitude operator :math:`sqrt{P(omega) / d_{

m grid}}` at rfft frequencies

(pre-compute with psd_to_sqrt_power()). [dimensionless]

n_pointsint

Number of grid points (should match the length of xi).

ndarray, shape (n_points,)

GP realization on the log-age grid [dimensionless].

JIT-compatible: yes — uses jnp.fft.rfft and jnp.fft.irfft.

Gradient-safe: yes — differentiable w.r.t. sqrt_power.

Implements the NIFTy correlated field model:

\[s = \mathrm{IFFT}(\sqrt{P} \cdot \hat{\xi})\]

The rfft (real FFT) preserves Hermitian symmetry for real-valued output and ensures correct variance normalization: \(E[|\mathrm{rfft}(\xi)_k|^2] = N\), so with \(\sqrt{P/\Delta x}\) we recover \(\mathrm{Var}[s] = \int P(f) df\).

This is the primary function called during MCMC inference and mock galaxy generation. The sampler proposes values of \(\xi\) and this function maps them to correlated SFH realizations.

>>> import jax.numpy as jnp
>>> from tengri import gp_from_xi, make_log_age_grid, compute_sqrt_power_drw
>>> n = 64
>>> grid = make_log_age_grid(n)
>>> d = float(grid[1] - grid[0])
>>> sqrt_power = compute_sqrt_power_drw(n, d, psd_sigma=1.0, psd_tau_yr=1e8)
>>> xi = jnp.zeros(n)
>>> sfh = gp_from_xi(xi, sqrt_power, n)
>>> sfh.shape
(64,)
tengri.make_log_age_grid(n_grid: int = 256, log_age_min: float = 6.0, log_age_max: float = 10.14) Array[source]

Create uniform grid in log10(age/yr).

Default range: 1 Myr to ~13.8 Gyr (approximately the age of the universe).

Parameters:
  • n_grid (int, optional) – Number of grid points (should be even for FFT efficiency). Default: 256.

  • log_age_min (float, optional) – Minimum log10(age/yr). Default: 6.0 (1 Myr).

  • log_age_max (float, optional) – Maximum log10(age/yr). Default: 10.14 (~13.8 Gyr).

Returns:

Uniform grid in log10(age/yr) [dimensionless log values].

Return type:

ndarray, shape (n_grid,)

Notes

JIT-compatible: yes — uses jnp.linspace.

This grid is used as the internal representation for age in GP-based SFH models. The log-space parametrization provides better resolution at young ages and maps naturally to the logarithmic timescales of stellar evolution.

Examples

>>> from tengri import make_log_age_grid
>>> grid = make_log_age_grid(n_grid=64)
>>> grid.shape
(64,)
>>> float(grid[0]), float(grid[-1])
(6.0, 10.14)

Dust Attenuation

Two-component dust attenuation (birth cloud + diffuse ISM) following Charlot & Fall (2000), with pluggable attenuation curves and geometry models.

tengri.two_component_dust(wavelength: Array, age_grid: Array, tau_v1: float, tau_v2: float, law_bc: str = 'power_law', law_diff: str = 'power_law', f_obscuration: float = 0.0, t_birth: float = 10000000.0, transition_width: float = 0.3, **law_params) Array[source]

Two-component dust attenuation following Charlot & Fall (2000) with smooth age transition.

Separates dust into birth-cloud (young stars) and diffuse ISM (all stars) components with independent optical depths and attenuation curves. Transition between components uses a smooth sigmoid in log-age, enabling automatic differentiation.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

  • age_grid (array_like, shape (n_ages,)) – Stellar population ages. [yr]

  • tau_v1 (float) – Birth-cloud V-band optical depth (at 5500 Å). [dimensionless] Note: tengri applies tau_bc internally but exposes tau_v1 after normalizing by attenuation curve slope. See docs/known_bugs.md (CROSSVAL-01) for cross-code comparison.

  • tau_v2 (float) – Diffuse ISM V-band optical depth. [dimensionless]

  • law_bc (str, optional) – Attenuation curve name for birth cloud. Default: “power_law”. Resolved from DUST_LAWS registry.

  • law_diff (str, optional) – Attenuation curve name for diffuse ISM. Default: “power_law”.

  • f_obscuration (float, optional) – Fraction of unattenuated sightlines in clumpy geometry (Lower 2022). [dimensionless, in [0, 1]] Default: 0.0 (uniform screen).

  • t_birth (float, optional) – Birth-cloud dispersal age (sigmoid center). [yr] Default: 1e7 (10 Myr).

  • transition_width (float, optional) – Sigmoid transition width in dex. [dimensionless] Default: 0.3 (~5-20 Myr range).

  • **law_params – Keyword arguments passed to attenuation curve functions (e.g., n_slope, dust_bump_strength, dust_delta, dust_Rv).

Returns:

Multiplicative transmission factor T(λ, t_age), where T ∈ [0, 1]. [dimensionless]

Return type:

ndarray, shape (n_ages, n_wave)

Notes

JIT-compatible: yes — all operations are jnp primitives and safe for jax.jit.

Gradient-safe: yes — differentiable everywhere; smooth sigmoid age transition preserves gradients through the birth-cloud boundary.

The total optical depth is:

\[\tau(\lambda, t_{\text{age}}) = w(t_{\text{age}}) \cdot \tau_{{\rm V,BC}} \cdot k_{\rm BC}(\lambda) + \tau_{{\rm V,ISM}} \cdot k_{\rm ISM}(\lambda)\]

where \(w(t_{\text{age}})\) is the sigmoid weight:

\[w(t_{\text{age}}) = \sigma\left(-\frac{\log_{10} t_{\text{age}} - \log_{10} t_{\text{birth}}}{\Delta_{\text{trans}}}\right)\]

and \(\sigma(x) = 1/(1 + e^{-x})\) is the logistic sigmoid. The transmission is then:

\[T(\lambda, t_{\text{age}}) = f_{\rm obs} + (1 - f_{\rm obs}) \cdot \exp[-\tau(\lambda, t_{\text{age}})]\]

where \(f_{\rm obs}\) is the unattenuated sightline fraction.

Upstream: Implements the Charlot & Fall (2000) two-component framework [1]_ with sigmoid age transition following tengri’s differentiable design. Birth-cloud + diffuse ISM separation enables realistic modeling of age-dependent dust geometry in galaxies.

References

Examples

>>> import jax.numpy as jnp
>>> from tengri import two_component_dust
>>> wave = jnp.linspace(1000.0, 30000.0, 300)
>>> ages = jnp.logspace(6.0, 10.14, 64)
>>> T = two_component_dust(wave, ages, tau_v1=1.0, tau_v2=0.3)
>>> T.shape
(64, 300)

Attenuation curves: calzetti, cardelli, kriek_conroy, smc, lmc, power_law, salim, li08.

Dust attenuation model for tengri.

This module provides a generalized two-component dust attenuation framework (Charlot & Fall 2000) with pluggable attenuation curves and clumpy dust geometry (f_obscuration, Lower 2022).

Structure

Two physical components with independent attenuation curves: - Birth cloud: affects young stars (age < t_birth), optical depth tau_v1 - Diffuse ISM: affects all stars, optical depth tau_v2

The wavelength dependence k(lambda) is pluggable per component:

tau(lambda, age) = w(age) * tau_v1 * k_bc(lambda) + tau_v2 * k_diff(lambda)
transmission = f_obs + (1 - f_obs) * exp(-tau)

where w(age) is a smooth sigmoid transition at t_birth.

Available Attenuation Curves

  • power_law: (lambda/5500)^n — original Charlot & Fall (2000)

  • calzetti: Calzetti et al. (2000) starburst polynomial, R_V=4.05

  • leitherer02: Leitherer et al. (2002) UV extension of Calzetti (970-1800 A)

  • kriek_conroy: Calzetti + UV bump + slope delta — Prospector default

  • noll09: Noll et al. (2009) modified Calzetti+L02: (base+bump)*slope

  • salim_sbl18: Salim+2018 modified Calzetti+L02: base*slope+bump

  • smc: Pei (1992, ApJ 395 130) SMC Bar, steep UV, no 2175A bump

  • cardelli: Cardelli et al. (1989) MW curve with free R_V

  • li08: Li et al. (2008) Eq. (1) four-coefficient curve (continuum + FUV rise + 2175 Å bump)

  • salim: Salim et al. (2018) modified Calzetti (= DSPS default)

  • tea: Haskell et al. (2024) TEA 3-param empirical (NIHAO-SKIRT bump-slope correlation)

  • narayanan_z: Narayanan et al. (2018) redshift-dependent Kriek-Conroy (SIMBA RT)

  • conroy2010: Conroy+2010 mixed MW + power-law (FSPS dust_type=1)

  • vw07_bc: Wild+2007 birth cloud power-law (n=-1.3)

  • vw07_diff: Wild+2007 diffuse ISM power-law (n=-0.7)

Dust Geometries (Witt & Gordon 2000)

  • wg00_shell: Foreground screen — standard exp(-tau*k)

  • wg00_cloudy: Homogeneous dust-star mix (slab) — greyer than screen

  • wg00_dusty: Clumpy two-phase medium (Natta & Panagia 1984) — greyest

References

  • Calzetti et al. 2000, ApJ, 533, 682

  • Cardelli et al. 1989, ApJ, 345, 245

  • Charlot & Fall 2000, ApJ, 539, 718

  • Conroy, White & Gunn 2010, ApJ, 708, 58

  • Gordon et al. 2003, ApJ, 594, 279

  • Haskell et al. 2024, arXiv:2401.11007

  • Hobson & Padman 1993, MNRAS, 264, 161

  • Kriek & Conroy 2013, ApJL, 775, L16

  • Leitherer et al. 2002, ApJS, 140, 303

  • Li et al. 2008, ApJ, 685, 1046

  • Lower et al. 2022, ApJ, 931, 14

  • Narayanan et al. 2018, ApJ, 869, 70

  • Natta & Panagia 1984, ApJ, 287, 228

  • Noll et al. 2009, A&A, 507, 1793

  • Salim, Boquien & Lee 2018, ApJ, 859, 11

  • Wild et al. 2007, MNRAS, 381, 543

  • Witt & Gordon 2000, ApJ, 528, 799

  • Zacharegkas et al. 2025, arXiv:2506.19919

class tengri.components.dust.attenuation.DustLawRegistryEntry(callable: Callable, citation: str = '', status: str = 'production', short_doc: str = '')[source]

Bases: object

Registry entry for a dust attenuation law with optional metadata.

callable

The dust attenuation law function.

Type:

Callable

citation

Optional academic citation. Default empty string.

Type:

str

status

Model status: “production”, “experimental”, “demo”, or “deprecated”. Default “production”.

Type:

str

short_doc

Optional one-line description. Default empty string.

Type:

str

Notes

JIT-compatible: no — dataclass for registry initialization.

callable: Callable
citation: str = ''
short_doc: str = ''
status: str = 'production'
tengri.components.dust.attenuation.calzetti(wavelength: Array, **_kwargs) Array[source]

Calzetti et al. (2000) starburst attenuation curve.

R_V = 4.05 (fixed). Valid: 0.12 - 2.2 μm. Widely used for star-forming galaxies.

Parameters:

wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

Returns:

Normalized attenuation curve k(λ), where k(5500 Å) = 1. [dimensionless]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

Uses piecewise polynomials in \(x = 1/\lambda\) [μm⁻¹]:

\[\begin{split}k'(\lambda) = \begin{cases} 2.659(-2.156 + 1.509x - 0.198x^2 + 0.011x^3) + R_V & \lambda < 0.63 \, \mu{\rm m} \\ 2.659(-1.857 + 1.040x) + R_V & \lambda \geq 0.63 \, \mu{\rm m} \end{cases}\end{split}\]

then normalized: \(k(\lambda) = k'(\lambda) / R_V\) with \(R_V = 4.05\).

References

tengri.components.dust.attenuation.cardelli(wavelength: Array, dust_Rv: float = 3.1, **_kwargs) Array[source]

Cardelli, Clayton & Mathis (1989) MW extinction with free R_V.

Detailed piecewise fit to Milky Way extinction spanning UV to IR. R_V parameterization allows flexibility for different dust types.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

  • dust_Rv (float) – Total-to-selective extinction ratio. [dimensionless] Default: 3.1.

Returns:

Extinction curve A(λ)/A(V) normalized to k(5500 Å) = 1. [dimensionless]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

Uses piecewise polynomials in infrared, optical, UV, and far-UV regimes. Parameterized by \(x = 1/\lambda\) [μm⁻¹] with \(a(x)\) and \(b(x)\) coefficients fitted to extinction curves.

References

tengri.components.dust.attenuation.conroy2010(wavelength: Array, dust_Rv: float = 3.1, n_slope: float = -0.7, **_kwargs) Array[source]

Conroy+2010 mixed MW + power-law attenuation (FSPS dust_type=1).

Milky Way (Cardelli 1989) curve dominates at UV wavelengths, power-law dominates in the infrared. A smooth sigmoid blend ensures differentiability.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

  • dust_Rv (float) – Total-to-selective extinction ratio for the MW component. [dimensionless] Default: 3.1.

  • n_slope (float) – Power-law index for the long-wavelength component. [dimensionless] Default: -0.7.

Returns:

Attenuation curve k(λ), normalized to k(5500 Å) = 1. [dimensionless]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

A smooth sigmoid transition function weights between MW (short wavelength) and power-law (long wavelength) components at the V-band (5500 Å):

\[k(\lambda) = [w(\lambda) \, k_{\rm MW}(\lambda) + (1-w(\lambda)) \, k_{\rm PL}(\lambda)] / k_{\rm MW}(5500 \, \text{\AA})\]

where \(w(\lambda) = \sigma(\log_{10}(\lambda/5500 \, \text{\AA}) / 0.05)\) is a sigmoid.

References

tengri.components.dust.attenuation.d03_mwrv31(wavelength: Array, **_kwargs) Array[source]

Draine (2003) MW R_V=3.1 updated grain model attenuation curve.

Updated Milky Way grain model incorporating revised PAH properties and emission efficiencies relative to WD01. R_V = 3.1.

Parameters:

wavelength (array_like, shape (n_wave,)) – Wavelength grid [Å].

Returns:

Normalized attenuation curve k(λ), k(5500 Å) = 1 [dimensionless].

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — uses jnp.interp on precomputed curve.

Upstream credit: grain model data from Draine (2003) [1]_, accessed via the dust-extinction astropy-affiliated package. Independent implementation in synthesizer (Wilkins et al. 2025 [2]_).

References

tengri.components.dust.attenuation.dust_to_gas_scaling_remy_ruyer(logzsol: float) float[source]

Metallicity-dependent dust-to-gas ratio scaling (Rémy-Ruyer+2014).

Returns multiplicative factor for dust optical depths relative to solar. Broken power law: linear above 0.1 Z_sun, quadratic below.

Parameters:

logzsol (float) – log10(Z / Z_sun). [dimensionless]

Returns:

D/G ratio relative to solar (1.0 at solar metallicity). [dimensionless]

Return type:

float

Notes

JIT-compatible: yes — all operations are jnp primitives.

The scaling is a broken power law:

\[\begin{split}\text{D/G} = \begin{cases} (Z/Z_{\odot})^2 \times 0.1 & (Z/Z_{\odot}) \leq 0.1 \\ (Z/Z_{\odot}) & (Z/Z_{\odot}) > 0.1 \end{cases}\end{split}\]

References

tengri.components.dust.attenuation.hd23_mwrv31(wavelength: Array, **_kwargs) Array[source]

Hensley & Draine (2023) astrodust+PAH MW R_V=3.1 grain model.

State-of-the-art MW grain model combining astrodust grains with PAH emission. R_V = 3.1. Supersedes WD01/D03 for the diffuse MW ISM.

Parameters:

wavelength (array_like, shape (n_wave,)) – Wavelength grid [Å].

Returns:

Normalized attenuation curve k(λ), k(5500 Å) = 1 [dimensionless].

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — uses jnp.interp on precomputed curve.

Upstream credit: grain model data from Hensley & Draine (2023) [1]_, accessed via the dust-extinction astropy-affiliated package. Independent implementation in synthesizer (Wilkins et al. 2025 [2]_).

References

tengri.components.dust.attenuation.kriek_conroy(wavelength: Array, dust_bump_strength: float = 1.0, dust_delta: float = 0.0, **_kwargs) Array[source]

Kriek & Conroy (2013) modified Calzetti + UV bump + slope delta.

Default in Prospector. Most flexible single-parameter-family curve. Combines Calzetti + 2175 Å bump + power-law modification.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

  • dust_bump_strength (float) – Amplitude of 2175 Å UV bump (E_b). [dimensionless] Default: 1.0 (no bump).

  • dust_delta (float) – Power-law slope modification. [dimensionless] Default: 0.0 (pure Calzetti).

Returns:

Normalized attenuation curve k(λ), where k(5500 Å) = 1. [dimensionless]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

The attenuation is:

\[k(\lambda) = k_{\rm Calz}(\lambda) \cdot \left(\frac{\lambda}{5500 \, \text{\AA}}\right)^\delta + E_b \cdot D(\lambda; \lambda_0 = 2175 \, \text{\AA})\]

where \(k_{\rm Calz}\) is from Calzetti et al. (2000), \(E_b\) is the bump amplitude, and \(D(\lambda; \lambda_0)\) is the Drude profile.

References

tengri.components.dust.attenuation.leitherer02(wavelength: Array, **_kwargs) Array[source]

Leitherer et al. (2002) UV starburst attenuation curve.

Far-UV extension of the Calzetti (2000) law, valid 970-1800 Å. Uses R_V = 4.05 (same as Calzetti). For wavelengths outside the L02 range, falls back to Calzetti (2000).

Parameters:

wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

Returns:

Attenuation curve k(λ), normalized to k(5500 Å) = 1. [dimensionless]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

The reddening curve k’(λ) = A(λ)/E(B−V) follows:

\[k'(\lambda) = 5.472 + 0.671x - 9.218 \times 10^{-3} x^2 + 2.620 \times 10^{-3} x^3\]

where \(x = 1/\lambda\) [μm⁻¹], valid for 970–1800 Å (0.097–0.18 μm). The final k(λ) = k’(λ) / R_V with R_V = 4.05.

References

tengri.components.dust.attenuation.li08(wavelength: Array, dust_c1: float = 6.0, dust_c2: float = 4.0, dust_c3: float = 2.0, dust_c4: float = 0.04, **_kwargs) Array[source]

Li et al. (2008) analytical dust attenuation/extinction curve.

A flexible 4-parameter analytical model that can reproduce MW, LMC, SMC, and Calzetti-like curves through a single functional form. The three terms represent a UV/optical continuum, a far-UV rise, and the 2175 Angstrom UV bump respectively.

The normalized attenuation is (Li et al. 2008, Eq. 1):

A_lam/A_V = c1 / [(lam/0.08)^c2 + (0.08/lam)^c2 + c3]
          + 233 * [1 - c1/(6.88^c2 + 0.145^c2 + c3) - c4/4.60]
            / [(lam/0.046)^2 + (0.046/lam)^2 + 90]
          + c4 / [(lam/0.2175)^2 + (0.2175/lam)^2 - 1.95]

where lam is the wavelength in micron and c1-c4 are dimensionless.

Parameters:
  • wavelength (array, shape (n_wave,)) – Wavelength grid in Angstrom.

  • dust_c1 (float) – Continuum amplitude. Controls overall UV-optical shape.

  • dust_c2 (float) – Continuum curvature. Higher values produce steeper UV rises.

  • dust_c3 (float) – Continuum offset. Shifts the overall curve level.

  • dust_c4 (float) – UV bump amplitude at 2175 Angstrom. Set to 0 for bump-free.

Returns:

Attenuation curve k(lambda), normalized to k(5500 A) = 1.

Return type:

array, shape (n_wave,)

Notes

Approximate presets for common curves (Markov et al. 2023, 2025):

  • MW-like: c1~6.0, c2~4.0, c3~2.0, c4~0.04

  • SMC-like: c1~5.0, c2~5.5, c3~1.5, c4~0.0

  • Calzetti-like: c1~3.5, c2~2.5, c3~3.0, c4~0.0

References

Li, A., Liang, S. L., Kann, D. A., et al. 2008, ApJ, 685, 1046 Markov, V., Gallerani, S., Pallottini, A., et al. 2023, A&A, 679, A12 Markov, V., Gallerani, S., Pallottini, A., et al. 2025, A&A (arXiv:2504.12378)

tengri.components.dust.attenuation.lmc(wavelength: Array, **_kwargs) Array[source]

LMC average extinction curve (Pei 1992, ApJ, 395, 130).

Weak 2175 Å bump, intermediate between MW and SMC. R_V = 3.16. Uses generalized Drude profile sum — fully continuous.

Parameters:

wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

Returns:

Normalized extinction curve k(λ). [dimensionless]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

From Pei (1992) Table 4: Large Magellanic Cloud average parameters, 6 Drude components with R_V = 3.16. Shows weak 2175 Å bump feature.

References

P. G. Pei, “Interstellar Dust from the Ultraviolet to the Infrared,” ApJ, 395, 130 (1992).

tengri.components.dust.attenuation.narayanan_z(wavelength: Array, dust_delta: float = -0.2, dust_bump_strength: float = 1.0, redshift: float = 0.0, **_kwargs) Array[source]

Narayanan+2018 redshift-dependent attenuation.

Uses the Kriek & Conroy (2013) curve with z-dependent median parameters calibrated on SIMBA cosmological radiative-transfer simulations.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

  • dust_delta (float) – Power-law slope modification. [dimensionless] Default: -0.2 (triggers z-scaling).

  • dust_bump_strength (float) – UV bump amplitude E_b. [dimensionless] Default: 1.0 (triggers z-scaling).

  • redshift (float) – Galaxy redshift. [dimensionless] Default: 0.0.

Returns:

Attenuation curve k(λ), normalized to k(5500 Å) = 1. [dimensionless]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

When default parameters are used (δ = -0.2, E_b = 1.0), the z-dependent medians from SIMBA are applied:

\[\begin{split}\delta(z) &\approx -0.2 - 0.1 z \quad \text{(steeper at high z)} \\ E_b(z) &\approx \max(0, 1.0 - 0.15 z) \quad \text{(weaker bump at high z)}\end{split}\]

References

tengri.components.dust.attenuation.noll09(wavelength: Array, dust_bump_strength: float = 0.0, dust_delta: float = 0.0, dust_bump_x0: float = 0.2175, dust_bump_gamma: float = 0.035, **_kwargs) Array[source]

Noll et al. (2009) modified Calzetti + L02 with UV bump + slope delta.

This is the N09 model from the dust_attenuation package. Uses Leitherer (2002) for λ < 1500 Å and Calzetti (2000) above. The modification order is: (base + bump) × power_law.

This differs from kriek_conroy which does NOT use L02 and applies the bump AFTER the slope: base × power_law + bump.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

  • dust_bump_strength (float) – Amplitude of 2175 Å UV bump (E_b). [dimensionless] Default: 0.0 (no bump).

  • dust_delta (float) – Power-law slope modification. [dimensionless] Default: 0.0 (pure Calzetti+L02).

  • dust_bump_x0 (float) – Central wavelength of UV bump. [μm] Default: 0.2175.

  • dust_bump_gamma (float) – FWHM of UV bump. [μm] Default: 0.035.

Returns:

Attenuation curve k(λ) = k’(λ) / R_V with R_V = 4.05. [dimensionless]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

The attenuation is:

\[k(\lambda) = [k_{\rm L02+C00}(\lambda) + E_b D(\lambda; \lambda_0, \gamma)] \times \left(\frac{\lambda}{5500 \, \text{\AA}}\right)^\delta / R_V\]

Normalization follows the dust_attenuation package convention: k(λ) = k’(λ)/R_V where R_V = 4.05 is fixed. This means k(V) is NOT exactly 1.0 when bump or slope modifications are applied.

References

tengri.components.dust.attenuation.power_law(wavelength: Array, n_slope: float = -0.7, **_kwargs) Array[source]

Power-law dust attenuation curve following Charlot & Fall (2000).

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

  • n_slope (float, optional) – Power-law slope. Default: -0.7 (standard Charlot & Fall). [dimensionless]

Returns:

Normalized attenuation curve k(λ), where k(5500 Å) = 1. [dimensionless]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

The attenuation is:

\[k(\lambda) = \left(\frac{\lambda}{5500 \, \text{\AA}}\right)^n\]

where \(n = -0.7\) produces the standard Charlot & Fall (2000) wavelength dependence. Negative slopes make dust redder (stronger attenuation at short wavelengths).

References

tengri.components.dust.attenuation.precompute_dust_age_mask(age_grid: Array, t_birth: float = 10000000.0) tuple[Array, Array][source]

Precompute hard young/old masks for fast two-CSP dust decomposition.

Uses a hard threshold at t_birth instead of a smooth sigmoid. This is the original Charlot & Fall (2000) formulation and enables a fast path where dust is factored out of the age sum entirely.

Parameters:
  • age_grid (array_like, shape (n_ages,)) – Stellar population ages. [yr]

  • t_birth (float) – Birth cloud dispersal age. [yr] Default: 1e7 (10 Myr).

Returns:

  • young_mask (ndarray, shape (n_ages,)) – 1.0 for young ages (< t_birth), 0.0 for old. [dimensionless]

  • old_mask (ndarray, shape (n_ages,)) – 1.0 for old ages (≥ t_birth), 0.0 for young. [dimensionless]

Notes

JIT-compatible: yes — all operations are jnp primitives.

The original Charlot & Fall (2000) model uses a hard cutoff instead of a sigmoid. This returns complementary masks: young_mask + old_mask = 1 everywhere.

tengri.components.dust.attenuation.precompute_dust_age_weights(age_grid: Array, t_birth: float = 10000000.0, transition_width: float = 0.3) Array[source]

Precompute the birth-cloud sigmoid weight.

Call once at Model init; pass result to two_component_dust_fast.

Parameters:
  • age_grid (array_like, shape (n_ages,)) – Stellar population ages. [yr]

  • t_birth (float) – Birth cloud dispersal age. [yr] Default: 1e7 (10 Myr).

  • transition_width (float) – Sigmoid width in dex. [dimensionless] Default: 0.3.

Returns:

Sigmoid weight: 1 for young stars (t < t_birth), 0 for old. [dimensionless]

Return type:

ndarray, shape (n_ages,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

The weight is:

\[w(t_{\text{age}}) = \sigma\left(-\frac{\log_{10} t_{\text{age}} - \log_{10} t_{\text{birth}}}{\Delta_{\text{trans}}}\right)\]

where \(\sigma(x) = 1/(1 + e^{-x})\) is the logistic sigmoid.

tengri.components.dust.attenuation.prevot_smc(wavelength: Array, **_kwargs) Array[source]

Prevot et al. (1984) SMC extinction law for AGN obscuration.

Analytic SMC extinction curve from the UV to near-infrared, used in AGNfitter for AGN disc reddening. The published Prevot+1984 form gives \(k_{\rm raw}(\lambda) = A(\lambda)/E(B-V)\) with \(R_V = 2.72\); this function returns the V-band-normalised curve \(k(\lambda) = A(\lambda)/A(V) = k_{\rm raw}(\lambda)/k_{\rm raw}(V)\), matching the convention used by cardelli and the rest of the components.dust.attenuation registry (k(V)=1).

Parameters:

wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

Returns:

Extinction curve \(k(\lambda) = A(\lambda)/A(V)\) normalised to k(5500 A) = 1. [dimensionless]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

\[k_{\rm raw}(\lambda) = 1.39 \, \lambda_{\mu m}^{-1.2} - 0.38, \qquad k(\lambda) = k_{\rm raw}(\lambda) / k_{\rm raw}(0.55\,\mu m)\]

where \(\lambda_{\mu m}\) is wavelength in micrometres and \(k_{\rm raw}(0.55) \approx 2.468\).

For wavelengths below 62 Å, reddening is ramped to zero using a smooth sigmoid (no hard discontinuity) to suppress extinction in the X-ray regime where dust is ineffective.

Gradient-compatible: yes — enables optimization of extinction parameters.

References

tengri.components.dust.attenuation.register_dust_law(name: str, citation: str = '', status: str = 'production', short_doc: str = '') Callable[source]

Decorator factory that registers a dust attenuation curve under a name.

Parameters:
  • name (str) – Registry key (e.g. "calzetti", "power_law").

  • citation (str, optional) – Academic citation for the model. Default empty string.

  • status (str, optional) – Model status (“production”, “experimental”, “demo”, “deprecated”). Default “production”.

  • short_doc (str, optional) – One-line description. Default empty string.

Returns:

Decorator that registers the decorated function and returns it unchanged.

Return type:

Callable

Notes

JIT-compatible: no — registration happens at factory time before JIT.

Decorated functions must implement the DustAttenuationLaw protocol: accept a wavelength array and keyword arguments, returning an attenuation curve k(λ) [dimensionless].

tengri.components.dust.attenuation.resolve_dust_law(name: str) Callable[source]

Return a registered dust attenuation law by name.

Parameters:

name (str) – Registry key (e.g. "calzetti", "power_law").

Returns:

The registered dust law function.

Return type:

Callable

Raises:

ValueError – If name is not in the registry.

Notes

JIT-compatible: no — registry lookup happens at factory time.

The returned function matches the DustAttenuationLaw protocol and can be called with wavelengths and law-specific parameters.

tengri.components.dust.attenuation.salim(wavelength: Array, dust_bump_strength: float = 0.0, dust_delta: float = 0.0, **_kwargs) Array[source]

Salim et al. (2018) modified Calzetti law (DSPS/Zacharegkas+2025 default).

Same functional form as kriek_conroy(); aliased here as the default Zacharegkas+2025 diffuse ISM attenuation law.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelengths. [Angstrom]

  • dust_bump_strength (float) – Amplitude of the 2175 Angstrom UV bump. [dimensionless]

  • dust_delta (float) – Power-law tilt relative to Calzetti slope. [dimensionless]

  • **_kwargs – Ignored extra keyword arguments (for registry compatibility).

Returns:

Attenuation curve k(lambda) normalized to k(5500 A) = 1. [dimensionless]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — uses only jnp primitives.

Gradient-safe: yes — differentiable everywhere.

This is the default attenuation law in DSPS and Zacharegkas+2025 simulations. It combines Calzetti et al. (2000) optical/NIR continuum with Leitherer et al. (2002) UV extension, plus a variable 2175 Å bump and power-law tilt.

tengri.components.dust.attenuation.salim_sbl18(wavelength: Array, dust_bump_strength: float = 0.0, dust_delta: float = 0.0, dust_bump_x0: float = 0.2175, dust_bump_gamma: float = 0.035, **_kwargs) Array[source]

Salim, Boquien & Lee (2018) modified Calzetti + L02 with UV bump + slope.

This is the SBL18 model from the dust_attenuation package. Uses Leitherer (2002) for λ < 1500 Å and Calzetti (2000) above. The modification order is: (base × power_law) + bump.

This differs from noll09 which applies: (base + bump) × power_law. The SBL18 order is identical to kriek_conroy, but SBL18 additionally uses L02 in the far-UV.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

  • dust_bump_strength (float) – Amplitude of 2175 Å UV bump (E_b). [dimensionless] Default: 0.0 (no bump).

  • dust_delta (float) – Power-law slope modification. [dimensionless] Default: 0.0 (pure Calzetti+L02).

  • dust_bump_x0 (float) – Central wavelength of UV bump. [μm] Default: 0.2175.

  • dust_bump_gamma (float) – FWHM of UV bump. [μm] Default: 0.035.

Returns:

Attenuation curve k(λ) = k’(λ) / R_V with R_V = 4.05. [dimensionless]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

The attenuation is:

\[k(\lambda) = \left[k_{\rm L02+C00}(\lambda) \times \left(\frac{\lambda}{5500 \, \text{\AA}}\right)^\delta + E_b D(\lambda; \lambda_0, \gamma)\right] / R_V\]

References

tengri.components.dust.attenuation.single_component_dust(wavelength: Array, tau_v: float, law: str = 'power_law', f_obscuration: float = 0.0, **law_params) Array[source]

Single-component (uniform foreground screen) dust attenuation.

Applies a single attenuation curve at uniform optical depth to all stellar ages. Age-independent, enabling factorization out of stellar population integration. Simpler but less realistic than two-component models; useful for low-precision fits or high-redshift galaxies where birth-cloud/ISM distinction is unresolved.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

  • tau_v (float) – V-band optical depth at 5500 Å. [dimensionless]

  • law (str, optional) – Attenuation curve name, resolved from DUST_LAWS registry. Default: “power_law”.

  • f_obscuration (float, optional) – Unattenuated sightline fraction in clumpy geometry (Lower 2022). [dimensionless, in [0, 1]] Default: 0.0 (uniform foreground screen).

  • **law_params – Keyword arguments passed to the attenuation curve function (e.g., n_slope, dust_bump_strength, dust_delta, dust_Rv).

Returns:

Multiplicative transmission T(λ) ∈ [0, 1]. [dimensionless]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

Gradient-safe: yes — differentiable everywhere.

The transmission is:

\[T(\lambda) = f_{\rm obs} + (1 - f_{\rm obs}) \cdot \exp[-\tau_V \, k(\lambda)]\]

where \(k(\lambda)\) is the normalized attenuation curve with \(k(5500 \, \text{\AA}) = 1\), \(\tau_V\) is the V-band optical depth, and \(f_{\rm obs}\) is the fraction of unattenuated sightlines (Lower 2022; default 0 = full screen).

Age independence: Unlike two-component models, there is no age-dependence, so this transmission can be factored out of the stellar population age integration, enabling faster computation.

Geometry: When \(f_{\rm obs} = 0\), this recovers the standard Beer-Lambert foreground screen. When \(f_{\rm obs} > 0\), it models a clumpy geometry where a fraction of photons are unattenuated (Lower 2022).

References

tengri.components.dust.attenuation.single_component_dust_fast(wavelengths: Array, n_ages: int, tau_v: float, law: str = 'power_law', f_obscuration: float = 0.0, **law_params) Array[source]

Single-component dust attenuation broadcast to (n_ages, n_wave).

Computes exp() on the 1-D wavelength grid only, then broadcasts to (n_ages, n_wave) via jnp.broadcast_to (zero-copy in XLA). This is the production path used by the SED pipeline.

Parameters:
  • wavelengths (array_like, shape (n_wave,)) – Evaluation wavelengths (rest-frame). [Å]

  • n_ages (int) – Number of SSP age bins (for output shape). [dimensionless]

  • tau_v (float) – V-band optical depth. [dimensionless]

  • law (str) – Attenuation curve name (from DUST_LAWS registry). Default: “power_law”.

  • f_obscuration (float) – Fraction of unattenuated sightlines. [dimensionless, in [0, 1]] Default: 0.0 (Lower 2022).

  • **law_params – Passed to curve function.

Returns:

Multiplicative transmission factor in [0, 1]. [dimensionless] All age rows are identical (age-independent attenuation).

Return type:

ndarray, shape (n_ages, n_wave)

Notes

JIT-compatible: yes — all operations are jnp primitives.

Gradient-safe: yes — differentiable everywhere.

Memory efficiency: Using jnp.broadcast_to avoids materializing the full (n_ages, n_wave) grid in memory; the result is a zero-copy view.

tengri.components.dust.attenuation.smc(wavelength: Array, **_kwargs) Array[source]

SMC Bar extinction curve (Pei 1992, ApJ, 395, 130).

Steep UV rise, NO 2175 Å bump. Common at high redshift. R_V = 2.93. Uses generalized Drude profile sum — fully continuous, no piecewise boundaries.

Parameters:

wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

Returns:

Normalized extinction curve k(λ). [dimensionless]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

From Pei (1992) Table 4: Small Magellanic Cloud Bar parameters, 6 Drude components with R_V = 2.93 (relatively grey).

References

P. G. Pei, “Interstellar Dust from the Ultraviolet to the Infrared,” ApJ, 395, 130 (1992).

tengri.components.dust.attenuation.tea(wavelength: Array, dust_delta: float = -0.2, dust_tea_scatter: float = 0.0, **_kwargs) Array[source]

TEA attenuation curve (Haskell+2024, NIHAO-SKIRT).

Three-parameter empirical attenuation with physically motivated bump-slope correlation from radiative transfer simulations. The functional form is identical to Kriek & Conroy (2013), but E_b is derived from delta via a tight relation calibrated on NIHAO-SKIRT.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

  • dust_delta (float) – Power-law slope modification. [dimensionless] Default: -0.2. Steeper (more negative) = weaker bump.

  • dust_tea_scatter (float) – Scatter in E_b around the median relation. [dex] Default: 0.0 (median).

Returns:

Attenuation curve k(λ), normalized to k(5500 Å) = 1. [dimensionless]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

The bump amplitude is derived from the slope via:

\[E_b = 2.5 \times \exp(3.5 \times \delta) \times 10^{\text{scatter}}\]

then uses Kriek & Conroy (2013) functional form with the derived E_b.

References

tengri.components.dust.attenuation.two_component_dust(wavelength: Array, age_grid: Array, tau_v1: float, tau_v2: float, law_bc: str = 'power_law', law_diff: str = 'power_law', f_obscuration: float = 0.0, t_birth: float = 10000000.0, transition_width: float = 0.3, **law_params) Array[source]

Two-component dust attenuation following Charlot & Fall (2000) with smooth age transition.

Separates dust into birth-cloud (young stars) and diffuse ISM (all stars) components with independent optical depths and attenuation curves. Transition between components uses a smooth sigmoid in log-age, enabling automatic differentiation.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

  • age_grid (array_like, shape (n_ages,)) – Stellar population ages. [yr]

  • tau_v1 (float) – Birth-cloud V-band optical depth (at 5500 Å). [dimensionless] Note: tengri applies tau_bc internally but exposes tau_v1 after normalizing by attenuation curve slope. See docs/known_bugs.md (CROSSVAL-01) for cross-code comparison.

  • tau_v2 (float) – Diffuse ISM V-band optical depth. [dimensionless]

  • law_bc (str, optional) – Attenuation curve name for birth cloud. Default: “power_law”. Resolved from DUST_LAWS registry.

  • law_diff (str, optional) – Attenuation curve name for diffuse ISM. Default: “power_law”.

  • f_obscuration (float, optional) – Fraction of unattenuated sightlines in clumpy geometry (Lower 2022). [dimensionless, in [0, 1]] Default: 0.0 (uniform screen).

  • t_birth (float, optional) – Birth-cloud dispersal age (sigmoid center). [yr] Default: 1e7 (10 Myr).

  • transition_width (float, optional) – Sigmoid transition width in dex. [dimensionless] Default: 0.3 (~5-20 Myr range).

  • **law_params – Keyword arguments passed to attenuation curve functions (e.g., n_slope, dust_bump_strength, dust_delta, dust_Rv).

Returns:

Multiplicative transmission factor T(λ, t_age), where T ∈ [0, 1]. [dimensionless]

Return type:

ndarray, shape (n_ages, n_wave)

Notes

JIT-compatible: yes — all operations are jnp primitives and safe for jax.jit.

Gradient-safe: yes — differentiable everywhere; smooth sigmoid age transition preserves gradients through the birth-cloud boundary.

The total optical depth is:

\[\tau(\lambda, t_{\text{age}}) = w(t_{\text{age}}) \cdot \tau_{{\rm V,BC}} \cdot k_{\rm BC}(\lambda) + \tau_{{\rm V,ISM}} \cdot k_{\rm ISM}(\lambda)\]

where \(w(t_{\text{age}})\) is the sigmoid weight:

\[w(t_{\text{age}}) = \sigma\left(-\frac{\log_{10} t_{\text{age}} - \log_{10} t_{\text{birth}}}{\Delta_{\text{trans}}}\right)\]

and \(\sigma(x) = 1/(1 + e^{-x})\) is the logistic sigmoid. The transmission is then:

\[T(\lambda, t_{\text{age}}) = f_{\rm obs} + (1 - f_{\rm obs}) \cdot \exp[-\tau(\lambda, t_{\text{age}})]\]

where \(f_{\rm obs}\) is the unattenuated sightline fraction.

Upstream: Implements the Charlot & Fall (2000) two-component framework [1]_ with sigmoid age transition following tengri’s differentiable design. Birth-cloud + diffuse ISM separation enables realistic modeling of age-dependent dust geometry in galaxies.

References

Examples

>>> import jax.numpy as jnp
>>> from tengri import two_component_dust
>>> wave = jnp.linspace(1000.0, 30000.0, 300)
>>> ages = jnp.logspace(6.0, 10.14, 64)
>>> T = two_component_dust(wave, ages, tau_v1=1.0, tau_v2=0.3)
>>> T.shape
(64, 300)
tengri.components.dust.attenuation.two_component_dust_fast(wavelengths: Array, dust_age_weights: Array, tau_v1: float, tau_v2: float, law_bc: str = 'power_law', law_diff: str = 'power_law', f_obscuration: float = 0.0, **law_params) Array[source]

Fast dust attenuation using precomputed age weights.

Avoids recomputing the birth-cloud age sigmoid every call. Used by both the fused kernel (at effective wavelengths) and the exact path (at the full wavelength grid).

The output dtype follows the input wavelengths dtype, enabling mixed-precision: pass float32 arrays to halve memory traffic on the (n_ages, n_wave) intermediates (~1.6x speedup on CPU).

Parameters:
  • wavelengths (array_like, shape (n_wave,)) – Evaluation wavelengths (rest-frame). [Å] Can be the full SSP grid or just the filter effective wavelengths.

  • dust_age_weights (array_like, shape (n_ages,)) – Pre-computed sigmoid weights from precompute_dust_age_weights. Computed once at Model init.

  • tau_v1 (float) – Birth-cloud V-band optical depth. [dimensionless]

  • tau_v2 (float) – Diffuse ISM V-band optical depth. [dimensionless]

  • law_bc (str) – Attenuation curve name for birth cloud. [dimensionless] Default: “power_law”. Looked up in DUST_LAWS registry.

  • law_diff (str) – Attenuation curve name for diffuse ISM. Default: “power_law”.

  • f_obscuration (float) – Fraction of unattenuated sightlines. [dimensionless, in [0, 1]] Default: 0.0 (Lower 2022).

  • **law_params – Passed to curve functions: n_slope, dust_bump_strength, dust_delta, dust_Rv, etc.

Returns:

Multiplicative attenuation factor in [0, 1]. [dimensionless]

Return type:

ndarray, shape (n_ages, n_wave)

Notes

JIT-compatible: yes — all operations are jnp primitives.

Gradient-safe: yes — differentiable everywhere.

tengri.components.dust.attenuation.two_component_dust_separable(wavelength: Array, dust_age_weights: Array, tau_v1: float, tau_v2: float, law_bc_fn: Callable, law_diff_fn: Callable, f_obscuration: float = 0.0, **law_params) Array[source]

Optimized two-component dust attenuation with factorized age-independent term.

Exploits the exponential factorization exp(a + b) = exp(a) · exp(b) to separate the diffuse ISM component from the age-dependent outer product. The diffuse exponentiation operates on (n_wave,) instead of (n_ages, n_wave), saving one full-grid exponential. Accepts pre-resolved law functions to avoid dict lookups in hot code.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

  • dust_age_weights (array_like, shape (n_ages,)) – Pre-computed sigmoid birth-cloud weights from precompute_dust_age_weights. Computed once at Model init and cached.

  • tau_v1 (float) – Birth-cloud V-band optical depth. [dimensionless]

  • tau_v2 (float) – Diffuse ISM V-band optical depth. [dimensionless]

  • law_bc_fn (Callable) – Pre-resolved birth-cloud attenuation function (e.g., resolve_dust_law("calzetti")).

  • law_diff_fn (Callable) – Pre-resolved diffuse ISM attenuation function.

  • f_obscuration (float, optional) – Unattenuated sightline fraction. [dimensionless, in [0, 1]] Default: 0.0.

  • **law_params – Keyword arguments passed to both law functions.

Returns:

Multiplicative transmission T(λ, t_age) ∈ [0, 1]. [dimensionless]

Return type:

ndarray, shape (n_ages, n_wave)

Notes

JIT-compatible: yes — all operations are jnp primitives.

Gradient-safe: yes — differentiable everywhere.

Performance: Reduces memory traffic by ~40% on (n_ages, n_wave) grids relative to two_component_dust because the diffuse exponential is computed on (n_wave,) and broadcast rather than materialized as (n_ages, n_wave). Significant speedup on CPU; moderate benefit on GPU (memory bandwidth more abundant).

The transmission factorizes as:

\[T(\lambda, t_{\text{age}}) = T_{\rm BC}(\lambda, t_{\text{age}}) \cdot T_{\rm ISM}(\lambda)\]

where

\[T_{\rm BC}(\lambda, t_{\text{age}}) = f_{\rm obs} + (1 - f_{\rm obs}) \, \exp[-w(t_{\text{age}}) \, \tau_{\rm V,BC} \, k_{\rm BC}(\lambda)]\]
\[T_{\rm ISM}(\lambda) = \exp[-\tau_{\rm V,ISM} \, k_{\rm ISM}(\lambda)]\]

The ISM component is computed once on (n_wave,) and then broadcast with the age-dependent birth-cloud term, avoiding the full (n_ages, n_wave) grid in intermediate storage.

References

tengri.components.dust.attenuation.vw07_bc(wavelength: Array, **_kwargs) Array[source]

Wild+2007 birth cloud: power-law with n = -1.3 (steep UV).

Steeper than the diffuse ISM curve, reflecting the denser dust geometry around young stellar populations.

Parameters:

wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

Returns:

Normalized attenuation curve k(λ), where k(5500 Å) = 1. [dimensionless]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

The attenuation is:

\[k(\lambda) = \left(\frac{\lambda}{5500 \, \text{\AA}}\right)^{-1.3}\]

References

tengri.components.dust.attenuation.vw07_diff(wavelength: Array, **_kwargs) Array[source]

Wild+2007 diffuse ISM: power-law with n = -0.7 (standard CF00).

Parameters:

wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

Returns:

Normalized attenuation curve k(λ), where k(5500 Å) = 1. [dimensionless]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

The attenuation is:

\[k(\lambda) = \left(\frac{\lambda}{5500 \, \text{\AA}}\right)^{-0.7}\]

References

tengri.components.dust.attenuation.wd01_mwrv31(wavelength: Array, **_kwargs) Array[source]

Weingartner & Draine (2001) MW R_V=3.1 grain model attenuation curve.

Physically motivated dust grain-size + composition distribution for Milky Way diffuse ISM dust: prominent 2175 Å bump, R_V = 3.1.

Parameters:

wavelength (array_like, shape (n_wave,)) – Wavelength grid [Å].

Returns:

Normalized attenuation curve k(λ), k(5500 Å) = 1 [dimensionless].

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — uses jnp.interp on precomputed curve.

Upstream credit: grain model data from Weingartner & Draine (2001) [1]_, accessed via the dust-extinction astropy-affiliated package. Independent implementation in synthesizer (Wilkins et al. 2025 [2]_).

References

tengri.components.dust.attenuation.wd01_smcbar(wavelength: Array, **_kwargs) Array[source]

Weingartner & Draine (2001) SMC Bar grain model attenuation curve.

Physically motivated dust grain-size + composition distribution for SMC-like dust: steep UV rise, no 2175 Å bump. Most relevant for high-redshift galaxies.

Parameters:

wavelength (array_like, shape (n_wave,)) – Wavelength grid [Å].

Returns:

Normalized attenuation curve k(λ), k(5500 Å) = 1 [dimensionless].

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — uses jnp.interp on precomputed curve.

Precomputed from dust_extinction.grain_models.WD01("SMCBar") at module import time. Data cover ~100–107 Å. Values outside the tabulated range are held constant at boundary values.

Upstream credit: grain model data from Weingartner & Draine (2001) [1]_, accessed via the dust-extinction astropy-affiliated package. Independent implementation in synthesizer (Wilkins et al. 2025 [2]_).

References

tengri.components.dust.attenuation.wg00_cloudy(wavelength: Array, tau_v: float, law: str = 'cardelli', **law_params) Array[source]

Witt & Gordon (2000) CLOUDY dust geometry — homogeneous dust-star mix.

Stars and dust are uniformly mixed throughout a slab of total V-band optical depth. The analytic solution integrates radiative transfer, producing a wavelength-dependent transmission that is greyer (less wavelength-dependent) than a foreground screen. Realistic for galaxies with well-mixed ISM (e.g., starburst regions).

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

  • tau_v (float) – Total V-band optical depth through the slab. [dimensionless]

  • law (str, optional) – Underlying attenuation curve name, from DUST_LAWS registry. Default: “cardelli” (MW).

  • **law_params – Keyword arguments passed to the attenuation curve function (e.g., dust_Rv).

Returns:

Transmission T(λ) ∈ [0, 1]. [dimensionless]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives, with numerically stable Taylor expansion for small optical depth.

Gradient-safe: yes — differentiable everywhere via smooth blending between exact and Taylor regimes.

The transmission for a homogeneous slab is:

\[T(\lambda) = \frac{1 - \exp[-\tau_V \, k(\lambda)]}{\tau_V \, k(\lambda)}\]

where \(k(\lambda)\) is the normalized attenuation curve. This follows the solution of radiative transfer in a uniform dust-star slab (Natta & Panagia 1984, Section 3.1; Calzetti et al. 1994).

Limiting behaviour: At low optical depth (\(\tau_V k \ll 1\)), \(T \to 1\) (transparent). At high optical depth, \(T \approx 1/(\tau_V k)\), producing a greyer (less wavelength-dependent) effective attenuation than the foreground screen because stars near the observer-facing side suffer less extinction.

Numerical stability: Uses a Taylor expansion (correct to order \(\tau^3\)) for \(\tau_V k < 10^{-4}\) to avoid division-by-zero, preserving gradients throughout.

Approximation: The analytic solution assumes pure absorption (zero scattering). Witt & Gordon (2000) Monte Carlo simulations including scattering find the effective attenuation is slightly greyer still. The analytic form captures the dominant geometric effect and is widely used in SED fitting codes (e.g., Synthesizer, CIGALE).

References

tengri.components.dust.attenuation.wg00_dusty(wavelength: Array, tau_v: float, law: str = 'cardelli', n_clumps: float = 10.0, **law_params) Array[source]

Witt & Gordon (2000) DUSTY geometry — clumpy two-phase medium.

The ISM is modelled as n_clumps identical clumps, each with optical depth tau_clump = tau_V / n_clumps, distributed along random sightlines (Natta & Panagia 1984; Hobson & Padman 1993). The probability of a photon traversing N clumps follows a Poisson distribution, giving the mean transmission.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

  • tau_v (float) – Total V-band optical depth (= n_clumps × tau_clump). [dimensionless]

  • law (str) – Underlying extinction curve name. Default: “cardelli” (MW).

  • n_clumps (float) – Mean number of clumps along a sightline. [dimensionless] Default: 10.0. Higher values approach the homogeneous limit. Typical range: 1–40.

  • **law_params – Passed to the extinction curve function (e.g., dust_Rv).

Returns:

Transmission T(λ) in [0, 1]. [dimensionless]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

Gradient-safe: yes — differentiable everywhere.

The transmission is:

\[T(\lambda) = \exp[-n_{\rm clumps} (1 - \exp[-\tau_{\rm clump} \, k(\lambda)])]\]

where \(\tau_{\rm clump} = \tau_V / n_{\rm clumps}\).

This produces the greyest (least wavelength-dependent) effective attenuation of the three WG00 geometries because photons preferentially escape through low-column channels between clumps.

Limiting behaviour: - \(n_{\rm clumps} \to \infty\) (fixed \(\tau_V\)): recovers the homogeneous slab. - \(n_{\rm clumps} = 1\): single clump with Poisson averaging. - \(\tau_V = 0\): T = 1 (transparent), regardless of n_clumps.

References

tengri.components.dust.attenuation.wg00_shell(wavelength: Array, tau_v: float, law: str = 'cardelli', **law_params) Array[source]

Witt & Gordon (2000) SHELL geometry — foreground screen.

The simplest geometry: a uniform dust slab in front of all stars. Transmission is the standard Beer-Lambert law.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Wavelength grid. [Å]

  • tau_v (float) – V-band optical depth (at 5500 Å). [dimensionless]

  • law (str) – Underlying extinction curve name. Default: “cardelli” (MW).

  • **law_params – Passed to the extinction curve function (e.g., dust_Rv).

Returns:

Transmission T(λ) in [0, 1]. [dimensionless]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

Gradient-safe: yes — differentiable everywhere.

The transmission is:

\[T(\lambda) = \exp[-\tau_V k(\lambda)]\]

This is identical to single_component_dust with f_obscuration=0 and is included for completeness alongside the CLOUDY and DUSTY models.

References

Dust Emission

Energy-balanced IR dust emission models.

Dust emission models for tengri.

This module implements IR re-emission of UV/optical light absorbed by dust. All models are pure JAX (JIT-compatible, fully differentiable) and follow the energy-balance constraint: total IR luminosity equals total absorbed luminosity from the attenuation step.

Available Emission Models

  • modified_blackbody: Optically-thin modified blackbody (2-3 params)

  • casey2012: Casey (2012) modified blackbody + mid-IR power law (3 params)

  • pah_drude: Smith et al. (2007) PAH Drude profiles (0 params, pure template)

  • dale2014: Dale et al. (2014) 1-parameter IR template family (tabulated)

  • draine_li2007: Draine & Li (2007) 3-parameter model (tabulated)

  • draine_li2014: Draine & Li (2014 update) 4-parameter model (tabulated)

  • astrodust: Hensley & Draine (2023) Astrodust+PAH model (tabulated)

  • bosa: Boquien & Salim (2021) (L_TIR, sSFR)-parameterized model (tabulated)

  • themis: Jones et al. (2017) THEMIS/DustEM model (tabulated)

Template Auto-Loading

The "draine_li2007", "dale2014", "draine_li2014", "astrodust", "bosa", and "themis" models auto-load tabulated templates from the data/ directory on first use. If templates are not found, they fall back to analytic approximations with a warning. The analytic fallbacks are crude (single-Gaussian PAH, hand-tuned temperatures) and should NOT be used for science.

Energy Balance

The normalization for every model is set by:

L_dust_emission = L_dust_absorbed
                = integral[(1 - transmission) * L_stellar_intrinsic * dlambda]

This is computed from the attenuation step and passed to each model as L_absorbed (scalar, in Lsun).

References

  • Casey 2012, MNRAS, 425, 3094

  • Dale et al. 2014, ApJ, 784, 83

  • Draine & Li 2007, ApJ, 657, 810

  • Draine & Li 2014 update (CIGALE implementation, Boquien+2019)

  • Aniano et al. 2012, ApJ, 756, 138

  • da Cunha et al. 2013, ApJ, 766, 13

  • Hildebrand 1983, QJRAS, 24, 267

  • Hensley & Draine 2023, ApJ, 948, 55 (Astrodust+PAH)

  • Boquien & Salim 2021, A&A, 653, A149 (BOSA templates)

  • Jones et al. 2017, A&A, 602, A46 (THEMIS dust model)

tengri.components.dust.emission.apply_dust_emission(model_name: str, wavelength_aa: Array, L_absorbed: float, **params) Array[source]

Apply a named dust emission model.

Dispatches to a registered model function by name.

Parameters:
  • model_name (str) – Registered model name (e.g. “modified_blackbody”, “draine_li2007”).

  • wavelength_aa (array_like, shape (n_wave,)) – Wavelength grid. [Å]

  • L_absorbed (float) – Absorbed luminosity. [Lsun]

  • **params – Model-specific keyword arguments (e.g., dust_T, dust_umin, dust_gamma_dl).

Returns:

Dust emission L_ν. [Lsun Hz⁻¹]

Return type:

ndarray, shape (n_wave,)

Raises:

ValueError – If model_name is not registered.

Notes

JIT-compatible: yes if the underlying model is JIT-compatible.

tengri.components.dust.emission.astrodust(*args, **kwargs)[source]

Astrodust+PAH (Hensley & Draine 2023) — dispatches to the registry.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelength grid. [Å]

  • L_absorbed (float) – Total absorbed luminosity. [L_sun]

  • **kwargs – Model-specific parameters. See registered function for details.

Returns:

Dust emission L_ν [erg/s/Hz or L_sun/Hz].

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes (returned function is JIT-compiled).

Dispatches to the lazy-loaded Astrodust+PAH template model. Auto-loads HDF5 templates on first call.

tengri.components.dust.emission.bosa(*args, **kwargs)[source]

BOSA (Boquien & Salim 2021) — dispatches to the registry.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelength grid. [Å]

  • L_absorbed (float) – Total absorbed luminosity. [L_sun]

  • **kwargs – Model-specific parameters. See registered function for details.

Returns:

Dust emission L_ν [erg/s/Hz or L_sun/Hz].

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes (returned function is JIT-compiled).

Dispatches to the lazy-loaded BOSA template model. Auto-loads HDF5 templates on first call. BOSA parameterizes dust by (L_TIR, sSFR).

tengri.components.dust.emission.casey2012(wavelength_aa: Array, L_absorbed: float, dust_T: float = 35.0, dust_beta_ir: float = 1.8, dust_alpha_mir: float = 2.0, optically_thin: bool = False, redshift: float = 0.0, **_kwargs) Array[source]

Casey (2012) modified blackbody + mid-IR power law dust emission.

Combines a modified blackbody (FIR peak from cold/warm dust) with a mid-IR power law (Wien-side excess from warm dust continuum), joined by a smooth sigmoid transition function.

When optically_thin=True, the mid-IR power-law component is zeroed, leaving only the modified blackbody. This variant is useful for cold dust-dominated galaxies where the power law is unphysical.

Note

The mid-IR power-law contribution is only significant for warm/hot dust (T ≳ 60 K). For typical cold ISM dust (T = 25–60 K) the Wien cutoff exp(-hν/kT) kills the power-law component at 8–40 μm (x ≈ 10–51 at those wavelengths), so the model produces less 8–40 μm flux than a pure MBB normalised to the same L_absorbed. The 8–40 μm advantage described in Casey (2012) applies to warmer starburst / AGN-heated dust components where T ≳ 80–100 K.

The implemented model uses the following convention (see code comments):

S(ν) = N_pl * ν^α_mid * f(λ)         [mid-IR power law, f→1 at short λ]
     + N_bb * ν^(3+β) / (exp(hν/kT) - 1) * (1 - f(λ))   [FIR MBB, 1-f→1 at long λ]

where the transition function (f→1 selects power law at short λ) is:

f(λ) = 1 / (1 + (λ / λ_0)^2)

Note: Casey (2012, MNRAS 425 3094) Eq. 2 defines the carrier function differently; the code’s convention has f→1 at short λ (mid-IR) and 1-f→1 at long λ (FIR). The shapes produced are equivalent; only the labelling of f vs (1-f) differs.

The empirical turnover wavelength is (Eq. 3, with errata):

λ_0 = b1 + b2 * T[μm]

with b1 = 26.68 μm, b2 = 6.246e-3 μm/K.

Both components are normalized so that the total frequency integral equals L_absorbed.

When redshift > 0, the dust temperature is corrected for CMB heating (da Cunha et al. 2013) and the observed flux is reduced by the CMB contrast factor.

Parameters:
  • wavelength_aa (array, shape (n_wave,)) – Wavelength grid in Angstrom (sorted ascending). [Å]

  • L_absorbed (float) – Total absorbed luminosity in Lsun (sets the normalization). [L_sun]

  • dust_T (float) – Dust temperature in Kelvin. Typical range: 25–60 K. [K]

  • dust_beta_ir (float) – Dust emissivity index for the MBB component. Typical range: 1.5–2.0. [dimensionless]

  • dust_alpha_mir (float) – Mid-IR power-law slope. Typical range: 1.5–2.5. [dimensionless]

  • optically_thin (bool) – If True, zero the mid-IR power-law component, leaving only the modified blackbody. Default: False. [dimensionless]

  • redshift (float) – Source redshift. When > 0, CMB heating correction is applied. Default 0 (no correction). [dimensionless]

Returns:

Dust emission L_nu in Lsun/Hz.

Return type:

array, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

Gradient-safe: yes — differentiable everywhere.

References

Casey, C. M., 2012, MNRAS, 425, 3094. da Cunha, E. et al., 2013, ApJ, 766, 13 (CMB corrections).

tengri.components.dust.emission.cmb_contrast_factor(wavelength_aa: Array, T_eff: float, redshift: float) Array[source]

Flux suppression factor from observing dust against the CMB.

The observed flux is reduced because the galaxy’s dust emission is measured against the CMB background (da Cunha et al. 2013).

Parameters:
  • wavelength_aa (array_like, shape (n_wave,)) – Wavelength grid. [Å]

  • T_eff (float) – CMB-corrected effective dust temperature. [K]

  • redshift (float) – Source redshift. [dimensionless]

Returns:

Multiplicative contrast factor in [0, 1]. [dimensionless]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

The contrast factor is:

\[C(\lambda) = 1 - \frac{B_\nu(T_{\rm CMB}(z))}{B_\nu(T_{\rm eff})}\]

Since \(T_{\rm eff} > T_{\rm CMB}(z)\), we have \(0 \leq C(\lambda) \leq 1\).

tengri.components.dust.emission.cmb_corrected_temperature(T_dust: float, redshift: float, beta_ir: float = 1.6) float[source]

Effective dust temperature including CMB heating.

At high redshift the CMB sets a temperature floor on dust grains. The effective equilibrium temperature is (da Cunha et al. 2013).

Parameters:
  • T_dust (float) – Intrinsic dust temperature (what the galaxy would have at z=0 in isolation). [K]

  • redshift (float) – Source redshift. [dimensionless]

  • beta_ir (float) – Dust emissivity index. [dimensionless] Default: 1.6.

Returns:

Effective dust temperature including CMB heating. [K]

Return type:

float

Notes

JIT-compatible: yes — all operations are jnp primitives.

The effective temperature is:

\[T_{\rm eff} = \left[T_{\rm dust}^{4+\beta} + T_{\rm CMB}(z)^{4+\beta} - T_{\rm CMB}(z=0)^{4+\beta}\right]^{1/(4+\beta)}\]

where \(T_{\rm CMB}(z) = T_{\rm CMB,0} (1 + z)\) with \(T_{\rm CMB,0} = 2.725\) K.

tengri.components.dust.emission.compute_absorbed_luminosity(wavelength_aa: Array, L_nu_intrinsic: Array, transmission: Array) float[source]

Compute total luminosity absorbed by dust.

Integrates (1 - transmission) × L_nu over frequency to get the total absorbed energy, which must be re-emitted in the IR (energy balance).

Parameters:
  • wavelength_aa (array_like, shape (n_wave,)) – Rest-frame wavelength grid. [Å] Must be sorted ascending.

  • L_nu_intrinsic (array_like, shape (n_wave,)) – Intrinsic (dust-free) luminosity density. [Lsun Hz⁻¹]

  • transmission (array_like, shape (n_wave,)) – Dust transmission fraction in [0, 1]. For age-dependent models this should be the SFH-weighted effective transmission.

Returns:

Total absorbed luminosity. [Lsun]

Return type:

float

Notes

JIT-compatible: yes — all operations are jnp primitives.

The absorbed luminosity is:

\[L_{\rm absorbed} = \int [1 - T(\lambda)] L_\nu(\lambda) d\nu\]

where the integral is over frequency (ν is descending as λ is ascending).

tengri.components.dust.emission.compute_absorbed_luminosity_from_tau(wavelength_aa: Array, L_nu_intrinsic: Array, tau_lambda: Array) float[source]

Compute total absorbed luminosity from optical depth.

Convenience wrapper when you have τ(λ) rather than transmission T(λ).

Parameters:
  • wavelength_aa (array_like, shape (n_wave,)) – Rest-frame wavelength grid. [Å] Must be sorted ascending.

  • L_nu_intrinsic (array_like, shape (n_wave,)) – Intrinsic luminosity density. [Lsun Hz⁻¹]

  • tau_lambda (array_like, shape (n_wave,)) – Optical depth as a function of wavelength. [dimensionless]

Returns:

Total absorbed luminosity. [Lsun]

Return type:

float

Notes

JIT-compatible: yes — all operations are jnp primitives.

Internally converts τ(λ) to transmission via T(λ) = exp(−τ(λ)) then calls compute_absorbed_luminosity.

tengri.components.dust.emission.dale2014(*args, **kwargs)[source]

Dale et al. (2014) — dispatches to the registry (auto-loads templates).

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelength grid. [Å]

  • L_absorbed (float) – Total absorbed luminosity. [L_sun]

  • **kwargs – Model-specific parameters (alpha). See registered function for details.

Returns:

Dust emission L_ν [erg/s/Hz or L_sun/Hz].

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes (returned function is JIT-compiled).

Dispatches to the lazy-loaded Dale2014 template model. Auto-loads HDF5 templates on first call.

tengri.components.dust.emission.draine_li2007(*args, **kwargs)[source]

Draine & Li (2007) — dispatches to the registry (auto-loads templates).

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelength grid. [Å]

  • L_absorbed (float) – Total absorbed luminosity. [L_sun]

  • **kwargs – Model-specific parameters (alpha, U_min, gamma, q_pah). See registered function for details.

Returns:

Dust emission L_ν [erg/s/Hz or L_sun/Hz].

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes (returned function is JIT-compiled).

Dispatches to the lazy-loaded tabulated DL07 model. Auto-loads HDF5 templates on first call.

tengri.components.dust.emission.draine_li2014(*args, **kwargs)[source]

Draine & Li (2014) — dispatches to the registry (auto-loads templates).

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelength grid. [Å]

  • L_absorbed (float) – Total absorbed luminosity. [L_sun]

  • **kwargs – Model-specific parameters (alpha, U_min, gamma, q_pah). See registered function for details.

Returns:

Dust emission L_ν [erg/s/Hz or L_sun/Hz].

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes (returned function is JIT-compiled).

Dispatches to the lazy-loaded DL14 template model. Auto-loads HDF5 templates on first call. DL14 is the 2014 update to Draine & Li 2007 with additional alpha (radiation field) dependence.

tengri.components.dust.emission.energy_balance_split(wavelength_aa: Array, L_absorbed_stellar: float, L_agn_ir: float = 0.0, eta_balance: float = 1.0, f_cold: float = 0.5, dust_T_warm: float = 45.0, dust_T_cold: float = 20.0, dust_beta_warm: float = 1.5, dust_beta_cold: float = 2.0, redshift: float = 0.0, **_kwargs) Array[source]

Two-temperature energy balance with AGN contribution.

Extends simple eta_balance by decomposing IR into warm (SF-heated) and cold (diffuse ISM) components, plus optional AGN IR contribution.

Parameters:
  • wavelength_aa (array_like, shape (n_wave,)) – Wavelength grid. [Å] Must be sorted ascending.

  • L_absorbed_stellar (float) – Total absorbed stellar luminosity. [Lsun]

  • L_agn_ir (float) – Additional AGN-heated IR luminosity. [Lsun] Default: 0.0.

  • eta_balance (float) – Energy balance parameter: ratio of re-emitted to absorbed stellar luminosity. [dimensionless] Default: 1.0 (strict energy balance).

  • f_cold (float) – Fraction of total IR luminosity in the cold component. [dimensionless, in [0, 1]] Default: 0.5.

  • dust_T_warm (float) – Warm dust temperature. [K] Default: 45.0.

  • dust_T_cold (float) – Cold dust temperature. [K] Default: 20.0.

  • dust_beta_warm (float) – Warm component emissivity index. [dimensionless] Default: 1.5.

  • dust_beta_cold (float) – Cold component emissivity index. [dimensionless] Default: 2.0.

  • redshift (float) – Source redshift. [dimensionless] When > 0, CMB heating correction is applied to both components. Default: 0.0.

Returns:

Dust emission L_ν. [Lsun Hz⁻¹]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

The total IR luminosity budget is:

\[ \begin{align}\begin{aligned}L_{\rm IR,total} = \eta_{\rm balance} L_{\rm absorbed,\star} + L_{\rm AGN,IR}\\L_{\rm warm} = (1 - f_{\rm cold}) L_{\rm IR,total}\\L_{\rm cold} = f_{\rm cold} L_{\rm IR,total}\end{aligned}\end{align} \]

Each component is a modified blackbody (via modified_blackbody).

References

tengri.components.dust.emission.modified_blackbody(wavelength_aa: Array, L_absorbed: float, dust_T: float = 30.0, dust_beta_ir: float = 1.8, redshift: float = 0.0, **_kwargs) Array[source]

Optically-thin modified blackbody dust emission.

The unnormalized spectrum is:

S_nu ~ nu^beta * B_nu(T_dust)

which is then normalized so that the frequency integral equals L_absorbed.

When redshift > 0, the dust temperature is corrected for CMB heating (da Cunha et al. 2013) and the observed flux is reduced by the CMB contrast factor.

Parameters:
  • wavelength_aa (array, shape (n_wave,)) – Wavelength grid in Angstrom (sorted ascending). [Å]

  • L_absorbed (float) – Total absorbed luminosity. Unit-agnostic: the output L_nu will be in the same units per Hz (e.g. pass erg/s → get erg/s/Hz; pass Lsun → get Lsun/Hz). In sed_pipeline.py the pipeline passes erg/s (from a frequency-integrated trapezoid) and receives erg/s/Hz.

  • dust_T (float) – Dust temperature in Kelvin. Typical range: 20–60 K. [K]

  • dust_beta_ir (float) – Emissivity index. Typical range: 1.5–2.0. [dimensionless]

  • redshift (float) – Source redshift. When > 0, CMB heating correction is applied. Default 0 (no correction). [dimensionless]

Returns:

Dust emission L_nu in [L_absorbed units] / Hz.

Return type:

array, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

Gradient-safe: yes — differentiable everywhere.

tengri.components.dust.emission.pah_drude(wavelength_aa: Array, L_absorbed: float, redshift: float = 0.0, **_kwargs) Array[source]

Smith et al. (2007) PAH Drude profiles dust emission.

A sum of 18 PAH Drude profiles (normalized to Smith+2007 SINGS median strengths). Runtime evaluation prefers the precomputed lookup from dust_analytic_precompute; this function provides a direct full-wavelength evaluation used when precompute is unavailable.

Parameters:
  • wavelength_aa (array, shape (n_wave,)) – Wavelength grid in Angstrom (sorted ascending). [Å]

  • L_absorbed (float) – Total absorbed luminosity. [Lsun or erg/s, as passed]

  • redshift (float) – Source redshift (unused for this model). [dimensionless]

Returns:

Dust emission L_nu in [L_absorbed units] / Hz.

Return type:

array, shape (n_wave,)

Notes

JIT-compatible: yes — computed via triweight interpolation in precompute mode; this function is a fallback stub.

Gradient-safe: yes — inherited from precompute lookup.

The PAH template is a pure shape (no free axes). Runtime evaluation uses the precomputed lookup from dust_analytic_precompute and skips the full-wavelength evaluation in the hybrid kernel.

References

tengri.components.dust.emission.planck_bnu(wavelength_aa: Array, temperature: float) Array[source]

Planck function B_ν(T) evaluated at given wavelengths.

Parameters:
  • wavelength_aa (array_like, shape (n_wave,)) – Wavelength grid. [Å]

  • temperature (float) – Blackbody temperature. [K]

Returns:

Planck brightness. [erg s⁻¹ cm⁻² Hz⁻¹ sr⁻¹]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

The Planck function is:

\[B_\nu(T) = \frac{2 h \nu^3}{c^2} \frac{1}{\exp(h\nu / k_B T) - 1}\]

where \(\nu = c / \lambda\) is the frequency, \(h\) is Planck’s constant, \(k_B\) is Boltzmann’s constant, and \(c\) is the speed of light.

tengri.components.dust.emission.preload_emission_model(name: str) Callable[source]

Force lazy template loading outside any JAX JIT scope.

Template-based emission models use lazy loaders that fire on first call. If the first call happens inside a @jax.jit scope, jnp.array() inside the loader creates DynamicJaxprTracer objects that escape into closures, causing UnexpectedTracerError on subsequent non-JIT calls.

Call this function at factory time (outside JIT) so templates are loaded into DUST_EMISSION_MODELS[name] as regular DeviceArray objects. Dynamic JAX indexing inside JIT then works correctly.

Parameters:

name (str) – Registry name (e.g. "draine_li2007").

Returns:

The loaded (real) emission function — NOT a lazy wrapper.

Return type:

Callable

Notes

JIT-compatible: no — template loading happens at factory time.

Safe to call at SEDModel.__init__ time to prevent tracer leaks when models are first called inside a @jax.jit scope.

tengri.components.dust.emission.register_emission_model(name: str) Callable[source]

Decorator factory that registers a dust emission model under a name.

Parameters:

name (str) – Registry key (e.g. "dale2014", "draine_li2007").

Returns:

Decorator that registers the decorated function and returns it unchanged.

Return type:

Callable

Notes

JIT-compatible: no — registration happens at factory time before JIT.

Decorated functions must implement the DustEmissionTemplate protocol: accept a wavelength array, absorbed luminosity, and keyword arguments, returning an emission SED L_ν [erg/s/Hz].

tengri.components.dust.emission.resolve_emission_model(name: str) Callable[source]

Get a registered emission model by name.

Parameters:

name (str) – Model name (e.g. "modified_blackbody").

Returns:

The model function with signature (wavelength, L_absorbed, **params) -> L_nu_emission.

Return type:

Callable

Raises:

ValueError – If name is not in the registry.

Notes

JIT-compatible: no — registry lookup happens at factory time.

The returned function matches the DustEmissionTemplate protocol and can be called with wavelengths, absorbed luminosity, and model-specific parameters.

tengri.components.dust.emission.schreiber2016(wavelength_aa: Array, L_absorbed: float, dust_T: float = 30.0, dust_f_pah: float = 0.05, redshift: float = 0.0, **_kwargs) Array[source]

Schreiber et al. (2016) 2-parameter dust emission model.

Mixes dust continuum and PAH emission by a fractional parameter. The dust continuum is a modified blackbody (modified_blackbody with beta=1.5). The PAH component is approximated as a sum of Drude profiles at standard wavelengths.

Parameters:
  • wavelength_aa (array_like, shape (n_wave,)) – Wavelength grid in Ångstrom (sorted ascending).

  • L_absorbed (float) – Total absorbed luminosity. Unit-agnostic: the output L_nu will be in the same units per Hz.

  • dust_T (float) – Dust continuum temperature in Kelvin. Typical range: 15–60 K. Default: 30.0.

  • dust_f_pah (float) – Fractional contribution from PAH emission in [0, 1]. Default: 0.05.

  • redshift (float) – Source redshift. When > 0, CMB heating correction is applied. Default: 0.

Returns:

Dust emission L_nu in [L_absorbed units] / Hz.

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations are jnp primitives.

The model composition is:

\[L_\nu = (1 - f_{\rm PAH}) L_\nu^{\rm continuum} + f_{\rm PAH} L_\nu^{\rm PAH}\]

where:

  • Dust continuum: modified blackbody with temperature T_dust and emissivity index β = 1.5, using the same normalization as modified_blackbody.

  • PAH: sum of Drude profiles at standard rest wavelengths (3.3, 6.2, 7.7, 8.6, 11.3, 12.7 μm) with relative strengths from Smith et al. (2007).

The total integral over frequency is normalized to L_absorbed.

References

tengri.components.dust.emission.themis(*args, **kwargs)[source]

THEMIS (Jones+2017) — dispatches to the registry.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelength grid. [Å]

  • L_absorbed (float) – Total absorbed luminosity. [L_sun]

  • **kwargs – Model-specific parameters. See registered function for details.

Returns:

Dust emission L_ν [erg/s/Hz or L_sun/Hz].

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes (returned function is JIT-compiled).

Dispatches to the lazy-loaded THEMIS/DustEM template model. Auto-loads HDF5 templates on first call. Uses a-C(:H) aromatic carbon composition rather than PAH fraction.

Dust Priors

Redshift-dependent attenuation priors from cosmological simulations.

Redshift-dependent dust attenuation priors from Narayanan+2018.

Based on cosmological radiative transfer simulations (SIMBA/Narayanan et al. 2018, ApJ, 869, 70), which show systematic trends in attenuation curve shape and optical depth with redshift and stellar mass:

  • Higher-z galaxies have steeper curves (more negative delta)

  • UV bump strength decreases with redshift

  • Optical depth scales with stellar mass and redshift

These functions return dicts of Gaussian distributions suitable for direct use in Parameters kwargs.

References

Narayanan, D., et al. (2018). ApJ, 869, 70.

“A Theory for the Variation of Dust Attenuation Laws in Galaxies”

tengri.components.dust.priors.narayanan_prior(z: float) dict[source]

Recommended dust attenuation priors based on Narayanan+2018 z-dependent trends.

Returns dict of Gaussian distributions for dust_delta and dust_bump_strength, suitable for direct use in Parameters.

Based on Narayanan et al. (2018, ApJ, 869, 70) cosmological RT simulations:

  • Higher-z galaxies have steeper curves (more negative dust_delta)

  • UV bump strength decreases with redshift

Parameters:

z (float) – Source redshift [dimensionless].

Returns:

Keys "dust_delta" and "dust_bump_strength", each mapping to a Gaussian distribution [dimensionless]. Suitable for direct use in Parameters(..., **narayanan_prior(z)).

Return type:

dict

Examples

>>> from tengri import Parameters
>>> from tengri.components.dust.priors import narayanan_prior
>>> spec = Parameters(..., **narayanan_prior(z=2.0))

Notes

JIT-compatible: no — prior specification is a factory-time operation.

Gradient-safe: no — returns static prior distributions.

tengri.components.dust.priors.narayanan_tau_prior(z: float, log_mstar: float = 10.0) dict[source]

Recommended dust optical depth prior based on Narayanan+2018 scaling.

The diffuse-ISM optical depth tau_diff scales with stellar mass and redshift, reflecting the higher dust content in massive, high-z galaxies.

Parameters:
  • z (float) – Source redshift [dimensionless].

  • log_mstar (float, optional) – log₁₀(M_star / M_sun) [dimensionless]. Default: 10.0.

Returns:

Key "dust_tau_diff" mapping to a Gaussian distribution [dimensionless]. Suitable for direct use in Parameters(..., **narayanan_tau_prior(z, log_mstar)).

Return type:

dict

Examples

>>> from tengri import Parameters
>>> from tengri.components.dust.priors import narayanan_tau_prior
>>> spec = Parameters(..., **narayanan_tau_prior(z=1.5, log_mstar=10.5))

Notes

JIT-compatible: no — prior specification is a factory-time operation.

Gradient-safe: no — returns static prior distributions.

AGN

Accretion disc, torus, BLR, NLR, and unified AGN models.

Accretion disc models for AGN emission.

Four models are provided:

  1. Simple power-law + UV cutoff — minimal AGN disc with 3 parameters.

  2. Multi-color disc (Shakura-Sunyaev) — physically-motivated standard thin disc following Kubota & Done (2018), simplified to the key parameters. Implements the outer standard disc zone only.

  3. Kubota & Done 3-zone disc — full K&D (2018) model with outer standard disc, warm Comptonization (soft X-ray excess), and hot corona (hard X-ray power law). Three radially-stratified zones with self-consistent radii.

  4. ADAF + truncated disc — for low-luminosity AGN (L/L_Edd < 0.01). The inner disc transitions to an advection-dominated accretion flow (optically thin, radiatively inefficient). Based on Mahadevan (1997) and Nemmen+2014.

All return specific luminosity L_nu in erg/s/Hz as a function of rest-frame wavelength. All functions are pure JAX and JIT-compilable.

Physical constants are in CGS. Wavelength inputs are in Angstrom.

References

  • Shakura & Sunyaev 1973, A&A, 24, 337

  • Kubota & Done 2018, MNRAS, 480, 1247

  • Nandra & Pounds 1994, MNRAS, 268, 405 (power-law slopes)

  • Done et al. 2012, MNRAS, 420, 1848 (QSOSED)

  • Mahadevan 1997, ApJ, 477, 585 (ADAF spectra)

  • Nemmen et al. 2014, MNRAS, 438, 2804 (ADAF modelling)

  • Lopez et al. 2024 (ADAF + truncated disc for LLAGN)

  • Beloborodov 1999, ApJ, 510, L123 (self-consistent Gamma_hot)

tengri.components.agn.disc.adaf_disc(wavelength: Array, agn_log_lbol: float, agn_frac: float = 0.1, agn_log_mbh: float = 8.0, agn_log_ledd: float = -3.0, agn_r_tr: float = 100.0, agn_adaf_beta: float = 0.5, agn_adaf_delta: float = 0.01, agn_cos_inc: float = 0.5, n_radii: int = 50, **_kwargs) Array[source]

Advection-dominated accretion flow (ADAF) plus truncated disc.

Model a low-luminosity AGN in the sub-Eddington regime where the inner accretion flow transitions from a geometrically thin, optically thick disc to an advection-dominated accretion flow (ADAF). In an ADAF, most of the released gravitational energy is advected into the black hole rather than radiated away, making the flow radiatively inefficient. The outer disc remains as a Shakura-Sunyaev disc truncated at a transition radius r_tr.

The total spectrum is the sum of four components:

  1. ADAF synchrotron (radio to millimeter): relativistic electrons gyrating in a turbulent magnetic field.

  2. ADAF bremsstrahlung (X-ray): thermal bremsstrahlung from collisions in the hot electron-ion plasma (T_e ~ 10^9–10^11 K).

  3. ADAF inverse Compton (hard X-ray): upscattering of bremsstrahlung photons by relativistic electrons.

  4. Truncated outer disc (UV/optical): Shakura-Sunyaev multi-color disc from r_tr outward.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelength grid. [Angstrom]

  • agn_log_lbol (float) – Total AGN bolometric luminosity. [log10(L_sun)]

  • agn_frac (float, optional) – Fraction of bolometric luminosity emitted by this component. Default: 0.1. [dimensionless, 0–1]

  • agn_log_mbh (float, optional) – Black hole mass. Scales the synchrotron peak frequency and gravitational radius. Default: 8.0. [log10(M_sun)]

  • agn_log_ledd (float, optional) – Eddington ratio. This model is designed for sub-Eddington accretion (log(L/L_Edd) < -2). Default: -3.0. [log10(L / L_Edd)]

  • agn_r_tr (float, optional) – Truncation radius in units of the gravitational radius R_g = GM/c^2. This marks the transition between the ADAF (r < r_tr) and the thin disc (r > r_tr). Typical range: 10–1000 R_g. Default: 100.0. [dimensionless, ≥ 6]

  • agn_adaf_beta (float, optional) – Ratio of gas pressure to total pressure in the ADAF. Controls the magnetic field strength and synchrotron emission. Range: [0, 1]. Default: 0.5. [dimensionless]

  • agn_adaf_delta (float, optional) – Fraction of viscously dissipated energy that directly heats electrons (as opposed to protons). Controls the electron temperature. Lower δ → hotter electrons. Range: [0, 1]. Default: 0.01. [dimensionless]

  • agn_cos_inc (float, optional) – Cosine of the inclination angle (outer disc view angle). Default: 0.5 (60°). [dimensionless, 0.01–1.0]

  • n_radii (int, optional) – Number of radial integration points for the outer disc component. Default: 50.

Returns:

Spectral luminosity density L_ν. [erg/s/Hz]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — uses jnp primitives.

Radiative efficiency (Mahadevan 1997, Eq. 49): In an ADAF, the radiated luminosity scales as:

\[L_{\rm rad} \approx 0.1 \, \dot{m}^2 \, L_{\rm Edd}\]

where \(\dot{m} = L / L_{\rm Edd}\) is the dimensionless accretion rate. For sub-Eddington rates (\(\dot{m} \lesssim 0.1\)), the radiated luminosity is highly suppressed. The ADAF component luminosity is estimated from the fractional contribution determined by the truncation radius.

ADAF electron temperature (Mahadevan 1997, Eq. 40): The electron temperature is set by the energy-balance equation:

\[T_e = 1.1 \times 10^9 \, \mathrm{K} \cdot \left(\frac{\delta}{\dot{m}}\right)^{1/7}\]

where \(\delta\) is the electron heating fraction and \(\dot{m}\) is the dimensionless accretion rate. The exponent 1/7 (not 1/2) comes from the coupled electron-proton heating equations. Clipped to [10^8, 5×10^11] K for physical plausibility.

Synchrotron component (Mahadevan 1997, Eq. 21, 25): Electrons in the turbulent ADAF magnetic field produce synchrotron emission with a characteristic peak frequency:

\[\nu_{\rm peak} \approx 10^{12} \, \mathrm{Hz} \cdot \left(\frac{M_{\rm BH}}{10^8 \, M_\odot}\right)^{-1/2} \dot{m}^{1/2}\]

Below the self-absorption frequency \(\nu_{\rm sa} \approx \nu_{\rm peak}/3\), the spectrum is optically thick (Rayleigh-Jeans: \(\propto \nu^2\)). Above \(\nu_{\rm sa}\), it is optically thin with spectral index 2/5: \(\propto \nu^{2/5}\) (not 1/3). The two regimes join continuously at \(\nu_{\rm sa}\).

Bremsstrahlung (Mahadevan 1997, Eq. 3): Thermal bremsstrahlung from the hot electron-ion plasma provides a flat spectrum (\(\propto \nu^0\)) below the exponential cutoff at \(k_B T_e / h\).

Inverse Compton (Mahadevan 1997, Eq. 34, 35): Thomson (optically thin) scattering of bremsstrahlung photons by relativistic electrons. The photon index is:

\[\alpha_c = -\frac{\ln(\tau_{\rm es})}{\ln(A)}\]

where \(\tau_{\rm es}\) is the electron scattering optical depth and \(A \approx 4 \Theta\) with \(\Theta = k_B T_e / m_e c^2\). For typical ADAF parameters (\(T_e \sim 10^{10}\) K, \(\tau_{\rm es} \sim 1\)), this yields \(\alpha_c \sim 0.7–1.0\).

Component weights (Mahadevan 1997, Eq. 26, 29, 35): The relative contributions of synchrotron, bremsstrahlung, and inverse Compton are determined by the magnetic field strength (via \(\beta\) = gas pressure / total pressure), the optical depth, and the particle distribution. The weights are not arbitrary: synchrotron dominates at low frequencies, bremsstrahlung at intermediate frequencies, and inverse Compton at high frequencies. The beta parameter controls the magnetic energy density.

Truncated outer disc: The standard Shakura-Sunyaev multi-color disc (see multicolor_disc()) contributes from the truncation radius r_tr outward. This dominates the UV/optical and provides the warm photons that seed inverse Compton scattering.

Known limitations:

  • This implementation is based on Mahadevan (1997) analytic prescriptions and does not solve the full magnetohydrodynamic ADAF equations.

  • The inverse Compton calculation uses a simplified approximation; full solutions would require iterative energy balance with the photon field.

  • The model has not been extensively validated against 3D ADAF simulations or a comprehensive sample of observed low-luminosity AGN. Use with caution in quantitative inference.

References

tengri.components.agn.disc.beloborodov_gamma_hot(l_diss_hot: float, l_seed: float) float[source]

Self-consistent hard X-ray photon index (Beloborodov 1999).

Derives the spectral index of the hot corona from the ratio of dissipated luminosity to seed photon luminosity.

Kubota & Done (2018, MNRAS 480 1247) Eq. 6 rewrites the Beloborodov (1999, ApJ 510 L123) Compton-amplification result as:

Gamma_hot = (7/3) * (L_diss / L_seed)^{-0.1}

The exponent -0.1 is the K&D 2018 formulation of Beloborodov (1999).

Parameters:
  • l_diss_hot (float) – Luminosity dissipated in the hot corona [any units].

  • l_seed (float) – Soft photon luminosity intercepted by the corona [same units].

Returns:

Hard X-ray photon index, clipped to [1.4, 3.0].

Return type:

float

Notes

JIT-compatible: yes — uses only jnp primitives.

This implementation follows Kubota & Done (2018, MNRAS 480 1247, Eq. 6), which rewrites the Beloborodov (1999) Compton-amplification result as a power law in the luminosity ratio. The exponent -0.1 encodes the energy-balance relation between the dissipated power in the corona and the seed photon luminosity intercepted from the disc. Output is clipped to [1.4, 3.0] to match the physical range of typical AGN.

References

tengri.components.agn.disc.compute_l2500(wavelength: Array, l_nu: Array) float[source]

Extract monochromatic luminosity at rest-frame 2500 Angstrom.

Linearly interpolates L_nu onto 2500 A. Useful for computing alpha_ox and other AGN diagnostics.

Parameters:
  • wavelength (array, shape (n_wave,)) – Rest-frame wavelength [Angstrom], need not be sorted.

  • l_nu (array, shape (n_wave,)) – Specific luminosity [erg s^-1 Hz^-1].

Returns:

L_nu at 2500 A [erg s^-1 Hz^-1].

Return type:

float

Notes

JIT-compatible: yes — uses jnp primitives.

The 2500 Å point is a canonical AGN diagnostic wavelength, used to compute the optical-to-X-ray spectral index (alpha_ox) and as a benchmark for intrinsic AGN continuum comparisons. Linear interpolation in wavelength space is sufficient for the optical/UV continuum variability timescales. The wavelength array need not be pre-sorted; this function sorts internally before interpolation.

tengri.components.agn.disc.create_relagn_disc_from_grid(grid_path: str) Callable[source]

Return a JIT-compatible RELAGN disc SED function from a precomputed grid.

The grid was built with the real RELAGN Python class (Hagen & Done 2023) using KYCONV (Dovciak, Karas & Yaqoob 2004) per-annulus Kerr ray-tracing. It stores absolute L_ν (erg/s/Hz) at cos_inc = 0.5; the inclination correction is applied analytically as 2·cos_inc (valid for the non-relativistic outer disc; approximate for the GR inner disc).

Parameters:

grid_path (str) – Path to data/relagn_disc_grid.h5.

Returns:

Function with signature:

fn(wavelength, agn_log_mbh, agn_log_mdot, agn_astar,
   agn_cos_inc, **kwargs) -> L_nu [erg s^-1 Hz^-1]

Return type:

callable

Raises:

FileNotFoundError – If grid_path does not exist.

Notes

JIT-compatible: yes — the returned function is pure JAX. Grid loading is cached via @functools.cache.

Gradient-safe: yes — triweight interpolation is C²-continuous.

Inclination: grid stored at cos_inc = 0.5; scaled by 2·cos_inc. This is exact for r > 1000 r_g (non-relativistic regime) and approximate for the GR inner disc where KYCONV applies full Kerr ray-tracing.

Grid axes: log_mbh ∈ [7, 10], log_mdot ∈ [−1.5, 0.3], astar ∈ [0, 0.998] (prograde only; KYCONV rejects retrograde spins).

References

tengri.components.agn.disc.kubota_done_disc(wavelength: Array, agn_log_lbol: float, agn_frac: float = 1.0, agn_log_mbh: float = 8.0, agn_log_ledd: float = -1.0, agn_a_spin: float = 0.0, agn_cos_inc: float = 0.5, agn_f_hard: float = 0.02, agn_gamma_warm: float = 2.5, agn_kt_warm: float = 0.2, agn_gamma_hard: float = 1.8, agn_kt_hot: float = 100.0, agn_r_warm_ratio: float = 2.0, n_radii: int = 50, agn_self_consistent_gamma: bool = False, **_kwargs) Array[source]

Kubota & Done (2018) three-zone accretion disc with self-consistent corona.

Model a physically stratified AGN accretion disc as three radially distinct zones, each with different physics and electron temperatures. This is the reference model for intermediate to high accretion rates and is used in tengri’s default AGN configuration.

The three zones share a single Novikov-Thorne temperature profile but have different radiation mechanisms:

  1. Outer standard disc (r > R_warm): Optically thick, geometrically thin. Temperature decreases with radius (∝ r^{-3/4}). Radiates as multi-color blackbody. Dominates the optical/UV “big blue bump.”

  2. Warm Comptonization zone (R_hot < r < R_warm): Optically thick, warm electrons (kT_e ~ 0.2 keV, τ ~ 10-20). Inverse Compton-scattered disc photons plus thermal radiation. Produces the soft X-ray excess. Computed via precomputed nthcomp Kompaneets templates (when available) or a simplified modified-blackbody proxy.

  3. Hot corona (R_ISCO < r < R_hot): Optically thin, hot electrons (kT_e ~ 100 keV, τ ~ 1). Inverse Compton scatters disc seed photons to produce hard X-ray power-law spectrum with exponential cutoff.

Zone boundaries are determined self-consistently:

  • R_hot: Solved via bisection from the energy-balance constraint that the dissipated power in the corona equals f_hard × L_Edd. Uses the exact analytic Novikov-Thorne integral rather than approximations.

  • R_warm: Parameterized as a multiple of R_hot (default 2, per K&D).

  • R_out: Set to the Laor & Netzer (1989) self-gravity (Toomre) radius, beyond which the disc becomes unstable and fragments.

The hard X-ray photon index Γ_hot is derived self-consistently from the Beloborodov (1999) energy-balance relation if requested; otherwise uses the input value.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelength grid. [Angstrom]

  • agn_log_lbol (float) – Total AGN bolometric luminosity (all three zones). [log10(L_sun)]

  • agn_frac (float, optional) – Fraction of bolometric luminosity emitted by the disc system (all zones). Default: 1.0. [dimensionless, 0–1]

  • agn_log_mbh (float, optional) – Black hole mass. Determines the Eddington luminosity and temperature scaling. Default: 8.0. [log10(M_sun)]

  • agn_log_ledd (float, optional) – Eddington ratio. Controls the inner temperature, radiative efficiency, and zone locations. Typical range: -3 to 0. Default: -1.0. [log10(L / L_Edd)]

  • agn_a_spin (float, optional) – Dimensionless black hole spin parameter (Kerr, prograde). Range: [0, 0.998]. Higher spin → smaller R_ISCO, higher η. Default: 0.0 (Schwarzschild). [dimensionless]

  • agn_cos_inc (float, optional) – Cosine of the inclination angle between the disc normal and the line of sight. Range: [0.01, 1.0]. Used to compute the projected disc area. Default: 0.5 (60°). [dimensionless]

  • agn_f_hard (float, optional) – Fraction of Eddington luminosity dissipated in the hot corona. Controls the corona zone extent R_hot. Typical range: 0.01–0.1. Default: 0.02. [dimensionless, 0–0.5]

  • agn_gamma_warm (float, optional) – Photon index of the warm Comptonization zone (nthcomp). Range: ~1.5–3.5. Default: 2.5. [dimensionless]

  • agn_kt_warm (float, optional) – Electron temperature in the warm Comptonization zone. Default: 0.2. [keV]

  • agn_gamma_hard (float, optional) – Photon index of the hard X-ray power law (hot corona). Typical range: 1.5–2.5. Default: 1.8. Ignored if agn_self_consistent_gamma=True. [dimensionless]

  • agn_kt_hot (float, optional) – Electron temperature in the hot corona. Default: 100.0. [keV]

  • agn_r_warm_ratio (float, optional) – Radius ratio R_warm / R_hot. Controls the warm zone extent. Default: 2.0 (per K&D 2018). [dimensionless, ≥ 1.1]

  • n_radii (int, optional) – Number of radial integration points per zone. Default: 50. Higher values increase accuracy at computational cost.

  • agn_self_consistent_gamma (bool, optional) – If True, compute agn_gamma_hard self-consistently from the Beloborodov (1999) energy-balance relation: Γ = 7/3 × (L_diss / L_seed)^{-0.1} If False, use the input agn_gamma_hard value. Default: False.

Returns:

Spectral luminosity density L_ν. [erg/s/Hz]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — uses jnp primitives and jax.vmap.

Gradient-safe: yes — fully differentiable w.r.t. all parameters, including the bisection-solved R_hot.

Key self-consistent physics:

All three zones share the Novikov-Thorne temperature profile:

\[T(r) = T_{\rm in} \left(\frac{r}{r_{\rm ISCO}}\right)^{-3/4} \left[1 - \sqrt{\frac{r_{\rm ISCO}}{r}}\right]^{1/4}\]

where \(T_{\rm in} = (3 G M M_{\rm dot} / 8\pi \sigma_{\rm SB} r_{\rm ISCO}^3)^{1/4}\), and the inner temperature increases with accretion rate. The radii are ordered as R_ISCO < R_hot < R_warm < R_out.

Zone luminosity computation:

Each zone is divided into annuli at radii {r_i}, each of which contributes L_ν from its local Planck function (outer disc), nthcomp prescription (warm zone), or hot-corona power law (inner zone). All zones are summed and renormalized to conserve total bolometric energy.

Seed photon calculation (K&D 2018 Eq. 3): The hot corona inverse-Compton scatters disc seed photons. The seed photon luminosity is computed from the geometric integral of the warm-zone blackbody flux intercepted by the corona geometry:

\[L_{\rm seed} = 2 \int_{R_{\rm hot}}^{R_{\rm out}} F_{\rm NT}(r) \cdot \frac{\Theta(r)}{\pi} \cdot 2\pi r \, dr\]

where \(\Theta(r) = \theta_0 - \sin(2\theta_0)/2\) and \(\sin\theta_0 = R_{\rm hot}/r\). This drives the self-consistent Γ_hot via Beloborodov.

Precomputed nthcomp templates: For maximum speed and accuracy, the warm Comptonization is computed via interpolation in precomputed nthcomp Kompaneets templates (see scripts/build_nthcomp_templates.py). These templates span (Γ_warm, kT_e) parameter space and return (Γ_warm, kT_e, normalisation)-dependent SED at each radius. If templates are unavailable, a simplified modified-blackbody proxy is used, which has ~5–10% shape error but allows offline computation.

Approximations and accuracy:

The reference QSOSED/RELAGN codes use non-differentiable operations (root solvers, C implementations). Tengri’s JAX reimplementation makes key approximations documented in the code (see docs/dev/design/agn-kd-model.md):

  • R_hot: 40-step JAX-compatible bisection (exact to ~10^{-12}).

  • Warm zone: precomputed nthcomp templates with (Γ_w, kT_e) interpolation (accuracy: ≲ 2% in flux density).

  • Seed photons: K&D Eq. 3 integrated on 100-point log grid (exact).

  • Outer radius: Laor & Netzer self-gravity radius (2–4× improvement over fixed 1000 r_ISCO).

References

tengri.components.agn.disc.multicolor_disc(wavelength: Array, agn_log_lbol: float, agn_frac: float = 1.0, agn_log_mbh: float = 8.0, agn_log_ledd: float = -1.0, agn_a_spin: float = 0.0, agn_cos_inc: float = 0.5, n_radii: int = 50, **_kwargs) Array[source]

Shakura-Sunyaev thin accretion disc with multi-color blackbody emission.

Compute the SED of a standard geometrically thin, optically thick accretion disc via the Shakura-Sunyaev model. The disc is stratified into radial annuli, each radiating as a blackbody at its local temperature. This is the outer-disc component of the Kubota & Done (2018) three-zone model (see kubota_done_disc() for the full model including corona).

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelength grid. [Angstrom]

  • agn_log_lbol (float) – Total AGN bolometric luminosity. [log10(L_sun)]

  • agn_frac (float, optional) – Fraction of bolometric luminosity emitted by the disc. Default: 1.0. [dimensionless, 0–1]

  • agn_log_mbh (float, optional) – Black hole mass. Default: 8.0. [log10(M_sun)]

  • agn_log_ledd (float, optional) – Eddington ratio (accretion rate relative to Eddington luminosity). Typical range: -3 to 0. Default: -1.0. [log10(L / L_Edd)]

  • agn_a_spin (float, optional) – Dimensionless black hole spin parameter (prograde). Range: [0, 0.998]. Default: 0.0 (Schwarzschild). [dimensionless]

  • agn_cos_inc (float, optional) – Cosine of the inclination angle. Range: [0.01, 1.0]. Default: 0.5 (60°). [dimensionless]

  • n_radii (int, optional) – Number of radial bins for numerical integration. Default: 50.

Returns:

Spectral luminosity density L_ν. [erg/s/Hz]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — uses jnp primitives and jax.vmap.

The temperature profile follows the Novikov-Thorne (1974) emissivity for a thin, radiatively efficient disc:

\[T(r) = T_{\rm in} \left(\frac{r}{r_{\rm ISCO}}\right)^{-3/4} \left[1 - \sqrt{\frac{r_{\rm ISCO}}{r}}\right]^{1/4}\]

where \(T_{\rm in}\) is the inner temperature determined by the accretion rate and \(r_{\rm ISCO}\) is the innermost stable circular orbit (radius from Bardeen et al. 1972, depends on spin).

The disc luminosity is computed as:

\[L_\nu = \sum_{i=1}^{N_r} B_\nu(T_i) \cdot 2\pi^2 r_i \, dr_i \cdot \cos(i)\]

where \(B_\nu(T)\) is the Planck function, \(r_i\) is the ring radius [cm], \(dr_i\) is the ring width [cm], and \(\cos(i)\) is the projection factor (inclination).

Key physics:

  • Radiative efficiency: \(\eta = 1 - \sqrt{1 - 2/(3 r_{\rm ISCO})}\), computed from Novikov-Thorne theory. For Schwarzschild (a=0): η ≈ 0.057; for maximally spinning (a→0.998): η ≈ 0.32.

  • Outer radius: Uses the Laor & Netzer (1989) self-gravity (Toomre) radius, beyond which the disc fragments. This is an improvement over fixed approximations (e.g., 1000 r_ISCO) that can err by factors of a few at extreme masses or accretion rates.

  • Eddington ratio clamping: The accretion luminosity is capped at \(L_{\rm Edd}\) (i.e., log(L/L_Edd) is clipped to [0, 1] in linear space), reflecting the physical limit of radiatively efficient accretion.

Numerical method: Radii are logarithmically spaced to ensure fine resolution at small radii where the temperature gradient is steep. Integration uses summation; the trapezoidal rule is applied in log-radius space via the spacing \(d\log r = \Delta(\log r)\).

References

tengri.components.agn.disc.powerlaw_disc(wavelength: Array, agn_log_lbol: float, agn_frac: float = 1.0, agn_alpha: float = -1.0, agn_T_max: float = 100000.0, **_kwargs) Array[source]

Simple power-law accretion disc with exponential UV cutoff.

A phenomenological single-component AGN disc model that approximates the optical/UV emission as a power law with an exponential cutoff at high frequencies. This is a faster alternative to multi-color disc models when fine spectral details are not required.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelength grid. [Angstrom]

  • agn_log_lbol (float) – Total AGN bolometric luminosity. [log10(L_sun)]

  • agn_frac (float, optional) – Fraction of bolometric luminosity emitted by this disc component. Default: 1.0. [dimensionless, 0–1]

  • agn_alpha (float, optional) – Power-law spectral index. Typical range: -1.5 to -0.5. Default: -1.0 (flat in nu*L_nu). [dimensionless]

  • agn_T_max (float, optional) – Maximum blackbody temperature, setting the UV cutoff frequency. Typical range: 10^4 to 10^6. Default: 10^5. [K]

Returns:

Spectral luminosity density L_ν. [erg/s/Hz]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations use jnp primitives.

The unnormalized spectral shape is:

\[L_\nu^{\rm unnorm} = \nu^\alpha \exp\left(-\frac{h\nu}{k_B T_{\rm max}}\right)\]

where \(\alpha\) is the spectral index, \(h\) is Planck’s constant, \(\nu\) is frequency [Hz], \(k_B\) is Boltzmann’s constant, and \(T_{\rm max}\) is the cutoff temperature [K].

The normalization constant \(C\) is computed numerically by integrating the shape over the wavelength grid via the trapezoidal rule, ensuring that the integral over frequency equals the target luminosity \(L_{\rm bol} \cdot f_{\rm disc}\).

Approximation: This model is a simplified representation of the true accretion disc spectrum, which consists of multiple temperature zones (see multicolor_disc() and kubota_done_disc() for more realistic models). The power-law form breaks down at low frequencies (radio/submm) where the SED transitions to a different regime, and does not capture the soft X-ray excess or hard X-ray corona. Use this model only when computational speed is prioritized over spectral fidelity.

Broad Line Region (BLR) emission model.

The BLR is dense gas close to the black hole producing broad permitted emission lines with FWHM ~ 1000-10000 km/s. The BLR is geometrically compact and lies within the torus, so it is obscured at high inclinations (Type 2 AGN).

This module provides an analytic BLR template using broad Gaussian profiles at the wavelengths of the strongest permitted lines, plus an Fe II pseudo-continuum modeled as a sum of broad Gaussians at key multiplet wavelength groups (Tsuzuki+2006, Kovacevic+2010).

All functions are pure JAX and JIT-compilable.

References

  • Vanden Berk et al. 2001, AJ, 122, 549 (SDSS composite quasar spectrum)

  • Shen et al. 2011, ApJS, 194, 45 (SDSS quasar BLR properties)

  • Boroson & Green 1992, ApJS, 80, 109 (Fe II / H-beta ratio)

  • Vestergaard & Wilkes 2001, ApJS, 134, 1 (UV Fe II templates)

  • Tsuzuki et al. 2006, ApJ, 650, 57 (UV Fe II decomposition)

  • Kovacevic et al. 2010, ApJS, 189, 15 (optical Fe II model)

tengri.components.agn.blr.compute_blr_sed(wavelength: Array, l_disc_bol_erg: float, covering_fraction: float = 0.1, fwhm_kms: float = 5000.0, agn_fe2_strength: float = 0.0, line_efficiency: float = 0.08) Array[source]

BLR emission spectrum: broad permitted lines + Fe II pseudo-continuum.

The BLR receives covering_fraction * L_disc and converts a fraction into broad emission lines. When agn_fe2_strength > 0, an Fe II pseudo-continuum is added, scaled relative to H-beta luminosity using the standard R_Fe ratio.

Note: geometric masking by the torus is NOT applied here; it must be applied by the caller.

Parameters:
  • wavelength (array, shape (n_wave,)) – Rest-frame wavelength [Angstrom].

  • l_disc_bol_erg (float) – Bolometric disc luminosity [erg s^-1].

  • covering_fraction (float) – BLR covering fraction (0 to 1). Default 0.1.

  • fwhm_kms (float) – Line FWHM [km/s]. Default 5000.

  • agn_fe2_strength (float) – Fe II to H-beta flux ratio R_Fe = F(Fe II 4434-4684)/F(H-beta). Typical range 0.5-2.0. Default 0.0 (disabled).

  • line_efficiency (float) – Fraction of intercepted luminosity converted to line emission. Default 0.08.

Returns:

BLR L_nu [erg s^-1 Hz^-1] (before torus masking).

Return type:

array, shape (n_wave,)

Notes

JIT-compatible: yes — uses jnp primitives and jax.vmap.

The broad emission lines are modeled as Gaussian profiles at rest-frame wavelengths (Vanden Berk et al. 2001). Line strengths are calibrated to typical Type 1 AGN composite spectra. The Fe II pseudo-continuum follows the Tsuzuki+2006 / Kovacevic+2010 approach: broad Gaussians at UV and optical multiplet centers, normalized to the standard R_Fe ratio.

Torus geometry: This function returns the “bare” BLR spectrum without geometric masking by the dusty torus. The caller is responsible for applying inclination-dependent torus obscuration if using a torus model.

References

Narrow Line Region (NLR) emission model.

The NLR is photoionized gas illuminated by the AGN accretion disc. It produces nebular-like emission: a power-law continuum plus forbidden-line emission at key wavelengths.

The NLR emission is isotropic (not masked by the torus) because it extends on kpc scales beyond the torus opening angle.

For computational efficiency this module uses analytic line profiles rather than full CLOUDY grids. Each emission line is a Gaussian with FWHM ~ 500 km/s (narrow lines) placed at the laboratory wavelength.

All functions are pure JAX and JIT-compilable.

References

  • Groves et al. 2004, ApJS, 153, 75 (NLR photoionization models)

  • Feltre et al. 2016, MNRAS, 456, 3354 (NLR emission-line diagnostics)

  • Vijarnwannaluk et al. 2022 (NLR covering fractions)

tengri.components.agn.nlr.compute_nlr_sed(wavelength: Array, l_disc_bol_erg: float, covering_fraction: float = 0.1, fwhm_kms: float = 500.0, line_efficiency: float = 0.1) Array[source]

NLR emission spectrum: line emission + power-law continuum.

The NLR receives covering_fraction * L_disc and re-emits a fraction as emission lines and a small continuum component.

Parameters:
  • wavelength (array, shape (n_wave,)) – Rest-frame wavelength [Angstrom].

  • l_disc_bol_erg (float) – Bolometric disc luminosity [erg s^-1].

  • covering_fraction (float) – NLR covering fraction (0 to 1). Default 0.1.

  • fwhm_kms (float) – Line FWHM [km/s]. Default 500.

  • line_efficiency (float) – Fraction of intercepted luminosity converted to line emission. Default 0.10.

Returns:

NLR L_nu [erg s^-1 Hz^-1].

Return type:

array, shape (n_wave,)

Notes

JIT-compatible: yes — uses jnp primitives and jax.vmap.

tengri.components.agn.nlr.compute_nlr_sed_richardson2014(wavelength: Array, l_disc_bol_erg: float, covering_fraction: float = 0.1, fwhm_kms: float = 500.0, line_efficiency: float = 0.1) Array[source]

AGN NLR spectrum using Richardson+2014 Table 3 ‘a42’ line template.

The narrow-line region (NLR) is photoionized gas illuminated by the AGN accretion disc. This function synthesizes the NLR emission spectrum using the emission-line template from Richardson et al. (2014), which provides AGN-specific line ratios derived from the ‘a42’ column of Table 3 (moderate AGN luminosity, intermediate inclination angle).

The NLR receives covering_fraction * L_disc and converts a fraction into line emission. Each line is modeled as a Gaussian profile at the rest-frame wavelength.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelength [Angstrom].

  • l_disc_bol_erg (float) – Bolometric disc luminosity [erg s^-1].

  • covering_fraction (float, optional) – NLR covering fraction (0 to 1). Default 0.1.

  • fwhm_kms (float, optional) – Line FWHM [km/s]. Default 500.

  • line_efficiency (float, optional) – Fraction of intercepted luminosity converted to line emission. Default 0.10.

Returns:

l_nu – NLR L_nu [erg s^-1 Hz^-1].

Return type:

ndarray, shape (n_wave,)

Notes

This function is JIT-compilable (no traced control flow).

The Richardson+2014 ‘a42’ template uses 23 emission lines normalized to H-beta = 1. The strongest line is [O III] 5007 at 8.53× H-beta. Implemented via FSPS emission-line indices. Line profiles are Gaussian with fixed FWHM (narrow lines, ~500 km/s).

Ported from Prospector (Johnson et al. 2021 [1]_), which implements the same table for AGN NLR modeling.

References

Unified AGN SED: combined disc + torus + NLR + BLR emission.

Combines accretion disc and dust torus models into a single AGN SED. The disc emission is partly absorbed by the torus (via covering factor) and re-emitted in the IR.

Architecture overview

The component tree and geometric masking design in this module is inspired by the UnifiedAGN class in the Synthesizer package (Lovell et al. 2025, Open J. Astrophys., arXiv:2508.03888;

Roper et al. 2025, arXiv:2506.15811; https://github.com/synthesizer-project/synthesizer).

Synthesizer’s model defines:

  • An accretion disc whose inclination-dependent emission is extracted from precomputed photoionisation grids (CLOUDY-based).

  • NLR emission as grid-extracted nebular spectra, not masked by the torus (isotropic: always visible at all inclinations).

  • BLR emission as grid-extracted nebular spectra, masked by the torus using a hard binary condition: inclination + theta_torus > 90° → zeroed.

  • A torus that reprocesses the isotropic disc emission, scaled by torus_fraction = theta_torus / 90° (geometric fraction of sky covered).

  • Optional diffuse dust attenuation layered on top.

Differences in this implementation

tengri adopts the same conceptual decomposition but diverges in several ways for compatibility with gradient-based inference (VI, HMC):

  1. Analytic disc models, not grids: disc emission uses the closed-form multicolor/Shakura-Sunyaev or power-law models from disc.py rather than a photoionisation grid. This makes the disc JIT/grad-compatible.

  2. Analytic NLR/BLR templates, not grids: NLR and BLR use empirically calibrated Gaussian line templates (Vanden Berk et al. 2001; Groves et al. 2004) rather than CLOUDY-generated photoionisation grids. Grids would require non-differentiable table look-ups over (U, n_H) axes.

  3. Smooth sigmoid geometric mask, not a hard binary: Synthesizer zeros the disc/BLR whenever inclination + theta_torus > 90°. tengri replaces this with a smooth sigmoid centred at the critical angle (see _sigmoid_mask). The sigmoid preserves the gradient through the inclination parameter, which would otherwise be zero almost everywhere under the hard mask.

  4. Explicit ``agn_torus_frac`` parameter, not derived from theta_torus: Synthesizer internally computes torus_fraction = theta_torus / 90°, coupling the torus covering factor to the opening angle. tengri keeps agn_torus_frac as an independent free parameter. This decoupling is intentional: deriving the covering factor from cos(theta_torus) in the forward pass introduces a gradient discontinuity at theta_torus=0 and at theta_torus=90° (see CLAUDE.md gotcha).

  5. Polar dust reddening (SMC law): an optional agn_polar_ebv parameter applies SMC-law extinction to the disc and BLR for Type 1 sightlines. This is absent from Synthesizer’s basic UnifiedAGN but present in the CIGALE skirtor2016 module (Yang et al. 2020, MNRAS, 491, 740).

Pre-registered configurations

  • simple: power-law disc + single-temperature torus (3 free params).

  • standard: multi-color disc + two-temperature torus (5-6 free params).

  • kubota_done: multi-color disc with BH physics + clumpy torus (8+ params).

  • unified_nlr_blr: full Synthesizer-inspired model with NLR/BLR + polar dust.

Warning

agn_log_lbol is log10(L_bol / L_sun), not log10(L_bol / [erg/s]). See the convention note below before setting this parameter.

Convention for agn_log_lbol

agn_log_lbol is \(\log_{10}(L_{\rm bol} / L_\odot)\) — the bolometric luminosity expressed in solar luminosities, not erg/s. This matches the internal computation in components/agn/_phys.py (l_bol_erg = 10**agn_log_lbol * L_SUN).

  • Typical bright Seyfert: \(L_{\rm bol}\!\sim\!10^{44}\) erg/s \(\Rightarrow\) agn_log_lbol = 10.5.

  • Bright quasar: \(L_{\rm bol}\!\sim\!10^{46}\) erg/s \(\Rightarrow\) agn_log_lbol = 12.5.

  • Synthesizer’s bolometric_luminosity is in erg/s; convert with agn_log_lbol = log10(L_bol_erg) - log10(L_SUN_erg), i.e. subtract \(\approx 33.58\) from synthesizer’s value.

Defaults of agn_log_lbol=44.0 in the function signatures of this module are inherited from early test fixtures and correspond to \(L_{\rm bol}\!\approx\!4\times 10^{77}\) erg/s — not a physical AGN luminosity. They are kept for backward-compatibility with the existing test suite. Always set this parameter explicitly in production fits; do not rely on the default.

Usage:

from tengri.components.agn.unified import unified_agn, resolve_agn_model

# Use a named configuration. agn_log_lbol = 11 → L_bol ≈ 4e44 erg/s
# (a typical bright Seyfert nucleus).
model_fn = resolve_agn_model("simple")
l_nu = model_fn(wavelength, agn_log_lbol=11.0, agn_frac=0.1, ...)

# Or use the generic combiner directly.
l_nu = unified_agn(wavelength, agn_log_lbol=11.0, disc_model="powerlaw", ...)

References

class tengri.components.agn.unified.AGNRegistryEntry(callable: Callable, citation: str = '', status: str = 'production', short_doc: str = '')[source]

Bases: object

Registry entry for an AGN model with optional metadata.

callable

The AGN model function.

Type:

Callable

citation

Optional academic citation. Default empty string.

Type:

str

status

Model status: “production”, “experimental”, “demo”, or “deprecated”. Default “production”.

Type:

str

short_doc

Optional one-line description. Default empty string.

Type:

str

Notes

JIT-compatible: no — class for registry initialization.

tengri.components.agn.unified.adaf_agn(wavelength: Array, agn_log_lbol: float, agn_frac: float = 0.1, agn_log_mbh: float = 8.0, agn_log_ledd: float = -3.0, agn_r_tr: float = 100.0, agn_adaf_beta: float = 0.5, agn_adaf_delta: float = 0.01, agn_cos_inc: float = 0.5, agn_torus_frac: float = 0.3, agn_T_torus: float = 500.0, agn_ebv_disc: float = 0.0, **_kwargs) Array[source]

ADAF + truncated disc + simple torus for low-luminosity AGN.

At low accretion rates (L/L_Edd < 0.01), the inner disc transitions to an ADAF. The outer disc remains as a truncated Shakura-Sunyaev disc. A simple torus re-emits a fraction of the bolometric luminosity in the IR.

6 free parameters (+ agn_frac scaling):

  • agn_log_mbh: BH mass

  • agn_log_ledd: Eddington ratio (should be < -2 for ADAF regime)

  • agn_r_tr: truncation radius [R_g]

  • agn_adaf_beta: magnetic pressure fraction

  • agn_adaf_delta: electron heating fraction

  • agn_torus_frac: torus covering factor

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelength [Angstrom].

  • agn_log_lbol (float) – log10 of bolometric luminosity [Lsun].

  • agn_frac (float, optional) – AGN luminosity fraction [dimensionless]. Default 0.1.

  • agn_log_mbh (float, optional) – log10 of black hole mass [Msun]. Default 8.0.

  • agn_log_ledd (float, optional) – log10 of Eddington ratio [dimensionless]. Default -3.0.

  • agn_r_tr (float, optional) – Truncation radius [R_g]. Default 100.

  • agn_adaf_beta (float, optional) – Magnetic pressure fraction [dimensionless], range [0, 1]. Default 0.5.

  • agn_adaf_delta (float, optional) – Electron heating fraction [dimensionless], range [0, 1]. Default 0.01.

  • agn_cos_inc (float, optional) – cos(inclination) [dimensionless]. Default 0.5.

  • agn_torus_frac (float, optional) – Torus covering factor [dimensionless]. Default 0.3.

  • agn_T_torus (float, optional) – Torus temperature [K]. Default 500.

Returns:

L_nu [erg/s/Hz].

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — uses adaf_disc() and simple_torus().

tengri.components.agn.unified.cat3d_wind_agn(wavelength: Array, agn_log_lbol: float = 44.0, agn_frac: float = 0.1, agn_cos_inc: float = 0.5, agn_a_cat3d: float = -2.0, agn_fwd_cat3d: float = 0.45, agn_torus_frac: float = 0.5, agn_ebv_disc: float = 0.0, **_kwargs) Array[source]

CAT3D-Wind AGN: power-law disc + clumpy-disc + polar-wind torus.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelength. [Å]

  • agn_log_lbol (float, optional) – log10(L_bol / L_sun). Default 44.0.

  • agn_frac (float, optional) – Overall AGN luminosity fraction applied on top of the disc-plus-torus sum. Default 0.1.

  • agn_cos_inc (float, optional) – Cosine of inclination. Default 0.5.

  • agn_a_cat3d (float, optional) – Radial power-law index of the clumpy-cloud distribution. Default −2.0.

  • agn_fwd_cat3d (float, optional) – Polar-wind mass fraction. Default 0.2.

  • agn_torus_frac (float, optional) – Fraction of L_bol reprocessed by the torus. Disc receives 1 - agn_torus_frac. Default 0.5.

Returns:

Combined AGN SED. [erg/s/Hz]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes.

Grid templates ported from AGNfitter-rX (Martínez-Ramírez et al. 2024, A&A 688, A46, arXiv:2405.12111) — Hönig & Kishimoto 2017 CAT3D-Wind three-parameter projection. See tengri.components.agn.cat3d_wind and scripts/build_cat3d_wind_grid.py.

tengri.components.agn.unified.kubota_done_full_agn(wavelength: Array, agn_log_lbol: float, agn_frac: float = 0.1, agn_log_mbh: float = 8.0, agn_log_ledd: float = -1.0, agn_a_spin: float = 0.0, agn_cos_inc: float = 0.5, agn_f_hard: float = 0.02, agn_gamma_warm: float = 2.5, agn_kt_warm: float = 0.2, agn_gamma_hard: float = 1.8, agn_kt_hot: float = 100.0, agn_r_warm_ratio: float = 2.0, agn_T_hot: float = 1200.0, agn_T_warm: float = 300.0, agn_frac_hot: float = 0.3, agn_tau_torus: float = 5.0, agn_torus_frac: float = 0.5, agn_ebv_disc: float = 0.0, **_kwargs) Array[source]

Full Kubota & Done (2018) 3-zone disc + two-temperature torus.

Extends multicolor_agn with the full K&D 3-zone disc model: outer standard disc, warm Comptonization (soft X-ray excess), and hot corona (hard X-ray power law). Combined with a two-temperature dust torus.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelength [Angstrom].

  • agn_log_lbol (float) – log10 of bolometric luminosity [Lsun].

  • agn_frac (float, optional) – AGN luminosity fraction [dimensionless]. Default 0.1.

  • agn_log_mbh (float, optional) – log10 of black hole mass [Msun]. Default 8.0.

  • agn_log_ledd (float, optional) – log10 of Eddington ratio [dimensionless]. Default -1.0.

  • agn_a_spin (float, optional) – Black hole spin [dimensionless], range [0, 0.998]. Default 0.0.

  • agn_cos_inc (float, optional) – cos(inclination) [dimensionless]. Default 0.5.

  • agn_f_hard (float, optional) – Fraction of L_Edd in corona [dimensionless]. Default 0.02.

  • agn_gamma_warm (float, optional) – Warm Comptonization photon index [dimensionless]. Default 2.5.

  • agn_kt_warm (float, optional) – Warm electron temperature [keV]. Default 0.2.

  • agn_gamma_hard (float, optional) – Hard X-ray photon index [dimensionless]. Default 1.8.

  • agn_kt_hot (float, optional) – Hot corona temperature [keV]. Default 100.0.

  • agn_r_warm_ratio (float, optional) – R_warm / R_hot ratio [dimensionless]. Default 2.0.

  • agn_T_hot (float, optional) – Hot dust temperature [K]. Default 1200.

  • agn_T_warm (float, optional) – Warm dust temperature [K]. Default 300.

  • agn_frac_hot (float, optional) – Hot dust mass fraction [dimensionless]. Default 0.3.

  • agn_tau_torus (float, optional) – Torus optical depth at 9.7 um [dimensionless]. Default 5.

  • agn_torus_frac (float, optional) – Torus covering factor [dimensionless]. Default 0.5.

Returns:

L_nu [erg/s/Hz].

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — uses kubota_done_disc() and two_temperature_torus().

tengri.components.agn.unified.make_cue_nlr_fn(weights, gas_logU: float = -2.0, gas_logZ: float = 0.0, gas_log_nH: float = 2.0, gas_xi_d: float = 0.3, ionspec_index1: float = 1.7, ionspec_logLratio1: float = 0.0, ionspec_index2: float = -0.5, ionspec_logLratio2: float = 0.0, ionspec_index3: float = -1.5, ionspec_logLratio3: float = 0.0, ionspec_index4: float = -3.0, gas_logqion: float = 49.1)[source]

Create a Cue-backed NLR emission callable for unified_nlr_blr.

Returns a closure that uses the Cue neural-net emulator (Li et al. 2024 [1]_) to predict NLR emission lines and continuum as a function of ionising spectrum shape and gas properties. The closure matches the standard NLR backend interface expected by unified_nlr_blr().

The Cue weights are closed over as static pytree leaves, so the returned callable is JIT-compatible.

Parameters:
  • weights (CueWeights) – Pre-loaded Cue network weights from load_cue_weights().

  • gas_logU (float) – log10(ionization parameter U). Default -2.0.

  • gas_logZ (float) – log10(Z/Zsun) of the NLR gas. Default 0.0 (solar).

  • gas_log_nH (float) – log10(hydrogen number density [cm^-3]). Default 2.0.

  • gas_xi_d (float) – Dust-to-metal ratio. Default 0.3.

  • ionspec_index1..4 (float) – Power-law indices of the ionising spectrum piecewise approximation. Defaults correspond to a typical Seyfert 1 AGN spectrum.

  • ionspec_logLratio1..3 (float) – log10(L) ratios between power-law segments. Default 0.0.

  • gas_logqion (float) – log10(Q_H) — total ionising photon rate [photons/s]. Used for normalisation. Default 49.1 (typical AGN at log L_bol ~ 44).

Returns:

NLR emission function with signature:

fn(wavelength, l_disc_bol_erg, covering_fraction,
   fwhm_kms=500.0, **kwargs) -> ndarray, shape (n_wave,)

The returned L_nu is in [erg/s/Hz].

Return type:

callable

Notes

JIT/grad compatible when Cue weights are registered as JAX pytrees. The weights must first be loaded via tengri.components.nebular.cue.load_cue_weights.

The Cue emulator was trained for AGN-illuminated gas (not stellar HII regions). It is appropriate for NLR emission; it does NOT model the BLR (which requires a separate, denser photoionisation model with n_H ~ 10^10 cm^-3 and no dust).

References

tengri.components.agn.unified.multicolor_agn(wavelength: Array, agn_log_lbol: float, agn_frac: float = 0.1, agn_log_mbh: float = 8.0, agn_log_ledd: float = -1.0, agn_a_spin: float = 0.0, agn_cos_inc: float = 0.5, agn_T_hot: float = 1200.0, agn_T_warm: float = 300.0, agn_frac_hot: float = 0.3, agn_tau_torus: float = 5.0, agn_torus_frac: float = 0.5, **_kwargs) Array[source]

Multicolor Shakura-Sunyaev disc + two-temperature torus.

Standard thin-disc SED with spin-dependent ISCO and Novikov-Thorne radiative efficiency. This is the outer standard disc only — no warm Comptonization or hot corona (for the full 3-zone model, see kubota_done_full).

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelength [Angstrom].

  • agn_log_lbol (float) – log10 of bolometric luminosity [Lsun].

  • agn_frac (float, optional) – AGN luminosity fraction [dimensionless]. Default 0.1.

  • agn_log_mbh (float, optional) – log10 of black hole mass [Msun]. Default 8.0.

  • agn_log_ledd (float, optional) – log10 of Eddington ratio [dimensionless]. Default -1.0.

  • agn_a_spin (float, optional) – Black hole spin [dimensionless], range [0, 0.998]. Default 0.0.

  • agn_cos_inc (float, optional) – cos(inclination) [dimensionless], range [0, 1]. Default 0.5.

  • agn_T_hot (float, optional) – Hot dust temperature [K]. Default 1200.

  • agn_T_warm (float, optional) – Warm dust temperature [K]. Default 300.

  • agn_frac_hot (float, optional) – Hot dust mass fraction [dimensionless]. Default 0.3.

  • agn_tau_torus (float, optional) – Torus optical depth at 9.7 um [dimensionless]. Default 5.

  • agn_torus_frac (float, optional) – Torus covering factor [dimensionless]. Default 0.5.

Returns:

L_nu [erg/s/Hz].

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — uses multicolor_disc() and two_temperature_torus().

Also registered as “kubota_done” (deprecated alias).

tengri.components.agn.unified.register_agn_model(name: str, *, citation: str = '', status: str = 'production', short_doc: str = '') Callable[source]

Register an AGN model function (decorator factory).

Parameters:
  • name (str) – Unique model name for the registry.

  • citation (str, optional) – Academic citation for the model. Default empty string.

  • status (str, optional) – Model status (“production”, “experimental”, “demo”, “deprecated”). Default “production”.

  • short_doc (str, optional) – One-line description. Default empty string.

Returns:

Decorator that registers the decorated function in AGN_MODELS.

Return type:

callable

Notes

JIT-compatible: no — registers at module load time (not JIT-compilable).

The registered function must have signature:

fn(wavelength, agn_log_lbol, **kwargs) -> L_nu [erg/s/Hz]

Metadata is stored in an AGNRegistryEntry for introspection via the registry.list_agn_models() façade.

tengri.components.agn.unified.relagn_agn(wavelength: Array, agn_log_mbh: float = 8.0, agn_log_mdot: float = -1.0, agn_astar: float = 0.0, agn_cos_inc: float = 0.5, agn_torus_frac: float = 0.5, agn_T_hot: float = 1200.0, agn_T_warm: float = 300.0, agn_frac_hot: float = 0.3, agn_ebv_disc: float = 0.0, **_kwargs) Array[source]

RELAGN relativistic outer disc + two-temperature dust torus.

Uses the precomputed RELAGN grid (Hagen & Done 2023) with KYCONV (Dovciak, Karas & Yaqoob 2004) per-annulus Kerr ray-tracing for the disc, and a two-temperature modified blackbody for the torus.

The disc luminosity is self-consistent with BH mass and accretion rate; the torus re-emits agn_torus_frac of the disc bolometric output.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelength. [Å]

  • agn_log_mbh (float, optional) – log10(M_BH / M_sun), range [7, 10]. Default 8.0.

  • agn_log_mdot (float, optional) – log10(Ṁ / Ṁ_Edd), range [−1.5, 0.3]. Default −1.0.

  • agn_astar (float, optional) – Dimensionless BH spin, prograde only, range [0, 0.998]. Default 0.0.

  • agn_cos_inc (float, optional) – Cosine of inclination (1 = face-on). Default 0.5.

  • agn_torus_frac (float, optional) – Torus covering factor; torus re-emits this fraction of disc L_bol. Disc is attenuated by (1 agn_torus_frac). Default 0.5.

  • agn_T_hot (float, optional) – Hot dust temperature [K]. Default 1200.

  • agn_T_warm (float, optional) – Warm dust temperature [K]. Default 300.

  • agn_frac_hot (float, optional) – Hot dust mass fraction. Default 0.3.

  • agn_ebv_disc (float, optional) – SMC-law colour excess applied to disc [mag]. Default 0.0.

Returns:

Total AGN L_ν. [erg s⁻¹ Hz⁻¹]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — disc interpolation is pure JAX triweight kernel.

Gradient-safe: yes — C²-continuous triweight kernel on all grid axes.

Grid required: data/relagn_disc_grid.h5 built by scripts/build_relagn_disc_grid.py (requires HEASOFT/XSPEC + KYCONV).

Torus normalization: derived by integrating the disc L_ν over the output wavelength grid via jnp.trapezoid — no separate agn_log_lbol parameter needed.

References

tengri.components.agn.unified.resolve_agn_model(name: str) Callable[source]

Retrieve a registered AGN model by name.

Parameters:

name (str) – Model name (e.g., “simple”, “standard”, “multicolor_agn”, “adaf”).

Returns:

Model function: fn(wavelength, agn_log_lbol, **kwargs) -> L_nu [erg/s/Hz].

Return type:

callable

Raises:

ValueError – If name is not registered.

Notes

JIT-compatible: no — performs dictionary lookup at initialization time.

tengri.components.agn.unified.silva04_agn(wavelength: Array, agn_log_lbol: float = 44.0, agn_frac: float = 0.1, agn_log_nh_silva: float = 23.0, agn_torus_frac: float = 0.5, agn_ebv_disc: float = 0.0, **_kwargs) Array[source]

Silva+04 smooth-torus AGN: power-law disc + Silva+04 torus.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelength. [Å]

  • agn_log_lbol (float, optional) – log10(L_bol / L_sun). Default 44.0.

  • agn_frac (float, optional) – Overall AGN luminosity fraction applied on top of the disc-plus-torus sum. Default 0.1.

  • agn_log_nh_silva (float, optional) – Hydrogen column density, log10(N_H / cm^-2). Default 23.0.

  • agn_torus_frac (float, optional) – Fraction of L_bol reprocessed by the torus. Disc receives 1 - agn_torus_frac. Default 0.5.

Returns:

Combined AGN SED. [erg/s/Hz]

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — both the power-law disc and the Silva+04 grid interpolation are pure JAX.

Grid templates ported from AGNfitter (Calistro Rivera et al. 2016); see tengri.components.agn.silva04 and scripts/build_silva04_grid.py.

tengri.components.agn.unified.simple_agn(wavelength: Array, agn_log_lbol: float, agn_frac: float = 0.1, agn_alpha: float = -1.0, agn_T_torus: float = 1000.0, agn_torus_frac: float = 0.5, **_kwargs) Array[source]

Simple AGN: power-law disc + single-temperature torus.

Minimal 3-parameter model (+ overall luminosity scaling).

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelength [Angstrom].

  • agn_log_lbol (float) – log10 of bolometric luminosity [Lsun].

  • agn_frac (float, optional) – AGN luminosity fraction of total galaxy SED [dimensionless]. Applied as overall scaling. Default 0.1.

  • agn_alpha (float, optional) – Disc spectral slope [dimensionless]. Default -1.0.

  • agn_T_torus (float, optional) – Torus temperature [K]. Default 1000.

  • agn_torus_frac (float, optional) – Torus covering factor [dimensionless], range [0, 1]. Default 0.5.

Returns:

L_nu [erg/s/Hz].

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — delegates to unified_agn().

tengri.components.agn.unified.skirtor_agn(wavelength: Array, agn_log_lbol: float = 44.0, agn_frac: float = 0.1, agn_tau_skirtor: float = 7.0, agn_p_skirtor: float = 1.0, agn_q_skirtor: float = 1.0, agn_oa_skirtor: float = 40.0, agn_cos_inc: float = 0.5, agn_torus_frac: float = 0.5, agn_ebv_disc: float = 0.0, **_kwargs) Array[source]

SKIRTOR clumpy torus AGN: power-law disc + SKIRTOR torus (analytic).

Uses the analytic SKIRTOR approximation (Stalevski et al. 2012, 2016) for the torus emission, combined with a power-law accretion disc.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelength [Angstrom].

  • agn_log_lbol (float, optional) – log10 of bolometric luminosity [Lsun]. Default 44.0.

  • agn_frac (float, optional) – AGN luminosity fraction [dimensionless]. Default 0.1.

  • agn_tau_skirtor (float, optional) – Optical depth at 9.7 um [dimensionless], range [3, 11]. Default 7.0.

  • agn_p_skirtor (float, optional) – Radial density gradient [dimensionless], range [0, 1.5]. Default 1.0.

  • agn_q_skirtor (float, optional) – Polar density gradient [dimensionless], range [0, 1.5]. Default 1.0.

  • agn_oa_skirtor (float, optional) – Opening angle [degrees], range [20, 60]. Default 40.0.

  • agn_cos_inc (float, optional) – cos(inclination) [dimensionless], 0=edge-on, 1=face-on. Default 0.5.

  • agn_torus_frac (float, optional) – Torus covering factor [dimensionless]. Default 0.5.

Returns:

L_nu [erg/s/Hz].

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: no — requires SKIRTOR template interpolation (non-differentiable).

tengri.components.agn.unified.standard_agn(wavelength: Array, agn_log_lbol: float, agn_frac: float = 0.1, agn_log_mbh: float = 8.0, agn_log_ledd: float = -1.0, agn_T_hot: float = 1200.0, agn_T_warm: float = 300.0, agn_frac_hot: float = 0.3, agn_torus_frac: float = 0.5, **_kwargs) Array[source]

Standard AGN: multi-color disc + two-temperature torus.

5-6 free parameters controlling BH accretion physics and dust emission.

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelength [Angstrom].

  • agn_log_lbol (float) – log10 of bolometric luminosity [Lsun].

  • agn_frac (float, optional) – AGN luminosity fraction [dimensionless]. Default 0.1.

  • agn_log_mbh (float, optional) – log10 of black hole mass [Msun]. Default 8.0.

  • agn_log_ledd (float, optional) – log10 of Eddington ratio [dimensionless]. Default -1.0.

  • agn_T_hot (float, optional) – Hot dust temperature [K]. Default 1200.

  • agn_T_warm (float, optional) – Warm dust temperature [K]. Default 300.

  • agn_frac_hot (float, optional) – Hot component mass fraction [dimensionless]. Default 0.3.

  • agn_torus_frac (float, optional) – Torus covering factor [dimensionless]. Default 0.5.

Returns:

L_nu [erg/s/Hz].

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — delegates to unified_agn() with multicolor disc.

tengri.components.agn.unified.unified_agn(wavelength: Array, agn_log_lbol: float, disc_model: str = 'powerlaw', torus_model: str = 'simple', agn_torus_frac: float = 0.5, agn_ebv_disc: float = 0.0, **kwargs) Array[source]

Compute unified AGN SED: disc + torus.

The torus re-emits a fraction agn_torus_frac of the bolometric luminosity, while the disc emits the remaining (1 - agn_torus_frac).

Parameters:
  • wavelength (array_like, shape (n_wave,)) – Rest-frame wavelength [Angstrom].

  • agn_log_lbol (float) – log10 of bolometric luminosity [Lsun].

  • disc_model (str, optional) – Disc model name: “powerlaw”, “multicolor”, “kubota_done_3zone”, “adaf”. Default “powerlaw”.

  • torus_model (str, optional) – Torus model name: “simple”, “two_temperature”, “skirtor”. Default “simple”.

  • agn_torus_frac (float, optional) – Torus covering factor [dimensionless], range [0, 1]. Disc gets (1 - agn_torus_frac). Default 0.5.

  • **kwargs – Additional parameters passed to disc and torus functions (they ignore unrecognized kwargs).

Returns:

Total AGN L_nu [erg/s/Hz] = L_disc + L_torus.

Return type:

ndarray, shape (n_wave,)

References

Notes

JIT-compatible: depends on disc and torus models (mostly yes for analytic).

Gradient-safe: yes when using analytic disc and torus models.

tengri.components.agn.unified.unified_nlr_blr(wavelength: Array, agn_log_lbol: float = 44.0, agn_cos_inc: float = 0.5, agn_theta_torus: float = 30.0, agn_nlr_cf: float = 0.1, agn_blr_cf: float = 0.1, agn_log_mbh: float = 7.0, agn_log_ledd: float = -1.0, agn_a_spin: float = 0.0, agn_T_hot: float = 1200.0, agn_T_warm: float = 300.0, agn_frac_hot: float = 0.3, agn_tau_torus: float = 5.0, agn_torus_frac: float = 0.5, agn_frac: float = 0.1, agn_blr_fwhm: float = 5000.0, agn_nlr_fwhm: float = 500.0, agn_polar_ebv: float = 0.0, nlr_fn: Callable | None = None, blr_fn: Callable | None = None, include_xray: bool = False, xray_gamma_agn: float = 1.8, xray_alpha_ox: float = -1.4, xray_E_cut: float = 300.0, include_radio: bool = False, radio_q_ir: float = 2.64, radio_alpha_sf: float = 0.8, radio_loudness: float = 0.0, radio_alpha_agn: float = 0.7, **_kwargs) Array[source]

Unified AGN SED with NLR/BLR decomposition and geometric masking.

Implements the same conceptual decomposition as the UnifiedAGN class in Synthesizer (Lovell et al. 2025 [1]_, Roper et al. 2025 [2]_): an accretion disc, dusty torus, narrow line region (NLR), and broad line region (BLR), combined with inclination-dependent geometric masking. Polar dust reddening (SMC law) is additionally adapted from CIGALE’s skirtor2016 module [3]_.

Correspondence with Synthesizer’s UnifiedAGN

The following table maps tengri parameters to Synthesizer attributes (synthesizer.components.blackhole.BlackholeComponent):

tengri

Synthesizer

Note

agn_cos_inc

cos(inclination)

Synth uses inclination [deg]

agn_theta_torus

theta_torus [deg]

Same meaning

agn_nlr_cf

covering_fraction_nlr

Identical semantics

agn_blr_cf

covering_fraction_blr

Identical semantics

agn_torus_frac

torus_fraction = theta_torus/90°

Different: independent param

agn_blr_fwhm

velocity_dispersion_blr [km/s]

tengri uses FWHM directly

agn_nlr_fwhm

velocity_dispersion_nlr [km/s]

Same

Deliberate differences from Synthesizer

  1. Analytic disc, not a grid: Synthesizer extracts disc emission from precomputed CLOUDY photoionisation grids. tengri uses the closed-form multicolor_disc (Shakura-Sunyaev / Novikov-Thorne) from disc.py. Rationale: grid look-ups are not JAX-jittable under gradient tape; analytic models are differentiable by construction.

  2. Analytic NLR/BLR templates, not grids: Synthesizer uses CLOUDY grids over (logU, log n_H) for both NLR and BLR emission. tengri uses empirically calibrated Gaussian line templates (Vanden Berk et al. 2001 for BLR; Groves et al. 2004 for NLR). Rationale: same as above.

  3. Smooth sigmoid mask, not a hard binary: Synthesizer zeros the disc and BLR when inclination + theta_torus > 90° — a hard step function. tengri replaces this with a smooth sigmoid (see _sigmoid_mask) centred at the same critical angle with a ~2° transition width. Rationale: the hard mask has zero gradient with respect to inclination almost everywhere, making gradient-based VI and HMC blind to inclination information near the critical angle.

  4. ``agn_torus_frac`` decoupled from ``theta_torus``: Synthesizer internally sets torus_fraction = theta_torus / 90°, coupling the reprocessed fraction to the geometric opening angle. tengri keeps agn_torus_frac as an independent free parameter. Rationale: auto-deriving from cos(theta_torus) in the forward pass introduces a gradient discontinuity at the poles (CLAUDE.md gotcha). The coupling can be enforced at the Parameters level via a fixed/derived param.

  5. Polar dust reddening (SMC law): agn_polar_ebv applies SMC extinction to the disc and BLR for Type 1 sightlines (mask > 0). Absent from Synthesizer’s basic UnifiedAGNIntrinsic; adapted from CIGALE skirtor2016 (Yang et al. 2020 [3]_).

Geometry summary

\[L_{\rm AGN}(\lambda) = \sigma(i, \theta_t)\, A_{\rm pol,eff}(\lambda)\, L_{\rm disc}(\lambda) + L_{\rm torus}(\lambda) + f_{\rm NLR}\, \eta_{\rm NLR}(\lambda)\, L_{\rm bol,disc} + \sigma(i, \theta_t)\, A_{\rm pol,eff}(\lambda)\, f_{\rm BLR}\, \eta_{\rm BLR}(\lambda)\, L_{\rm bol,disc} + \mathbb{1}_{\rm X-ray} \, L_{\rm X-ray}(\lambda) + \mathbb{1}_{\rm radio} \, L_{\rm radio}(\lambda)\]

where \(\sigma(i, \theta_t)\) is the smooth sigmoid mask (1 = visible, 0 = obscured), \(A_{\rm pol,eff}(\lambda) = 1 + \sigma(i, \theta_t) \, (A_{\rm pol}(\lambda) - 1)\) is the visibility-weighted SMC polar dust transmission (SMC law; only reddens when disc is visible to the observer), \(f_{\rm NLR/BLR}\) are the covering fractions, \(\eta_{\rm NLR/BLR}\) are the normalised line templates, and \(\mathbb{1}_{\rm X-ray/radio}\) are indicator functions controlling whether X-ray and radio components are included.

Parameters:
  • wavelength (array, shape (n_wave,)) – Rest-frame wavelength [Angstrom].

  • agn_log_lbol (float) – \(\log_{10}(L_{\rm bol} / L_\odot)\) — total AGN bolometric luminosity expressed in solar luminosities (not erg/s). Convert from synthesizer’s bolometric_luminosity [erg/s] via agn_log_lbol = log10(L_bol_erg) - log10(L_SUN_erg), i.e. subtract \(\approx 33.58\). Typical bright Seyfert: 10.5; bright quasar: 12.5. The default 44.0 is a legacy test fixture that is not a physical AGN luminosity — set this parameter explicitly. See module-level “Convention” note. Default 44.0.

  • agn_cos_inc (float) – Cosine of inclination angle (0 = edge-on/Type 2, 1 = face-on/Type 1). Synthesizer uses inclination in degrees; tengri uses cos(inclination). Default 0.5.

  • agn_theta_torus (float) – Torus half-opening angle [degrees]. Controls the critical inclination above which the disc and BLR are obscured. Same meaning as theta_torus in Synthesizer. Default 30.0.

  • agn_nlr_cf (float) – NLR covering fraction (0 to 1). Fraction of disc luminosity reprocessed into NLR emission. Synthesizer: covering_fraction_nlr. Default 0.1.

  • agn_blr_cf (float) – BLR covering fraction (0 to 1). Fraction of disc luminosity reprocessed into BLR emission. Synthesizer: covering_fraction_blr. Default 0.1.

  • agn_log_mbh (float) – log10(M_BH / Msun). Default 7.0.

  • agn_log_ledd (float) – log10(L/L_Edd). Default -1.0.

  • agn_a_spin (float) – BH spin (0 to 0.998). Default 0.0.

  • agn_T_hot (float) – Hot dust temperature [K]. Default 1200.

  • agn_T_warm (float) – Warm dust temperature [K]. Default 300.

  • agn_frac_hot (float) – Hot-to-warm dust fraction. Default 0.3.

  • agn_tau_torus (float) – Torus optical depth at 9.7 um. Default 5.

  • agn_torus_frac (float) – Torus covering factor (fraction of L_bol intercepted by torus). In Synthesizer this is derived as theta_torus / 90°; here it is an independent free parameter. Default 0.5.

  • agn_frac (float) – Overall AGN fraction scaling. Default 0.1.

  • agn_blr_fwhm (float) – BLR line FWHM [km/s]. Synthesizer uses velocity_dispersion_blr; tengri uses FWHM directly. Default 5000.

  • agn_nlr_fwhm (float) – NLR line FWHM [km/s]. Default 500.

  • agn_polar_ebv (float) – Polar dust E(B-V) applied to disc + BLR for Type 1 views (SMC law). Adapted from CIGALE skirtor2016 [3]_. Not in Synthesizer’s basic UnifiedAGN. Default 0.0 (no reddening).

  • nlr_fn (callable or None) –

    NLR emission backend. Signature:

    nlr_fn(wavelength, l_disc_bol_erg, covering_fraction,
           fwhm_kms=500.0, **kwargs) -> ndarray, shape (n_wave,)
    

    where the return value is L_nu [erg/s/Hz]. Default None uses the built-in analytic template from nlr_emission(). To use the Cue neural-net emulator, pass the result of make_cue_nlr_fn().

  • blr_fn (callable or None) – BLR emission backend. Same signature as nlr_fn. Default None uses the built-in analytic template from blr_emission().

  • include_xray (bool) – If True, include X-ray corona emission. Default False (UV-optical-IR only).

  • xray_gamma_agn (float) – X-ray photon index (power-law slope). Default 1.8. Valid range: 1.4–2.4.

  • xray_alpha_ox (float) – UV-to-X-ray slope. Default -1.4. Valid range: -2.0 to -1.0. Controls the relative normalization of X-ray vs. optical SED.

  • xray_E_cut (float) – X-ray exponential cutoff energy [keV]. Default 300.0.

  • include_radio (bool) – If True, include radio synchrotron + AGN radio jet emission. Default False.

  • radio_q_ir (float) – FIR–radio correlation parameter (Bell+2003 mode). Default 2.64.

  • radio_alpha_sf (float) – Star-forming synchrotron spectral index. Default 0.8.

  • radio_loudness (float) – AGN radio-loudness log10(L_5GHz/L_B). Default 0.0 (no radio AGN).

  • radio_alpha_agn (float) – AGN radio spectral index. Default 0.7.

Returns:

Total AGN L_nu [erg s^-1 Hz^-1].

Return type:

array, shape (n_wave,)

Notes

JIT/grad/vmap compatible when nlr_fn and blr_fn are JIT-compatible (the default analytic templates are; Cue-backed closures are also JIT-safe since the weights are closed over as static pytree leaves).

X-ray and radio components (when included):

  • X-ray: Power-law corona emission normalised via the alpha_OX relation (Tananbaum+1979). Wavelength range: λ < 124 Å (E > ~100 eV). Computed via xray_agn_corona() [4].

  • Radio: Synchrotron + optional free-free + AGN jet emission. Wavelength range: λ > 1 mm (ν < 300 GHz). Computed via radio_total() [5].

When both include_xray=False and include_radio=False, the function returns the UV-optical-FIR unified AGN SED (original behaviour). The new components are additive and do not affect existing parameters or output ranges.

References

Examples

>>> import jax.numpy as jnp
>>> from tengri import unified_nlr_blr
>>> wave = jnp.linspace(1000.0, 30000.0, 512)
>>> sed = unified_nlr_blr(wave, agn_log_lbol=45.0, agn_cos_inc=0.8)
>>> sed.shape
(512,)
>>> bool(jnp.all(sed >= 0))
True

Nebular Emission

Nebular line and continuum emission from CLOUDY grids or the Cue neural network emulator, with optional shock and DIG mixing.

MAPPINGS III + V shock emission line model.

Loads Allen+2008 (ApJS 178 20) MAPPINGS III and Alarie & Morisset 2019 (3MdBs, RMxAA 55 279) MAPPINGS V grids from data/mappings_templates.h5 and interpolates line ratios across the full 4-D grid:

(v_shock, B/√n or B, log_density, abundance)

If the HDF5 file is missing, falls back to the Allen+2008 Table 5 hardcoded arrays (solar, n=1 cm⁻³, 8 velocity points, 10 lines) with a DeprecationWarning. Build the grid with:

python scripts/download_mappings_templates.py

Interpolation strategy

  • velocity, B-field, log_density : interp_nd_triweight — C²-continuous triweight kernel (Hearin et al. 2023 / DSPS), jointly interpolated across all three continuous axes. Bin edges are precomputed at grid load time via edges_for_grid to avoid rebuilding inside JIT traces.

  • abundance, component, version : Python string → integer index (static)

References

  • Allen et al. 2008, ApJS, 178, 20 (MAPPINGS III)

  • Sutherland & Dopita 2017, ApJS, 229, 34 (MAPPINGS V)

  • Alarie & Morisset 2019, RMxAA, 55, 279 (3MdBs / Zenodo 14140949)

class tengri.components.nebular.shock.ShockBackend(shock_abundance: str = 'solar', shock_component: str = 'combined')[source]

Bases: object

MAPPINGS V shock emission backend satisfying the NebularBackend Protocol.

Wraps compute_shock_sed() as an object-oriented backend so that shock emission can be stored and dispatched using the same interface as other nebular backends (CueBackend, CloudyGridBackend, etc.).

Static (non-traced) configuration is stored as dataclass fields. JAX-traced continuous parameters (velocity, density, B-field, Hα luminosity) are passed as arguments to predict_nebular_sed().

Parameters:
  • shock_abundance (str) – Abundance set name: "solar", "2xsolar", "lmc", "smc", etc.

  • shock_component (str) – "shock", "precursor", or "combined".

  • has_continuum (bool) – Always False — MAPPINGS V provides line emission only.

  • has_free_params (bool) – Always True — velocity, density, B-field are differentiable parameters.

  • name (str) – Backend identifier string (“shock”).

Notes

JIT-compatible: Methods return JAX arrays suitable for JIT compilation. All computations use pure functions with no side effects.

Attributes: has_continuum is always False — MAPPINGS V provides shock-associated emission lines only (no underlying continuum). has_free_params is always True — all parameters (velocity, density, B-field) are differentiable and suitable for optimization.

has_continuum: bool = False
has_free_params: bool = True
name: str = 'shock'
predict_nebular_sed(wavelength: Array, shock_velocity: float, l_shock_halpha: float, shock_log_density: float = 0.0, shock_b_over_sqrt_n: float = 1.0, line_sigma_aa: float = 0.0, **_kwargs) Array[source]

Compute shock emission line SED on a wavelength grid.

Parameters:
  • wavelength (array, shape (n_wave,)) – Wavelength grid in Å (rest-frame, increasing).

  • shock_velocity (float) – Shock velocity in km/s.

  • l_shock_halpha (float) – Total shock Hα luminosity in erg/s (normalization anchor).

  • shock_log_density (float) – Log10 pre-shock density in cm⁻³.

  • shock_b_over_sqrt_n (float) – Absolute B-field in μG.

  • line_sigma_aa (float) – Gaussian line width in Å. 0 → delta function into nearest pixel.

  • **_kwargs – Extra keyword arguments silently ignored for protocol compatibility.

Returns:

Shock emission SED [erg/s/Hz].

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — delegates to compute_shock_sed.

Abundance and component: These are fixed at backend initialization via shock_abundance and shock_component dataclass fields. Continuous parameters (velocity, density, B-field, H-alpha luminosity) are traced and differentiable under jax.jit.

shock_abundance: str = 'solar'
shock_component: str = 'combined'
tengri.components.nebular.shock.compute_shock_sed(wavelength: Array, shock_velocity: float, l_shock_halpha: float, shock_log_density: float = 0.0, shock_b_over_sqrt_n: float = 1.0, shock_abundance: str = 'solar', shock_component: str = 'combined', line_sigma_aa: float = 0.0) Array[source]

Compute shock emission line SED.

Places MAPPINGS V (3MdBs) shock emission lines on an arbitrary wavelength grid as Gaussians (line_sigma_aa > 0) or delta functions.

Parameters:
  • wavelength (array, shape (n_wave,)) – Wavelength grid in Å (rest-frame, increasing).

  • shock_velocity (float) – Shock velocity in km/s.

  • l_shock_halpha (float) – Total shock Hα luminosity in erg/s (normalization anchor).

  • shock_log_density (float) – Log10 pre-shock density in cm⁻³.

  • shock_b_over_sqrt_n (float) – Absolute B-field in μG (3MdBs MAPPINGS V). Snapped to nearest grid point.

  • shock_abundance (str) – Abundance set (see shock_line_ratios).

  • shock_component (str) – "shock", "precursor", or "combined".

  • line_sigma_aa (float) – Gaussian line width in Å. 0 → delta function into nearest pixel.

Returns:

Shock emission SED in erg/s/Hz [erg/s/Hz].

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes — all operations use jnp primitives and calls to _shock_line_arrays and _place_line_profiles.

Line placement: Emission lines are placed as Gaussian profiles (if line_sigma_aa > 0) or delta functions (if line_sigma_aa = 0). Delta functions scatter energy into the nearest pixel, normalized by pixel width to preserve flux.

tengri.components.nebular.shock.shock_line_ratios(shock_velocity: float, shock_log_density: float = 0.0, shock_b_over_sqrt_n: float = 1.0, shock_abundance: str = 'solar', shock_component: str = 'combined') dict[str, float][source]

Return emission line luminosity ratios relative to Hβ.

Loads the MAPPINGS V (3MdBs) grid from data/mappings_templates.h5. If the file is missing, falls back to the Allen+2008 Table 5 hardcoded subset with a DeprecationWarning.

Parameters:
  • shock_velocity (float) – Shock velocity in km/s. Must be within the grid range (100–1000 km/s fallback; 200–1000 km/s HDF5). Raises ValueError if out of range. Continuously interpolated — safe under jax.jit.

  • shock_log_density (float) – Log10 pre-shock density in cm⁻³ (e.g. 0.0 = 1 cm⁻³). Must be within [0, 3]. Continuously interpolated via triweight kernel — safe under jax.jit. Raises ValueError if out of range.

  • shock_b_over_sqrt_n (float) – Absolute B-field strength in μG (3MdBs MAPPINGS V convention). Must be within [0.0001, 10] μG. Continuously interpolated via triweight kernel — safe under jax.jit. Raises ValueError if out of range.

  • shock_abundance (str) – Abundance pattern. Accepted short names: "solar", "2xsolar" / "twice_solar", "dopita2005", "lmc", "smc". Full 3MdBs DB names (e.g. "Allen2008_Solar") also accepted. Raises ValueError for unknown names.

  • shock_component (str) – Which emission component to return. One of "shock", "precursor", "combined" (default). Raises ValueError for unknown values.

Returns:

PyNeb-format line name → luminosity ratio relative to Hβ [dimensionless].

Return type:

dict[str, float]

Raises:

ValueError – If any parameter is outside the grid bounds or invalid.

References

Notes

JIT-compatible: yes — continuous parameters (velocity, density, B-field) are interpolated via interp_nd_triweight, safe under jax.jit. Discrete parameters (abundance, component) are resolved at call time and not traced.

Fallback: If the MAPPINGS V HDF5 grid is missing, falls back to the Allen+2008 Table 5 hardcoded array (solar abundance, n=1 cm⁻³, 8 velocity points) with a DeprecationWarning. To avoid this, download the grid via scripts/download_mappings_templates.py.

Interpolation: Triweight kernel (C²-continuous) jointly interpolates velocity, density, and B-field across all three axes. Grid edges are precomputed at load time to avoid rebuilding inside JIT traces.

Examples

>>> ratios = shock_line_ratios(300.0)  # solar, combined, 300 km/s
>>> ratios = shock_line_ratios(500.0, shock_component="precursor")
>>> ratios = shock_line_ratios(400.0, shock_abundance="lmc", shock_log_density=1.0)

Diffuse Ionized Gas (DIG) nebular emission model.

Models the mixing of ionizing photon-powered emission from two gas components: dense HII regions and diffuse ionized gas (DIG). DIG is low-density ionized gas between HII regions, characterised by lower ionization parameter (log U ~ −4 vs −2.5 in HII regions). This produces a distinct emission-line signature: enhanced low-ionization diagnostics ([NII]/Hα, [SII]/Hα, [OI]/Hα).

Observationally, DIG contributes ~30–60% of Hα flux in local star-forming galaxies (Reynolds 1984; Haffner et al. 2009; Tacchella et al. 2022).

Mixing formula: Returns a weighted average of nebular emission at two ionization parameters (HII + DIG):

L_total = (1 − f_DIG) × L(log U_HII) + f_DIG × L(log U_DIG)

where log U_DIG = log U_HII + Δ log U, with Δ log U = −1 dex (default).

When f_DIG = 0 (default), collapses to pure HII region emission.

tengri.components.nebular.dig.mix_dig_emission(nebular_backend, ssp_wave: Array, ssp_weights: Array, ssp_log_ages_yr: Array, log_z: float, neb_logU: float = -3.0, neb_logZ_gas: float | None = None, neb_fesc: float = 0.0, neb_fesc_lya: float = 0.0, neb_dig_frac: float = 0.0, neb_dig_delta_logU: float = -1.0, line_sigma_aa: float = 0.0, **kwargs) Array[source]

Predict nebular SED with HII region and diffuse ionized gas components.

Computes emission from two ionization regimes (HII + DIG) using any backend, then combines via mass-weighted mixing. This captures the enhanced low-ionization emission ([NII], [SII], [OI]) characteristic of warm, ionized ISM.

Parameters:
  • nebular_backend (NebularBackend (CloudyGridBackend or CueBackend)) – Backend with predict_nebular_sed() method. Called twice (HII, DIG).

  • ssp_wave (array, shape (n_wave,)) – SSP wavelength grid in Å. [Å]

  • ssp_weights (array, shape (n_age,)) – Stellar mass weights of composite stellar population per age bin. [Msun]

  • ssp_log_ages_yr (array, shape (n_age,)) – Age grid of SSP basis (in cosmic time). [log10(yr)]

  • log_z (float) – Stellar metallicity (SSP grid metallicity). [log10(Z)]

  • neb_logU (float, optional) – HII region ionization parameter. Default: −3.0. [log10(U)]

  • neb_logZ_gas (float, optional) – Gas-phase metallicity (relative to solar). If None, defaults to stellar metallicity. Default: None. [log10(Z/Zsun)]

  • neb_fesc (float, optional) – Ionizing photon escape fraction (applies to both HII and DIG). Default: 0.0. [dimensionless, ∈ [0, 1]]

  • neb_fesc_lya (float, optional) – Lyman-α-specific escape fraction. Default: 0.0. [dimensionless, ∈ [0, 1]]

  • neb_dig_frac (float, optional) – DIG mass fraction. Default: 0.0. [dimensionless, ∈ [0, 1]]

  • neb_dig_delta_logU (float, optional) – Offset in ionization parameter for DIG (negative). Default: −1.0 dex. [dex]

  • line_sigma_aa (float, optional) – Gaussian line width for emission-line placement. Default: 0.0 (delta). [Å]

  • **kwargs – Additional backend-specific keyword arguments (passed to both calls).

Returns:

Combined nebular SED: (1 − f_DIG) × L_HII + f_DIG × L_DIG. [erg/s/Hz]

Return type:

array, shape (n_wave,)

Notes

JIT-compatible: yes — all operations use jnp primitives, but note that when neb_dig_frac is a traced JAX value, both HII and DIG forward passes execute (no short-circuit optimisation).

Mixing model (Haffner et al. 2009, Tacchella et al. 2022):

The diffuse ionized gas (DIG) has a lower ionization parameter than HII regions, producing a distinct line-ratio signature. We approximate the total emission as a linear combination:

\[L_{\mathrm{total}}(\lambda) = (1 - f_{\mathrm{DIG}}) \, L_{\mathrm{HII}}(\lambda, \log U_{\mathrm{HII}}) + f_{\mathrm{DIG}} \, L_{\mathrm{DIG}}(\lambda, \log U_{\mathrm{DIG}})\]

where log U_DIG = log U_HII + Δ log U, and both components use the same metallicity, escape fraction, and stellar population weights.

Physical picture:
  • HII regions: dense, photoionised by young (< 1 Myr) OB stars. Log U ~ −2.5 to −3. Dominated by recombination lines ([OIII], [SIII], etc.).

  • DIG: diffuse, warm (~8000 K), ionised by stellar radiation and shocks. Log U ~ −3 to −4. Dominated by forbidden lines ([NII], [SII], [OI]).

Escape fraction:

Both HII and DIG share the same f_esc (stellar-population-level escape). This is a simplification; physically, dust might preferentially shield DIG, but we do not model this spatial variation.

Pitfall: When neb_dig_frac is a JAX-traced (differentiable) value, both forward passes (HII + DIG) execute and contribute to gradients, even if neb_dig_frac = 0 at runtime. Pre-compute DIG mixing only when needed.

References

Observation Models

Photometry, spectroscopy, calibration, and emission line marginalization.

Spectrophotometric calibration polynomials.

When fitting spectra, the observed spectrum has wavelength-dependent calibration errors from flux calibration, slit losses, and telluric residuals. A low-order Chebyshev polynomial corrects this multiplicatively:

spec_obs(lambda) = C(lambda) * spec_physical(lambda)

where C(lambda) = 1 + sum_{n=1}^{order} a_n * T_n(x) and x = 2*(lambda - lambda_min)/(lambda_max - lambda_min) - 1 maps wavelengths to [-1, 1]. The constant term is unity by convention (overall normalization is handled elsewhere); the fitted coefficients represent deviations from a flat calibration.

Coefficients a_n have a Gaussian(0, sigma) prior that regularizes the polynomial toward unity, preventing overfitting of broad spectral features.

Parameters integration example

To add calibration coefficients as free parameters:

from tengri.parameters.priors import Gaussian

spec = Parameters(
    ...,
    # 3rd-order calibration polynomial (3 free coefficients)
    cal_c1=Gaussian(0.0, 0.1),
    cal_c2=Gaussian(0.0, 0.1),
    cal_c3=Gaussian(0.0, 0.05),
)

# In the forward model, pack coefficients and apply:
coeffs = jnp.array([params["cal_c1"], params["cal_c2"], params["cal_c3"]])
model_spec = apply_calibration(physical_spec, wave_obs, coeffs, wave_min, wave_max)

References

Johnson et al. (2021) — Prospector calibration model.

tengri.observation.calibration.apply_calibration(spectrum: Array, wavelength: Array, coeffs: Array, wave_min: float, wave_max: float) Array[source]

Apply calibration polynomial to a spectrum.

Returns spectrum * C(lambda), where C(lambda) is the Chebyshev calibration polynomial evaluated at the given wavelengths.

Parameters:
  • spectrum (array, shape (n_wave,)) – Physical model spectrum (any flux units).

  • wavelength (array, shape (n_wave,)) – Wavelength grid [Angstrom].

  • coeffs (array, shape (order,)) – Chebyshev coefficients a_1, …, a_order.

  • wave_min (float) – Wavelength range for normalization to [-1, 1].

  • wave_max (float) – Wavelength range for normalization to [-1, 1].

Returns:

Calibrated spectrum (same units as input spectrum).

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes. Gradient-safe: yes — differentiable w.r.t. spectrum and coefficients.

tengri.observation.calibration.apply_double_calibration(spectrum: Array, wavelength: Array, coeffs_blue: Array, coeffs_red: Array, wave_split: float) Array[source]

Apply piecewise calibration polynomial to a spectrum.

Parameters:
  • spectrum (array, shape (n_wave,)) – Physical model spectrum (any flux units).

  • wavelength (array, shape (n_wave,)) – Wavelength grid [Angstrom], must be sorted.

  • coeffs_blue (array, shape (order_blue,)) – Chebyshev coefficients for the blue arm (wavelength < wave_split).

  • coeffs_red (array, shape (order_red,)) – Chebyshev coefficients for the red arm (wavelength >= wave_split).

  • wave_split (float) – Wavelength boundary [Angstrom].

Returns:

Calibrated spectrum (same units as input spectrum).

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes. Gradient-safe: yes — differentiable w.r.t. spectrum and coefficients.

tengri.observation.calibration.calibration_polynomial(wavelength: Array, coeffs: Array, wave_min: float, wave_max: float) Array[source]

Multiplicative calibration polynomial C(lambda).

C(lambda) = 1 + sum_{n=1}^{order} a_n * T_n(x)

The constant term is fixed to 1 (flat calibration); coeffs supplies a_1 through a_order representing deviations from unity.

Parameters:
  • wavelength (array, shape (n_wave,)) – Wavelength grid [Angstrom].

  • coeffs (array, shape (order,)) – Chebyshev coefficients a_1, …, a_order. Empty array gives C(lambda) = 1 everywhere.

  • wave_min (float) – Wavelength range for normalization to [-1, 1].

  • wave_max (float) – Wavelength range for normalization to [-1, 1].

Returns:

Multiplicative calibration factor (dimensionless).

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes. Gradient-safe: yes — differentiable w.r.t. coefficients and wavelengths. Uses Clenshaw’s backward recurrence for numerically stable evaluation.

tengri.observation.calibration.chebyshev_basis(wavelength: Array, order: int, wave_min: float, wave_max: float) Array[source]

Evaluate Chebyshev polynomial basis at given wavelengths.

Uses the three-term recurrence relation for numerical stability: T_0(x) = 1, T_1(x) = x, T_{n+1}(x) = 2x*T_n(x) - T_{n-1}(x).

Parameters:
  • wavelength (array, shape (n_wave,)) – Wavelength grid [Angstrom].

  • order (int) – Maximum polynomial order (returns order+1 basis functions, from T_0 through T_order).

  • wave_min (float) – Minimum wavelength for normalization to [-1, 1] [Angstrom].

  • wave_max (float) – Maximum wavelength for normalization to [-1, 1] [Angstrom].

Returns:

Chebyshev basis functions T_0(x), T_1(x), …, T_order(x) (dimensionless).

Return type:

ndarray, shape (order+1, n_wave)

Notes

JIT-compatible: yes — order is a static argument. Gradient-safe: yes — differentiable w.r.t. wavelength.

tengri.observation.calibration.double_calibration_polynomial(wavelength: Array, coeffs_blue: Array, coeffs_red: Array, wave_split: float) Array[source]

Piecewise calibration: independent Chebyshev polynomials for blue and red halves.

For two-arm spectrographs (e.g. X-SHOOTER, DEIMOS) where a detector gap or dichroic split causes a calibration discontinuity, fitting a single polynomial across the full range can bias the result. This function applies separate polynomials to each half, matched to their local wavelength range.

Parameters:
  • wavelength (array, shape (n_wave,)) – Wavelength grid [Angstrom], must be sorted.

  • coeffs_blue (array, shape (order_blue,)) – Chebyshev coefficients for the blue arm (wavelength < wave_split).

  • coeffs_red (array, shape (order_red,)) – Chebyshev coefficients for the red arm (wavelength >= wave_split).

  • wave_split (float) – Wavelength boundary between blue and red arms [Angstrom].

Returns:

Multiplicative calibration factor (dimensionless).

Return type:

ndarray, shape (n_wave,)

Notes

JIT-compatible: yes. Gradient-safe: yes — differentiable w.r.t. coefficients and wavelengths.

tengri.observation.calibration.marginalize_calibration(model_flux: Array, obs_flux: Array, obs_err: Array, wavelength: Array, n_poly: int = 3, prior_sigma: float = 1.0) tuple[Array, Array, Array][source]

Analytically marginalize over calibration polynomial coefficients.

Given a model SED m(lambda) and observed spectrum d(lambda) with noise sigma(lambda), the calibrated model is C(lambda)*m(lambda) where C is a Chebyshev polynomial. With a Gaussian prior on the polynomial coefficients c ~ N(0, prior_sigma^2 I), the optimal coefficients and marginalized log-likelihood are computed in closed form.

This follows the Prospector approach: the calibration polynomial is treated as a nuisance and integrated out analytically at each likelihood evaluation, reducing the dimensionality of the sampling problem.

Parameters:
  • model_flux (array, shape (n_wave,)) – Physical model spectrum (any flux units, matching obs_flux).

  • obs_flux (array, shape (n_wave,)) – Observed spectrum (same units as model_flux).

  • obs_err (array, shape (n_wave,)) – 1-sigma uncertainties on the observed spectrum (same units).

  • wavelength (array, shape (n_wave,)) – Wavelength grid [Angstrom].

  • n_poly (int, optional) – Number of Chebyshev polynomial coefficients (order 1 through n_poly). The constant term (T_0 = 1) is implicit and fixed. Default 3.

  • prior_sigma (float, optional) – Standard deviation of the Gaussian prior on each coefficient. Default 1.0.

Returns:

  • log_likelihood_marginal (scalar) – Marginalized log-likelihood with calibration polynomial integrated out.

  • c_hat (ndarray, shape (n_poly,)) – MAP calibration coefficients (a_1 … a_n_poly).

  • c_hat_err (ndarray, shape (n_poly,)) – Posterior standard deviations of the coefficients.

Notes

JIT-compatible: yes — n_poly is a static argument. Gradient-safe: no — uses matrix inversion (not safe for gradient).

The marginalized log-likelihood integrates the Gaussian likelihood and Gaussian prior on the polynomial coefficients in closed form via the matrix determinant lemma.

References

Johnson et al. 2021, ApJS, 254, 22 (Prospector).

Stellar Population Synthesis

DSPS-based stellar population synthesis: loading SSP grids and computing effective metallicities.

class tengri.SSPData(ssp_wave: Array, ssp_flux: Array, ssp_lg_age_gyr: Array, ssp_lgmet: Array, ssp_mass_remaining: Array | None = None, ssp_alpha_fe: Array | None = None)[source]

Bases: NamedTuple

Immutable container for SSP template library.

Holds wavelength grid, flux templates, and metadata (age/metallicity grids) needed by the CSP integration engine. Typically loaded once from disk and reused across many forward-model evaluations.

Parameters:
  • ssp_wave (array, shape (n_wave,)) – Rest-frame wavelength grid [Angstrom].

  • ssp_flux (array, shape (n_met, n_age, n_wave)) – Spectral luminosity density of simple stellar populations (SSPs) per unit stellar mass [erg/s/Hz/Msun]. Origin: BC03, BPASS, FSPS, ProGeny, or other DSPS-compatible library.

  • ssp_lg_age_gyr (array, shape (n_age,)) – Age grid in log10 space [log10(Gyr)].

  • ssp_lgmet (array, shape (n_met,)) – Metallicity grid (absolute, NOT solar-relative) [log10(Z)]. Offset: log10(Z_sun) ≈ −1.848 (Asplund+2009). Do NOT confuse with user-facing log10(Z/Z_sun).

  • ssp_mass_remaining (array, shape (n_met, n_age), optional) – Fraction of initial stellar mass still present (living stars + remnants) at each (age, metallicity) [dimensionless, ∈ [0, 1]]. Used for stellar mass normalization in CSP integral. Depends on IMF and isochrone library; None if unavailable.

  • ssp_alpha_fe (array, optional) – Alpha enhancement grid (for future use). Currently None. When implemented: ssp_flux will be (n_met, n_alpha, n_age, n_wave).

Notes

Metallicity convention: ssp_lgmet is absolute log10(Z), NOT relative to solar. To convert user-supplied log10(Z/Z_sun) to grid coordinates, add LOG10_ZSUN ≈ −1.848.

Future extension: ssp_alpha_fe support for alpha-element abundance variations (Vazdekis+2015, MIST, etc.) is planned. Currently, metallicity is the only dimension; alpha is fixed (typically solar, α = 0).

Survival mass: ssp_mass_remaining encodes stellar mass loss due to stellar evolution (main-sequence turnoff, white dwarf cooling, etc.). It is essential for mass-based inferences but may not be present in older SSP libraries.

Examples

>>> from tengri import SSPData, load_ssp_data
>>> ssp = load_ssp_data("data/ssp_miles.h5")
>>> ssp.ssp_flux.shape  # (n_met, n_age, n_wave)
(22, 107, 4563)
ssp_alpha_fe: Array | None

Alias for field number 5

ssp_flux: Array

Alias for field number 1

ssp_lg_age_gyr: Array

Alias for field number 2

ssp_lgmet: Array

Alias for field number 3

ssp_mass_remaining: Array | None

Alias for field number 4

ssp_wave: Array

Alias for field number 0

tengri.load_ssp_data(filepath: str) SSPData[source]

Load SSP templates from a DSPS-format HDF5 file.

Reads stellar population synthesis templates stored in HDF5 format (compatible with DSPS library and distributed SSP libraries: BC03, BPASS, FSPS, ProGeny). Handles optional fields (ssp_mass_remaining, ssp_alpha_fe) gracefully.

Parameters:

filepath (str) – Path to HDF5 file. Expected fields: ssp_wave, ssp_flux, ssp_lg_age_gyr, ssp_lgmet. Optional: ssp_mass_remaining, ssp_alpha_fe.

Returns:

Loaded SSP container with all template data and metadata.

Return type:

SSPData

Raises:
  • ImportError – If h5py is not installed.

  • KeyError – If required HDF5 fields are missing.

  • OSError – If filepath does not exist or is not readable.

Notes

JIT-compatible: yes — only file I/O occurs; returned SSPData is immutable and suitable for use in JAX operations.

File format: Standard DSPS HDF5 layout. See DSPS documentation and distributed templates on halos.as.arizona.edu for format details.

Examples

>>> from tengri.components.stellar.sps import load_ssp_data
>>> ssp = load_ssp_data("data/ssp_bc03.h5")
>>> print(ssp.ssp_wave.shape, ssp.ssp_flux.shape)
(6000,) (50, 300, 6000)
tengri.effective_metallicity(log_z_fe: float, alpha_fe: float = 0.0) float[source]

Convert [Fe/H] + [alpha/Fe] to effective total metallicity.

Approximates the effect of alpha-element enhancement on the SED as a shift in the total metallicity used for SSP interpolation. Used when SSP templates are computed at fixed solar abundance ratios and cannot vary [alpha/Fe] at runtime.

Parameters:
  • log_z_fe (float) – Iron abundance [Fe/H] (equivalently, log10(Z/Zsun) when alpha_fe = 0). [dex]

  • alpha_fe (float, optional) – Alpha-element enhancement [alpha/Fe] relative to solar. Default 0.0 (solar abundance ratios). [dex]

Returns:

Effective total metallicity log10(Z_eff/Zsun). Same units as log_z_fe. [dex]

Return type:

float

Notes

JIT-compatible: yes — pure arithmetic; decorated with @jax.jit. Gradient-safe: yes.

Approximation (Thomas, Maraston & Bender 2003 [1]_): only valid for SSP grids that lack an explicit [alpha/Fe] axis. When the grid does include one, prefer bilinear (Z, [alpha/Fe]) interpolation; use has_alpha_grid() to test the SSP container at construction.

\[[Z/H]_{\mathrm{eff}} = [\mathrm{Fe}/\mathrm{H}] + 0.75 \, [\alpha/\mathrm{Fe}]\]

The coefficient 0.75 (_ALPHA_TO_Z_COEFF) is the empirical enhancement-to-metallicity scaling adopted by the Vazdekis et al. 2015 [2]_ MILES library for E-MILES alpha-enhanced SSPs.

References

Examples

>>> from tengri import effective_metallicity
>>> round(float(effective_metallicity(-0.5, alpha_fe=0.3)), 4)
-0.275

Filters

Loading and managing photometric filter transmission curves.

tengri.load_filter_set(names: list[str], cache_dir: str = 'data/filters') tuple[list[Array], list[Array], list[FilterCurve]][source]

Load multiple filters by short name.

Parameters:
  • names (list of str) – Short names from FILTER_REGISTRY.

  • cache_dir (str) – Directory for cached filter files. Default: "data/filters".

Returns:

  • filter_waves (list of jnp.ndarray) – Wavelength arrays per filter, each shape (n_wave,) [Angstrom].

  • filter_trans (list of jnp.ndarray) – Transmission arrays per filter, each shape (n_wave,) (dimensionless [0, 1]).

  • filter_curves (list of FilterCurve) – Full FilterCurve objects with wavelength, transmission, and name.

Raises:

KeyError – If any name is not in FILTER_REGISTRY.

Notes

Filters are downloaded from SVO on first use and cached locally. See load_filter() for single-filter loading.

Examples

>>> from tengri import load_filter_set
>>> waves, trans, curves = load_filter_set(["sdss_r", "sdss_i", "sdss_z"])
>>> len(curves)
3
>>> curves[0].name
'sdss_r'