Coverage for python / lsst / analysis / tools / tasks / diffimKernelQuiverPlotVisit.py: 29%
88 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-15 00:23 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-15 00:23 +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/>.
22__all__ = (
23 "MakeDiffimKernelQuiverPlotVisitConfig",
24 "MakeDiffimKernelQuiverPlotVisitTask",
25)
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
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 )
61class MakeDiffimKernelQuiverPlotVisitConfig(
62 pipeBase.PipelineTaskConfig, pipelineConnections=MakeDiffimKernelQuiverPlotVisitConnections
63):
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 )
128class MakeDiffimKernelQuiverPlotVisitTask(pipeBase.PipelineTask):
130 ConfigClass = MakeDiffimKernelQuiverPlotVisitConfig
131 _DefaultName = "makeDiffimKernelQuiverPlotVisitConfig"
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.
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 """
156 inputs = butlerQC.get(inputRefs)
158 fig = self.run(**inputs)
160 butlerQC.put(pipeBase.Struct(quiverKernelPlot=fig), outputRefs)
162 def run(self, spatiallySampledMetrics, visit_summary):
163 """Create a full focal plane quiver plot
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.
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)
190 qKeyLabel = f"{self.config.qKeySize} arcsec Kernel offset direction"
191 xAxisLabel = "RA (degrees)"
192 yAxisLabel = "Dec (degrees)"
194 if self.config.useDetectorColors:
195 detectorColors = self.config.detectorColors
196 camera = LsstCam().getCamera()
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"]
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]
214 U = dataL * np.cos(dataA)
215 V = dataL * np.sin(dataA)
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)
223 if self.config.useDetectorColors:
224 detectorColor = detectorColors[camera[detector].getPhysicalType()]
225 else:
226 detectorColor = "blue"
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
243def draw_detector_outlines(ax, detector, visit_summary, color="red", linewidth=1):
244 """Draw the outline of a detector on a plot.
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
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)
268 ra_corners.append(ra_corners[0]) # repeat to close loop
269 dec_corners.append(dec_corners[0]) # repeat to close loop
271 ax.plot(ra_corners, dec_corners, "--", color=color, alpha=0.9, linewidth=linewidth)
272 ax.text(ra_center, dec_center, detector, alpha=1)