Source code for naima.models

# -*- coding: utf-8 -*-
# Licensed under a 3-clause BSD style license - see LICENSE.rst
import os

import astropy.units as u
import numpy as np
from astropy.table import Table
from astropy.utils.data import get_pkg_data_filename

from .extern.validator import (
    validate_array,
    validate_physical_type,
    validate_scalar,
)
from .model_utils import memoize
from .radiative import Bremsstrahlung, InverseCompton, PionDecay, Synchrotron

__all__ = [
    "Synchrotron",
    "InverseCompton",
    "PionDecay",
    "Bremsstrahlung",
    "BrokenPowerLaw",
    "ExponentialCutoffPowerLaw",
    "PowerLaw",
    "LogParabola",
    "ExponentialCutoffBrokenPowerLaw",
    "TableModel",
    "EblAbsorptionModel",
]


def _validate_ene(ene):
    if isinstance(ene, dict) or isinstance(ene, Table):
        try:
            ene = validate_array(
                "energy", u.Quantity(ene["energy"]), physical_type="energy"
            )
        except KeyError:
            raise TypeError("Table or dict does not have 'ene' column")
    else:
        if not isinstance(ene, u.Quantity):
            ene = u.Quantity(ene)
        validate_physical_type("energy", ene, physical_type="energy")

    return ene


[docs]class PowerLaw: """ One dimensional power law model. Parameters ---------- amplitude : float Model amplitude. e_0 : `~astropy.units.Quantity` float Reference energy alpha : float Power law index See Also -------- PowerLaw, BrokenPowerLaw, LogParabola Notes ----- Model formula (with :math:`A` for ``amplitude``, :math:`\\alpha` for ``alpha``): .. math:: f(E) = A (E / E_0) ^ {-\\alpha} """ param_names = ["amplitude", "e_0", "alpha"] _memoize = False _cache = {} _queue = [] def __init__(self, amplitude, e_0, alpha): self.amplitude = amplitude self.e_0 = validate_scalar( "e_0", e_0, domain="positive", physical_type="energy" ) self.alpha = alpha
[docs] @staticmethod def eval(e, amplitude, e_0, alpha): """One dimensional power law model function""" xx = e / e_0 return amplitude * xx ** (-alpha)
@memoize def _calc(self, e): return self.eval( e.to("eV").value, self.amplitude, self.e_0.to("eV").value, self.alpha, ) def __call__(self, e): """One dimensional power law model function""" e = _validate_ene(e) return self._calc(e)
[docs]class ExponentialCutoffPowerLaw: """ One dimensional power law model with an exponential cutoff. Parameters ---------- amplitude : float Model amplitude e_0 : `~astropy.units.Quantity` float Reference point alpha : float Power law index e_cutoff : `~astropy.units.Quantity` float Cutoff point beta : float Cutoff exponent See Also -------- PowerLaw, BrokenPowerLaw, LogParabola Notes ----- Model formula (with :math:`A` for ``amplitude``, :math:`\\alpha` for ``alpha``, and :math:`\\beta` for ``beta``): .. math:: f(E) = A (E / E_0) ^ {-\\alpha} \\exp (- (E / E_{cutoff}) ^ \\beta) """ param_names = ["amplitude", "e_0", "alpha", "e_cutoff", "beta"] _memoize = False _cache = {} _queue = [] def __init__(self, amplitude, e_0, alpha, e_cutoff, beta=1.0): self.amplitude = amplitude self.e_0 = validate_scalar( "e_0", e_0, domain="positive", physical_type="energy" ) self.alpha = alpha self.e_cutoff = validate_scalar( "e_cutoff", e_cutoff, domain="positive", physical_type="energy" ) self.beta = beta
[docs] @staticmethod def eval(e, amplitude, e_0, alpha, e_cutoff, beta): "One dimensional power law with an exponential cutoff model function" xx = e / e_0 return amplitude * xx ** (-alpha) * np.exp(-((e / e_cutoff) ** beta))
@memoize def _calc(self, e): return self.eval( e.to("eV").value, self.amplitude, self.e_0.to("eV").value, self.alpha, self.e_cutoff.to("eV").value, self.beta, ) def __call__(self, e): "One dimensional power law with an exponential cutoff model function" e = _validate_ene(e) return self._calc(e)
[docs]class BrokenPowerLaw: """ One dimensional power law model with a break. Parameters ---------- amplitude : float Model amplitude at the break energy e_0 : `~astropy.units.Quantity` float Reference point e_break : `~astropy.units.Quantity` float Break energy alpha_1 : float Power law index for x < x_break alpha_2 : float Power law index for x > x_break See Also -------- PowerLaw, ExponentialCutoffPowerLaw, LogParabola Notes ----- Model formula (with :math:`A` for ``amplitude``, :math:`E_0` for ``e_0``, :math:`\\alpha_1` for ``alpha_1`` and :math:`\\alpha_2` for ``alpha_2``): .. math:: f(E) = \\left \\{ \\begin{array}{ll} A (E / E_0) ^ {-\\alpha_1} & : E < E_{break} \\\\ A (E_{break}/E_0) ^ {\\alpha_2-\\alpha_1} (E / E_0) ^ {-\\alpha_2} & : E > E_{break} \\\\ \\end{array} \\right. """ param_names = ["amplitude", "e_0", "e_break", "alpha_1", "alpha_2"] _memoize = False _cache = {} _queue = [] def __init__(self, amplitude, e_0, e_break, alpha_1, alpha_2): self.amplitude = amplitude self.e_0 = validate_scalar( "e_0", e_0, domain="positive", physical_type="energy" ) self.e_break = validate_scalar( "e_break", e_break, domain="positive", physical_type="energy" ) self.alpha_1 = alpha_1 self.alpha_2 = alpha_2
[docs] @staticmethod def eval(e, amplitude, e_0, e_break, alpha_1, alpha_2): """One dimensional broken power law model function""" K = np.where(e < e_break, 1, (e_break / e_0) ** (alpha_2 - alpha_1)) alpha = np.where(e < e_break, alpha_1, alpha_2) return amplitude * K * (e / e_0) ** -alpha
@memoize def _calc(self, e): return self.eval( e.to("eV").value, self.amplitude, self.e_0.to("eV").value, self.e_break.to("eV").value, self.alpha_1, self.alpha_2, ) def __call__(self, e): """One dimensional broken power law model function""" e = _validate_ene(e) return self._calc(e)
[docs]class ExponentialCutoffBrokenPowerLaw: """ One dimensional power law model with a break. Parameters ---------- amplitude : float Model amplitude at the break point e_0 : `~astropy.units.Quantity` float Reference point e_break : `~astropy.units.Quantity` float Break energy alpha_1 : float Power law index for x < x_break alpha_2 : float Power law index for x > x_break e_cutoff : `~astropy.units.Quantity` float Exponential Cutoff energy beta : float, optional Exponential cutoff rapidity. Default is 1. See Also -------- PowerLaw, ExponentialCutoffPowerLaw, LogParabola Notes ----- Model formula (with :math:`A` for ``amplitude``, :math:`E_0` for ``e_0``, :math:`\\alpha_1` for ``alpha_1``, :math:`\\alpha_2` for ``alpha_2``, :math:`E_{cutoff}` for ``e_cutoff``, and :math:`\\beta` for ``beta``): .. math:: f(E) = \\exp(-(E / E_{cutoff})^\\beta)\\left \\{ \\begin{array}{ll} A (E / E_0) ^ {-\\alpha_1} & : E < E_{break} \\\\ A (E_{break}/E_0) ^ {\\alpha_2-\\alpha_1} (E / E_0) ^ {-\\alpha_2} & : E > E_{break} \\\\ \\end{array} \\right. """ param_names = [ "amplitude", "e_0", "e_break", "alpha_1", "alpha_2", "e_cutoff", "beta", ] _memoize = False _cache = {} _queue = [] def __init__( self, amplitude, e_0, e_break, alpha_1, alpha_2, e_cutoff, beta=1.0 ): self.amplitude = amplitude self.e_0 = validate_scalar( "e_0", e_0, domain="positive", physical_type="energy" ) self.e_break = validate_scalar( "e_break", e_break, domain="positive", physical_type="energy" ) self.alpha_1 = alpha_1 self.alpha_2 = alpha_2 self.e_cutoff = validate_scalar( "e_cutoff", e_cutoff, domain="positive", physical_type="energy" ) self.beta = beta
[docs] @staticmethod def eval(e, amplitude, e_0, e_break, alpha_1, alpha_2, e_cutoff, beta): """One dimensional broken power law model function""" K = np.where(e < e_break, 1, (e_break / e_0) ** (alpha_2 - alpha_1)) alpha = np.where(e < e_break, alpha_1, alpha_2) ee2 = e / e_cutoff return amplitude * K * (e / e_0) ** -alpha * np.exp(-(ee2 ** beta))
@memoize def _calc(self, e): return self.eval( e.to("eV").value, self.amplitude, self.e_0.to("eV").value, self.e_break.to("eV").value, self.alpha_1, self.alpha_2, self.e_cutoff.to("eV").value, self.beta, ) def __call__(self, e): """One dimensional broken power law model with exponential cutoff function""" e = _validate_ene(e) return self._calc(e)
[docs]class LogParabola: """ One dimensional log parabola model (sometimes called curved power law). Parameters ---------- amplitude : float Model amplitude e_0 : `~astropy.units.Quantity` float Reference point alpha : float Power law index beta : float Power law curvature See Also -------- PowerLaw, BrokenPowerLaw, ExponentialCutoffPowerLaw Notes ----- Model formula (with :math:`A` for ``amplitude`` and :math:`\\alpha` for ``alpha`` and :math:`\\beta` for ``beta``): .. math:: f(e) = A \\left(\\frac{E}{E_{0}}\\right)^ {- \\alpha - \\beta \\log{\\left (\\frac{E}{E_{0}} \\right )}} """ param_names = ["amplitude", "e_0", "alpha", "beta"] _memoize = False _cache = {} _queue = [] def __init__(self, amplitude, e_0, alpha, beta): self.amplitude = amplitude self.e_0 = validate_scalar( "e_0", e_0, domain="positive", physical_type="energy" ) self.alpha = alpha self.beta = beta
[docs] @staticmethod def eval(e, amplitude, e_0, alpha, beta): """One dimenional log parabola model function""" ee = e / e_0 eeponent = -alpha - beta * np.log(ee) return amplitude * ee ** eeponent
@memoize def _calc(self, e): return self.eval( e.to("eV").value, self.amplitude, self.e_0.to("eV").value, self.alpha, self.beta, ) def __call__(self, e): """One dimensional curved power law function""" e = _validate_ene(e) return self._calc(e)
[docs]class TableModel: """ A model generated from a table of energy and value arrays. The units returned will be the units of the values array provided at initialization. The model will return values interpolated in log-space, returning 0 for energies outside of the limits of the provided energy array. Parameters ---------- energy : `~astropy.units.Quantity` array Array of energies at which the model values are given values : array Array with the values of the model at energies ``energy``. amplitude : float Model amplitude that is multiplied to the supplied arrays. Defaults to 1. """ def __init__(self, energy, values, amplitude=1): from scipy.interpolate import interp1d self._energy = validate_array( "energy", energy, domain="positive", physical_type="energy" ) self._values = values self.amplitude = amplitude loge = np.log10(self._energy.to("eV").value) try: self.unit = self._values.unit logy = np.log10(self._values.value) except AttributeError: self.unit = u.Unit("") logy = np.log10(self._values) self._interplogy = interp1d( loge, logy, fill_value=-np.Inf, bounds_error=False, kind="cubic" ) def __call__(self, e): e = _validate_ene(e) interpy = np.power(10, self._interplogy(np.log10(e.to("eV").value))) return self.amplitude * interpy * self.unit
[docs]class EblAbsorptionModel(TableModel): """ A TableModel containing the different absorption values from a specific model. It returns dimensionless opacity values, that could be multiplied to any model. Parameters ---------- redshift : float Redshift considered for the absorption evaluation. ebl_absorption_model : {'Dominguez'} Name of the EBL absorption model to use (Dominguez by default). Notes ----- Dominguez model refers to the Dominguez 2011 EBL model. Current implementation does NOT perform an interpolation in the redshift, so it just uses the closest z value from the finely binned tau_dominguez11.npz file (delta_z=0.01). See Also -------- TableModel """ def __init__(self, redshift, ebl_absorption_model="Dominguez"): # check that the redshift is a positive scalar if not isinstance(redshift, u.Quantity): redshift *= u.dimensionless_unscaled self.redshift = validate_scalar( "redshift", redshift, domain="positive", physical_type="dimensionless", ) self.model = ebl_absorption_model if self.model == "Dominguez": """Table generated by Alberto Dominguez containing tau vs energy [TeV] vs redshift. Energy is defined between 1 GeV and 100 TeV, in 500 bins uniform in log(E). Redshift is defined between 0.01 and 4, in steps of 0.01.""" filename = get_pkg_data_filename( os.path.join("data", "tau_dominguez11.npz") ) taus_table = np.load(filename)["arr_0"] redshift_list = np.arange(0.01, 4, 0.01) energy = taus_table["energy"] * u.TeV if self.redshift >= 0.01: colname = "col%s" % ( 2 + (np.abs(redshift_list - self.redshift)).argmin() ) table_values = taus_table[colname] # Set maximum value of the log(Tau) to 150, as it is high # enough. This solves later overflow problems. table_values[table_values > 150.0] = 150.0 taus = 10 ** table_values * u.dimensionless_unscaled elif self.redshift < 0.01: taus = ( 10 ** np.zeros(len(taus_table["energy"])) * u.dimensionless_unscaled ) else: raise ValueError('Model should be one of: ["Dominguez"]') super().__init__(energy, taus) def transmission(self, e): e = _validate_ene(e) taus = np.zeros(len(e)) for i in range(0, len(e)): if e[i].to("GeV").value < 1.0: taus[i] = 0.0 elif e[i].to("TeV").value > 100.0: taus[i] = np.log10(6000.0) else: taus[i] = np.log10(self(e[i])) return np.exp(-taus)