Spectrum Fitting

In this first basic example we will perform a basic spectral analysis of the Crab nebula using the public H.E.S.S. data release (already included in gammapy).

Let’s get started by importing all the relevant stuff.

[1]:
import astropy.units as u
from astropy.coordinates import Angle, SkyCoord

from astromodels.core.model import Model
from astromodels.core.units import get_units
from astromodels.functions import Log_parabola, Log_uniform_prior, Uniform_prior
from astromodels.sources import PointSource

from gammapy.data import DataStore
from gammapy.datasets import Datasets, SpectrumDataset
from gammapy.makers import (
    ReflectedRegionsBackgroundMaker,
    SafeMaskMaker,
    SpectrumDatasetMaker,
)
from gammapy.maps import MapAxis, RegionGeom, WcsGeom
from gammapy.modeling import Fit
from gammapy.modeling.models import LogParabolaSpectralModel, SkyModel

from regions import CircleSkyRegion

from threeML import BayesianAnalysis
from threeML.data_list import DataList

from gammapy_plugin.converter import AstromodelConverter
from gammapy_plugin.gammapy_like import GammapyLike
from gammapy_plugin.test.utils import get_close

Let’s start by laoding the relevant data

[2]:
datastore = DataStore.from_dir("$GAMMAPY_DATA/hess-dl3-dr1/")
obs_ids = [23523, 23526, 23559, 23592]
observations = datastore.get_observations(obs_ids)

We perform a standard gammapy-workflow of creating a dataset

  • setting as target

  • excluding the target region

  • creating our energy axis

[3]:
target_position = SkyCoord(ra=83.63, dec=22.01, unit="deg", frame="icrs")

on_region_radius = Angle("0.11 deg")
on_region = CircleSkyRegion(center=target_position.galactic, radius=on_region_radius)
exclusion_region = CircleSkyRegion(
    center=SkyCoord(183.604, -8.708, unit="deg", frame="galactic"),
    radius=0.5 * u.deg,
)

skydir = target_position.galactic
geom = WcsGeom.create(
    npix=(250, 250), binsz=0.02, skydir=skydir, proj="TAN", frame="galactic"
)

exclusion_mask = ~geom.region_mask([exclusion_region])
energy_axis = MapAxis.from_energy_bounds(
    0.5, 40, nbin=10, per_decade=True, unit="TeV", name="energy"
)
energy_axis_true = MapAxis.from_energy_bounds(
    0.1, 100, nbin=20, per_decade=True, unit="TeV", name="energy_true"
)

geom = RegionGeom.create(region=on_region, axes=[energy_axis])
dataset_empty = SpectrumDataset.create(geom=geom, energy_axis_true=energy_axis_true)

We now have everything to create our makers and run them. In this example we use a ReflectedRegionsBackgroundMaker

[4]:
dataset_maker = SpectrumDatasetMaker(
    containment_correction=True, selection=["counts", "exposure", "edisp"]
)
bkg_maker = ReflectedRegionsBackgroundMaker(exclusion_mask=exclusion_mask)

safe_mask_maker = SafeMaskMaker(methods=["aeff-max"], aeff_percent=10)
datasets = Datasets()

for obs_id, observation in zip(obs_ids, observations):
    dataset = dataset_maker.run(dataset_empty.copy(name=str(obs_id)), observation)
    dataset_on_off = bkg_maker.run(dataset, observation)
    dataset_on_off = safe_mask_maker.run(dataset_on_off, observation)
    datasets.append(dataset_on_off)
datasets_copy = datasets.copy()

Let’s continue with the threeML steps: First lets choose and set up a model - we first initalize the spectral shape and assign it to the PointSource so that the units are already set in astromodels.

[5]:
logp = Log_parabola()
ps = PointSource(
    source_name="crab",
    ra=target_position.ra.deg,
    dec=target_position.dec.deg,
    spectral_shape=logp,
)
logp.K.prior = Log_uniform_prior(lower_bound=1e-22, upper_bound=1e-18)  # this is in keV
logp.K = 1e-11 * u.Unit("TeV-1 cm-2 s-1")
logp.piv = 1e9  # this is in keV
logp.piv.free = False
logp.alpha.prior = Uniform_prior(lower_bound=-3.5, upper_bound=-0.5)
logp.alpha = -2
logp.beta.prior = Uniform_prior(lower_bound=-0.2, upper_bound=2)
logp.beta = 1

model = Model(ps)

Let’s take a look at it:

[6]:
model
[6]:
Model summary:

N
Point sources 1
Extended sources 0
Particle sources 0


Free parameters (3):

value min_value max_value unit
crab.spectrum.main.Log_parabola.K 0.0 0.0 100000.0 keV-1 s-1 cm-2
crab.spectrum.main.Log_parabola.alpha -2.0 None None
crab.spectrum.main.Log_parabola.beta 1.0 None None


Fixed parameters (3):
(abridged. Use complete=True to see all fixed parameters)


Properties (0):

(none)


Linked parameters (0):

(none)

Independent variables:

(none)

Linked functions (0):

(none)

Perfect we have one PointSource with a Log_parabola spectrum. Please be aware of the different defintions of a Logparabolas in astromodels and gammapy.

Now we convert this model. Alternatively we can skip that part and let the GammapyLike deal with it by not supplying the converted_model in the set_model() method.

[7]:
conv = AstromodelConverter(model)
gl = GammapyLike("hess", sources="crab")
gl.set_datasets(datasets)
gl.set_model(model, converted_model=conv)

Nice :) We now initialize an BayesianAnalysis that links the model to the data and handles the sampling. We use ultranest as a sampler with the default arguments.

[8]:
ba = BayesianAnalysis(model, DataList(gl))
ba.set_sampler("ultranest")
ba.sampler.setup()
ba.sample()
res = ba.results
res
[ultranest] Sampling 400 live points from prior ...
[ultranest] Explored until L=-4e+01
[ultranest] Likelihood function evaluations: 10358
[ultranest]   logZ = -53.99 +- 0.1065
[ultranest] Effective samples strategy satisfied (ESS = 1915.5, need >400)
[ultranest] Posterior uncertainty strategy is satisfied (KL: 0.45+-0.08 nat, need <0.50 nat)
[ultranest] Evidency uncertainty strategy is satisfied (dlogz=0.11, need <0.5)
[ultranest]   logZ error budget: single: 0.14 bs:0.11 tail:0.01 total:0.11 required:<0.50
[ultranest] done iterating.
Maximum a posteriori probability (MAP) point:

result unit
parameter
crab.spectrum.main.Log_parabola.K (4.36 -0.23 +0.22) x 10^-20 1 / (keV s cm2)
crab.spectrum.main.Log_parabola.alpha -2.38 -0.13 +0.15
crab.spectrum.main.Log_parabola.beta (1.6 -0.7 +1.0) x 10^-1
Values of -log(posterior) at the minimum:

-log(posterior)
hess 0.161442
total 0.161442
Values of statistical measures:

statistical measures
AIC 5.992905
BIC 12.823195
DIC 5.734141
PDIC 3.008551
log(Z) -23.453740
[8]:
<threeML.analysis_results.BayesianResults at 0x7f28811fb8c0>

Perfect! It seems like it succeeded - let’s take a look at the parameter distributions:

[9]:
_ = res.corner_plot()

Since threeML v2.6.0 we can also use display_spectrum_model_counts to see the modeled and observed count rate and the residuals. The units might be a bit unusual for IACT astronomers/astrophysicists ;)

[10]:
from threeML.io.plotting.post_process_data_plots import display_spectrum_model_counts

fig = display_spectrum_model_counts(ba)
fig
[10]:
<Figure size 640x480 with 2 Axes>