Imaging analysis#

We start with some preparatory steps. Uncomment these commands or run them on terminal, making sure you have loaded the heasoft environment

user_name = 'samueleronchini'
# root = f'/home/idies/workspace/Storage/{user_name}/persistent'
root = '/Users/sjs8171/Desktop/bat_glimpse'
# %cd {root}
# ! git clone https://github.com/Swift-BAT/NITRATES.git
# %cd NITRATES
# ! git fetch origin
# ! git switch -c acme origin/acme
# ! git branch
# ! pip install -e .
# ! pip install BatAnalysis 

Edit here trigid and trigger_time . We start analysing this GRB 241105A https://gcn.nasa.gov/circulars/38091

trigid = '752515599_c0'
triggertime = '2024-11-05T16:06:04.660000'
# root = f'/home/idies/workspace/Storage/{user_name}/persistent'
# root = '/Users/sjs8171/Desktop/bat_glimpse'
# create data directory and work dir if it does not exist
import os

# data_dir = f'{root}/{trigid}/data'
data_dir = f'{root}/data'

if not os.path.exists(data_dir):
    os.makedirs(data_dir)

workdir = f'{root}/{trigid}'

if not os.path.exists(workdir):
    os.makedirs(workdir)
    
def filter_data(obsid):
    command = f'python -m nitrates.data_prep.mkdb --work_dir {workdir} '
    ! echo {command}
    ! {command}
    
    command = '' \
    '' \
    'python -m nitrates.data_prep.do_data_setup ' \
    f'--work_dir {workdir}' \
    f' --trig_time {triggertime}' \
    f' --evfname {root}/data/{obsid}/bat/event/sw{obsid}bevshsl_uf.evt.gz' \
    f' {root}/data/{obsid}/bat/event/sw{obsid}bevshpo_uf.evt.gz' \
    f' --dmask {root}/data/{obsid}/bat/hk/sw{obsid}bdecb.hk.gz' \
    f' --att_fname {root}/data/{obsid}/auxil/sw{obsid}pat.fits.gz' \
    f' --acs_fname {root}/data/{obsid}/auxil/sw{obsid}pat.fits.gz' \
    f' --data_dbfname {workdir}/results.db'
    
    ! echo {command}
    ! {command}

We first get data from GUANO, using swifttools. For a complete guide to use GUANO API, see https://www.swift.psu.edu/too_api/index.php?md=Swift GUANO Example Notebook.ipynb

import matplotlib.pyplot as plt
import numpy as np
import astropy.units as u
import os
import batanalysis as ba

import logging
import time
from pathlib import Path
import traceback
from swifttools.swift_too import GUANO, Clock, Data

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

import warnings
warnings.filterwarnings("ignore")



def guano_query(triggertime, ext_obsid, workdir, datadir):

    if '.' not in triggertime.split('T')[1]:
        triggertime = triggertime + '.000Z'
    else:
        triggertime = triggertime + 'Z'

    logging.info(f'Using triggertime: {triggertime}')

    guano = GUANO(triggertime=triggertime, successful = False)

    logging.info(guano)

    for item in guano: # this loop is required if multiple triggers are associated with the same obsid
        logging.info(f'running {item}')

        if item.data.exposure is None:
            logging.error(f'No exposure time found for obsid {item.obsid}. Skipping this obsid.')
            exit()

        if ext_obsid is not None:
            obsid = ext_obsid
        else:
            obsid = item.obsid

        start_time_try = time.time()
        event = None
        while time.time() - start_time_try < 1800 and event is None:
            try:
                '''
                We need to remove any data or results already existing, since BatAnalysis can have problems
                '''
                # Remove directories if they exist
                obsid_dir = f"{datadir}/{obsid}"
                obsid_eventresult_dir = f"{datadir}/{obsid}_eventresult"
                if os.path.exists(obsid_dir):
                    os.system(f"rm -rf {obsid_dir}")
                if os.path.exists(obsid_eventresult_dir):
                    os.system(f"rm -rf {obsid_eventresult_dir}")


                data = Data(obsid=obsid, bat=True, outdir=datadir, clobber=True, uksdc=True)
                logging.info(data)
                ba.datadir(datadir)
                event = ba.BatEvent(obsid, is_guano=True)

                # filter data using nitrates
                filter_data(obsid)
                
                # We use here the filtered files produced by NITRATES
                event.detector_quality_file = Path(f'{workdir}/detmask.fits')
                event.event_files = Path(f'{workdir}/filter_evdata.fits')
                event.attitude_file = Path(f'{workdir}/attitude.fits')

                event._parse_event_file()

                ba.mosaic._pcodethresh = 0.01  

            except Exception:
                logging.error(f"Failed to create Data and BatEvent for obsid {obsid}: {traceback.format_exc()}")
    
    return event

Data preparation#

event = guano_query(triggertime, None, workdir, data_dir)
2025-12-18 12:22:16,497 - INFO - Using triggertime: 2024-11-05T16:06:04.660000Z
2025-12-18 12:22:17,398 - INFO - +--------------+----------------------------+------------+---------------------+----------------+
| Trigger Type |               Trigger Time | Offset (s) | Window Duration (s) | Observation ID |
+--------------+----------------------------+------------+---------------------+----------------+
|      GBM GRB | 2024-11-05 16:06:04.660000 |       50.0 |               200.0 |    00018897003 |
+--------------+----------------------------+------------+---------------------+----------------+
2025-12-18 12:22:17,401 - INFO - running +-------------+----------------------------+
|   Parameter |                      Value |
+-------------+----------------------------+
| triggertime | 2024-11-05 16:06:04.660000 |
| triggertype |                    GBM GRB |
|      offset |                       50.0 |
|    duration |                      200.0 |
|   quadsaway |                   14491126 |
|      obsnum |                00018897003 |
|          ra |                       -1.0 |
|         dec |                       -1.0 |
|        data |   200.1s of BAT event data |
+-------------+----------------------------+
2025-12-18 12:22:29,709 - INFO - +-------------------------+-----------------------------------------+-----------------------------------------+
|                    Path |                                Filename |                             Description |
+-------------------------+-----------------------------------------+-----------------------------------------+
|       00018897003/auxil | SWIFT_TLE_ARCHIVE.txt.24317.95387196.gz |                      Auxillary TLE file |
|                      '' |                sw00018897003pat.fits.gz |                 Auxillary attitude file |
|                      '' |                 sw00018897003pjb.par.gz |                          Auxillary file |
|                      '' |                 sw00018897003pob.cat.gz |                       Auxillary catalog |
|                      '' |                 sw00018897003ppr.par.gz |                          Auxillary file |
|                      '' |                   sw00018897003s.mkf.gz |                          Auxillary file |
|                      '' |                sw00018897003sao.fits.gz |                          Auxillary file |
|                      '' |                sw00018897003sat.fits.gz |                 Auxillary attitude file |
|                      '' |                  sw00018897003sen.hk.gz |                  Auxillary housekeeping |
|                      '' |                sw00018897003sti.fits.gz |                          Auxillary file |
|                      '' |                sw00018897003uat.fits.gz |                 Auxillary attitude file |
|                      '' |                   sw00018897003x.mkf.gz |                          Auxillary file |
|   00018897003/bat/event |          sw00018897003bevshpo_uf.evt.gz | BAT pointed unfiltered event event file |
|                      '' |          sw00018897003bevshsl_uf.evt.gz |    BAT slew unfiltered event event file |
|      00018897003/bat/hk |                sw00018897003bdecb.hk.gz |                   BAT housekeeping file |
|                      '' |                  sw00018897003bdp.hk.gz |                   BAT housekeeping file |
|                      '' |                sw00018897003bdqcb.hk.gz |                   BAT housekeeping file |
|                      '' |                  sw00018897003ben.hk.gz |                   BAT housekeeping file |
|                      '' |              sw00018897003bevtlsp.hk.gz |                   BAT housekeeping file |
|                      '' |              sw00018897003bevtssp.hk.gz |                   BAT housekeeping file |
|                      '' |                sw00018897003bgocb.hk.gz |                   BAT housekeeping file |
|                      '' |                  sw00018897003bhd.hk.gz |                   BAT housekeeping file |
| 00018897003/bat/masktag |       sw00018897003bmt00010005_rw.lc.gz |          BAT raw mask tagged lightcurve |
|                      '' |          sw00018897003bmt00010009.lc.gz |              BAT mask tagged lightcurve |
|                      '' |       sw00018897003bmt00010009_rw.lc.gz |          BAT raw mask tagged lightcurve |
|    00018897003/bat/rate |                sw00018897003brt1s.lc.gz |                     BAT rate lightcurve |
|                      '' |                sw00018897003brtmc.lc.gz |                     BAT rate lightcurve |
|                      '' |                sw00018897003brtms.lc.gz |                     BAT rate lightcurve |
|                      '' |                sw00018897003brtqd.lc.gz |                     BAT rate lightcurve |
|  00018897003/bat/survey |     sw00018897003bsvpbo019ag0071.dph.gz |                          BAT survey DPH |
|                      '' |     sw00018897003bsvpbo019eg0072.dph.gz |                          BAT survey DPH |
|                      '' |     sw00018897003bsvpbo01a7g0074.dph.gz |                          BAT survey DPH |
+-------------------------+-----------------------------------------+-----------------------------------------+
A save file has been written to /Users/sjs8171/Desktop/bat_glimpse/data/00018897003_eventresult/batevent.pickle.

Convert the t0 from UTC to MET

t0 = Clock(utctime=triggertime+'Z').met

Visualising the attitude info#

plt.plot(event.attitude.time.value-t0, event.attitude.ra, label='RA', color='red')
plt.plot(event.attitude.time.value-t0, event.attitude.dec, label='Dec', color='blue')

# Identify intervals where RA and Dec change
ra_diff = np.diff(event.attitude.ra.value)
dec_diff = np.diff(event.attitude.dec.value)
margin = 1 / 60  # Margin of 1/60 degree, i.e., 1 arcmin
change_indices = np.where((np.abs(ra_diff) > margin) | (np.abs(dec_diff) > margin))[0]

for idx in change_indices:
    plt.axvspan(event.attitude.time[idx].value - t0, event.attitude.time[idx + 1].value - t0, color='gray', alpha=0.3, linewidth=0, label='Slew' if idx == change_indices[0] else None)

plt.xlim(-50, 50)
plt.xlabel("Time [s] (t - t0)")
plt.ylabel("RA/Dec [deg]")
plt.legend()
plt.show()
plt.savefig(f'{workdir}/attitude.png', dpi=500)
plt.close()
../_images/403da31fc429d7beb6fab40b8331f1956234e04b7b5318660f6048c331e48f4e.png

Check the external position with respect to BAT FOV#

We first need to downloea the Fermi-GBM map:

output_file = os.path.join(workdir, 'ext_loc_fermi.fit')
! curl -o {output_file} https://heasarc.gsfc.nasa.gov/FTP/fermi/data/gbm/triggers/2024/bn241105671/quicklook/glg_healpix_all_bn241105671.fit
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 1549k  100 1549k    0     0   267k      0  0:00:05  0:00:05 --:--:--  397k
from matplotlib.lines import Line2D
import ligo.skymap.io
import ligo.skymap.plot
import ligo.skymap.postprocess

from matplotlib.colors import LinearSegmentedColormap


def map_ext(fit_file, workdir, ax_globe):

    skymap, _ = ligo.skymap.io.fits.read_sky_map(os.path.join(workdir, fit_file), nest=True, distances=False)
    skymap_prob = skymap.copy()

    white_to_blue = LinearSegmentedColormap.from_list("whiteblue", ["white", "orange"])

    ax_globe.imshow_hpx((skymap_prob, 'ICRS'), cmap=white_to_blue, alpha=1.0, nested=True, zorder=0)

    cls = 100 * ligo.skymap.postprocess.util.find_greedy_credible_levels(skymap)
    
    ax_globe.contour_hpx((cls, 'ICRS'), nested=True, colors='black',
                        levels=(50, 90), zorder=0,
                        linestyles=['dashed', 'solid'],
                        linewidths=0.7)

    loc_line = [
        Line2D([0], [0], color='red', linestyle='dotted', linewidth=2, label='External localization', alpha=.0),
        Line2D([0], [0], color='black', linestyle='solid', linewidth=2, label='90% C.L.'),
        Line2D([0], [0], color='black', linestyle='dashed', linewidth=2, label='50% C.L.')
    ]

    return loc_line


def map_imaging(event, t0, workdir):


    fig = plt.figure(figsize=(11, 5))

    ax_globe = fig.add_axes([
        0.05,  # left
        0.10,  # bottom
        0.68,  # width
        0.80   # height
    ], projection='astro degrees mollweide')

    '''
    We create a sky image and extract partial coding
    '''


    skyview = event.create_skyview(timebins=[0,1]*u.s, energybins=[15,350]*u.keV,
                                    is_relative=True, recalc = True, T0=t0)
    
    t = skyview.pcode_img # We extract the partial coding image

    hp_proj = t.healpix_projection(coordsys="icrs", nside=1024) # projecting in healpix (from BAT FOV coords to sky coords)

    '''
    The hp_proj object contains the healpix projection of the sky image.
    It is an array that contains [time, sky pixel, energy band]. 
    Since we created a single sky image for a single time bin and a single energy band,
    we can extract the first time and first energy band to get the sky map.
    '''

    plot_quantity_pc = hp_proj.contents[0,:,0]

    # Contour partial coding
    cs = ax_globe.contour_hpx((plot_quantity_pc, 'ICRS'), nested=False,
                                colors='red', levels=[0.01,0.3,0.8],
                                linewidths=1, zorder=2)
    

    '''
    External localization
    '''

    fit_file = next((fname for fname in os.listdir(workdir) if 'ext_loc' in fname), None)
    loc_handles = None
    if fit_file:
        loc_handles = map_ext(fit_file, workdir, ax_globe)
        

    # Here some aestethic adjustments

    ax_globe.clabel(cs, inline=True, fmt='%1.2f', fontsize=8)
    ax_globe.grid()
    ax_globe.set_position([0.08, 0.12, 0.84, 0.64])
    # Handles per la legenda del partial coding
    red_line = [Line2D([0], [0], color='red', lw=1, label='Partial coding')]
    ax_leg_top = fig.add_axes([0.76, 0.55, 0.22, 0.28])
    ax_leg_top.axis('off')
    if loc_handles is not None:
        leg_ext = ax_leg_top.legend(handles=loc_handles, loc='upper left', frameon=True, borderaxespad=0.5)
        ax_leg_top.add_artist(leg_ext)
    ax_leg_bot = fig.add_axes([0.76, -0.1, 0.22, 0.28])
    ax_leg_bot.axis('off')
    leg_pc = ax_leg_bot.legend(handles=red_line, loc='upper left', frameon=True, borderaxespad=0.5)
    ax_leg_bot.add_artist(leg_pc)

    plt.savefig(os.path.join(workdir,'map_imaging.png'), bbox_inches='tight', dpi=300)

    plt.show()
    plt.close()

map_imaging(event, t0, workdir)
../_images/c3c9ab04672de86f111abcabad09d59cefd2977e94ba135f9faddc5ad1aeb993.png

Count Rate light curve#

We define here a function to read event data fits file

import astropy.io.fits as fits

def load_event_times(filename):
    with fits.open(filename) as hdul:
        for hdu in hdul:
            if hdu.name == "EVENTS":
                event_times = hdu.data["TIME"]
            if hdu.name == "GTI":
                gti_start = hdu.data["START"]
                gti_stop = hdu.data["STOP"]
    return event_times, gti_start, gti_stop

The filtered event data gives Good Time Intervals, namely intervals where data are reliable. The following function creates a mask to select counts only during GTIs

def bin_light_curve(times, bin_size, gti_start, gti_stop, t0):

    time_min = t0 - 50  # Start time for binning
    time_max = t0 + 150  # End time for binning

    # Create bins from time_min to time_max
    bins = np.arange(time_min, time_max + bin_size, bin_size)
    bin_centers = bins[:-1] + 0.5 * bin_size

    # Find mask for good bins (not intersecting with any bad interval)
    good_mask = np.ones_like(bin_centers, dtype=bool)
    # Vectorized GTI mask: keep bins fully inside any GTI interval
    left_edges = bin_centers - 0.5 * bin_size
    right_edges = bin_centers + 0.5 * bin_size

    # For each bin, check if it is fully inside any GTI interval
    # This creates a (n_bins, n_gti) boolean array
    in_gti = (left_edges[:, None] >= gti_start[None, :]) & (right_edges[:, None] <= gti_stop[None, :])
    good_mask = np.any(in_gti, axis=1)

    # Histogram counts for all bins
    counts, _ = np.histogram(times, bins=bins)

    # Select only good bins and corresponding counts
    bin_centers_good = bin_centers[good_mask]
    counts_good = counts[good_mask]

    return bin_centers_good, counts_good

Background fitting#

! pip install emcee 
import emcee
import multiprocessing
from scipy.optimize import curve_fit
from scipy.special import gammaln
from tqdm import tqdm

def poly3_func(x, a, b, c, d):
    return a * x**3 + b * x**2 + c * x + d

def model(xx, t0, best_params, norm_val):
    return poly3_func(xx - t0, *best_params) * norm_val


def fit_background_linear(bin_centers, counts, t0):

    x = bin_centers
    '''

    Change the following line to define properly the background interval. In this case
    we are using -50s to t0-5s and t0+20s to t0+150s for background fitting.

    '''
    mask = ((x >= t0 - 50) & (x < t0 - 5)) | ((x > t0 + 20) & (x <= t0 + 150))

    x_fit = x[mask]
    counts_fit = counts[mask]
    # Normalize counts by the median of the pre-t0 region for stability
    x0 = x_fit - t0
    norm_val = np.median(counts_fit)
    if norm_val == 0:
        norm_val = 1  # Prevent division by zero
    y = counts_fit / norm_val


    p0 = [0, 0, 0, np.mean(y)]
    popt, _ = curve_fit(poly3_func, x0, y, p0=p0)

    
    # Prepare data for MCMC: x0, y, and errors
    yerr = np.sqrt(np.abs(y))
    yerr[yerr == 0] = 1.0  # Avoid zero errors

    def log_prior(theta):
        a, b, c, d = theta
        # Wide but reasonable priors
        if popt[0]-10 < a < popt[0]+10 and popt[1]-10 < b < popt[1]+10 and popt[2]-10 < c < popt[2]+10 and 0.1 * popt[3] < d < 10 * popt[3]:
            return 0.0
        return -np.inf

    def log_likelihood(theta, x, y):
        # Poisson log-likelihood for counts data
        model = poly3_func(x, *theta)
        model = np.clip(model, 1e-6, None)  # Avoid log(0)
        # Scale back to original counts
        model_counts = model * norm_val
        y_counts = y * norm_val
        # Use gammaln for log-factorial
        return np.sum(y_counts * np.log(model_counts) - model_counts - gammaln(y_counts + 1))

    def log_probability(theta, x, y):
        lp = log_prior(theta)
        if not np.isfinite(lp):
            return -np.inf
        return lp + log_likelihood(theta, x, y)

    ndim, nwalkers = 4, 32
    # Initial guess near least squares
    p0_ls, _ = curve_fit(poly3_func, x0, y, p0=[0, 0, 0, np.mean(y)])
    p0 = p0_ls + 1e-4 * np.random.randn(nwalkers, ndim)

    ncores = multiprocessing.cpu_count()
    sampler = emcee.EnsembleSampler(nwalkers, ndim, log_probability, args=(x0, y), threads=ncores)
    # Add a progress bar using tqdm
    try:
        nsteps = 1000 # Number of MCMC steps, not so many to avoid long runtimes
        for _ in tqdm(range(nsteps), desc="MCMC sampling"):
            sampler.run_mcmc(p0, 1, progress=False)
            p0 = sampler.get_last_sample().coords
    except ImportError:
        # Fallback to normal run if tqdm is not available
        sampler.run_mcmc(p0, 1000, progress=True)
    samples = sampler.get_chain(discard=200, flat=True)

    # Best fit: parameters with maximum posterior likelihood
    log_probs = np.array([log_probability(theta, x0, y) for theta in samples])
    best_params = samples[np.argmax(log_probs)]

    print( f'Best fit mcmc parameters: {best_params}' )

    return best_params, samples, norm_val
Requirement already satisfied: emcee in /Users/sjs8171/opt/anaconda3/envs/bat/lib/python3.10/site-packages (3.1.6)
Requirement already satisfied: numpy in /Users/sjs8171/opt/anaconda3/envs/bat/lib/python3.10/site-packages (from emcee) (2.0.1)

Search of time seeds for the analysis#

For different time bins, we compute the signal to noise ratio (SNR), knowing the level of background. The formula for each time bin i is

\[ SNR_i = \frac{C_i - B_i}{\sqrt{C_i + B_i}} \]
from matplotlib.patches import Patch

def find_seeds(bin_centers, counts, model_bkg, counts_sub, bin_size_ms, t0, workdir, samples, norm_val):

    plt.figure(figsize=(15, 5))
    x = bin_centers - t0
    errors = np.sqrt(counts)

    # Find indices where the gap is large
    gaps = np.where(np.diff(x) > 1.1*bin_size_ms/1000)[0]

    # Split indices into contiguous segments
    segments = np.split(np.arange(len(x)), gaps + 1)

    # Plot non-background-subtracted light curve (step only)
    plt.figure(figsize=(10, 5))
    x = bin_centers - t0
    for seg in segments:
        plt.step(x[seg], counts[seg], where='mid', color='black', alpha=0.6)


    # Plot 90% confidence band for background model using MCMC samples
    n_samples = 1000
    if samples.shape[0] > n_samples:
        idx = np.random.choice(samples.shape[0], n_samples, replace=False)
        sample_subset = samples[idx]
    else:
        sample_subset = samples

    model_curves = np.array([model(bin_centers, t0, params, norm_val) for params in sample_subset])
    lower = np.percentile(model_curves, 5, axis=0)
    upper = np.percentile(model_curves, 95, axis=0)
    plt.fill_between(x, lower, upper, color='red', alpha=0.2)
    plt.plot(x, model_bkg, color='red', linestyle='-', linewidth=2, label='Background model')
    plt.axvline(0, color='gray', linestyle='--', label='t0')
    plt.title(f"Raw Light Curve (binning: {bin_size_ms} ms)")
    plt.xlabel("Time [s] (t - t0)")
    plt.ylabel("Counts per bin")
    plt.legend()
    plt.grid(True)
    tmax = 150
    plt.xlim(-50, tmax)
    mask = (x >= -50) & (x <= tmax)
    ymin, ymax = counts[mask].min(), counts[mask].max()
    yrange = ymax - ymin
    plt.ylim(ymin - 0.1 * yrange, ymax + 0.1 * yrange)
    plt.tight_layout()
    plt.show()
    plt.savefig(os.path.join(workdir, f"light_curve_raw_{bin_size_ms}ms.png"), dpi=300)
    plt.close()

    plt.figure(figsize=(15, 5))

    for seg in segments:
        plt.step(x[seg], counts_sub[seg], where='mid', color='blue', alpha=0.6)


    # Overlay error bars
    plt.errorbar(x, counts_sub, yerr=errors, fmt='o', color='blue', alpha=0.6, label='BKG-subtracted', markersize=3, capsize=2)

    snr = np.zeros_like(counts_sub, dtype=float)

    mask = (x < -5) | (x > 20)
    if np.any(mask):
        bkg_std = np.std(counts_sub[mask])
    else:
        bkg_std = np.std(counts_sub)


    with np.errstate(divide='ignore', invalid='ignore'):
        snr = counts_sub / (model_bkg + counts)**0.5
        snr[snr < 0] = 0

    print(f'Max SNR: {snr.max()}')
    # Normalize SNR for colormap (clip at 0, max in window for visualization)
    snr_max = snr.max() 
    snr_clipped = np.clip(snr, 0, snr_max)
    cmap = plt.get_cmap('GnBu')
    norm = plt.Normalize(0, snr_max)

    # Prepare to add legend for hatching

    hatch_legend_added = False
    double_hatch_legend_added = False
    for i, xc in enumerate(x):
        color = cmap(norm(snr_clipped[i]))
        if snr[i] > 3.5:
            plt.axvspan(
                xc - bin_size_ms/2000.0,
                xc + bin_size_ms/2000.0,
                color=color,
                alpha=0.3,
                zorder=1,
                label='SNR > 3.5' if not hatch_legend_added else None
            )
            hatch_legend_added = True
        # No shading for SNR <= 3.5

    # Add custom legend handles for hatching if not present
    handles, labels = plt.gca().get_legend_handles_labels()
    if 'SNR > 3.5' not in labels:
        handles.append(Patch(facecolor='none', edgecolor='black', hatch='/', label='SNR > 3.5'))
    plt.legend(handles=handles, labels=[h.get_label() for h in handles])

    # Add color bar for SNR
    sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
    sm.set_array([])
    ax = plt.gca()
    cbar = plt.colorbar(sm, pad=0.01, ax=ax, alpha=0.3)
    cbar.set_label('SNR')

    # Highlight spikes
    # for spike_center in bin_centers[spikes[0]]:
    #     spike_x = spike_center - t0
    #     plt.axvspan(spike_x - bin_size_ms/2000.0, spike_x + bin_size_ms/2000.0, color='red', alpha=0.3, label='Spikes' if 'Spikes' not in plt.gca().get_legend_handles_labels()[1] else None, zorder=4)

    plt.title(f"Light Curve (binning: {bin_size_ms} ms)")
    plt.xlabel("Time [s] (t - t0)")
    plt.ylabel("Counts per bin")
    
    plt.axhline(0, color='gray', linestyle=':', linewidth=1)

    max_snr_val = snr.max()
    max_snr_idx = np.where(snr == max_snr_val)[0][0]
    t_star = bin_centers[max_snr_idx] - t0  # Time of maximum SNR

    window = min(45, 30 * bin_size_ms / 1000.0)  # Set window size based on bin size
    plt.xlim(t_star - window, t_star + window)
    mask = (x >= -window) & (x <= window)

    # Add horizontal bands for 3.5 and 5 sigma of background

    plt.axhspan(-3.5 * bkg_std, 3.5 * bkg_std, color='blue', alpha=0.1)
    plt.axhspan(-5 * bkg_std, 5 * bkg_std, color='blue', alpha=0.1, label='3.5σ and 5σ')
    plt.autoscale(axis='y')
    plt.legend()
    plt.tight_layout()
    plt.show()
    plt.savefig(os.path.join(workdir, f"light_curve_{bin_size_ms}ms.png"), dpi=300)


    if max_snr_val > 4.0:
        return bin_centers[max_snr_idx] - t0, max_snr_val
    return None, None
def cust_seeds(t0, workdir):

    filename = os.path.join(workdir, 'filter_evdata.fits')
    #filename = str(event.event_files) # uncomment this to check what happens with the non-filtered event file
    event_times, gti_start, gti_stop = load_event_times(filename)

    snr_max = 0
    seed_max = None
    seeds = []


    for bin_size_ms in [64, 512, 1024, 4096]:

        try:
            bin_size = bin_size_ms / 1000.0
            bin_centers, counts = bin_light_curve(event_times, bin_size, gti_start, gti_stop, t0)
            if len(bin_centers) < 20:
                logging.info(f'Not enough bins for bin size {bin_size_ms} ms, skipping...')
                continue

            best_params, samples, norm_val  = fit_background_linear(bin_centers, counts, t0)
            model_bkg = model(bin_centers, t0, best_params, norm_val)
            
            counts_sub = counts - model_bkg

            # Detect spikes
            time_bin, snr_bin = find_seeds(bin_centers, counts, model_bkg, counts_sub, bin_size_ms, t0, workdir, samples, norm_val)
            print(f'Bin size: {bin_size_ms} ms, Seed time: {time_bin}, SNR: {snr_bin}', flush=True)
            if time_bin is not None or snr_bin is not None:
                if snr_bin > 4.0:
                    seeds.append([time_bin, bin_size_ms, snr_bin])
                if snr_bin > snr_max:
                    seed_max, dur_max, snr_max = time_bin, bin_size_ms, snr_bin
        except Exception as e:
            # logging.error(f'Error processing bin size {bin_size_ms} ms: {traceback.format_exc()}')
            continue
    print(f'Max SNR: {snr_max} found at time {seed_max} with duration {dur_max} ms', flush=True)
cust_seeds(t0, workdir)
MCMC sampling: 100%|██████████| 1000/1000 [00:04<00:00, 211.09it/s]
Best fit mcmc parameters: [ 8.70321759e-09 -3.79619004e-06  2.88329446e-04  1.00546461e+00]
<Figure size 1500x500 with 0 Axes>
../_images/f7188524d7016c27489db3f3550193a5294e80eed25f9c7b1d94e605818dcdbf.png
Max SNR: 7.474911389159172
../_images/f281868dfc87f122ff40c33fb4863641f166f7b19ba3d24e83914c1564c91c24.png
Bin size: 64 ms, Seed time: 0.20800817012786865, SNR: 7.474911389159172
MCMC sampling: 100%|██████████| 1000/1000 [00:01<00:00, 663.36it/s]
Best fit mcmc parameters: [ 8.60277131e-09 -3.72088249e-06  2.83410358e-04  1.00305922e+00]
<Figure size 640x480 with 0 Axes>
<Figure size 1500x500 with 0 Axes>
../_images/1015546c6fd9665358e9ad2c179cdf42b5c19a5939974bd7ff8016188ba78250.png
Max SNR: 15.390259838136728
../_images/f3a37061f990b3cf08454933b407a006cff4930a0bae7813baef4858b98d44ff.png
Bin size: 512 ms, Seed time: -0.08000338077545166, SNR: 15.390259838136728
MCMC sampling: 100%|██████████| 1000/1000 [00:01<00:00, 773.26it/s]
Best fit mcmc parameters: [ 1.04304029e-08 -4.05552801e-06  2.88254274e-04  1.00340674e+00]
<Figure size 640x480 with 0 Axes>
<Figure size 1500x500 with 0 Axes>
../_images/f9dcd6497f6a54d7b6ea1ab3d38343760b376ed7ce3851d08e48cfb9f8a64092.png
Max SNR: 17.82672006365474
../_images/0f20c2e8c019992133466d2865a9a33ea634775875cc54932273f96f2262fbcf.png
Bin size: 1024 ms, Seed time: 0.688002347946167, SNR: 17.82672006365474
MCMC sampling: 100%|██████████| 1000/1000 [00:01<00:00, 968.83it/s]
Best fit mcmc parameters: [ 1.78614674e-08 -5.43397999e-06  3.37843729e-04  1.00293172e+00]
<Figure size 640x480 with 0 Axes>
<Figure size 1500x500 with 0 Axes>
../_images/edc35e0988f9bc123174ab6ac80e1b7ccc42b7af24e20bc98594117e130c7523.png
Max SNR: 18.136174223779157
../_images/67694e4ebc8a3682b7e5bda4224851e8b6b1ec04c0aac3f2979d74c85591b19b.png
Bin size: 4096 ms, Seed time: 1.199999451637268, SNR: 18.136174223779157
Max SNR: 18.136174223779157 found at time 1.199999451637268 with duration 4096 ms
<Figure size 640x480 with 0 Axes>

Note

Try to see what happens if you load the unfiltered event data

Imaging#

time_bins = [0, 4] * u.s
energybins = [15, 350] * u.keV

settled_skyview = event.create_skyview(timebins=time_bins, energybins=energybins, is_relative=True, T0=t0, input_dict=dict(aperture="CALDB:DETECTION"))

detected_sources = settled_skyview.detect_sources(input_dict=dict(signifmap=settled_skyview.snr_img_file,bkgvarmap=settled_skyview.bkg_stddev_img_file,clobber="yes", pcodethresh=0.01))
detected_sources
QTable length=194
CATNUMNAMESKYCOORDALWAYS_CLEANTIMETIME_STOPEXPOSUREIMXIMYIMX_ERRIMY_ERRIMXPIXIMYPIXRA_OBJ_ERRDEC_OBJ_ERRERR_RADIMX_FWHMIMY_FWHMIMX_FHWM_ERRIMY_FHWM_ERRTHETAPHIGRMCLONGRMCLATCOUNTSCOUNTS_ERRCENT_COUNTSCONTAM_COUNTSBKGBKG_ERRBKG_VARBKG_CELLBKG_FITSNRCENT_SNRNPIXSOUNPIXBKGDETECT_METHODCHI2DOFCHI2_NUDETECT_STATUSCONSTRAINT_FLAGSRC_WINDOW_RADBKG_WINDOW_RADNGOODPIXBAT_ZOBJPCODEFRRA_PER_IMXDEC_PER_IMXRA_PER_IMYDEC_PER_IMYGAINAPPCLOCKAPPBTOFFAPPFFAPPNFAPPPCODEAPPNGPIXAPPMSKWTAPPBDISTAPPBACKAPPACOLAPP
deg,deg
int16str30SkyCoordboolfloat64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64int32int32int32float64float64float64int32int32int32int32int32float64float64float64float64float64float64int32int32int32int32int32int32int32int32int32int32int32
-1UNKNOWN66.23074895013434,-49.760066496491035False752515599.141511752515603.1415114.0-0.072078409670204330.78106490264511960.000311713038639422640.00031135282483172683913.9975665071069848.36455518731550.0115372764202113670.0178598409547866930.0178598409547866930.0065298002749940750.0065298002749940750.00.038.11005659289098-95.27244930084947-37.99214614802073-3.25117244972243520.64703087209017480.068814691649429420.6002906628697119-0.00702894411971192-0.007028944119711920.0232716891288279260.05979296192526817-0.006419851910322905-0.0070289441197119210.82118783309082310.039486982096339727077123.29484663117101109.01.1311453819373487086301692310000000.00.193359375-57.91262162802013-25.02504774071212528.554343060704923-30.40594477710101310110111001
502MRK 15012.6292005426738214,10.974901288746368False752515599.141511752515603.1415114.00.7068158670014243-0.87983250195057740.00.0543.146878017689257.867078424140860.00.00.00.0065298002749940750.0065298002749940750.00.048.4568472805725151.22324970171959541.34236822028129427.9531176477466370.384084258847721350.147573593209386870.3842816365712086-0.042484761571208604-0.0424847615712086040.049827027220217930.12861254811286926-0.009188631549477577-0.0424847615712086042.98636691740725272.98790158666296200271.34154286648669111.00.6427166024007810116301692310000000.00.03515625-30.241562008370693-15.6578954602082382.955561129169837-30.72192625452885310110111001
5418SWIFT J0517.3+013479.33499451553868,1.0541638545045076False752515599.141511752515603.1415114.0-0.838911304243943-0.050492189565463520.00.01278.8249313907927452.61055331348030.00.00.00.0065298002749940750.0065298002749940750.00.040.04468216764138176.55565067387782.890534593001187-39.957754987195090.085662617523259460.039467625404115020.08946417035670423-0.03160376930739844-0.0316028422317042240.0139248370357912160.03374343365430832-0.0011487625306472182-0.0316028422317042242.53864554511103122.6513060666332504002290.5891473543328297.00.97841463755667610116301692310000000.00.5546875-30.87813226443359-13.313779247292618.65460194494517-39.6630072339032610110111001
51332MASX J23444387-4243124356.1828598974576,-42.720109725462756False752515599.141511752515603.1415114.00.9923824816902640.44588798598907420.00.0407.255259270126689.01230136644440.00.00.00.0065298002749940750.0065298002749940750.00.047.41207136023298-24.194889703795283-24.0315181021089542.188039870465610.089476165983301830.051452573624388020.10196625513785934-0.03458344263785934-0.034583442637859340.0173215888254859460.04501110687851906-0.0038620559498667717-0.034583442637859341.98786859929461352.265357646348425300264.60004459507276111.00.58198238373939430116301692310000000.00.33203125-37.782871913805147.4045938092126561.522435240985942-36.9490621494605910110111001
219ESO 297-01824.65479914825102,-40.01140090305095False752515599.141511752515603.1415114.00.409475380453472760.34764187008334710.00.0684.7486669677043642.15682273142170.00.00.00.0065298002749940750.0065298002749940750.00.028.242254982179052-40.331035183927185-19.16959177888486521.1449894771916820.054514658318857870.0290402514325186870.05645431672841234-0.008358613603412344-0.0083586136034123440.0095217927552010070.025880930945277214-0.0035148453898727894-0.0083586136034123442.10636388753263852.181309352734632002123.76414580128326111.01.11499230451606550116301692310000000.00.7734375-61.0441889854485-5.49433099905627415.0176568247673-46.6525130491390910110111001
8008IGR J03184-001449.62044468157205,-0.256430046969631False752515599.141511752515603.1415114.0-0.21254332228421205-0.23700952358211230.00.0980.8288376704849363.855781860796360.00.00.00.0065298002749940750.0065298002749940750.00.017.658992966679474131.8848411672501613.333613787888222-11.684830622717340.054708812629424170.0254537714879596350.0474891175259361-0.009891461275936096-0.0098914612759360960.0085167447655101910.02243804745376110.00019182937103323638-0.0098914612759360962.43821628161561772.116454991183999002267.9780937119124222.01.20710853023383960116301692310000000.00.921875-50.60102765241936-17.26370291704748519.56743660258553-49.4550700449458710110111001
487PKS 2300-18345.7620032232108,-18.68999314769623False752515599.141511752515603.1415114.01.376488667485734-0.189636040378052730.00.0224.35959434440474386.53765169064480.00.00.00.0065298002749940750.0065298002749940750.00.054.257870960815387.84414425255213310.73783897208945453.519530529437840.141891921660190430.086080506837019610.1486413281796237-0.01582882817962369-0.015828828179623690.0279302437880966230.077507689595222470.005154812708497047-0.015828828179623691.83068186397000491.91776234017412700290.00939035631244111.00.81089540861542740116301692310000000.00.1240234375-20.896188345886912-2.0365432860103283-1.7124808096014021-33.22308086239910110111001
5151CGCG 459-05822.101651590673022,16.459273879202687False752515599.141511752515603.1415114.00.19010670028381013-0.78504070479229290.00.0789.1413202153545102.981809885324760.00.00.00.0065298002749940750.0065298002749940750.00.038.92888731089533476.387219891462438.133356647153458.5046124789304170.100318682750933740.068296221634146670.111326259456814640.077150303043185360.077150303043185360.0236422488563870460.0585518851876258850.0113300224766135220.077150303043185361.71332968066645061.9013266455909412002321.0679712453431269.01.19356123139532740116301692310000000.00.1484375-42.581080741399546-16.6014317564133769.835899479782029-34.01080813958401410110111001
2502MASX J04440903+281300371.03759935132192,28.21680095976791False752515599.141511752515603.1415114.0-1.0087325456397362-0.82241653285194690.00.01359.7469798231285.238916503064960.00.00.00.0065298002749940750.0065298002749940750.00.052.46339993635712140.809745227084839.43444407539414-37.922029946977580.34005143840112880.21188505855464740.3591105463383969-0.1442667963383969-0.14426679633839690.068687290438755090.19018673896789550.0005814690957777202-0.14426679633839691.78798711333144621.8881997151179746002251.68511127494344222.01.13371671745470030116301692310000000.00.0224609375-31.2314740540387-0.979175830195799816.54372886150668-26.46002781148126810110111001
.............................................................................................................................................................................................
5041ESO 359- G 01961.25695898305532,-37.18750041203201False752515599.141511752515603.1415114.0-0.13090665142846240.47366074737264170.00.0941.9962679343682702.0849957006740.00.00.00.0065298002749940750.0065298002749940750.00.026.17030453241633-105.44931786433577-25.34507795319189-6.747093885358727-0.059869245156643350.03515817788457934-0.050681484203497410.00307406232849741270.00307406232849741270.0121001899097387830.0302517972886562350.00165441504213958980.0030740623284974127-1.9790310170792071-1.6753214270182177002129.29288072580061111.01.1648007272594650116301692310000000.00.609375-55.93875949472249-24.93524674207492425.651326247100315-41.8184309722846610110111001
5238NGC 67827.35359973379599,21.99729978853679False752515599.141511752515603.1415114.00.043450572203574844-0.88430606867175680.00.0858.970661820211155.7410153979943740.00.00.00.0065298002749940750.0065298002749940750.00.041.5208078486258487.1870216424070241.486524581017561.864280766506127-0.137442046699605940.11560739223382636-0.16070374139828240.011214452214439410.046445928898282390.0461205571606033360.09591926634311676-0.0060220309533178810.04644592889828239-1.4328930145061403-1.6754062820228672002209.18485957370487246.00.85034495761668650116301692310000000.00.0732421875-43.25820188455509-15.15150609776583211.315070917561343-30.38881580863517210110111001
542NGC 106840.66959961084038,-0.013299973184957315False752515599.141511752515603.1415114.0-0.05972016869757972-0.295186316301217330.00.0908.0785831598702336.16312722209520.00.00.00.0065298002749940750.0065298002749940750.00.016.760558897017074101.4373296357759216.445879236014775-3.2781413767838234-0.039421541920219450.025410792107972462-0.04314799303299720.00725932115799720.00725932115799720.0086362085954825010.0221140887588262560.0012026709737256170.0072593211579972-1.782643741288475-1.95115401333350500283.36217868218715111.00.75101061875844280116301692310000000.00.8828125-51.96846801425181-17.298581536576517.460751976727806-49.643511269932910110111001
590SWIFT J0525.3-460081.32699850398288,-46.00499813492253False752515599.141511752515603.1415114.0-0.319033415843146650.88810777174317360.00.01031.500120171665899.2607126613570.00.00.00.0065298002749940750.0065298002749940750.00.043.340035864449185-109.75972275246546-41.60852872723626-13.416667961831457-0.12448747922976740.11957203235850877-0.207106258763418650.0186296962634186630.0186296962634186630.03969720166827460.10453096777200699-0.0104528749361634250.018629696263418663-1.190914825368188-1.9812909339473364002172.33196211649388222.00.77627009962384630116301692310000000.00.0654296875-40.87364898452961-28.92991506797670825.54107689434204-26.40574493995018210110111001
5027SWIFT J0040.3-064710.068709551388778,-6.775442588175119False752515599.141511752515603.1415114.00.5605972248349094-0.35512651260882340.00.0612.742033602633307.628785182203960.00.00.00.0065298002749940750.0065298002749940750.00.033.5688252513171932.3533957275981319.5512994389547327.846349039927816-0.056219636974087980.03345950205698969-0.061387030203214590.0220803895782145930.0220803895782145930.011157509732720320.0293936710804700850.00058928626822307710.022080389578214593-1.9126442838724476-2.0884438025844863002199.6686554657367111.01.79881671590753770116301692310000000.00.5703125-40.59946567025463-12.5089119684673736.247715838014756-45.1782452834947910110111001
378NGC 061223.490599052688015,-36.49330053148277False752515599.141511752515603.1415114.00.41091557509191210.27356050800368120.00.0684.0556399439132606.90334902751110.00.00.00.0065298002749940750.0065298002749940750.00.026.273093122547618-33.65305987814713-15.29954502527660621.621023899669733-0.0621151082059423350.0276119123949452-0.05546948147207330.00151440334707329560.00151440334707329560.0094895041778793370.023800687864422798-0.0040613836608827110.0015144033470732956-2.609803067868128-2.330583123817565002100.03803815963528111.00.90124358702374120116301692310000000.00.8828125-58.91699262861039-6.14546347321720514.093207230558846-48.5012769502191110110111001
51062MASS J01053879-141613516.411700477600235,-14.537897565069636False752515599.141511752515603.1415114.00.4494070503995551-0.165277150820075660.00.0665.6889048280099398.015485794323350.00.00.00.0065298002749940750.0065298002749940750.00.025.58674430957170620.1918642813098589.38484311688237423.91210975423364-0.061172871993544590.02780372495336762-0.056530448394442910.0120968546444429150.0120968546444429150.0094227183165797090.0241887699812650680.00184843083843588830.012096854644442915-2.5289782010794606-2.337053452417276600281.05629695299899111.00.73023690948647730116301692310000000.00.859375-47.36147137762714-11.4007294467199939.286922576734469-50.3025247748922810110111001
5253SWIFT J0034.6-04228.651792647590732,-4.374386716546788False752515599.141511752515603.1415114.00.5870589685251432-0.416958686732676660.00.0600.1414327289789278.19077995340660.00.00.00.0065298002749940750.0065298002749940750.00.035.7563182870374835.3842775114276622.63411991064168728.450801064091827-0.083526429561065610.035163738723386316-0.091495887194431430.0128826059444314240.0128826059444314240.0117805375257603330.03095262683928013-0.0023956466466188430.012882605944431424-2.6985247486351374-2.955997488339809002107.03223134691139111.00.96425433645866110116301692310000000.00.4765625-38.86882304016126-13.008535645120395.590661352528059-43.39690567700671410110111001
867MCG -07-48-021356.6831665542768,-38.58927520460067False752515599.141511752515603.1415114.00.97418000694586370.33376655761850570.00.0415.9090534467148635.64293752438410.00.00.00.0065298002749940750.0065298002749940750.00.045.84028638009655-18.912117184347586-18.4572856495441842.74003894351432-0.133376044324856360.04291208548041765-0.115711053056736860.0104864436817368610.0104864436817368610.0140245740057511680.03833043947815895-0.00105559639632701870.010486443681736861-3.4796377537193464-3.01877710331680700264.77625836962018111.00.58356989522180350116301692310000000.00.41796875-36.835668765888845.5157946761141831.6625057810415456-38.7998936730582510110111001

This is just a function to read results

def log_imaging(detected_sources):
    # Print first 10 rows of detected_sources with selected columns if it's an astropy QTable
    try:
        # Prepare table header and rows with custom variable names to avoid conflicts
        # Print detected_sources as a nicely aligned table (first 10 rows)
        _table_header = "{:<3} {:<16} {:>10} {:>10} {:>8}  {:>9}".format(
            "Idx", "NAME", "RA", "DEC", "SNR", "PCODEFR"
        )
        _table_rows = []
        _ra_unknown = None
        _dec_unknown = None
        
        for _idx, _src in enumerate(detected_sources[:10]):
            _name = str(_src['NAME'])[:15]  # Truncate name if too long
            _ra = f"{_src['SKYCOORD'].ra.deg:10.3f}"
            _dec = f"{_src['SKYCOORD'].dec.deg:10.3f}"
            _snr = f"{_src['SNR']:8.3f}"
            _pcodefr = f"{_src['PCODEFR']:10.3f}"
            _table_rows.append(
                "{:<3} {:<16} {} {} {} {}".format(
                    _idx, _name, _ra, _dec, _snr, _pcodefr
                )
            )
 
            # Check if this is the UNKNOWN source
            if 'UNKNOWN' in str(_src['NAME']) :
                print("Found UNKNOWN source.")
                _ra_unknown = _src['SKYCOORD'].ra.deg
                _dec_unknown = _src['SKYCOORD'].dec.deg
        
        _table_str = "\n".join([_table_header] + _table_rows)
        logging.info("Detected sources (first 10):\n" + _table_str)
        
        return _ra_unknown, _dec_unknown
        
    except Exception as _exc:
        logging.error(f"Error printing detected_sources: {traceback.format_exc()}")
        return None, None
ra_source, dec_source = log_imaging(detected_sources)
2025-12-18 14:12:13,106 - INFO - Detected sources (first 10):
Idx NAME                     RA        DEC      SNR    PCODEFR
0   UNKNOWN              66.231    -49.760   10.821      0.193
1   MRK 1501              2.629     10.975    2.986      0.035
2   SWIFT J0517.3+0      79.335      1.054    2.539      0.555
3   2MASX J23444387     356.183    -42.720    1.988      0.332
4   ESO 297-018          24.655    -40.011    2.106      0.773
5   IGR J03184-0014      49.620     -0.256    2.438      0.922
6   PKS 2300-18         345.762    -18.690    1.831      0.124
7   CGCG 459-058         22.102     16.459    1.713      0.148
8   2MASX J04440903      71.038     28.217    1.788      0.022
9   2MASX J01064523      16.689      6.634    1.594      0.348
Found UNKNOWN source.

What happens if we create an image with a time interval much larger than GRB duration

time_bins = [-0.5, 1] * u.s
energybins = [15, 350] * u.keV

settled_skyview = event.create_skyview(timebins=time_bins, energybins=energybins, is_relative=True, T0=t0, input_dict=dict(aperture="CALDB:DETECTION"))

detected_sources = settled_skyview.detect_sources(input_dict=dict(signifmap=settled_skyview.snr_img_file,bkgvarmap=settled_skyview.bkg_stddev_img_file,clobber="yes", pcodethresh=0.01))

log_imaging(detected_sources)
2025-12-18 14:13:51,601 - INFO - Detected sources (first 10):
Idx NAME                     RA        DEC      SNR    PCODEFR
0   UNKNOWN              66.231    -49.754   14.652      0.193
1   MRK 1501              2.629     10.975    2.386      0.035
2   SWIFT J0336.8-2      54.200    -25.243    2.192      0.992
3   SWIFT J0245.2-0      41.300     -3.817    2.755      0.977
4   CGCG 468-002 NE      77.082     17.363    2.004      0.176
5   PKS 2300-18         345.762    -18.690    2.359      0.124
6   NGC 788              30.277     -6.815    2.016      0.922
7   XY Ari               44.037     19.441    2.044      0.277
8   Mrk 1018             31.567     -0.291    1.747      0.789
9   ESO 297-018          24.655    -40.011    1.602      0.773
Found UNKNOWN source.
(np.float64(66.23076195663347), np.float64(-49.75352693756179))

Note

SNR is lower when you include more background in the image. The choice of the right time bin becomes relevant when the source is barely above the background level

Plotting#

from astropy.modeling.models import Gaussian1D

def map_imaging(ra_s, dec_s, workdir, skyview):

    fig = plt.figure()

    ax_zoom_rect = plt.axes([-1.2, 0.0, 0.9, 0.9],projection='astro degrees zoom',
                center=f'{ra_s}d {dec_s}d', radius='2 deg')
    
    ax_globe = plt.axes(
        [-0.3, -0.05, 1.25, 1.25],
        projection='astro degrees mollweide',
        )
    ax_hist = fig.add_axes([1.1, 0.2, 0.5, 0.8])  # [left, bottom, width, height]


    t = skyview.snr_img # loading the sky image already done outside

    hp_proj = t.healpix_projection(coordsys="icrs", nside=1024) # projecting in healpix (from detector coords to sky coords)

    '''
    The hp_proj object contains the healpix projection of the sky image.
    It is an array that contains [time, sky pixel, energy band]. 
    Since we created a single sky image for a single time bin and a single energy band,
    we can extract the first time and first energy band to get the sky map.
    '''

    snr_map = hp_proj.contents[0,:,0]
    ax_zoom_rect.imshow_hpx((snr_map, 'ICRS'),  cmap="magma", alpha=1.0, zorder=1)

    '''
    We load again the partial coding image to overplot the contours
    '''

    plot_quantity_pc = skyview.pcode_img.healpix_projection(coordsys="icrs", nside=1024).contents[0,:,0]

    cs = ax_globe.contour_hpx((plot_quantity_pc, 'ICRS'), nested=False, colors='red', levels=[0.01,0.3,0.8], linewidths=1, zorder=2)

    '''
    We finally create the histogram of the SNR values
    '''

    h = skyview.snr_img.contents.flatten()
    val, _, _=plt.hist(h, bins=100, alpha=0.8, color='blue', label='SNR histogram', histtype='step')
    
    max_snr_value = np.nanmax(h)
    min_snr_value = np.nanmin(h)
    ax_hist.axvline(max_snr_value, color='red', linestyle='--', label=f'Max SNR: {max_snr_value:.2f}')

    g = Gaussian1D(amplitude=val.max(), stddev=1)
    x = np.arange(-30, 30, .01)
    ax_hist.plot(x, g(x), 'k-')

    # Here some aestethic adjustments

    ax_zoom_rect.coords[0].set_major_formatter('d.d')  # 3 decimal degrees
    ax_zoom_rect.coords[1].set_major_formatter('d.d')  # 3 decimal degrees
    ax_zoom_rect.grid()
    vmin= np.nanmin(snr_map)
    vmax=np.nanmax(snr_map)
    cmap_custom = plt.cm.inferno
    norm = plt.Normalize(vmin=vmin, vmax=vmax)
    sm = plt.cm.ScalarMappable(cmap=cmap_custom, norm=norm)
    cbar = plt.colorbar(sm, ax=ax_zoom_rect, shrink=0.5, orientation='horizontal', aspect=30, pad=0.15)
    cbar.mappable.set_clim(vmin=vmin,vmax=vmax)
    cbar.set_label('SNR')

    ax_globe.grid()
    ax_globe.mark_inset_axes(ax_zoom_rect)
    ax_globe.connect_inset_axes(ax_zoom_rect, 'upper right')
    ax_globe.connect_inset_axes(ax_zoom_rect, 'lower right')


    ax_hist.set_xlabel('')
    ax_hist.set_ylabel('Counts')
    ax_hist.set_ylabel('SNR')
    ax_hist.set_yscale('log')
    ax_hist.yaxis.tick_right()
    ax_hist.yaxis.set_label_position("right")
    ax_hist.set_ylim([0.1,2*val.max()])
    ax_hist.set_xlim([min_snr_value-1,max_snr_value+1])
    ax_hist.legend(
        loc='upper right')
    
    # If we want to implement at some point the ext localization
    fit_file = next((fname for fname in os.listdir(workdir) if 'ext_loc' in fname), None)
    if fit_file:
        loc_handles = map_ext(fit_file, workdir, ax_globe)        
        leg_ext = ax_globe.legend(handles=loc_handles, loc='lower left', frameon=True, bbox_to_anchor=(-0.1, -0.15), borderaxespad=0.5)
        ax_globe.add_artist(leg_ext)

    ax_globe.clabel(cs, inline=True, fmt='%1.2f', fontsize=8)
    red_line = [Line2D([0], [0], color='red', lw=1, label='Partial coding')]

    ax_leg_bot = fig.add_axes([0.76, 0.15, 0.22, 0.28])
    ax_leg_bot.axis('off')

    fov_legend = ax_leg_bot.legend(handles=red_line, loc='lower left', frameon=True, borderaxespad=0.5)
    ax_leg_bot.add_artist(fov_legend)

    plt.savefig(os.path.join(workdir,'map_imaging.png'), bbox_inches='tight', dpi=300)
map_imaging(ra_source, dec_source, workdir, settled_skyview)
../_images/6b051e884ee95587d50ad15401012e2ea7ff07b20100a39acad6d121ea955c21.png

Looking at the DPI#

from mpl_toolkits.axes_grid1 import make_axes_locatable


def plot_dpi(self, mask_value = None, emin=None, emax=None, tmin=None, tmax=None, plot_rate=False):
        """
        This method allows the user to conveniently plot the histogram for a single energy bin and time interval.
        Any detectors with 0 counts (due to detectors being off or due to there being no detectors in the specified
        DETX and DETY coordinates) are blacked out.

        By default, the histogram is binned along the energy and time axes. This behavior can be changed by specifying
        emin/emax and/or tmin/tmax. These values should all exist in the ebins and tbins attributes.

        :param emin: None or an astropy Quantity array of the beginning of the energy bins
        :param emax: None or an astropy Quantity array of the end of the energy bins
        :param tmin: None or an astropy Quantity array of the starting time bin edges that the histogram will be
            rebinned into
        :param tmax: None or an astropy Quantity array of the end time bin edges that the histogram will be
            rebinned into
        :param plot_rate: Boolean to denote if the count rate should be plotted. The histogram gets divided by the
            exposure time of the plotted histogram
        :return: matplotlib figure and axis for the plotted histogram
        """

        if emin is None and emax is None:
            plot_emin = self.ebins["E_MIN"].min()
            plot_emax = self.ebins["E_MAX"].max()
        elif emin is not None and emax is not None:
            plot_emin = emin
            plot_emax = emax
        else:
            raise ValueError(
                "emin and emax must either both be None or both be specified."
            )
        plot_e_idx = np.where(
            (self.ebins["E_MIN"] >= plot_emin) & (self.ebins["E_MAX"] <= plot_emax)
        )[0]

        if tmin is None and tmax is None:
            plot_tmin = self.tbins["TIME_START"].min()
            plot_tmax = self.tbins["TIME_STOP"].max()
        elif tmin is not None and tmax is not None:
            plot_tmin = tmin
            plot_tmax = tmax
        else:
            raise ValueError(
                "tmin and tmax must either both be None or both be specified."
            )
        plot_t_idx = np.where(
            (self.tbins["TIME_START"] >= plot_tmin)
            & (self.tbins["TIME_STOP"] <= plot_tmax)
        )[0]

        # now start to accumulate the DPH counts based on the time and energy range that we care about
        plot_data = self.contents[plot_t_idx, :, :, :]

        if len(plot_t_idx) > 0:
            plot_data = plot_data.sum(axis=0)
        else:
            raise ValueError(
                f"There are no DPH time bins that fall between {plot_tmin} and {plot_tmax}"
            )

        plot_data = plot_data[:, :, plot_e_idx]

        if len(plot_e_idx) > 0:
            plot_data = plot_data.sum(axis=-1)
        else:
            raise ValueError(
                f"There are no DPH energy bins that fall between {plot_emin} and {plot_emax}"
            )

        if plot_rate:
            # calculate the total exposure
            exposure_tot = np.sum(self.exposure[plot_t_idx])
            plot_data /= exposure_tot

        # set any 0 count detectors to nan so they get plotted in black
        # this includes detectors that are off and "space holders" between detectors where the value is 0
        plot_data[plot_data == 0] = np.nan
        
        mask = (plot_data.value < mask_value[0]) | (plot_data.value > mask_value[1])
        plot_data[mask] = np.nan

        data = plot_data.flatten()
        data = data[data != 0]
        data = data.value

        # Create the figure with two subplots: main image and histogram
        fig, (ax, ax_hist) = plt.subplots(
            1, 2,
            figsize=(12, 5),  # wider figure
            gridspec_kw={'width_ratios': [4, 2]}  # more space for histogram
        )

        # Main image panel
        divider = make_axes_locatable(ax)
        cax = divider.append_axes("right", size="5%", pad=0.05)

        cmap = plt.colormaps.get_cmap("viridis")
        cmap.set_bad(color="k")

        im = ax.imshow(plot_data.value, origin="lower", interpolation="none", cmap=cmap)
        fig.colorbar(im, cax=cax, orientation="vertical", label=plot_data.unit)

        ax.set_ylabel("DETY")
        ax.set_xlabel("DETX")

        # Histogram panel
        # Compute bin edges so that bins are centered at 1, 2, 3, ... (integers)
        min_center = 0
        max_center = int(np.ceil(np.nanmax(data)))
        bin_centers = np.arange(min_center, max_center + 1)
        bins = bin_centers - 0.5
        bins = np.append(bins, bins[-1] + 1)
        ax_hist.hist(data, bins=bins, color="gray", histtype="step", edgecolor="black", label="Counts")
        ax_hist.set_xlabel("Counts per detector")
        ax_hist.set_yscale("log")
        ax_hist.grid(True)
        mean_val = np.nanmean(data)
        ax_hist.axvline(mean_val, color="red", linestyle="--", label="Mean = {:.2f}".format(mean_val))
        ax_hist.legend()

        plt.tight_layout(rect=[0, 0, 1, 0.8])  # Make space for suptitle

        return fig, ax
time_bins=[0,3]*u.s
energybins = [15, 350] * u.keV

event_dpi=event.create_dpi(timebins=time_bins, energybins=energybins, T0=t0, is_relative = True)

f = plot_dpi(event.dpis[-1], mask_value = [0, np.inf])
plt.show()
plt.savefig(os.path.join(workdir, 'dpi.png'), dpi=300, bbox_inches='tight')
../_images/23667e57bfe9de0064c988e70de8caba854c04bae8820c84cfa7560167f32b7d.png
<Figure size 640x480 with 0 Axes>
time_bins=[10,13]*u.s
logging.info(f'Plotting DPI plot for time bins {time_bins}')
energybins = [15, 350] * u.keV
event_dpi=event.create_dpi(timebins=time_bins, energybins=energybins, T0=t0, is_relative = True)
f = plot_dpi(event.dpis[-1], mask_value = [0, np.inf])
plt.show()
2025-12-18 12:23:41,287 - INFO - Plotting DPI plot for time bins [10. 13.] s
../_images/80c4f91d9fa41254ee04ddcb0810ac8e61776c273194fd0053038f79f3ba8cce.png

Light curve after applying mask weighting#

event.apply_mask_weighting(ra=ra_source*u.deg, dec=dec_source*u.deg)

Under the hood apply_mask_weighting does the following

input_dict = dict(
    infile=str(self.event_files),
    attitude=str(self.attitude_file),
    detmask=str(self.detector_quality_file),
    ra=ra.value,
    dec=dec.value,
    auxfile=str(temp_auxil_raytracing_file),
    clobber="YES",
)
batmaskwtevt_return = self._call_batmaskwtevt(input_dict)
def plot_lc(event, t0, deltat, workdir, pipe):

    lc = event.create_lightcurve(energybins=[15, 50, 100, 350] * u.keV) # By default the mask weighting is applied

    color = ['red','blue', 'green', 'black']
    label = ['15-50 keV', '50-100 keV', '100-350 keV', '15-350 keV']
    bin_sizes = [0.064, 0.256, 1.024]  # in seconds

    fig, axes = plt.subplots(len(bin_sizes), 1, figsize=(10, 5 * len(bin_sizes)), sharex=True)

    for i, bin_size in enumerate(bin_sizes):
        ax = axes[i]
        lc.set_timebins(
                    timebinalg="uniform",
                    timedelta=np.timedelta64(int(bin_size*1000), 'ms'),
                    tmin=-5*deltat*u.s,
                    tmax=10*deltat*u.s,
                    T0=t0,
                    is_relative = True)
        lc.set_energybins(energybins=[15, 50, 100, 350]* u.keV)
        # Now plot each sub-band
        for n in range(0,4):
            time = lc.data['TIME'][:].value
            counts = lc.data['RATE'][:,n].value
            errors = lc.data['ERROR'][:,n].value
            if n==3:
                ax.errorbar(time-t0, counts, yerr=errors, color=color[n], ls='', zorder=n, alpha=1.0)
                ax.step(time-t0, counts, where='mid', color=color[n], label=label[n], zorder=n, alpha=1.0)
            else:
                ax.step(time-t0, counts, where='mid', color=color[n], label=label[n], zorder=n, alpha=0.4)
        ax.set_title(f"Light Curve (bin size: {bin_size:.3f} s)")
        ax.axhline(y=0.0, color='r', linestyle='--', linewidth=1)
        ax.set_ylabel("Counts/bin")
        ax.legend()


    axes[-1].set_xlabel("Time [s] (t - t0)")
    plt.tight_layout()
    plt.show()
    plt.savefig(os.path.join(workdir, f'lc_{pipe}.png'), dpi=300)
    plt.close()
delta_t = 4  # typical duration in seconds of the event
plot_lc(event, t0, delta_t, workdir, 'imaging')
../_images/1f3d740b1188a7ae76f6f319d7b25427fe0223eaa9d86de52d3c525ef51d918d.png

Bayesian blocks light curve#

lc=event.create_lightcurve(energybins=[15, 350] * u.keV, recalc=True)

lc.set_timebins(timebinalg="bayesian", save_durations=True)

bayesian_tstart = lc.tbins["TIME_START"]
bayesian_tstop = lc.tbins["TIME_STOP"]
t90_start = lc.tdurs['T90']['TSTART']
t90_stop = lc.tdurs['T90']['TSTOP']
mask = (bayesian_tstop >= t90_start) & (bayesian_tstart <= t90_stop)

lc.plot(T0=t0, plot_relative=True)

# Highlight T90 interval
plt.axvspan(t90_start.value - t0, t90_stop.value - t0, color='gray', alpha=0.3, label='T90 Interval')
plt.legend()
plt.xlim(-5, 50)
plt.title("Bayesian Block Light Curve")
Text(0.5, 1.0, 'Bayesian Block Light Curve')
../_images/709e1fd9759a24da5c349bab223b9a1cf07ff37eb091c263e47120794ff84855.png
print(f"The t_90 duration is {lc.tdurs['T90']['TSTOP'] - lc.tdurs['T90']['TSTART']:.2f}")
print(f"Emission start time: {lc.tdurs['T90']['TSTART'].value - t0:.2f} seconds relative to t0")
print(f"Emission stop time: {lc.tdurs['T90']['TSTOP'].value - t0:.2f} seconds relative to t0")
The t_90 duration is 46.59 s
Emission start time: -0.26 seconds relative to t0
Emission stop time: 46.33 seconds relative to t0

We just discovered that, even if the burst looks short, there is a long tail, also known as extended emission

https://academic.oup.com/mnras/article/544/1/548/8256866

Here I just copied and pasted the _call_battblocks from BatAnalysis, trying to edit by hand some setup variables of the ‘battblocks’

try:
    import heasoftpy.swift as hsp
    import heasoftpy as hsp_core
except ModuleNotFoundError as err:
    # Error handling
    print(err)

def _call_battblocks_my(self, output_file=None, save_durations=True):
    """
    This method calls battblocks for bayesian blocks binning of a lightcurve. This rebins the lightcurve into a 64 ms
    energy integrated energy bin (based on current ebins) to calculate the bayesian block time bins and then
    restores the lightcurve back to the prior energy binning.

    :param output_file: Path object of the file that will be created by battblocks with the good time intervals of
        interest
    :param save_durations: Boolean determining if battblocks should calculate durations such as T90, T50, background
        durations, etc and save those to a file. This file, if created, will be read in and saved to the tdurs attribute.
        This file will be saved to the gti subdirectory of the event results directory.
    :return: output_file Path object, HEASoftpy results object from battblocks call
    """

    if len(self.ebins["INDEX"]) > 1:
        recalc_energy = True
        # need to rebin to a single energy and save current energy bins
        old_ebins = self.ebins.copy()
        # see if we have the enrgy integrated bin included in the arrays:
        if (self.ebins["E_MIN"][0] == self.ebins["E_MIN"][-1]) and (
                self.ebins["E_MAX"][-2] == self.ebins["E_MAX"][-1]):
            self.set_energybins(emin=self.ebins["E_MIN"][-1], emax=self.ebins["E_MAX"][-1])
            calc_energy_integrated = True  # this is for recalculating the lightcurve later in the method
        else:
            self.set_energybins(emin=self.ebins["E_MIN"][0], emax=self.ebins["E_MAX"][-1])
            calc_energy_integrated = False
    else:
        recalc_energy = False

    # set the time binning to be 64 ms. This time binning will be over written anyways so dont need to restore anything
    self.set_timebins()

    # get the set of default values which we will modify
    # stop
    test = hsp_core.HSPTask('battblocks')
    input_dict = test.default_params.copy()

    if output_file is None:
        # use the same filename as for the lightcurve file but replace suffix with gti and put it in gti subdir instead of lc
        new_path = self.lightcurve_file.parts
        new_name = self.lightcurve_file.name.replace("lc", "gti")

        output_file = Path(*new_path[:self.lightcurve_file.parts.index('lc')]).joinpath("gti").joinpath(new_name)

    # modify some of the inputs here
    input_dict["infile"] = str(
        self.lightcurve_file)  # this should ideally be a 64 ms lightcurve of a single energy bin
    input_dict["outfile"] = str(output_file)

    # these are used by batgrbproducts:
    input_dict["bkgsub"] = "YES"
    input_dict["clobber"] = "YES"
    input_dict["tlookback"] = 10
    # input_dict["nspill"] = 256
    # input_dict["(tpeak"] = 0.01
    # input_dict["(ncp prior"] = 2
    input_dict["(coalescefrac"] = 0.0
    if save_durations:
        dur_output_file = output_file.parent / output_file.name.replace("gti", "dur")
        input_dict["durfile"] = str(dur_output_file)

    try:
        battblocks_return = hsp.battblocks(**input_dict)

    except Exception as e:
        print(e)
        raise RuntimeError(f"The call to Heasoft battblocks failed with inputs {input_dict}.")

    # reset the energy bins to what they were before
    if recalc_energy:
        if calc_energy_integrated:
            min_ebins = old_ebins["E_MIN"][:-1]
            max_ebins = old_ebins["E_MAX"][:-1]
        else:
            min_ebins = old_ebins["E_MIN"]
            max_ebins = old_ebins["E_MAX"]

        self.set_energybins(emin=min_ebins, emax=max_ebins,
                            calc_energy_integrated=calc_energy_integrated)

    if battblocks_return.returncode != 0:
        raise RuntimeError(f'The call to Heasoft battblocks failed with message: {battblocks_return.output}')

    if save_durations:
        self._parse_durations(dur_output_file)

    return output_file, battblocks_return
_call_battblocks_my(lc)
(PosixPath('/Users/sjs8171/Desktop/bat_glimpse/data/00018897003_eventresult/gti/t_uniform_dt_0.064_1chan.gti'),
 ---------------------
 :: Execution Result ::
 ---------------------
 > Return Code: 0
 > Output:
 ******************************************
          battblocks v1.18
 ------------------------------------------
      Input Data: /Users/sjs8171/Desktop/bat_glimpse/data/00018897003_eventresult/lc/t_uniform_dt_0.064_1chan.lc
      Output GTI: /Users/sjs8171/Desktop/bat_glimpse/data/00018897003_eventresult/gti/t_uniform_dt_0.064_1chan.gti
  Events to Skip: 128     Changepoint log(prob) Prior: 6.000000
   Internal Tick: 1.000000e-04 s
   Lookback time: 10 s
  Bkg. Subtract?: yes (for fluence/T50/T90 calculations)
 ------------------------------------------
   Estimated T90 duration: 46.592 s +/- 10.2472 s
   Estimated T50 duration: 25.728 s +/- 6.54335 s
   Estimated Peak Interval: MET   752515599.48799992 +/- 0.500000 s
   Estimated background rate 1:        0.001 near MET 752515201.280000 s
   Estimated background rate 2:        0.010 near MET 752515699.392000 s
   Estimated total duration: 50.112 s (for data selection)
        (from MET 752515598.560000 to MET 752515648.672000)
   Estimated total fluence: 2.554683 count
   Created GTI with 4 entries
 ------------------------------------------
 
 > Parameters:
 	infile    : /Users/sjs8171/Desktop/bat_glimpse/data/00018897003_eventresult/lc/t_uniform_dt_0.064_1chan.lc
 	outfile   : /Users/sjs8171/Desktop/bat_glimpse/data/00018897003_eventresult/gti/t_uniform_dt_0.064_1chan.gti
 	durfile   : /Users/sjs8171/Desktop/bat_glimpse/data/00018897003_eventresult/gti/t_uniform_dt_0.064_1chan.dur
 	nspill    : 128
 	timedel   : 0.0001
 	ncp_prior : 6.0
 	gaussian  : INDEF
 	txx       : 0.0
 	tlookback : 10
 	tpeak     : 1.0
 	bkgsub    : yes
 	burst_tstart: INDEF
 	burst_tstop: INDEF
 	coalescefrac: 0.05
 	timecol   : TIME
 	countscol : INDEF
 	errcol    : ERROR
 	expocol   : INDEF
 	hduclas3  : INDEF
 	durerrmeth: TOTVAR
 	diagfile  : NONE
 	breakfile : NONE
 	global_tstart: INDEF
 	global_tstop: INDEF
 	clobber   : yes
 	chatter   : 2
 	history   : yes
 	mode      : ql)
print(f"The t_90 duration is {lc.tdurs['T90']['TSTOP'] - lc.tdurs['T90']['TSTART']:.2f}")
print(f"Emission start time: {lc.tdurs['T90']['TSTART'].value - t0:.2f} seconds relative to t0")
print(f"Emission stop time: {lc.tdurs['T90']['TSTOP'].value - t0:.2f} seconds relative to t0")
The t_90 duration is 46.59 s
Emission start time: -0.26 seconds relative to t0
Emission stop time: 46.33 seconds relative to t0

Spectrum during the \(t_{90}\) interval#

We are going to create here the spectrum. We just need to select the time interval

spectrum_t90=event.create_pha(tstart=lc.tdurs['T90']['TSTART'],
                               tstop=lc.tdurs['T90']['TSTOP'],
                                recalc=True)

Once we have the spectrum, we can also fit. The ‘fit_spectrum’ function calls XSPEC and save the results

ba.fit_spectrum(spectrum_t90, generic_model="cflux*po", # simple power-law with flux normalization
                setPars={1:"15,-1", 2:"150,-1",3:-9, 4:"-1",5:".001,-1"},
                use_cstat=False, fit_iterations=10000)
1 spectrum  in use
 
Spectral Data File: t_752515598.88-752515645.4719999_80chan.pha  Spectrum 1
Net count rate (cts/s) for Spectrum:1  5.661e-02 +/- 4.842e-03
 Assigned to Data Group 1 and Plot Group 1
  Noticed Channels:  1-80
  Telescope: SWIFT Instrument: BAT  Channel Type: PI
  Exposure Time: 46.59 sec
 Using fit statistic: chi
 Using Response (RMF) File            t_752515598.88-752515645.4719999_80chan.rsp for Source 1


Fit statistic  : Chi-Squared                   55.93     using 80 bins.

***Warning: Chi-square may not be valid due to bins with zero variance
            in spectrum number: 1

Test statistic : Chi-Squared                   55.93     using 80 bins.

***Warning: Chi-square may not be valid due to bins with zero variance
            in spectrum number(s): 1 

 Null hypothesis probability of 9.72e-01 with 78 degrees of freedom
 Current data and model not fit yet.

========================================================================
Model cflux<1>*powerlaw<2> Source No.: 1   Active/On
Model Model Component  Parameter  Unit     Value
 par  comp
   1    1   cflux      Emin       keV      15.0000      frozen
   2    1   cflux      Emax       keV      150.000      frozen
   3    1   cflux      lg10Flux   cgs      -9.00000     +/-  0.0          
   4    2   powerlaw   PhoIndex            -1.00000     +/-  0.0          
   5    2   powerlaw   norm                1.00000E-03  frozen
________________________________________________________________________


Fit statistic  : Chi-Squared                  234.42     using 80 bins.

***Warning: Chi-square may not be valid due to bins with zero variance
            in spectrum number: 1

Test statistic : Chi-Squared                  234.42     using 80 bins.

***Warning: Chi-square may not be valid due to bins with zero variance
            in spectrum number(s): 1 

 Null hypothesis probability of 1.47e-17 with 78 degrees of freedom
 Current data and model not fit yet.
Default fit statistic is set to: Chi-Squared
   This will apply to all current and newly loaded spectra.

Fit statistic  : Chi-Squared                  234.42     using 80 bins.

***Warning: Chi-square may not be valid due to bins with zero variance
            in spectrum number: 1

Test statistic : Chi-Squared                  234.42     using 80 bins.

***Warning: Chi-square may not be valid due to bins with zero variance
            in spectrum number(s): 1 

 Null hypothesis probability of 1.47e-17 with 78 degrees of freedom
 Current data and model not fit yet.
     4 channels (1-4) ignored in spectrum #     1
    18 channels (63-80) ignored in spectrum #     1

Fit statistic  : Chi-Squared                  221.20     using 58 bins.

Test statistic : Chi-Squared                  221.20     using 58 bins.
 Null hypothesis probability of 1.71e-21 with 56 degrees of freedom
 Current data and model not fit yet.

 Warning: renorm - no variable model to allow  renormalization

Fit statistic  : Chi-Squared                  221.20     using 58 bins.

Test statistic : Chi-Squared                  221.20     using 58 bins.
 Null hypothesis probability of 1.71e-21 with 56 degrees of freedom
 Current data and model not fit yet.
 Warning: renorm - no variable model to allow  renormalization
                                   Parameters
Chi-Squared  |beta|/N    Lvl    3:lg10Flux    4:PhoIndex
186.738      3.41455       0      -8.10469     -0.945633
53.3625      18.931        0      -7.54966       1.78005
41.0945      29.9641      -1      -7.42252       1.41058
41.0247      3.69184      -2      -7.41681       1.42770
41.0247      0.0174237    -3      -7.41674       1.42722
==============================
 Variances and Principal Axes
                 3        4  
 9.9249E-04|  0.9943   0.1069  
 1.7664E-02| -0.1069   0.9943  
------------------------------

========================
  Covariance Matrix
        1           2   
   1.183e-03  -1.773e-03
  -1.773e-03   1.747e-02
------------------------

========================================================================
Model cflux<1>*powerlaw<2> Source No.: 1   Active/On
Model Model Component  Parameter  Unit     Value
 par  comp
   1    1   cflux      Emin       keV      15.0000      frozen
   2    1   cflux      Emax       keV      150.000      frozen
   3    1   cflux      lg10Flux   cgs      -7.41674     +/-  3.43970E-02  
   4    2   powerlaw   PhoIndex            1.42722      +/-  0.132186     
   5    2   powerlaw   norm                1.00000E-03  frozen
________________________________________________________________________


Fit statistic  : Chi-Squared                   41.02     using 58 bins.

Test statistic : Chi-Squared                   41.02     using 58 bins.
 Null hypothesis probability of 9.33e-01 with 56 degrees of freedom

Parameters defined:
========================================================================
Model cflux<1>*powerlaw<2> Source No.: 1   Active/On
Model Model Component  Parameter  Unit     Value
 par  comp
   1    1   cflux      Emin       keV      15.0000      frozen
   2    1   cflux      Emax       keV      150.000      frozen
   3    1   cflux      lg10Flux   cgs      -7.41674     +/-  3.43970E-02  
   4    2   powerlaw   PhoIndex            1.42722      +/-  0.132186     
   5    2   powerlaw   norm                1.00000E-03  frozen
________________________________________________________________________


Fit statistic  : Chi-Squared                   41.02     using 58 bins.

Test statistic : Chi-Squared                   41.02     using 58 bins.
 Null hypothesis probability of 9.33e-01 with 56 degrees of freedom
*** Parameter 1 is not a variable model parameter and no confidence range will be calculated.
 Parameter   Confidence Range (2.706)
*** Parameter 2 is not a variable model parameter and no confidence range will be calculated.
 Parameter   Confidence Range (2.706)
 Parameter   Confidence Range (2.706)
     3     -7.47736     -7.36324    (-0.0606158,0.053499)
 Parameter   Confidence Range (2.706)
     4      1.20991      1.64452    (-0.217318,0.21729)
*** Parameter 5 is not a variable model parameter and no confidence range will be calculated.
 Parameter   Confidence Range (2.706)
../_images/fde626fa3a19b1d51de1b0536c113dda7ebc3641a55e2db0eaff4304fecc0d08.png

We can access the results which are saved in the form of a dictionary

params = spectrum_t90.spectral_model['parameters']
lg10_flux = params['lg10Flux']
flux = 10 ** lg10_flux['val']
flux_lo = 10 ** lg10_flux['lolim']
flux_hi = 10 ** lg10_flux['hilim']

print(f"Converted flux: best fit = {flux:.3e}, interval = ({flux_lo:.3e}, {flux_hi:.3e})")
# photon index
photon_index = params['PhoIndex']['val']
photon_index_lo = params['PhoIndex']['lolim']
photon_index_hi = params['PhoIndex']['hilim']

print(f"Photon index: best fit = {photon_index:.3f}, interval = ({photon_index_lo:.3f}, {photon_index_hi:.3f})")
Converted flux: best fit = 3.831e-08, interval = (3.332e-08, 4.333e-08)
Photon index: best fit = 1.427, interval = (1.210, 1.645)

Time resolved spectra using Bayesian blocks#

We can also repeat the spectral fit but done for each temporal bin we define. In this case we are using the bayesian blocks output, but we can use any time bin, defining start and stop times of them

bayesian_spectra = event.create_pha(
    tstart=bayesian_tstart[mask],
    tstop=bayesian_tstop[mask],
    recalc=True)
bayesian_tstop[mask]
\[[7.525156 \times 10^{8},~7.5251565 \times 10^{8}] \; \mathrm{s}\]

For the fit here we can also use multiple CPUs, if available. Since we are inside a jupyter notebook, we need to to that in a ‘parallel_backend’. If you run it outside jupyter, there is no need for that.

import os
from joblib import parallel_backend

joblib_tmp = os.path.join(workdir, "joblib_tmp")
os.makedirs(joblib_tmp, exist_ok=True)
os.environ["JOBLIB_TEMP_FOLDER"] = joblib_tmp

n_procs = min(len(bayesian_spectra), multiprocessing.cpu_count())

with parallel_backend(
    'loky',
    n_jobs=n_procs,
    max_nbytes=None,             
    inner_max_num_threads=1,      
    temp_folder=joblib_tmp
):
    output_bayesian_spectra = ba.parallel.batspectrum_TTE_analysis(
        bayesian_spectra,
        generic_model="cflux*po",
        setPars={1:"15,-1", 2:"150,-1", 3:-9, 4:"-1", 5:".001,-1"},
        nprocs=n_procs,
        use_cstat=False,
        fit_iterations=10000,
        recalc=True
    )
1 spectrum  in use
 
Spectral Data File: t_752515600.416-752515648.672_80chan.pha  Spectrum 1
Net count rate (cts/s) for Spectrum:1  4.168e-02 +/- 4.927e-03
 Assigned to Data Group 1 and Plot Group 1
  Noticed Channels:  1-80
  Telescope: SWIFT Instrument: BAT  Channel Type: PI
  Exposure Time: 48.26 sec
 Using fit statistic: chi
 Using Response (RMF) File            t_752515600.416-752515648.672_80chan.rsp for Source 1


========================================================================
Model cflux<1>*powerlaw<2> Source No.: 1   Active/On
Model Model Component  Parameter  Unit     Value
 par  comp
   1    1   cflux      Emin       keV      15.0000      frozen
   2    1   cflux      Emax       keV      150.000      frozen
   3    1   cflux      lg10Flux   cgs      -9.00000     +/-  0.0          
   4    2   powerlaw   PhoIndex            -1.00000     +/-  0.0          
   5    2   powerlaw   norm                1.00000E-03  frozen
________________________________________________________________________


Fit statistic  : Chi-Squared                  163.21     using 80 bins.

***Warning: Chi-square may not be valid due to bins with zero variance
            in spectrum number: 1

Test statistic : Chi-Squared                  163.21     using 80 bins.

***Warning: Chi-square may not be valid due to bins with zero variance
            in spectrum number(s): 1 

 Null hypothesis probability of 5.62e-08 with 78 degrees of freedom
 Current data and model not fit yet.
Default fit statistic is set to: Chi-Squared
   This will apply to all current and newly loaded spectra.

Fit statistic  : Chi-Squared                  163.21     using 80 bins.

***Warning: Chi-square may not be valid due to bins with zero variance
            in spectrum number: 1

Test statistic : Chi-Squared                  163.21     using 80 bins.

***Warning: Chi-square may not be valid due to bins with zero variance
            in spectrum number(s): 1 

 Null hypothesis probability of 5.62e-08 with 78 degrees of freedom
 Current data and model not fit yet.

1 spectrum  in use
 
Spectral Data File: t_752515598.56-752515600.4159999_80chan.pha  Spectrum 1
Net count rate (cts/s) for Spectrum:1  4.815e-01 +/- 3.243e-02
 Assigned to Data Group 1 and Plot Group 1
  Noticed Channels:  1-80
  Telescope: SWIFT Instrument: BAT  Channel Type: PI
  Exposure Time: 1.856 sec
 Using fit statistic: chi
 Using Response (RMF) File            t_752515598.56-752515600.4159999_80chan.rsp for Source 1


========================================================================
Model cflux<1>*powerlaw<2> Source No.: 1   Active/On
Model Model Component  Parameter  Unit     Value
 par  comp
   1    1   cflux      Emin       keV      15.0000      frozen
   2    1   cflux      Emax       keV      150.000      frozen
   3    1   cflux      lg10Flux   cgs      -9.00000     +/-  0.0          
   4    2   powerlaw   PhoIndex            -1.00000     +/-  0.0          
   5    2   powerlaw   norm                1.00000E-03  frozen
________________________________________________________________________


Fit statistic  : Chi-Squared                  300.35     using 80 bins.

***Warning: Chi-square may not be valid due to bins with zero variance
            in spectrum number: 1

Test statistic : Chi-Squared                  300.35     using 80 bins.

***Warning: Chi-square may not be valid due to bins with zero variance
            in spectrum number(s): 1 

 Null hypothesis probability of 7.89e-28 with 78 degrees of freedom
 Current data and model not fit yet.
Default fit statistic is set to: Chi-Squared
   This will apply to all current and newly loaded spectra.

Fit statistic  : Chi-Squared                  300.35     using 80 bins.

***Warning: Chi-square may not be valid due to bins with zero variance
            in spectrum number: 1

Test statistic : Chi-Squared                  300.35     using 80 bins.

***Warning: Chi-square may not be valid due to bins with zero variance
            in spectrum number(s): 1 

 Null hypothesis probability of 7.89e-28 with 78 degrees of freedom
 Current data and model not fit yet.
     4 channels (1-4) ignored in spectrum #     1
    18 channels (63-80) ignored in spectrum #     1

Fit statistic  : Chi-Squared                  153.93     using 58 bins.

Test statistic : Chi-Squared                  153.93     using 58 bins.
 Null hypothesis probability of 4.48e-11 with 56 degrees of freedom
 Current data and model not fit yet.

 Warning: renorm - no variable model to allow  renormalization

Fit statistic  : Chi-Squared                  153.93     using 58 bins.

Test statistic : Chi-Squared                  153.93     using 58 bins.
 Null hypothesis probability of 4.48e-11 with 56 degrees of freedom
 Current data and model not fit yet.
 Warning: renorm - no variable model to allow  renormalization
                                   Parameters
Chi-Squared  |beta|/N    Lvl    3:lg10Flux    4:PhoIndex
138.205      2.64763       0      -8.35834     -0.399148
     4 channels (1-4) ignored in spectrum #     1
122.568      10.5618       0      -8.20020      0.539740
103.757      18.5083       0      -8.05993       1.05581
61.0311      26.7225       0      -7.54817       2.05345
    18 channels (63-80) ignored in spectrum #     1
41.3928      40.638       -1      -7.55271       1.51653

Fit statistic  : Chi-Squared                  277.33     using 58 bins.

Test statistic : Chi-Squared                  277.33     using 58 bins.
 Null hypothesis probability of 4.65e-31 with 56 degrees of freedom
 Current data and model not fit yet.

 Warning: renorm - no variable model to allow  renormalization
41.0569      2.75164      -2      -7.53450       1.42048

Fit statistic  : Chi-Squared                  277.33     using 58 bins.

Test statistic : Chi-Squared                  277.33     using 58 bins.
 Null hypothesis probability of 4.65e-31 with 56 degrees of freedom
 Current data and model not fit yet.
 Warning: renorm - no variable model to allow  renormalization
41.0536      0.207145     -3      -7.53523       1.43034
==============================
 Variances and Principal Axes
                 3        4  
 1.5732E-03|  0.9943   0.1064  
                                   Parameters
Chi-Squared  |beta|/N    Lvl    3:lg10Flux    4:PhoIndex
 2.7716E-02| -0.1064   0.9943  
------------------------------

========================
  Covariance Matrix
        1           2   
   1.869e-03  -2.765e-03
  -2.765e-03   2.742e-02
------------------------

========================================================================
Model cflux<1>*powerlaw<2> Source No.: 1   Active/On
Model Model Component  Parameter  Unit     Value
 par  comp
   1    1   cflux      Emin       keV      15.0000      frozen
   2    1   cflux      Emax       keV      150.000      frozen
   3    1   cflux      lg10Flux   cgs      -7.53523     +/-  4.32304E-02  
   4    2   powerlaw   PhoIndex            1.43034      +/-  0.165592     
   5    2   powerlaw   norm                1.00000E-03  frozen
________________________________________________________________________


Fit statistic  : Chi-Squared                   41.05     using 58 bins.

Test statistic : Chi-Squared                   41.05     using 58 bins.
 Null hypothesis probability of 9.33e-01 with 56 degrees of freedom
274.7        0.463216      1      -8.38606     -0.583545
271.781      1.97407       1      -8.20589      0.215838
266.367      3.66656       1      -8.03551      0.915934
226.726      6.78376       0      -6.78121       4.21954
118.251      160.652       0      -6.83669       3.11802
62.3988      14.6682      -1      -6.66467       1.48489
44.3242      54.4272      -2      -6.52856       1.69774
*** Parameter 1 is not a variable model parameter and no confidence range will be calculated.
 Parameter   Confidence Range (2.706)
43.3621      16.4575      -3      -6.54358       1.64077
*** Parameter 2 is not a variable model parameter and no confidence range will be calculated.
 Parameter   Confidence Range (2.706)
 Parameter   Confidence Range (2.706)
43.3613      0.394858     -4      -6.54390       1.63863
==============================
 Variances and Principal Axes
                 3        4  
 7.8090E-04|  0.9905   0.1374  
 1.4820E-02| -0.1374   0.9905  
------------------------------

========================
  Covariance Matrix
        1           2   
   1.046e-03  -1.911e-03
  -1.911e-03   1.455e-02
------------------------

========================================================================
Model cflux<1>*powerlaw<2> Source No.: 1   Active/On
Model Model Component  Parameter  Unit     Value
 par  comp
   1    1   cflux      Emin       keV      15.0000      frozen
   2    1   cflux      Emax       keV      150.000      frozen
   3    1   cflux      lg10Flux   cgs      -6.54390     +/-  3.23416E-02  
   4    2   powerlaw   PhoIndex            1.63863      +/-  0.120643     
   5    2   powerlaw   norm                1.00000E-03  frozen
________________________________________________________________________


Fit statistic  : Chi-Squared                   43.36     using 58 bins.

Test statistic : Chi-Squared                   43.36     using 58 bins.
 Null hypothesis probability of 8.91e-01 with 56 degrees of freedom
*** Parameter 1 is not a variable model parameter and no confidence range will be calculated.
 Parameter   Confidence Range (2.706)
*** Parameter 2 is not a variable model parameter and no confidence range will be calculated.
 Parameter   Confidence Range (2.706)
 Parameter   Confidence Range (2.706)
     3     -7.61233     -7.46914    (-0.0771955,0.0659956)
 Parameter   Confidence Range (2.706)
     4      1.16611      1.69258    (-0.263371,0.263104)
*** Parameter 5 is not a variable model parameter and no confidence range will be calculated.
     3     -6.60053     -6.49336    (-0.0566242,0.0505463)
 Parameter   Confidence Range (2.706)
 Parameter   Confidence Range (2.706)
     4      1.44423       1.8413    (-0.194426,0.202647)
*** Parameter 5 is not a variable model parameter and no confidence range will be calculated.
 Parameter   Confidence Range (2.706)
The condition here is 2.9165351491186855e-08 [2.4415839980767538e-08, 3.395180105356154e-08] 3 4.767980536397e-09 1.4861409881995856e-08
The condition here is 2.8582373578106247e-07 [2.5088466404429285e-07, 3.2110318427351297e-07] 3 3.510926011461006e-08 1.804959554372323e-07

We finally concatenate spectra

from batanalysis.batlib import concatenate_spectrum_data

spect_data = concatenate_spectrum_data(output_bayesian_spectra, ["flux", "phoindex"])

and plot

fig, axes = plt.subplots(3, sharex=True, figsize=(6, 12))

start_times = lc.tbins["TIME_START"]-t0*u.s
end_times = lc.tbins["TIME_STOP"]-t0*u.s
mid_times = lc.tbins["TIME_CENT"]-t0*u.s

bin_start_rel = (spect_data["TIME_START"] - t0 * u.s).to_value(u.s)
bin_stop_rel = (spect_data["TIME_STOP"] - t0 * u.s).to_value(u.s)
bin_colors = ["#ffe9e9", "#b7cefc"]
for idx, (start, stop) in enumerate(zip(bin_start_rel, bin_stop_rel)):
    color = bin_colors[idx % len(bin_colors)]
    for ax in axes:
        ax.axvspan(start, stop, color=color, alpha=0.25, zorder=0)

edges_rel = np.unique(np.concatenate([bin_start_rel, [bin_stop_rel[-1]]]))
for edge in edges_rel:
    for ax in axes:
        ax.axvline(edge, color="0.75", linewidth=0.8, alpha=0.7, zorder=1)

rate = lc.data['RATE']
rate_error = lc.data["ERROR"]
        
line = axes[0].plot(start_times, rate, ds='steps-post')
line_handle, = axes[0].plot(end_times, rate, ds='steps-pre', color=line[-1].get_color())

flux = spect_data['flux']
flux_lolim = spect_data['flux_lolim']
flux_hilim = spect_data['flux_hilim']

axes[0].errorbar(mid_times, rate, yerr=rate_error, ls='None', color=line[-1].get_color())
axes[0].set_ylabel('Count rate (ct/s)')

axes[0].axvline(lc.tdurs['T90']['TSTART'].value-t0, color='green', alpha=0.5, label="t90 start")
axes[0].axvline(lc.tdurs['T90']['TSTOP'].value-t0, color='red', alpha=0.5, label="t90 stop")

axes[0].legend()

spec_param = 'flux'

y = spect_data[spec_param]

# get the errors
lolim = spect_data[f"{spec_param}_lolim"]
hilim = spect_data[f"{spec_param}_hilim"]

yerr = np.array([lolim, hilim])
y_upperlim = spect_data[f"{spec_param}_upperlim"]

# find where we have upper limits and set the error to 1 since the nan error value isnt
# compatible with upperlimits
yerr[:, y_upperlim] = 0.2 * y[y_upperlim]

tbin_cent = 0.5 * (spect_data["TIME_START"] + spect_data["TIME_STOP"])
tbin_err = 0.5 * (spect_data["TIME_STOP"] - spect_data["TIME_START"])

axes[1].errorbar(
    tbin_cent-t0*u.s,
    y,
    xerr=tbin_err,
    yerr=yerr,
    uplims=y_upperlim,
    linestyle="None",
    marker="o",
    markersize=3,
    zorder=3,
)  # color="red"

axes[1].set_ylabel('Flux ($erg \, cm^{-2} s^{-1}$)')

spec_param = 'phoindex'

y = spect_data[spec_param]

# get the errors
lolim = spect_data[f"{spec_param}_lolim"]
hilim = spect_data[f"{spec_param}_hilim"]

yerr = np.array([lolim, hilim])
y_upperlim = spect_data[f"{spec_param}_upperlim"]

axes[2].errorbar(
    tbin_cent-t0*u.s,
    y,
    xerr=tbin_err,
    yerr=yerr,
    uplims=y_upperlim,
    linestyle="None",
    marker="o",
    markersize=3,
    zorder=3,
)  # color="red"

axes[2].set_ylabel('PhoIndex')
plt.xlim(min(spect_data["TIME_START"].value - t0) - 1, max(spect_data["TIME_STOP"].value - t0) + 1)
plt.xlabel('Time (s) - t0')
plt.tight_layout()
../_images/18abf2a5e4bbab5827b2238eabf985ed4d7298aaf92ac59fb441f96e431fb7ad.png