Coverage for python / lsst / analysis / tools / tasks / diffimKernelQuiverPlotVisit.py: 29%

88 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-05 18:53 +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 

22__all__ = ( 

23 "MakeDiffimKernelQuiverPlotVisitConfig", 

24 "MakeDiffimKernelQuiverPlotVisitTask", 

25) 

26 

27import lsst.pex.config 

28import lsst.pipe.base as pipeBase 

29import matplotlib.pyplot as plt 

30import numpy as np 

31from lsst.obs.lsst import LsstCam 

32from lsst.pipe.base import connectionTypes 

33 

34 

35class MakeDiffimKernelQuiverPlotVisitConnections( 

36 pipeBase.PipelineTaskConnections, 

37 dimensions=("instrument", "visit"), 

38): 

39 spatiallySampledMetrics = connectionTypes.Input( 

40 doc="QA metrics evaluated in locations throughout the difference image.", 

41 name="difference_image_metrics", 

42 storageClass="ArrowAstropy", 

43 dimensions=("instrument", "visit", "detector"), 

44 deferLoad=True, 

45 multiple=True, 

46 ) 

47 visit_summary = connectionTypes.Input( 

48 doc="A summary of the visit-level metadata.", 

49 name="preliminary_visit_summary", 

50 storageClass="ExposureCatalog", 

51 dimensions=("instrument", "visit"), 

52 ) 

53 quiverKernelPlot = connectionTypes.Output( 

54 doc="A quiver plot of the diffim kernel centroid over the focal plane.", 

55 name="diffimPlots_kernelQuiver_QuiverPlot_visit", 

56 storageClass="Plot", 

57 dimensions=("instrument", "visit"), 

58 ) 

59 

60 

61class MakeDiffimKernelQuiverPlotVisitConfig( 

62 pipeBase.PipelineTaskConfig, pipelineConnections=MakeDiffimKernelQuiverPlotVisitConnections 

63): 

64 

65 pivot = lsst.pex.config.ChoiceField( 

66 dtype=str, 

67 default="middle", 

68 doc="Where to rotate the quiver about the sample point.", 

69 allowed={ 

70 "middle": "Rotate the arrows of the quiver about their midpoint.", 

71 "tail": "Rotate the arrows of the quiver about their tail.", 

72 "tip": "Rotate the arrows of the quiver about their tip.", 

73 }, 

74 ) 

75 color = lsst.pex.config.Field( 

76 dtype=str, 

77 default="hsv", 

78 doc="Color map for quiver arrows", 

79 ) 

80 width = lsst.pex.config.Field( 

81 dtype=float, 

82 default=0.0005, 

83 doc="Width of quiver arrows", 

84 ) 

85 scale = lsst.pex.config.Field( 

86 dtype=float, 

87 default=1.0, 

88 doc="Scale factor for quiver arrows", 

89 ) 

90 scaleUnits = lsst.pex.config.Field( 

91 dtype=str, 

92 default="xy", 

93 doc="Scale units for quiver arrows", 

94 ) 

95 qKeySize = lsst.pex.config.Field( 

96 dtype=float, 

97 default=0.1, 

98 doc="Size of quiver key in arcsec", 

99 ) 

100 detectorColors = lsst.pex.config.DictField( 

101 keytype=str, 

102 itemtype=str, 

103 default={"ITL": "blue", "E2V": "red"}, 

104 doc="Colors to use for different detector types", 

105 ) 

106 useDetectorColors = lsst.pex.config.Field( 

107 dtype=bool, 

108 default=True, 

109 doc="Use different colors for different detector types", 

110 ) 

111 directionKey = lsst.pex.config.Field( 

112 dtype=str, 

113 default="psfMatchingKernel_direction", 

114 doc="Column name to use for the angle of the vectors.", 

115 ) 

116 lengthKey = lsst.pex.config.Field( 

117 dtype=str, 

118 default="psfMatchingKernel_length", 

119 doc="Column name to use for the length of the vectors.", 

120 ) 

121 titleText = lsst.pex.config.Field( 

122 dtype=str, 

123 default="Diffim Kernel Quiver Plot", 

124 doc="Title text to display on the plot.", 

125 ) 

126 

127 

128class MakeDiffimKernelQuiverPlotVisitTask(pipeBase.PipelineTask): 

129 

130 ConfigClass = MakeDiffimKernelQuiverPlotVisitConfig 

131 _DefaultName = "makeDiffimKernelQuiverPlotVisitConfig" 

132 

133 def runQuantum(self, butlerQC, inputRefs, outputRefs): 

134 """Takes a set of detector spatially sampled metrics for a visit and 

135 makes a quiver plot showing the spatial variation of the diffim kernel 

136 over the focal plane. 

137 

138 

139 Parameters 

140 ---------- 

141 butlerQC : `lsst.pipe.base.QuantumContext` 

142 A butler which is specialized to operate in the context of a 

143 `lsst.daf.butler.Quantum`. 

144 inputRefs : `lsst.pipe.base.InputQuantizedConnection` 

145 Datastructure containing named attributes 'data and 'skymap'. 

146 The values of these attributes are the corresponding 

147 `lsst.daf.butler.DatasetRef` objects defined in the corresponding 

148 `PipelineTaskConnections` class. 

149 outputRefs : `lsst.pipe.base.OutputQuantizedConnection` 

150 Datastructure containing named attribute 'postageStamp'. 

151 The value of this attribute is the corresponding 

152 `lsst.daf.butler.DatasetRef` object defined in the corresponding 

153 `PipelineTaskConnections` class. 

154 """ 

155 

156 inputs = butlerQC.get(inputRefs) 

157 

158 fig = self.run(**inputs) 

159 

160 butlerQC.put(pipeBase.Struct(quiverKernelPlot=fig), outputRefs) 

161 

162 def run(self, spatiallySampledMetrics, visit_summary): 

163 """Create a full focal plane quiver plot 

164 

165 Parameters 

166 ---------- 

167 spatiallySampledMetrics : `astropy.table.Table` 

168 Image quality metrics computed at spatially sampled locations. 

169 visit_summary : `lsst.afw.table.ExposureCatalog` 

170 Table of metadata for all exposures contained in the visit. 

171 

172 Returns 

173 ------- 

174 fig : `matplotlib.figure.Figure` 

175 The finished figure. 

176 """ 

177 quiverConf = { 

178 "pivot": self.config.pivot, 

179 "width": self.config.width, 

180 "scale": self.config.scale, 

181 "scale_units": self.config.scaleUnits, 

182 } 

183 if self.config.color not in plt.colormaps(): 

184 quiverConf["color"] = self.config.color 

185 else: 

186 # Normalize directions to [0, 1] for colormap, covering -pi to pi 

187 norm = plt.Normalize(vmin=-np.pi, vmax=np.pi) 

188 cmap = plt.get_cmap(self.config.color) 

189 

190 qKeyLabel = f"{self.config.qKeySize} arcsec Kernel offset direction" 

191 xAxisLabel = "RA (degrees)" 

192 yAxisLabel = "Dec (degrees)" 

193 

194 if self.config.useDetectorColors: 

195 detectorColors = self.config.detectorColors 

196 camera = LsstCam().getCamera() 

197 

198 fig = plt.figure(dpi=300, figsize=(20, 20)) 

199 ax = fig.add_subplot(111) 

200 dec_avg = [] 

201 for data_ref in spatiallySampledMetrics: 

202 detector = data_ref.dataId["detector"] 

203 

204 data = data_ref.get() 

205 dataSelector = np.isfinite(data[self.config.directionKey]) & np.isfinite( 

206 data[self.config.lengthKey] 

207 ) 

208 dataX = np.rad2deg(data["coord_ra"][dataSelector]) 

209 dataY = np.rad2deg(data["coord_dec"][dataSelector]) 

210 dec_avg.append(np.mean(data["coord_dec"][dataSelector])) 

211 dataA = data[self.config.directionKey][dataSelector] 

212 dataL = data[self.config.lengthKey][dataSelector] 

213 

214 U = dataL * np.cos(dataA) 

215 V = dataL * np.sin(dataA) 

216 

217 if "color" in quiverConf: 

218 q = ax.quiver(dataX, dataY, U, V, **quiverConf) 

219 else: 

220 colors = cmap(norm(dataA)) 

221 q = ax.quiver(dataX, dataY, U, V, color=colors, **quiverConf) 

222 

223 if self.config.useDetectorColors: 

224 detectorColor = detectorColors[camera[detector].getPhysicalType()] 

225 else: 

226 detectorColor = "blue" 

227 

228 draw_detector_outlines(ax, detector, visit_summary, color=detectorColor, linewidth=0.5) 

229 dec_avg = np.mean(dec_avg) 

230 ax.quiverkey(q, 0.8, 0.95, self.config.qKeySize, qKeyLabel, labelpos="E", coordinates="axes") 

231 ax.invert_xaxis() 

232 ax.set_xlabel(xAxisLabel, fontsize=16) 

233 ax.set_ylabel(yAxisLabel, fontsize=16) 

234 ax.set_aspect(1 / np.cos(dec_avg)) 

235 title = f"{self.config.titleText}\nvisit: {visit_summary['visit'][0]}" 

236 title += f" - {visit_summary.asAstropy()['band'][0]} band" 

237 ax.set_title(title, fontsize=16) 

238 plt.tight_layout() 

239 fig.canvas.draw() 

240 return fig 

241 

242 

243def draw_detector_outlines(ax, detector, visit_summary, color="red", linewidth=1): 

244 """Draw the outline of a detector on a plot. 

245 

246 Parameters 

247 ---------- 

248 ax : `matplotlib.axes.Axes` 

249 The plot to draw the detector outline on. 

250 detector : `int` 

251 The detector number to draw. 

252 visit_summary : `lsst.afw.table.ExposureCatalog` 

253 Table of metadata for all exposures contained in the visit. 

254 color : `str`, optional 

255 The color of the detector outline. 

256 linewidth : `int`, optional 

257 The width of the detector outline. 

258 """ 

259 if detector not in visit_summary["id"]: 

260 return 

261 

262 row = np.where(visit_summary["id"] == detector)[0][0] 

263 ra_corners = list(visit_summary["raCorners"][row]) 

264 dec_corners = list(visit_summary["decCorners"][row]) 

265 ra_center = np.mean(ra_corners) 

266 dec_center = np.mean(dec_corners) 

267 

268 ra_corners.append(ra_corners[0]) # repeat to close loop 

269 dec_corners.append(dec_corners[0]) # repeat to close loop 

270 

271 ax.plot(ra_corners, dec_corners, "--", color=color, alpha=0.9, linewidth=linewidth) 

272 ax.text(ra_center, dec_center, detector, alpha=1)