Coverage for python/lsst/validate/drp/calcsrd/amx.py: 8%

Shortcuts 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

78 statements  

1# LSST Data Management System 

2# Copyright 2016 AURA/LSST. 

3# 

4# This product includes software developed by the 

5# LSST Project (http://www.lsst.org/). 

6# 

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

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

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

10# (at your option) any later version. 

11# 

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

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

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

15# GNU General Public License for more details. 

16# 

17# You should have received a copy of the LSST License Statement and 

18# the GNU General Public License along with this program. If not, 

19# see <https://www.lsstcorp.org/LegalNotices/>. 

20 

21import numpy as np 

22import astropy.units as u 

23 

24from lsst.verify import Measurement, Datum 

25 

26from ..util import (averageRaFromCat, averageDecFromCat, 

27 sphDist) 

28 

29 

30def measureAMx(metric, matchedDataset, D, width=2., magRange=None, verbose=False): 

31 r"""Measurement of AMx (x=1,2,3): The maximum rms of the astrometric 

32 distance distribution for stellar pairs with separations of D arcmin 

33 (repeatability). 

34 

35 Parameters 

36 ---------- 

37 metric : `lsst.verify.Metric` 

38 An AM1, AM2 or AM3 `~lsst.verify.Metric` instance. 

39 matchedDataset : lsst.verify.Blob 

40 Contains the spacially matched dataset for the measurement 

41 D : `astropy.units.Quantity` 

42 Radial distance of the annulus in arcmin 

43 width : `float` or `astropy.units.Quantity`, optional 

44 Width around fiducial distance to include. [arcmin] 

45 magRange : 2-element `list`, `tuple`, or `numpy.ndarray`, optional 

46 brighter, fainter limits of the magnitude range to include. 

47 Default: ``[17.5, 21.5]`` mag. 

48 verbose : `bool`, optional 

49 Output additional information on the analysis steps. 

50 

51 Returns 

52 ------- 

53 measurement : `lsst.verify.Measurement` 

54 Measurement of AMx (x=1,2,3) and associated metadata. 

55 

56 Notes 

57 ----- 

58 This table below is provided ``validate_drp``\ 's :file:`metrics.yaml`. 

59 

60 LPM-17 dated 2011-07-06 

61 

62 Specification: 

63 The rms of the astrometric distance distribution for 

64 stellar pairs with separation of D arcmin (repeatability) 

65 will not exceed AMx milliarcsec (median distribution for a large number 

66 of sources). No more than AFx % of the sample will deviate by more than 

67 ADx milliarcsec from the median. AMx, AFx, and ADx are specified for 

68 D=5, 20 and 200 arcmin for x= 1, 2, and 3, in the same order (Table 18). 

69 

70 The three selected characteristic distances reflect the size of an 

71 individual sensor, a raft, and the camera. The required median astrometric 

72 precision is driven by the desire to achieve a proper motion accuracy of 

73 0.2 mas/yr and parallax accuracy of 1.0 mas over the course of the survey. 

74 These two requirements correspond to relative astrometric precision for a 

75 single image of 10 mas (per coordinate). 

76 

77 ========================= ====== ======= ======= 

78 Astrometric Repeatability Specification 

79 ------------------------- ---------------------- 

80 Metric Design Minimum Stretch 

81 ========================= ====== ======= ======= 

82 AM1 (milliarcsec) 10 20 5 

83 AF1 (%) 10 20 5 

84 AD1 (milliarcsec) 20 40 10 

85 AM2 (milliarcsec) 10 20 5 

86 AF2 (%) 10 20 5 

87 AD2 (milliarcsec) 20 40 10 

88 AM3 (milliarcsec) 15 30 10 

89 AF3 (%) 10 20 5 

90 AD3 (milliarcsec) 30 50 20 

91 ========================= ====== ======= ======= 

92 

93 Table 18: The specifications for astrometric precision. 

94 The three blocks of values correspond to D=5, 20 and 200 arcmin, 

95 and to astrometric measurements performed in the r and i bands. 

96 """ 

97 

98 matches = matchedDataset.matchesBright 

99 

100 datums = {} 

101 

102 # Measurement Parameters 

103 datums['D'] = Datum(quantity=D, label="Distance", description="Radial distance of annulus (arcmin)") 

104 

105 if not isinstance(width, u.Quantity): 

106 width = width * u.arcmin 

107 datums['Width'] = Datum(quantity=width, label='Width', description='Width of annulus') 

108 if magRange is None: 

109 magRange = np.array([17.0, 21.5]) * u.mag 

110 else: 

111 assert len(magRange) == 2 

112 if not isinstance(magRange, u.Quantity): 

113 magRange = np.array(magRange) * u.mag 

114 datums['magRange'] = Datum(quantity=magRange, description='Stellar magnitude selection range.') 

115 

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

117 datums['annulus'] = Datum(quantity=annulus, label='annulus radii', 

118 description='Inner and outer radii of selection annulus.') 

119 

120 # Register measurement extras 

121 rmsDistances = calcRmsDistances( 

122 matches, 

123 annulus, 

124 magRange=magRange, 

125 verbose=verbose) 

126 

127 if len(rmsDistances) == 0: 

128 # Should be a proper log message 

129 print('No stars found that are {0:.1f}--{1:.1f} apart.'.format( 

130 annulus[0], annulus[1])) 

131 datums['rmsDistMas'] = Datum(quantity=None, label='RMS') 

132 quantity = np.nan * u.marcsec 

133 else: 

134 datums['rmsDistMas'] = Datum(quantity=rmsDistances.to(u.marcsec), label='RMS') 

135 quantity = np.median(rmsDistances.to(u.marcsec)) 

136 

137 return Measurement(metric.name, quantity, extras=datums) 

138 

139 

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

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

142 

143 Parameters 

144 ---------- 

145 groupView : lsst.afw.table.GroupView 

146 GroupView object of matched observations from MultiMatch. 

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

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

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

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

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

152 Magnitude range from which to select objects. 

153 verbose : bool, optional 

154 Output additional information on the analysis steps. 

155 

156 Returns 

157 ------- 

158 rmsDistances : `astropy.units.Quantity` 

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

160 """ 

161 

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

163 importantKeys = [groupView.schema.find(name).key for 

164 name in ['id', 'coord_ra', 'coord_dec', 

165 'object', 'visit', 'base_PsfFlux_mag']] 

166 

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

168 

169 def magInRange(cat): 

170 mag = cat.get('base_PsfFlux_mag') 

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

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

173 return minMag <= medianMag and medianMag < maxMag 

174 

175 groupViewInMagRange = groupView.where(magInRange) 

176 

177 # List of lists of id, importantValue 

178 matchKeyOutput = [obj.get(key) 

179 for key in importantKeys 

180 for obj in groupViewInMagRange.groups] 

181 

182 jump = len(groupViewInMagRange) 

183 

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

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

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

187 

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

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

190 meanRa = groupViewInMagRange.aggregate(averageRaFromCat) 

191 meanDec = groupViewInMagRange.aggregate(averageDecFromCat) 

192 

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

194 

195 rmsDistances = list() 

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

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

198 objectsInAnnulus, = np.where((annulusRadians[0] <= dist) 

199 & (dist < annulusRadians[1])) 

200 for obj2 in objectsInAnnulus: 

201 distances = matchVisitComputeDistance( 

202 visit[obj1], ra[obj1], dec[obj1], 

203 visit[obj2], ra[obj2], dec[obj2]) 

204 if not distances: 

205 if verbose: 

206 print("No matching visits found for objs: %d and %d" % 

207 (obj1, obj2)) 

208 continue 

209 

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

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

212 if len(finiteEntries) > 1: 

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

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

215 rmsDistances.append(rmsDist) 

216 

217 # return quantity 

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

219 return rmsDistances 

220 

221 

222def matchVisitComputeDistance(visit_obj1, ra_obj1, dec_obj1, 

223 visit_obj2, ra_obj2, dec_obj2): 

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

225 

226 For each visit shared between visit_obj1 and visit_obj2, 

227 calculate the spherical distance between the obj1 and obj2. 

228 

229 visit_obj1 and visit_obj2 are assumed to be unsorted. 

230 

231 Parameters 

232 ---------- 

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

234 List of visits for object 1. 

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

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

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

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

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

240 List of visits for object 2. 

241 ra_obj2 : list or numpy.array of float 

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

243 dec_obj2 : list or numpy.array of float 

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

245 

246 Results 

247 ------- 

248 list of float 

249 spherical distances (in radians) for matching visits. 

250 """ 

251 distances = [] 

252 visit_obj1_idx = np.argsort(visit_obj1) 

253 visit_obj2_idx = np.argsort(visit_obj2) 

254 j_raw = 0 

255 j = visit_obj2_idx[j_raw] 

256 for i in visit_obj1_idx: 

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

258 j_raw += 1 

259 j = visit_obj2_idx[j_raw] 

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

261 if np.isfinite([ra_obj1[i], dec_obj1[i], 

262 ra_obj2[j], dec_obj2[j]]).all(): 

263 distances.append(sphDist(ra_obj1[i], dec_obj1[i], 

264 ra_obj2[j], dec_obj2[j])) 

265 return distances 

266 

267 

268def radiansToMilliarcsec(rad): 

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

270 

271 

272def arcminToRadians(arcmin): 

273 return np.deg2rad(arcmin/60)