Пример #1
0
def make_image():
    table = Table.read('acceptance_curve.fits')
    table.pprint()
    center = SkyCoord(83.63, 22.01, unit='deg').galactic

    counts_image = make_empty_image(nxpix=1000, nypix=1000, binsz=0.01, xref=center.l.deg, yref=center.b.deg,
                                    proj='TAN')
    bkg_image = counts_image.copy()
    data_store = DataStore.from_dir('$GAMMAPY_EXTRA/datasets/hess-crab4-hd-hap-prod2')

    for events in data_store.load_all("events"):
        center = events.pointing_radec.galactic
        livetime = events.observation_live_time_duration
        solid_angle = Angle(0.01, "deg") ** 2

        counts_image.data += bin_events_in_image(events, counts_image).data

        #interp_param = dict(bounds_error=False, fill_value=None)

        acc_hdu = fill_acceptance_image(bkg_image.header, center, table["offset"], table["Acceptance"])
        acc = Quantity(acc_hdu.data, table["Acceptance"].unit) * solid_angle * livetime
        bkg_image.data += acc.decompose()
        print(acc.decompose().sum())

    counts_image.writeto("counts_image.fits", clobber=True)
    bkg_image.writeto("bkg_image.fits", clobber=True)
Пример #2
0
def test_saturation_water_pressure():

    args_list = [
        (1.e-30, None, apu.K),
        (1.e-30, None, apu.hPa),
        ]
    check_astro_quantities(atm.saturation_water_pressure, args_list)

    temp = Quantity([100, 200, 300], apu.K)
    press = Quantity([900, 1000, 1100], apu.hPa)

    press_w = Quantity(
        [2.57439748e-17, 3.23857740e-03, 3.55188758e+01], apu.hPa
        )

    assert_quantity_allclose(
        atm.saturation_water_pressure(temp, press),
        press_w
        )
    assert_quantity_allclose(
        atm.saturation_water_pressure(
            temp.to(apu.mK), press.to(apu.Pa)
            ),
        press_w
        )
Пример #3
0
    def evaluate(self, x):
        """Wrapper around the evaluate method on the Astropy model classes.

        Parameters
        ----------
        x : `~gammapy.utils.energy.Energy`
            Evaluation point
        """
        if self.spectral_model == 'PowerLaw':
            flux = models.PowerLaw1D.evaluate(x, self.parameters.norm,
                                              self.parameters.reference,
                                              self.parameters.index)
        elif self.spectral_model == 'LogParabola':
            # LogParabola evaluation does not work with arrays because
            # there is bug when using '**' with Quantities
            # see https://github.com/astropy/astropy/issues/4764
            flux = Quantity([models.LogParabola1D.evaluate(xx,
                                                           self.parameters.norm,
                                                           self.parameters.reference,
                                                           self.parameters.alpha,
                                                           self.parameters.beta)
                             for xx in x])
        else:
            raise NotImplementedError('Not implemented for model {}.'.format(self.spectral_model))

        return flux.to(self.parameters.norm.unit)
Пример #4
0
    def logspace(cls, vmin, vmax, nbins, unit=None):
        """Create axis with equally log-spaced nodes

        if no unit is given, it will be taken from vmax

        Parameters
        ----------
        vmin : `~astropy.units.Quantity`, float
            Lowest value
        vmax : `~astropy.units.Quantity`, float
            Highest value
        bins : int
            Number of bins
        unit : `~astropy.units.UnitBase`, str
            Unit
        """

        if unit is not None:
            vmin = Quantity(vmin, unit)
            vmax = Quantity(vmax, unit)
        else:
            vmin = Quantity(vmin)
            vmax = Quantity(vmax)
            unit = vmax.unit
            vmin = vmin.to(unit)

        x_min, x_max = np.log10([vmin.value, vmax.value])
        vals = np.logspace(x_min, x_max, nbins)

        return cls(vals * unit, interpolation_mode='log')
Пример #5
0
def solid_angle(image):
    """Compute the solid angle of each pixel.

    This will only give correct results for CAR maps!

    Parameters
    ----------
    image : `~astropy.io.fits.ImageHDU`
        Input image

    Returns
    -------
    area_image : `~astropy.units.Quantity`
        Solid angle image (matching the input image) in steradians.
    """
    # Area of one pixel at the equator
    cdelt0 = image.header['CDELT1']
    cdelt1 = image.header['CDELT2']
    equator_area = Quantity(abs(cdelt0 * cdelt1), 'deg2')

    # Compute image with fraction of pixel area at equator
    glat = coordinates(image)[1]
    area_fraction = np.cos(np.radians(glat))

    result = area_fraction * equator_area.to('sr')

    return result
Пример #6
0
def clean_up(table_in):
    """Create a new table with exactly the columns / info we want.
    """
    table = Table()
    
    v = Quantity(table_in['v'].data, table_in['v'].unit)
    energy = v.to('MeV', equivalencies=u.spectral())
    table['energy'] = energy
    
    #vFv = Quantity(table['vFv'].data, table['vFv'].unit)
    #flux = (vFv / (energy ** 2)).to('cm^-2 s^-1 MeV^-1')
    
    table['energy_flux'] = table_in['vFv']
    table['energy_flux_err_lo'] = table_in['ed_vFv'] 
    table['energy_flux_err_hi'] = table_in['eu_vFv']
    # Compute symmetrical error because most chi^2 fitters
    # can't handle asymmetrical Gaussian erros
    table['energy_flux_err'] = 0.5 * (table_in['eu_vFv'] + table_in['ed_vFv'])

    table['component'] = table_in['component']
    table['paper'] = table_in['paper']
    
    mask = table['energy_flux_err_hi'] == 0

    return table
Пример #7
0
    def make_bkg_cube(self, bkg_norm=True):
        """
        Make the background image for one observation from a bkg model.

        Parameters
        ----------
        bkg_norm : bool
            If true, apply the scaling factor from the number of counts
            outside the exclusion region to the bkg image
        """
        for i_E in range(len(self.energy_reco) - 1):
            energy_band = Energy(
                [self.energy_reco[i_E].value, self.energy_reco[i_E + 1].value],
                self.energy_reco.unit)
            table = self.bkg.acceptance_curve_in_energy_band(
                energy_band=energy_band)
            center = self.obs_center.galactic
            bkg_hdu = fill_acceptance_image(self.header, center,
                                            table["offset"],
                                            table["Acceptance"],
                                            self.offset_band[1])
            bkg_image = Quantity(bkg_hdu.data, table[
                "Acceptance"].unit) * self.bkg_cube.sky_image_ref.solid_angle() * self.livetime
            self.bkg_cube.data[i_E, :, :] = bkg_image.decompose().value

        if bkg_norm:
            scale = self.background_norm_factor()
            self.bkg_cube.data = scale * self.bkg_cube.data
            if self.save_bkg_scale:
                self.table_bkg_scale.add_row([self.obs_id, scale])
Пример #8
0
class MyQuantity(Quantity):

    def __init__(self,coord,unit):
        self.q = Quantity(coord,unit)

    @property
    def value(self):
        return self.q.value

    @property
    def unit(self):
        return self.q.unit

    def set_value(self,value):
        assert isinstance(value,(float,int))
        _unit = self.q.unit
        self.q = Quantity(value,_unit)

    def change_unit(self,unit):
        assert isinstance(unit,(str,Unit))
        _newQ = self.q.to(unit)
        self.q = _newQ

    def asunit(self,unit):
        _q = self.q.to(unit,equivalencies=spectral())
        return _q
Пример #9
0
def test_LogEnergyAxis():
    from scipy.stats import gmean
    energy = Quantity([1, 10, 100], 'TeV')
    energy_axis = LogEnergyAxis(energy)

    energy = Quantity(gmean([1, 10]), 'TeV')
    pix = energy_axis.wcs_world2pix(energy.to('MeV'))
    assert_allclose(pix, 0.5)

    world = energy_axis.wcs_pix2world(pix)
    assert_quantity_allclose(world, energy)
Пример #10
0
 def yindex(self, index):
     if index is None:
         del self.yindex
         return
     if not isinstance(index, Quantity):
         index = Quantity(index, unit=self._default_yunit, copy=False)
     self.y0 = index[0]
     index.regular = is_regular(index.value)
     if index.regular:
         self.dy = index[1] - index[0]
     else:
         del self.dy
     self._yindex = index
Пример #11
0
    def _get_range_from_textfields(self, min_text, max_text, linelist_units, plot_units):
        amin = amax = None
        if min_text.hasAcceptableInput() and max_text.hasAcceptableInput():

            amin = float(min_text.text())
            amax = float(max_text.text())

            amin = Quantity(amin, plot_units)
            amax = Quantity(amax, plot_units)

            amin = amin.to(linelist_units, equivalencies=u.spectral())
            amax = amax.to(linelist_units, equivalencies=u.spectral())

        return (amin, amax)
Пример #12
0
    def evaluate(self, method=None, **kwargs):
        """Evaluate NDData Array

        This function provides a uniform interface to several interpolators.
        The evaluation nodes are given as ``kwargs``.

        Currently available:
        `~scipy.interpolate.RegularGridInterpolator`, methods: linear, nearest

        Parameters
        ----------
        method : str {'linear', 'nearest'}, optional
            Interpolation method
        kwargs : dict
            Keys are the axis names, Values the evaluation points

        Returns
        -------
        array : `~astropy.units.Quantity`
            Interpolated values, axis order is the same as for the NDData array
        """
        values = []
        for axis in self.axes:
            # Extract values for each axis, default: nodes
            temp = Quantity(kwargs.pop(axis.name, axis.nodes))
            # Transform to correct unit
            temp = temp.to(axis.unit).value
            # Transform to match interpolation behaviour of axis
            values.append(np.atleast_1d(axis._interp_values(temp)))

        # This is to catch e.g. typos in axis names
        if kwargs != {}:
            raise ValueError("Input given for unknown axis: {}".format(kwargs))

        if method is None:
            out = self._eval_regular_grid_interp(values)
        elif method == 'linear':
            out = self._eval_regular_grid_interp(values, method='linear')
        elif method == 'nearest':
            out = self._eval_regular_grid_interp(values, method='nearest')
        else:
            raise ValueError('Interpolator {} not available'.format(method))

        # Clip interpolated values to be non-negative
        np.clip(out, 0, None, out=out)
        # Attach units to the output
        out = out * self.data.unit

        return out
Пример #13
0
def parse_value(value, default_units, equivalence=None):
    if isinstance(value, string_types):
        v = value.split(",")
        if len(v) == 2:
            value = (float(v[0]), v[1])
        else:
            value = float(v[0])
    if hasattr(value, "to_astropy"):
        value = value.to_astropy()
    if isinstance(value, Quantity):
        q = Quantity(value.value, value.unit)
    elif iterable(value):
        q = Quantity(value[0], value[1])
    else:
        q = Quantity(value, default_units)
    return q.to(default_units, equivalencies=equivalence).value
Пример #14
0
    def __init__(self, energy, rad, exposure=None, psf_value=None):

        self.energy = Quantity(energy).to('GeV')
        self.rad = Quantity(rad).to('radian')
        if exposure is None:
            self.exposure = Quantity(np.ones(len(energy)), 'cm^2 s')
        else:
            self.exposure = Quantity(exposure).to('cm^2 s')

        if psf_value is None:
            self.psf_value = Quantity(np.zeros(len(energy), len(rad)), 'sr^-1')
        else:
            self.psf_value = Quantity(psf_value).to('sr^-1')

        # Cache for TablePSF at each energy ... only computed when needed
        self._table_psf_cache = [None] * len(self.energy)
Пример #15
0
    def __init__(self, energy, offset, exposure=None, psf_value=None):

        self.energy = Quantity(energy).to("GeV")
        self.offset = Quantity(offset).to("radian")
        if not exposure:
            self.exposure = Quantity(np.ones(len(energy)), "cm^2 s")
        else:
            self.exposure = Quantity(exposure).to("cm^2 s")

        if not psf_value:
            self.psf_value = Quantity(np.zeros(len(energy), len(offset)), "sr^-1")
        else:
            self.psf_value = Quantity(psf_value).to("sr^-1")

        # Cache for TablePSF at each energy ... only computed when needed
        self._table_psf_cache = [None] * len(self.energy)
Пример #16
0
class Spectrum2D(object):
    """
    A 2D spectrum.

    Parameters
    ----------
    dispersion : `astropy.units.Quantity` or array, shape (N,)
        Spectral dispersion axis.

    data : array, shape (N, M)
        The spectral data.

    unit : `astropy.units.UnitBase` or str, optional
        Unit for the dispersion axis.

    """

    def __init__(self, dispersion, data, unit=None):

        self.dispersion = Quantity(dispersion, unit=unit)

        if unit is not None:
            self.wavelength = self.dispersion.to(angstrom)

        else:
            self.wavelength = self.dispersion

        self.data = data
Пример #17
0
    def __init__(
        self,
        energy_lo,
        energy_hi,
        theta,
        sigmas,
        norms,
        energy_thresh_lo="0.1 TeV",
        energy_thresh_hi="100 TeV",
    ):
        self.energy_lo = Quantity(energy_lo, "TeV")
        self.energy_hi = Quantity(energy_hi, "TeV")
        ebounds = EnergyBounds.from_lower_and_upper_bounds(
            self.energy_lo, self.energy_hi
        )
        self.energy = ebounds.log_centers
        self.theta = Quantity(theta, "deg")
        sigmas[0][sigmas[0] == 0] = 1
        sigmas[1][sigmas[1] == 0] = 1
        sigmas[2][sigmas[2] == 0] = 1
        self.sigmas = sigmas

        self.norms = norms
        self.energy_thresh_lo = Quantity(energy_thresh_lo, "TeV")
        self.energy_thresh_hi = Quantity(energy_thresh_hi, "TeV")

        self._interp_norms = self._setup_interpolators(self.norms)
        self._interp_sigmas = self._setup_interpolators(self.sigmas)
Пример #18
0
    def __str__(self):
        """Summary report (`str`).
        """
        ss = '*** Observation summary ***\n'
        ss += 'Target position: {}\n'.format(self.target_pos)

        ss += 'Number of observations: {}\n'.format(len(self.obs_table))

        livetime = Quantity(sum(self.obs_table['LIVETIME']), 'second')
        ss += 'Livetime: {:.2f}\n'.format(livetime.to('hour'))
        zenith = self.obs_table['ZEN_PNT']
        ss += 'Zenith angle: (mean={:.2f}, std={:.2f})\n'.format(
            zenith.mean(), zenith.std())
        offset = self.offset
        ss += 'Offset: (mean={:.2f}, std={:.2f})\n'.format(
            offset.mean(), offset.std())

        return ss
Пример #19
0
    def solid_angle_image(self):
        """Solid angle image.

        TODO: currently uses CDELT1 x CDELT2, which only
              works for cartesian images near the equator.

        Returns
        -------
        solid_angle_image : `~astropy.units.Quantity`
            Solid angle image (steradian)
        """
        cdelt = self.wcs.wcs.cdelt
        solid_angle = np.abs(cdelt[0]) * np.abs(cdelt[1])
        shape = self.data.shape[:2]

        solid_angle = solid_angle * np.ones(shape, dtype=float)
        solid_angle = Quantity(solid_angle, 'deg^2')

        return solid_angle.to('steradian')
Пример #20
0
def test_reproject_cube():
    # TODO: a better test can probably be implemented here to avoid
    # repeating code
    filenames = FermiGalacticCenter.filenames()
    spectral_cube = SpectralCube.read(filenames['diffuse_model'])
    exposure_cube = SpectralCube.read(filenames['exposure_cube'])

    original_cube = Quantity(np.nan_to_num(spectral_cube.data.value),
                             spectral_cube.data.unit)
    spectral_cube = spectral_cube.reproject_to(exposure_cube)
    reprojected_cube = Quantity(np.nan_to_num(spectral_cube.data.value),
                                spectral_cube.data.unit)
    # 0.5 degrees per pixel in diffuse model
    # 2 degrees in reprojection reference
    # sum of reprojected should be 1/16 of sum of original if flux-preserving
    expected = 0.0625 * original_cube.sum()
    actual = reprojected_cube.sum()

    assert_quantity_allclose(actual, expected, rtol=1e-2)
Пример #21
0
    def __init__(self, dispersion, data, unit=None):

        self.dispersion = Quantity(dispersion, unit=unit)

        if unit is not None:
            self.wavelength = self.dispersion.to(angstrom)

        else:
            self.wavelength = self.dispersion

        self.data = data
Пример #22
0
    def find_node(self, val):
        """Find next node

        Parameters
        ----------
        val : `~astropy.units.Quantity`
            Lookup value
        """
        val = Quantity(val)

        if not val.unit.is_equivalent(self.unit):
            raise ValueError("Units {} and {} do not match".format(val.unit, self.unit))

        val = val.to(self.data.unit)
        val = np.atleast_1d(val)
        x1 = np.array([val] * self.nbins).transpose()
        x2 = np.array([self.nodes] * len(val))
        temp = np.abs(x1 - x2)
        idx = np.argmin(temp, axis=1)
        return idx
Пример #23
0
    def __new__(cls, value, unit=None):

        if (isinstance(value, QueryableAttribute) or
                isinstance(value, ClauseElement)):
            unit = Unit(unit) if unit is not None else dimensionless_unscaled
            value = np.array(value)
            value = value.view(cls)
            value._unit = unit
            return value

        else:
            return Quantity.__new__(Quantity, value, unit=unit)
Пример #24
0
def test_refractive_index():

    args_list = [
        (1.e-30, None, apu.K),
        (1.e-30, None, apu.hPa),
        (1.e-30, None, apu.hPa),
        ]
    check_astro_quantities(atm.refractive_index, args_list)

    temp = Quantity([100, 200, 300], apu.K)
    press = Quantity([900, 1000, 1100], apu.hPa)
    press_w = Quantity([200, 500, 1000], apu.hPa)

    refr_index = Quantity(
        [1.0081872, 1.0050615, 1.00443253], cnv.dimless
        )

    assert_quantity_allclose(
        atm.refractive_index(temp, press, press_w),
        refr_index
        )
    assert_quantity_allclose(
        atm.refractive_index(
            temp.to(apu.mK), press.to(apu.Pa), press_w.to(apu.Pa)
            ),
        refr_index
        )
Пример #25
0
    def __init__(self, dispersion, flux, error=None, continuum=None,
                 mask=None, unit=None, dispersion_unit=None, meta=None):

        _unit = flux.unit if unit is None and hasattr(flux, 'unit') else unit

        if isinstance(error, (Quantity, Data)):
            if error.unit != _unit:
                raise UnitsError('The error unit must be the same as the '
                                 'flux unit.')
            error = error.value

        elif isinstance(error, Column):
            if error.unit != _unit:
                raise UnitsError('The error unit must be the same as the '
                                 'flux unit.')
            error = error.data

        # Set zero error elements to NaN:
        if error is not None:
            zero = error == 0
            error[zero] = np.nan

            # Mask these elements:
            if mask is not None:
                self.mask = mask | np.isnan(error)

            else:
                self.mask = np.isnan(error)

        # If dispersion is a `Quantity`, `Data`, or `Column` instance with the
        # unit attribute set, that unit is preserved if `dispersion_unit` is
        # None, but overriden otherwise
        self.dispersion = Quantity(dispersion, unit=dispersion_unit)

        if dispersion_unit is not None:
            self.wavelength = self.dispersion.to(angstrom)

        else:
            # Assume wavelength units:
            self.wavelength = self.dispersion

        self.flux = Data(flux, error, unit)

        if continuum is not None:
            self.continuum = Quantity(continuum, unit=unit)

        else:
            self.continuum = None

        self.meta = meta
Пример #26
0
# Licensed under a 3-clause BSD style license - see LICENSE.rst
from __future__ import absolute_import, division, print_function, unicode_literals
from numpy.testing import assert_allclose
import numpy as np
from astropy.units import Quantity
from ...source import SNR, SNRTrueloveMcKee

t = Quantity([0, 1, 10, 100, 1000, 10000], "yr")
snr = SNR()
snr_mckee = SNRTrueloveMcKee()


def test_SNR_luminosity_tev():
    """Test SNR luminosity"""
    reference = [0, 0, 0, 0, 1.076e+33, 1.076e+33]
    assert_allclose(snr.luminosity_tev(t).value, reference, rtol=1e-3)


def test_SNR_radius():
    """Test SNR radius"""
    reference = [0, 3.085e+16, 3.085e+17, 3.085e+18, 1.174e+19, 2.950e+19]
    assert_allclose(snr.radius(t).value, reference, rtol=1e-3)


def test_SNR_radius_inner():
    """Test SNR radius"""
    reference = (1 - 0.0914) * np.array(
        [0, 3.085e+16, 3.085e+17, 3.085e+18, 1.174e+19, 2.950e+19])
    assert_allclose(snr.radius_inner(t).value, reference, rtol=1e-3)

Пример #27
0
    def __init__(self,
                 atom_data,
                 ionization_data,
                 levels=None,
                 lines=None,
                 macro_atom_data=None,
                 macro_atom_references=None,
                 zeta_data=None,
                 collision_data=None,
                 collision_data_temperatures=None,
                 synpp_refs=None,
                 photoionization_data=None):

        self.prepared = False

        # CONVERT VALUES TO CGS UNITS

        # Convert atomic masses to CGS
        # We have to use constants.u because astropy uses
        # different values for the unit u and the constant.
        # This is changed in later versions of astropy (
        # the value of constants.u is used in all cases)
        if u.u.cgs == const.u.cgs:
            atom_data.loc[:, "mass"] = Quantity(atom_data["mass"].values,
                                                "u").cgs
        else:
            atom_data.loc[:, "mass"] = atom_data["mass"].values * const.u.cgs

        # Convert ionization energies to CGS
        ionization_data = ionization_data.squeeze()
        ionization_data[:] = Quantity(ionization_data[:], "eV").cgs

        # Convert energy to CGS
        levels.loc[:, "energy"] = Quantity(levels["energy"].values, 'eV').cgs

        # Sort the dataframe before indexing to improve performance
        lines.sort_index(inplace=True)

        # Create a new columns with wavelengths in the CGS units
        lines.loc[:, 'wavelength_cm'] = Quantity(lines['wavelength'],
                                                 'angstrom').cgs

        # SET ATTRIBUTES

        self.atom_data = atom_data
        self.ionization_data = ionization_data
        self.levels = levels
        self.lines = lines

        # Rename these (drop "_all") when `prepare_atom_data` is removed!
        self.macro_atom_data_all = macro_atom_data
        self.macro_atom_references_all = macro_atom_references

        self.zeta_data = zeta_data

        self.collision_data = collision_data
        self.collision_data_temperatures = collision_data_temperatures

        self.synpp_refs = synpp_refs

        self.photoionization_data = photoionization_data

        self._check_related()

        self.symbol2atomic_number = OrderedDict(
            zip(self.atom_data['symbol'].values, self.atom_data.index))
        self.atomic_number2symbol = OrderedDict(
            zip(self.atom_data.index, self.atom_data['symbol']))
Пример #28
0
class EnergyDependentTablePSF(object):
    """Energy-dependent radially-symmetric table PSF (``gtpsf`` format).

    TODO: add references and explanations.

    Parameters
    ----------
    energy : `~astropy.units.Quantity`
        Energy (1-dim)
    offset : `~astropy.units.Quantity` with angle units
        Offset angle (1-dim)
    exposure : `~astropy.units.Quantity` 
        Exposure (1-dim)
    psf_value : `~astropy.units.Quantity`
        PSF (2-dim with axes: psf[energy_index, offset_index]
    """

    def __init__(self, energy, offset, exposure=None, psf_value=None):

        self.energy = Quantity(energy).to("GeV")
        self.offset = Quantity(offset).to("radian")
        if not exposure:
            self.exposure = Quantity(np.ones(len(energy)), "cm^2 s")
        else:
            self.exposure = Quantity(exposure).to("cm^2 s")

        if not psf_value:
            self.psf_value = Quantity(np.zeros(len(energy), len(offset)), "sr^-1")
        else:
            self.psf_value = Quantity(psf_value).to("sr^-1")

        # Cache for TablePSF at each energy ... only computed when needed
        self._table_psf_cache = [None] * len(self.energy)

    @classmethod
    def from_fits(cls, hdu_list):
        """Create `EnergyDependentTablePSF` from ``gtpsf`` format HDU list.

        Parameters
        ----------
        hdu_list : `~astropy.io.fits.HDUList`
            HDU list with ``THETA`` and ``PSF`` extensions.
        """
        offset = Angle(hdu_list["THETA"].data["Theta"], "deg")
        energy = Quantity(hdu_list["PSF"].data["Energy"], "MeV")
        exposure = Quantity(hdu_list["PSF"].data["Exposure"], "cm^2 s")
        psf_value = Quantity(hdu_list["PSF"].data["PSF"], "sr^-1")

        return cls(energy, offset, exposure, psf_value)

    def to_fits(self):
        """Convert to FITS HDU list format.

        Returns
        -------
        hdu_list : `~astropy.io.fits.HDUList`
            PSF in HDU list format.
        """
        # TODO: write HEADER keywords as gtpsf

        data = self.offset
        theta_hdu = fits.BinTableHDU(data=data, name="Theta")

        data = [self.energy, self.exposure, self.psf_value]
        psf_hdu = fits.BinTableHDU(data=data, name="PSF")

        hdu_list = fits.HDUList([theta_hdu, psf_hdu])
        return hdu_list

    @classmethod
    def read(cls, filename):
        """Create `EnergyDependentTablePSF` from ``gtpsf``-format FITS file.

        Parameters
        ----------
        filename : str
            File name
        """
        hdu_list = fits.open(filename)
        return cls.from_fits(hdu_list)

    def write(self, *args, **kwargs):
        """Write to FITS file.

        Calls `~astropy.io.fits.HDUList.writeto`, forwarding all arguments.
        """
        self.to_fits().writeto(*args, **kwargs)

    def evaluate(self, energy=None, offset=None, interp_kwargs=None):
        """Interpolate the value of the `EnergyOffsetArray` at a given offset and Energy.

        Parameters
        ----------
        energy : `~astropy.units.Quantity`
            energy value
        offset : `~astropy.coordinates.Angle`
            offset value
        interp_kwargs : dict
            option for interpolation for `~scipy.interpolate.RegularGridInterpolator`

        Returns
        -------
        values : `~astropy.units.Quantity`
            Interpolated value
        """
        if not interp_kwargs:
            interp_kwargs = dict(bounds_error=False, fill_value=None)

        from scipy.interpolate import RegularGridInterpolator

        if energy is None:
            energy = self.energy
        if offset is None:
            offset = self.offset
        energy = Energy(energy).to("TeV")
        offset = Angle(offset).to("deg")
        energy_bin = self.energy.to("TeV")
        offset_bin = self.offset.to("deg")
        points = (energy_bin, offset_bin)
        interpolator = RegularGridInterpolator(points, self.psf_value, **interp_kwargs)
        ee, off = np.meshgrid(energy.value, offset.value, indexing="ij")
        shape = ee.shape
        pix_coords = np.column_stack([ee.flat, off.flat])
        data_interp = interpolator(pix_coords)
        return Quantity(data_interp.reshape(shape), self.psf_value.unit)

    def table_psf_at_energy(self, energy, interp_kwargs=None, **kwargs):
        """Evaluate the `EnergyOffsetArray` at one given energy.

        Parameters
        ----------
        energy : `~astropy.units.Quantity`
            Energy
        interp_kwargs : dict
            Option for interpolation for `~scipy.interpolate.RegularGridInterpolator`

        Returns
        -------
        table : `~astropy.table.Table`
            Table with two columns: offset, value
        """
        psf_value = self.evaluate(energy, None, interp_kwargs)[0, :]
        table_psf = TablePSF(self.offset, psf_value, **kwargs)

        return table_psf

    def table_psf_in_energy_band(self, energy_band, spectral_index=2, spectrum=None, **kwargs):
        """Average PSF in a given energy band.

        Expected counts in sub energy bands given the given exposure
        and spectrum are used as weights.

        Parameters
        ----------
        energy_band : `~astropy.units.Quantity`
            Energy band
        spectral_index : float
            Power law spectral index (used if spectrum=None).
        spectrum : callable
            Spectrum (callable with energy as parameter).

        Returns
        -------
        psf : `TablePSF`
            Table PSF
        """
        if spectrum is None:

            def spectrum(energy):
                return (energy / energy_band[0]) ** (-spectral_index)

        # TODO: warn if `energy_band` is outside available data.
        energy_idx_min, energy_idx_max = self._energy_index(energy_band)

        # TODO: extract this into a utility function `npred_weighted_mean()`

        # Compute weights for energy bins
        weights = np.zeros_like(self.energy.value, dtype=np.float64)
        for idx in range(energy_idx_min, energy_idx_max - 1):
            energy_min = self.energy[idx]
            energy_max = self.energy[idx + 1]
            exposure = self.exposure[idx]

            flux = spectrum(energy_min)
            weights[idx] = (exposure * flux * (energy_max - energy_min)).value

        # Normalize weights to sum to 1
        weights = weights / weights.sum()

        # Compute weighted PSF value array
        total_psf_value = np.zeros_like(self._get_1d_psf_values(0), dtype=np.float64)
        for idx in range(energy_idx_min, energy_idx_max - 1):
            psf_value = self._get_1d_psf_values(idx)
            total_psf_value += weights[idx] * psf_value

        # TODO: add version that returns `total_psf_value` without
        # making a `TablePSF`.
        return TablePSF(self.offset, total_psf_value, **kwargs)

    def containment_radius(self, energy, fraction, interp_kwargs=None):
        """Containment radius.

        Parameters
        ----------
        energy : `~astropy.units.Quantity`
            Energy
        fraction : float
            Containment fraction in %

        Returns
        -------
        radius : `~astropy.units.Quantity`
            Containment radius in deg
        """
        # TODO: useless at the moment ... support array inputs or remove!
        psf = self.table_psf_at_energy(energy, interp_kwargs)
        return psf.containment_radius(fraction)

    def integral(self, energy, offset_min, offset_max):
        """Containment fraction.

        Parameters
        ----------
        energy : `~astropy.units.Quantity`
            Energy
        offset_min, offset_max : `~astropy.coordinates.Angle`
            Offset

        Returns
        -------
        fraction : array_like
            Containment fraction (in range 0 .. 1)
        """
        # TODO: useless at the moment ... support array inputs or remove!

        psf = self.table_psf_at_energy(energy)
        return psf.integral(offset_min, offset_max)

    def info(self):
        """Print basic info."""
        # Summarise data members
        ss = array_stats_str(self.offset.to("deg"), "offset")
        ss += array_stats_str(self.energy, "energy")
        ss += array_stats_str(self.exposure, "exposure")

        # ss += 'integral = {0}\n'.format(self.integral())

        # Print some example containment radii
        fractions = [0.68, 0.95]
        energies = Quantity([10, 100], "GeV")
        for energy in energies:
            for fraction in fractions:
                radius = self.containment_radius(energy=energy, fraction=fraction)
                ss += "{0}% containment radius at {1}: {2}\n".format(100 * fraction, energy, radius)
        return ss

    def plot_psf_vs_theta(self, filename=None, energies=[1e4, 1e5, 1e6]):
        """Plot PSF vs theta.

        Parameters
        ----------
        TODO
        """
        import matplotlib.pyplot as plt

        plt.figure(figsize=(6, 4))

        for energy in energies:
            energy_index = self._energy_index(energy)
            psf = self.psf_value[energy_index, :]
            label = "{0} GeV".format(1e-3 * energy)
            x = np.hstack([-self.theta[::-1], self.theta])
            y = 1e-6 * np.hstack([psf[::-1], psf])
            plt.plot(x, y, lw=2, label=label)
        # plt.semilogy()
        # plt.loglog()
        plt.legend()
        plt.xlim(-0.2, 0.5)
        plt.xlabel("Offset (deg)")
        plt.ylabel("PSF (1e-6 sr^-1)")
        plt.tight_layout()

        if filename != None:
            plt.savefig(filename)

    def plot_containment_vs_energy(self, filename=None):
        """Plot containment versus energy."""
        raise NotImplementedError
        import matplotlib.pyplot as plt

        plt.clf()

        if filename != None:
            plt.savefig(filename)

    def plot_exposure_vs_energy(self, filename=None):
        """Plot exposure versus energy."""
        import matplotlib.pyplot as plt

        plt.figure(figsize=(4, 3))
        plt.plot(self.energy, self.exposure, color="black", lw=3)
        plt.semilogx()
        plt.xlabel("Energy (MeV)")
        plt.ylabel("Exposure (cm^2 s)")
        plt.xlim(1e4 / 1.3, 1.3 * 1e6)
        plt.ylim(0, 1.5e11)
        plt.tight_layout()

        if filename != None:
            plt.savefig(filename)

    def _energy_index(self, energy):
        """Find energy array index.
        """
        # TODO: test with array input
        return np.searchsorted(self.energy, energy)

    def _get_1d_psf_values(self, energy_index):
        """Get 1-dim PSF value array.

        Parameters
        ----------
        energy_index : int
            Energy index

        Returns
        -------
        psf_values : `~astropy.units.Quantity`
            PSF value array
        """
        psf_values = self.psf_value[energy_index, :].flatten().copy()
        return psf_values

    def _get_1d_table_psf(self, energy_index, **kwargs):
        """Get 1-dim TablePSF (cached).

        Parameters
        ----------
        energy_index : int
            Energy index

        Returns
        -------
        table_psf : `TablePSF`
            Table PSF
        """
        # TODO: support array_like `energy_index` here?
        if self._table_psf_cache[energy_index] is None:
            psf_value = self._get_1d_psf_values(energy_index)
            table_psf = TablePSF(self.offset, psf_value, **kwargs)
            self._table_psf_cache[energy_index] = table_psf

        return self._table_psf_cache[energy_index]
Пример #29
0
class EnergyDependentTablePSF(object):
    """Energy-dependent radially-symmetric table PSF (``gtpsf`` format).

    TODO: add references and explanations.

    Parameters
    ----------
    energy : `~astropy.units.Quantity`
        Energy (1-dim)
    rad : `~astropy.units.Quantity` with angle units
        Offset angle wrt source position (1-dim)
    exposure : `~astropy.units.Quantity`
        Exposure (1-dim)
    psf_value : `~astropy.units.Quantity`
        PSF (2-dim with axes: psf[energy_index, offset_index]
    """

    def __init__(self, energy, rad, exposure=None, psf_value=None):

        self.energy = Quantity(energy).to('GeV')
        self.rad = Quantity(rad).to('radian')
        if exposure is None:
            self.exposure = Quantity(np.ones(len(energy)), 'cm^2 s')
        else:
            self.exposure = Quantity(exposure).to('cm^2 s')

        if psf_value is None:
            self.psf_value = Quantity(np.zeros(len(energy), len(rad)), 'sr^-1')
        else:
            self.psf_value = Quantity(psf_value).to('sr^-1')

        # Cache for TablePSF at each energy ... only computed when needed
        self._table_psf_cache = [None] * len(self.energy)

    def __str__(self):
        ss = 'EnergyDependentTablePSF\n'
        ss += '-----------------------\n'
        ss += '\nAxis info:\n'
        ss += '  ' + array_stats_str(self.rad.to('deg'), 'rad')
        ss += '  ' + array_stats_str(self.energy, 'energy')
        # ss += '  ' + array_stats_str(self.exposure, 'exposure')

        # ss += 'integral = {}\n'.format(self.integral())

        ss += '\nContainment info:\n'
        # Print some example containment radii
        fractions = [0.68, 0.95]
        energies = Quantity([10, 100], 'GeV')
        for fraction in fractions:
            rads = self.containment_radius(energies=energies, fraction=fraction)
            for energy, rad in zip(energies, rads):
                ss += '  ' + '{}% containment radius at {:3.0f}: {:.2f}\n'.format(100 * fraction, energy, rad)
        return ss

    @classmethod
    def from_fits(cls, hdu_list):
        """Create `EnergyDependentTablePSF` from ``gtpsf`` format HDU list.

        Parameters
        ----------
        hdu_list : `~astropy.io.fits.HDUList`
            HDU list with ``THETA`` and ``PSF`` extensions.
        """
        rad = Angle(hdu_list['THETA'].data['Theta'], 'deg')
        energy = Quantity(hdu_list['PSF'].data['Energy'], 'MeV')
        exposure = Quantity(hdu_list['PSF'].data['Exposure'], 'cm^2 s')
        psf_value = Quantity(hdu_list['PSF'].data['PSF'], 'sr^-1')

        return cls(energy, rad, exposure, psf_value)

    def to_fits(self):
        """Convert to FITS HDU list format.

        Returns
        -------
        hdu_list : `~astropy.io.fits.HDUList`
            PSF in HDU list format.
        """
        # TODO: write HEADER keywords as gtpsf

        data = self.rad
        theta_hdu = fits.BinTableHDU(data=data, name='Theta')

        data = [self.energy, self.exposure, self.psf_value]
        psf_hdu = fits.BinTableHDU(data=data, name='PSF')

        hdu_list = fits.HDUList([theta_hdu, psf_hdu])
        return hdu_list

    @classmethod
    def read(cls, filename):
        """Create `EnergyDependentTablePSF` from ``gtpsf``-format FITS file.

        Parameters
        ----------
        filename : str
            File name
        """
        hdu_list = fits.open(filename)
        return cls.from_fits(hdu_list)

    def write(self, *args, **kwargs):
        """Write to FITS file.

        Calls `~astropy.io.fits.HDUList.writeto`, forwarding all arguments.
        """
        self.to_fits().writeto(*args, **kwargs)

    def evaluate(self, energy=None, rad=None, interp_kwargs=None):
        """Evaluate the PSF at a given energy and offset

        Parameters
        ----------
        energy : `~astropy.units.Quantity`
            energy value
        rad : `~astropy.coordinates.Angle`
            Offset wrt source position
        interp_kwargs : dict
            option for interpolation for `~scipy.interpolate.RegularGridInterpolator`

        Returns
        -------
        values : `~astropy.units.Quantity`
            Interpolated value
        """
        if interp_kwargs is None:
            interp_kwargs = dict(bounds_error=False, fill_value=None)

        from scipy.interpolate import RegularGridInterpolator
        if energy is None:
            energy = self.energy
        if rad is None:
            rad = self.rad
        energy = Energy(energy).to('TeV')
        rad = Angle(rad).to('deg')
        energy_bin = self.energy.to('TeV')
        rad_bin = self.rad.to('deg')
        points = (energy_bin, rad_bin)
        interpolator = RegularGridInterpolator(points, self.psf_value, **interp_kwargs)
        energy_grid, rad_grid = np.meshgrid(energy.value, rad.value, indexing='ij')
        shape = energy_grid.shape
        pix_coords = np.column_stack([energy_grid.flat, rad_grid.flat])
        data_interp = interpolator(pix_coords)
        return Quantity(data_interp.reshape(shape), self.psf_value.unit)

    def table_psf_at_energy(self, energy, interp_kwargs=None, **kwargs):
        """Evaluate the `EnergyOffsetArray` at one given energy.

        Parameters
        ----------
        energy : `~astropy.units.Quantity`
            Energy
        interp_kwargs : dict
            Option for interpolation for `~scipy.interpolate.RegularGridInterpolator`

        Returns
        -------
        table : `~astropy.table.Table`
            Table with two columns: offset, value
        """
        psf_value = self.evaluate(energy, None, interp_kwargs)[0, :]
        table_psf = TablePSF(self.rad, psf_value, **kwargs)
        return table_psf

    def kernels(self, cube, rad_max, **kwargs):
        """
        Make a set of 2D kernel images, representing the PSF at different energies.

        The kernel image is evaluated on the spatial and energy grid defined by
        the reference sky cube.

        Parameters
        ----------
        cube : `~gammapy.cube.SkyCube`
            Reference sky cube.
        rad_max `~astropy.coordinates.Angle`
            PSF kernel size
        kwargs : dict
            Keyword arguments passed to `EnergyDependentTablePSF.table_psf_in_energy_band()`.

        Returns
        -------
        kernels : list of `~numpy.ndarray`
            List of 2D convolution kernels.
        """
        energies = cube.energies(mode='edges')

        kernels = []
        for emin, emax in zip(energies[:-1], energies[1:]):
            energy_band = Quantity([emin, emax])
            try:
                psf = self.table_psf_in_energy_band(energy_band, **kwargs)
                kernel = psf.kernel(cube.sky_image_ref, rad_max=rad_max)
            except ValueError:
                kernel = np.nan * np.ones((1, 1))  # Dummy, means "no kernel available"
            kernels.append(kernel)
        return kernels

    def table_psf_in_energy_band(self, energy_band, spectral_index=2,
                                 spectrum=None, **kwargs):
        """Average PSF in a given energy band.

        Expected counts in sub energy bands given the given exposure
        and spectrum are used as weights.

        Parameters
        ----------
        energy_band : `~astropy.units.Quantity`
            Energy band
        spectral_index : float
            Power law spectral index (used if spectrum=None).
        spectrum : callable
            Spectrum (callable with energy as parameter).

        Returns
        -------
        psf : `TablePSF`
            Table PSF
        """
        if spectrum is None:
            def spectrum(energy):
                return (energy / energy_band[0]) ** (-spectral_index)

        # TODO: warn if `energy_band` is outside available data.
        energy_idx_min, energy_idx_max = self._energy_index(energy_band)

        # TODO: improve this, probably by evaluating the PSF (i.e. interpolating in energy) onto a new energy grid
        # This is a bit of a hack, but makes sure that a PSF is given, by forcing at least one slice:
        if energy_idx_max - energy_idx_min < 2:
            # log.warning('Dubious case of PSF energy binning')
            # Note that below always range stop of `energy_idx_max - 1` is used!
            # That's why we put +2 here to make sure we have at least one bin.
            energy_idx_max = max(energy_idx_min + 2, energy_idx_max)
            # Make sure we don't step out of the energy array (doesn't help much)
            energy_idx_max = min(energy_idx_max, len(self.energy))

        # TODO: extract this into a utility function `npred_weighted_mean()`

        # Compute weights for energy bins
        weights = np.zeros_like(self.energy.value, dtype=np.float64)
        for idx in range(energy_idx_min, energy_idx_max - 1):
            energy_min = self.energy[idx]
            energy_max = self.energy[idx + 1]
            exposure = self.exposure[idx]
            flux = spectrum(energy_min)
            weights[idx] = (exposure * flux * (energy_max - energy_min)).value

        # Normalize weights to sum to 1
        weights = weights / weights.sum()

        # Compute weighted PSF value array
        total_psf_value = np.zeros_like(self._get_1d_psf_values(0), dtype=np.float64)
        for idx in range(energy_idx_min, energy_idx_max - 1):
            psf_value = self._get_1d_psf_values(idx)
            total_psf_value += weights[idx] * psf_value

        # TODO: add version that returns `total_psf_value` without
        # making a `TablePSF`.
        return TablePSF(self.rad, total_psf_value, **kwargs)

    def containment_radius(self, energies, fraction, interp_kwargs=None):
        """Containment radius.

        Parameters
        ----------
        energies : `~astropy.units.Quantity`
            Energy
        fraction : float
            Containment fraction in %

        Returns
        -------
        rad : `~astropy.units.Quantity`
            Containment radius in deg
        """
        # TODO: figure out if there's a more efficient implementation to support
        # arrays of energy
        energies = np.atleast_1d(energies)
        psfs = [self.table_psf_at_energy(energy, interp_kwargs) for energy in energies]
        rad = [psf.containment_radius(fraction) for psf in psfs]
        return Quantity(rad)

    def integral(self, energy, rad_min, rad_max):
        """Containment fraction.

        Parameters
        ----------
        energy : `~astropy.units.Quantity`
            Energy
        rad_min, rad_max : `~astropy.coordinates.Angle`
            Offset

        Returns
        -------
        fraction : array_like
            Containment fraction (in range 0 .. 1)
        """
        # TODO: useless at the moment ... support array inputs or remove!

        psf = self.table_psf_at_energy(energy)
        return psf.integral(rad_min, rad_max)

    def info(self):
        """Print basic info"""
        print(self.__str__)

    def plot_psf_vs_rad(self, energies=[1e4, 1e5, 1e6]):
        """Plot PSF vs radius.

        Parameters
        ----------
        TODO
        """
        import matplotlib.pyplot as plt
        plt.figure(figsize=(6, 4))

        for energy in energies:
            energy_index = self._energy_index(energy)
            psf = self.psf_value[energy_index, :]
            label = '{} GeV'.format(1e-3 * energy)
            x = np.hstack([-self.rad[::-1], self.rad])
            y = 1e-6 * np.hstack([psf[::-1], psf])
            plt.plot(x, y, lw=2, label=label)
        # plt.semilogy()
        # plt.loglog()
        plt.legend()
        plt.xlim(-0.2, 0.5)
        plt.xlabel('Offset (deg)')
        plt.ylabel('PSF (1e-6 sr^-1)')
        plt.tight_layout()

    def plot_containment_vs_energy(self, ax=None, fractions=[0.63, 0.8, 0.95], **kwargs):
        """Plot containment versus energy."""
        import matplotlib.pyplot as plt

        ax = plt.gca() if ax is None else ax

        energy = Energy.equal_log_spacing(
            self.energy.min(), self.energy.max(), 10)

        for fraction in fractions:
            rad = self.containment_radius(energy, fraction)
            label = '{:.1f}% Containment'.format(100 * fraction)
            ax.plot(energy.value, rad.value, label=label, **kwargs)

        ax.semilogx()
        ax.legend(loc='best')
        ax.set_xlabel('Energy (GeV)')
        ax.set_ylabel('Containment radius (deg)')

    def plot_exposure_vs_energy(self):
        """Plot exposure versus energy."""
        import matplotlib.pyplot as plt
        plt.figure(figsize=(4, 3))
        plt.plot(self.energy, self.exposure, color='black', lw=3)
        plt.semilogx()
        plt.xlabel('Energy (MeV)')
        plt.ylabel('Exposure (cm^2 s)')
        plt.xlim(1e4 / 1.3, 1.3 * 1e6)
        plt.ylim(0, 1.5e11)
        plt.tight_layout()

    def _energy_index(self, energy):
        """Find energy array index.
        """
        # TODO: test with array input
        return np.searchsorted(self.energy, energy)

    def _get_1d_psf_values(self, energy_index):
        """Get 1-dim PSF value array.

        Parameters
        ----------
        energy_index : int
            Energy index

        Returns
        -------
        psf_values : `~astropy.units.Quantity`
            PSF value array
        """
        psf_values = self.psf_value[energy_index, :].flatten().copy()
        where_are_NaNs = np.isnan(psf_values)
        # When the PSF Table is not filled (with nan), the psf estimation at a given energy crashes
        psf_values[where_are_NaNs] = 0
        return psf_values

    def _get_1d_table_psf(self, energy_index, **kwargs):
        """Get 1-dim TablePSF (cached).

        Parameters
        ----------
        energy_index : int
            Energy index

        Returns
        -------
        table_psf : `TablePSF`
            Table PSF
        """
        # TODO: support array_like `energy_index` here?
        if self._table_psf_cache[energy_index] is None:
            psf_value = self._get_1d_psf_values(energy_index)
            table_psf = TablePSF(self.rad, psf_value, **kwargs)
            self._table_psf_cache[energy_index] = table_psf

        return self._table_psf_cache[energy_index]
Пример #30
0
def power_law(sources: Union[BaseSource, BaseSample],
              outer_radius: Union[str, Quantity],
              inner_radius: Union[str, Quantity] = Quantity(0, 'arcsec'),
              redshifted: bool = False,
              lum_en: Quantity = Quantity([[0.5, 2.0], [0.01, 100.0]], "keV"),
              start_pho_index: float = 1.,
              lo_en: Quantity = Quantity(0.3, "keV"),
              hi_en: Quantity = Quantity(7.9, "keV"),
              freeze_nh: bool = True,
              par_fit_stat: float = 1.,
              lum_conf: float = 68.,
              abund_table: str = "angr",
              fit_method: str = "leven",
              group_spec: bool = True,
              min_counts: int = 5,
              min_sn: float = None,
              over_sample: float = None,
              one_rmf: bool = True,
              num_cores: int = NUM_CORES,
              timeout: Quantity = Quantity(1, 'hr')):
    """
    This is a convenience function for fitting a tbabs absorbed powerlaw (or zpowerlw if redshifted
    is selected) to source spectra, with a multiplicative constant included to deal with different spectrum
    normalisations (constant*tbabs*powerlaw, or constant*tbabs*zpowerlw).

    :param List[BaseSource] sources: A single source object, or a sample of sources.
    :param str/Quantity outer_radius: The name or value of the outer radius of the region that the
        desired spectrum covers (for instance 'r200' would be acceptable for a GalaxyCluster,
        or Quantity(1000, 'kpc')). If 'region' is chosen (to use the regions in region files), then any
        inner radius will be ignored. If you are fitting for multiple sources then you can also pass a
        Quantity with one entry per source.
    :param str/Quantity inner_radius: The name or value of the outer radius of the region that the
        desired spectrum covers (for instance 'r200' would be acceptable for a GalaxyCluster,
        or Quantity(1000, 'kpc')). If 'region' is chosen (to use the regions in region files), then any
        inner radius will be ignored. By default this is zero arcseconds, resulting in a circular spectrum. If
        you are fitting for multiple sources then you can also pass a Quantity with one entry per source.
    :param bool redshifted: Whether the powerlaw that includes redshift (zpowerlw) should be used.
    :param Quantity lum_en: Energy bands in which to measure luminosity.
    :param float start_pho_index: The starting value for the photon index of the powerlaw.
    :param Quantity lo_en: The lower energy limit for the data to be fitted.
    :param Quantity hi_en: The upper energy limit for the data to be fitted.
    :param bool freeze_nh: Whether the hydrogen column density should be frozen.    :param start_pho_index:
    :param float par_fit_stat: The delta fit statistic for the XSPEC 'error' command, default is 1.0 which
        should be equivelant to 1sigma errors if I've understood (https://heasarc.gsfc.nasa.gov/xanadu/xspec
        /manual/XSerror.html) correctly.
    :param float lum_conf: The confidence level for XSPEC luminosity measurements.
    :param str abund_table: The abundance table to use for the fit.
    :param str fit_method: The XSPEC fit method to use.
    :param bool group_spec: A boolean flag that sets whether generated spectra are grouped or not.
    :param float min_counts: If generating a grouped spectrum, this is the minimum number of counts per channel.
        To disable minimum counts set this parameter to None.
    :param float min_sn: If generating a grouped spectrum, this is the minimum signal to noise in each channel.
        To disable minimum signal to noise set this parameter to None.
    :param float over_sample: The minimum energy resolution for each group, set to None to disable. e.g. if
        over_sample=3 then the minimum width of a group is 1/3 of the resolution FWHM at that energy.
    :param bool one_rmf: This flag tells the method whether it should only generate one RMF for a particular
        ObsID-instrument combination - this is much faster in some circumstances, however the RMF does depend
        slightly on position on the detector.
    :param int num_cores: The number of cores to use (if running locally), default is set to 90% of available.
    :param Quantity timeout: The amount of time each individual fit is allowed to run for, the default is one hour.
        Please note that this is not a timeout for the entire fitting process, but a timeout to individual source
        fits.
    """
    sources, inn_rad_vals, out_rad_vals = _pregen_spectra(
        sources, outer_radius, inner_radius, group_spec, min_counts, min_sn,
        over_sample, one_rmf, num_cores)
    sources = _check_inputs(sources, lum_en, lo_en, hi_en, fit_method,
                            abund_table, timeout)

    # This function is for a set model, either absorbed powerlaw or absorbed zpowerlw
    # These will be inserted into the general XSPEC script template, so lists of parameters need to be in the form
    #  of TCL lists.
    lum_low_lims = "{" + " ".join(lum_en[:,
                                         0].to("keV").value.astype(str)) + "}"
    lum_upp_lims = "{" + " ".join(lum_en[:,
                                         1].to("keV").value.astype(str)) + "}"
    if redshifted:
        model = "constant*tbabs*zpowerlw"
        par_names = "{factor nH PhoIndex Redshift norm}"
    else:
        model = "constant*tbabs*powerlaw"
        par_names = "{factor nH PhoIndex norm}"

    script_paths = []
    outfile_paths = []
    src_inds = []
    for src_ind, source in enumerate(sources):
        spec_objs = source.get_spectra(out_rad_vals[src_ind],
                                       inner_radius=inn_rad_vals[src_ind],
                                       group_spec=group_spec,
                                       min_counts=min_counts,
                                       min_sn=min_sn,
                                       over_sample=over_sample)

        # This is because many other parts of this function assume that spec_objs is iterable, and in the case of
        #  a source with only a single valid instrument for a single valid observation this may not be the case
        if isinstance(spec_objs, Spectrum):
            spec_objs = [spec_objs]

        if len(spec_objs) == 0:
            raise NoProductAvailableError(
                "There are no matching spectra for {s}, you "
                "need to generate them first!".format(s=source.name))

        # Turn spectra paths into TCL style list for substitution into template
        specs = "{" + " ".join([spec.path for spec in spec_objs]) + "}"
        # For this model, we have to know the redshift of the source.
        if redshifted and source.redshift is None:
            raise ValueError(
                "You cannot supply a source without a redshift if you have elected to fit zpowerlw."
            )
        elif redshifted and source.redshift is not None:
            par_values = "{{{0} {1} {2} {3} {4}}}".format(
                1.,
                source.nH.to("10^22 cm^-2").value, start_pho_index,
                source.redshift, 1.)
        else:
            par_values = "{{{0} {1} {2} {3}}}".format(
                1.,
                source.nH.to("10^22 cm^-2").value, start_pho_index, 1.)

        # Set up the TCL list that defines which parameters are frozen, dependant on user input
        if redshifted and freeze_nh:
            freezing = "{F T F T F}"
        elif not redshifted and freeze_nh:
            freezing = "{F T F F}"
        elif redshifted and not freeze_nh:
            freezing = "{F F F T F}"
        elif not redshifted and not freeze_nh:
            freezing = "{F F F F}"

        # Set up the TCL list that defines which parameters are linked across different spectra,
        #  dependant on user input
        if redshifted:
            linking = "{F T T T T}"
        else:
            linking = "{F T T T}"

        # If the powerlaw with redshift has been chosen, then we use the redshift attached to the source object
        #  If not we just pass a filler redshift and the luminosities are invalid
        if redshifted or (not redshifted and source.redshift is not None):
            z = source.redshift
        else:
            z = 1
            warnings.warn(
                "{s} has no redshift information associated, so luminosities from this fit"
                " will be invalid, as redshift has been set to one.".format(
                    s=source.name))

        out_file, script_file = _write_xspec_script(
            source, spec_objs[0].storage_key, model, abund_table, fit_method,
            specs, lo_en, hi_en, par_names, par_values, linking, freezing,
            par_fit_stat, lum_low_lims, lum_upp_lims, lum_conf, z, False, "{}",
            "{}", "{}", "{}", True)

        # If the fit has already been performed we do not wish to perform it again
        try:
            res = source.get_results(out_rad_vals[src_ind], model,
                                     inn_rad_vals[src_ind], None, group_spec,
                                     min_counts, min_sn, over_sample)
        except ModelNotAssociatedError:
            script_paths.append(script_file)
            outfile_paths.append(out_file)
            src_inds.append(src_ind)

    run_type = "fit"
    return script_paths, outfile_paths, num_cores, run_type, src_inds, None, timeout
Пример #31
0
 def rsun_obs(self):
     """
     Returns the solar radius as measured by EIT in arcseconds.
     """
     return Quantity(self.meta['solar_r'] * self.meta['cdelt1'], 'arcsec')
Пример #32
0
 def time_delta(self):
     """GTI durations in seconds (`~astropy.units.Quantity`)."""
     start = self.table["START"].astype("float64")
     stop = self.table["STOP"].astype("float64")
     return Quantity(stop - start, "second")
Пример #33
0
 def validate(cls, v):
     v = Quantity(v)
     if v.unit.physical_type != "energy":
         raise ValueError(f"Invalid unit for energy: {v.unit!r}")
     return v
Пример #34
0
 def __init__(self, lo, hi, **kwargs):
     self.lo = Quantity(lo)
     self.hi = Quantity(hi)
     super(BinnedDataAxis, self).__init__(None, **kwargs)
Пример #35
0
 def test_model_plot(self):
     model = self.source.spectral_model
     erange = Quantity([1, 10], 'TeV')
     model.plot(erange)
Пример #36
0
 def pointing_zen(self):
     """Pointing zenith angle sky (`~astropy.units.Quantity`)."""
     return Quantity(self.obs_info["ZEN_PNT"], unit="deg")
Пример #37
0
 def tstop(self):
     """Observation stop time (`~astropy.time.Time`)."""
     met_ref = time_ref_from_dict(self.data_store.obs_table.meta)
     met = Quantity(self.obs_info["TSTOP"].astype("float64"), "second")
     time = met_ref + met
     return time
Пример #38
0
 def uncertainty(self):
     return Quantity(self.layer.uncertainty.array,
                     unit=self.layer.units[1]).to(
                         self._plot_units[1],
                         equivalencies=spectral_density(self.dispersion))
Пример #39
0
def make_test_observation_table(observatory_name='HESS',
                                n_obs=10,
                                az_range=Angle([0, 360], 'deg'),
                                alt_range=Angle([45, 90], 'deg'),
                                date_range=(Time('2010-01-01'),
                                            Time('2015-01-01')),
                                use_abs_time=False,
                                n_tels_range=(3, 4),
                                random_state='random-seed'):
    """Make a test observation table.

    Create an observation table following a specific pattern.

    For the moment, only random observation tables are created.

    The observation table is created according to a specific
    observatory, and randomizing the observation pointingpositions
    in a specified az-alt range.

    If a *date_range* is specified, the starting time
    of the observations will be restricted to the specified interval.
    These parameters are interpreted as date, the precise hour of the
    day is ignored, unless the end date is closer than 1 day to the
    starting date, in which case, the precise time of the day is also
    considered.

    In addition, a range can be specified for the number of telescopes.

    Parameters
    ----------
    observatory_name : str, optional
        Name of the observatory; a list of choices is given in
        `~gammapy.data.observatory_locations`.
    n_obs : int, optional
        Number of observations for the obs table.
    az_range : `~astropy.coordinates.Angle`, optional
        Azimuth angle range (start, end) for random generation of
        observation pointing positions.
    alt_range : `~astropy.coordinates.Angle`, optional
        Altitude angle range (start, end) for random generation of
        observation pointing positions.
    date_range : `~astropy.time.Time`, optional
        Date range (start, end) for random generation of observation
        start time.
    use_abs_time : bool, optional
        Use absolute UTC times instead of [MET]_ seconds after the reference.
    n_tels_range : int, optional
        Range (start, end) of number of telescopes participating in
        the observations.
    random_state : {int, 'random-seed', 'global-rng', `~numpy.random.RandomState`}, optional
        Defines random number generator initialisation.
        Passed to `~gammapy.utils.random.get_random_state`.

    Returns
    -------
    obs_table : `~gammapy.data.ObservationTable`
        Observation table.
    """
    from ..data import ObservationTable, observatory_locations
    random_state = get_random_state(random_state)

    n_obs_start = 1

    obs_table = ObservationTable()

    # build a time reference as the start of 2010
    dateref = Time('2010-01-01T00:00:00')
    dateref_mjd_fra, dateref_mjd_int = np.modf(dateref.mjd)

    # define table header
    obs_table.meta['OBSERVATORY_NAME'] = observatory_name
    obs_table.meta['MJDREFI'] = dateref_mjd_int
    obs_table.meta['MJDREFF'] = dateref_mjd_fra
    if use_abs_time:
        # show the observation times in UTC
        obs_table.meta['TIME_FORMAT'] = 'absolute'
    else:
        # show the observation times in seconds after the reference
        obs_table.meta['TIME_FORMAT'] = 'relative'
    header = obs_table.meta

    # obs id
    obs_id = np.arange(n_obs_start, n_obs_start + n_obs)
    obs_table['OBS_ID'] = obs_id

    # obs time: 30 min
    ontime = Quantity(30. * np.ones_like(obs_id), 'minute').to('second')
    obs_table['ONTIME'] = ontime

    # livetime: 25 min
    time_live = Quantity(25. * np.ones_like(obs_id), 'minute').to('second')
    obs_table['LIVETIME'] = time_live

    # start time
    #  - random points between the start of 2010 and the end of 2014 (unless
    # otherwise specified)
    #  - using the start of 2010 as a reference time for the header of the table
    #  - observations restrict to night time (only if specified time interval is
    # more than 1 day)
    #  - considering start of astronomical day at midday: implicit in setting
    # the start of the night, when generating random night hours
    datestart = date_range[0]
    dateend = date_range[1]
    time_start = random_state.uniform(datestart.mjd, dateend.mjd, len(obs_id))
    time_start = Time(time_start, format='mjd', scale='utc')

    # check if time interval selected is more than 1 day
    if (dateend - datestart).jd > 1.:
        # keep only the integer part (i.e. the day, not the fraction of the day)
        time_start_f, time_start_i = np.modf(time_start.mjd)
        time_start = Time(time_start_i, format='mjd', scale='utc')

        # random generation of night hours: 6 h (from 22 h to 4 h), leaving 1/2 h
        # time for the last run to finish
        night_start = Quantity(22., 'hour')
        night_duration = Quantity(5.5, 'hour')
        hour_start = random_state.uniform(
            night_start.value, night_start.value + night_duration.value,
            len(obs_id))
        hour_start = Quantity(hour_start, 'hour')

        # add night hour to integer part of MJD
        time_start += hour_start

    if use_abs_time:
        # show the observation times in UTC
        time_start = Time(time_start.isot)
    else:
        # show the observation times in seconds after the reference
        time_start = time_relative_to_ref(time_start, header)
        # converting to quantity (better treatment of units)
        time_start = Quantity(time_start.sec, 'second')

    obs_table['TSTART'] = time_start

    # stop time
    # calculated as TSTART + ONTIME
    if use_abs_time:
        time_stop = Time(obs_table['TSTART'])
        time_stop += TimeDelta(obs_table['ONTIME'])
    else:
        time_stop = TimeDelta(obs_table['TSTART'])
        time_stop += TimeDelta(obs_table['ONTIME'])
        # converting to quantity (better treatment of units)
        time_stop = Quantity(time_stop.sec, 'second')

    obs_table['TSTOP'] = time_stop

    # az, alt
    # random points in a portion of sphere; default: above 45 deg altitude
    az, alt = sample_sphere(size=len(obs_id),
                            lon_range=az_range,
                            lat_range=alt_range,
                            random_state=random_state)
    az = Angle(az, 'deg')
    alt = Angle(alt, 'deg')
    obs_table['AZ'] = az
    obs_table['ALT'] = alt

    # RA, dec
    # derive from az, alt taking into account that alt, az represent the values
    # at the middle of the observation, i.e. at time_ref + (TIME_START + TIME_STOP)/2
    # (or better: time_ref + TIME_START + (TIME_OBSERVATION/2))
    # in use_abs_time mode, the time_ref should not be added, since it's already included
    # in TIME_START and TIME_STOP
    az = Angle(obs_table['AZ'])
    alt = Angle(obs_table['ALT'])
    if use_abs_time:
        obstime = Time(obs_table['TSTART'])
        obstime += TimeDelta(obs_table['ONTIME']) / 2.
    else:
        obstime = time_ref_from_dict(obs_table.meta)
        obstime += TimeDelta(obs_table['TSTART'])
        obstime += TimeDelta(obs_table['ONTIME']) / 2.
    location = observatory_locations[observatory_name]
    altaz_frame = AltAz(obstime=obstime, location=location)
    alt_az_coord = SkyCoord(az, alt, frame=altaz_frame)
    sky_coord = alt_az_coord.transform_to('icrs')
    obs_table['RA'] = sky_coord.ra
    obs_table['DEC'] = sky_coord.dec

    # positions

    # number of telescopes
    # random integers in a specified range; default: between 3 and 4
    n_tels = random_state.randint(n_tels_range[0], n_tels_range[1] + 1,
                                  len(obs_id))
    obs_table['N_TELS'] = n_tels

    # muon efficiency
    # random between 0.6 and 1.0
    muoneff = random_state.uniform(low=0.6, high=1.0, size=len(obs_id))
    obs_table['MUONEFF'] = muoneff

    return obs_table
Пример #40
0
 def time_start(self):
     """GTI start times (`~astropy.time.Time`)."""
     met = Quantity(self.table["START"].astype("float64"), "second")
     return self.time_ref + met
Пример #41
0
 def test_solid_angle_image(self):
     actual = self.spectral_cube.solid_angle_image[10][30]
     expected = Quantity(self.spectral_cube.wcs.wcs.cdelt[:-1].prod(), 'deg2')
     assert_quantity(actual, expected.to('sr'), rtol=1e-4)
Пример #42
0
class EnergyDependentMultiGaussPSF:
    """
    Triple Gauss analytical PSF depending on energy and theta.

    To evaluate the PSF call the ``to_energy_dependent_table_psf`` or ``psf_at_energy_and_theta`` methods.

    Parameters
    ----------
    energy_lo : `~astropy.units.Quantity`
        Lower energy boundary of the energy bin.
    energy_hi : `~astropy.units.Quantity`
        Upper energy boundary of the energy bin.
    theta : `~astropy.units.Quantity`
        Center values of the theta bins.
    sigmas : list of 'numpy.ndarray'
        Triple Gauss sigma parameters, where every entry is
        a two dimensional 'numpy.ndarray' containing the sigma
        value for every given energy and theta.
    norms : list of 'numpy.ndarray'
        Triple Gauss norm parameters, where every entry is
        a two dimensional 'numpy.ndarray' containing the norm
        value for every given energy and theta. Norm corresponds
        to the value of the Gaussian at theta = 0.
    energy_thresh_lo : `~astropy.units.Quantity`
        Lower save energy threshold of the psf.
    energy_thresh_hi : `~astropy.units.Quantity`
        Upper save energy threshold of the psf.

    Examples
    --------
    Plot R68 of the PSF vs. theta and energy:

    .. plot::
        :include-source:

        import matplotlib.pyplot as plt
        from gammapy.irf import EnergyDependentMultiGaussPSF
        filename = '$GAMMAPY_DATA/tests/unbundled/irfs/psf.fits'
        psf = EnergyDependentMultiGaussPSF.read(filename, hdu='POINT SPREAD FUNCTION')
        psf.plot_containment(0.68, show_safe_energy=False)
        plt.show()
    """

    def __init__(
        self,
        energy_lo,
        energy_hi,
        theta,
        sigmas,
        norms,
        energy_thresh_lo="0.1 TeV",
        energy_thresh_hi="100 TeV",
    ):
        self.energy_lo = Quantity(energy_lo, "TeV")
        self.energy_hi = Quantity(energy_hi, "TeV")
        ebounds = EnergyBounds.from_lower_and_upper_bounds(
            self.energy_lo, self.energy_hi
        )
        self.energy = ebounds.log_centers
        self.theta = Quantity(theta, "deg")
        sigmas[0][sigmas[0] == 0] = 1
        sigmas[1][sigmas[1] == 0] = 1
        sigmas[2][sigmas[2] == 0] = 1
        self.sigmas = sigmas

        self.norms = norms
        self.energy_thresh_lo = Quantity(energy_thresh_lo, "TeV")
        self.energy_thresh_hi = Quantity(energy_thresh_hi, "TeV")

        self._interp_norms = self._setup_interpolators(self.norms)
        self._interp_sigmas = self._setup_interpolators(self.sigmas)

    def _setup_interpolators(self, values_list):
        interps = []
        for values in values_list:
            interp = ScaledRegularGridInterpolator(
                points=(self.theta, self.energy), values=values
            )
            interps.append(interp)
        return interps

    @classmethod
    def read(cls, filename, hdu="PSF_2D_GAUSS"):
        """Create `EnergyDependentMultiGaussPSF` from FITS file.

        Parameters
        ----------
        filename : str
            File name
        """
        filename = make_path(filename)
        with fits.open(str(filename), memmap=False) as hdulist:
            psf = cls.from_fits(hdulist[hdu])

        return psf

    @classmethod
    def from_fits(cls, hdu):
        """Create `EnergyDependentMultiGaussPSF` from HDU list.

        Parameters
        ----------
        hdu : `~astropy.io.fits.BintableHDU`
            HDU
        """
        energy_lo = Quantity(hdu.data["ENERG_LO"][0], "TeV")
        energy_hi = Quantity(hdu.data["ENERG_HI"][0], "TeV")
        theta = Angle(hdu.data["THETA_LO"][0], "deg")

        # Get sigmas
        shape = (len(theta), len(energy_hi))
        sigmas = []
        for key in ["SIGMA_1", "SIGMA_2", "SIGMA_3"]:
            sigma = hdu.data[key].reshape(shape).copy()
            sigmas.append(sigma)

        # Get amplitudes
        norms = []
        for key in ["SCALE", "AMPL_2", "AMPL_3"]:
            norm = hdu.data[key].reshape(shape).copy()
            norms.append(norm)

        opts = {}
        try:
            opts["energy_thresh_lo"] = Quantity(hdu.header["LO_THRES"], "TeV")
            opts["energy_thresh_hi"] = Quantity(hdu.header["HI_THRES"], "TeV")
        except KeyError:
            pass

        return cls(energy_lo, energy_hi, theta, sigmas, norms, **opts)

    def to_fits(self):
        """
        Convert psf table data to FITS hdu list.

        Returns
        -------
        hdu_list : `~astropy.io.fits.HDUList`
            PSF in HDU list format.
        """
        # Set up data
        names = [
            "ENERG_LO",
            "ENERG_HI",
            "THETA_LO",
            "THETA_HI",
            "SCALE",
            "SIGMA_1",
            "AMPL_2",
            "SIGMA_2",
            "AMPL_3",
            "SIGMA_3",
        ]
        units = ["TeV", "TeV", "deg", "deg", "", "deg", "", "deg", "", "deg"]

        data = [
            self.energy_lo,
            self.energy_hi,
            self.theta,
            self.theta,
            self.norms[0],
            self.sigmas[0],
            self.norms[1],
            self.sigmas[1],
            self.norms[2],
            self.sigmas[2],
        ]

        table = Table()
        for name_, data_, unit_ in zip(names, data, units):
            table[name_] = [data_]
            table[name_].unit = unit_

        # Create hdu and hdu list
        hdu = fits.BinTableHDU(table)
        hdu.header["LO_THRES"] = self.energy_thresh_lo.value
        hdu.header["HI_THRES"] = self.energy_thresh_hi.value

        return fits.HDUList([fits.PrimaryHDU(), hdu])

    def write(self, filename, *args, **kwargs):
        """Write PSF to FITS file.

        Calls `~astropy.io.fits.HDUList.writeto`, forwarding all arguments.
        """
        self.to_fits().writeto(filename, *args, **kwargs)

    def psf_at_energy_and_theta(self, energy, theta):
        """
        Get `~gammapy.image.models.MultiGauss2D` model for given energy and theta.

        No interpolation is used.

        Parameters
        ----------
        energy : `~astropy.units.Quantity`
            Energy at which a PSF is requested.
        theta : `~astropy.coordinates.Angle`
            Offset angle at which a PSF is requested.

        Returns
        -------
        psf : `~gammapy.morphology.MultiGauss2D`
            Multigauss PSF object.
        """
        energy = Energy(energy)
        theta = Quantity(theta)

        pars = {}
        for name, interp_norm in zip(["scale", "A_2", "A_3"], self._interp_norms):
            pars[name] = interp_norm((theta, energy))

        for idx, interp_sigma in enumerate(self._interp_sigmas):
            pars["sigma_{}".format(idx + 1)] = interp_sigma((theta, energy))

        psf = HESSMultiGaussPSF(pars)
        return psf.to_MultiGauss2D(normalize=True)

    def containment_radius(self, energy, theta, fraction=0.68):
        """Compute containment for all energy and theta values"""
        # This is a false positive from pylint
        # See https://github.com/PyCQA/pylint/issues/2435
        energies = Energy(energy).flatten()  # pylint:disable=assignment-from-no-return
        thetas = Angle(theta).flatten()
        radius = np.empty((theta.size, energy.size))

        for idx, energy in enumerate(energies):
            for jdx, theta in enumerate(thetas):
                try:
                    psf = self.psf_at_energy_and_theta(energy, theta)
                    radius[jdx, idx] = psf.containment_radius(fraction)
                except ValueError:
                    log.debug(
                        "Computing containment failed for E = {:.2f}"
                        " and Theta={:.2f}".format(energy, theta)
                    )
                    log.debug("Sigmas: {} Norms: {}".format(psf.sigmas, psf.norms))
                    radius[jdx, idx] = np.nan

        return Angle(radius, "deg")

    def plot_containment(
        self, fraction=0.68, ax=None, show_safe_energy=False, add_cbar=True, **kwargs
    ):
        """
        Plot containment image with energy and theta axes.

        Parameters
        ----------
        fraction : float
            Containment fraction between 0 and 1.
        add_cbar : bool
            Add a colorbar
        """
        import matplotlib.pyplot as plt

        ax = plt.gca() if ax is None else ax

        energy = self.energy_hi
        offset = self.theta

        # Set up and compute data
        containment = self.containment_radius(energy, offset, fraction)

        # plotting defaults
        kwargs.setdefault("cmap", "GnBu")
        kwargs.setdefault("vmin", np.nanmin(containment.value))
        kwargs.setdefault("vmax", np.nanmax(containment.value))

        # Plotting
        x = energy.value
        y = offset.value
        caxes = ax.pcolormesh(x, y, containment.value, **kwargs)

        # Axes labels and ticks, colobar
        ax.semilogx()
        ax.set_ylabel("Offset ({unit})".format(unit=offset.unit))
        ax.set_xlabel("Energy ({unit})".format(unit=energy.unit))
        ax.set_xlim(x.min(), x.max())
        ax.set_ylim(y.min(), y.max())

        if show_safe_energy:
            self._plot_safe_energy_range(ax)

        if add_cbar:
            label = "Containment radius R{:.0f} ({})".format(
                100 * fraction, containment.unit
            )
            ax.figure.colorbar(caxes, ax=ax, label=label)

        return ax

    def _plot_safe_energy_range(self, ax):
        """add safe energy range lines to the plot"""
        esafe = self.energy_thresh_lo
        omin = self.offset.value.min()
        omax = self.offset.value.max()
        ax.hlines(y=esafe.value, xmin=omin, xmax=omax)
        label = "Safe energy threshold: {:3.2f}".format(esafe)
        ax.text(x=0.1, y=0.9 * esafe.value, s=label, va="top")

    def plot_containment_vs_energy(
        self, fractions=[0.68, 0.95], thetas=Angle([0, 1], "deg"), ax=None, **kwargs
    ):
        """Plot containment fraction as a function of energy.
        """
        import matplotlib.pyplot as plt

        ax = plt.gca() if ax is None else ax

        energy = Energy.equal_log_spacing(self.energy_lo[0], self.energy_hi[-1], 100)

        for theta in thetas:
            for fraction in fractions:
                radius = self.containment_radius(energy, theta, fraction).squeeze()
                label = "{} deg, {:.1f}%".format(theta.deg, 100 * fraction)
                ax.plot(energy.value, radius.value, label=label)

        ax.semilogx()
        ax.legend(loc="best")
        ax.set_xlabel("Energy (TeV)")
        ax.set_ylabel("Containment radius (deg)")

    def peek(self, figsize=(15, 5)):
        """Quick-look summary plots."""
        import matplotlib.pyplot as plt

        fig, axes = plt.subplots(nrows=1, ncols=3, figsize=figsize)

        self.plot_containment(fraction=0.68, ax=axes[0])
        self.plot_containment(fraction=0.95, ax=axes[1])
        self.plot_containment_vs_energy(ax=axes[2])

        # TODO: implement this plot
        # psf = self.psf_at_energy_and_theta(energy='1 TeV', theta='1 deg')
        # psf.plot_components(ax=axes[2])

        plt.tight_layout()

    def info(
        self,
        fractions=[0.68, 0.95],
        energies=Quantity([1.0, 10.0], "TeV"),
        thetas=Quantity([0.0], "deg"),
    ):
        """
        Print PSF summary info.

        The containment radius for given fraction, energies and thetas is
        computed and printed on the command line.

        Parameters
        ----------
        fractions : list
            Containment fraction to compute containment radius for.
        energies : `~astropy.units.Quantity`
            Energies to compute containment radius for.
        thetas : `~astropy.units.Quantity`
            Thetas to compute containment radius for.

        Returns
        -------
        ss : string
            Formatted string containing the summary info.
        """
        ss = "\nSummary PSF info\n"
        ss += "----------------\n"
        # Summarise data members
        ss += array_stats_str(self.theta.to("deg"), "Theta")
        ss += array_stats_str(self.energy_hi, "Energy hi")
        ss += array_stats_str(self.energy_lo, "Energy lo")
        ss += "Safe energy threshold lo: {:6.3f}\n".format(self.energy_thresh_lo)
        ss += "Safe energy threshold hi: {:6.3f}\n".format(self.energy_thresh_hi)

        for fraction in fractions:
            containment = self.containment_radius(energies, thetas, fraction)
            for i, energy in enumerate(energies):
                for j, theta in enumerate(thetas):
                    radius = containment[j, i]
                    ss += (
                        "{:2.0f}% containment radius at theta = {} and "
                        "E = {:4.1f}: {:5.8f}\n"
                        "".format(100 * fraction, theta, energy, radius)
                    )
        return ss

    def to_energy_dependent_table_psf(self, theta=None, rad=None, exposure=None):
        """
        Convert triple Gaussian PSF ot table PSF.

        Parameters
        ----------
        theta : `~astropy.coordinates.Angle`
            Offset in the field of view. Default theta = 0 deg
        rad : `~astropy.coordinates.Angle`
            Offset from PSF center used for evaluating the PSF on a grid.
            Default offset = [0, 0.005, ..., 1.495, 1.5] deg.
        exposure : `~astropy.units.Quantity`
            Energy dependent exposure. Should be in units equivalent to 'cm^2 s'.
            Default exposure = 1.

        Returns
        -------
        tabe_psf : `~gammapy.irf.EnergyDependentTablePSF`
            Instance of `EnergyDependentTablePSF`.
        """
        # Convert energies to log center
        energies = self.energy
        # Defaults and input handling
        if theta is None:
            theta = Angle(0, "deg")
        else:
            theta = Angle(theta)

        if rad is None:
            rad = Angle(np.arange(0, 1.5, 0.005), "deg")
        else:
            rad = Angle(rad).to("deg")

        psf_value = Quantity(np.zeros((energies.size, rad.size)), "deg^-2")

        for idx, energy in enumerate(energies):
            psf_gauss = self.psf_at_energy_and_theta(energy, theta)
            psf_value[idx] = Quantity(psf_gauss(rad), "deg^-2")

        return EnergyDependentTablePSF(
            energy=energies, rad=rad, exposure=exposure, psf_value=psf_value
        )

    def to_psf3d(self, rad):
        """Create a PSF3D from an analytical PSF.

        Parameters
        ----------
        rad : `~astropy.units.Quantity` or `~astropy.coordinates.Angle`
            the array of position errors (rad) on which the PSF3D will be defined

        Returns
        -------
        psf3d : `~gammapy.irf.PSF3D`
            the PSF3D. It will be defined on the same energy and offset values than the input psf.
        """
        offsets = self.theta
        energy = self.energy
        energy_lo = self.energy_lo
        energy_hi = self.energy_hi
        rad_lo = rad[:-1]
        rad_hi = rad[1:]

        psf_values = np.zeros(
            (rad_lo.shape[0], offsets.shape[0], energy_lo.shape[0])
        ) * Unit("sr-1")

        for i, offset in enumerate(offsets):
            psftable = self.to_energy_dependent_table_psf(offset)
            psf_values[:, i, :] = psftable.evaluate(energy, 0.5 * (rad_lo + rad_hi)).T

        return PSF3D(
            energy_lo,
            energy_hi,
            offsets,
            rad_lo,
            rad_hi,
            psf_values,
            self.energy_thresh_lo,
            self.energy_thresh_hi,
        )
Пример #43
0
def _write_xspec_script(source: BaseSource, spec_storage_key: str, model: str, abund_table: str, fit_method: str,
                        specs: str, lo_en: Quantity, hi_en: Quantity, par_names: str, par_values: str,
                        linking: str, freezing: str, par_fit_stat: float, lum_low_lims: str, lum_upp_lims: str,
                        lum_conf: float, redshift: float, pre_check: bool, check_par_names: str, check_par_lo_lims: str,
                        check_par_hi_lims: str, check_par_err_lims: str, norm_scale: bool) -> Tuple[str, str]:
    """
    This writes out a configured XSPEC script, and is common to all fit functions.

    :param BaseSource source: The source for which an XSPEC script is being created
    :param str spec_storage_key: The storage key that the spectra that have been included in the current fit
        are stored under.
    :param str model: The model being fitted to the data.
    :param str abund_table: The chosen abundance table for XSPEC to use.
    :param str fit_method: Which fit method should XSPEC use to fit the model to data.
    :param str specs: A string containing the paths to all spectra to be fitted.
    :param Quantity lo_en: The lower energy limit for the data to be fitted.
    :param Quantity hi_en: The upper energy limit for the data to be fitted.
    :param str par_names: A string containing the names of the model parameters.
    :param str par_values: A string containing the start values of the model parameters.
    :param str linking: A string containing the linking settings for the model.
    :param str freezing: A string containing the freezing settings for the model.
    :param float par_fit_stat: The delta fit statistic for the XSPEC 'error' command.
    :param str lum_low_lims: A string containing the lower energy limits for the luminosity measurements.
    :param str lum_upp_lims: A string containing the upper energy limits for the luminosity measurements.
    :param float lum_conf: The confidence level for XSPEC luminosity measurements.
    :param float redshift: The redshift of the object.
    :param bool pre_check: Flag indicating whether a pre-check of the quality of the input spectra
        should be performed.
    :param str check_par_names: A string representing a TCL list of model parameter names that checks should be
        performed on.
    :param str check_par_lo_lims: A string representing a TCL list of allowed lower limits for the check_par_names
        parameter entries.
    :param str check_par_hi_lims: A string representing a TCL list of allowed upper limits for the check_par_names
        parameter entries.
    :param str check_par_err_lims: A string representing a TCL list of allowed upper limits for the parameter
        uncertainties.
    :param bool norm_scale: Is there an extra constant designed to account for the differences in normalisation
        you can get from different observations of a cluster.
    :return: The paths to the output file and the script file.
    :rtype: Tuple[str, str]
    """
    # Read in the template file for the XSPEC script.
    with open(BASE_XSPEC_SCRIPT, 'r') as x_script:
        script = x_script.read()

    # There has to be a directory to write this xspec script to, as well as somewhere for the fit output
    #  to be stored
    dest_dir = OUTPUT + "XSPEC/" + source.name + "/"
    if not os.path.exists(dest_dir):
        os.makedirs(dest_dir)
    # Defining where the output summary file of the fit is written
    out_file = dest_dir + source.name + "_" + spec_storage_key + "_" + model
    script_file = dest_dir + source.name + "_" + spec_storage_key + "_" + model + ".xcm"

    # The template is filled out here, taking everything we have generated and everything the user
    #  passed in. The result is an XSPEC script that can be run as is.
    script = script.format(xsp=XGA_EXTRACT, ab=abund_table, md=fit_method, H0=source.cosmo.H0.value,
                           q0=0., lamb0=source.cosmo.Ode0, sp=specs, lo_cut=lo_en.to("keV").value,
                           hi_cut=hi_en.to("keV").value, m=model, pn=par_names, pv=par_values,
                           lk=linking, fr=freezing, el=par_fit_stat, lll=lum_low_lims, lul=lum_upp_lims,
                           of=out_file, redshift=redshift, lel=lum_conf, check=pre_check, cps=check_par_names,
                           cpsl=check_par_lo_lims, cpsh=check_par_hi_lims, cpse=check_par_err_lims, ns=norm_scale)

    # Write out the filled-in template to its destination
    with open(script_file, 'w') as xcm:
        xcm.write(script)

    return out_file, script_file
Пример #44
0
def make_test_bg_cube_model(detx_range=Angle([-10., 10.], 'deg'),
                            ndetx_bins=24,
                            dety_range=Angle([-10., 10.], 'deg'),
                            ndety_bins=24,
                            energy_band=Quantity([0.01, 100.], 'TeV'),
                            nenergy_bins=14,
                            altitude=Angle(70., 'deg'),
                            sigma=Angle(5., 'deg'),
                            spectral_index=2.7,
                            apply_mask=False,
                            do_not_force_mev_units=False):
    """Make a test bg cube model.

    The background counts cube is created following a 2D symmetric
    gaussian model for the spatial coordinates (X, Y) and a power-law
    in energy.
    The gaussian width varies in energy from sigma/2 to sigma.
    The power-law slope in log-log representation is given by
    the spectral_index parameter.
    The norm depends linearly on the livetime
    and on the altitude angle of the observation.
    It is possible to mask 1/4th of the image (for **x > x_center**
    and **y > y_center**). Useful for testing coordinate rotations.

    Per default units of *1 / (MeV sr s)* for the bg rate are
    enforced, unless *do_not_force_mev_units* is set.
    This is in agreement to the convention applied in
    `~gammapy.background.make_bg_cube_model`.

    This method is useful for instance to produce true (simulated)
    background cube models to compare to the reconstructed ones
    produced with `~gammapy.background.make_bg_cube_model`.
    For details on how to do this, please refer to
    :ref:`background_make_background_models_datasets_for_testing`.

    Parameters
    ----------
    detx_range : `~astropy.coordinates.Angle`, optional
        X coordinate range (min, max).
    ndetx_bins : int, optional
        Number of (linear) bins in X coordinate.
    dety_range : `~astropy.coordinates.Angle`, optional
        Y coordinate range (min, max).
    ndety_bins : int, optional
        Number of (linear) bins in Y coordinate.
    energy_band : `~astropy.units.Quantity`, optional
        Energy range (min, max).
    nenergy_bins : int, optional
        Number of (logarithmic) bins in energy.
    altitude : `~astropy.coordinates.Angle`, optional
        observation altitude angle for the model.
    sigma : `~astropy.coordinates.Angle`, optional
        Width of the gaussian model used for the spatial coordinates.
    spectral_index : float, optional
        Index for the power-law model used for the energy coordinate.
    apply_mask : bool, optional
        If set, 1/4th of the image is masked (for **x > x_center**
        and **y > y_center**).
    do_not_force_mev_units : bool, optional
        Set to ``True`` to use the same energy units as the energy
        binning for the bg rate.

    Returns
    -------
    bg_cube_model : `~gammapy.background.FOVCubeBackgroundModel`
        Bacground cube model.
    """
    from ..background import FOVCubeBackgroundModel

    # spatial bins (linear)
    delta_x = (detx_range[1] - detx_range[0]) / ndetx_bins
    detx_bin_edges = np.arange(ndetx_bins + 1) * delta_x + detx_range[0]

    delta_y = (dety_range[1] - dety_range[0]) / ndety_bins
    dety_bin_edges = np.arange(ndety_bins + 1) * delta_y + dety_range[0]

    # energy bins (logarithmic)
    log_delta_energy = (np.log(energy_band[1].value) -
                        np.log(energy_band[0].value)) / nenergy_bins
    energy_bin_edges = np.exp(
        np.arange(nenergy_bins + 1) * log_delta_energy +
        np.log(energy_band[0].value))
    energy_bin_edges = Quantity(energy_bin_edges, energy_band[0].unit)
    # TODO: this function should be reviewed/re-written, when
    # the following PR is completed:
    # https://github.com/gammapy/gammapy/pull/290

    # define empty bg cube model and set bins

    bg_cube_model = FOVCubeBackgroundModel.set_cube_binning(
        detx_edges=detx_bin_edges,
        dety_edges=dety_bin_edges,
        energy_edges=energy_bin_edges)

    # counts

    # define coordinate grids for the calculations
    det_bin_centers = bg_cube_model.counts_cube.image_bin_centers
    energy_bin_centers = bg_cube_model.counts_cube.energy_edges.log_centers
    energy_points, dety_points, detx_points = np.meshgrid(energy_bin_centers,
                                                          det_bin_centers[1],
                                                          det_bin_centers[0],
                                                          indexing='ij')

    E_0 = Quantity(1., 'TeV')  # reference energy for the model

    # norm of the model
    # taking as reference for now a dummy value of 1
    # it is linearly dependent on the zenith angle (90 deg - altitude)
    # it is norm_max at alt = 90 deg and norm_max/2 at alt = 0 deg
    norm_max = Quantity(1, '')
    alt_min = Angle(0., 'deg')
    alt_max = Angle(90., 'deg')
    slope = (norm_max - norm_max / 2) / (alt_max - alt_min)
    free_term = norm_max / 2 - slope * alt_min
    norm = altitude * slope + free_term

    # define E dependent sigma
    # it is defined via a PL, in order to be log-linear
    # it is equal to the parameter sigma at E max
    # and sigma/2. at E min
    sigma_min = sigma / 2.  # at E min
    sigma_max = sigma  # at E max
    s_index = np.log(sigma_max / sigma_min)
    s_index /= np.log(energy_bin_edges[-1] / energy_bin_edges[0])
    s_norm = sigma_min * ((energy_bin_edges[0] / E_0)**-s_index)
    sigma = s_norm * ((energy_points / E_0)**s_index)

    # calculate counts
    gaussian = np.exp(-((detx_points)**2 + (dety_points)**2) / sigma**2)
    powerlaw = (energy_points / E_0)**-spectral_index
    counts = norm * gaussian * powerlaw

    bg_cube_model.counts_cube.data = Quantity(counts, '')

    # livetime
    # taking as reference for now a dummy value of 1 s
    livetime = Quantity(1., 'second')
    bg_cube_model.livetime_cube.data += livetime

    # background
    bg_cube_model.background_cube.data = bg_cube_model.counts_cube.data.copy()
    bg_cube_model.background_cube.data /= bg_cube_model.livetime_cube.data
    bg_cube_model.background_cube.data /= bg_cube_model.background_cube.bin_volume
    # bg_cube_model.background_cube.set_zero_level()

    if not do_not_force_mev_units:
        # use units of 1 / (MeV sr s) for the bg rate
        bg_rate = bg_cube_model.background_cube.data.to('1 / (MeV sr s)')
        bg_cube_model.background_cube.data = bg_rate

    # apply mask if requested
    if apply_mask:
        # find central coordinate
        detx_center = (detx_range[1] + detx_range[0]) / 2.
        dety_center = (dety_range[1] + dety_range[0]) / 2.
        mask = (detx_points <= detx_center) & (dety_points <= dety_center)
        bg_cube_model.counts_cube.data *= mask
        bg_cube_model.livetime_cube.data *= mask
        bg_cube_model.background_cube.data *= mask

    return bg_cube_model
Пример #45
0
def single_temp_apec(sources: Union[BaseSource, BaseSample],
                     outer_radius: Union[str, Quantity],
                     inner_radius: Union[str,
                                         Quantity] = Quantity(0, 'arcsec'),
                     start_temp: Quantity = Quantity(3.0, "keV"),
                     start_met: float = 0.3,
                     lum_en: Quantity = Quantity([[0.5, 2.0], [0.01, 100.0]],
                                                 "keV"),
                     freeze_nh: bool = True,
                     freeze_met: bool = True,
                     lo_en: Quantity = Quantity(0.3, "keV"),
                     hi_en: Quantity = Quantity(7.9, "keV"),
                     par_fit_stat: float = 1.,
                     lum_conf: float = 68.,
                     abund_table: str = "angr",
                     fit_method: str = "leven",
                     group_spec: bool = True,
                     min_counts: int = 5,
                     min_sn: float = None,
                     over_sample: float = None,
                     one_rmf: bool = True,
                     num_cores: int = NUM_CORES,
                     spectrum_checking: bool = True,
                     timeout: Quantity = Quantity(1, 'hr')):
    """
    This is a convenience function for fitting an absorbed single temperature apec model(constant*tbabs*apec) to an
    object. It would be possible to do the exact same fit using the custom_model function, but as it will
    be a very common fit a dedicated function is in order. If there are no existing spectra with the passed
    settings, then they will be generated automatically.

    If the spectrum checking step of the XSPEC fit is enabled (using the boolean flag spectrum_checking), then
    each individual spectrum available for a given source will be fitted, and if the measured temperature is less
    than or equal to 0.01keV, or greater than 20keV, or the temperature uncertainty is greater than 15keV, then
    that spectrum will be rejected and not included in the final fit. Spectrum checking also involves rejecting any
    spectra with fewer than 10 noticed channels.

    :param List[BaseSource] sources: A single source object, or a sample of sources.
    :param str/Quantity outer_radius: The name or value of the outer radius of the region that the
        desired spectrum covers (for instance 'r200' would be acceptable for a GalaxyCluster,
        or Quantity(1000, 'kpc')). If 'region' is chosen (to use the regions in region files), then any
        inner radius will be ignored. If you are fitting for multiple sources then you can also pass a
        Quantity with one entry per source.
    :param str/Quantity inner_radius: The name or value of the outer radius of the region that the
        desired spectrum covers (for instance 'r200' would be acceptable for a GalaxyCluster,
        or Quantity(1000, 'kpc')). If 'region' is chosen (to use the regions in region files), then any
        inner radius will be ignored. By default this is zero arcseconds, resulting in a circular spectrum. If
        you are fitting for multiple sources then you can also pass a Quantity with one entry per source.
    :param Quantity start_temp: The initial temperature for the fit.
    :param start_met: The initial metallicity for the fit (in ZSun).
    :param Quantity lum_en: Energy bands in which to measure luminosity.
    :param bool freeze_nh: Whether the hydrogen column density should be frozen.
    :param bool freeze_met: Whether the metallicity parameter in the fit should be frozen.
    :param Quantity lo_en: The lower energy limit for the data to be fitted.
    :param Quantity hi_en: The upper energy limit for the data to be fitted.
    :param float par_fit_stat: The delta fit statistic for the XSPEC 'error' command, default is 1.0 which should be
        equivelant to 1σ errors if I've understood (https://heasarc.gsfc.nasa.gov/xanadu/xspec/manual/XSerror.html)
        correctly.
    :param float lum_conf: The confidence level for XSPEC luminosity measurements.
    :param str abund_table: The abundance table to use for the fit.
    :param str fit_method: The XSPEC fit method to use.
    :param bool group_spec: A boolean flag that sets whether generated spectra are grouped or not.
    :param float min_counts: If generating a grouped spectrum, this is the minimum number of counts per channel.
        To disable minimum counts set this parameter to None.
    :param float min_sn: If generating a grouped spectrum, this is the minimum signal to noise in each channel.
        To disable minimum signal to noise set this parameter to None.
    :param float over_sample: The minimum energy resolution for each group, set to None to disable. e.g. if
        over_sample=3 then the minimum width of a group is 1/3 of the resolution FWHM at that energy.
    :param bool one_rmf: This flag tells the method whether it should only generate one RMF for a particular
        ObsID-instrument combination - this is much faster in some circumstances, however the RMF does depend
        slightly on position on the detector.
    :param int num_cores: The number of cores to use (if running locally), default is set to 90% of available.
    :param bool spectrum_checking: Should the spectrum checking step of the XSPEC fit (where each spectrum is fit
        individually and tested to see whether it will contribute to the simultaneous fit) be activated?
    :param Quantity timeout: The amount of time each individual fit is allowed to run for, the default is one hour.
        Please note that this is not a timeout for the entire fitting process, but a timeout to individual source
        fits.
    """
    sources, inn_rad_vals, out_rad_vals = _pregen_spectra(
        sources, outer_radius, inner_radius, group_spec, min_counts, min_sn,
        over_sample, one_rmf, num_cores)
    sources = _check_inputs(sources, lum_en, lo_en, hi_en, fit_method,
                            abund_table, timeout)

    # This function is for a set model, absorbed apec, so I can hard code all of this stuff.
    # These will be inserted into the general XSPEC script template, so lists of parameters need to be in the form
    #  of TCL lists.
    model = "constant*tbabs*apec"
    par_names = "{factor nH kT Abundanc Redshift norm}"
    lum_low_lims = "{" + " ".join(lum_en[:,
                                         0].to("keV").value.astype(str)) + "}"
    lum_upp_lims = "{" + " ".join(lum_en[:,
                                         1].to("keV").value.astype(str)) + "}"

    script_paths = []
    outfile_paths = []
    src_inds = []
    # This function supports passing multiple sources, so we have to setup a script for all of them.
    for src_ind, source in enumerate(sources):
        # Find matching spectrum objects associated with the current source
        spec_objs = source.get_spectra(out_rad_vals[src_ind],
                                       inner_radius=inn_rad_vals[src_ind],
                                       group_spec=group_spec,
                                       min_counts=min_counts,
                                       min_sn=min_sn,
                                       over_sample=over_sample)
        # This is because many other parts of this function assume that spec_objs is iterable, and in the case of
        #  a cluster with only a single valid instrument for a single valid observation this may not be the case
        if isinstance(spec_objs, Spectrum):
            spec_objs = [spec_objs]

        # Obviously we can't do a fit if there are no spectra, so throw an error if that's the case
        if len(spec_objs) == 0:
            raise NoProductAvailableError(
                "There are no matching spectra for {s} object, you "
                "need to generate them first!".format(s=source.name))

        # Turn spectra paths into TCL style list for substitution into template
        specs = "{" + " ".join([spec.path for spec in spec_objs]) + "}"
        # For this model, we have to know the redshift of the source.
        if source.redshift is None:
            raise ValueError(
                "You cannot supply a source without a redshift to this model.")

        # Whatever start temperature is passed gets converted to keV, this will be put in the template
        t = start_temp.to("keV", equivalencies=u.temperature_energy()).value
        # Another TCL list, this time of the parameter start values for this model.
        par_values = "{{{0} {1} {2} {3} {4} {5}}}".format(
            1.,
            source.nH.to("10^22 cm^-2").value, t, start_met, source.redshift,
            1.)

        # Set up the TCL list that defines which parameters are frozen, dependant on user input
        if freeze_nh and freeze_met:
            freezing = "{F T F T T F}"
        elif not freeze_nh and freeze_met:
            freezing = "{F F F T T F}"
        elif freeze_nh and not freeze_met:
            freezing = "{F T F F T F}"
        elif not freeze_nh and not freeze_met:
            freezing = "{F F F F T F}"

        # Set up the TCL list that defines which parameters are linked across different spectra, only the
        #  multiplicative constant that accounts for variation in normalisation over different observations is not
        #  linked
        linking = "{F T T T T T}"

        # If the user wants the spectrum cleaning step to be run, then we have to setup some acceptable
        #  limits. For this function they will be hardcoded, for simplicities sake, and we're only going to
        #  check the temperature, as its the main thing we're fitting for with constant*tbabs*apec
        if spectrum_checking:
            check_list = "{kT}"
            check_lo_lims = "{0.01}"
            check_hi_lims = "{20}"
            check_err_lims = "{15}"
        else:
            check_list = "{}"
            check_lo_lims = "{}"
            check_hi_lims = "{}"
            check_err_lims = "{}"

        out_file, script_file = _write_xspec_script(
            source, spec_objs[0].storage_key, model, abund_table, fit_method,
            specs, lo_en, hi_en, par_names, par_values, linking, freezing,
            par_fit_stat, lum_low_lims, lum_upp_lims, lum_conf,
            source.redshift, spectrum_checking, check_list, check_lo_lims,
            check_hi_lims, check_err_lims, True)

        # If the fit has already been performed we do not wish to perform it again
        try:
            res = source.get_results(out_rad_vals[src_ind], model,
                                     inn_rad_vals[src_ind], 'kT', group_spec,
                                     min_counts, min_sn, over_sample)
        except ModelNotAssociatedError:
            script_paths.append(script_file)
            outfile_paths.append(out_file)
            src_inds.append(src_ind)

    run_type = "fit"
    return script_paths, outfile_paths, num_cores, run_type, src_inds, None, timeout
Пример #46
0
 def mDM(self, mDM):
     mDM_vals = self.table['mDM'].data
     mDM_ = Quantity(mDM).to('GeV').value
     interp_idx = np.argmin(np.abs(mDM_vals - mDM_))
     self._mDM = Quantity(mDM_vals[interp_idx], 'GeV')
Пример #47
0
 def lumi(t):
     t = Quantity(t, "s")
     return pulsar.luminosity_spindown(t).value
Пример #48
0
def tr_add_unit(value, unitname):
    return Quantity(value, unitname)
Пример #49
0
def test_Pulsar_period():
    """Test pulsar period"""
    reference = Quantity([0.10000001, 0.10000123, 0.10012331, 0.11270709], "s")
    assert_quantity_allclose(pulsar.period(time), reference)
Пример #50
0
def _dens_setup(
    sources: Union[GalaxyCluster, ClusterSample],
    outer_radius: Union[str, Quantity],
    inner_radius: Union[str, Quantity],
    abund_table: str,
    lo_en: Quantity,
    hi_en: Quantity,
    group_spec: bool = True,
    min_counts: int = 5,
    min_sn: float = None,
    over_sample: float = None,
    obs_id: Union[str, list] = None,
    inst: Union[str, list] = None,
    conv_temp: Quantity = None,
    conv_outer_radius: Quantity = "r500",
    num_cores: int = NUM_CORES
) -> Tuple[Union[ClusterSample, List], List[Quantity], list, list]:
    """
    An internal function which exists because all the density profile methods that I have planned
    need the same product checking and setup steps. This function checks that all necessary spectra/fits have
    been generated/run, then uses them to calculate the conversion factors from count-rate/volume to squared
    hydrogen number density.

    :param Union[GalaxyCluster, ClusterSample] sources: The source objects/sample object for which the density profile
    is being found.
    :param str/Quantity outer_radius: The name or value of the outer radius of the spectra that should be used
        to calculate conversion factors (for instance 'r200' would be acceptable for a GalaxyCluster, or
        Quantity(1000, 'kpc')). If 'region' is chosen (to use the regions in region files), then any
        inner radius will be ignored.
    :param str/Quantity inner_radius: The name or value of the inner radius of the spectra that should be used
        to calculate conversion factors (for instance 'r500' would be acceptable for a GalaxyCluster, or
        Quantity(300, 'kpc')). By default this is zero arcseconds, resulting in a circular spectrum.
    :param str abund_table: Which abundance table should be used for the XSPEC fit, FakeIt run, and for the
        electron/hydrogen number density ratio.
    :param Quantity lo_en: The lower energy limit of the combined ratemap used to calculate density.
    :param Quantity hi_en: The upper energy limit of the combined ratemap used to calculate density.
    :param bool group_spec: Whether the spectra that were fitted for the desired result were grouped.
    :param float min_counts: The minimum counts per channel, if the spectra that were fitted for the
        desired result were grouped by minimum counts.
    :param float min_sn: The minimum signal to noise per channel, if the spectra that were fitted for the
        desired result were grouped by minimum signal to noise.
    :param float over_sample: The level of oversampling applied on the spectra that were fitted.
    :param str/list obs_id: A specific ObsID(s) to measure the density from. This should be a string if a single
        source is being analysed, and a list of ObsIDs the same length as the number of sources otherwise. The
        default is None, in which case the combined data will be used to measure the density profile.
    :param str/list inst: A specific instrument(s) to measure the density from. This can either be passed as a
        single string (e.g. 'pn') if only one source is being analysed, or the same instrument should be used for
        every source in a sample, or a list of strings if different instruments are required for each source. The
        default is None, in which case the combined data will be used to measure the density profile.
    :param Quantity conv_temp: If set this will override XGA measured temperatures within the conv_outer_radius, and
        the fakeit run to calculate the normalisation conversion factor will use these temperatures. The quantity
         should have an entry for each cluster being analysed. Default is None.
    :param str/Quantity conv_outer_radius: The outer radius within which to generate spectra and measure temperatures
        for the conversion factor calculation, default is 'r500'. An astropy quantity may also be passed, with either
        a single value or an entry for each cluster being analysed.
    :param int num_cores: The number of cores that the evselect call and XSPEC functions are allowed to use.
    :return: The source object(s)/sample that was passed in, an array of the calculated conversion factors to take the
        count-rate/volume to a number density of hydrogen, the parsed obs_id variable, and the parsed inst variable.
    :rtype: Tuple[Union[ClusterSample, List], List[Quantity], list, list]
    """
    # If its a single source I shove it in a list so I can just iterate over the sources parameter
    #  like I do when its a Sample object
    if isinstance(sources, BaseSource):
        sources = [sources]

    # Perform some checks on the ObsID and instrument parameters to make sure that they are in the correct
    #  format if they have been set. We don't need to check that the ObsIDs are associated with the sources
    #  here, because that will happen when the ratemaps are retrieved from the source objects.
    if all([obs_id is not None, inst is not None]):
        if isinstance(obs_id, str):
            obs_id = [obs_id]
        if isinstance(inst, str):
            inst = [inst]

        if len(obs_id) != len(sources):
            raise ValueError(
                "If you set the obs_id argument there must be one entry per source being analysed."
            )

        if len(inst) != len(sources) and len(inst) != 1:
            raise ValueError(
                "The value passed for inst must either be a single instrument name, or a list "
                "of instruments the same length as the number of sources being analysed."
            )
    elif all([obs_id is None, inst is None]):
        obs_id = [None] * len(sources)
        inst = [None] * len(sources)
    else:
        raise ValueError(
            "If a value is supplied for obs_id, then a value must be supplied for inst as well, and "
            "vice versa.")

    if not all([type(src) == GalaxyCluster for src in sources]):
        raise TypeError(
            "Only GalaxyCluster sources can be passed to cluster_density_profile."
        )

    # Triggers an exception if the abundance table name passed isn't recognised
    if abund_table not in ABUND_TABLES:
        ab_list = ", ".join(ABUND_TABLES)
        raise ValueError(
            "{0} is not in the accepted list of abundance tables; {1}".format(
                abund_table, ab_list))

    # This check will eventually become obselete, but I haven't yet implemented electron to proton ratios for
    #  all allowed abundance tables - so this just checks whether the chosen table has an ratio associated.
    try:
        e_to_p_ratio = NHC[abund_table]
    except KeyError:
        raise NotImplementedError(
            "That is an acceptable abundance table, but I haven't added the conversion factor "
            "to the dictionary yet")

    if conv_temp is not None and not conv_temp.isscalar and len(
            conv_temp) != len(sources):
        raise ValueError(
            "If multiple there are multiple entries in conv_temp, then there must be the same number"
            " of entries as there are sources being analysed.")
    elif conv_temp is not None:
        temps = conv_temp
    else:
        # Check that the spectra we will be relying on for conversion calculation have been fitted, calling
        #  this function will also make sure that they are generated
        single_temp_apec(sources,
                         conv_outer_radius,
                         inner_radius,
                         abund_table=abund_table,
                         num_cores=num_cores,
                         group_spec=group_spec,
                         min_counts=min_counts,
                         min_sn=min_sn,
                         over_sample=over_sample)

        # Then we need to grab the temperatures and pass them through to the cluster conversion factor
        #  calculator - this may well change as I intend to let cluster_cr_conv grab temperatures for
        #  itself at some point
        temp_temps = []
        for src in sources:
            try:
                # A temporary temperature variable
                temp_temp = src.get_temperature(conv_outer_radius,
                                                "constant*tbabs*apec",
                                                inner_radius, group_spec,
                                                min_counts, min_sn,
                                                over_sample)[0]
            except (ModelNotAssociatedError, ParameterNotAssociatedError):
                warn(
                    "{s}'s temperature fit is not valid, so I am defaulting to a temperature of "
                    "3keV".format(s=src.name))
                temp_temp = Quantity(3, 'keV')
            temp_temps.append(temp_temp.value)
        temps = Quantity(temp_temps, 'keV')

    # This call actually does the fakeit calculation of the conversion factors, then stores them in the
    #  XGA Spectrum objects
    cluster_cr_conv(sources,
                    conv_outer_radius,
                    inner_radius,
                    temps,
                    abund_table=abund_table,
                    num_cores=num_cores,
                    group_spec=group_spec,
                    min_counts=min_counts,
                    min_sn=min_sn,
                    over_sample=over_sample)

    # This where the combined conversion factor that takes a count-rate/volume to a squared number density
    #  of hydrogen
    to_dens_convs = []
    # These are from the distance and redshift, also the normalising 10^-14 (see my paper for
    #  more of an explanation)
    for src_ind, src in enumerate(sources):
        src: GalaxyCluster
        # Both the angular_diameter_distance and redshift are guaranteed to be present here because redshift
        #  is REQUIRED to define GalaxyCluster objects
        factor = ((4 * e_to_p_ratio * np.pi *
                   (src.angular_diameter_distance.to("cm") *
                    (1 + src.redshift))**2) / 10**-14)
        total_factor = factor * src.norm_conv_factor(
            conv_outer_radius, lo_en, hi_en, inner_radius, group_spec,
            min_counts, min_sn, over_sample, obs_id[src_ind], inst[src_ind])
        to_dens_convs.append(total_factor)

    return sources, to_dens_convs, obs_id, inst
Пример #51
0
# Licensed under a 3-clause BSD style license - see LICENSE.rst
from numpy.testing import assert_allclose
from astropy.units import Quantity
from astropy.table import Table
from ....utils.testing import assert_quantity_allclose
from ...source import Pulsar, SimplePulsar

pulsar = Pulsar()
time = Quantity([1e2, 1e4, 1e6, 1e8], "yr")


def get_atnf_catalog_sample():
    data = """
    NUM   NAME            Gl          Gb       P0        P1        AGE        BSURF     EDOT
    1     J0006+1834      108.172    -42.985   0.693748  2.10e-15  5.24e+06   1.22e+12  2.48e+32
    2     J0007+7303      119.660     10.463   0.315873  3.60e-13  1.39e+04   1.08e+13  4.51e+35
    3     B0011+47        116.497    -14.631   1.240699  5.64e-16  3.48e+07   8.47e+11  1.17e+31
    7     B0021-72E       305.883    -44.883   0.003536  9.85e-20  5.69e+08   5.97e+08  8.79e+34
    8     B0021-72F       305.899    -44.892   0.002624  6.45e-20  6.44e+08   4.16e+08  1.41e+35
    16    J0024-7204O     305.897    -44.889   0.002643  3.04e-20  1.38e+09   2.87e+08  6.49e+34
    18    J0024-7204Q     305.877    -44.899   0.004033  3.40e-20  1.88e+09   3.75e+08  2.05e+34
    21    J0024-7204T     305.890    -44.894   0.007588  2.94e-19  4.09e+08   1.51e+09  2.65e+34
    22    J0024-7204U     305.890    -44.905   0.004343  9.52e-20  7.23e+08   6.51e+08  4.59e+34
    28    J0026+6320      120.176      0.593   0.318358  1.50e-16  3.36e+07   2.21e+11  1.84e+32
    """
    return Table.read(data, format="ascii")


def test_SimplePulsar_atnf():
    """Test functions against ATNF pulsar catalog values"""
    atnf = get_atnf_catalog_sample()
Пример #52
0
def _onion_peel_data(sources: Union[GalaxyCluster, ClusterSample],
                     outer_radius: Union[str, Quantity] = "r500",
                     num_dens: bool = True,
                     use_peak: bool = True,
                     pix_step: int = 1,
                     min_snr: Union[int, float] = 0.0,
                     abund_table: str = "angr",
                     lo_en: Quantity = Quantity(0.5, 'keV'),
                     hi_en: Quantity = Quantity(2.0, 'keV'),
                     psf_corr: bool = True,
                     psf_model: str = "ELLBETA",
                     psf_bins: int = 4,
                     psf_algo: str = "rl",
                     psf_iter: int = 15,
                     num_samples: int = 10000,
                     group_spec: bool = True,
                     min_counts: int = 5,
                     min_sn: float = None,
                     over_sample: float = None,
                     obs_id: Union[str, list] = None,
                     inst: Union[str, list] = None,
                     conv_temp: Quantity = None,
                     conv_outer_radius: Quantity = "r500",
                     num_cores: int = NUM_CORES):

    raise NotImplementedError("This isn't finished at the moment")
    # Run the setup function, calculates the factors that translate 3D countrate to density
    #  Also checks parameters and runs any spectra/fits that need running
    sources, conv_factors, obs_id, inst = _dens_setup(
        sources, outer_radius, Quantity(0, 'arcsec'), abund_table, lo_en,
        hi_en, group_spec, min_counts, min_sn, over_sample, obs_id, inst,
        conv_temp, conv_outer_radius, num_cores)

    # Calls the handy spectrum region setup function to make a predictable set of outer radius values
    out_rads = region_setup(sources, outer_radius, Quantity(0, 'arcsec'),
                            False, '')[-1]

    final_dens_profs = []
    # I need the ratio of electrons to protons here as well, so just fetch that for the current abundance table
    e_to_p_ratio = NHC[abund_table]
    with tqdm(desc="Generating density profiles based on onion-peeled data",
              total=len(sources)) as dens_onwards:
        for src_ind, src in enumerate(sources):
            sb_prof = _run_sb(src, out_rads[src_ind], use_peak, lo_en, hi_en,
                              psf_corr, psf_model, psf_bins, psf_algo,
                              psf_iter, pix_step, min_snr, obs_id[src_ind],
                              inst[src_ind])
            if sb_prof is None:
                final_dens_profs.append(None)
                continue
            else:
                src.update_products(sb_prof)

            rad_bounds = sb_prof.annulus_bounds.to("cm")
            vol_intersects = shell_ann_vol_intersect(rad_bounds, rad_bounds)
            print(vol_intersects.min())
            plt.imshow(vol_intersects.value)
            plt.show()
            # Generating random normalisation profile realisations from DATA
            sb_reals = sb_prof.generate_data_realisations(
                num_samples) * sb_prof.areas

            # Using a loop here is ugly and relatively slow, but it should be okay
            transformed = []
            for i in range(0, num_samples):
                transformed.append(
                    np.linalg.inv(vol_intersects.T) @ sb_reals[i, :])

            transformed = Quantity(transformed)
            print(np.percentile(transformed, 50, axis=0))
            import sys
            sys.exit()

            # We convert the volume element to cm^3 now, this is the unit we expect for the density conversion
            transformed = transformed.to('ct/(s*cm^3)')
            num_dens_dist = np.sqrt(
                transformed * conv_factors[src_ind]) * (1 + e_to_p_ratio)

            print(
                np.where(np.isnan(np.sqrt(transformed *
                                          conv_factors[src_ind])))[0].shape)
            import sys
            sys.exit()

            med_num_dens = np.percentile(num_dens_dist, 50, axis=1)
            num_dens_err = np.std(num_dens_dist, axis=1)

            # Setting up the instrument and ObsID to pass into the density profile definition
            if obs_id[src_ind] is None:
                cur_inst = "combined"
                cur_obs = "combined"
            else:
                cur_inst = inst[src_ind]
                cur_obs = obs_id[src_ind]

            dens_rads = sb_prof.radii.copy()
            dens_rads_errs = sb_prof.radii_err.copy()
            dens_deg_rads = sb_prof.deg_radii.copy()
            print(np.where(np.isnan(transformed)))
            print(med_num_dens)
            try:
                # I now allow the user to decide if they want to generate number or mass density profiles using
                #  this function, and here is where that distinction is made
                if num_dens:
                    dens_prof = GasDensity3D(dens_rads.to("kpc"),
                                             med_num_dens,
                                             sb_prof.centre,
                                             src.name,
                                             cur_obs,
                                             cur_inst,
                                             'onion',
                                             sb_prof,
                                             dens_rads_errs,
                                             num_dens_err,
                                             deg_radii=dens_deg_rads)
                else:
                    # The mean molecular weight multiplied by the proton mass
                    conv_mass = MEAN_MOL_WEIGHT * m_p
                    dens_prof = GasDensity3D(
                        dens_rads.to("kpc"),
                        (med_num_dens * conv_mass).to('Msun/Mpc^3'),
                        sb_prof.centre,
                        src.name,
                        cur_obs,
                        cur_inst,
                        'onion',
                        sb_prof,
                        dens_rads_errs,
                        (num_dens_err * conv_mass).to('Msun/Mpc^3'),
                        deg_radii=dens_deg_rads)

                src.update_products(dens_prof)
                final_dens_profs.append(dens_prof)

            # If, for some reason, there are some inf/NaN values in any of the quantities passed to the GasDensity3D
            #  declaration, this is where an error will be thrown
            except ValueError:
                final_dens_profs.append(None)
                warn(
                    "One or more of the quantities passed to the init of {}'s density profile has a NaN or Inf value"
                    " in it.".format(src.name))

            dens_onwards.update(1)

    return final_dens_profs
Пример #53
0
def _power_law(E, N, k):
    E = Quantity(E, "TeV")
    E0 = Quantity(1, "TeV")
    N = Quantity(N, "m^-2 s^-1 TeV^-1 sr^-1")
    flux = N * (E / E0)**(-k)
    return flux
Пример #54
0
 def __init__(self, nodes, name='Default', interpolation_mode='linear'):
     # Need this for subclassing (see BinnedDataAxis)
     if nodes is not None:
         self._data = Quantity(nodes)
     self.name = name
     self._interpolation_mode = interpolation_mode
Пример #55
0
 def time_stop(self):
     """GTI end times (`~astropy.time.Time`)."""
     met = Quantity(self.table["STOP"].astype("float64"), "second")
     return self.time_ref + met
Пример #56
0
def make_test_eventlist(observation_table,
                        obs_id,
                        sigma=Angle(5., 'deg'),
                        spectral_index=2.7,
                        random_state='random-seed'):
    """
    Make a test event list for a specified observation.

    The observation can be specified with an observation table object
    and the observation ID pointing to the correct observation in the
    table.

    For now, only a very rudimentary event list is generated, containing
    only detector X, Y coordinates (a.k.a. nominal system) and energy
    columns for the events. And the livetime of the observations stored
    in the header.

    The model used to simulate events is also very simple. Only
    dummy background is created (no signal).
    The background is created following a 2D symmetric gaussian
    model for the spatial coordinates (X, Y) and a power-law in
    energy.
    The gaussian width varies in energy from sigma/2 to sigma.
    The number of events generated depends linearly on the livetime
    and on the altitude angle of the observation.
    The model can be tuned via the sigma and spectral_index parameters.

    In addition, an effective area table is produced. For the moment
    only the low energy threshold is filled.

    See also :ref:`datasets_obssim`.

    Parameters
    ----------
    observation_table : `~gammapy.data.ObservationTable`
        Observation table containing the observation to fake.
    obs_id : int
        Observation ID of the observation to fake inside the observation table.
    sigma : `~astropy.coordinates.Angle`, optional
        Width of the gaussian model used for the spatial coordinates.
    spectral_index : float, optional
        Index for the power-law model used for the energy coordinate.
    random_state : {int, 'random-seed', 'global-rng', `~numpy.random.RandomState`}, optional
        Defines random number generator initialisation.
        Passed to `~gammapy.utils.random.get_random_state`.

    Returns
    -------
    event_list : `~gammapy.data.EventList`
        Event list.
    aeff_hdu : `~astropy.io.fits.BinTableHDU`
        Effective area table.
    """
    from ..data import EventList
    random_state = get_random_state(random_state)

    # find obs row in obs table
    obs_ids = observation_table['OBS_ID'].data
    obs_index = np.where(obs_ids == obs_id)
    row = obs_index[0][0]

    # get observation information
    alt = Angle(observation_table['ALT'])[row]
    livetime = Quantity(observation_table['LIVETIME'])[row]

    # number of events to simulate
    # it is linearly dependent on the livetime, taking as reference
    # a trigger rate of 300 Hz
    # it is linearly dependent on the zenith angle (90 deg - altitude)
    # it is n_events_max at alt = 90 deg and n_events_max/2 at alt = 0 deg
    n_events_max = Quantity(300., 'Hz') * livetime
    alt_min = Angle(0., 'deg')
    alt_max = Angle(90., 'deg')
    slope = (n_events_max - n_events_max / 2) / (alt_max - alt_min)
    free_term = n_events_max / 2 - slope * alt_min
    n_events = alt * slope + free_term

    # simulate energy
    # the index of `~numpy.random.RandomState.power` has to be
    # positive defined, so it is necessary to translate the (0, 1)
    # interval of the random variable to (emax, e_min) in order to
    # have a decreasing power-law
    e_min = Quantity(0.1, 'TeV')
    e_max = Quantity(100., 'TeV')
    energy = sample_powerlaw(e_min.value,
                             e_max.value,
                             spectral_index,
                             size=n_events,
                             random_state=random_state)
    energy = Quantity(energy, 'TeV')

    E_0 = Quantity(1., 'TeV')  # reference energy for the model

    # define E dependent sigma
    # it is defined via a PL, in order to be log-linear
    # it is equal to the parameter sigma at E max
    # and sigma/2. at E min
    sigma_min = sigma / 2.  # at E min
    sigma_max = sigma  # at E max
    s_index = np.log(sigma_max / sigma_min)
    s_index /= np.log(e_max / e_min)
    s_norm = sigma_min * ((e_min / E_0)**-s_index)
    sigma = s_norm * ((energy / E_0)**s_index)

    # simulate detx, dety
    detx = Angle(random_state.normal(loc=0, scale=sigma.deg, size=n_events),
                 'deg')
    dety = Angle(random_state.normal(loc=0, scale=sigma.deg, size=n_events),
                 'deg')

    # fill events in an event list
    event_list = EventList()
    event_list['DETX'] = detx
    event_list['DETY'] = dety
    event_list['ENERGY'] = energy

    # store important info in header
    event_list.meta['LIVETIME'] = livetime.to('second').value
    event_list.meta['EUNIT'] = str(energy.unit)

    # effective area table
    aeff_table = Table()

    # fill threshold, for now, a default 100 GeV will be set
    # independently of observation parameters
    energy_threshold = Quantity(0.1, 'TeV')
    aeff_table.meta['LO_THRES'] = energy_threshold.value
    aeff_table.meta['name'] = 'EFFECTIVE AREA'

    # convert to BinTableHDU and add necessary comment for the units
    aeff_hdu = table_to_fits_table(aeff_table)
    aeff_hdu.header.comments['LO_THRES'] = '[' + str(
        energy_threshold.unit) + ']'

    return event_list, aeff_hdu
Пример #57
0
__all__ = [
    "CaseBattacharya1998",
    "FaucherKaspi2006",
    "Lorimer2006",
    "Paczynski1990",
    "YusifovKucuk2004",
    "YusifovKucuk2004B",
    "Exponential",
    "LogSpiral",
    "FaucherSpiral",
    "ValleeSpiral",
    "radial_distributions",
]

# Simulation range used for random number drawing
RMIN, RMAX = Quantity([0, 20], "kpc")
ZMIN, ZMAX = Quantity([-0.5, 0.5], "kpc")


class Paczynski1990(Fittable1DModel):
    """Radial distribution of the birth surface density of neutron stars - Paczynski 1990.

    .. math ::
        f(r) = A r_{exp}^{-2} \\exp \\left(-\\frac{r}{r_{exp}} \\right)

    Reference: http://adsabs.harvard.edu/abs/1990ApJ...348..485P (Formula (2))

    Parameters
    ----------
    amplitude : float
        See formula
Пример #58
0
class FaucherSpiral(LogSpiral):
    """Milky way spiral arm used in Faucher et al (2006).

    Reference: http://adsabs.harvard.edu/abs/2006ApJ...643..332F
    """

    # Parameters
    k = Quantity([4.25, 4.25, 4.89, 4.89], "rad")
    r_0 = Quantity([3.48, 3.48, 4.9, 4.9], "kpc")
    theta_0 = Quantity([1.57, 4.71, 4.09, 0.95], "rad")
    spiralarms = np.array(
        ["Norma", "Carina Sagittarius", "Perseus", "Crux Scutum"])

    def _blur(self, radius, theta, amount=0.07, random_state="random-seed"):
        """Blur the positions around the centroid of the spiralarm.

        The given positions are blurred by drawing a displacement in radius from
        a normal distribution, with sigma = amount * radius. And a direction
        theta from a uniform distribution in the interval [0, 2 * pi].

        Parameters
        ----------
        radius : `~astropy.units.Quantity`
            Radius coordinate
        theta : `~astropy.units.Quantity`
            Angle coordinate
        amount: float, optional
            Amount of blurring of the position, given as a fraction of `radius`.
        random_state : {int, 'random-seed', 'global-rng', `~numpy.random.RandomState`}
            Defines random number generator initialisation.
            Passed to `~gammapy.utils.random.get_random_state`.
        """
        random_state = get_random_state(random_state)

        dr = Quantity(
            abs(random_state.normal(0, amount * radius, radius.size)), "kpc")
        dtheta = Quantity(random_state.uniform(0, 2 * np.pi, radius.size),
                          "rad")
        x, y = cartesian(radius, theta)
        dx, dy = cartesian(dr, dtheta)
        return polar(x + dx, y + dy)

    def _gc_correction(self,
                       radius,
                       theta,
                       r_corr=Quantity(2.857, "kpc"),
                       random_state="random-seed"):
        """Correction of source distribution towards the galactic center.

        To avoid spiralarm features near the Galactic Center, the position angle theta
        is blurred by a certain amount towards the GC.

        Parameters
        ----------
        radius : `~astropy.units.Quantity`
            Radius coordinate
        theta : `~astropy.units.Quantity`
            Angle coordinate
        r_corr : `~astropy.units.Quantity`, optional
            Scale of the correction towards the GC
        random_state : {int, 'random-seed', 'global-rng', `~numpy.random.RandomState`}
            Defines random number generator initialisation.
            Passed to `~gammapy.utils.random.get_random_state`.
        """
        random_state = get_random_state(random_state)

        theta_corr = Quantity(random_state.uniform(0, 2 * np.pi, radius.size),
                              "rad")
        return radius, theta + theta_corr * np.exp(-radius / r_corr)

    def __call__(self, radius, blur=True, random_state="random-seed"):
        """Draw random position from spiral arm distribution.

        Returns the corresponding angle theta[rad] to a given radius[kpc] and number of spiralarm.
        Possible numbers are:

        * Norma = 0,
        * Carina Sagittarius = 1,
        * Perseus = 2
        * Crux Scutum = 3.

        Parameters
        ----------
        random_state : {int, 'random-seed', 'global-rng', `~numpy.random.RandomState`}
            Defines random number generator initialisation.
            Passed to `~gammapy.utils.random.get_random_state`.

        Returns
        -------
        Returns dx and dy, if blurring= true.
        """
        random_state = get_random_state(random_state)

        # Choose spiral arm
        N = random_state.randint(0, 4, radius.size)
        theta = self.k[N] * np.log(radius / self.r_0[N]) + self.theta_0[N]
        spiralarm = self.spiralarms[N]

        if blur:  # Apply blurring model according to Faucher
            radius, theta = self._blur(radius,
                                       theta,
                                       random_state=random_state)
            radius, theta = self._gc_correction(radius,
                                                theta,
                                                random_state=random_state)
        return radius, theta, spiralarm
Пример #59
0
def make_test_eventlist(
    observation_table, obs_id, sigma=Angle(5.0, "deg"), spectral_index=2.7, random_state="random-seed"
):
    """
    Make a test event list for a specified observation.

    The observation can be specified with an observation table object
    and the observation ID pointing to the correct observation in the
    table.

    For now, only a very rudimentary event list is generated, containing
    only detector X, Y coordinates (a.k.a. nominal system) and energy
    columns for the events. And the livetime of the observations stored
    in the header.

    The model used to simulate events is also very simple. Only
    dummy background is created (no signal).
    The background is created following a 2D symmetric gaussian
    model for the spatial coordinates (X, Y) and a power-law in
    energy.
    The gaussian width varies in energy from sigma/2 to sigma.
    The number of events generated depends linearly on the livetime
    and on the altitude angle of the observation.
    The model can be tuned via the sigma and spectral_index parameters.

    In addition, an effective area table is produced. For the moment
    only the low energy threshold is filled.

    See also :ref:`datasets_obssim`.

    Parameters
    ----------
    observation_table : `~gammapy.data.ObservationTable`
        Observation table containing the observation to fake.
    obs_id : int
        Observation ID of the observation to fake inside the observation table.
    sigma : `~astropy.coordinates.Angle`, optional
        Width of the gaussian model used for the spatial coordinates.
    spectral_index : double, optional
        Index for the power-law model used for the energy coordinate.
    random_state : {int, 'random-seed', 'global-rng', `~numpy.random.RandomState`}, optional
        Defines random number generator initialisation.
        Passed to `~gammapy.utils.random.get_random_state`.

    Returns
    -------
    event_list : `~gammapy.data.EventList`
        Event list.
    aeff_hdu : `~astropy.io.fits.BinTableHDU`
        Effective area table.
    """
    from ..data import EventList

    random_state = get_random_state(random_state)

    # find obs row in obs table
    obs_ids = observation_table["OBS_ID"].data
    obs_index = np.where(obs_ids == obs_id)
    row = obs_index[0][0]

    # get observation information
    alt = Angle(observation_table["ALT"])[row]
    livetime = Quantity(observation_table["LIVETIME"])[row]

    # number of events to simulate
    # it is linearly dependent on the livetime, taking as reference
    # a trigger rate of 300 Hz
    # it is linearly dependent on the zenith angle (90 deg - altitude)
    # it is n_events_max at alt = 90 deg and n_events_max/2 at alt = 0 deg
    n_events_max = Quantity(300.0, "Hz") * livetime
    alt_min = Angle(0.0, "deg")
    alt_max = Angle(90.0, "deg")
    slope = (n_events_max - n_events_max / 2) / (alt_max - alt_min)
    free_term = n_events_max / 2 - slope * alt_min
    n_events = alt * slope + free_term

    # simulate energy
    # the index of `~numpy.random.RandomState.power` has to be
    # positive defined, so it is necessary to translate the (0, 1)
    # interval of the random variable to (emax, e_min) in order to
    # have a decreasing power-law
    e_min = Quantity(0.1, "TeV")
    e_max = Quantity(100.0, "TeV")
    energy = sample_powerlaw(e_min.value, e_max.value, spectral_index, size=n_events, random_state=random_state)
    energy = Quantity(energy, "TeV")

    E_0 = Quantity(1.0, "TeV")  # reference energy for the model

    # define E dependent sigma
    # it is defined via a PL, in order to be log-linear
    # it is equal to the parameter sigma at E max
    # and sigma/2. at E min
    sigma_min = sigma / 2.0  # at E min
    sigma_max = sigma  # at E max
    s_index = np.log(sigma_max / sigma_min)
    s_index /= np.log(e_max / e_min)
    s_norm = sigma_min * ((e_min / E_0) ** -s_index)
    sigma = s_norm * ((energy / E_0) ** s_index)

    # simulate detx, dety
    detx = Angle(random_state.normal(loc=0, scale=sigma.deg, size=n_events), "deg")
    dety = Angle(random_state.normal(loc=0, scale=sigma.deg, size=n_events), "deg")

    # fill events in an event list
    event_list = EventList()
    event_list["DETX"] = detx
    event_list["DETY"] = dety
    event_list["ENERGY"] = energy

    # store important info in header
    event_list.meta["LIVETIME"] = livetime.to("second").value
    event_list.meta["EUNIT"] = str(energy.unit)

    # effective area table
    aeff_table = Table()

    # fill threshold, for now, a default 100 GeV will be set
    # independently of observation parameters
    energy_threshold = Quantity(0.1, "TeV")
    aeff_table.meta["LO_THRES"] = energy_threshold.value
    aeff_table.meta["name"] = "EFFECTIVE AREA"

    # convert to BinTableHDU and add necessary comment for the units
    aeff_hdu = table_to_fits_table(aeff_table)
    aeff_hdu.header.comments["LO_THRES"] = "[" + str(energy_threshold.unit) + "]"

    return event_list, aeff_hdu
Пример #60
0
def image_profile(profile_axis,
                  image,
                  lats,
                  lons,
                  binsz,
                  counts=None,
                  mask=None,
                  errors=False,
                  standard_error=0.1):
    """Creates a latitude or longitude profile from input flux image HDU.

    Parameters
    ----------
    profile_axis : String, {'lat', 'lon'}
        Specified whether galactic latitude ('lat') or longitude ('lon')
        profile is to be returned.
    image : `~astropy.io.fits.ImageHDU`
        Image HDU object to produce GLAT or GLON profile.
    lats : array_like
        Specified as [GLAT_min, GLAT_max], with GLAT_min and GLAT_max
        as floats. A 1x2 array specifying the maximum and minimum latitudes to
        include in the region of the image for which the profile is formed, 
        which should be within the spatial bounds of the image.
    lons : array_like
        Specified as [GLON_min, GLON_max], with GLON_min and GLON_max
        as floats. A 1x2 array specifying the maximum and minimum longitudes to
        include in the region of the image for which the profile is formed, 
        which should be within the spatial bounds of the image.
    binsz : float
        Latitude bin size of the resulting latitude profile. This should be
        no less than 5 times the pixel resolution.
    counts : `~astropy.io.fits.ImageHDU`
        Counts image to allow Poisson errors to be calculated. If not provided,
        a standard_error should be provided, or zero errors will be returned.
        (Optional).
    mask : array_like
        2D mask array, matching spatial dimensions of input image. (Optional).
        A mask value of True indicates a value that should be ignored, while a
        mask value of False indicates a valid value.
    errors : bool
        If True, computes errors, if possible, according to provided inputs.
        If False (default), returns all errors as zero.
    standard_error : float
        If counts image is not provided, but error values required, this
        specifies a standard fractional error to be applied to values.
        Default = 0.1.

    Returns
    -------
    table : `~astropy.table.Table`
        Galactic latitude or longitude profile as table, with latitude bin
        boundaries, profile values and errors.
    """

    lon, lat = coordinates(image)
    mask_init = (lats[0] <= lat) & (lat < lats[1])
    mask_bounds = mask_init & (lons[0] <= lon) & (lon < lons[1])
    if mask != None:
        mask = mask_bounds & mask
    else:
        mask = mask_bounds

    # Need to preserve shape here so use multiply
    cut_image = image.data * mask
    if counts != None:
        cut_counts = counts.data * mask
    values = []
    count_vals = []

    if profile_axis == 'lat':

        bins = np.arange((lats[1] - lats[0]) / binsz)
        glats_min = lats[0] + bins[:-1] * binsz
        glats_max = lats[0] + bins[1:] * binsz

        # Table is required here to avoid rounding problems with floats
        bounds = Table([glats_min, glats_max], names=('GLAT_MIN', 'GLAT_MAX'))
        for bin in bins[:-1]:
            lat_mask = (bounds['GLAT_MIN'][bin] <=
                        lat) & (lat < bounds['GLAT_MAX'][bin])
            lat_band = cut_image[lat_mask]
            values.append(lat_band.sum())
            if counts != None:
                count_band = cut_counts[lat_mask]
                count_vals.append(count_band.sum())
            else:
                count_vals.append(0)

    elif profile_axis == 'lon':

        bins = np.arange((lons[1] - lons[0]) / binsz)
        glons_min = lons[0] + bins[:-1] * binsz
        glons_max = lons[0] + bins[1:] * binsz

        # Table is required here to avoid rounding problems with floats
        bounds = Table([glons_min, glons_max], names=('GLON_MIN', 'GLON_MAX'))
        for bin in bins[:-1]:
            lon_mask = (bounds['GLON_MIN'][bin] <=
                        lon) & (lon < bounds['GLON_MAX'][bin])
            lon_band = cut_image[lon_mask]
            values.append(lon_band.sum())
            if counts != None:
                count_band = cut_counts[lon_mask]
                count_vals.append(count_band.sum())
            else:
                count_vals.append(0)

    if errors == True:
        if counts != None:
            rel_errors = 1. / np.sqrt(count_vals)
            error_vals = values * rel_errors
        else:
            error_vals = values * (np.ones(len(values)) * standard_error)
    else:
        error_vals = np.zeros_like(values)

    if profile_axis == 'lat':
        table = Table([
            Quantity(glats_min, 'deg'),
            Quantity(glats_max, 'deg'), values, error_vals
        ],
                      names=('GLAT_MIN', 'GLAT_MAX', 'BIN_VALUE', 'BIN_ERR'))

    elif profile_axis == 'lon':
        table = Table([
            Quantity(glons_min, 'deg'),
            Quantity(glons_max, 'deg'), values, error_vals
        ],
                      names=('GLON_MIN', 'GLON_MAX', 'BIN_VALUE', 'BIN_ERR'))

    return table