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

158 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-11 10:52 +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["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[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["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[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 if len(sepResiduals) > 0: 

264 sepResiduals = np.concatenate(sepResiduals) * u.radian 

265 return sepResiduals 

266 

267 

268def matchVisitComputeDistance( 

269 visit_obj1, ra_obj1, dec_obj1, visit_obj2, ra_obj2, dec_obj2 

270): 

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

272 For each visit shared between visit_obj1 and visit_obj2, 

273 calculate the spherical distance between the obj1 and obj2. 

274 visit_obj1 and visit_obj2 are assumed to be unsorted. 

275 Parameters 

276 ---------- 

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

278 List of visits for object 1. 

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

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

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

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

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

284 List of visits for object 2. 

285 ra_obj2 : list or numpy.array of float 

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

287 dec_obj2 : list or numpy.array of float 

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

289 Results 

290 ------- 

291 list of float 

292 spherical distances (in radians) for matching visits. 

293 """ 

294 distances = [] 

295 visit_obj1_idx = np.argsort(visit_obj1) 

296 visit_obj2_idx = np.argsort(visit_obj2) 

297 j_raw = 0 

298 j = visit_obj2_idx[j_raw] 

299 for i in visit_obj1_idx: 

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

301 j_raw += 1 

302 j = visit_obj2_idx[j_raw] 

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

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

305 distances.append( 

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

307 ) 

308 return distances 

309 

310 

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

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

313 Parameters 

314 ---------- 

315 groupView : lsst.afw.table.GroupView 

316 GroupView object of matched observations from MultiMatch. 

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

318 Magnitude range from which to select objects. 

319 verbose : bool, optional 

320 Output additional information on the analysis steps. 

321 Returns 

322 ------- 

323 rmsDistances : `astropy.units.Quantity` 

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

325 separations : `astropy.units.Quantity` 

326 Angular separations of the set a matched objects. 

327 """ 

328 

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

330 

331 def magInRange(cat): 

332 mag = cat["base_PsfFlux_mag"] 

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

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

335 return minMag <= medianMag and medianMag < maxMag 

336 

337 groupViewInMagRange = groupView.where(magInRange) 

338 

339 # Get lists of the unique objects and visits: 

340 uniqObj = groupViewInMagRange.ids 

341 uniqVisits = set() 

342 for id in uniqObj: 

343 for v, f in zip( 

344 groupViewInMagRange[id]["visit"], groupViewInMagRange[id]["filt"] 

345 ): 

346 if f == band: 

347 uniqVisits.add(v) 

348 

349 uniqVisits = list(uniqVisits) 

350 

351 if not isinstance(refVisit, int): 

352 refVisit = int(refVisit) 

353 

354 if refVisit in uniqVisits: 

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

356 uniqVisits.remove(refVisit) 

357 

358 rmsDistances = list() 

359 

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

361 for vis in uniqVisits: 

362 

363 distancesVisit = list() 

364 

365 for obj in uniqObj: 

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

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

368 

369 raObj = groupViewInMagRange[obj]["coord_ra"] 

370 decObj = groupViewInMagRange[obj]["coord_dec"] 

371 

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

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

374 distances = sphDist( 

375 raObj[refMatch], decObj[refMatch], raObj[visMatch], decObj[visMatch] 

376 ) 

377 

378 distancesVisit.append(distances) 

379 

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

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

382 if len(finiteEntries) > 1: 

383 # Calculate the RMS of these offsets: 

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

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

386 pos_rms_mas = geom.radToMas(pos_rms_rad) # milliarcsec 

387 rmsDistances.append(pos_rms_mas) 

388 

389 else: 

390 rmsDistances.append(np.nan) 

391 

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

393 return rmsDistances 

394 

395 

396def radiansToMilliarcsec(rad): 

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

398 

399 

400def arcminToRadians(arcmin): 

401 return np.deg2rad(arcmin / 60)