SPEX introduction

SPEX is a spectral fitting program used to fit high-resolution X-ray spectra. The code contains several simple and detailed models that are able to deal with the radiative processes observed in the X-ray band.

SPEX has both a command-line and a Python interface. This chapter provides some basic commands and threads to fit X-ray spectra from Python. Alternatively, you could read the Introduction to the SPEX command-line interface.

Before we begin, here are some useful links:

The SPEX data format

The data files containing the spectrum of the source and the response need to be in the correct format. In the SPEX installation, we provide a program called trafo to convert OGIP standard fits files into SPEX format. There is also a Python based alternative called ogip2spex. In this chapter, we assume that you already have spectra in SPEX format provided by the exercises. Each exercise contains a .spo and .res file that can be downloaded from this site.

SPEX needs two files per spectrum:

  • <filename>.spo – This file contains the countrate per energy bin for the source (Di), as well as the background countrate and the errors (σi).

  • <filename>.res – This file contains the instrumental response: the energy redistribution and effective area (Rij Aj).

In order to calculate the observed model spectrum, SPEX uses this integral equation to account for the imperfections caused by the instrument:

  • D(c) = ∫ R(c,E) A(E) S(E) dE

  • Di= Σn j=1 RijAjSj

The .res and .spo files are so-called FITS files. This is a data format widely used in Astronomy. FITS files can contain images as well as data tables. The software package FTOOLS provided by NASA contains a large number of tools to manipulate FITS files. If you are interested, then you can launch flaunch to see which tools are available.

Starting SPEX in Python/Jupyter

Before you continue, please make sure that you have downloaded the files from https://github.com/summerschool-ahead2020/spex-hands-on using git as described on the page above.

The intro directory that you downloaded using git, contains a SPEX_Intro.ipynb file. You can open this notebook to get a SPEX demonstration in Python.

When the SPEX package is installed and loaded in the environment successfully, the notebook should be able to run. Below, we walk through the commands step-by-step to explain what we do.

In a Python or Jupyter session, SPEX can be started like this:

>>> from pyspex.spex import Session
>>> s=Session()
Welcome user to SPEX version 3.07.03
...
>>>

The object s now contains the SPEX program. Through s we can send commands to SPEX and sometimes get data in return. The commands are very similar to the commands used on the SPEX command line.

As a first step, we can load the data files. This is done using the s.data() command. The command first expects the response file (with the .res extension) and then the spectrum file (.spo extension). In the intro directory we have a file called powerl.spo and powerl.res. To load them, you type:

>>> s.data("powerl.res","powerl.spo")

The responsefile (.res) is entered first and then the file containing the spectrum (.spo). Remember that the order of the words in the commands is very important!

Plotting the data

If the s.data() command was successful, we can now have a look at the spectra. Using default settings, the easiest way of plotting a spectrum is as follows:

>>> s.plot_data()

The command above opens a Matplotlib window and tells SPEX that we want to plot the spectral data (s.plot_data()). This will create a linear-linear plot in keV units.

The plot can be tailored to your wishes. Below is an example to create a plot to a log-log plot in Å:

>>> s.plot_data(xlog=True, ylog=True, wave=True)

To make sure the axes are logarithmic, we provide the options (xlog=True and ylog=True) and change the axes to unit Å using wave=true.

If you want to change more features of the plot, you can get the matplotlib plt object from the command like this:

>>> (pdata, plt) = s.plot_data(show=False)

The pdata object contains an astropy table with the plot data, while the plt object contains the matplotlib.pyplot object. You can use this plt object to change plot features, for example:

>>> plt.xlim(xmin, xmax)
>>> plt.ylim(ymin, ymax)

To change the reach of the x and y axes. You can use all the available matplotlib.pyplot commands to modify your plot and finally show it with the plt.show() command.

Ignoring and rebinning

High-resolution X-ray spectra from Chandra and XMM-Newton are usually oversampled (e.g. the energy bins are much smaller than the spectral resolution) and contain a lot more channels then is useful. Therefore, it is necessary to remove wavelength intervals which contain bad data and rebin your spectrum. The SPEX command to ignore parts of the spectrum is called s.ignore() and the command to rebin is called s.bin(). In the next example we bin the spectrum over the 0.5 to 10 keV range with a factor of 5 and ignore the rest of the spectrum:

>>> s.ignore(1, 1, 0.0, 0.5, unit='kev')
>>> s.ignore(1, 1, 10.0, 1000., unit='kev')
>>> s.bin(1, 1, 0.5, 10.0, 5, unit='kev')

The s.ignore() and s.bin() commands need to know which spectrum in the data structure you want to change. If you just loaded one spectrum, this is easy. SPEX organizes spectra in instruments and regions. The first spectrum is by default in instrument 1 and region 1. The first two places in the s.ignore() and s.bin() commands need to contain the instrument number and the region number of the spectrum that you want to change.

The third and forth number is the energy range to apply the command to. So, the third number is the minimum energy and the forth the maximum.

For the bin command, the binning factor is the fifth entry. Finally, we provide the unit of our energy values as unit='kev' in the last entry of the command.

Defining a model

Now we have a clean and rebinned spectrum that is ready to fit. Before we can start fitting, we first need to define a model. It’s equivalent to S(E) in the equation above. The model can contain one or more of these components:

  • absm Model for interstellar absorption.

  • reds Redshift.

  • po Powerlaw.

And there are more! The following command sequence defines a simple powerlaw model at a certain redshift and absorbed by the interstellar medium. The individual components of the model are loaded one-by-one with the s.com() command:

>>> s.com('reds')
>>> s.com('absm')
>>> s.com('po')
>>> s.com_rel(1,3,numpy.array([1,2]))

The last command (s.com_rel(1,3,numpy.array([1,2]))) tells SPEX that component 3, the powerlaw, is first redshifted by component 1 and then absorbed by component 2. The order of the 1 and the 2 is important! Always think what happens in which order on the way from the source to the telescope.

Distance

For most sources the distance is more or less known. To get a right luminosity estimate for the source, the expected distance has to be provided to SPEX. This is done with the s.distance() command:

>>> s.dist(1, 0.1, 'z')

With this command, the distance to the source is set to a redshift of 0.1.

Setting initial parameters

Now we have to estimate the initial parameters. With the command s.par_show() we can see which parameters there are:

>>> s.par_show()
----------------------------------------------------------------------------------
sect comp mod  acro parameter with unit     value      status    minimum   maximum

  1    1 reds z    Redshift              0.000000     frozen   -1.0      1.00E+10

  1    2 absm nh   Column (1E28/m**2)   9.9999997E-05 thawn     0.0      1.00E+20
  1    2 absm f    Covering fraction     1.000000     frozen    0.0       1.0

  1    3 pow  norm Norm (1E44 ph/s/keV)  1.000000     thawn     0.0      1.00E+20
  1    3 pow  gamm Photon index          2.000000     thawn    -10.       10.
  1    3 pow  dgam Photon index break    0.000000     frozen   -10.       10.
  1    3 pow  e0   Break energy (keV)   1.0000000E+10 frozen    0.0      1.00E+20
  1    3 pow  b    Break strength        0.000000     frozen    0.0       10.
  1    3 pow  type Type of norm          0.000000     frozen    0.0       1.0
  1    3 pow  elow Low flux limit (keV)  2.000000     frozen   1.00E-20  1.00E+10
  1    3 pow  eupp Upp flux limit (keV)  10.00000     frozen   1.00E-20  1.00E+10
  1    3 pow  lum  Luminosity (1E30 W)   1.000000     frozen    0.0      1.00E+20

--------------------------------------------------------------------------------
Fluxes and restframe luminosities between   2.0000     and    10.000     keV

sect comp mod   photon flux   energy flux nr of photons    luminosity
           (phot/m**2/s)      (W/m**2)   (photons/s)           (W)
   1    3 pow    0.00000       0.00000       0.00000       0.00000

We can set the parameters using the s.par() command. The commands then look like this:

>>> s.par(1, 1, 'z', 0.1)
>>> s.par(1, 2, 'nh', 2.E-3, thawn=False)
>>> s.par(1, 3, 'norm', 1.E+10, thawn=True)
>>> s.par(1, 3, 'gamm', 1.5, thawn=True)

When the parameter should be free during the fit, then add the optional thawn=True parameter to the command. Then, we run s.par_show() again to see what happened:

>>> s.par_show()
----------------------------------------------------------------------------------
sect comp mod  acro parameter with unit     value      status    minimum   maximum

   1    1 reds z    Redshift              0.100000     frozen   -1.0      1.00E+10

   1    2 absm nh   Column (1E28/m**2)   2.0000001E-03 thawn     0.0      1.00E+20
   1    2 absm f    Covering fraction     1.000000     frozen    0.0       1.0

   1    3 pow  norm Norm (1E44 ph/s/keV)  1.000000E+10 thawn     0.0      1.00E+20
   1    3 pow  gamm Photon index          1.500000     thawn    -10.       10.
   1    3 pow  dgam Photon index break    0.000000     frozen   -10.       10.
   1    3 pow  e0   Break energy (keV)   1.0000000E+10 frozen    0.0      1.00E+20
   1    3 pow  b    Break strength        0.000000     frozen    0.0       10.
   1    3 pow  type Type of norm          0.000000     frozen    0.0       1.0
   1    3 pow  elow Low flux limit (keV)  2.000000     frozen   1.00E-20  1.00E+10
   1    3 pow  eupp Upp flux limit (keV)  10.00000     frozen   1.00E-20  1.00E+10
   1    3 pow  lum  Luminosity (1E30 W)  5.6014867E+08 frozen    0.0      1.00E+20

--------------------------------------------------------------------------------
Fluxes and restframe luminosities between   2.0000     and    10.000     keV

 sect comp mod   photon flux   energy flux nr of photons    luminosity
            (phot/m**2/s)      (W/m**2)   (photons/s)           (W)
    1    3 pow    0.00000       0.00000       0.00000       0.00000

Finding the right initial values for the parameters is a game of trial and error. To see whether you are going in the right direction, you can calculate the model with the command s.calc() and see the result with s.plot_data(). If you see the model appear in your screen, then the model is close enough to be fitted. Especially the normalization of the powerlaw (norm) can vary a lot depending on the count rate of the source.

Fitting the data

We are ready to fit the data! You can give the s.fit() command to start fitting. When the fit is done, then the parameters and C-stat are printed on screen. If the C-stat value is close to the expected C-stat value, then your fit is acceptable. Sometimes more runs of the command s.fit() are necessary after changing some initial parameters. This is especially true when using complex models. Again this is a game of trial and error:

>>> s.fit()

You also might want to fix or free certain parameters to see if they can be constrained. You can fix a parameter with the command s.par_fix and freeing is done with s.par_free() (thawn). For example, you can free the NH by the following command:

>>> s.par_free(1,2,'nh')

Calculating errors

When the fit is acceptable, you might want to know the uncertainties on your fitted parameters. Errors are determined one-by-one by fixing the parameter to some value and calculate the ΔC-stat with respect to the best fit. If you want to know the 1σ error on the parameter, you need to know its values at ΔC-stat = 1. This is done by the s.error command. You can calculate the error for each parameter. For example gamma:

>>> gamm_err = s.error(1,1,'gamm')

The result of the error calculation is stored in the gamm_err object.