Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Gamera spectral model example #28

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions recipes/fitting-with-gamera/env.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Declare your specific environment
#Gamera is not pip installable therefore it is not listed here

name: gp-gamera-fit

channels:
- conda-forge

dependencies:
- gammapy=1.2
- python=3.9
- numpy=1.24
- jupyter
- matplotlib
322 changes: 322 additions & 0 deletions recipes/fitting-with-gamera/fitting_with_gamera.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Fitting a physical model using Gamera\n",
"\n",
"## Context\n",
"\n",
"[Gamera](http://libgamera.github.io/GAMERA/docs/documentation.html) is a source modelling code for gamma ray astronomy that allows for the calculation of gamma-ray spectra from underlying populations of leptonic and hadronic cosmic rays. Fitting the resulting spectra to data would directly allow to constrain the parameters of these underlying distributions.\n",
"\n",
"## Proposed approach\n",
"\n",
"\n",
"Here, we will demonstrate the two possible ways in which Gamera can be used in combination with gammapy to fit the spectra of gamma-ray sources. As an example, we will use the scenario of a leptonic source producing gamma rays through Inverse Compton scattering.\n",
"\n",
"The two possible approaches are\n",
" \n",
"* Subclassing `~gammapy.modeling.models.SpectralModel` and creating a `GameraSpectralModel`. This is probbaly the most elegant way to combine Gamera and gammapy, but can also be slow because it requires the repeated execution of Gamera for every model evaluation\n",
"* Creating a `~gammapy.modeling.models.TemplateNDSpectralModel` from Gamera spectra produced on a grid of model parameters. While less elegant than the first approach, it can be significantly faster because no Gamera evaluations are required during the fit.\n",
"\n",
"For more modelling options with Gamera, see the [documentation](http://libgamera.github.io/GAMERA/docs/documentation.html).\n",
"Also note that gamera is not pip installable. See installation instructions [here](http://libgamera.github.io/GAMERA/docs/download_installation.html)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"As always, we start the notebook with some setup and imports. The environment variable `GAMERA_LIB_PATH` should point to the direcotry containing the installed Gamera library."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"import os\n",
"sys.path.append(os.environ[\"GAMERA_LIB_PATH\"])\n",
"import gappa as gp"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import astropy.units as u\n",
"from astropy.coordinates import SkyCoord,Angle\n",
"import itertools\n",
"from regions import CircleSkyRegion"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from gammapy.maps import MapAxis,RegionNDMap,RegionGeom\n",
"from gammapy.modeling.models import (\n",
" SpectralModel,\n",
" SPECTRAL_MODEL_REGISTRY,\n",
" TemplateNDSpectralModel\n",
")\n",
"from gammapy.modeling import Parameter "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Approach 1: GameraSpectralModel\n",
"\n",
"Below is the implementation of the `GameraSpectralModel` class. It is a subclass of `SpectralModel`. With every evaluation, it evolves a population of electrons in a time-independent environment (i.e. fixed magnetic field and radiation field), takes the output electron spectrum and calculates the gamma-ray spectrum from this.\n",
"\n",
"As the radiation field, we use the model from [Popescu et al. 2017](https://doi.org/10.1093/mnras/stx1282) together with the CMB spectrum, in this case evaluated at the position of the crab nebula. The data of the model can also be found [here](http://cdsarc.u-strasbg.fr/viz-bin/Cat?J/MNRAS/470/2539#/browse). Any other model or radiation spectrum is also possible. It is read here from a txt file with the energies and energy densities of the radiation.\n",
"\n",
"The model has a number of parameters related to the modelled source. Many of these should not be fit, but just be set at the model instantiation and frozen. \n",
"\n",
"Also, Gamera does not work with astropy units. Therefore, the input quantities to the `GameraSpectralModel` need to be internally stripped of their units for the Gamera calculations. The units are then reattached to the output.\n",
"\n",
"For more details about the Gamera code and methods, see [here](http://libgamera.github.io/GAMERA/docs/time_independent_modeling.html).\n",
"\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class GameraSpectralModel(SpectralModel):\n",
" \"\"\"Spectral model from GAMERA synchrotron and IC emission.\n",
" A power law with cutoff is assumed for the electron spectrum.\n",
" To limit the span of the parameters space, we fit the log10 of the parameters\n",
" whose range is expected to cover several orders of magnitudes.\n",
"\n",
" Some of the parameters (like the distance) should not be fit, but are there just to evaluate the model.\n",
" \"\"\"\n",
"\n",
" tag = [\"GAMERA\", \"g\"]\n",
" # parameters that we actually might want to fit\n",
" index = Parameter(\"index\", 2.5, min=1.5, max=4.0) #it's positive: E^-index\n",
" log10_effic = Parameter(\"log10_effic\", -2, min=-8, max=0)\n",
" log10_E_min = Parameter(\"log10_E_min\", -5, min=-9, max=0,frozen=True) # in TeV\n",
" log10_E_cut = Parameter(\"log10_E_cut\", 4, min=0, max=6,frozen=True) # in TeV\n",
" log10_B = Parameter(\"log10_B\", -5, min=-8, max=-2,frozen=True) #in Gauss\n",
"\n",
" # parameters that we should not fit but just provide\n",
" L_tot = Parameter(\"L_tot\", \"1e43 erg s-1\", min=1e30, max=1e50, frozen=True) # in ergs/s\n",
" d = Parameter(\"d\", \"2 kpc\", min=1, max=10, frozen=True) # in kpc\n",
" age = Parameter(\"age\", \"3e4 yr\", min=1e3, max=1e6, frozen=True)\n",
"\n",
"\n",
" @staticmethod\n",
" def evaluate(\n",
" energy,\n",
" log10_B,\n",
" index,\n",
" log10_effic,\n",
" log10_E_min,\n",
" log10_E_cut,\n",
" L_tot,\n",
" d,\n",
" age,\n",
" ):\n",
" # conversions\n",
" B = (10 ** log10_B) * u.G\n",
" effic = 10 ** log10_effic\n",
" E_min = (10 ** log10_E_min) * u.TeV\n",
" E_cut = (10 ** log10_E_cut) * u.TeV\n",
"\n",
" # Get the relevant GAMERA modules\n",
" fu = gp.Utils()\n",
" fp = gp.Particles()\n",
" fr = gp.Radiation()\n",
" fp.ToggleQuietMode()\n",
" fp.SetSolverMethod(1)\n",
" fr.ToggleQuietMode()\n",
"\n",
" # GAMERA doesn't know about astropy units\n",
" # so we need to get everything in the right unit\n",
" # and as a normal float and not a quantity\n",
" b_field = B.to_value('G')\n",
" t_max = age.to_value('yr')\n",
" distance = d.to_value('pc')\n",
" e_cut = np.log10(E_cut.to_value('erg'))\n",
" e_min = np.log10(E_min.to_value('erg'))\n",
" norm = (effic * L_tot).to_value('erg s-1')\n",
" energy_shape=energy.shape\n",
" photon_energies = energy.flatten().to_value('erg')\n",
" index = index.value\n",
" # Define the electron energy range and the injected spectrum\n",
" # We want to go beyond the cutoff so we use two orders of magnitude more than e_cut\n",
" energy_electrons_edges = np.logspace(e_min, e_cut+2, 100+1) # it's in ergs\n",
"\n",
" padded = False\n",
"\n",
" \n",
" # GAMERA removes the highest and lowest energy bin so we need to pad it so that our choice of E_min is the true one\n",
" step_log = np.diff(np.log10(energy_electrons_edges))[0]\n",
" energy_electrons_edges = np.append(energy_electrons_edges[0]*10**-step_log, energy_electrons_edges)\n",
" energy_electrons_edges = np.append(energy_electrons_edges, energy_electrons_edges[-1]*10**step_log)\n",
" padded = True\n",
"\n",
" if padded:\n",
" range = slice(1,-1, 1)\n",
" else:\n",
" range = slice(None, None, 1)\n",
"\n",
" energy_electrons = MapAxis.from_edges(energy_electrons_edges, interp='log', unit='erg')\n",
" exp_cutoff = np.exp(-(energy_electrons.center.to('erg')/E_cut.to('erg')).value)\n",
" power_law = (energy_electrons.center.to_value('erg') ** -index)*exp_cutoff\n",
"\n",
" # Normalize to total power\n",
" # Note that if we have padded, the range is different\n",
" power_law *= norm / fu.Integrate(list(zip(energy_electrons.center.to_value('erg')[range], power_law[range] * energy_electrons.center.to_value('erg')[range])))\n",
"\n",
" # Bundle together in the way GAMERA wants\n",
" power_law_spectrum = np.array(list(zip(energy_electrons.center.to_value('erg'), power_law)))\n",
"\n",
" #Read the radiation field spectrum, in this case the Popescu et al. 2017 model + CM at the crab nebula position. \n",
" rad_field = np.loadtxt(\"radiation_field.txt\")\n",
"\n",
" RADIATION_FIELD = list(zip(rad_field[:,0], rad_field[:,1]))\n",
"\n",
" # Set everything that is left\n",
" fp.AddArbitraryTargetPhotons(RADIATION_FIELD)\n",
" fp.SetCustomInjectionSpectrum(power_law_spectrum) # the injection rate is constant and given by the normalization of injected PL\n",
" fr.AddArbitraryTargetPhotons(RADIATION_FIELD)\n",
" fr.SetDistance(distance)\n",
" fp.SetBField(b_field)\n",
" fr.SetBField(b_field)\n",
"\n",
"\n",
" #Run the evolution of the electron spectrum and set the output as parent population for radiation calculation\n",
" fp.SetAge(t_max)\n",
" fp.CalculateElectronSpectrum()\n",
" sp = np.array(fp.GetParticleSpectrum())\n",
" fr.SetElectrons(sp)\n",
"\n",
" # compute the gamma-ray spectrum on the points given by the data\n",
" fr.CalculateDifferentialPhotonSpectrum(photon_energies)\n",
" rad = np.array(fr.GetTotalSpectrum()) # this is (dN/dE vs E, units: 1/ erg / cm^2 / s vs TeV)\n",
"\n",
" sed=rad[:,1]\n",
"\n",
" # Get back to the world of units\n",
" sed *= u.erg**-1 * u.cm**-2 *u.s **-1\n",
"\n",
" return sed.to(\"1 / (cm2 eV s)\").reshape(energy_shape)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"SPECTRAL_MODEL_REGISTRY.append(GameraSpectralModel)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"gamera_custom_spectral_model=GameraSpectralModel()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Approach 2: TemplateNDSpectralModel\n",
"\n",
"The other option is to fill a grid of spectra dependent on the parameters to be fit. Here, we demonstrate this using the `GameraSpectralModel` to fill the grid of spectra, with `log10_effic` and `index` as the parameters to be fit. Of course, any other function that returns the spectrum as the function of the parameters to be fit could be used.\n",
"\n",
"As an example we use the crab nebula position as the center of the `CircleSkyRegion` on which the model is based."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def make_template_spectral_model(spectral_model,on_region,n_energies,n_efficiencies,n_index,log10_energy_bounds,log10_eff_bounds,index_bounds):\n",
"\n",
" energy_axis=MapAxis.from_energy_edges(np.logspace(log10_energy_bounds[0],log10_energy_bounds[1],n_energies+1)*u.TeV,name=\"energy_true\")\n",
" efficiency_axis=MapAxis(np.linspace(log10_eff_bounds[0],log10_eff_bounds[1],n_efficiencies+1),name=\"log10_effic\",node_type=\"edges\")\n",
" spectral_index_axis=MapAxis(np.linspace(index_bounds[0],index_bounds[1],n_index+1),name=\"index\",node_type=\"edges\")\n",
"\n",
" region_geom=RegionGeom(on_region,axes=[energy_axis,efficiency_axis,spectral_index_axis])\n",
"\n",
" template_map=np.empty((n_efficiencies,n_index,n_energies))\n",
"\n",
" for (i, j) in itertools.product(range(n_efficiencies),range(n_index)):\n",
" spectral_model.parameters[\"log10_effic\"].value=efficiency_axis.center[i]\n",
" spectral_model.parameters[\"index\"].value=spectral_index_axis.center[j]\n",
" template_map[j,i]=spectral_model(energy_axis.center)\n",
"\n",
" template_region_map=RegionNDMap(region_geom,data=template_map,unit=u.eV**-1*u.cm**-2*u.s**-1)\n",
"\n",
" template_spectral_model=TemplateNDSpectralModel(template_region_map)\n",
"\n",
" return template_spectral_model\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"target_position = SkyCoord(ra=83.63, dec=22.01, unit=\"deg\", frame=\"icrs\")\n",
"on_region_radius = Angle(\"0.11 deg\")\n",
"on_region = CircleSkyRegion(center=target_position, radius=on_region_radius)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"gamera_template_spectral_model=make_template_spectral_model(gamera_custom_spectral_model,on_region,30,10,10,(-1,2),(-2.5,0),(1.5,4))"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
Loading
Loading