Fitting NuSTAR Spectra: Glesener et al. 2020 comparison#

A real example from [Glesener2020] of fitting two NuSTAR spectra simultaneously with gain correction.

import warnings

import matplotlib.pyplot as plt
import numpy as np
from numpy.exceptions import VisibleDeprecationWarning
from parfive import Downloader

from sunkit_spex.legacy.fitting.fitter import Fitter

warnings.filterwarnings("ignore", category=RuntimeWarning)
try:
    warnings.filterwarnings("ignore", category=VisibleDeprecationWarning)
except AttributeError:
    warnings.filterwarnings("ignore", category=np.exceptions.VisibleDeprecationWarning)

Set up some plotting numbers

spec_single_plot_size = (8, 10)
spec_plot_size = (25, 10)
spec_font_size = 18
default_font_size = 10
x_limits, y_limits = [1.6, 8.5], [1e-1, 1e3]

# Let’s fit a more realistic example

Try fitting the spectra presented in [Glesener et al. 2020](https://iopscience.iop.org/article/10.3847/2041-8213/ab7341). The spectrum presented shows clear evidence of non-thermal emission in an A5.7 microflare.

## Let’s recreate Figure 4 (left) where NuSTAR FPMB is fitted with a thermal+cold thick target model.

set up plotting info stuff

gles_xlims, gles_ylims = [2, 12], [1e1, 1e4]

Download data files

dl = Downloader()

base_url = "https://sky.dias.ie/public.php/dav/files/BHW6y6aXiGGosM6/nustar/Glesener2020/"
file_names = [
    "nu20312001001B06_cl_grade0_sr_grp.pha",
    "nu20312001001B06_cl_grade0_sr.arf",
    "nu20312001001B06_cl_grade0_sr.rmf",
    "nu20312001001A06_cl_grade0_sr_grp.pha",
    "nu20312001001A06_cl_grade0_sr.arf",
    "nu20312001001A06_cl_grade0_sr.rmf",
]

for fname in file_names:
    dl.enqueue_file(base_url + fname, path="../../nustar/Glesener2020/")
files = dl.download()
Files Downloaded:   0%|          | 0/6 [00:00<?, ?file/s]


nu20312001001B06_cl_grade0_sr.arf:   0%|          | 0.00/63.4k [00:00<?, ?B/s]



nu20312001001B06_cl_grade0_sr.rmf:   0%|          | 0.00/27.0M [00:00<?, ?B/s]

nu20312001001B06_cl_grade0_sr_grp.pha:   0%|          | 0.00/141k [00:00<?, ?B/s]





nu20312001001A06_cl_grade0_sr.arf:   0%|          | 0.00/63.4k [00:00<?, ?B/s]




nu20312001001A06_cl_grade0_sr_grp.pha:   0%|          | 0.00/141k [00:00<?, ?B/s]


nu20312001001B06_cl_grade0_sr.arf:   2%|▏         | 1.02k/63.4k [00:00<00:09, 6.64kB/s]

nu20312001001B06_cl_grade0_sr_grp.pha:   1%|          | 1.02k/141k [00:00<00:21, 6.54kB/s]





nu20312001001A06_cl_grade0_sr.arf:   2%|▏         | 1.02k/63.4k [00:00<00:09, 6.57kB/s]



nu20312001001B06_cl_grade0_sr.rmf:   0%|          | 1.02k/27.0M [00:00<1:13:07, 6.16kB/s]




nu20312001001A06_cl_grade0_sr_grp.pha:   1%|          | 1.02k/141k [00:00<00:23, 5.98kB/s]

nu20312001001B06_cl_grade0_sr_grp.pha:  18%|█▊        | 25.6k/141k [00:00<00:00, 121kB/s]





nu20312001001A06_cl_grade0_sr.arf:  53%|█████▎    | 33.8k/63.4k [00:00<00:00, 160kB/s]


nu20312001001B06_cl_grade0_sr.arf:  66%|██████▋   | 42.0k/63.4k [00:00<00:00, 137kB/s]



Files Downloaded:  17%|█▋        | 1/6 [00:00<00:03,  1.39file/s]







nu20312001001B06_cl_grade0_sr_grp.pha:  65%|██████▌   | 92.2k/141k [00:00<00:00, 341kB/s]



nu20312001001B06_cl_grade0_sr.rmf:   0%|          | 42.0k/27.0M [00:00<03:23, 132kB/s]




nu20312001001A06_cl_grade0_sr_grp.pha:  30%|██▉       | 42.0k/141k [00:00<00:00, 131kB/s]

nu20312001001B06_cl_grade0_sr_grp.pha:  94%|█████████▎| 132k/141k [00:00<00:00, 362kB/s]


Files Downloaded:  50%|█████     | 3/6 [00:00<00:00,  4.31file/s]



nu20312001001B06_cl_grade0_sr.rmf:   0%|          | 116k/27.0M [00:00<01:23, 321kB/s]




nu20312001001A06_cl_grade0_sr_grp.pha:  89%|████████▉ | 126k/141k [00:00<00:00, 350kB/s]







nu20312001001A06_cl_grade0_sr.rmf:   0%|          | 0.00/26.8M [00:00<?, ?B/s]



nu20312001001B06_cl_grade0_sr.rmf:   1%|          | 247k/27.0M [00:00<00:42, 626kB/s]


nu20312001001A06_cl_grade0_sr.rmf:   0%|          | 1.02k/26.8M [00:00<1:11:19, 6.26kB/s]



nu20312001001B06_cl_grade0_sr.rmf:   2%|▏         | 460k/27.0M [00:00<00:24, 1.09MB/s]


nu20312001001A06_cl_grade0_sr.rmf:   0%|          | 99.3k/26.8M [00:00<00:57, 461kB/s]



nu20312001001B06_cl_grade0_sr.rmf:   4%|▎         | 983k/27.0M [00:00<00:11, 2.35MB/s]


nu20312001001A06_cl_grade0_sr.rmf:   1%|          | 263k/26.8M [00:00<00:28, 929kB/s]



nu20312001001B06_cl_grade0_sr.rmf:   7%|▋         | 1.93M/27.0M [00:00<00:05, 4.51MB/s]


nu20312001001A06_cl_grade0_sr.rmf:   2%|▏         | 640k/26.8M [00:00<00:13, 1.96MB/s]



nu20312001001B06_cl_grade0_sr.rmf:  14%|█▍        | 3.89M/27.0M [00:00<00:02, 9.06MB/s]


nu20312001001A06_cl_grade0_sr.rmf:   5%|▌         | 1.40M/26.8M [00:00<00:06, 3.86MB/s]



nu20312001001B06_cl_grade0_sr.rmf:  28%|██▊       | 7.69M/27.0M [00:01<00:01, 17.3MB/s]


nu20312001001A06_cl_grade0_sr.rmf:  11%|█         | 2.90M/26.8M [00:00<00:03, 7.49MB/s]



nu20312001001B06_cl_grade0_sr.rmf:  41%|████      | 11.1M/27.0M [00:01<00:00, 20.8MB/s]


nu20312001001A06_cl_grade0_sr.rmf:  21%|██        | 5.50M/26.8M [00:00<00:01, 13.3MB/s]



nu20312001001B06_cl_grade0_sr.rmf:  55%|█████▍    | 14.8M/27.0M [00:01<00:00, 23.7MB/s]


nu20312001001A06_cl_grade0_sr.rmf:  33%|███▎      | 8.90M/26.8M [00:00<00:00, 19.6MB/s]



nu20312001001B06_cl_grade0_sr.rmf:  69%|██████▉   | 18.6M/27.0M [00:01<00:00, 25.4MB/s]


nu20312001001A06_cl_grade0_sr.rmf:  47%|████▋     | 12.6M/26.8M [00:01<00:00, 23.0MB/s]



nu20312001001B06_cl_grade0_sr.rmf:  83%|████████▎ | 22.3M/27.0M [00:01<00:00, 26.6MB/s]


nu20312001001A06_cl_grade0_sr.rmf:  61%|██████▏   | 16.4M/26.8M [00:01<00:00, 25.0MB/s]



nu20312001001B06_cl_grade0_sr.rmf:  97%|█████████▋| 26.1M/27.0M [00:01<00:00, 27.4MB/s]




Files Downloaded:  83%|████████▎ | 5/6 [00:02<00:00,  2.27file/s]


nu20312001001A06_cl_grade0_sr.rmf:  75%|███████▌  | 20.2M/26.8M [00:01<00:00, 26.3MB/s]


nu20312001001A06_cl_grade0_sr.rmf:  89%|████████▉ | 23.9M/26.8M [00:01<00:00, 27.3MB/s]



Files Downloaded: 100%|██████████| 6/6 [00:02<00:00,  2.66file/s]
Files Downloaded: 100%|██████████| 6/6 [00:02<00:00,  2.59file/s]

First, load in your data files, here we load in 2 spectra

_dir = "../../nustar/Glesener2020/"
# In the files here, the ARF and RMF file have different names to the PHA files so cannot use the PHA file name to help find the others so...
spec = Fitter(
    pha_file=_dir + "nu20312001001B06_cl_grade0_sr_grp.pha",
    arf_file=_dir + "nu20312001001B06_cl_grade0_sr.arf",
    rmf_file=_dir + "nu20312001001B06_cl_grade0_sr.rmf",
)

Define model, here we go for a single isothermal model + cold thick model

spec.model = "f_vth + thick_fn"

Define fitting range

spec.energy_fitting_range = [2.8, 10.5]

Sort temperature param from f_vth

spec.params["T1_spectrum1"] = {"Value": 10.3, "Bounds": (1.1, 15)}
# emission measure param from f_vth
spec.params["EM1_spectrum1"] = {"Value": 0.5, "Bounds": (1e-2, 1e1)}
# electron flux param from thick_fn
spec.params["total_eflux1_spectrum1"] = {"Value": 2.1, "Bounds": (1e-3, 10)}  # units 1e35 e^-/s
# electron index param from thick_fn
spec.params["index1_spectrum1"] = {"Value": 6.2, "Bounds": (3, 10)}
# electron low energy cut-off param from thick_fn
spec.params["e_c1_spectrum1"] = {"Value": 6.2, "Bounds": (1, 12)}  # units keV

This fit requires altering the gain parameters

Gain parameters can be tweaked in the same way model parameters can.

The difference is that gain parameters all have specific starting values (slope=1, offset=0) and are frozen by default.

# from Glesener et al. 2020 which had a gain correction fixed at 0.95
spec.rParams["gain_slope_spectrum1"] = {"Status": "fixed", "Value": 0.95}

print(spec.rParams)

print(spec.show_rParams)
                       Status  Value       Bounds       Error
gain_slope_spectrum1   frozen   0.95   (0.8, 1.2)  (0.0, 0.0)
gain_offset_spectrum1  frozen   0.00  (-0.1, 0.1)  (0.0, 0.0)
        Param         Status   Value       Bounds            Error
                                         (min, max)          (-, +)
--------------------- ------ ---------- ----------- ------------------------
 gain_slope_spectrum1 frozen      0.950  (0.8, 1.2) (      0.00,       0.00)
gain_offset_spectrum1 frozen      0.000 (-0.1, 0.1) (      0.00,       0.00)

Fit the model to the spectrum

spec.fit(tol=1e-8)

# Plot the result
plt.rcParams["font.size"] = spec_font_size
plt.figure(figsize=spec_single_plot_size)
axes, res_axes = spec.plot()
for a in axes:
    a.set_xlim(gles_xlims)
    a.set_ylim(gles_ylims)
plt.show()
plt.rcParams["font.size"] = default_font_size
Spectrum 1

Let’s recreate something similar to Figure 3(c)

Both NuSTAR FPMs are fitted with a thermal+cold thick target model simultaneously.

In the original Figure 3(c), a broken power law is used as the cold thick target does not exist ins XPSEC.

First, load in your data files, here we load in 2 spectra

_dir = "../../nustar/Glesener2020/"
spec = Fitter(
    pha_file=[_dir + "nu20312001001A06_cl_grade0_sr_grp.pha", _dir + "nu20312001001B06_cl_grade0_sr_grp.pha"],
    arf_file=[_dir + "nu20312001001A06_cl_grade0_sr.arf", _dir + "nu20312001001B06_cl_grade0_sr.arf"],
    rmf_file=[_dir + "nu20312001001A06_cl_grade0_sr.rmf", _dir + "nu20312001001B06_cl_grade0_sr.rmf"],
)

Define model, here we go for a single isothermal model + cold thick model

spec.model = "C*(f_vth + thick_fn)"

Define fitting range

spec.energy_fitting_range = [2.8, 10.5]

Sort temperature param from f_vth

spec.params["T1_spectrum1"] = {"Value": 10.3, "Bounds": (1.1, 15)}
# emission measure param from f_vth
spec.params["EM1_spectrum1"] = {"Value": 0.5, "Bounds": (1e-2, 1e1)}
# electron flux param from thick_fn
spec.params["total_eflux1_spectrum1"] = {"Value": 2.1, "Bounds": (1e-3, 10)}  # units 1e35 e^-/s
# electron index param from thick_fn
spec.params["index1_spectrum1"] = {"Value": 6.2, "Bounds": (3, 10)}
# electron low energy cut-off param from thick_fn
spec.params["e_c1_spectrum1"] = {"Value": 6.2, "Bounds": (1, 12)}  # units keV
# constant for systematic offset between FPMs, found to be about 1.1
spec.params["C_spectrum1"] = "frozen"
# constant for systematic offset between FPMs, found to be about 1.1
spec.params["C_spectrum2"] = {"Status": "fixed", "Value": 1.1}
# from Gles. 2020 which had a gain correction fixed at 0.95
spec.rParams["gain_slope_spectrum1"] = {"Status": "fixed", "Value": 0.95}
spec.rParams["gain_slope_spectrum2"] = spec.rParams["gain_slope_spectrum1"]

Fit the model to the spectrum

spec.fit(tol=1e-8)

# Plot the result
plt.rcParams["font.size"] = spec_font_size
plt.figure(figsize=spec_plot_size)
axes, res_axes = spec.plot()
for a in axes:
    a.set_xlim(gles_xlims)
    a.set_ylim(gles_ylims)
plt.show()
plt.rcParams["font.size"] = default_font_size
Spectrum 1, Spectrum 2, Combined Spectra

For the thermal and cold thick target total model we compare

Model Parameter

OSPEX (Glesener et al. 2020) [*]

This Work (just FPMB)

This Work (FPMA&B)

Temperature [MK]

10.3\(\pm\)0.7

11.3\(\pm\)0.7

11.2\(\pm\)0.4

Emission Measure [cm-3]

5.0 \(\pm\) 1.3\(\times\)10 45

3.7 \(\pm\) 0.9\(\times\)10 45

3.7 \(\pm\) 0.5\(\times\)10 45

Electron Flux [e$^{-}$ s$^{-1}$]

2.1\(\pm\)1.1\(\times\)10 35

2.2\(\pm\)1.1\(\times\)10 35

1.6\(\pm\)0.5\(\times\)10 35

Index

6.2\(\pm\)0.6

6.5\(\pm\)0.8

6.5\(\pm\)0.6

Low Energy Cut-off [keV]

6.2\(\pm\)0.9

6.5\(\pm\)1.0

6.7\(\pm\)0.7

Now let’s recreate Figure 4 (right)

NuSTAR FPMB is fitted with a warm thick target model.

The warm thick target model helps to constrain the non-thermal emission with observed values (e.g., loop length, etc) and ties it to the thermal emission parameters.

First, load in your data files, here we load in 1 spectrum

_dir = "../../nustar/Glesener2020/"
spec = Fitter(
    pha_file=_dir + "nu20312001001B06_cl_grade0_sr_grp.pha",
    arf_file=_dir + "nu20312001001B06_cl_grade0_sr.arf",
    rmf_file=_dir + "nu20312001001B06_cl_grade0_sr.rmf",
)

define model, here we go for a single isothermal model + cold thick model

spec.model = "thick_warm"

define fitting range

spec.energy_fitting_range = [2.8, 10.5]
# electron flux param
spec.params["tot_eflux1_spectrum1"] = {"Value": 2, "Bounds": (1e-2, 10)}
# electron index param
spec.params["ind1_spectrum1"] = {"Value": 6, "Bounds": (3, 10)}
# electron low energy cut-off param
spec.params["ec1_spectrum1"] = {"Value": 7, "Bounds": (3, 12)}
# loop plasma temperature param
spec.params["loop_temp1_spectrum1"] = {"Value": 10, "Bounds": (5, 15)}
# plasma number density param
spec.params["plasma_d1_spectrum1"] = {"Value": 1, "Bounds": (1e-2, 1e1)}  # units 1e10 cm^-3
# loop length param
spec.params["length1_spectrum1"] = {"Status": "fixed", "Value": 15}  # units Mm
# from Gles. 2020 which had a gain correction fixed at 0.95
spec.rParams["gain_slope_spectrum1"] = {"Status": "fixed", "Value": 0.95}

fit the model to the spectrum

spec.fit(tol=1e-10)

# plot the result
plt.rcParams["font.size"] = spec_font_size
plt.figure(figsize=spec_single_plot_size)
axes, res_axes = spec.plot()
for a in axes:
    a.set_xlim(gles_xlims)
    a.set_ylim(gles_ylims)
plt.show()
plt.rcParams["font.size"] = default_font_size
Spectrum 1
Minimum length>length

Fitting the warm thick target model to both FPMs simultaneously

First, load in your data files, here we load in 2 spectra

_dir = "../../nustar/Glesener2020/"
spec = Fitter(
    pha_file=[_dir + "nu20312001001A06_cl_grade0_sr_grp.pha", _dir + "nu20312001001B06_cl_grade0_sr_grp.pha"],
    arf_file=[_dir + "nu20312001001A06_cl_grade0_sr.arf", _dir + "nu20312001001B06_cl_grade0_sr.arf"],
    rmf_file=[_dir + "nu20312001001A06_cl_grade0_sr.rmf", _dir + "nu20312001001B06_cl_grade0_sr.rmf"],
)

Define model, here we go for a single isothermal model + cold thick model

spec.model = "C*thick_warm"

Define fitting range

spec.energy_fitting_range = [2.8, 10.5]

# electron flux param
spec.params["tot_eflux1_spectrum1"] = {"Value": 2, "Bounds": (1e-3, 10)}
# electron index param
spec.params["ind1_spectrum1"] = {"Value": 6, "Bounds": (3, 10)}
# electron low energy cut-off param
spec.params["ec1_spectrum1"] = {"Value": 7, "Bounds": (1, 12)}
# loop plasma temperature param
spec.params["loop_temp1_spectrum1"] = {"Value": 10, "Bounds": (1.1, 15)}
# plasma number density param
spec.params["plasma_d1_spectrum1"] = {"Value": 1, "Bounds": (1e-2, 1e1)}
# loop length param
spec.params["length1_spectrum1"] = {"Status": "fixed", "Value": 15}
# constant for systematic offset between FPMs, found to be about 1.1
spec.params["C_spectrum1"] = "frozen"
spec.params["C_spectrum2"] = {"Status": "fixed", "Value": 1.1}
# from Gles. 2020 which had a gain correction fixed at 0.95
spec.rParams["gain_slope_spectrum1"] = {"Status": "fixed", "Value": 0.95}
spec.rParams["gain_slope_spectrum2"] = spec.rParams["gain_slope_spectrum1"]

Fit the model to the spectrum

spec.fit(tol=1e-5)

# Plot the result
plt.rcParams["font.size"] = spec_font_size
plt.figure(figsize=spec_plot_size)
axes, res_axes = spec.plot()
for a in axes:
    a.set_xlim(gles_xlims)
    a.set_ylim(gles_ylims)
plt.show()
plt.rcParams["font.size"] = default_font_size
Spectrum 1, Spectrum 2, Combined Spectra

For warm thick target total model we compare

Model Parameter

OSPEX (Glesener et al. 2020)

This Work (just FPMB)

This Work (FPMA&B)

Temperature [MK]

10.2\(\pm\)0.7

11.2\(\pm\)0.6

11.2\(\pm\)0.4

Plasma Density [cm-3]

6.0 \(\pm\) 2.0\(\times\)10 9

5.0 \(\pm\) 1.3\(\times\)10 9

5.7 \(\pm\) 1.1\(\times\)10 9

Electron Flux [e$^{-}$ s$^{-1}$]

1.8\(\pm\)0.8\(\times\)10 35

2.0\(\pm\)0.8\(\times\)10 35

1.6\(\pm\)0.5\(\times\)10 35

Index

6.3\(\pm\)0.7

6.6\(\pm\)0.8

6.5\(\pm\)0.6

Low Energy Cut-off [keV]

6.5\(\pm\)0.9

6.7\(\pm\)0.9

6.7\(\pm\)0.7

All parameter values appear to be within error margins (or extremely close). This is more impressive when the errors calculated in this work for the minimised values assumes the parameter’s have a Gaussian and independent posterior distribution (which is clearly not the case) and so these errors are likely to be larger; to be investigated with an MCMC.

The simultaneous fit of FPMA&B with the cold thick target model and the warm thick model is not able to be performed in OSPEX.

Total running time of the script: (2 minutes 16.829 seconds)

Gallery generated by Sphinx-Gallery