LSST Applications 27.0.0,g0265f82a02+469cd937ee,g02d81e74bb+21ad69e7e1,g1470d8bcf6+cbe83ee85a,g2079a07aa2+e67c6346a6,g212a7c68fe+04a9158687,g2305ad1205+94392ce272,g295015adf3+81dd352a9d,g2bbee38e9b+469cd937ee,g337abbeb29+469cd937ee,g3939d97d7f+72a9f7b576,g487adcacf7+71499e7cba,g50ff169b8f+5929b3527e,g52b1c1532d+a6fc98d2e7,g591dd9f2cf+df404f777f,g5a732f18d5+be83d3ecdb,g64a986408d+21ad69e7e1,g858d7b2824+21ad69e7e1,g8a8a8dda67+a6fc98d2e7,g99cad8db69+f62e5b0af5,g9ddcbc5298+d4bad12328,ga1e77700b3+9c366c4306,ga8c6da7877+71e4819109,gb0e22166c9+25ba2f69a1,gb6a65358fc+469cd937ee,gbb8dafda3b+69d3c0e320,gc07e1c2157+a98bf949bb,gc120e1dc64+615ec43309,gc28159a63d+469cd937ee,gcf0d15dbbd+72a9f7b576,gdaeeff99f8+a38ce5ea23,ge6526c86ff+3a7c1ac5f1,ge79ae78c31+469cd937ee,gee10cc3b42+a6fc98d2e7,gf1cff7945b+21ad69e7e1,gfbcc870c63+9a11dc8c8f
LSST Data Management Base Package
Loading...
Searching...
No Matches
computeSpatiallySampledMetrics.py
Go to the documentation of this file.
1# This file is part of ip_diffim.
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
22import numpy as np
23
24import lsst.geom
25
26import lsst.afw.table as afwTable
27import lsst.pipe.base as pipeBase
28import lsst.pex.config as pexConfig
29
30from lsst.ip.diffim.utils import getPsfFwhm, angleMean, evaluateMaskFraction
31from lsst.meas.algorithms import SkyObjectsTask
32from lsst.pex.exceptions import InvalidParameterError
33from lsst.utils.timer import timeMethod
34
35import lsst.utils
36
37__all__ = ["SpatiallySampledMetricsConfig", "SpatiallySampledMetricsTask"]
38
39
40class SpatiallySampledMetricsConnections(pipeBase.PipelineTaskConnections,
41 dimensions=("instrument", "visit", "detector"),
42 defaultTemplates={"coaddName": "deep",
43 "warpTypeSuffix": "",
44 "fakesType": ""}):
45 science = pipeBase.connectionTypes.Input(
46 doc="Input science exposure.",
47 dimensions=("instrument", "visit", "detector"),
48 storageClass="ExposureF",
49 name="{fakesType}calexp"
50 )
51 matchedTemplate = pipeBase.connectionTypes.Input(
52 doc="Warped and PSF-matched template used to create the difference image.",
53 dimensions=("instrument", "visit", "detector"),
54 storageClass="ExposureF",
55 name="{fakesType}{coaddName}Diff_matchedExp",
56 )
57 template = pipeBase.connectionTypes.Input(
58 doc="Warped and not PSF-matched template used to create the difference image.",
59 dimensions=("instrument", "visit", "detector"),
60 storageClass="ExposureF",
61 name="{fakesType}{coaddName}Diff_templateExp",
62 )
63 difference = pipeBase.connectionTypes.Input(
64 doc="Difference image with detection mask plane filled in.",
65 dimensions=("instrument", "visit", "detector"),
66 storageClass="ExposureF",
67 name="{fakesType}{coaddName}Diff_differenceExp",
68 )
69 diaSources = pipeBase.connectionTypes.Input(
70 doc="Filtered diaSources on the difference image.",
71 dimensions=("instrument", "visit", "detector"),
72 storageClass="SourceCatalog",
73 name="{fakesType}{coaddName}Diff_candidateDiaSrc",
74 )
75 spatiallySampledMetrics = pipeBase.connectionTypes.Output(
76 doc="Summary metrics computed at randomized locations.",
77 dimensions=("instrument", "visit", "detector"),
78 storageClass="ArrowAstropy",
79 name="{fakesType}{coaddName}Diff_spatiallySampledMetrics",
80 )
81
82
83class SpatiallySampledMetricsConfig(pipeBase.PipelineTaskConfig,
84 pipelineConnections=SpatiallySampledMetricsConnections):
85 """Config for SpatiallySampledMetricsTask
86 """
87 metricsMaskPlanes = lsst.pex.config.ListField(
88 dtype=str,
89 doc="List of mask planes to include in metrics",
90 default=('BAD', 'CLIPPED', 'CR', 'DETECTED', 'DETECTED_NEGATIVE', 'EDGE',
91 'INEXACT_PSF', 'INJECTED', 'INJECTED_TEMPLATE', 'INTRP', 'NOT_DEBLENDED',
92 'NO_DATA', 'REJECTED', 'SAT', 'SAT_TEMPLATE', 'SENSOR_EDGE', 'STREAK', 'SUSPECT',
93 'UNMASKEDNAN',
94 ),
95 )
96 metricSources = pexConfig.ConfigurableField(
97 target=SkyObjectsTask,
98 doc="Generate QA metric sources",
99 )
100
101 def setDefaults(self):
102 self.metricSources.avoidMask = ["NO_DATA", "EDGE"]
103
104
105class SpatiallySampledMetricsTask(lsst.pipe.base.PipelineTask):
106 """Detect and measure sources on a difference image.
107 """
108 ConfigClass = SpatiallySampledMetricsConfig
109 _DefaultName = "spatiallySampledMetrics"
110
111 def __init__(self, **kwargs):
112 super().__init__(**kwargs)
113
114 self.makeSubtask("metricSources")
115 self.schema = afwTable.SourceTable.makeMinimalSchema()
116 self.schema.addField(
117 "x", "F",
118 "X location of the metric evaluation.",
119 units="pixel")
120 self.schema.addField(
121 "y", "F",
122 "Y location of the metric evaluation.",
123 units="pixel")
124 self.metricSources.skySourceKey = self.schema.addField("sky_source", type="Flag",
125 doc="Metric evaluation objects.")
126 self.schema.addField(
127 "source_density", "F",
128 "Density of diaSources at location.",
129 units="count/degree^2")
130 self.schema.addField(
131 "dipole_density", "F",
132 "Density of dipoles at location.",
133 units="count/degree^2")
134 self.schema.addField(
135 "dipole_direction", "F",
136 "Mean dipole orientation.",
137 units="radian")
138 self.schema.addField(
139 "dipole_separation", "F",
140 "Mean dipole separation.",
141 units="pixel")
142 self.schema.addField(
143 "template_value", "F",
144 "Median of template at location.",
145 units="nJy")
146 self.schema.addField(
147 "science_value", "F",
148 "Median of science at location.",
149 units="nJy")
150 self.schema.addField(
151 "diffim_value", "F",
152 "Median of diffim at location.",
153 units="nJy")
154 self.schema.addField(
155 "science_psfSize", "F",
156 "Width of the science image PSF at location.",
157 units="pixel")
158 self.schema.addField(
159 "template_psfSize", "F",
160 "Width of the template image PSF at location.",
161 units="pixel")
162 for maskPlane in self.config.metricsMaskPlanes:
163 self.schema.addField(
164 "%s_mask_fraction"%maskPlane.lower(), "F",
165 "Fraction of pixels with %s mask"%maskPlane
166 )
167
168 @timeMethod
169 def run(self, science, matchedTemplate, template, difference, diaSources):
170 """Calculate difference image metrics on specific locations across the images
171
172 Parameters
173 ----------
174 science : `lsst.afw.image.ExposureF`
175 Science exposure that the template was subtracted from.
176 matchedTemplate : `lsst.afw.image.ExposureF`
177 Warped and PSF-matched template that was used produce the
178 difference image.
179 template : `lsst.afw.image.ExposureF`
180 Warped and non PSF-matched template that was used produce
181 the difference image.
182 difference : `lsst.afw.image.ExposureF`
183 Result of subtracting template from the science image.
184 diaSources : `lsst.afw.table.SourceCatalog`
185 The catalog of detected sources.
186
187 Returns
188 -------
189 results : `lsst.pipe.base.Struct`
190 ``spatiallySampledMetrics`` : `astropy.table.Table`
191 Image quality metrics spatially sampled locations.
192 """
193
194 idFactory = lsst.meas.base.IdGenerator().make_table_id_factory()
195
196 spatiallySampledMetrics = afwTable.SourceCatalog(self.schema)
197 spatiallySampledMetrics.getTable().setIdFactory(idFactory)
198
199 self.metricSources.run(mask=science.mask, seed=difference.info.id, catalog=spatiallySampledMetrics)
200
201 metricsMaskPlanes = []
202 for maskPlane in self.config.metricsMaskPlanes:
203 try:
204 metricsMaskPlanes.append(maskPlane)
205 except InvalidParameterError:
206 self.log.info("Unable to calculate metrics for mask plane %s: not in image"%maskPlane)
207
208 for src in spatiallySampledMetrics:
209 self._evaluateLocalMetric(src, science, matchedTemplate, template, difference, diaSources,
210 metricsMaskPlanes=metricsMaskPlanes)
211
212 return pipeBase.Struct(spatiallySampledMetrics=spatiallySampledMetrics.asAstropy())
213
214 def _evaluateLocalMetric(self, src, science, matchedTemplate, template, difference, diaSources,
215 metricsMaskPlanes):
216 """Calculate image quality metrics at spatially sampled locations.
217
218 Parameters
219 ----------
220 src : `lsst.afw.table.SourceRecord`
221 The source record to be updated with metric calculations.
222 diaSources : `lsst.afw.table.SourceCatalog`
223 The catalog of detected sources.
224 science : `lsst.afw.image.Exposure`
225 The science image.
226 matchedTemplate : `lsst.afw.image.Exposure`
227 The reference image, warped and psf-matched to the science image.
228 difference : `lsst.afw.image.Exposure`
229 Result of subtracting template from the science image.
230 metricsMaskPlanes : `list` of `str`
231 Mask planes to calculate metrics from.
232 """
233 bbox = src.getFootprint().getBBox()
234 pix = bbox.getCenter()
235 src.set('science_psfSize', getPsfFwhm(science.psf, position=pix))
236 try:
237 src.set('template_psfSize', getPsfFwhm(template.psf, position=pix))
238 except InvalidParameterError:
239 src.set('template_psfSize', np.nan)
240
241 metricRegionSize = 100
242 bbox.grow(metricRegionSize)
243 bbox = bbox.clippedTo(science.getBBox())
244 nPix = bbox.getArea()
245 pixScale = science.wcs.getPixelScale()
246 area = nPix*pixScale.asDegrees()**2
247 peak = src.getFootprint().getPeaks()[0]
248 src.set('x', peak['i_x'])
249 src.set('y', peak['i_y'])
250 src.setCoord(science.wcs.pixelToSky(peak['i_x'], peak['i_y']))
251 selectSources = diaSources[bbox.contains(diaSources.getX(), diaSources.getY())]
252 sourceDensity = len(selectSources)/area
253 dipoleSources = selectSources[selectSources["ip_diffim_DipoleFit_flag_classification"]]
254 dipoleDensity = len(dipoleSources)/area
255
256 if dipoleSources:
257 meanDipoleOrientation = angleMean(dipoleSources["ip_diffim_DipoleFit_orientation"])
258 src.set('dipole_direction', meanDipoleOrientation)
259 meanDipoleSeparation = np.mean(dipoleSources["ip_diffim_DipoleFit_separation"])
260 src.set('dipole_separation', meanDipoleSeparation)
261
262 templateVal = np.median(matchedTemplate[bbox].image.array)
263 scienceVal = np.median(science[bbox].image.array)
264 diffimVal = np.median(difference[bbox].image.array)
265 src.set('source_density', sourceDensity)
266 src.set('dipole_density', dipoleDensity)
267 src.set('template_value', templateVal)
268 src.set('science_value', scienceVal)
269 src.set('diffim_value', diffimVal)
270 for maskPlane in metricsMaskPlanes:
271 src.set("%s_mask_fraction"%maskPlane.lower(),
272 evaluateMaskFraction(difference.mask[bbox], maskPlane)
273 )
run(self, coaddExposures, bbox, wcs, dataIds, physical_filter=None, **kwargs)