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 astropy.units as u 

2import numpy as np 

3from lsst.pipe.base import Struct, Task 

4from lsst.pex.config import Config, Field, ListField 

5from lsst.verify import Measurement, Datum 

6from lsst.faro.utils.filtermatches import filterMatches 

7from lsst.faro.utils.separations import (calcRmsDistances, calcRmsDistancesVsRef, 

8 astromResiduals) 

9from lsst.faro.utils.phot_repeat import photRepeat 

10 

11 

12__all__ = ("PA1TaskConfig", "PA1Task", "PF1TaskConfig", "PF1Task", 

13 "AMxTaskConfig", "AMxTask", "ADxTask", "AFxTask", "AB1TaskConfig", "AB1Task") 

14 

15 

16filter_dict = {'u': 1, 'g': 2, 'r': 3, 'i': 4, 'z': 5, 'y': 6, 

17 'HSC-U': 1, 'HSC-G': 2, 'HSC-R': 3, 'HSC-I': 4, 'HSC-Z': 5, 'HSC-Y': 6} 

18 

19 

20class PA1TaskConfig(Config): 

21 """Config fields for the PA1 photometric repeatability metric. 

22 """ 

23 brightSnrMin = Field(doc="Minimum median SNR for a source to be considered bright.", 

24 dtype=float, default=200) 

25 brightSnrMax = Field(doc="Maximum median SNR for a source to be considered bright.", 

26 dtype=float, default=np.Inf) 

27 nMinPhotRepeat = Field(doc="Minimum number of objects required for photometric repeatability.", 

28 dtype=int, default=50) 

29 writeExtras = Field(doc="Write out the magnitude residuals and rms values for debugging.", 

30 dtype=bool, default=False) 

31 

32 

33class PA1Task(Task): 

34 """A Task that computes the PA1 photometric repeatability metric from an 

35 input set of multiple visits of the same field. 

36 

37 Notes 

38 ----- 

39 The intended usage is to retarget the run method of 

40 `lsst.faro.measurement.TractMatchedMeasurementTask` to PA1Task. 

41 This metric is calculated on a set of matched visits, and aggregated at the tract level. 

42 """ 

43 

44 ConfigClass = PA1TaskConfig 

45 _DefaultName = "PA1Task" 

46 

47 def __init__(self, config: PA1TaskConfig, *args, **kwargs): 

48 super().__init__(*args, config=config, **kwargs) 

49 self.brightSnrMin = self.config.brightSnrMin 

50 self.brightSnrMax = self.config.brightSnrMax 

51 self.nMinPhotRepeat = self.config.nMinPhotRepeat 

52 self.writeExtras = self.config.writeExtras 

53 

54 def run(self, matchedCatalog, metricName): 

55 """Calculate the photometric repeatability. 

56 

57 Parameters 

58 ---------- 

59 matchedCatalog : `lsst.afw.table.base.Catalog` 

60 `~lsst.afw.table.base.Catalog` object as created by 

61 `~lsst.afw.table.multiMatch` matching of sources from multiple visits. 

62 metricName : `str` 

63 The name of the metric. 

64 

65 Returns 

66 ------- 

67 measurement : `lsst.verify.Measurement` 

68 Measurement of the repeatability and its associated metadata. 

69 """ 

70 self.log.info(f"Measuring {metricName}") 

71 

72 pa1 = photRepeat(matchedCatalog, nMinPhotRepeat=self.nMinPhotRepeat, 

73 snrMax=self.brightSnrMax, snrMin=self.brightSnrMin) 

74 

75 if 'magMean' in pa1.keys(): 

76 if self.writeExtras: 

77 extras = {} 

78 extras['rms'] = Datum(pa1['rms'], label='RMS', 

79 description='Photometric repeatability rms for each star.') 

80 extras['count'] = Datum(pa1['count']*u.count, label='count', 

81 description='Number of detections used to calculate ' 

82 'repeatability.') 

83 extras['mean_mag'] = Datum(pa1['magMean'], label='mean_mag', 

84 description='Mean magnitude of each star.') 

85 return Struct(measurement=Measurement("PA1", pa1['repeatability'], extras=extras)) 

86 else: 

87 return Struct(measurement=Measurement("PA1", pa1['repeatability'])) 

88 else: 

89 return Struct(measurement=Measurement("PA1", np.nan*u.mmag)) 

90 

91 

92class PF1TaskConfig(Config): 

93 brightSnrMin = Field(doc="Minimum median SNR for a source to be considered bright.", 

94 dtype=float, default=200) 

95 brightSnrMax = Field(doc="Maximum median SNR for a source to be considered bright.", 

96 dtype=float, default=np.Inf) 

97 nMinPhotRepeat = Field(doc="Minimum number of objects required for photometric repeatability.", 

98 dtype=int, default=50) 

99 # The defaults for threshPA2 correspond to the SRD "design" thresholds. 

100 threshPA2 = Field(doc="Threshold in mmag for PF1 calculation.", dtype=float, default=15.0) 

101 

102 

103class PF1Task(Task): 

104 """A Task that computes PF1, the percentage of photometric repeatability measurements 

105 that deviate by more than PA2 mmag from the mean. 

106 

107 Notes 

108 ----- 

109 The intended usage is to retarget the run method of 

110 `lsst.faro.measurement.TractMatchedMeasurementTask` to PF1Task. This Task uses the 

111 same set of photometric residuals that are calculated for the PA1 metric. 

112 This metric is calculated on a set of matched visits, and aggregated at the tract level. 

113 """ 

114 ConfigClass = PF1TaskConfig 

115 _DefaultName = "PF1Task" 

116 

117 def __init__(self, config: PF1TaskConfig, *args, **kwargs): 

118 super().__init__(*args, config=config, **kwargs) 

119 self.brightSnrMin = self.config.brightSnrMin 

120 self.brightSnrMax = self.config.brightSnrMax 

121 self.nMinPhotRepeat = self.config.nMinPhotRepeat 

122 self.threshPA2 = self.config.threshPA2 

123 

124 def run(self, matchedCatalog, metricName): 

125 """Calculate the percentage of outliers in the photometric repeatability values. 

126 

127 Parameters 

128 ---------- 

129 matchedCatalog : `lsst.afw.table.base.Catalog` 

130 `~lsst.afw.table.base.Catalog` object as created by 

131 `~lsst.afw.table.multiMatch` matching of sources from multiple visits. 

132 metricName : `str` 

133 The name of the metric. 

134 

135 Returns 

136 ------- 

137 measurement : `lsst.verify.Measurement` 

138 Measurement of the percentage of repeatability outliers, and associated metadata. 

139 """ 

140 self.log.info(f"Measuring {metricName}") 

141 pa2_thresh = self.threshPA2 * u.mmag 

142 

143 pf1 = photRepeat(matchedCatalog, nMinPhotRepeat=self.nMinPhotRepeat, 

144 snrMax=self.brightSnrMax, snrMin=self.brightSnrMin) 

145 

146 if 'magResid' in pf1.keys(): 

147 # Previously, validate_drp used the first random sample from PA1 measurement 

148 # Now, use all of them. 

149 # Keep only stars with > 2 observations: 

150 okrms = (pf1['count'] > 2) 

151 magResid0 = pf1['magResid'] 

152 magResid = np.concatenate(magResid0[okrms]) 

153 

154 percentileAtPA2 = 100 * np.mean(np.abs(magResid.value) > pa2_thresh.value) * u.percent 

155 

156 return Struct(measurement=Measurement("PF1", percentileAtPA2)) 

157 else: 

158 return Struct(measurement=Measurement("PF1", np.nan*u.percent)) 

159 

160 

161def isSorted(a): 

162 return all(a[i] <= a[i+1] for i in range(len(a)-1)) 

163 

164 

165def bins(window, n): 

166 delta = window/n 

167 return [i*delta for i in range(n+1)] 

168 

169 

170class AMxTaskConfig(Config): 

171 annulus_r = Field(doc="Radial distance of the annulus in arcmin (5, 20, or 200 for AM1, AM2, AM3)", 

172 dtype=float, default=5.) 

173 width = Field(doc="Width of annulus in arcmin", 

174 dtype=float, default=2.) 

175 bright_mag_cut = Field(doc="Bright limit of catalog entries to include", 

176 dtype=float, default=17.0) 

177 faint_mag_cut = Field(doc="Faint limit of catalog entries to include", 

178 dtype=float, default=21.5) 

179 # The defaults for threshADx and threshAFx correspond to the SRD "design" thresholds. 

180 threshAD = Field(doc="Threshold in mas for AFx calculation.", dtype=float, default=20.0) 

181 threshAF = Field(doc="Percentile of differences that can vary by more than threshAD.", 

182 dtype=float, default=10.0) 

183 bins = ListField(doc="Bins for histogram.", 

184 dtype=float, minLength=2, maxLength=1500, 

185 listCheck=isSorted, default=bins(30, 200)) 

186 

187 

188class AMxTask(Task): 

189 ConfigClass = AMxTaskConfig 

190 _DefaultName = "AMxTask" 

191 

192 def run(self, matchedCatalog, metric_name): 

193 self.log.info(f"Measuring {metric_name}") 

194 

195 filteredCat = filterMatches(matchedCatalog) 

196 

197 magRange = np.array([self.config.bright_mag_cut, self.config.faint_mag_cut]) * u.mag 

198 D = self.config.annulus_r * u.arcmin 

199 width = self.config.width * u.arcmin 

200 annulus = D + (width/2)*np.array([-1, +1]) 

201 

202 rmsDistances = calcRmsDistances( 

203 filteredCat, 

204 annulus, 

205 magRange=magRange) 

206 

207 values, bins = np.histogram(rmsDistances.to(u.marcsec), bins=self.config.bins*u.marcsec) 

208 extras = {'bins': Datum(bins, label='binvalues', description='bins'), 

209 'values': Datum(values*u.count, label='counts', description='icounts in bins')} 

210 

211 if len(rmsDistances) == 0: 

212 return Struct(measurement=Measurement(metric_name, np.nan*u.marcsec, extras=extras)) 

213 

214 return Struct(measurement=Measurement(metric_name, np.median(rmsDistances.to(u.marcsec)), 

215 extras=extras)) 

216 

217 

218class ADxTask(Task): 

219 ConfigClass = AMxTaskConfig 

220 _DefaultName = "ADxTask" 

221 

222 def run(self, matchedCatalog, metric_name): 

223 self.log.info(f"Measuring {metric_name}") 

224 

225 sepDistances = astromResiduals(matchedCatalog, self.config.bright_mag_cut, 

226 self.config.faint_mag_cut, self.config.annulus_r, 

227 self.config.width) 

228 

229 afThresh = self.config.threshAF * u.percent 

230 afPercentile = 100.0*u.percent - afThresh 

231 

232 if len(sepDistances) <= 1: 

233 return Struct(measurement=Measurement(metric_name, np.nan*u.marcsec)) 

234 else: 

235 # absolute value of the difference between each astrometric rms 

236 # and the median astrometric RMS 

237 # absRmsDiffs = np.abs(rmsDistances - np.median(rmsDistances)).to(u.marcsec) 

238 absDiffsMarcsec = (sepDistances-np.median(sepDistances)).to(u.marcsec) 

239 return Struct(measurement=Measurement(metric_name, np.percentile(absDiffsMarcsec.value, 

240 afPercentile.value)*u.marcsec)) 

241 

242 

243class AFxTask(Task): 

244 ConfigClass = AMxTaskConfig 

245 _DefaultName = "AFxTask" 

246 

247 def run(self, matchedCatalog, metric_name): 

248 self.log.info(f"Measuring {metric_name}") 

249 

250 sepDistances = astromResiduals(matchedCatalog, self.config.bright_mag_cut, 

251 self.config.faint_mag_cut, self.config.annulus_r, 

252 self.config.width) 

253 

254 adxThresh = self.config.threshAD * u.marcsec 

255 

256 if len(sepDistances) <= 1: 

257 return Struct(measurement=Measurement(metric_name, np.nan*u.percent)) 

258 else: 

259 # absolute value of the difference between each astrometric rms 

260 # and the median astrometric RMS 

261 # absRmsDiffs = np.abs(rmsDistances - np.median(rmsDistances)).to(u.marcsec) 

262 absDiffsMarcsec = (sepDistances-np.median(sepDistances)).to(u.marcsec) 

263 percentileAtADx = 100 * np.mean(np.abs(absDiffsMarcsec.value) > adxThresh.value) * u.percent 

264 return Struct(measurement=Measurement(metric_name, percentileAtADx)) 

265 

266 

267class AB1TaskConfig(Config): 

268 bright_mag_cut = Field(doc="Bright limit of catalog entries to include", 

269 dtype=float, default=17.0) 

270 faint_mag_cut = Field(doc="Faint limit of catalog entries to include", 

271 dtype=float, default=21.5) 

272 ref_filter = Field(doc="String representing the filter to use as reference", 

273 dtype=str, default="r") 

274 

275 

276class AB1Task(Task): 

277 ConfigClass = AB1TaskConfig 

278 _DefaultName = "AB1Task" 

279 

280 def run(self, matchedCatalogMulti, metric_name, in_id, out_id): 

281 self.log.info(f"Measuring {metric_name}") 

282 

283 if self.config.ref_filter not in filter_dict: 

284 raise Exception('Reference filter supplied for AB1 not in dictionary.') 

285 

286 filteredCat = filterMatches(matchedCatalogMulti) 

287 rmsDistancesAll = [] 

288 

289 if len(filteredCat) > 0: 

290 

291 filtnum = filter_dict[self.config.ref_filter] 

292 

293 refVisits = set() 

294 for id in filteredCat.ids: 

295 grptmp = filteredCat[id] 

296 filtmch = (grptmp['filt'] == filtnum) 

297 if len(filtmch) > 0: 

298 refVisits.update(set(grptmp[filtmch]['visit'])) 

299 

300 refVisits = list(refVisits) 

301 

302 magRange = np.array([self.config.bright_mag_cut, self.config.faint_mag_cut]) * u.mag 

303 for rv in refVisits: 

304 rmsDistances = calcRmsDistancesVsRef( 

305 filteredCat, 

306 rv, 

307 magRange=magRange, 

308 band=filter_dict[out_id['band']]) 

309 finiteEntries = np.where(np.isfinite(rmsDistances))[0] 

310 if len(finiteEntries) > 0: 

311 rmsDistancesAll.append(rmsDistances[finiteEntries]) 

312 

313 if len(rmsDistancesAll) == 0: 

314 return Struct(measurement=Measurement(metric_name, np.nan*u.marcsec)) 

315 else: 

316 rmsDistancesAll = np.concatenate(rmsDistancesAll) 

317 return Struct(measurement=Measurement(metric_name, np.mean(rmsDistancesAll))) 

318 

319 else: 

320 return Struct(measurement=Measurement(metric_name, np.nan*u.marcsec))