Coverage for python/lsst/analysis/drp/plotUtils.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

135 statements  

1import numpy as np 

2import matplotlib.pyplot as plt 

3import scipy.odr as scipyODR 

4from matplotlib import colors 

5 

6from lsst.geom import Box2D, SpherePoint, degrees 

7 

8 

9def parsePlotInfo(dataId, runName, tableName, bands, plotName, SN): 

10 """Parse plot info from the dataId 

11 

12 Parameters 

13 ---------- 

14 dataId : `lsst.daf.butler.core.dimensions.` 

15 `_coordinate._ExpandedTupleDataCoordinate` 

16 runName : `str` 

17 

18 Returns 

19 ------- 

20 plotInfo : `dict` 

21 """ 

22 plotInfo = {"run": runName, "tractTableType": tableName, "plotName": plotName, "SN": SN} 

23 

24 for dataInfo in dataId: 

25 plotInfo[dataInfo.name] = dataId[dataInfo.name] 

26 

27 bandStr = "" 

28 for band in bands: 

29 bandStr += (", " + band) 

30 plotInfo["bands"] = bandStr[2:] 

31 

32 if "tract" not in plotInfo.keys(): 

33 plotInfo["tract"] = "N/A" 

34 if "visit" not in plotInfo.keys(): 

35 plotInfo["visit"] = "N/A" 

36 

37 return plotInfo 

38 

39 

40def generateSummaryStats(cat, colName, skymap, plotInfo): 

41 """Generate a summary statistic in each patch or detector 

42 

43 Parameters 

44 ---------- 

45 cat : `pandas.core.frame.DataFrame` 

46 colName : `str` 

47 skymap : `lsst.skymap.ringsSkyMap.RingsSkyMap` 

48 plotInfo : `dict` 

49 

50 Returns 

51 ------- 

52 patchInfoDict : `dict` 

53 """ 

54 

55 # TODO: what is the more generic type of skymap? 

56 tractInfo = skymap.generateTract(plotInfo["tract"]) 

57 tractWcs = tractInfo.getWcs() 

58 

59 # For now also convert the gen 2 patchIds to gen 3 

60 

61 patchInfoDict = {} 

62 maxPatchNum = tractInfo.num_patches.x*tractInfo.num_patches.y 

63 patches = np.arange(0, maxPatchNum, 1) 

64 for patch in patches: 

65 if patch is None: 

66 continue 

67 # Once the objectTable_tract catalogues are using gen 3 patches 

68 # this will go away 

69 onPatch = (cat["patch"] == patch) 

70 stat = np.nanmedian(cat[colName].values[onPatch]) 

71 try: 

72 patchTuple = (int(patch.split(",")[0]), int(patch.split(",")[-1])) 

73 patchInfo = tractInfo.getPatchInfo(patchTuple) 

74 gen3PatchId = tractInfo.getSequentialPatchIndex(patchInfo) 

75 except AttributeError: 

76 # For native gen 3 tables the patches don't need converting 

77 # When we are no longer looking at the gen 2 -> gen 3 

78 # converted repos we can tidy this up 

79 gen3PatchId = patch 

80 patchInfo = tractInfo.getPatchInfo(patch) 

81 

82 corners = Box2D(patchInfo.getInnerBBox()).getCorners() 

83 skyCoords = tractWcs.pixelToSky(corners) 

84 

85 patchInfoDict[gen3PatchId] = (skyCoords, stat) 

86 

87 tractCorners = Box2D(tractInfo.getBBox()).getCorners() 

88 skyCoords = tractWcs.pixelToSky(tractCorners) 

89 patchInfoDict["tract"] = (skyCoords, np.nan) 

90 

91 return patchInfoDict 

92 

93 

94def generateSummaryStatsVisit(cat, colName, visitSummaryTable, plotInfo): 

95 """Generate a summary statistic in each patch or detector 

96 

97 Parameters 

98 ---------- 

99 cat : `pandas.core.frame.DataFrame` 

100 colName : `str` 

101 visitSummaryTable : `pandas.core.frame.DataFrame` 

102 plotInfo : `dict` 

103 

104 Returns 

105 ------- 

106 visitInfoDict : `dict` 

107 """ 

108 

109 visitInfoDict = {} 

110 for ccd in cat.detector.unique(): 

111 if ccd is None: 

112 continue 

113 onCcd = (cat["detector"] == ccd) 

114 stat = np.nanmedian(cat[colName].values[onCcd]) 

115 

116 sumRow = (visitSummaryTable["id"] == ccd) 

117 corners = zip(visitSummaryTable["raCorners"][sumRow][0], visitSummaryTable["decCorners"][sumRow][0]) 

118 cornersOut = [] 

119 for (ra, dec) in corners: 

120 corner = SpherePoint(ra, dec, units=degrees) 

121 cornersOut.append(corner) 

122 

123 visitInfoDict[ccd] = (cornersOut, stat) 

124 

125 return visitInfoDict 

126 

127 

128def addPlotInfo(fig, plotInfo): 

129 """Add useful information to the plot 

130 

131 Parameters 

132 ---------- 

133 fig : `matplotlib.figure.Figure` 

134 plotInfo : `dict` 

135 

136 Returns 

137 ------- 

138 fig : `matplotlib.figure.Figure` 

139 """ 

140 

141 # TO DO: figure out how to get this information 

142 photocalibDataset = "None" 

143 astroDataset = "None" 

144 

145 plt.text(0.01, 0.99, plotInfo["plotName"], fontsize=8, transform=fig.transFigure, ha="left", va="top") 

146 

147 run = plotInfo["run"] 

148 datasetsUsed = f"\nPhotoCalib: {photocalibDataset}, Astrometry: {astroDataset}" 

149 tableType = f"\nTable: {plotInfo['tractTableType']}" 

150 

151 dataIdText = "" 

152 if str(plotInfo["tract"]) != "N/A": 

153 dataIdText += f", Tract: {plotInfo['tract']}" 

154 if str(plotInfo["visit"]) != "N/A": 

155 dataIdText += f", Visit: {plotInfo['visit']}" 

156 

157 bandsText = f", Bands: {''.join(plotInfo['bands'].split(' '))}" 

158 SNText = f", S/N: {plotInfo['SN']}" 

159 infoText = f"\n{run}{datasetsUsed}{tableType}{dataIdText}{bandsText}{SNText}" 

160 plt.text(0.01, 0.98, infoText, fontsize=7, transform=fig.transFigure, alpha=0.6, ha="left", va="top") 

161 

162 return fig 

163 

164 

165def stellarLocusFit(xs, ys, paramDict): 

166 """Make a fit to the stellar locus 

167 

168 Parameters 

169 ---------- 

170 xs : `numpy.ndarray` 

171 The color on the xaxis 

172 ys : `numpy.ndarray` 

173 The color on the yaxis 

174 paramDict : lsst.pex.config.dictField.Dict 

175 A dictionary of parameters for line fitting 

176 xMin : `float` 

177 The minimum x edge of the box to use for initial fitting 

178 xMax : `float` 

179 The maximum x edge of the box to use for initial fitting 

180 yMin : `float` 

181 The minimum y edge of the box to use for initial fitting 

182 yMax : `float` 

183 The maximum y edge of the box to use for initial fitting 

184 mHW : `float` 

185 The hardwired gradient for the fit 

186 bHW : `float` 

187 The hardwired intercept of the fit 

188 

189 Returns 

190 ------- 

191 paramsOut : `dict` 

192 A dictionary of the calculated fit parameters 

193 xMin : `float` 

194 The minimum x edge of the box to use for initial fitting 

195 xMax : `float` 

196 The maximum x edge of the box to use for initial fitting 

197 yMin : `float` 

198 The minimum y edge of the box to use for initial fitting 

199 yMax : `float` 

200 The maximum y edge of the box to use for initial fitting 

201 mHW : `float` 

202 The hardwired gradient for the fit 

203 bHW : `float` 

204 The hardwired intercept of the fit 

205 mODR : `float` 

206 The gradient calculated by the ODR fit 

207 bODR : `float` 

208 The intercept calculated by the ODR fit 

209 yBoxMin : `float` 

210 The y value of the fitted line at xMin 

211 yBoxMax : `float` 

212 The y value of the fitted line at xMax 

213 bPerpMin : `float` 

214 The intercept of the perpendicular line that goes through xMin 

215 bPerpMax : `float` 

216 The intercept of the perpendicular line that goes through xMax 

217 mODR2 : `float` 

218 The gradient from the second round of fitting 

219 bODR2 : `float` 

220 The intercept from the second round of fitting 

221 mPerp : `float` 

222 The gradient of the line perpendicular to the line from the 

223 second fit 

224 

225 Notes 

226 ----- 

227 The code does two rounds of fitting, the first is initiated using the 

228 hardwired values given in the `paramDict` parameter and is done using 

229 an Orthogonal Distance Regression fit to the points defined by the 

230 box of xMin, xMax, yMin and yMax. Once this fitting has been done a 

231 perpendicular bisector is calculated at either end of the line and 

232 only points that fall within these lines are used to recalculate the fit. 

233 """ 

234 

235 # Points to use for the fit 

236 fitPoints = np.where((xs > paramDict["xMin"]) & (xs < paramDict["xMax"]) 

237 & (ys > paramDict["yMin"]) & (ys < paramDict["yMax"]))[0] 

238 

239 linear = scipyODR.polynomial(1) 

240 

241 data = scipyODR.Data(xs[fitPoints], ys[fitPoints]) 

242 odr = scipyODR.ODR(data, linear, beta0=[paramDict["bHW"], paramDict["mHW"]]) 

243 params = odr.run() 

244 mODR = float(params.beta[1]) 

245 bODR = float(params.beta[0]) 

246 

247 paramsOut = {"xMin": paramDict["xMin"], "xMax": paramDict["xMax"], "yMin": paramDict["yMin"], 

248 "yMax": paramDict["yMax"], "mHW": paramDict["mHW"], "bHW": paramDict["bHW"], 

249 "mODR": mODR, "bODR": bODR} 

250 

251 # Having found the initial fit calculate perpendicular ends 

252 mPerp = -1.0/mODR 

253 # When the gradient is really steep we need to use 

254 # the y limits of the box rather than the x ones 

255 

256 if np.abs(mODR) > 1: 

257 yBoxMin = paramDict["yMin"] 

258 xBoxMin = (yBoxMin - bODR)/mODR 

259 yBoxMax = paramDict["yMax"] 

260 xBoxMax = (yBoxMax - bODR)/mODR 

261 else: 

262 yBoxMin = mODR*paramDict["xMin"] + bODR 

263 xBoxMin = paramDict["xMin"] 

264 yBoxMax = mODR*paramDict["xMax"] + bODR 

265 xBoxMax = paramDict["xMax"] 

266 

267 bPerpMin = yBoxMin - mPerp*xBoxMin 

268 

269 paramsOut["yBoxMin"] = yBoxMin 

270 paramsOut["bPerpMin"] = bPerpMin 

271 

272 bPerpMax = yBoxMax - mPerp*xBoxMax 

273 

274 paramsOut["yBoxMax"] = yBoxMax 

275 paramsOut["bPerpMax"] = bPerpMax 

276 

277 # Use these perpendicular lines to chose the data and refit 

278 fitPoints = ((ys > mPerp*xs + bPerpMin) & (ys < mPerp*xs + bPerpMax)) 

279 data = scipyODR.Data(xs[fitPoints], ys[fitPoints]) 

280 odr = scipyODR.ODR(data, linear, beta0=[bODR, mODR]) 

281 params = odr.run() 

282 mODR = float(params.beta[1]) 

283 bODR = float(params.beta[0]) 

284 

285 paramsOut["mODR2"] = float(params.beta[1]) 

286 paramsOut["bODR2"] = float(params.beta[0]) 

287 

288 paramsOut["mPerp"] = -1.0/paramsOut["mODR2"] 

289 

290 return paramsOut 

291 

292 

293def perpDistance(p1, p2, points): 

294 """Calculate the perpendicular distance to a line from a point 

295 

296 Parameters 

297 ---------- 

298 p1 : `numpy.ndarray` 

299 A point on the line 

300 p2 : `numpy.ndarray` 

301 Another point on the line 

302 points : `zip` 

303 The points to calculate the distance to 

304 

305 Returns 

306 ------- 

307 dists : `list` 

308 The distances from the line to the points. Uses the cross 

309 product to work this out. 

310 """ 

311 dists = [] 

312 for point in points: 

313 point = np.array(point) 

314 distToLine = np.cross(p2 - p1, point - p1)/np.linalg.norm(p2 - p1) 

315 dists.append(distToLine) 

316 

317 return dists 

318 

319 

320def mkColormap(colorNames): 

321 """Make a colormap from the list of color names. 

322 

323 Parameters 

324 ---------- 

325 colorNames : `list` 

326 A list of strings that correspond to matplotlib 

327 named colors. 

328 

329 Returns 

330 ------- 

331 cmap : `matplotlib.colors.LinearSegmentedColormap` 

332 """ 

333 

334 nums = np.linspace(0, 1, len(colorNames)) 

335 blues = [] 

336 greens = [] 

337 reds = [] 

338 for (num, color) in zip(nums, colorNames): 

339 r, g, b = colors.colorConverter.to_rgb(color) 

340 blues.append((num, b, b)) 

341 greens.append((num, g, g)) 

342 reds.append((num, r, r)) 

343 

344 colorDict = {"blue": blues, "red": reds, "green": greens} 

345 cmap = colors.LinearSegmentedColormap("newCmap", colorDict) 

346 return cmap 

347 

348 

349def extremaSort(xs): 

350 """Return the ids of the points reordered so that those 

351 furthest from the median, in absolute terms, are last. 

352 

353 Parameters 

354 ---------- 

355 xs : `np.array` 

356 An array of the values to sort 

357 

358 Returns 

359 ------- 

360 ids : `np.array` 

361 """ 

362 

363 med = np.median(xs) 

364 dists = np.abs(xs - med) 

365 ids = np.argsort(dists) 

366 return ids