Coverage for python / lsst / analysis / tools / tasks / diffimKernelQuiverPlotVisit.py: 29%
88 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-01 08:55 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-01 08:55 +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 matplotlib.pyplot as plt
28import numpy as np
30import lsst.pex.config
31import lsst.pipe.base as pipeBase
32from lsst.obs.lsst import LsstCam
33from lsst.pipe.base import connectionTypes
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 )
62class MakeDiffimKernelQuiverPlotVisitConfig(
63 pipeBase.PipelineTaskConfig, pipelineConnections=MakeDiffimKernelQuiverPlotVisitConnections
64):
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 )
129class MakeDiffimKernelQuiverPlotVisitTask(pipeBase.PipelineTask):
131 ConfigClass = MakeDiffimKernelQuiverPlotVisitConfig
132 _DefaultName = "makeDiffimKernelQuiverPlotVisitConfig"
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.
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 """
157 inputs = butlerQC.get(inputRefs)
159 fig = self.run(**inputs)
161 butlerQC.put(pipeBase.Struct(quiverKernelPlot=fig), outputRefs)
163 def run(self, spatiallySampledMetrics, visit_summary):
164 """Create a full focal plane quiver plot
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.
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)
191 qKeyLabel = f"{self.config.qKeySize} arcsec Kernel offset direction"
192 xAxisLabel = "RA (degrees)"
193 yAxisLabel = "Dec (degrees)"
195 if self.config.useDetectorColors:
196 detectorColors = self.config.detectorColors
197 camera = LsstCam().getCamera()
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"]
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]
215 U = dataL * np.cos(dataA)
216 V = dataL * np.sin(dataA)
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)
224 if self.config.useDetectorColors:
225 detectorColor = detectorColors[camera[detector].getPhysicalType()]
226 else:
227 detectorColor = "blue"
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
244def draw_detector_outlines(ax, detector, visit_summary, color="red", linewidth=1):
245 """Draw the outline of a detector on a plot.
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
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)
269 ra_corners.append(ra_corners[0]) # repeat to close loop
270 dec_corners.append(dec_corners[0]) # repeat to close loop
272 ax.plot(ra_corners, dec_corners, "--", color=color, alpha=0.9, linewidth=linewidth)
273 ax.text(ra_center, dec_center, detector, alpha=1)