Coverage for python/lsst/analysis/tools/actions/plot/focalPlanePlot.py: 23%
90 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-23 10:01 +0000
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-23 10:01 +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/>.
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 fig = plt.figure(dpi=300)
154 ax = fig.add_subplot(111)
156 if plotInfo is None:
157 plotInfo = {}
159 detectorIds = np.unique(data["detector"])
160 focalPlane_x = np.zeros(len(data["x"]))
161 focalPlane_y = np.zeros(len(data["y"]))
162 for detectorId in detectorIds:
163 detector = camera[detectorId]
164 map = detector.getTransform(PIXELS, FOCAL_PLANE).getMapping()
166 detectorInd = data["detector"] == detectorId
167 points = np.array([data["x"][detectorInd], data["y"][detectorInd]])
169 fp_x, fp_y = map.applyForward(points)
170 focalPlane_x[detectorInd] = fp_x
171 focalPlane_y[detectorInd] = fp_y
173 binsx = np.linspace(focalPlane_x.min(), focalPlane_x.max(), self.nBins)
174 binsy = np.linspace(focalPlane_y.min(), focalPlane_y.max(), self.nBins)
176 statistic, x_edge, y_edge, binnumber = binned_statistic_2d(
177 focalPlane_x, focalPlane_y, data["z"], statistic=self.statistic, bins=[binsx, binsy]
178 )
179 binExtent = [x_edge[0], x_edge[-1], y_edge[0], y_edge[-1]]
181 sortedArrs = sortAllArrays([data["z"], data["x"], data["y"], data["statMask"]])
182 [colorVals, xs, ys, stat] = sortedArrs
183 statMed, statMad, statsText = self.statsAndText(colorVals, mask=stat)
184 bbox = dict(facecolor="paleturquoise", alpha=0.5, edgecolor="none")
185 ax.text(0.8, 0.91, statsText, transform=fig.transFigure, fontsize=8, bbox=bbox)
187 median = np.nanmedian(statistic.ravel())
188 mad = nansigmaMad(statistic.ravel())
190 vmin = median - 2 * mad
191 vmax = median + 2 * mad
193 plot = ax.imshow(statistic.T, extent=binExtent, vmin=vmin, vmax=vmax, origin="lower")
195 cax = fig.add_axes([0.87 + 0.04, 0.11, 0.04, 0.77])
196 plt.colorbar(plot, cax=cax, extend="both")
197 text = cax.text(
198 0.5,
199 0.5,
200 self.zAxisLabel,
201 color="k",
202 rotation="vertical",
203 transform=cax.transAxes,
204 ha="center",
205 va="center",
206 fontsize=10,
207 )
208 text.set_path_effects([pathEffects.Stroke(linewidth=3, foreground="w"), pathEffects.Normal()])
209 cax.tick_params(labelsize=7)
211 ax.set_xlabel(self.xAxisLabel)
212 ax.set_ylabel(self.yAxisLabel)
213 ax.tick_params(axis="x", labelrotation=25)
214 ax.tick_params(labelsize=7)
216 ax.set_aspect("equal")
217 plt.draw()
219 # Add useful information to the plot
220 plt.subplots_adjust(wspace=0.0, hspace=0.0, right=0.85)
221 fig = plt.gcf()
222 fig = addPlotInfo(fig, plotInfo)
224 return fig