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

137 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 if "sourceType" in cat.columns: 

60 cat = cat.loc[cat["sourceType"] != 0] 

61 

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

63 

64 patchInfoDict = {} 

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

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

67 for patch in patches: 

68 if patch is None: 

69 continue 

70 # Once the objectTable_tract catalogues are using gen 3 patches 

71 # this will go away 

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

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

74 try: 

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

76 patchInfo = tractInfo.getPatchInfo(patchTuple) 

77 gen3PatchId = tractInfo.getSequentialPatchIndex(patchInfo) 

78 except AttributeError: 

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

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

81 # converted repos we can tidy this up 

82 gen3PatchId = patch 

83 patchInfo = tractInfo.getPatchInfo(patch) 

84 

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

86 skyCoords = tractWcs.pixelToSky(corners) 

87 

88 patchInfoDict[gen3PatchId] = (skyCoords, stat) 

89 

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

91 skyCoords = tractWcs.pixelToSky(tractCorners) 

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

93 

94 return patchInfoDict 

95 

96 

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

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

99 

100 Parameters 

101 ---------- 

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

103 colName : `str` 

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

105 plotInfo : `dict` 

106 

107 Returns 

108 ------- 

109 visitInfoDict : `dict` 

110 """ 

111 

112 visitInfoDict = {} 

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

114 if ccd is None: 

115 continue 

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

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

118 

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

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

121 cornersOut = [] 

122 for (ra, dec) in corners: 

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

124 cornersOut.append(corner) 

125 

126 visitInfoDict[ccd] = (cornersOut, stat) 

127 

128 return visitInfoDict 

129 

130 

131def addPlotInfo(fig, plotInfo): 

132 """Add useful information to the plot 

133 

134 Parameters 

135 ---------- 

136 fig : `matplotlib.figure.Figure` 

137 plotInfo : `dict` 

138 

139 Returns 

140 ------- 

141 fig : `matplotlib.figure.Figure` 

142 """ 

143 

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

145 photocalibDataset = "None" 

146 astroDataset = "None" 

147 

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

149 

150 run = plotInfo["run"] 

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

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

153 

154 dataIdText = "" 

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

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

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

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

159 

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

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

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

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

164 

165 return fig 

166 

167 

168def stellarLocusFit(xs, ys, paramDict): 

169 """Make a fit to the stellar locus 

170 

171 Parameters 

172 ---------- 

173 xs : `numpy.ndarray` 

174 The color on the xaxis 

175 ys : `numpy.ndarray` 

176 The color on the yaxis 

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

178 A dictionary of parameters for line fitting 

179 xMin : `float` 

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

181 xMax : `float` 

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

183 yMin : `float` 

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

185 yMax : `float` 

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

187 mHW : `float` 

188 The hardwired gradient for the fit 

189 bHW : `float` 

190 The hardwired intercept of the fit 

191 

192 Returns 

193 ------- 

194 paramsOut : `dict` 

195 A dictionary of the calculated fit parameters 

196 xMin : `float` 

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

198 xMax : `float` 

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

200 yMin : `float` 

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

202 yMax : `float` 

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

204 mHW : `float` 

205 The hardwired gradient for the fit 

206 bHW : `float` 

207 The hardwired intercept of the fit 

208 mODR : `float` 

209 The gradient calculated by the ODR fit 

210 bODR : `float` 

211 The intercept calculated by the ODR fit 

212 yBoxMin : `float` 

213 The y value of the fitted line at xMin 

214 yBoxMax : `float` 

215 The y value of the fitted line at xMax 

216 bPerpMin : `float` 

217 The intercept of the perpendicular line that goes through xMin 

218 bPerpMax : `float` 

219 The intercept of the perpendicular line that goes through xMax 

220 mODR2 : `float` 

221 The gradient from the second round of fitting 

222 bODR2 : `float` 

223 The intercept from the second round of fitting 

224 mPerp : `float` 

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

226 second fit 

227 

228 Notes 

229 ----- 

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

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

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

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

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

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

236 """ 

237 

238 # Points to use for the fit 

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

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

241 

242 linear = scipyODR.polynomial(1) 

243 

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

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

246 params = odr.run() 

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

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

249 

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

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

252 "mODR": mODR, "bODR": bODR} 

253 

254 # Having found the initial fit calculate perpendicular ends 

255 mPerp = -1.0/mODR 

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

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

258 

259 if np.abs(mODR) > 1: 

260 yBoxMin = paramDict["yMin"] 

261 xBoxMin = (yBoxMin - bODR)/mODR 

262 yBoxMax = paramDict["yMax"] 

263 xBoxMax = (yBoxMax - bODR)/mODR 

264 else: 

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

266 xBoxMin = paramDict["xMin"] 

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

268 xBoxMax = paramDict["xMax"] 

269 

270 bPerpMin = yBoxMin - mPerp*xBoxMin 

271 

272 paramsOut["yBoxMin"] = yBoxMin 

273 paramsOut["bPerpMin"] = bPerpMin 

274 

275 bPerpMax = yBoxMax - mPerp*xBoxMax 

276 

277 paramsOut["yBoxMax"] = yBoxMax 

278 paramsOut["bPerpMax"] = bPerpMax 

279 

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

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

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

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

284 params = odr.run() 

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

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

287 

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

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

290 

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

292 

293 return paramsOut 

294 

295 

296def perpDistance(p1, p2, points): 

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

298 

299 Parameters 

300 ---------- 

301 p1 : `numpy.ndarray` 

302 A point on the line 

303 p2 : `numpy.ndarray` 

304 Another point on the line 

305 points : `zip` 

306 The points to calculate the distance to 

307 

308 Returns 

309 ------- 

310 dists : `list` 

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

312 product to work this out. 

313 """ 

314 dists = [] 

315 for point in points: 

316 point = np.array(point) 

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

318 dists.append(distToLine) 

319 

320 return dists 

321 

322 

323def mkColormap(colorNames): 

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

325 

326 Parameters 

327 ---------- 

328 colorNames : `list` 

329 A list of strings that correspond to matplotlib 

330 named colors. 

331 

332 Returns 

333 ------- 

334 cmap : `matplotlib.colors.LinearSegmentedColormap` 

335 """ 

336 

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

338 blues = [] 

339 greens = [] 

340 reds = [] 

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

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

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

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

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

346 

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

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

349 return cmap 

350 

351 

352def extremaSort(xs): 

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

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

355 

356 Parameters 

357 ---------- 

358 xs : `np.array` 

359 An array of the values to sort 

360 

361 Returns 

362 ------- 

363 ids : `np.array` 

364 """ 

365 

366 med = np.median(xs) 

367 dists = np.abs(xs - med) 

368 ids = np.argsort(dists) 

369 return ids