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

1"""A group of metrics that work together to evaluate season characteristics (length, number, etc). 

2In addition, these supports the time delay metric calculation for strong lensing. 

3""" 

4 

5import numpy as np 

6from .baseMetric import BaseMetric 

7from lsst.sims.photUtils import Dust_values 

8 

9__all__ = ['calcSeason', 'findSeasonEdges', 

10 'SeasonLengthMetric', 'CampaignLengthMetric', 

11 'MeanCampaignFrequencyMetric', 'TdcMetric'] 

12 

13 

14def calcSeason(ra, time): 

15 """Calculate the 'season' in the survey for a series of ra/dec/time values of an observation. 

16 Based only on the RA of the point on the sky, it calculates the 'season' based on when this 

17 point would be overhead .. the season is considered +/- 0.5 years around this time. 

18 

19 Parameters 

20 ---------- 

21 ra : float 

22 The RA (in degrees) of the point on the sky 

23 time : np.ndarray 

24 The times of the observations, in MJD 

25 

26 Returns 

27 ------- 

28 np.ndarray 

29 The season values 

30 """ 

31 # Reference RA and equinox to anchor ra/season reference - RA = 0 is overhead at autumnal equinox 

32 # autumn equinox 2014 happened on september 23 --> equinox MJD 

33 Equinox = 2456923.5 - 2400000.5 

34 # convert ra into 'days' 

35 dayRA = ra / 360 * 365.25 

36 firstSeasonBegan = Equinox + dayRA - 0.5 * 365.25 

37 seasons = (time - firstSeasonBegan) / 365.25 

38 # Set first season to 0 

39 seasons = seasons - np.floor(np.min(seasons)) 

40 return seasons 

41 

42def findSeasonEdges(seasons): 

43 """Given the seasons, return the indexes of each start/end of the season. 

44 

45 Parameters 

46 ---------- 

47 seasons: np.ndarray 

48 Seasons, such as calculated by calcSeason. 

49 Note that seasons should be sorted!! 

50 

51 Returns 

52 ------- 

53 np.ndarray, np.ndarray 

54 The indexes of the first and last date in the season. 

55 """ 

56 intSeasons = np.floor(seasons) 

57 # Get the unique seasons, so that we can separate each one 

58 season_list = np.unique(intSeasons) 

59 # Find the first and last observation of each season. 

60 firstOfSeason = np.searchsorted(intSeasons, season_list) 

61 lastOfSeason = np.searchsorted(intSeasons, season_list, side='right') - 1 

62 return firstOfSeason, lastOfSeason 

63 

64 

65class SeasonLengthMetric(BaseMetric): 

66 """ 

67 Calculate the length of LSST seasons, in days. 

68 

69 Parameters 

70 ---------- 

71 minExpTime: float, opt 

72 Minimum visit exposure time to count for a 'visit', in seconds. Default 20. 

73 reduceFunc : function, optional 

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

75 This reduces the season length in each season from 10 separate values to a single value. 

76 Default np.median. 

77 """ 

78 def __init__(self, mjdCol='observationStartMJD', expTimeCol='visitExposureTime', minExpTime=20, 

79 reduceFunc=np.median, metricName='SeasonLength', **kwargs): 

80 units = 'days' 

81 self.mjdCol = mjdCol 

82 self.expTimeCol = expTimeCol 

83 self.minExpTime = minExpTime 

84 self.reduceFunc = reduceFunc 

85 super().__init__(col=[self.mjdCol, self.expTimeCol], 

86 units=units, metricName=metricName, **kwargs) 

87 

88 def run(self, dataSlice, slicePoint): 

89 """Calculate the (reduceFunc) of the length of each season. 

90 Uses the slicePoint RA/Dec to calculate the position in question, then uses the times of the visits 

91 to assign them into seasons (based on where the sun is relative to the slicePoint RA). 

92 

93 Parameters 

94 ---------- 

95 dataSlice : numpy.array 

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

97 slicePoint : dict 

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

99 

100 Returns 

101 ------- 

102 float 

103 The (reduceFunc) of the length of each season, in days. 

104 """ 

105 # Order data Slice/times and exclude visits which are too short. 

106 long = np.where(dataSlice[self.expTimeCol] > self.minExpTime) 

107 if len(long[0]) == 0: 

108 return self.badval 

109 data = np.sort(dataSlice[long], order=self.mjdCol) 

110 # SlicePoints ra/dec are always in radians - convert to degrees to calculate season 

111 seasons = calcSeason(np.degrees(slicePoint['ra']), data[self.mjdCol]) 

112 firstOfSeason, lastOfSeason = findSeasonEdges(seasons) 

113 seasonlengths = data[self.mjdCol][lastOfSeason] - data[self.mjdCol][firstOfSeason] 

114 result = self.reduceFunc(seasonlengths) 

115 return result 

116 

117 

118class CampaignLengthMetric(BaseMetric): 

119 """Calculate the number of seasons (roughly, years) a pointing is observed for. 

120 This corresponds to the 'campaign length' for lensed quasar time delays. 

121 """ 

122 def __init__(self, mjdCol='observationStartMJD', expTimeCol='visitExposureTime', minExpTime=20, **kwargs): 

123 units = '' 

124 self.expTimeCol = expTimeCol 

125 self.minExpTime = minExpTime 

126 self.mjdCol = mjdCol 

127 super().__init__(col=[self.mjdCol, self.expTimeCol], units=units, **kwargs) 

128 

129 def run(self, dataSlice, slicePoint): 

130 # Order data Slice/times and exclude visits which are too short. 

131 long = np.where(dataSlice[self.expTimeCol] > self.minExpTime) 

132 if len(long[0]) == 0: 

133 return self.badval 

134 data = np.sort(dataSlice[long], order=self.mjdCol) 

135 seasons = calcSeason(np.degrees(slicePoint['ra']), data[self.mjdCol]) 

136 intSeasons = np.floor(seasons) 

137 count = len(np.unique(intSeasons)) 

138 return count 

139 

140 

141class MeanCampaignFrequencyMetric(BaseMetric): 

142 """Calculate the mean separation between nights, within a season - then the mean over the campaign. 

143 Calculate per season, to avoid any influence from season gaps. 

144 """ 

145 def __init__(self, mjdCol='observationStartMJD', expTimeCol='visitExposureTime', minExpTime=20, 

146 nightCol='night', **kwargs): 

147 self.mjdCol = mjdCol 

148 self.expTimeCol = expTimeCol 

149 self.minExpTime = minExpTime 

150 self.nightCol = nightCol 

151 units = 'nights' 

152 super().__init__(col=[self.mjdCol, self.expTimeCol, self.nightCol], units=units, **kwargs) 

153 

154 def run(self, dataSlice, slicePoint): 

155 # Order data Slice/times and exclude visits which are too short. 

156 long = np.where(dataSlice[self.expTimeCol] > self.minExpTime) 

157 if len(long[0]) == 0: 

158 return self.badval 

159 data = np.sort(dataSlice[long], order=self.mjdCol) 

160 # SlicePoints ra/dec are always in radians - convert to degrees to calculate season 

161 seasons = calcSeason(np.degrees(slicePoint['ra']), data[self.mjdCol]) 

162 firstOfSeason, lastOfSeason = findSeasonEdges(seasons) 

163 seasonMeans = np.zeros(len(firstOfSeason), float) 

164 for i, (first, last) in enumerate(zip(firstOfSeason, lastOfSeason)): 

165 if first < last: 

166 n = data[self.nightCol][first:last+1] 

167 deltaNights = np.diff(np.unique(n)) 

168 if len(deltaNights) > 0: 

169 seasonMeans[i] = np.mean(deltaNights) 

170 return np.mean(seasonMeans) 

171 

172 

173class TdcMetric(BaseMetric): 

174 """Calculate the Time Delay Challenge metric, as described in Liao et al 2015 

175 (https://arxiv.org/pdf/1409.1254.pdf). 

176 

177 This combines the MeanCampaignFrequency/MeanNightSeparation, the SeasonLength, and the CampaignLength 

178 metrics above, but rewritten to calculate season information only once. 

179 

180 cadNorm = in units of days 

181 seaNorm = in units of months 

182 campNorm = in units of years 

183 

184 This metric also adds a requirement to achieve limiting magnitudes after galactic dust extinction, 

185 in various bandpasses, in order to exclude visits which are not useful for detecting quasars 

186 (due to being short or having high sky brightness, etc.) and to reject regions with 

187 high galactic dust extinction. 

188 

189 Parameters 

190 ---------- 

191 mjdCol: str, opt 

192 Column name for mjd. Default observationStartMJD. 

193 nightCol: str, opt 

194 Column name for night. Default night. 

195 filterCol: str, opt 

196 Column name for filter. Default filter. 

197 m5Col: str, opt 

198 Column name for five-sigma depth. Default fiveSigmaDepth. 

199 magCuts: dict, opt 

200 Dictionary with filtername:mag limit (after dust extinction). Default None in kwarg. 

201 Defaults set within metric: {'u': 22.7, 'g': 24.1, 'r': 23.7, 'i': 23.1, 'z': 22.2, 'y': 21.4} 

202 metricName: str, opt 

203 Metric Name. Default TDC. 

204 cadNorm: float, opt 

205 Cadence normalization constant, in units of days. Default 3. 

206 seaNorm: float, opt 

207 Season normalization constant, in units of months. Default 4. 

208 campNorm: float, opt 

209 Campaign length normalization constant, in units of years. Default 5. 

210 badval: float, opt 

211 Return this value instead of the dictionary for bad points. 

212 

213 Returns 

214 ------- 

215 dictionary 

216 Dictionary of values for {'rate', 'precision', 'accuracy'} at this point in the sky. 

217 """ 

218 def __init__(self, mjdCol='observationStartMJD', nightCol='night', filterCol='filter', 

219 m5Col='fiveSigmaDepth', magCuts=None, 

220 metricName = 'TDC', cadNorm=3., seaNorm=4., campNorm=5., badval=-999, **kwargs): 

221 # Save the normalization values. 

222 self.cadNorm = cadNorm 

223 self.seaNorm = seaNorm 

224 self.campNorm = campNorm 

225 self.mjdCol = mjdCol 

226 self.m5Col = m5Col 

227 self.nightCol = nightCol 

228 self.filterCol = filterCol 

229 if magCuts is None: 

230 self.magCuts = {'u': 22.7, 'g': 24.1, 'r': 23.7, 'i': 23.1, 'z': 22.2, 'y': 21.4} 

231 else: 

232 self.magCuts = magCuts 

233 if not isinstance(self.magCuts, dict): 

234 raise Exception('magCuts should be a dictionary') 

235 # Set up dust map requirement 

236 maps = ['DustMap'] 

237 # Set the default wavelength limits for the lsst filters. These are approximately correct. 

238 dust_properties = Dust_values() 

239 self.Ax1 = dust_properties.Ax1 

240 super().__init__(col=[self.mjdCol, self.m5Col, self.nightCol, self.filterCol], 

241 badval=badval, maps=maps, 

242 metricName = metricName, units = '%s' %('%'), **kwargs) 

243 

244 def run(self, dataSlice, slicePoint): 

245 # Calculate dust-extinction limiting magnitudes for each visit. 

246 filterlist = np.unique(dataSlice[self.filterCol]) 

247 m5Dust = np.zeros(len(dataSlice), float) 

248 for f in filterlist: 

249 match = np.where(dataSlice[self.filterCol] == f) 

250 A_x = self.Ax1[f] * slicePoint['ebv'] 

251 m5Dust[match] = dataSlice[self.m5Col][match] - A_x 

252 m5Dust[match] = np.where(m5Dust[match] > self.magCuts[f], m5Dust[match], -999) 

253 idxs = np.where(m5Dust > -998) 

254 if len(idxs[0]) == 0: 

255 return self.badval 

256 data = np.sort(dataSlice[idxs], order=self.mjdCol) 

257 # SlicePoints ra/dec are always in radians - convert to degrees to calculate season 

258 seasons = calcSeason(np.degrees(slicePoint['ra']), data[self.mjdCol]) 

259 intSeasons = np.floor(seasons) 

260 firstOfSeason, lastOfSeason = findSeasonEdges(seasons) 

261 # Campaign length 

262 camp = len(np.unique(intSeasons)) 

263 # Season length 

264 seasonlengths = data[self.mjdCol][lastOfSeason] - data[self.mjdCol][firstOfSeason] 

265 sea = np.median(seasonlengths) 

266 # Convert to months 

267 sea = sea / 30.0 

268 # Campaign frequency / mean night separation 

269 seasonMeans = np.zeros(len(firstOfSeason), float) 

270 for i, (first, last) in enumerate(zip(firstOfSeason, lastOfSeason)): 

271 n = data[self.nightCol][first:last+1] 

272 deltaNights = np.diff(np.unique(n)) 

273 if len(deltaNights) > 0: 

274 seasonMeans[i] = np.mean(deltaNights) 

275 cad = np.mean(seasonMeans) 

276 # Evaluate precision and accuracy for TDC 

277 if sea == 0 or cad == 0 or camp == 0: 

278 return self.badval 

279 else: 

280 accuracy = 0.06 * (self.seaNorm / sea) * \ 

281 (self.campNorm / camp)**(1.1) 

282 precision = 4.0 * (cad / self.cadNorm)**(0.7) * \ 

283 (self.seaNorm/sea)**(0.3) * \ 

284 (self.campNorm / camp)**(0.6) 

285 rate = 30. * (self.cadNorm / cad)**(0.4) * \ 

286 (sea / self.seaNorm)**(0.8) * \ 

287 (self.campNorm / camp)**(0.2) 

288 return {'accuracy':accuracy, 'precision':precision, 'rate':rate, 

289 'cadence':cad, 'season':sea, 'campaign':camp} 

290 

291 def reduceAccuracy(self, metricValue): 

292 return metricValue['accuracy'] 

293 

294 def reducePrecision(self, metricValue): 

295 return metricValue['precision'] 

296 

297 def reduceRate(self, metricValue): 

298 return metricValue['rate'] 

299 

300 def reduceCadence(self, metricValue): 

301 return metricValue['cadence'] 

302 

303 def reduceSeason(self, metricValue): 

304 return metricValue['season'] 

305 

306 def reduceCampaign(self, metricValue): 

307 return metricValue['campaign']