Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1import numpy as np 

2from .baseMetric import BaseMetric 

3 

4__all__ = ['TemplateExistsMetric', 'UniformityMetric', 

5 'RapidRevisitUniformityMetric', 'RapidRevisitMetric','NRevisitsMetric', 'IntraNightGapsMetric', 

6 'InterNightGapsMetric', 'VisitGapMetric'] 

7 

8 

9class fSMetric(BaseMetric): 

10 """Calculate the fS value (Nvisit-weighted delta(M5-M5srd)). 

11 """ 

12 def __init__(self, filterCol='filter', metricName='fS', **kwargs): 

13 self.filterCol = filterCol 

14 cols = [self.filterCol] 

15 super().__init__(cols=cols, metricName=metricName, units='fS', **kwargs) 

16 

17 def run(self, dataSlice, slicePoint=None): 

18 """"Calculate the fS (reserve above/below the m5 values from the LSST throughputs) 

19 

20 Parameters 

21 ---------- 

22 dataSlice : numpy.array 

23 Numpy structured array containing the data related to the visits provided by the slicer. 

24 slicePoint : dict, optional 

25 Dictionary containing information about the slicepoint currently active in the slicer. 

26 

27 Returns 

28 ------- 

29 float 

30 The fS value. 

31 """ 

32 # We could import this from the m5_flat_sed values, but it makes sense to calculate the m5 

33 # directly from the throughputs. This is easy enough to do and will allow variation of 

34 # the throughput curves and readnoise and visit length, etc. 

35 pass 

36 

37 

38class TemplateExistsMetric(BaseMetric): 

39 """Calculate the fraction of images with a previous template image of desired quality. 

40 """ 

41 def __init__(self, seeingCol='seeingFwhmGeom', observationStartMJDCol='observationStartMJD', 

42 metricName='TemplateExistsMetric', **kwargs): 

43 cols = [seeingCol, observationStartMJDCol] 

44 super(TemplateExistsMetric, self).__init__(col=cols, metricName=metricName, 

45 units='fraction', **kwargs) 

46 self.seeingCol = seeingCol 

47 self.observationStartMJDCol = observationStartMJDCol 

48 

49 def run(self, dataSlice, slicePoint=None): 

50 """"Calculate the fraction of images with a previous template image of desired quality. 

51 

52 Parameters 

53 ---------- 

54 dataSlice : numpy.array 

55 Numpy structured array containing the data related to the visits provided by the slicer. 

56 slicePoint : dict, optional 

57 Dictionary containing information about the slicepoint currently active in the slicer. 

58 

59 Returns 

60 ------- 

61 float 

62 The fraction of images with a 'good' previous template image. 

63 """ 

64 # Check that data is sorted in observationStartMJD order 

65 dataSlice.sort(order=self.observationStartMJDCol) 

66 # Find the minimum seeing up to a given time 

67 seeing_mins = np.minimum.accumulate(dataSlice[self.seeingCol]) 

68 # Find the difference between the seeing and the minimum seeing at the previous visit 

69 seeing_diff = dataSlice[self.seeingCol] - np.roll(seeing_mins, 1) 

70 # First image never has a template; check how many others do 

71 good = np.where(seeing_diff[1:] >= 0.)[0] 

72 frac = (good.size) / float(dataSlice[self.seeingCol].size) 

73 return frac 

74 

75 

76class UniformityMetric(BaseMetric): 

77 """Calculate how uniformly the observations are spaced in time. 

78 Returns a value between -1 and 1. 

79 A value of zero means the observations are perfectly uniform. 

80 

81 Parameters 

82 ---------- 

83 surveyLength : float, optional 

84 The overall duration of the survey. Default 10. 

85 """ 

86 def __init__(self, mjdCol='observationStartMJD', units='', 

87 surveyLength=10., **kwargs): 

88 """surveyLength = time span of survey (years) """ 

89 self.mjdCol = mjdCol 

90 super(UniformityMetric, self).__init__(col=self.mjdCol, units=units, **kwargs) 

91 self.surveyLength = surveyLength 

92 

93 def run(self, dataSlice, slicePoint=None): 

94 """"Calculate the survey uniformity. 

95 

96 This is based on how a KS-test works: look at the cumulative distribution of observation dates, 

97 and compare to a perfectly uniform cumulative distribution. 

98 Perfectly uniform observations = 0, perfectly non-uniform = 1. 

99 

100 Parameters 

101 ---------- 

102 dataSlice : numpy.array 

103 Numpy structured array containing the data related to the visits provided by the slicer. 

104 slicePoint : dict, optional 

105 Dictionary containing information about the slicepoint currently active in the slicer. 

106 

107 Returns 

108 ------- 

109 float 

110 Uniformity of 'observationStartMJDCol'. 

111 """ 

112 # If only one observation, there is no uniformity 

113 if dataSlice[self.mjdCol].size == 1: 

114 return 1 

115 # Scale dates to lie between 0 and 1, where 0 is the first observation date and 1 is surveyLength 

116 dates = (dataSlice[self.mjdCol] - dataSlice[self.mjdCol].min()) / \ 

117 (self.surveyLength * 365.25) 

118 dates.sort() # Just to be sure 

119 n_cum = np.arange(1, dates.size + 1) / float(dates.size) 

120 D_max = np.max(np.abs(n_cum - dates - dates[1])) 

121 return D_max 

122 

123 

124class RapidRevisitUniformityMetric(BaseMetric): 

125 """Calculate uniformity of time between consecutive visits on short timescales (for RAV1). 

126 

127 Parameters 

128 ---------- 

129 mjdCol : str, optional 

130 The column containing the 'time' value. Default observationStartMJD. 

131 minNvisits : int, optional 

132 The minimum number of visits required within the time interval (dTmin to dTmax). 

133 Default 100. 

134 dTmin : float, optional 

135 The minimum dTime to consider (in days). Default 40 seconds. 

136 dTmax : float, optional 

137 The maximum dTime to consider (in days). Default 30 minutes. 

138 """ 

139 def __init__(self, mjdCol='observationStartMJD', minNvisits=100, 

140 dTmin=40.0 / 60.0 / 60.0 / 24.0, dTmax=30.0 / 60.0 / 24.0, 

141 metricName='RapidRevisitUniformity', **kwargs): 

142 self.mjdCol = mjdCol 

143 self.minNvisits = minNvisits 

144 self.dTmin = dTmin 

145 self.dTmax = dTmax 

146 super().__init__(col=self.mjdCol, metricName=metricName, **kwargs) 

147 # Update minNvisits, as 0 visits will crash algorithm and 1 is nonuniform by definition. 

148 if self.minNvisits <= 1: 

149 self.minNvisits = 2 

150 

151 def run(self, dataSlice, slicePoint=None): 

152 """Calculate the uniformity of visits within dTmin to dTmax. 

153 

154 Uses a the same 'uniformity' calculation as the UniformityMetric, based on the KS-test. 

155 A value of 0 is perfectly uniform; a value of 1 is purely non-uniform. 

156 

157 Parameters 

158 ---------- 

159 dataSlice : numpy.array 

160 Numpy structured array containing the data related to the visits provided by the slicer. 

161 slicePoint : dict, optional 

162 Dictionary containing information about the slicepoint currently active in the slicer. 

163 

164 Returns 

165 ------- 

166 float 

167 The uniformity measurement of the visits within time interval dTmin to dTmax. 

168 """ 

169 # Calculate consecutive visit time intervals 

170 dtimes = np.diff(np.sort(dataSlice[self.mjdCol])) 

171 # Identify dtimes within interval from dTmin/dTmax. 

172 good = np.where((dtimes >= self.dTmin) & (dtimes <= self.dTmax))[0] 

173 # If there are not enough visits in this time range, return bad value. 

174 if good.size < self.minNvisits: 

175 return self.badval 

176 # Throw out dtimes outside desired range, and sort, then scale to 0-1. 

177 dtimes = np.sort(dtimes[good]) 

178 dtimes = (dtimes - dtimes.min()) / float(self.dTmax - self.dTmin) 

179 # Set up a uniform distribution between 0-1 (to match dtimes). 

180 uniform_dtimes = np.arange(1, dtimes.size + 1, 1) / float(dtimes.size) 

181 # Look at the differences between our times and the uniform times. 

182 dmax = np.max(np.abs(uniform_dtimes - dtimes - dtimes[1])) 

183 return dmax 

184 

185 

186class RapidRevisitMetric(BaseMetric): 

187 def __init__(self, mjdCol='observationStartMJD', metricName='RapidRevisit', 

188 dTmin=40.0 / 60.0 / 60.0 / 24.0, dTpairs = 20.0 / 60.0 / 24.0, 

189 dTmax = 30.0 / 60.0 / 24.0, minN1 = 28, minN2 = 82, **kwargs): 

190 self.mjdCol = mjdCol 

191 self.dTmin = dTmin 

192 self.dTpairs = dTpairs 

193 self.dTmax = dTmax 

194 self.minN1 = minN1 

195 self.minN2 = minN2 

196 super().__init__(col=self.mjdCol, metricName=metricName, **kwargs) 

197 

198 def run(self, dataSlice, slicePoint=None): 

199 dtimes = np.diff(np.sort(dataSlice[self.mjdCol])) 

200 N1 = len(np.where((dtimes >= self.dTmin) & (dtimes <= self.dTpairs))[0]) 

201 N2 = len(np.where((dtimes >= self.dTmin) & (dtimes <= self.dTmax))[0]) 

202 if (N1 >= self.minN1) and (N2 >= self.minN2): 

203 val = 1 

204 else: 

205 val = 0 

206 return val 

207 

208 

209class NRevisitsMetric(BaseMetric): 

210 """Calculate the number of consecutive visits with time differences less than dT. 

211 

212 Parameters 

213 ---------- 

214 dT : float, optional 

215 The time interval to consider (in minutes). Default 30. 

216 normed : bool, optional 

217 Flag to indicate whether to return the total number of consecutive visits with time 

218 differences less than dT (False), or the fraction of overall visits (True). 

219 Note that we would expect (if all visits occur in pairs within dT) this fraction would be 0.5! 

220 """ 

221 def __init__(self, mjdCol='observationStartMJD', dT=30.0, normed=False, metricName=None, **kwargs): 

222 units = '' 

223 if metricName is None: 

224 if normed: 

225 metricName = 'Fraction of revisits faster than %.1f minutes' % (dT) 

226 else: 

227 metricName = 'Number of revisits faster than %.1f minutes' % (dT) 

228 units = '#' 

229 self.mjdCol = mjdCol 

230 self.dT = dT / 60. / 24. # convert to days 

231 self.normed = normed 

232 super(NRevisitsMetric, self).__init__(col=self.mjdCol, units=units, metricName=metricName, **kwargs) 

233 

234 def run(self, dataSlice, slicePoint=None): 

235 """Count the number of consecutive visits occuring within time intervals dT. 

236 

237 Parameters 

238 ---------- 

239 dataSlice : numpy.array 

240 Numpy structured array containing the data related to the visits provided by the slicer. 

241 slicePoint : dict, optional 

242 Dictionary containing information about the slicepoint currently active in the slicer. 

243 

244 Returns 

245 ------- 

246 float 

247 Either the total number of consecutive visits within dT or the fraction compared to overall visits. 

248 """ 

249 dtimes = np.diff(np.sort(dataSlice[self.mjdCol])) 

250 nFastRevisits = np.size(np.where(dtimes <= self.dT)[0]) 

251 if self.normed: 

252 nFastRevisits = nFastRevisits / float(np.size(dataSlice[self.mjdCol])) 

253 return nFastRevisits 

254 

255 

256class IntraNightGapsMetric(BaseMetric): 

257 """ 

258 Calculate the gap between consecutive observations within a night, in hours. 

259 

260 Parameters 

261 ---------- 

262 reduceFunc : function, optional 

263 Function that can operate on array-like structures. Typically numpy function. 

264 Default np.median. 

265 """ 

266 

267 def __init__(self, mjdCol='observationStartMJD', nightCol='night', reduceFunc=np.median, 

268 metricName='Median Intra-Night Gap', **kwargs): 

269 units = 'hours' 

270 self.mjdCol = mjdCol 

271 self.nightCol = nightCol 

272 self.reduceFunc = reduceFunc 

273 super(IntraNightGapsMetric, self).__init__(col=[self.mjdCol, self.nightCol], 

274 units=units, metricName=metricName, **kwargs) 

275 

276 def run(self, dataSlice, slicePoint=None): 

277 """Calculate the (reduceFunc) of the gap between consecutive obervations within a night. 

278 

279 Parameters 

280 ---------- 

281 dataSlice : numpy.array 

282 Numpy structured array containing the data related to the visits provided by the slicer. 

283 slicePoint : dict, optional 

284 Dictionary containing information about the slicepoint currently active in the slicer. 

285 

286 Returns 

287 ------- 

288 float 

289 The (reduceFunc) value of the gap, in hours. 

290 """ 

291 dataSlice.sort(order=self.mjdCol) 

292 dt = np.diff(dataSlice[self.mjdCol]) 

293 dn = np.diff(dataSlice[self.nightCol]) 

294 

295 good = np.where(dn == 0) 

296 if np.size(good[0]) == 0: 

297 result = self.badval 

298 else: 

299 result = self.reduceFunc(dt[good]) * 24 

300 return result 

301 

302 

303class InterNightGapsMetric(BaseMetric): 

304 """ 

305 Calculate the gap between consecutive observations in different nights, in days. 

306 

307 Parameters 

308 ---------- 

309 reduceFunc : function, optional 

310 Function that can operate on array-like structures. Typically numpy function. 

311 Default np.median. 

312 """ 

313 def __init__(self, mjdCol='observationStartMJD', nightCol='night', reduceFunc=np.median, 

314 metricName='Median Inter-Night Gap', **kwargs): 

315 units = 'days' 

316 self.mjdCol = mjdCol 

317 self.nightCol = nightCol 

318 self.reduceFunc = reduceFunc 

319 super(InterNightGapsMetric, self).__init__(col=[self.mjdCol, self.nightCol], 

320 units=units, metricName=metricName, **kwargs) 

321 

322 def run(self, dataSlice, slicePoint=None): 

323 """Calculate the (reduceFunc) of the gap between consecutive nights of observations. 

324 Parameters 

325 ---------- 

326 dataSlice : numpy.array 

327 Numpy structured array containing the data related to the visits provided by the slicer. 

328 slicePoint : dict, optional 

329 Dictionary containing information about the slicepoint currently active in the slicer. 

330 

331 Returns 

332 ------- 

333 float 

334 The (reduceFunc) of the gap between consecutive nights of observations, in days. 

335 """ 

336 dataSlice.sort(order=self.mjdCol) 

337 unights = np.unique(dataSlice[self.nightCol]) 

338 if np.size(unights) < 2: 

339 result = self.badval 

340 else: 

341 # Find the first and last observation of each night 

342 firstOfNight = np.searchsorted(dataSlice[self.nightCol], unights) 

343 lastOfNight = np.searchsorted(dataSlice[self.nightCol], unights, side='right') - 1 

344 diff = dataSlice[self.mjdCol][firstOfNight[1:]] - dataSlice[self.mjdCol][lastOfNight[:-1]] 

345 result = self.reduceFunc(diff) 

346 return result 

347 

348 

349class VisitGapMetric(BaseMetric): 

350 """ 

351 Calculate the gap between any consecutive observations, in hours, regardless of night boundaries. 

352 

353 Parameters 

354 ---------- 

355 reduceFunc : function, optional 

356 Function that can operate on array-like structures. Typically numpy function. 

357 Default np.median. 

358 """ 

359 def __init__(self, mjdCol='observationStartMJD', nightCol='night', reduceFunc=np.median, 

360 metricName='VisitGap', **kwargs): 

361 units = 'hours' 

362 self.mjdCol = mjdCol 

363 self.nightCol = nightCol 

364 self.reduceFunc = reduceFunc 

365 super().__init__(col=[self.mjdCol, self.nightCol], 

366 units=units, metricName=metricName, **kwargs) 

367 

368 def run(self, dataSlice, slicePoint=None): 

369 """Calculate the (reduceFunc) of the gap between consecutive observations. 

370 

371 Different from inter-night and intra-night gaps, between this is really just counting 

372 all of the times between consecutive observations (not time between nights or time within a night). 

373 

374 Parameters 

375 ---------- 

376 dataSlice : numpy.array 

377 Numpy structured array containing the data related to the visits provided by the slicer. 

378 slicePoint : dict, optional 

379 Dictionary containing information about the slicepoint currently active in the slicer. 

380 

381 Returns 

382 ------- 

383 float 

384 The (reduceFunc) of the time between consecutive observations, in hours. 

385 """ 

386 dataSlice.sort(order=self.mjdCol) 

387 diff = np.diff(dataSlice[self.mjdCol]) 

388 result = self.reduceFunc(diff) * 24. 

389 return result