LSST Afterglow detection metrics

Notebook for a mocked up GRB afterglow lightcurve.


In [1]:
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
import healpy as hp

import lsst.sims.maf.slicers as slicers
import lsst.sims.maf.metrics as metrics
import lsst.sims.maf.metricBundles as metricBundles
import lsst.sims.maf.db as db

In [2]:
import matplotlib as mpl
mpl.rcParams['figure.autolayout'] = False

On-axis afterglows

On-axis afterglows decay as $F \sim t^{-\alpha}$ until a jet break, with $\alpha \sim 1$.

This requires subclassing TransientMetric, which assumes linear rise & decays in magnitude. We therefore have writted GRBTransientMetric that synthesizes the appropriate lightcurve shape.

We will use the rough numbers in 2011PASP..123.1034J: a Gaussian with $\mu$=15.35 mag, $\sigma=1.59$ in R-band at 1 minute after explosion, $\alpha = 1.0$.
The right way to do this is not to assume fiducial values, but to simulate a cosmological population of events.

GRBTransientMetric


In [3]:
from mafContrib import GRBTransientMetric, TripletBandMetric

We'll calculate detection fractions for one, two, and three detections. For testing we may want to use one year at a time:

Now let's plot the lightcurves. Because we include a random draw from the brightness distribution these plots will change each time we re-execute the cell.


In [13]:
surveyDuration=10
if surveyDuration < 10:
    year = 3
    sqlconstraint = 'night between %f and %f' % ((365.25*year,365.25*(year+1)))
else:
    sqlconstraint = ''

In [14]:
colors = ['b','g','r','purple','y','magenta','k']
filterNames = ['u','g','r','i','z','y']

transDuration = 100 # days--controls how frequently we inject new transients.
transMetric = GRBTransientMetric(alpha= 1.,surveyDuration=surveyDuration, transDuration=transDuration)


times = np.logspace(-3,2,500)
for filterName, color in zip(filterNames,colors):
    filters = np.array([filterName]*times.size)
    lc = transMetric.lightCurve(times % transDuration,filters)
    plt.plot(times,lc, color, label=filterName)
plt.xlabel('time (days)')
plt.ylabel('mags')
plt.gca().invert_yaxis()
plt.xscale('log')
plt.legend()


Out[14]:
<matplotlib.legend.Legend at 0x7fba548a4050>

In [15]:
# Pick a slicer
slicer = slicers.HealpixSlicer(nside=16)

summaryMetrics = [metrics.MinMetric(), metrics.MeanMetric(), metrics.MaxMetric(), 
                  metrics.MedianMetric(), metrics.RmsMetric(), 
                 metrics.PercentileMetric(percentile=25), metrics.PercentileMetric(percentile=75)]
# Configure some metrics
metricList = []
# What fraction of transients are detected at least once?
metricList.append(transMetric)
# create metrics for 2 and 3 detections
m2 = GRBTransientMetric(alpha= 1., surveyDuration=surveyDuration, nPerFilter=2, metricName='GRBTransientMetric_k2')
#m2 = TripletBandMetric()
metricList.append(m2)
m3 = GRBTransientMetric(alpha= 1., surveyDuration=surveyDuration, nPerFilter=3, metricName='GRBTransientMetric_k3')
metricList.append(m3)


Healpix slicer using NSIDE=16, approximate resolution 219.871130 arcminutes

In [19]:
# Set the database and query
#runName = 'minion_1016' # baseline cadence
runName = 'enigma_1281' # NEO with triples

bDict={}
for i,metric in enumerate(metricList):
    bDict[i] = metricBundles.MetricBundle(metric, slicer, sqlconstraint, 
                                          runName=runName, summaryMetrics=summaryMetrics)

In [20]:
opsdb = db.OpsimDatabase(runName + '_sqlite.db')
outDir = 'Transients'
resultsDb = db.ResultsDb(outDir=outDir)

bgroup = metricBundles.MetricBundleGroup(bDict, opsdb, outDir=outDir, resultsDb=resultsDb)
bgroup.runAll()
bgroup.plotAll(closefigs=False)


Querying database with no constraint.
Found 2414737 visits
Running:  [0, 1, 2]
Completed metric generation.
Running reduce methods.
Running summary statistics.
Completed.
Plotting figures with  sqlconstraint now.
monopole: 0.023315  dipole: lon: 168.602, lat: -37.6598, amp: 0.00426763
monopole: 0.049048  dipole: lon: 105.943, lat: -45.1871, amp: 0.0235627
monopole: 0.0416966  dipole: lon: 104.416, lat: -43.7966, amp: 0.0166312
monopole: 0.0212609  dipole: lon: -157.106, lat: -56.8856, amp: 0.00460562
monopole: 0.00177272  dipole: lon: -94.4474, lat: -41.0305, amp: 0.00110993
monopole: 0.0660974  dipole: lon: 85.931, lat: -56.0916, amp: 0.0181813
/smallfiles/ebellm/anaconda/envs/maf/opt/lsst/sims_maf/python/lsst/sims/maf/utils/mafUtils.py:60: UserWarning: Optimal bin calculation tried to make 368 bins, returning 200
  warnings.warn('Optimal bin calculation tried to make %.0f bins, returning %i'%(nbins, nbinMax))
monopole: 0.00231839  dipole: lon: -122, lat: -45.1754, amp: 0.00154329
monopole: 0.00763373  dipole: lon: 165.674, lat: -59.5131, amp: 0.00266312
monopole: 0.0132422  dipole: lon: -50.0867, lat: -79.4275, amp: 0.0143649
monopole: 0.0526173  dipole: lon: 103.555, lat: -47.024, amp: 0.0181784
monopole: 0.0342131  dipole: lon: 128.082, lat: 60.7568, amp: 0.0118962
monopole: 0.145645  dipole: lon: 98.1903, lat: -45.5695, amp: 0.0442185
monopole: 0.0361038  dipole: lon: 164.56, lat: -63.1547, amp: 0.00730455
monopole: 0.0310179  dipole: lon: 81.4799, lat: -44.2269, amp: 0.00823396
monopole: 0.0938804  dipole: lon: 81.7442, lat: -45.4707, amp: 0.0257513
monopole: 0.0737426  dipole: lon: 166.619, lat: -60.0122, amp: 0.0176549
monopole: 0.0446942  dipole: lon: 80.5165, lat: -37.8758, amp: 0.0101443
monopole: 0.00564321  dipole: lon: 111.997, lat: -76.2243, amp: 0.00344014
monopole: 0.00497958  dipole: lon: 72.8896, lat: -78.95, amp: 0.00350294
monopole: 0.0119727  dipole: lon: 75.1276, lat: -40.2867, amp: 0.00544896
monopole: 0.0267943  dipole: lon: 87.3065, lat: -40.5785, amp: 0.00400963
monopole: 0.0291607  dipole: lon: 80.5094, lat: -21.966, amp: 0.00723332
monopole: 0.0183793  dipole: lon: 118.196, lat: -43.5247, amp: 0.021008
monopole: 0.0237353  dipole: lon: 176.492, lat: -59.107, amp: 0.00539247
Plotting complete.

In [21]:
# enigma_1281
for key in sorted(bDict):
    if  bDict[key].metric.name.endswith('anyNfilters'):
        bDict[key].computeSummaryStats(resultsDb=resultsDb)
        print bDict[key].metric.name, bDict[key].summaryValues


GRBTransientMetric_BandanyNfilters {'25th%ile': 0.055555555555555552, 'Rms': 0.15657996366154939, '75th%ile': 0.22222222222222221, 'Min': 0.027777777777777776, 'Max': 0.91666666666666663, 'Median': 0.1111111111111111, 'Mean': 0.15948669635719995}
GRBTransientMetric_k2_BandanyNfilters {'25th%ile': 0.019178082191780823, 'Rms': 0.1125626662442214, '75th%ile': 0.14520547945205478, 'Min': 0.0, 'Max': 0.55068493150684927, 'Median': 0.054794520547945202, 'Mean': 0.10196117078939589}
GRBTransientMetric_k3_BandanyNfilters {'25th%ile': 0.010958904109589041, 'Rms': 0.094046474817686188, '75th%ile': 0.12054794520547946, 'Min': 0.0, 'Max': 0.50136986301369868, 'Median': 0.035616438356164383, 'Mean': 0.07924087344605725}

In [18]:
# minion_1016
for key in sorted(bDict):
    if  bDict[key].metric.name.endswith('anyNfilters'):
        bDict[key].computeSummaryStats(resultsDb=resultsDb)
        print bDict[key].metric.name, bDict[key].summaryValues


GRBTransientMetric_BandanyNfilters {'25th%ile': 0.055555555555555552, 'Rms': 0.17161462241076481, '75th%ile': 0.22222222222222221, 'Min': 0.027777777777777776, 'Max': 0.88888888888888884, 'Median': 0.1111111111111111, 'Mean': 0.17266187050359713}
GRBTransientMetric_k2_BandanyNfilters {'25th%ile': 0.019178082191780823, 'Rms': 0.12983421948127216, '75th%ile': 0.17739726027397257, 'Min': 0.0, 'Max': 0.59178082191780823, 'Median': 0.06575342465753424, 'Mean': 0.11887257317433726}
GRBTransientMetric_k3_BandanyNfilters {'25th%ile': 0.0054794520547945206, 'Rms': 0.069740478622977359, '75th%ile': 0.057534246575342465, 'Min': 0.0, 'Max': 0.47123287671232877, 'Median': 0.01643835616438356, 'Mean': 0.045368793028199753}

So half of these afterglows are detected in one epoch, 11% in two, and 3% in three epochs (in any filter).

GRB Decay Rates and LSST revist times

Let's try to understand these detection efficiencies by looking more generically at how often we have three observations in a single filter within a relevant interval to detect a GRB.

We'll start by determining how long LSST can observe afterglows parameterized as above--this is be band-dependent, as both LSST's limiting magnitude and the afterglow color vary. For simplicity we consider just $r$ band for now.


In [19]:
colors = ['b','g','r','purple','y','magenta','k']
filterNames = ['u','g','r','i','z','y']

transDuration = 300 # days--controls how frequently we inject new transients.
transMetric = GRBTransientMetric(alpha= 1.,surveyDuration=1, transDuration=transDuration)



def find_time_at_limiting_mag(limiting_mag,times,lc):
    return np.interp(limiting_mag,lc,times)

limit_times = []
lc_mins = []
#times = np.linspace(0,transDuration+1,500)
times = np.logspace(-2,2,500)
for i in range(300):
    # since we haven't included band-dependent afterglow brightness yet, 
    # look at the brightness distribution as a function of time in a single band
    filters = np.array(['r']*times.size)
    lc = transMetric.lightCurve(times,filters)
    lc_mins.append(lc[-1])
    limit_times.append(find_time_at_limiting_mag(24.7,times,lc))
#    plt.plot(times,lc)
#plt.xlabel('time (days)')
#plt.ylabel('mags')
#plt.gca().invert_yaxis()
#plt.xscale('log')
plt.hist(limit_times)
plt.xlabel('Time after trigger last detectable by LSST (days)')
plt.ylabel('Number of simulated bursts')

print np.percentile(limit_times,50)


3.96226973493

So there is a substantial tail of afterglows detectable to tens or even hundreds of days after the burst. Let's look at the cumulative histogram:


In [20]:
plt.hist(limit_times,normed=True,cumulative=True,bins=np.logspace(-1,2,25))
plt.xscale('log')
plt.xlabel('Time after trigger last detectable by LSST (days)')
plt.ylabel('Cumulative Fraction')


Out[20]:
<matplotlib.text.Text at 0x12137f290>

It is important to remember that we have not included a jet break in our lightcurve model here--in reality we expect the afterglow decay to decay faster at late times. Still, it does appear that with appropriately-chosen cadences it should be possible to detect afterglows in multiple visits.