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 if "sourceType" in cat.columns:
60 cat = cat.loc[cat["sourceType"] != 0]
62 # For now also convert the gen 2 patchIds to gen 3
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)
85 corners = Box2D(patchInfo.getInnerBBox()).getCorners()
86 skyCoords = tractWcs.pixelToSky(corners)
88 patchInfoDict[gen3PatchId] = (skyCoords, stat)
90 tractCorners = Box2D(tractInfo.getBBox()).getCorners()
91 skyCoords = tractWcs.pixelToSky(tractCorners)
92 patchInfoDict["tract"] = (skyCoords, np.nan)
94 return patchInfoDict
97def generateSummaryStatsVisit(cat, colName, visitSummaryTable, plotInfo):
98 """Generate a summary statistic in each patch or detector
100 Parameters
101 ----------
102 cat : `pandas.core.frame.DataFrame`
103 colName : `str`
104 visitSummaryTable : `pandas.core.frame.DataFrame`
105 plotInfo : `dict`
107 Returns
108 -------
109 visitInfoDict : `dict`
110 """
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])
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)
126 visitInfoDict[ccd] = (cornersOut, stat)
128 return visitInfoDict
131def addPlotInfo(fig, plotInfo):
132 """Add useful information to the plot
134 Parameters
135 ----------
136 fig : `matplotlib.figure.Figure`
137 plotInfo : `dict`
139 Returns
140 -------
141 fig : `matplotlib.figure.Figure`
142 """
144 # TO DO: figure out how to get this information
145 photocalibDataset = "None"
146 astroDataset = "None"
148 plt.text(0.01, 0.99, plotInfo["plotName"], fontsize=8, transform=fig.transFigure, ha="left", va="top")
150 run = plotInfo["run"]
151 datasetsUsed = f"\nPhotoCalib: {photocalibDataset}, Astrometry: {astroDataset}"
152 tableType = f"\nTable: {plotInfo['tractTableType']}"
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']}"
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")
165 return fig
168def stellarLocusFit(xs, ys, paramDict):
169 """Make a fit to the stellar locus
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
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
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 """
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]
242 linear = scipyODR.polynomial(1)
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])
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}
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
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"]
270 bPerpMin = yBoxMin - mPerp*xBoxMin
272 paramsOut["yBoxMin"] = yBoxMin
273 paramsOut["bPerpMin"] = bPerpMin
275 bPerpMax = yBoxMax - mPerp*xBoxMax
277 paramsOut["yBoxMax"] = yBoxMax
278 paramsOut["bPerpMax"] = bPerpMax
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])
288 paramsOut["mODR2"] = float(params.beta[1])
289 paramsOut["bODR2"] = float(params.beta[0])
291 paramsOut["mPerp"] = -1.0/paramsOut["mODR2"]
293 return paramsOut
296def perpDistance(p1, p2, points):
297 """Calculate the perpendicular distance to a line from a point
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
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)
320 return dists
323def mkColormap(colorNames):
324 """Make a colormap from the list of color names.
326 Parameters
327 ----------
328 colorNames : `list`
329 A list of strings that correspond to matplotlib
330 named colors.
332 Returns
333 -------
334 cmap : `matplotlib.colors.LinearSegmentedColormap`
335 """
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))
347 colorDict = {"blue": blues, "red": reds, "green": greens}
348 cmap = colors.LinearSegmentedColormap("newCmap", colorDict)
349 return cmap
352def extremaSort(xs):
353 """Return the ids of the points reordered so that those
354 furthest from the median, in absolute terms, are last.
356 Parameters
357 ----------
358 xs : `np.array`
359 An array of the values to sort
361 Returns
362 -------
363 ids : `np.array`
364 """
366 med = np.median(xs)
367 dists = np.abs(xs - med)
368 ids = np.argsort(dists)
369 return ids