Coverage for python/lsst/analysis/tools/actions/plot/focalPlanePlot.py: 22%

95 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-04 23:12 +0000

1# This file is part of analysis_tools. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

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

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

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

12# (at your option) any later version. 

13# 

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

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

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

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ("FocalPlanePlot",) 

25 

26from typing import Mapping, Optional 

27 

28import matplotlib.patheffects as pathEffects 

29import matplotlib.pyplot as plt 

30import numpy as np 

31from lsst.afw.cameraGeom import FOCAL_PLANE, PIXELS, Camera 

32from lsst.pex.config import Field 

33from matplotlib.figure import Figure 

34from scipy.stats import binned_statistic_2d 

35 

36from ...interfaces import KeyedData, KeyedDataSchema, PlotAction, Scalar, Vector 

37from ...statistics import nansigmaMad 

38from .plotUtils import addPlotInfo, sortAllArrays 

39 

40 

41class FocalPlanePlot(PlotAction): 

42 """Plots the focal plane distribution of a parameter. 

43 

44 Given the detector positions in x and y, the focal plane positions are 

45 calculated using the camera model. A 2d binned statistic (default is mean) 

46 is then calculated and plotted for the parameter z as a function of the 

47 focal plane coordinates. 

48 """ 

49 

50 xAxisLabel = Field[str](doc="Label to use for the x axis.", optional=False) 

51 yAxisLabel = Field[str](doc="Label to use for the y axis.", optional=False) 

52 zAxisLabel = Field[str](doc="Label to use for the z axis.", optional=False) 

53 

54 nBins = Field[int]( 

55 doc="Number of bins to use within the effective plot ranges along the spatial directions.", 

56 default=200, 

57 ) 

58 statistic = Field[str]( 

59 doc="Operation to perform in binned_statistic_2d", 

60 default="mean", 

61 ) 

62 

63 def __call__(self, data: KeyedData, **kwargs) -> Mapping[str, Figure] | Figure: 

64 self._validateInput(data, **kwargs) 

65 return self.makePlot(data, **kwargs) 

66 # table is a dict that needs: x, y, run, skymap, filter, tract, 

67 

68 def _validateInput(self, data: KeyedData, **kwargs) -> None: 

69 """NOTE currently can only check that something is not a Scalar, not 

70 check that the data is consistent with Vector 

71 """ 

72 needed = self.getInputSchema(**kwargs) 

73 if remainder := {key.format(**kwargs) for key, _ in needed} - { 

74 key.format(**kwargs) for key in data.keys() 

75 }: 

76 raise ValueError(f"Task needs keys {remainder} but they were not found in input") 

77 for name, typ in needed: 

78 isScalar = issubclass((colType := type(data[name.format(**kwargs)])), Scalar) 

79 if isScalar and typ != Scalar: 

80 raise ValueError(f"Data keyed by {name} has type {colType} but action requires type {typ}") 

81 

82 def getInputSchema(self, **kwargs) -> KeyedDataSchema: 

83 base = [] 

84 base.append(("x", Vector)) 

85 base.append(("y", Vector)) 

86 base.append(("z", Vector)) 

87 base.append(("statMask", Vector)) 

88 

89 return base 

90 

91 def statsAndText(self, arr, mask=None): 

92 """Calculate some stats from an array and return them 

93 and some text. 

94 """ 

95 numPoints = len(arr) 

96 if mask is not None: 

97 arr = arr[mask] 

98 med = np.nanmedian(arr) 

99 sigMad = nansigmaMad(arr) 

100 

101 statsText = ( 

102 "Median: {:0.2f}\n".format(med) 

103 + r"$\sigma_{MAD}$: " 

104 + "{:0.2f}\n".format(sigMad) 

105 + r"n$_{points}$: " 

106 + "{}".format(numPoints) 

107 ) 

108 

109 return med, sigMad, statsText 

110 

111 def makePlot( 

112 self, 

113 data: KeyedData, 

114 camera: Camera, 

115 plotInfo: Optional[Mapping[str, str]] = None, 

116 **kwargs, 

117 ) -> Figure: 

118 """Prep the catalogue and then make a focalPlanePlot of the given 

119 column. 

120 

121 Uses the axisLabels config options `x` and `y` to make an image, where 

122 the color corresponds to the 2d binned statistic (the mean is the 

123 default) applied to the `z` column. A summary panel is shown in the 

124 upper right corner of the resultant plot. The code uses the 

125 selectorActions to decide which points to plot and the 

126 statisticSelector actions to determine which points to use for the 

127 printed statistics. 

128 

129 Parameters 

130 ---------- 

131 data : `pandas.core.frame.DataFrame` 

132 The catalog to plot the points from. 

133 camera : `lsst.afw.cameraGeom.Camera` 

134 The camera used to map from pixel to focal plane positions. 

135 plotInfo : `dict` 

136 A dictionary of information about the data being plotted with keys: 

137 ``"run"`` 

138 The output run for the plots (`str`). 

139 ``"skymap"`` 

140 The type of skymap used for the data (`str`). 

141 ``"filter"`` 

142 The filter used for this data (`str`). 

143 ``"tract"`` 

144 The tract that the data comes from (`str`). 

145 ``"bands"`` 

146 The band(s) that the data comes from (`list` of `str`). 

147 

148 Returns 

149 ------- 

150 fig : `matplotlib.figure.Figure` 

151 The resulting figure. 

152 """ 

153 if plotInfo is None: 

154 plotInfo = {} 

155 

156 if len(data["x"]) == 0: 

157 noDataFig = Figure() 

158 noDataFig.text(0.3, 0.5, "No data to plot after selectors applied") 

159 noDataFig = addPlotInfo(noDataFig, plotInfo) 

160 return noDataFig 

161 

162 fig = plt.figure(dpi=300) 

163 ax = fig.add_subplot(111) 

164 

165 detectorIds = np.unique(data["detector"]) 

166 focalPlane_x = np.zeros(len(data["x"])) 

167 focalPlane_y = np.zeros(len(data["y"])) 

168 for detectorId in detectorIds: 

169 detector = camera[detectorId] 

170 map = detector.getTransform(PIXELS, FOCAL_PLANE).getMapping() 

171 

172 detectorInd = data["detector"] == detectorId 

173 points = np.array([data["x"][detectorInd], data["y"][detectorInd]]) 

174 

175 fp_x, fp_y = map.applyForward(points) 

176 focalPlane_x[detectorInd] = fp_x 

177 focalPlane_y[detectorInd] = fp_y 

178 

179 # Add an arbitrary small offset to bins to ensure that the minimum does 

180 # not equal the maximum. 

181 binsx = np.linspace(focalPlane_x.min() - 1e-5, focalPlane_x.max() + 1e-5, self.nBins) 

182 binsy = np.linspace(focalPlane_y.min() - 1e-5, focalPlane_y.max() + 1e-5, self.nBins) 

183 

184 statistic, x_edge, y_edge, binnumber = binned_statistic_2d( 

185 focalPlane_x, focalPlane_y, data["z"], statistic=self.statistic, bins=[binsx, binsy] 

186 ) 

187 binExtent = [x_edge[0], x_edge[-1], y_edge[0], y_edge[-1]] 

188 

189 sortedArrs = sortAllArrays([data["z"], data["x"], data["y"], data["statMask"]]) 

190 [colorVals, xs, ys, stat] = sortedArrs 

191 statMed, statMad, statsText = self.statsAndText(colorVals, mask=stat) 

192 bbox = dict(facecolor="paleturquoise", alpha=0.5, edgecolor="none") 

193 ax.text(0.8, 0.91, statsText, transform=fig.transFigure, fontsize=8, bbox=bbox) 

194 

195 median = np.nanmedian(statistic.ravel()) 

196 mad = nansigmaMad(statistic.ravel()) 

197 

198 vmin = median - 2 * mad 

199 vmax = median + 2 * mad 

200 

201 plot = ax.imshow(statistic.T, extent=binExtent, vmin=vmin, vmax=vmax, origin="lower") 

202 

203 cax = fig.add_axes([0.87 + 0.04, 0.11, 0.04, 0.77]) 

204 plt.colorbar(plot, cax=cax, extend="both") 

205 text = cax.text( 

206 0.5, 

207 0.5, 

208 self.zAxisLabel, 

209 color="k", 

210 rotation="vertical", 

211 transform=cax.transAxes, 

212 ha="center", 

213 va="center", 

214 fontsize=10, 

215 ) 

216 text.set_path_effects([pathEffects.Stroke(linewidth=3, foreground="w"), pathEffects.Normal()]) 

217 cax.tick_params(labelsize=7) 

218 

219 ax.set_xlabel(self.xAxisLabel) 

220 ax.set_ylabel(self.yAxisLabel) 

221 ax.tick_params(axis="x", labelrotation=25) 

222 ax.tick_params(labelsize=7) 

223 

224 ax.set_aspect("equal") 

225 plt.draw() 

226 

227 # Add useful information to the plot 

228 plt.subplots_adjust(wspace=0.0, hspace=0.0, right=0.85) 

229 fig = plt.gcf() 

230 fig = addPlotInfo(fig, plotInfo) 

231 

232 return fig