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

90 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-09 03:19 -0700

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 fig = plt.figure(dpi=300) 

154 ax = fig.add_subplot(111) 

155 

156 if plotInfo is None: 

157 plotInfo = {} 

158 

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

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

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

162 for detectorId in detectorIds: 

163 detector = camera[detectorId] 

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

165 

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

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

168 

169 fp_x, fp_y = map.applyForward(points) 

170 focalPlane_x[detectorInd] = fp_x 

171 focalPlane_y[detectorInd] = fp_y 

172 

173 binsx = np.linspace(focalPlane_x.min(), focalPlane_x.max(), self.nBins) 

174 binsy = np.linspace(focalPlane_y.min(), focalPlane_y.max(), self.nBins) 

175 

176 statistic, x_edge, y_edge, binnumber = binned_statistic_2d( 

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

178 ) 

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

180 

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

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

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

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

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

186 

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

188 mad = nansigmaMad(statistic.ravel()) 

189 

190 vmin = median - 2 * mad 

191 vmax = median + 2 * mad 

192 

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

194 

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

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

197 text = cax.text( 

198 0.5, 

199 0.5, 

200 self.zAxisLabel, 

201 color="k", 

202 rotation="vertical", 

203 transform=cax.transAxes, 

204 ha="center", 

205 va="center", 

206 fontsize=10, 

207 ) 

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

209 cax.tick_params(labelsize=7) 

210 

211 ax.set_xlabel(self.xAxisLabel) 

212 ax.set_ylabel(self.yAxisLabel) 

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

214 ax.tick_params(labelsize=7) 

215 

216 ax.set_aspect("equal") 

217 plt.draw() 

218 

219 # Add useful information to the plot 

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

221 fig = plt.gcf() 

222 fig = addPlotInfo(fig, plotInfo) 

223 

224 return fig