Coverage for python/lsst/analysis/tools/actions/plot/focalPlanePlot.py: 22%
95 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-28 04:48 -0700
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-28 04:48 -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/>.
22from __future__ import annotations
24__all__ = ("FocalPlanePlot",)
26from typing import Mapping, Optional
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
36from ...interfaces import KeyedData, KeyedDataSchema, PlotAction, Scalar, Vector
37from ...statistics import nansigmaMad
38from .plotUtils import addPlotInfo, sortAllArrays
41class FocalPlanePlot(PlotAction):
42 """Plots the focal plane distribution of a parameter.
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 """
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)
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 )
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,
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}")
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))
89 return base
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)
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 )
109 return med, sigMad, statsText
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.
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.
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`).
148 Returns
149 -------
150 fig : `matplotlib.figure.Figure`
151 The resulting figure.
152 """
153 if plotInfo is None:
154 plotInfo = {}
156 if len(data["x"]) == 0:
157 noDataFig = Figure()
158 noDataFig.text(0.3, 0.5, "No data to plot after selectors applied")
159 noDataFig = addPlotInfo(noDataFig, plotInfo)
160 return noDataFig
162 fig = plt.figure(dpi=300)
163 ax = fig.add_subplot(111)
165 detectorIds = np.unique(data["detector"])
166 focalPlane_x = np.zeros(len(data["x"]))
167 focalPlane_y = np.zeros(len(data["y"]))
168 for detectorId in detectorIds:
169 detector = camera[detectorId]
170 map = detector.getTransform(PIXELS, FOCAL_PLANE).getMapping()
172 detectorInd = data["detector"] == detectorId
173 points = np.array([data["x"][detectorInd], data["y"][detectorInd]])
175 fp_x, fp_y = map.applyForward(points)
176 focalPlane_x[detectorInd] = fp_x
177 focalPlane_y[detectorInd] = fp_y
179 # Add an arbitrary small offset to bins to ensure that the minimum does
180 # not equal the maximum.
181 binsx = np.linspace(focalPlane_x.min() - 1e-5, focalPlane_x.max() + 1e-5, self.nBins)
182 binsy = np.linspace(focalPlane_y.min() - 1e-5, focalPlane_y.max() + 1e-5, self.nBins)
184 statistic, x_edge, y_edge, binnumber = binned_statistic_2d(
185 focalPlane_x, focalPlane_y, data["z"], statistic=self.statistic, bins=[binsx, binsy]
186 )
187 binExtent = [x_edge[0], x_edge[-1], y_edge[0], y_edge[-1]]
189 sortedArrs = sortAllArrays([data["z"], data["x"], data["y"], data["statMask"]])
190 [colorVals, xs, ys, stat] = sortedArrs
191 statMed, statMad, statsText = self.statsAndText(colorVals, mask=stat)
192 bbox = dict(facecolor="paleturquoise", alpha=0.5, edgecolor="none")
193 ax.text(0.8, 0.91, statsText, transform=fig.transFigure, fontsize=8, bbox=bbox)
195 median = np.nanmedian(statistic.ravel())
196 mad = nansigmaMad(statistic.ravel())
198 vmin = median - 2 * mad
199 vmax = median + 2 * mad
201 plot = ax.imshow(statistic.T, extent=binExtent, vmin=vmin, vmax=vmax, origin="lower")
203 cax = fig.add_axes([0.87 + 0.04, 0.11, 0.04, 0.77])
204 plt.colorbar(plot, cax=cax, extend="both")
205 text = cax.text(
206 0.5,
207 0.5,
208 self.zAxisLabel,
209 color="k",
210 rotation="vertical",
211 transform=cax.transAxes,
212 ha="center",
213 va="center",
214 fontsize=10,
215 )
216 text.set_path_effects([pathEffects.Stroke(linewidth=3, foreground="w"), pathEffects.Normal()])
217 cax.tick_params(labelsize=7)
219 ax.set_xlabel(self.xAxisLabel)
220 ax.set_ylabel(self.yAxisLabel)
221 ax.tick_params(axis="x", labelrotation=25)
222 ax.tick_params(labelsize=7)
224 ax.set_aspect("equal")
225 plt.draw()
227 # Add useful information to the plot
228 plt.subplots_adjust(wspace=0.0, hspace=0.0, right=0.85)
229 fig = plt.gcf()
230 fig = addPlotInfo(fig, plotInfo)
232 return fig