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
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
1import numpy as np
2import matplotlib.pyplot as plt
3import scipy.odr as scipyODR
4from matplotlib import colors
6from lsst.geom import Box2D, SpherePoint, degrees
9def parsePlotInfo(dataId, runName, tableName, bands, plotName, SN):
10 """Parse plot info from the dataId
12 Parameters
13 ----------
14 dataId : `lsst.daf.butler.core.dimensions.`
15 `_coordinate._ExpandedTupleDataCoordinate`
16 runName : `str`
18 Returns
19 -------
20 plotInfo : `dict`
21 """
22 plotInfo = {"run": runName, "tractTableType": tableName, "plotName": plotName, "SN": SN}
24 for dataInfo in dataId:
25 plotInfo[dataInfo.name] = dataId[dataInfo.name]
27 bandStr = ""
28 for band in bands:
29 bandStr += (", " + band)
30 plotInfo["bands"] = bandStr[2:]
32 if "tract" not in plotInfo.keys():
33 plotInfo["tract"] = "N/A"
34 if "visit" not in plotInfo.keys():
35 plotInfo["visit"] = "N/A"
37 return plotInfo
40def generateSummaryStats(cat, colName, skymap, plotInfo):
41 """Generate a summary statistic in each patch or detector
43 Parameters
44 ----------
45 cat : `pandas.core.frame.DataFrame`
46 colName : `str`
47 skymap : `lsst.skymap.ringsSkyMap.RingsSkyMap`
48 plotInfo : `dict`
50 Returns
51 -------
52 patchInfoDict : `dict`
53 """
55 # TODO: what is the more generic type of skymap?
56 tractInfo = skymap.generateTract(plotInfo["tract"])
57 tractWcs = tractInfo.getWcs()
59 # For now also convert the gen 2 patchIds to gen 3
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)
82 corners = Box2D(patchInfo.getInnerBBox()).getCorners()
83 skyCoords = tractWcs.pixelToSky(corners)
85 patchInfoDict[gen3PatchId] = (skyCoords, stat)
87 tractCorners = Box2D(tractInfo.getBBox()).getCorners()
88 skyCoords = tractWcs.pixelToSky(tractCorners)
89 patchInfoDict["tract"] = (skyCoords, np.nan)
91 return patchInfoDict
94def generateSummaryStatsVisit(cat, colName, visitSummaryTable, plotInfo):
95 """Generate a summary statistic in each patch or detector
97 Parameters
98 ----------
99 cat : `pandas.core.frame.DataFrame`
100 colName : `str`
101 visitSummaryTable : `pandas.core.frame.DataFrame`
102 plotInfo : `dict`
104 Returns
105 -------
106 visitInfoDict : `dict`
107 """
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])
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)
123 visitInfoDict[ccd] = (cornersOut, stat)
125 return visitInfoDict
128def addPlotInfo(fig, plotInfo):
129 """Add useful information to the plot
131 Parameters
132 ----------
133 fig : `matplotlib.figure.Figure`
134 plotInfo : `dict`
136 Returns
137 -------
138 fig : `matplotlib.figure.Figure`
139 """
141 # TO DO: figure out how to get this information
142 photocalibDataset = "None"
143 astroDataset = "None"
145 plt.text(0.01, 0.99, plotInfo["plotName"], fontsize=8, transform=fig.transFigure, ha="left", va="top")
147 run = plotInfo["run"]
148 datasetsUsed = f"\nPhotoCalib: {photocalibDataset}, Astrometry: {astroDataset}"
149 tableType = f"\nTable: {plotInfo['tractTableType']}"
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']}"
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")
162 return fig
165def stellarLocusFit(xs, ys, paramDict):
166 """Make a fit to the stellar locus
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
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
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 """
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]
239 linear = scipyODR.polynomial(1)
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])
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}
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
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"]
267 bPerpMin = yBoxMin - mPerp*xBoxMin
269 paramsOut["yBoxMin"] = yBoxMin
270 paramsOut["bPerpMin"] = bPerpMin
272 bPerpMax = yBoxMax - mPerp*xBoxMax
274 paramsOut["yBoxMax"] = yBoxMax
275 paramsOut["bPerpMax"] = bPerpMax
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])
285 paramsOut["mODR2"] = float(params.beta[1])
286 paramsOut["bODR2"] = float(params.beta[0])
288 paramsOut["mPerp"] = -1.0/paramsOut["mODR2"]
290 return paramsOut
293def perpDistance(p1, p2, points):
294 """Calculate the perpendicular distance to a line from a point
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
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)
317 return dists
320def mkColormap(colorNames):
321 """Make a colormap from the list of color names.
323 Parameters
324 ----------
325 colorNames : `list`
326 A list of strings that correspond to matplotlib
327 named colors.
329 Returns
330 -------
331 cmap : `matplotlib.colors.LinearSegmentedColormap`
332 """
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))
344 colorDict = {"blue": blues, "red": reds, "green": greens}
345 cmap = colors.LinearSegmentedColormap("newCmap", colorDict)
346 return cmap
349def extremaSort(xs):
350 """Return the ids of the points reordered so that those
351 furthest from the median, in absolute terms, are last.
353 Parameters
354 ----------
355 xs : `np.array`
356 An array of the values to sort
358 Returns
359 -------
360 ids : `np.array`
361 """
363 med = np.median(xs)
364 dists = np.abs(xs - med)
365 ids = np.argsort(dists)
366 return ids