"""
Numerical routines for evaluating and optimising FRB host galaxy magnitude models.
This module is the numerical workhorse for the PATH integration in zdm,
analogous to ``iteration.py`` for the zdm grid. It provides:
- **``function``** — objective function passed to ``scipy.optimize.minimize``
that evaluates a goodness-of-fit statistic for a given set of host model
parameters against the CRAFT ICS optical data.
- **``calc_path_priors``** — inner loop that runs PATH on a list of FRBs
across one or more surveys/grids, collecting priors, posteriors, and
undetected-host probabilities for each FRB.
- **``run_path``** — runs the PATH algorithm for a single named FRB,
loading its candidate host galaxies from the ``frb`` package data
and applying colour corrections to convert to r-band.
- **``calculate_likelihood_statistic``** and **``calculate_ks_statistic``**
— goodness-of-fit statistics comparing the model apparent magnitude prior
to the observed PATH posteriors across all FRBs.
- **``make_cumulative_plots``** — plotting routine for visualising
cumulative magnitude distributions for one or more models simultaneously.
- **``make_wrappers``**, **``make_cdf``**, **``flatten``**,
**``get_cand_properties``** — supporting utilities.
"""
import os
from importlib import resources
import numpy as np
from matplotlib import pyplot as plt
import pandas
from zdm import optical as op
from frb.frb import FRB
from astropath.priors import load_std_priors
from astropath.path import PATH
from frb.associate import frbassociate
[docs]
def function(x,args):
"""
Objective function for ``scipy.optimize.minimize`` over host model parameters.
Updates the host magnitude model with parameter vector ``x``, runs PATH
on all FRBs, computes the chosen goodness-of-fit statistic, and returns
a scalar value suitable for minimisation (i.e. smaller is better).
Parameters
----------
x : np.ndarray
Parameter vector passed to ``model.init_args(x)``. Its meaning
depends on the model (e.g. absolute magnitude bin weights for
``simple_host_model``, or ``fSFR`` for ``loudas_model``).
args : list
Packed argument tuple with the following elements, in order:
- ``frblist`` (list of str): TNS names of FRBs to evaluate.
- ``ss`` (list of Survey): surveys in which the FRBs may appear.
- ``gs`` (list of Grid): zdm grids corresponding to those surveys.
- ``model``: host magnitude model instance (must implement
``init_args``).
- ``POxcut`` (float or None): if not None, restrict the statistic
to FRBs whose best host candidate has P(O|x) > POxcut.
- ``istat`` (int): statistic to use — 0 for KS-like statistic,
1 for maximum-likelihood (returned as negative log-likelihood
so that minimisation maximises the likelihood).
Returns
-------
stat : float
Goodness-of-fit statistic (smaller is better). For ``istat=1``
this is the negative log-likelihood.
"""
frblist = args[0]
ss = args[1]
gs=args[2]
model=args[3]
POxcut=args[4] # either None, or a cut such as 0.9
istat=args[5]
# initialises model to the priors
# generates one per grid, due to possible different zvals
model.init_args(x)
wrappers = make_wrappers(model,gs)
NFRB,AppMags,AppMagPriors,ObsMags,ObsPriors,ObsPosteriors,PUprior,PUobs,sumPUprior,sumPUobs,frbs,dms = calc_path_priors(frblist,ss,gs,wrappers,verbose=False)
# we re-normalise the sum of PUs by NFRB
# prevents infinite plots being created
if istat==0:
stat = calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,
sumPUobs,sumPUprior,plotfile=None,POxcut=POxcut)
elif istat==1:
stat = calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,
PUobs,PUprior,plotfile=None,POxcut=POxcut)
# need to construct stat so that small values are good! Log-likelihood being good means large!
stat *= -1
return stat
[docs]
def make_wrappers(model,grids):
"""
returns a list of model wrapper objects for given model and grids
Args:
model: one of the optical model class objects
grids: list of grid class objects
Returns:
wrappers: list of wrappers around model, one for each grid
"""
wrappers = []
for i,g in enumerate(grids):
wrappers.append(op.model_wrapper(model,g.zvals))
return wrappers
[docs]
def make_cdf(xs,ys,ws,norm=True):
"""
Build a weighted empirical CDF evaluated on a fixed grid.
For each grid point ``x`` in ``xs``, accumulates the weights ``ws[i]``
of all observations ``ys[i]`` that fall below ``x``. The result is a
non-decreasing array that can be compared to a model prior CDF.
Parameters
----------
xs : np.ndarray
Grid of x values at which to evaluate the CDF (e.g. apparent
magnitude bin centres). Must be sorted in ascending order.
ys : array-like
Observed data values (e.g. host galaxy apparent magnitudes).
ws : array-like
Weight for each observation in ``ys`` (e.g. PATH posteriors P_Ox).
Must have the same length as ``ys``.
norm : bool, optional
If True (default), normalise the CDF so that its maximum value
is 1. Set to False to preserve the raw cumulative weight sum.
Returns
-------
cdf : np.ndarray, shape (len(xs),)
Weighted empirical CDF evaluated at each point in ``xs``.
"""
cdf = np.zeros([xs.size])
for i,y in enumerate(ys):
OK = np.where(xs > y)[0]
cdf[OK] += ws[i]
if norm:
cdf /= cdf[-1]
return cdf
[docs]
def calc_path_priors(frblist,ss,gs,wrappers,verbose=True,usemodel=True,P_U=0.1):
"""
Run PATH on a list of FRBs and return priors, posteriors, and P_U values.
For each FRB in ``frblist``, searches all surveys in ``ss`` for a match,
computes the zdm-derived apparent magnitude prior (if ``usemodel=True``),
and runs PATH to produce host association posteriors. Results for all FRBs
are collected into parallel lists (one entry per FRB).
Also writes a CSV file ``allgalaxies.csv`` (if it does not already exist)
containing the magnitude and VLT/FORS2 R-band columns for all candidate
host galaxies across all FRBs.
Parameters
----------
frblist : list of str
TNS names of FRBs to process (e.g. ``['FRB20180924B', ...]``).
ss : list of Survey
Survey objects to search for each FRB. The first survey containing
a given FRB is used.
gs : list of Grid
zdm grids corresponding to each survey in ``ss``.
wrappers : list of model_wrapper
One ``model_wrapper`` per grid (from ``make_wrappers``), used to
compute DM-dependent apparent magnitude priors.
verbose : bool, optional
If True, print a warning for each FRB not found in any survey.
Defaults to True.
usemodel : bool, optional
If True, use the zdm-derived magnitude prior from ``wrappers`` and
estimate P_U from the model. If False, use PATH's built-in inverse
prior and the supplied fixed ``P_U``. Defaults to True.
P_U : float, optional
Fixed prior probability that the host galaxy is undetected. Only
used when ``usemodel=False``. Defaults to 0.1.
Returns
-------
nfitted : int
Number of FRBs successfully matched to a survey and processed.
AppMags : np.ndarray
Internal apparent magnitude grid (from the last processed wrapper).
allMagPriors : list of np.ndarray
One array per FRB giving p(m_r | DM_EG) on the ``AppMags`` grid.
Entries are ``None`` when ``usemodel=False``.
allObsMags : list of np.ndarray
One array per FRB listing the r-band magnitudes of PATH candidate
host galaxies.
allPO : list of np.ndarray
One array per FRB giving the PATH prior P_O for each candidate.
allPOx : list of np.ndarray
One array per FRB giving the PATH posterior P(O|x) for each candidate.
allPU : list of float
Prior P_U (probability of unseen host) for each FRB.
allPUx : list of float
Posterior P(U|x) (probability host is unseen, given data) for each FRB.
sumPU : float
Sum of ``allPU`` across all FRBs.
sumPUx : float
Sum of ``allPUx`` across all FRBs.
frbs : list of str
TNS names of the FRBs that were successfully matched and processed.
dms : list of float
Extragalactic DM (pc cm⁻³) for each FRB in ``frbs``.
"""
NFRB = len(frblist)
# old version creating 1D lists
#allObsMags = None
#allPOx = None
#allMagPriors = None
# new version recording one list per FRB. For max likelihood functionality
allObsMags = []
allPOx = []
allPO = []
allMagPriors = []
sumPU = 0.
sumPUx = 0.
allPU = []
allPUx = []
nfitted = 0
frbs=[]
dms=[]
for i,frb in enumerate(frblist):
# interates over the FRBs. "Do FRB"
# P_O is the prior for each galaxy
# P_Ox is the posterior
# P_Ux is the posterior for it being unobserved
# mags is the list of galaxy magnitudes
# determines if this FRB was seen by the survey, and
# if so, what its DMEG is
for j,s in enumerate(ss):
imatch = op.matchFRB(frb,s)
if imatch is not None:
# this is the survey to be used
g=gs[j]
s = ss[j]
if usemodel:
wrapper = wrappers[j]
jmatch = j
frbs.append(frb)
break
if imatch is None:
if verbose:
print("Could not find ",frb," in any survey")
continue
nfitted += 1
if usemodel:
AppMags = wrapper.AppMags
else:
AppMags = None
# record this info
DMEG = s.DMEGs[imatch]
dms.append(DMEG)
if usemodel:
# this is where the particular survey comes into it
# Must be priors on magnitudes for this FRB
wrapper.init_path_raw_prior_Oi(DMEG,g)
# extracts priors as function of absolute magnitude for this grid and DMEG
MagPriors = wrapper.priors
else:
MagPriors = None
# defunct now
#mag_limit=26 # might not be correct. TODO! Should be in FRB object
# calculates unseen prior
if usemodel:
P_U = wrapper.estimate_unseen_prior()
#MagPriors[:] = 1./len(MagPriors) # log-uniform priors when no model used
P_O,P_Ox,P_Ux,ObsMags,ptbl = run_path(frb,usemodel=usemodel,P_U = P_U)
# replaces PO value with raw PO value, i.e. excluding the driver sigma
if usemodel:
P_O = wrapper.path_base_prior(ObsMags)
# kept here for debugging
if False:
print("P_U is ",P_U)
print("P_O is ",P_O)
print("P_Ox is ",P_Ox)
plt.figure()
plt.plot(AppMags,MagPriors)
plt.show()
plt.close()
if i==0:
allgals = ptbl
else:
allgals = pandas.concat([allgals,ptbl], ignore_index=True)
ObsMags = np.array(ObsMags)
# new version creating a list of lists
allObsMags.append(ObsMags)
allPOx.append(P_Ox)
allPO.append(P_O)
allMagPriors.append(MagPriors)
sumPU += P_U
sumPUx += P_Ux
allPU.append(P_U)
allPUx.append(P_Ux)
subset = allgals[['frb','mag','VLT_FORS2_R']].copy()
# saves all galaxies
if not os.path.exists("allgalaxies.csv"):
subset.to_csv("allgalaxies.csv",index=False)
return nfitted,AppMags,allMagPriors,allObsMags,allPO,allPOx,allPU,allPUx,sumPU,sumPUx,frbs,dms
[docs]
def calculate_likelihood_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs,
PUprior,plotfile=None,POxcut=None):
"""
Compute the total log-likelihood of the observed PATH posteriors given the model prior.
For each FRB, evaluates log10(Σ P(O_i|x) / rescale + P_U_prior), where the
rescale factor accounts for PATH's internal renormalisation of posteriors
relative to the model prior. Summing over all FRBs gives the total
log-likelihood returned to the caller.
Parameters
----------
NFRB : int
Number of FRBs to sum over.
AppMags : np.ndarray, shape (NMAG,)
Apparent magnitude grid used to compute the model prior (not used
directly in this function, but kept for API consistency with
``calculate_ks_statistic``).
AppMagPriors : list of np.ndarray, length NFRB
Model prior p(m_r | DM_EG) on the ``AppMags`` grid, one array per FRB.
ObsMags : list of np.ndarray, length NFRB
Observed r-band magnitudes of PATH candidate host galaxies, one array
per FRB (length NCAND varies by FRB).
ObsPosteriors : list of np.ndarray, length NFRB
PATH posterior P(O_i|x) for each candidate, one array per FRB.
PUobs : list of float, length NFRB
PATH posterior P(U|x) — probability that the true host is undetected —
for each FRB, as returned by PATH after renormalisation.
PUprior : list of float, length NFRB
Model prior P_U for each FRB, as estimated by
``wrapper.estimate_unseen_prior()``.
plotfile : str or None, optional
If provided, save a diagnostic plot comparing prior and posterior
magnitude distributions to this file path. Defaults to None.
POxcut : float or None, optional
If not None, restrict the statistic to FRBs whose maximum P(O|x)
exceeds this threshold (simulates requiring a confident host ID).
Defaults to None.
Returns
-------
stat : float
Total log10-likelihood summed over all NFRB FRBs. Larger values
indicate a better fit. Multiply by -1 for use as a minimisation
objective.
"""
# calculates log-likelihood of observation
stat=0
for i in np.arange(NFRB):
# sums the likelihoods over each galaxy: p(xi|oi)*p(oi)/Pfield
# calculate the factor by which the p...|x probabilities have been rescaled.
# allows us to undo this effect
rescale = PUobs[i]/PUprior[i]
# the problem is that the posteriors have been rescaled by some factor
# we do not want this! Hence, we work out the rescale factor by comparing
# the rescale on the unseen prior. Then we undo this factor
# (Note: PUobs / rescale = PUprior, hence must divide)
sumpost = np.sum(ObsPosteriors[i])/rescale+PUprior[i]
if False:
plt.figure()
plt.plot(AppMags,AppMagPriors[i]/np.max(AppMagPriors[i]),label="priors from model")
for j,mag in enumerate(ObsMags[i]):
plt.scatter(ObsMags[i],ObsPosteriors[i],label="posteriors")
print("Sum gives ",sumpost, " of which PU is ",PUprior[i])
plt.show()
plt.close()
ll = np.log10(sumpost)
stat += ll
return stat
[docs]
def flatten(xss):
"""
Flatten a list of lists into a single flat list.
"""
return [x for xs in xss for x in xs]
[docs]
def calculate_ks_statistic(NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs,
PUprior,plotfile=None,POxcut=None,plotlabel=None,abc=None,tag=""):
"""
Compute a KS-like goodness-of-fit statistic between model prior and observed posteriors.
Builds cumulative magnitude distributions for both the model prior and the
PATH posteriors, normalised by the number of FRBs, and returns the maximum
absolute difference between them — analogous to the KS test statistic.
Optionally produces a plot comparing the two cumulative distributions.
Parameters
----------
NFRB : int
Number of FRBs used for normalisation.
AppMags : np.ndarray, shape (NMAG,)
Apparent magnitude grid on which priors are defined.
AppMagPriors : list of np.ndarray, length NFRB
Model prior p(m_r | DM_EG) on ``AppMags``, one array per FRB.
ObsMags : list of np.ndarray, length NFRB
Observed r-band magnitudes of PATH candidate galaxies, one per FRB.
ObsPosteriors : list of np.ndarray, length NFRB
PATH posteriors P(O_i|x) for each candidate, one array per FRB.
PUobs : list of float
Posterior P(U|x) for each FRB (not used directly in the statistic,
kept for API consistency).
PUprior : list of float
Prior P_U for each FRB (not used directly, kept for API consistency).
plotfile : str or None, optional
If provided, save a CDF comparison plot to this path. Defaults to None.
POxcut : float or None, optional
If not None, restrict to candidates with P(O|x) > POxcut and
normalise both CDFs to unity (simulates the approach of selecting
only confidently identified hosts). Defaults to None.
plotlabel : str or None, optional
Text label placed in the centre-bottom of the plot. Defaults to None.
abc : str or None, optional
Panel label (e.g. ``'(a)'``) placed in the upper-left corner of the
figure in figure-coordinate space. Defaults to None.
tag : str, optional
String prefix added to the legend labels ``"Observed"`` and
``"Prior"``. Defaults to ``""``.
Returns
-------
stat : float
Maximum absolute difference between the observed and prior cumulative
distributions. Smaller values indicate a better fit.
"""
# sums the apparent mag priors over all FRBs to create a cumulative distribution
fAppMagPriors = np.zeros([len(AppMags)])
for i,amp in enumerate(AppMagPriors):
fAppMagPriors += amp
fObsPosteriors = np.array(flatten(ObsPosteriors))
fObsMags = np.array(flatten(ObsMags))
# we calculate a probability using a cumulative distribution
prior_dist = np.cumsum(fAppMagPriors)
if POxcut is not None:
# cuts data to "good" FRBs only
OK = np.where(fObsPosteriors > POxcut)[0]
Ndata = len(OK)
fObsMags = fObsMags[OK]
fObsPosteriors = np.full([Ndata],1.) # effectively sets these to unity
# makes a cdf in units of AppMags, with observations ObsMags weighted by ObsPosteriors
obs_dist = make_cdf(AppMags,fObsMags,fObsPosteriors,norm=False)
if POxcut is not None:
# current techniques just assume we have the full distribution
obs_dist /= obs_dist[-1]
prior_dist /= prior_dist[-1]
else:
# the above is normalised to NFRB. We now divide it by this
# might want to be careful here, and preserve this normalisation
obs_dist /= NFRB
prior_dist /= NFRB #((NFRB-PUprior)/NFRB) / prior_dist[-1]
# we calculate something like the k-statistic. Includes NFRB normalisation
diff = obs_dist - prior_dist
stat = np.max(np.abs(diff))
if plotfile is not None:
plt.figure()
plt.xlabel("Apparent magnitude $m_r$")
plt.ylabel("Cumulative host galaxy distribution")
plt.ylim(0,1)
# calcs lowest x that is essentially at max
ixmax = np.where(prior_dist > prior_dist[-1]*0.999)[0][0]
# rounds it up to multiple of 5
xmax = 5 * (int(AppMags[ixmax]/5.)+1)
ixmin = np.where(prior_dist < 0.01)[0][-1]
xmin = 5*(int(AppMags[ixmin]/5.))
plt.xlim(xmin,xmax)
#cx,cy = make_cdf_for_plotting(ObsMags,weights=ObsPosteriors)
plt.plot(AppMags,obs_dist,label=tag+"Observed",color="black")
plt.plot(AppMags,prior_dist,label=tag+"Prior",linestyle=":")
plt.legend()
# adds label to plot
if plotlabel is not None:
plt.text((xmin+xmax)/2.,0.05,plotlabel)
if abc is not None:
plt.text(0.02,0.9,abc,fontsize=16, transform=plt.gcf().transFigure)
plt.tight_layout()
plt.savefig(plotfile)
plt.close()
return stat
[docs]
def make_cumulative_plots(NMODELS,NFRB,AppMags,AppMagPriors,ObsMags,ObsPosteriors,PUobs,
PUprior,plotfile,plotlabel,POxcut=None,abc=None,onlyobs=None,
greyscale=[],addpriorlabel=True):
"""
Plot cumulative apparent magnitude distributions for multiple host models on one figure.
Computes the same normalised prior and observed CDFs as
``calculate_ks_statistic``, but for ``NMODELS`` models simultaneously,
overlaying them on a single figure with distinct line styles.
All list-valued parameters that appear in ``calculate_ks_statistic``
gain an additional leading dimension of size ``NMODELS`` here.
Parameters
----------
NMODELS : int
Number of models to plot.
NFRB : list of int, length NMODELS
Number of FRBs for each model, used for normalisation.
AppMags : list of np.ndarray, length NMODELS
Apparent magnitude grid for each model.
AppMagPriors : list of lists of np.ndarray, shape (NMODELS, NFRB, NMAG)
Model prior p(m_r | DM_EG) for each model and FRB.
ObsMags : list of lists of np.ndarray, shape (NMODELS, NFRB, NCAND)
Observed candidate magnitudes for each model and FRB.
ObsPosteriors : list of lists of np.ndarray, shape (NMODELS, NFRB, NCAND)
PATH posteriors P(O_i|x) for each model and FRB.
PUobs : list, length NMODELS
Posterior P(U|x) per model (not used directly in the plot).
PUprior : list, length NMODELS
Prior P_U per model (not used directly in the plot).
plotfile : str
Output file path for the saved figure.
plotlabel : list of str, length NMODELS
Legend label prefix for each model.
POxcut : float or None, optional
If not None, restrict to candidates with P(O|x) > POxcut and
normalise CDFs to unity. Defaults to None.
abc : str or None, optional
Panel label (e.g. ``'(a)'``) placed in the upper-left corner in
figure-coordinate space. Defaults to None.
onlyobs : int or None, optional
If not None, only draw the observed CDF for the model with this
index (useful when all models share the same observations). The
observed line is then labelled ``"Observed"`` without a model prefix.
Defaults to None.
greyscale : list of int, optional
Indices of models whose observed CDF should additionally be drawn
in grey (for background reference). Defaults to ``[]``.
addpriorlabel : bool, optional
If True (default), append ``": Prior"`` to each model's legend entry.
Set to False to use only ``plotlabel[imodel]`` as the label.
Returns
-------
None
"""
# arrays to hold created observed and prior distributions
prior_dists = []
obs_dists = []
linestyles=[":","--","-.","-"]
# loops over models to create prior distributions
for imodel in np.arange(NMODELS):
# sums the apparent mag priors over all FRBs to create a cumulative distribution
fAppMagPriors = np.zeros([len(AppMags[imodel])])
for i,amp in enumerate(AppMagPriors[imodel]):
fAppMagPriors += amp
fObsPosteriors = np.array(flatten(ObsPosteriors[imodel]))
fObsMags = np.array(flatten(ObsMags[imodel]))
# we calculate a probability using a cumulative distribution
prior_dist = np.cumsum(fAppMagPriors)
if POxcut is not None:
# cuts data to "good" FRBs only
OK = np.where(fObsPosteriors > POxcut)[0]
Ndata = len(OK)
fObsMags = fObsMags[OK]
fObsPosteriors = np.full([Ndata],1.) # effectively sets these to unity
# makes a cdf in units of AppMags, with observations ObsMags weighted by ObsPosteriors
obs_dist = make_cdf(AppMags[imodel],fObsMags,fObsPosteriors,norm=False)
if POxcut is not None:
# current techniques just assume we have the full distribution
obs_dist /= obs_dist[-1]
prior_dist /= prior_dist[-1]
else:
# the above is normalised to NFRB. We now divide it by this
# might want to be careful here, and preserve this normalisation
obs_dist /= NFRB[imodel]
prior_dist /= NFRB[imodel] #((NFRB-PUprior)/NFRB) / prior_dist[-1]
# we calculate something like the k-statistic. Includes NFRB normalisation
diff = obs_dist - prior_dist
stat = np.max(np.abs(diff))
obs_dists.append(obs_dist)
prior_dists.append(prior_dist)
# plotting!
plt.figure()
plt.xlabel("Apparent magnitude $m_r$")
plt.ylabel("Cumulative host galaxy distribution")
plt.ylim(0,1)
for imodel in np.arange(NMODELS):
if onlyobs is None or onlyobs == imodel:
if onlyobs is not None:
color = 'black'
label = "Observed" # don't sub-label, since this stands in for all observed
else:
color=plt.gca().lines[-1].get_color()
label = plotlabel[imodel]+": Observed"
plt.plot(AppMags[imodel],obs_dists[imodel],label=label,
color=color)
# adds gryescale 'background' plots of observed distributions
if imodel in greyscale:
plt.plot(AppMags[imodel],obs_dists[imodel],color="gray")
# add these in greyscale, to highlight they are 'background' plots
# this option never used, but experimented with.
# calcs lowest x that is essentially at max
ixmax = np.where(prior_dist > prior_dist[-1]*0.999)[0][0]
# rounds it up to multiple of 5
xmax = 5 * (int(AppMags[imodel][ixmax]/5.)+1)
ixmin = np.where(prior_dist < 0.001)[0][-1]
xmin = 5*(int(AppMags[imodel][ixmin]/5.))
# sets this for each one - yes, it's random which is which, oh well!
plt.xlim(xmin,xmax)
#cx,cy = make_cdf_for_plotting(ObsMags,weights=ObsPosteriors)
if addpriorlabel:
label = plotlabel[imodel]+": Prior"
else:
label = plotlabel[imodel]
plt.plot(AppMags[imodel],prior_dists[imodel],label=label,
linestyle=linestyles[imodel%4])
if abc is not None:
plt.text(0.02,0.9,abc,fontsize=16, transform=plt.gcf().transFigure)
plt.legend(fontsize=12,loc="upper left")
plt.tight_layout()
plt.savefig(plotfile)
plt.close()
return None
[docs]
def get_cand_properties(frblist):
"""
Load PATH candidate host galaxy properties for a list of FRBs.
Reads the pre-generated PATH CSV files from the ``frb`` package data
directory (``frb/data/Galaxies/PATH/<FRB>_PATH.csv``) and extracts
the columns ``['ang_size', 'mag', 'ra', 'dec', 'separation']`` for
each FRB.
Args:
frblist (list of str): TNS FRB names (e.g. ``['FRB20180924B', ...]``).
Returns:
all_candidates (list of pd.DataFrame): one DataFrame per FRB,
each with columns ``ang_size``, ``mag``, ``ra``, ``dec``,
and ``separation``.
"""
all_candidates=[]
for i,name in enumerate(frblist):
######### Loads FRB, and modifes properties #########
my_frb = FRB.by_name(name)
#this_path = frbassociate.FRBAssociate(my_frb, max_radius=10.)
# reads in galaxy info
ppath = os.path.join(resources.files('frb'), 'data', 'Galaxies', 'PATH')
pfile = os.path.join(ppath, f'{my_frb.frb_name}_PATH.csv')
ptbl = pandas.read_csv(pfile)
candidates = ptbl[['ang_size', 'mag', 'ra', 'dec', 'separation']]
all_candidates.append(candidates)
return all_candidates
[docs]
def run_path(name,P_U=0.1,usemodel=False,sort=False):
"""
Run the PATH algorithm on a single FRB and return host association results.
Loads the FRB object and its pre-generated PATH candidate table from the
``frb`` package, applies colour corrections to convert candidate magnitudes
to r-band (using fixed offsets: I → R: +0.65, g → R: −0.65), sets up the
FRB localisation ellipse and offset prior, and evaluates PATH posteriors.
The magnitude prior used for the candidates is:
- ``usemodel=False``: PATH's built-in ``'inverse'`` prior (uniform in log
surface density).
- ``usemodel=True``: the ``'user'`` prior, which must be set externally by
pointing ``pathpriors.USR_raw_prior_Oi`` at a ``model_wrapper`` method
before calling this function (typically done by
``wrapper.init_path_raw_prior_Oi``).
The offset prior is always the ``'exp'`` model from PATH's ``'adopted'``
standard priors, with scale 0.5 arcsec.
Parameters
----------
name : str
TNS name of the FRB (e.g. ``'FRB20180924B'``).
P_U : float, optional
Prior probability that the true host galaxy is undetected. Defaults
to 0.1.
usemodel : bool, optional
If True, use the externally set user prior for candidate magnitudes.
Defaults to False.
sort : bool, optional
If True, sort the returned arrays by P(O|x) in ascending order.
Defaults to False.
Returns
-------
P_O : np.ndarray
Prior probability P(O_i) for each candidate host galaxy.
P_Ox : np.ndarray
Posterior probability P(O_i|x) for each candidate.
P_Ux : float
Posterior probability P(U|x) that the true host is undetected.
mags : np.ndarray
R-band apparent magnitudes of the candidates (after colour correction).
ptbl : pd.DataFrame
Full PATH candidate table loaded from the CSV file, with an
additional ``'frb'`` column set to ``name``.
"""
######### Loads FRB, and modifes properties #########
my_frb = FRB.by_name(name)
this_path = frbassociate.FRBAssociate(my_frb, max_radius=10.)
# reads in galaxy info
ppath = os.path.join(resources.files('frb'), 'data', 'Galaxies', 'PATH')
pfile = os.path.join(ppath, f'{my_frb.frb_name}_PATH.csv')
ptbl = pandas.read_csv(pfile)
ngal = len(ptbl)
ptbl["frb"] = np.full([ngal],name)
# Load prior
priors = load_std_priors()
prior = priors['adopted'] # Default
theta_new = dict(method='exp',
max=priors['adopted']['theta']['max'],
scale=0.5)
prior['theta'] = theta_new
# change this to something depending on the FRB DM
prior['U']=P_U
candidates = ptbl[['ang_size', 'mag', 'ra', 'dec', 'separation']]
# implements a correction to their relative magnitudes.
# note that order is R, then I, then g
if "VLT_FORS2_R" in ptbl:
mags = np.array(candidates.mag.values)
elif "VLT_FORS2_I" in ptbl:
mags = np.array(candidates.mag.values) + 0.65
elif "VLT_FORS2_g" in ptbl:
mags = np.array(candidates.mag.values) - 0.65
elif "GMOS_S_i" in ptbl:
mags = np.array(candidates.mag.values) + 0.65
elif "LRIS_I" in ptbl:
mags = np.array(candidates.mag.values) + 0.65
else:
raise ValueError("Cannot implement colour correction")
#this_path = PATH()
this_path.init_candidates(candidates.ra.values,
candidates.dec.values,
candidates.ang_size.values,
mag=mags)
this_path.frb = my_frb
frb_eellipse = dict(a=np.abs(my_frb.sig_a),
b=np.abs(my_frb.sig_b),
theta=my_frb.eellipse['theta'])
this_path.init_localization('eellipse',
center_coord=this_path.frb.coord,
eellipse=frb_eellipse)
# this results in a prior which is uniform in log space
# when summed over all galaxies with the same magnitude
if usemodel:
this_path.init_cand_prior('user', P_U=prior['U'])
else:
this_path.init_cand_prior('inverse', P_U=prior['U'])
# this is for the offset
this_path.init_theta_prior(prior['theta']['method'],
prior['theta']['max'],
prior['theta']['scale'])
P_O=this_path.calc_priors()
# Calculate p(O_i|x)
debug = True
P_Ox,P_Ux = this_path.calc_posteriors('fixed',
box_hwidth=10.,
max_radius=10.,
debug=debug)
# mags already defined above
#mags = candidates['mag']
if sort:
indices = np.argsort(P_Ox)
P_O = P_O[indices]
P_Ox = P_Ox[indices]
mags = mags[indices]
return P_O,P_Ox,P_Ux,mags,ptbl