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

88 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-26 09:35 +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 matplotlib.pyplot as plt 

28import numpy as np 

29 

30import lsst.pex.config 

31import lsst.pipe.base as pipeBase 

32from lsst.obs.lsst import LsstCam 

33from lsst.pipe.base import connectionTypes 

34 

35 

36class MakeDiffimKernelQuiverPlotVisitConnections( 

37 pipeBase.PipelineTaskConnections, 

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

39): 

40 spatiallySampledMetrics = connectionTypes.Input( 

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

42 name="difference_image_metrics", 

43 storageClass="ArrowAstropy", 

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

45 deferLoad=True, 

46 multiple=True, 

47 ) 

48 visit_summary = connectionTypes.Input( 

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

50 name="preliminary_visit_summary", 

51 storageClass="ExposureCatalog", 

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

53 ) 

54 quiverKernelPlot = connectionTypes.Output( 

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

56 name="diffimPlots_kernelQuiver_QuiverPlot_visit", 

57 storageClass="Plot", 

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

59 ) 

60 

61 

62class MakeDiffimKernelQuiverPlotVisitConfig( 

63 pipeBase.PipelineTaskConfig, pipelineConnections=MakeDiffimKernelQuiverPlotVisitConnections 

64): 

65 

66 pivot = lsst.pex.config.ChoiceField( 

67 dtype=str, 

68 default="middle", 

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

70 allowed={ 

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

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

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

74 }, 

75 ) 

76 color = lsst.pex.config.Field( 

77 dtype=str, 

78 default="hsv", 

79 doc="Color map for quiver arrows", 

80 ) 

81 width = lsst.pex.config.Field( 

82 dtype=float, 

83 default=0.0005, 

84 doc="Width of quiver arrows", 

85 ) 

86 scale = lsst.pex.config.Field( 

87 dtype=float, 

88 default=1.0, 

89 doc="Scale factor for quiver arrows", 

90 ) 

91 scaleUnits = lsst.pex.config.Field( 

92 dtype=str, 

93 default="xy", 

94 doc="Scale units for quiver arrows", 

95 ) 

96 qKeySize = lsst.pex.config.Field( 

97 dtype=float, 

98 default=0.1, 

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

100 ) 

101 detectorColors = lsst.pex.config.DictField( 

102 keytype=str, 

103 itemtype=str, 

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

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

106 ) 

107 useDetectorColors = lsst.pex.config.Field( 

108 dtype=bool, 

109 default=True, 

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

111 ) 

112 directionKey = lsst.pex.config.Field( 

113 dtype=str, 

114 default="psfMatchingKernel_direction", 

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

116 ) 

117 lengthKey = lsst.pex.config.Field( 

118 dtype=str, 

119 default="psfMatchingKernel_length", 

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

121 ) 

122 titleText = lsst.pex.config.Field( 

123 dtype=str, 

124 default="Diffim Kernel Quiver Plot", 

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

126 ) 

127 

128 

129class MakeDiffimKernelQuiverPlotVisitTask(pipeBase.PipelineTask): 

130 

131 ConfigClass = MakeDiffimKernelQuiverPlotVisitConfig 

132 _DefaultName = "makeDiffimKernelQuiverPlotVisitConfig" 

133 

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

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

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

137 over the focal plane. 

138 

139 

140 Parameters 

141 ---------- 

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

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

144 `lsst.daf.butler.Quantum`. 

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

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

147 The values of these attributes are the corresponding 

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

149 `PipelineTaskConnections` class. 

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

151 Datastructure containing named attribute 'postageStamp'. 

152 The value of this attribute is the corresponding 

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

154 `PipelineTaskConnections` class. 

155 """ 

156 

157 inputs = butlerQC.get(inputRefs) 

158 

159 fig = self.run(**inputs) 

160 

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

162 

163 def run(self, spatiallySampledMetrics, visit_summary): 

164 """Create a full focal plane quiver plot 

165 

166 Parameters 

167 ---------- 

168 spatiallySampledMetrics : `astropy.table.Table` 

169 Image quality metrics computed at spatially sampled locations. 

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

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

172 

173 Returns 

174 ------- 

175 fig : `matplotlib.figure.Figure` 

176 The finished figure. 

177 """ 

178 quiverConf = { 

179 "pivot": self.config.pivot, 

180 "width": self.config.width, 

181 "scale": self.config.scale, 

182 "scale_units": self.config.scaleUnits, 

183 } 

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

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

186 else: 

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

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

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

190 

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

192 xAxisLabel = "RA (degrees)" 

193 yAxisLabel = "Dec (degrees)" 

194 

195 if self.config.useDetectorColors: 

196 detectorColors = self.config.detectorColors 

197 camera = LsstCam().getCamera() 

198 

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

200 ax = fig.add_subplot(111) 

201 dec_avg = [] 

202 for data_ref in spatiallySampledMetrics: 

203 detector = data_ref.dataId["detector"] 

204 

205 data = data_ref.get() 

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

207 data[self.config.lengthKey] 

208 ) 

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

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

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

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

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

214 

215 U = dataL * np.cos(dataA) 

216 V = dataL * np.sin(dataA) 

217 

218 if "color" in quiverConf: 

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

220 else: 

221 colors = cmap(norm(dataA)) 

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

223 

224 if self.config.useDetectorColors: 

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

226 else: 

227 detectorColor = "blue" 

228 

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

230 dec_avg = np.mean(dec_avg) 

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

232 ax.invert_xaxis() 

233 ax.set_xlabel(xAxisLabel, fontsize=16) 

234 ax.set_ylabel(yAxisLabel, fontsize=16) 

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

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

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

238 ax.set_title(title, fontsize=16) 

239 plt.tight_layout() 

240 fig.canvas.draw() 

241 return fig 

242 

243 

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

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

246 

247 Parameters 

248 ---------- 

249 ax : `matplotlib.axes.Axes` 

250 The plot to draw the detector outline on. 

251 detector : `int` 

252 The detector number to draw. 

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

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

255 color : `str`, optional 

256 The color of the detector outline. 

257 linewidth : `int`, optional 

258 The width of the detector outline. 

259 """ 

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

261 return 

262 

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

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

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

266 ra_center = np.mean(ra_corners) 

267 dec_center = np.mean(dec_corners) 

268 

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

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

271 

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

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