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 

2import astropy.units as u 

3import logging 

4import lsst.geom as geom 

5from lsst.faro.utils.filtermatches import filterMatches 

6from lsst.faro.utils.coord_util import averageRaFromCat, averageDecFromCat, sphDist 

7 

8__all__ = ( 

9 "astromRms", 

10 "astromResiduals", 

11 "calcRmsDistances", 

12 "calcSepOutliers", 

13 "matchVisitComputeDistance", 

14 "calcRmsDistancesVsRef", 

15) 

16 

17 

18def astromRms( 

19 matchedCatalog, mag_bright_cut, mag_faint_cut, annulus_r, width, **filterargs 

20): 

21 filteredCat = filterMatches(matchedCatalog, **filterargs) 

22 

23 magRange = np.array([mag_bright_cut, mag_faint_cut]) * u.mag 

24 D = annulus_r * u.arcmin 

25 width = width * u.arcmin 

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

27 

28 # Require at least 2 measurements to calculate the repeatability: 

29 nMinMeas = 2 

30 if filteredCat.count > nMinMeas: 

31 astrom_resid_rms_meas = calcRmsDistances( 

32 filteredCat, annulus, magRange=magRange 

33 ) 

34 return astrom_resid_rms_meas 

35 else: 

36 return {"nomeas": np.nan * u.marcsec} 

37 

38 

39def astromResiduals( 

40 matchedCatalog, mag_bright_cut, mag_faint_cut, annulus_r, width, **filterargs 

41): 

42 filteredCat = filterMatches(matchedCatalog, **filterargs) 

43 

44 magRange = np.array([mag_bright_cut, mag_faint_cut]) * u.mag 

45 D = annulus_r * u.arcmin 

46 width = width * u.arcmin 

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

48 

49 # Require at least 2 measurements to calculate the repeatability: 

50 nMinMeas = 2 

51 if filteredCat.count > nMinMeas: 

52 astrom_resid_meas = calcSepOutliers(filteredCat, annulus, magRange=magRange) 

53 return astrom_resid_meas 

54 else: 

55 return {"nomeas": np.nan * u.marcsec} 

56 

57 

58def calcRmsDistances(groupView, annulus, magRange, verbose=False): 

59 """Calculate the RMS distance of a set of matched objects over visits. 

60 Parameters 

61 ---------- 

62 groupView : lsst.afw.table.GroupView 

63 GroupView object of matched observations from MultiMatch. 

64 annulus : length-2 `astropy.units.Quantity` 

65 Distance range (i.e., arcmin) in which to compare objects. 

66 E.g., `annulus=np.array([19, 21]) * u.arcmin` would consider all 

67 objects separated from each other between 19 and 21 arcminutes. 

68 magRange : length-2 `astropy.units.Quantity` 

69 Magnitude range from which to select objects. 

70 verbose : bool, optional 

71 Output additional information on the analysis steps. 

72 Returns 

73 ------- 

74 rmsDistances : `astropy.units.Quantity` 

75 RMS angular separations of a set of matched objects over visits. 

76 """ 

77 log = logging.getLogger(__name__) 

78 

79 # First we make a list of the keys that we want the fields for 

80 importantKeys = [ 

81 groupView.schema.find(name).key 

82 for name in [ 

83 "id", 

84 "coord_ra", 

85 "coord_dec", 

86 "object", 

87 "visit", 

88 "base_PsfFlux_mag", 

89 ] 

90 ] 

91 

92 minMag, maxMag = magRange.to(u.mag).value 

93 

94 def magInRange(cat): 

95 mag = cat.get("base_PsfFlux_mag") 

96 (w,) = np.where(np.isfinite(mag)) 

97 medianMag = np.median(mag[w]) 

98 return minMag <= medianMag and medianMag < maxMag 

99 

100 groupViewInMagRange = groupView.where(magInRange) 

101 

102 # List of lists of id, importantValue 

103 matchKeyOutput = [ 

104 obj.get(key) for key in importantKeys for obj in groupViewInMagRange.groups 

105 ] 

106 

107 jump = len(groupViewInMagRange) 

108 

109 ra = matchKeyOutput[1 * jump: 2 * jump] 

110 dec = matchKeyOutput[2 * jump: 3 * jump] 

111 visit = matchKeyOutput[4 * jump: 5 * jump] 

112 

113 # Calculate the mean position of each object from its constituent visits 

114 # `aggregate` calculates a quantity for each object in the groupView. 

115 meanRa = groupViewInMagRange.aggregate(averageRaFromCat) 

116 meanDec = groupViewInMagRange.aggregate(averageDecFromCat) 

117 

118 annulusRadians = arcminToRadians(annulus.to(u.arcmin).value) 

119 

120 rmsDistances = list() 

121 for obj1, (ra1, dec1, visit1) in enumerate(zip(meanRa, meanDec, visit)): 

122 dist = sphDist(ra1, dec1, meanRa[obj1 + 1:], meanDec[obj1 + 1:]) 

123 (objectsInAnnulus,) = np.where( 

124 (annulusRadians[0] <= dist) & (dist < annulusRadians[1]) 

125 ) 

126 objectsInAnnulus += obj1 + 1 

127 for obj2 in objectsInAnnulus: 

128 distances = matchVisitComputeDistance( 

129 visit[obj1], ra[obj1], dec[obj1], visit[obj2], ra[obj2], dec[obj2] 

130 ) 

131 if not distances: 

132 if verbose: 

133 log.debug( 

134 "No matching visits " "found for objs: %d and %d" % obj1, obj2 

135 ) 

136 continue 

137 

138 (finiteEntries,) = np.where(np.isfinite(distances)) 

139 # Need at least 2 distances to get a finite sample stdev 

140 if len(finiteEntries) > 1: 

141 # ddof=1 to get sample standard deviation (e.g., 1/(n-1)) 

142 rmsDist = np.std(np.array(distances)[finiteEntries], ddof=1) 

143 rmsDistances.append(rmsDist) 

144 

145 # return quantity 

146 rmsDistances = np.array(rmsDistances) * u.radian 

147 return rmsDistances 

148 

149 

150def calcSepOutliers(groupView, annulus, magRange, verbose=False): 

151 """Calculate the RMS distance of a set of matched objects over visits. 

152 Parameters 

153 ---------- 

154 groupView : lsst.afw.table.GroupView 

155 GroupView object of matched observations from MultiMatch. 

156 annulus : length-2 `astropy.units.Quantity` 

157 Distance range (i.e., arcmin) in which to compare objects. 

158 E.g., `annulus=np.array([19, 21]) * u.arcmin` would consider all 

159 objects separated from each other between 19 and 21 arcminutes. 

160 magRange : length-2 `astropy.units.Quantity` 

161 Magnitude range from which to select objects. 

162 verbose : bool, optional 

163 Output additional information on the analysis steps. 

164 Returns 

165 ------- 

166 rmsDistances : `astropy.units.Quantity` 

167 RMS angular separations of a set of matched objects over visits. 

168 """ 

169 

170 log = logging.getLogger(__name__) 

171 

172 # First we make a list of the keys that we want the fields for 

173 importantKeys = [ 

174 groupView.schema.find(name).key 

175 for name in [ 

176 "id", 

177 "coord_ra", 

178 "coord_dec", 

179 "object", 

180 "visit", 

181 "base_PsfFlux_mag", 

182 ] 

183 ] 

184 

185 minMag, maxMag = magRange.to(u.mag).value 

186 

187 def magInRange(cat): 

188 mag = cat.get("base_PsfFlux_mag") 

189 (w,) = np.where(np.isfinite(mag)) 

190 medianMag = np.median(mag[w]) 

191 return minMag <= medianMag and medianMag < maxMag 

192 

193 groupViewInMagRange = groupView.where(magInRange) 

194 

195 # List of lists of id, importantValue 

196 matchKeyOutput = [ 

197 obj.get(key) for key in importantKeys for obj in groupViewInMagRange.groups 

198 ] 

199 

200 jump = len(groupViewInMagRange) 

201 

202 ra = matchKeyOutput[1 * jump: 2 * jump] 

203 dec = matchKeyOutput[2 * jump: 3 * jump] 

204 visit = matchKeyOutput[4 * jump: 5 * jump] 

205 

206 # Calculate the mean position of each object from its constituent visits 

207 # `aggregate` calulates a quantity for each object in the groupView. 

208 meanRa = groupViewInMagRange.aggregate(averageRaFromCat) 

209 meanDec = groupViewInMagRange.aggregate(averageDecFromCat) 

210 

211 annulusRadians = arcminToRadians(annulus.to(u.arcmin).value) 

212 

213 sepResiduals = list() 

214 for obj1, (ra1, dec1, visit1) in enumerate(zip(meanRa, meanDec, visit)): 

215 dist = sphDist(ra1, dec1, meanRa[obj1 + 1:], meanDec[obj1 + 1:]) 

216 (objectsInAnnulus,) = np.where( 

217 (annulusRadians[0] <= dist) & (dist < annulusRadians[1]) 

218 ) 

219 objectsInAnnulus += obj1 + 1 

220 for obj2 in objectsInAnnulus: 

221 distances = matchVisitComputeDistance( 

222 visit[obj1], ra[obj1], dec[obj1], visit[obj2], ra[obj2], dec[obj2] 

223 ) 

224 if not distances: 

225 if verbose: 

226 log.debug( 

227 "No matching visits found " "for objs: %d and %d" % obj1, obj2 

228 ) 

229 continue 

230 

231 (finiteEntries,) = np.where(np.isfinite(distances)) 

232 # Need at least 3 matched pairs so that the median position makes sense 

233 if len(finiteEntries) >= 3: 

234 okdist = np.array(distances)[finiteEntries] 

235 # Get rid of zeros from stars measured against themselves: 

236 (realdist,) = np.where(okdist > 0.0) 

237 if np.size(realdist) > 0: 

238 sepResiduals.append( 

239 np.abs(okdist[realdist] - np.median(okdist[realdist])) 

240 ) 

241 

242 # return quantity 

243 # import pdb; pdb.set_trace() 

244 if len(sepResiduals) > 0: 

245 sepResiduals = np.concatenate(np.array(sepResiduals)) * u.radian 

246 return sepResiduals 

247 

248 

249def matchVisitComputeDistance( 

250 visit_obj1, ra_obj1, dec_obj1, visit_obj2, ra_obj2, dec_obj2 

251): 

252 """Calculate obj1-obj2 distance for each visit in which both objects are seen. 

253 For each visit shared between visit_obj1 and visit_obj2, 

254 calculate the spherical distance between the obj1 and obj2. 

255 visit_obj1 and visit_obj2 are assumed to be unsorted. 

256 Parameters 

257 ---------- 

258 visit_obj1 : scalar, list, or numpy.array of int or str 

259 List of visits for object 1. 

260 ra_obj1 : scalar, list, or numpy.array of float 

261 List of RA in each visit for object 1. [radians] 

262 dec_obj1 : scalar, list or numpy.array of float 

263 List of Dec in each visit for object 1. [radians] 

264 visit_obj2 : list or numpy.array of int or str 

265 List of visits for object 2. 

266 ra_obj2 : list or numpy.array of float 

267 List of RA in each visit for object 2. [radians] 

268 dec_obj2 : list or numpy.array of float 

269 List of Dec in each visit for object 2. [radians] 

270 Results 

271 ------- 

272 list of float 

273 spherical distances (in radians) for matching visits. 

274 """ 

275 distances = [] 

276 visit_obj1_idx = np.argsort(visit_obj1) 

277 visit_obj2_idx = np.argsort(visit_obj2) 

278 j_raw = 0 

279 j = visit_obj2_idx[j_raw] 

280 for i in visit_obj1_idx: 

281 while (visit_obj2[j] < visit_obj1[i]) and (j_raw < len(visit_obj2_idx) - 1): 

282 j_raw += 1 

283 j = visit_obj2_idx[j_raw] 

284 if visit_obj2[j] == visit_obj1[i]: 

285 if np.isfinite([ra_obj1[i], dec_obj1[i], ra_obj2[j], dec_obj2[j]]).all(): 

286 distances.append( 

287 sphDist(ra_obj1[i], dec_obj1[i], ra_obj2[j], dec_obj2[j]) 

288 ) 

289 return distances 

290 

291 

292def calcRmsDistancesVsRef(groupView, refVisit, magRange, band, verbose=False): 

293 """Calculate the RMS distance of a set of matched objects over visits. 

294 Parameters 

295 ---------- 

296 groupView : lsst.afw.table.GroupView 

297 GroupView object of matched observations from MultiMatch. 

298 magRange : length-2 `astropy.units.Quantity` 

299 Magnitude range from which to select objects. 

300 verbose : bool, optional 

301 Output additional information on the analysis steps. 

302 Returns 

303 ------- 

304 rmsDistances : `astropy.units.Quantity` 

305 RMS angular separations of a set of matched objects over visits. 

306 separations : `astropy.units.Quantity` 

307 Angular separations of the set a matched objects. 

308 """ 

309 

310 minMag, maxMag = magRange.to(u.mag).value 

311 

312 def magInRange(cat): 

313 mag = cat.get("base_PsfFlux_mag") 

314 (w,) = np.where(np.isfinite(mag)) 

315 medianMag = np.median(mag[w]) 

316 return minMag <= medianMag and medianMag < maxMag 

317 

318 groupViewInMagRange = groupView.where(magInRange) 

319 

320 # Get lists of the unique objects and visits: 

321 uniqObj = groupViewInMagRange.ids 

322 uniqVisits = set() 

323 for id in uniqObj: 

324 for v, f in zip( 

325 groupViewInMagRange[id].get("visit"), groupViewInMagRange[id].get("filt") 

326 ): 

327 if f == band: 

328 uniqVisits.add(v) 

329 

330 uniqVisits = list(uniqVisits) 

331 

332 if not isinstance(refVisit, int): 

333 refVisit = int(refVisit) 

334 

335 if refVisit in uniqVisits: 

336 # Remove the reference visit from the set of visits: 

337 uniqVisits.remove(refVisit) 

338 

339 rmsDistances = list() 

340 

341 # Loop over visits, calculating the RMS for each: 

342 for vis in uniqVisits: 

343 

344 distancesVisit = list() 

345 

346 for obj in uniqObj: 

347 visMatch = np.where(groupViewInMagRange[obj].get("visit") == vis) 

348 refMatch = np.where(groupViewInMagRange[obj].get("visit") == refVisit) 

349 

350 raObj = groupViewInMagRange[obj].get("coord_ra") 

351 decObj = groupViewInMagRange[obj].get("coord_dec") 

352 

353 # Require it to have a match in both the reference and visit image: 

354 if np.size(visMatch[0]) > 0 and np.size(refMatch[0]) > 0: 

355 distances = sphDist( 

356 raObj[refMatch], decObj[refMatch], raObj[visMatch], decObj[visMatch] 

357 ) 

358 

359 distancesVisit.append(distances) 

360 

361 finiteEntries = np.where(np.isfinite(distancesVisit))[0] 

362 # Need at least 2 distances to get a finite sample stdev 

363 if len(finiteEntries) > 1: 

364 # Calculate the RMS of these offsets: 

365 # ddof=1 to get sample standard deviation (e.g., 1/(n-1)) 

366 pos_rms_rad = np.std(np.array(distancesVisit)[finiteEntries], ddof=1) 

367 pos_rms_mas = geom.radToMas(pos_rms_rad) # milliarcsec 

368 rmsDistances.append(pos_rms_mas) 

369 

370 else: 

371 rmsDistances.append(np.nan) 

372 

373 rmsDistances = np.array(rmsDistances) * u.marcsec 

374 return rmsDistances 

375 

376 

377def radiansToMilliarcsec(rad): 

378 return np.rad2deg(rad) * 3600 * 1000 

379 

380 

381def arcminToRadians(arcmin): 

382 return np.deg2rad(arcmin / 60)