Guide on flux and fluence measurements¶
Peak flux for an FRB¶
To calibrate the peak flux of an FRB, you need the gains file for the corresponding day. You also need to have installed the beam model library (here is a tutorial for the primary beam model) and be on a branch of baseband-analysis
that contains the flux.py
script. Below is an example of analysis steps to get the peak flux for event 172924963.
import numpy as np
import matplotlib.pyplot as plt
from baseband_analysis.dev.flux import *
from baseband_analysis.core import *
# Load the data
data_path = "/data/chime/baseband/processed/2021/06/03/astro_172924963/singlebeam_172924963.h5"
data = BBData.from_file(data_path)
gains = read_gains("/data/chime/daily_gain_solutions/hdf5_files/gain_20210603T130623.102388Z_cyga_timing.h5")
Find the spectrum at the peak.
Load the primary beam model.
import beam_model as bm
config = {
"datapath_xpol" : "/usr/local/lib/python3.6/site-packages/beam_model/bm_data/beam_v1_SVD_XX.h5",
"datapath_ypol" : "/usr/local/lib/python3.6/site-packages/beam_model/bm_data/beam_v1_SVD_YY.h5"
}
model = bm.primary.DataDrivenSVDPrimaryBeamModel(config)
Calibrate the data using the beam model.
# Find the sensitivity at the FRB's position and frequencies
position = get_frb_position(data)
sens = model.get_sensitivity(position, spect_peak["f"])
# Calibrate
calib_x = spect_peak["spect_x"]/sens["sens_x"].squeeze()
calib_y = spect_peak["spect_y"]/sens["sens_y"].squeeze()
calibrated = (calib_x + calib_y)
Finally, the peak flux is just the average of the spectrum at the peak.
Fluence of an FRB¶
The fluence is the flux of the burst integrated over time. This calculation happens in three parts: first, we calibrate the spectra at every point in the main peak. Then, we get the average flux for each of these spectra (this gives a calibrated main peak in time). Finally, we take the integral of this peak over time. Using the same imports as above:
Calibrate using the beam model. Note that we take the transpose of the sensitivities because the beam model returns sensitivities of shape (n positions, n frequencies)
, in this case (1, 1024)
, whereas the spectra are given in shape (n frequencies, n time points)
.
# Find the sensitivity at the FRB's position and frequencies
position = get_frb_position(data)
sens = model.get_sensitivity(position, spect["f"])
# Calibrate
calib_x = spect["spect_x"]/sens["sens_x"].T
calib_y = spect["spect_y"]/sens["sens_y"].T
calibrated = (calib_x + calib_y)
Calculate the fluence by integrating under the pulse.
# Calculate fluxes at every point in the burst
fluxes = np.nanmean(calibrated, axis=0)
# Calculate the fluence (Riemann sum)
downsampling_factor = spect["downsampling_factor"]
native_res = 2.56e-3 # in ms
t_res = native_res*downsampling_factor # time resolution of downsampled data
fluence = np.sum(fluxes*t_res)
print(fluence, "Jy ms")
Errors on the flux and fluence¶
The two main sources of error are from the data itself (noise) and from the primary beam model. From analyzing five observations of Cygnus A, it seems the errors that we find from the propagation are severely underestimated, but will likely improve as the new version of the primary beam model (the point source model) becomes available. Below is a process for getting the error on the peak flux and the fluence of an event.
Errors on the flux¶
Loading the data, same as above (this section also uses the same imports).
# Load the data
data_path = "/data/chime/baseband/processed/2021/06/03/astro_172924963/singlebeam_172924963.h5"
data = BBData.from_file(data_path)
gains = read_gains("/data/chime/daily_gain_solutions/hdf5_files/gain_20210603T130623.102388Z_cyga_timing.h5")
# Find the clean spectrum at the peak
spect_peak = get_frb_peak_spectrum(data, gains, thres=2.5, diagnostic_plots=True)
Import the primary beam model with the error data.
# Load the primary beam model
import beam_model as bm
config = {
"datapath_xpol" : "/home/yuliya/CHIMEFRB/beam-model/beam_model/bm_data/beam_v1_SVD_XX.h5",
"datapath_ypol" : "/home/yuliya/CHIMEFRB/beam-model/beam_model/bm_data/beam_v1_SVD_YY.h5",
"datapath_xpol_resid" : "/home/yuliya/CHIMEFRB/beam-model/beam_model/bm_data/beam_v1_SVD_diff_XX.h5",
"datapath_ypol_resid" : "/home/yuliya/CHIMEFRB/beam-model/beam_model/bm_data/beam_v1_SVD_diff_YY.h5"
}
model = bm.primary.DataDrivenSVDPrimaryBeamModel(config)
Then calibrate the spectrum, same as above.
# Find the sensitivity at the FRB's position and frequencies
position = get_frb_position(data)
sens = model.get_sensitivity(position, spect_peak["f"])
# Calibrate
calib_x = spect_peak["spect_x"]/sens["sens_x"].squeeze()
calib_y = spect_peak["spect_y"]/sens["sens_y"].squeeze()
calibrated = (calib_x + calib_y)
# Calculate the peak flux
flux = np.nanmean(calibrated)
print(flux)
Find the error on the spectrum calibrated in flux using the propagation. From the expression of the spectrum calibration, where s_X
and s_Y
are the uncalibrated spectra in each polarization and m_X
and m_Y
are the beam model responses (sensitivities): \
We can use basic error propagation to find the expression of the error on each point of the spectrum: \
This formula is scripted below.
# Find the error on the spectrum due to the data (first 2 terms of above formula)
err_data = (spect_peak["err_x"]/sens["sens_x"].squeeze())**2 + (spect_peak["err_y"]/sens["sens_y"].squeeze())**2
# Find the error on the spectrum due to the beam model
err_sens_x = model.get_error_on_sensitivity(position, spect_peak["f"])["err_x"].squeeze() # delta m_X
err_sens_y = model.get_error_on_sensitivity(position, spect_peak["f"])["err_y"].squeeze() # delta m_Y
err_model = (err_sens_x*spect_peak["spect_x"]/sens["sens_x"].squeeze()**2)**2 + (err_sens_y*spect_peak["spect_y"]/sens["sens_y"].squeeze()**2)**2
# Combine errors to find error on every spectrum point
d_f = np.sqrt(err_data + err_model)
# Note: Set either one of err_data or err_model to 0 to see how each influences the errors.
# It is suspected that the errors on the beam model are underestimated or not used
# properly, so more investigation is required.
We can now visualize the errors with a plot:
plt.figure()
plt.title("Spectrum of FRB {} with errors from propagation".format(str(data.attrs["event_id"])))
plt.errorbar(x=spect_peak["f"], y=calibrated, yerr=d_f, fmt="k.", capsize=2, ecolor="xkcd:grey")
plt.xlabel("Frequency [MHz]")
plt.ylabel("Flux [Jy]")
plt.show()
Finally, find the error on the peak flux, again using the propagation. The peak flux is just the average of fluxes in the peak spectrum:\
Hence, the error on the peak flux can be found using the propagation. All the errors on the fluxes at individual frequencies were found in the previous step. N
is the number of frequency channels.\
We expect the output of the above to be 0.31711864919244825
.
Errors on the fluence¶
Find the fluence (same as above).
spect = get_frb_spectra(data, gains)
# Find the sensitivity at the FRB's position and frequencies
position = get_frb_position(data)
sens = model.get_sensitivity(position, spect["f"])
# Calibrate
calib_x = spect["spect_x"]/sens["sens_x"].T
calib_y = spect["spect_y"]/sens["sens_y"].T
calibrated = (calib_x + calib_y)
# Calculate fluxes at every point in the burst
fluxes = np.nanmean(calibrated, axis=0)
# Calculate the fluence (Riemann sum)
downsampling_factor = spect["downsampling_factor"]
native_res = 2.56e-3 # in ms
## Time resolution
t_res = native_res*downsampling_factor
fluence = np.sum(fluxes*t_res)
print(fluence, "Jy ms")
The following script allows you to find the error using error propagation. As above, this error is underestimated and should not be used to report results.
## 1. Calculate the error on the spectrum, f(v, t)
# Find the error on the spectrum due to the data
err_data = (spect["err_x"]/sens["sens_x"])**2 + (spect["err_y"]/sens["sens_y"])**2
# Find the error on the spectrum due to the beam model
err_sens_x = model.get_error_on_sensitivity(position, spect["f"])["err_x"].T # delta m_X
err_sens_y = model.get_error_on_sensitivity(position, spect["f"])["err_y"].T # delta m_Y
err_model = (err_sens_x*spect["spect_x"]/sens["sens_x"].T**2)**2 + (err_sens_y*spect["spect_y"]/sens["sens_y"].T**2)**2
# Combine errors to get errors on each spectrum
d_spectra = np.sqrt(err_data.T + err_model)
print(d_spectra.shape) # should be: (n frequencies, n time points in the burst)
## 2. Calculate the error on the flux at each time point
d_f = np.sqrt(np.nansum(np.square(d_spectra), axis=0))/d_spectra.shape[0]
print(d_f.shape) # should be: (n time points)
## 3. Calculate the error on the fluence
d_F = t_res*np.sqrt(np.nansum(np.square(d_f)))
print(d_F) # Should be one value, the error on the fluence
Steady source spectrum calibration¶
Here is a worked example of calibrating five observations of Cygnus A from start to finish. First, load all files and imports.
from baseband_analysis.core import BBData
from baseband_analysis.core.calibration import read_gains
from baseband_analysis.dev.flux import *
import numpy as np
import matplotlib.pyplot as plt
# files
data_paths = ["/data/chime/baseband/processed/2020/11/20/CygA_20201120000547/singlebeam_20201120000547_3beams.h5",
"/data/chime/baseband/processed/2020/11/20/CygA_20201120235951/singlebeam_20201120235951_3beams.h5",
"/data/chime/baseband/processed/2020/11/20/CygA_20201120000047/singlebeam_20201120000047_3beams.h5",
"/data/chime/baseband/processed/2020/11/20/CygA_20201120235351/singlebeam_20201120235351_3beams.h5",
"/data/chime/baseband/processed/2020/11/20/CygA_20201120235151/singlebeam_20201120235151_3beams.h5"
]
gains_paths = [
"/data/chime/daily_gain_solutions/hdf5_files/gain_20201120T015901.218349Z_cyga_timing.h5",
]*len(data_paths)
data_files = []
gains_files = []
for n in range(len(data_paths)):
data_files.append(BBData.from_file(data_paths[n]))
gains_files.append(read_gains(gains_paths[n]))
Then, get the spectra. Set the plot
argument to True
to see all the diagnostic plots.
spectra = []
for n in range(len(data_paths)):
spectra.append(get_steady_source_spectrum(data_files[n], gains_files[n], remove_final=2, sep_pol=True, thres=2))
Find the positions of each observation of Cygnus A. As a sanity check, we expect all their y-positions to be the same. Note: get_frb_position
can be used for steady sources as well as one-off events.
positions = []
for n in range(len(data_files)):
positions.append(get_frb_position(data_files[n]))
Load the primary beam model for calibration.
import beam_model as bm
config = {
"datapath_xpol" : "/home/yuliya/CHIMEFRB/beam-model/beam_model/bm_data/beam_v1_SVD_XX.h5",
"datapath_ypol" : "/home/yuliya/CHIMEFRB/beam-model/beam_model/bm_data/beam_v1_SVD_YY.h5",
"datapath_xpol_resid" : "/home/yuliya/CHIMEFRB/beam-model/beam_model/bm_data/beam_v1_SVD_diff_XX.h5",
"datapath_ypol_resid" : "/home/yuliya/CHIMEFRB/beam-model/beam_model/bm_data/beam_v1_SVD_diff_YY.h5"
}
model = bm.primary.DataDrivenSVDPrimaryBeamModel(config)
sens = []
for n in range(len(data_paths)):
sens.append(model.get_sensitivity(positions[n], spectra[n]["f"]))
Finally, calibrate the spectra.
calib_svd = []
for n in range(len(data_paths)):
# Can use find_source_from_spectrum() to find which beam to use, in this case 1
x = spectra[n]["spect_x"][:,1] / sens[n]["sens_x"].squeeze()
y = spectra[n]["spect_y"][:,1] / sens[n]["sens_y"].squeeze()
calib_svd.append(x+y)
Use the cyg_a_polynomial
function from flux.py
to get the three best-fit parameters for the expected spectrum of Cygnus A (which follows a second-degree polynomial in frequency). This will be useful for plotting.
You can now generate a nice plot!
fig, (ax1, ax2) = plt.subplots(2,1, gridspec_kw={"height_ratios": [4,1]}, figsize=(10,7), sharex=True)
ax1.set_title("Cygnus A Spectrum") #
ax1.set_ylabel("Flux (Jy)")
ax1.set_xlabel("Frequency (MHz)")
f = np.linspace(400, 800, 1024)
ax1.plot(f, a*f**2 + b*f + c, "k--", label="Expected")
for n in range(len(data_files)):
ax1.plot(spect[n]["f"], calib_svd[n], ".", label="x = " + '{:.3f}'.format(positions[n][0]))
ax1.legend(bbox_to_anchor=(1.02, 1), loc='upper left')
ax1.set_ylim(0, 7000)
ax2.set_ylabel("Residuals")
ax2.axhline(y=0, color='k', linestyle='--')
for n in range(len(data_files)):
ax2.plot(spect[n]["f"], calib_svd[n] - (a*spect[n]["f"]**2 + b*spect[n]["f"] +c), ".")
plt.show()