Skip to content

Commit 07c0155

Browse files
committed
Add QA plots for source injection to analysis_tools
1 parent cdf17bb commit 07c0155

File tree

10 files changed

+640
-0
lines changed

10 files changed

+640
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
description: |
2+
Tier1 plots and metrics to assess injected coadd quality
3+
tasks:
4+
injectedObjectAnalysis:
5+
class: lsst.analysis.tools.tasks.injectedObjectAnalysis.InjectedObjectAnalysisTask
6+
config:
7+
atools.completenessHist: CompletenessPurityTool
8+
atools.targetInjectedCatDeltaRAScatterPlot: TargetInjectedCatDeltaRAScatterPlot
9+
atools.targetInjectedCatDeltaDecScatterPlot: TargetInjectedCatDeltaDecScatterPlot
10+
atools.targetInjectedCatDeltaPsfScatterPlot: TargetInjectedCatDeltaPsfScatterPlot
11+
atools.injectedMatchDiffMetrics: TargetInjectedCatDeltaMetrics
12+
atools.injectedMatchDiffMetrics.applyContext: CoaddContext
13+
python: |
14+
from lsst.analysis.tools.atools import *
15+
from lsst.analysis.tools.contexts import *
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from .calcDistances import *
22
from .keyedDataActions import *
3+
from .magPercentiles import *
34
from .stellarLocusFit import *
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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+
from __future__ import annotations
22+
23+
__all__ = ("MagPercentileAction",)
24+
25+
import logging
26+
27+
import numpy as np
28+
from astropy import units as u
29+
from lsst.pex.config import Field, ListField
30+
31+
from ...interfaces import KeyedData, KeyedDataSchema, Scalar, Vector, VectorAction
32+
from ...math import fluxToMag, isPercent
33+
34+
_LOG = logging.getLogger(__name__)
35+
36+
37+
class MagPercentileAction(VectorAction):
38+
"""Calculates the magnitude at the given percentile for completeness"""
39+
40+
matchDistanceKey = Field[str]("Match distance Vector")
41+
vectorKey = Field[str](doc="Key of vector which should be loaded")
42+
fluxUnits = Field[str](doc="Units for the column.", default="nanojansky")
43+
percentiles = ListField[float](
44+
doc="The percentiles to find the magnitude at.", default=[16.0, 50.0, 84.0], itemCheck=isPercent
45+
)
46+
47+
def getInputSchema(self) -> KeyedDataSchema:
48+
return (
49+
(self.matchDistanceKey, Vector),
50+
(self.vectorKey, Vector),
51+
)
52+
53+
def getOutputSchema(self) -> KeyedDataSchema:
54+
result = []
55+
for pct in self.percentiles:
56+
name = self.getPercentileName(pct)
57+
result.append((name, Scalar))
58+
return result
59+
60+
def getPercentileName(self, percentile: float) -> str:
61+
return f"mag_{percentile:.2f}"
62+
63+
def __call__(self, data: KeyedData, **kwargs) -> KeyedData:
64+
matched = np.isfinite(data[self.matchDistanceKey])
65+
fluxValues = data[self.vectorKey.format(**kwargs)]
66+
values = fluxToMag(fluxValues, flux_unit=u.Unit(self.fluxUnits))
67+
nInput, bins = np.histogram(
68+
values,
69+
range=(np.nanmin(values), np.nanmax(values)),
70+
bins=100,
71+
)
72+
nOutput, _ = np.histogram(
73+
values[matched],
74+
range=(np.nanmin(values[matched]), np.nanmax(values[matched])),
75+
bins=bins,
76+
)
77+
# Find bin where the fraction recovered first falls below a percentile.
78+
mags: KeyedData = {}
79+
for pct in self.percentiles:
80+
name = self.getPercentileName(pct)
81+
belowPercentile = np.where((nOutput / nInput < pct / 100))[0]
82+
if len(belowPercentile) == 0:
83+
mags[name] = np.nan
84+
else:
85+
mags[name] = np.min(bins[belowPercentile])
86+
return mags

python/lsst/analysis/tools/actions/plot/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from .barPlots import *
22
from .calculateRange import *
33
from .colorColorFitPlot import *
4+
from .completenessPlot import *
45
from .diaSkyPlot import *
56
from .focalPlanePlot import *
67
from .gridPlot import *
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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+
23+
from typing import Mapping
24+
25+
import matplotlib.pyplot as plt
26+
import numpy as np
27+
from lsst.pex.config import Field, ListField
28+
from matplotlib.figure import Figure
29+
30+
from ...interfaces import KeyedData, KeyedDataSchema, PlotAction, Scalar, ScalarType, Vector
31+
from .plotUtils import addPlotInfo
32+
33+
__all__ = ("CompletenessHist",)
34+
35+
36+
class CompletenessHist(PlotAction):
37+
"""Makes a scatter plot of the data with a marginal
38+
histogram for each axis.
39+
"""
40+
41+
magKey = Field[str](doc="Name of the magnitude column.", default="mag")
42+
matchDistanceKey = Field[str](doc="Name of the match distance column.", default="matchDistance")
43+
xAxisLabel = Field[str](doc="Label for the x axis.", default="Input Magnitude (mag)")
44+
inputLabel = Field[str](doc="Label for the input source histogram.", default="Synthetic Inputs")
45+
outputLabel = Field[str](doc="Label for the recovered source histogram.", default="Synthetic Recovered")
46+
numBins = Field[int](doc="Number of bins to use for the histograms.", default=100)
47+
completenessPercentiles = ListField[float](
48+
doc="Record the magnitudes at these percentiles", default=[84.0, 50.0, 16.0]
49+
)
50+
51+
def getInputSchema(self) -> KeyedDataSchema:
52+
base: list[tuple[str, type[Vector] | ScalarType]] = []
53+
base.append((self.magKey, Vector))
54+
base.append((self.matchDistanceKey, Vector))
55+
return base
56+
57+
def __call__(self, data: KeyedData, **kwargs) -> Mapping[str, Figure] | Figure:
58+
self._validateInput(data, **kwargs)
59+
return self.makePlot(data, **kwargs)
60+
61+
def _validateInput(self, data: KeyedData, **kwargs) -> None:
62+
"""NOTE currently can only check that something is not a Scalar, not
63+
check that the data is consistent with Vector
64+
"""
65+
needed = self.getFormattedInputSchema(**kwargs)
66+
if remainder := {key.format(**kwargs) for key, _ in needed} - {
67+
key.format(**kwargs) for key in data.keys()
68+
}:
69+
raise ValueError(f"Task needs keys {remainder} but they were not found in input")
70+
for name, typ in needed:
71+
isScalar = issubclass((colType := type(data[name.format(**kwargs)])), Scalar)
72+
if isScalar and typ != Scalar:
73+
raise ValueError(f"Data keyed by {name} has type {colType} but action requires type {typ}")
74+
75+
def makePlot(self, data, plotInfo, **kwargs):
76+
"""Makes a plot showing the fraction of injected sources recovered by
77+
input magnitude.
78+
79+
Parameters
80+
----------
81+
data : `KeyedData`
82+
All the data
83+
plotInfo : `dict`
84+
A dictionary of information about the data being plotted with keys:
85+
``camera``
86+
The camera used to take the data (`lsst.afw.cameraGeom.Camera`)
87+
``"cameraName"``
88+
The name of camera used to take the data (`str`).
89+
``"filter"``
90+
The filter used for this data (`str`).
91+
``"ccdKey"``
92+
The ccd/dectector key associated with this camera (`str`).
93+
``"visit"``
94+
The visit of the data; only included if the data is from a
95+
single epoch dataset (`str`).
96+
``"patch"``
97+
The patch that the data is from; only included if the data is
98+
from a coadd dataset (`str`).
99+
``"tract"``
100+
The tract that the data comes from (`str`).
101+
``"photoCalibDataset"``
102+
The dataset used for the calibration, e.g. "jointcal" or "fgcm"
103+
(`str`).
104+
``"skyWcsDataset"``
105+
The sky Wcs dataset used (`str`).
106+
``"rerun"``
107+
The rerun the data is stored in (`str`).
108+
109+
Returns
110+
------
111+
``fig``
112+
The figure to be saved (`matplotlib.figure.Figure`).
113+
114+
Notes
115+
-----
116+
Makes a histogram showing the fraction recovered in each magnitude
117+
bin with the number input and recovered overplotted.
118+
"""
119+
120+
# Make plot showing the fraction recovered in magnitude bins
121+
fig, axLeft = plt.subplots(dpi=300)
122+
axLeft.tick_params(axis="y", labelcolor="C0")
123+
axLeft.set_xlabel(self.xAxisLabel)
124+
axLeft.set_ylabel("Fraction Recovered", color="C0")
125+
axRight = axLeft.twinx()
126+
axRight.set_ylabel("Number of Sources")
127+
matched = np.isfinite(data[self.matchDistanceKey])
128+
nInput, bins, _ = axRight.hist(
129+
data[self.magKey],
130+
range=(np.nanmin(data[self.magKey]), np.nanmax(data[self.magKey])),
131+
bins=self.numBins,
132+
log=True,
133+
histtype="step",
134+
label=self.inputLabel,
135+
color="black",
136+
)
137+
nOutput, _, _ = axRight.hist(
138+
data[self.magKey][matched],
139+
range=(np.nanmin(data[self.magKey][matched]), np.nanmax(data[self.magKey][matched])),
140+
bins=bins,
141+
log=True,
142+
histtype="step",
143+
label=self.outputLabel,
144+
color="grey",
145+
)
146+
147+
# Find bin where the fraction recovered falls below a given percentile.
148+
percentileInfo = []
149+
xlims = plt.gca().get_xlim()
150+
for pct in self.completenessPercentiles:
151+
pct /= 100
152+
magArray = np.where((nOutput / nInput < pct))[0]
153+
if len(magArray) == 0:
154+
mag = np.nan
155+
else:
156+
mag = np.min(bins[magArray])
157+
axLeft.plot([xlims[0], mag], [pct, pct], ls=":", color="grey")
158+
axLeft.plot([mag, mag], [0, pct], ls=":", color="grey")
159+
percentileInfo.append("Magnitude at {}% recovered: {:0.2f}".format(pct * 100, mag))
160+
plt.xlim(xlims)
161+
axLeft.set_ylim(0, 1.05)
162+
axRight.legend(loc="lower left", ncol=2)
163+
axLeft.axhline(1, color="grey", ls="--")
164+
axLeft.bar(
165+
bins[:-1],
166+
nOutput / nInput,
167+
width=np.diff(bins),
168+
align="edge",
169+
color="C0",
170+
alpha=0.5,
171+
zorder=10,
172+
)
173+
174+
# Add useful information to the plot
175+
fig = plt.gcf()
176+
addPlotInfo(fig, plotInfo)
177+
statsText = ""
178+
for info in percentileInfo:
179+
statsText += f"{info}\n"
180+
bbox = dict(edgecolor="grey", linestyle=":", facecolor="none")
181+
fig.text(0.7, 0.075, statsText[:-1], bbox=bbox, transform=fig.transFigure, fontsize=6)
182+
fig.subplots_adjust(bottom=0.2)
183+
return fig

python/lsst/analysis/tools/atools/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from .skyFluxStatisticMetrics import *
3434
from .skyObject import *
3535
from .skySource import *
36+
from .sourceInjectionPlots import *
3637
from .sources import *
3738
from .stellarLocus import *
3839
from .wholeSkyPlotTool import *

0 commit comments

Comments
 (0)