Coverage for python/lsst/faro/utils/separations.py: 7%

158 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-08 09:03 +0000

1# This file is part of faro. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22import numpy as np 

23import astropy.units as u 

24import logging 

25import lsst.geom as geom 

26from lsst.faro.utils.filtermatches import filterMatches 

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

28 

29__all__ = ( 

30 "astromRms", 

31 "astromResiduals", 

32 "calcRmsDistances", 

33 "calcSepOutliers", 

34 "matchVisitComputeDistance", 

35 "calcRmsDistancesVsRef", 

36) 

37 

38 

39def astromRms( 

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_rms_meas = calcRmsDistances( 

53 filteredCat, annulus, magRange=magRange 

54 ) 

55 return astrom_resid_rms_meas 

56 else: 

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

58 

59 

60def astromResiduals( 

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

62): 

63 filteredCat = filterMatches(matchedCatalog, **filterargs) 

64 

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

66 D = annulus_r * u.arcmin 

67 width = width * u.arcmin 

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

69 

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

71 nMinMeas = 2 

72 if filteredCat.count > nMinMeas: 

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

74 return astrom_resid_meas 

75 else: 

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

77 

78 

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

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

81 Parameters 

82 ---------- 

83 groupView : lsst.afw.table.GroupView 

84 GroupView object of matched observations from MultiMatch. 

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

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

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

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

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

90 Magnitude range from which to select objects. 

91 verbose : bool, optional 

92 Output additional information on the analysis steps. 

93 Returns 

94 ------- 

95 rmsDistances : `astropy.units.Quantity` 

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

97 """ 

98 log = logging.getLogger(__name__) 

99 

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

101 importantKeys = [ 

102 groupView.schema.find(name).key 

103 for name in [ 

104 "id", 

105 "coord_ra", 

106 "coord_dec", 

107 "object", 

108 "visit", 

109 "base_PsfFlux_mag", 

110 ] 

111 ] 

112 

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

114 

115 def magInRange(cat): 

116 mag = cat.get("base_PsfFlux_mag") 

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

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

119 return minMag <= medianMag and medianMag < maxMag 

120 

121 groupViewInMagRange = groupView.where(magInRange) 

122 

123 # List of lists of id, importantValue 

124 matchKeyOutput = [ 

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

126 ] 

127 

128 jump = len(groupViewInMagRange) 

129 

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

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

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

133 

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

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

136 meanRa = groupViewInMagRange.aggregate(averageRaFromCat) 

137 meanDec = groupViewInMagRange.aggregate(averageDecFromCat) 

138 

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

140 

141 rmsDistances = list() 

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

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

144 (objectsInAnnulus,) = np.where( 

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

146 ) 

147 objectsInAnnulus += obj1 + 1 

148 for obj2 in objectsInAnnulus: 

149 distances = matchVisitComputeDistance( 

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

151 ) 

152 if not distances: 

153 if verbose: 

154 log.debug( 

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

156 ) 

157 continue 

158 

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

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

161 if len(finiteEntries) > 1: 

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

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

164 rmsDistances.append(rmsDist) 

165 

166 # return quantity 

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

168 return rmsDistances 

169 

170 

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

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

173 Parameters 

174 ---------- 

175 groupView : lsst.afw.table.GroupView 

176 GroupView object of matched observations from MultiMatch. 

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

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

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

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

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

182 Magnitude range from which to select objects. 

183 verbose : bool, optional 

184 Output additional information on the analysis steps. 

185 Returns 

186 ------- 

187 rmsDistances : `astropy.units.Quantity` 

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

189 """ 

190 

191 log = logging.getLogger(__name__) 

192 

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

194 importantKeys = [ 

195 groupView.schema.find(name).key 

196 for name in [ 

197 "id", 

198 "coord_ra", 

199 "coord_dec", 

200 "object", 

201 "visit", 

202 "base_PsfFlux_mag", 

203 ] 

204 ] 

205 

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

207 

208 def magInRange(cat): 

209 mag = cat.get("base_PsfFlux_mag") 

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

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

212 return minMag <= medianMag and medianMag < maxMag 

213 

214 groupViewInMagRange = groupView.where(magInRange) 

215 

216 # List of lists of id, importantValue 

217 matchKeyOutput = [ 

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

219 ] 

220 

221 jump = len(groupViewInMagRange) 

222 

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

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

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

226 

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

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

229 meanRa = groupViewInMagRange.aggregate(averageRaFromCat) 

230 meanDec = groupViewInMagRange.aggregate(averageDecFromCat) 

231 

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

233 

234 sepResiduals = list() 

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

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

237 (objectsInAnnulus,) = np.where( 

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

239 ) 

240 objectsInAnnulus += obj1 + 1 

241 for obj2 in objectsInAnnulus: 

242 distances = matchVisitComputeDistance( 

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

244 ) 

245 if not distances: 

246 if verbose: 

247 log.debug( 

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

249 ) 

250 continue 

251 

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

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

254 if len(finiteEntries) >= 3: 

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

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

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

258 if np.size(realdist) > 0: 

259 sepResiduals.append( 

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

261 ) 

262 

263 # return quantity 

264 # import pdb; pdb.set_trace() 

265 if len(sepResiduals) > 0: 

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

267 return sepResiduals 

268 

269 

270def matchVisitComputeDistance( 

271 visit_obj1, ra_obj1, dec_obj1, visit_obj2, ra_obj2, dec_obj2 

272): 

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

274 For each visit shared between visit_obj1 and visit_obj2, 

275 calculate the spherical distance between the obj1 and obj2. 

276 visit_obj1 and visit_obj2 are assumed to be unsorted. 

277 Parameters 

278 ---------- 

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

280 List of visits for object 1. 

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

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

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

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

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

286 List of visits for object 2. 

287 ra_obj2 : list or numpy.array of float 

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

289 dec_obj2 : list or numpy.array of float 

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

291 Results 

292 ------- 

293 list of float 

294 spherical distances (in radians) for matching visits. 

295 """ 

296 distances = [] 

297 visit_obj1_idx = np.argsort(visit_obj1) 

298 visit_obj2_idx = np.argsort(visit_obj2) 

299 j_raw = 0 

300 j = visit_obj2_idx[j_raw] 

301 for i in visit_obj1_idx: 

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

303 j_raw += 1 

304 j = visit_obj2_idx[j_raw] 

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

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

307 distances.append( 

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

309 ) 

310 return distances 

311 

312 

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

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

315 Parameters 

316 ---------- 

317 groupView : lsst.afw.table.GroupView 

318 GroupView object of matched observations from MultiMatch. 

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

320 Magnitude range from which to select objects. 

321 verbose : bool, optional 

322 Output additional information on the analysis steps. 

323 Returns 

324 ------- 

325 rmsDistances : `astropy.units.Quantity` 

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

327 separations : `astropy.units.Quantity` 

328 Angular separations of the set a matched objects. 

329 """ 

330 

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

332 

333 def magInRange(cat): 

334 mag = cat.get("base_PsfFlux_mag") 

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

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

337 return minMag <= medianMag and medianMag < maxMag 

338 

339 groupViewInMagRange = groupView.where(magInRange) 

340 

341 # Get lists of the unique objects and visits: 

342 uniqObj = groupViewInMagRange.ids 

343 uniqVisits = set() 

344 for id in uniqObj: 

345 for v, f in zip( 

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

347 ): 

348 if f == band: 

349 uniqVisits.add(v) 

350 

351 uniqVisits = list(uniqVisits) 

352 

353 if not isinstance(refVisit, int): 

354 refVisit = int(refVisit) 

355 

356 if refVisit in uniqVisits: 

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

358 uniqVisits.remove(refVisit) 

359 

360 rmsDistances = list() 

361 

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

363 for vis in uniqVisits: 

364 

365 distancesVisit = list() 

366 

367 for obj in uniqObj: 

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

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

370 

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

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

373 

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

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

376 distances = sphDist( 

377 raObj[refMatch], decObj[refMatch], raObj[visMatch], decObj[visMatch] 

378 ) 

379 

380 distancesVisit.append(distances) 

381 

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

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

384 if len(finiteEntries) > 1: 

385 # Calculate the RMS of these offsets: 

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

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

388 pos_rms_mas = geom.radToMas(pos_rms_rad) # milliarcsec 

389 rmsDistances.append(pos_rms_mas) 

390 

391 else: 

392 rmsDistances.append(np.nan) 

393 

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

395 return rmsDistances 

396 

397 

398def radiansToMilliarcsec(rad): 

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

400 

401 

402def arcminToRadians(arcmin): 

403 return np.deg2rad(arcmin / 60)