30from lsst.ip.diffim.utils
import getPsfFwhm, angleMean, evaluateMaskFraction
33from lsst.utils.timer
import timeMethod
37__all__ = [
"SpatiallySampledMetricsConfig",
"SpatiallySampledMetricsTask"]
41 dimensions=(
"instrument",
"visit",
"detector"),
42 defaultTemplates={
"coaddName":
"deep",
45 science = pipeBase.connectionTypes.Input(
46 doc=
"Input science exposure.",
47 dimensions=(
"instrument",
"visit",
"detector"),
48 storageClass=
"ExposureF",
49 name=
"{fakesType}calexp"
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",
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",
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",
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",
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",
83class SpatiallySampledMetricsConfig(pipeBase.PipelineTaskConfig,
84 pipelineConnections=SpatiallySampledMetricsConnections):
85 """Config for SpatiallySampledMetricsTask
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',
96 metricSources = pexConfig.ConfigurableField(
97 target=SkyObjectsTask,
98 doc=
"Generate QA metric sources",
101 def setDefaults(self):
102 self.metricSources.avoidMask = [
"NO_DATA",
"EDGE"]
105class SpatiallySampledMetricsTask(lsst.pipe.base.PipelineTask):
106 """Detect and measure sources on a difference image.
108 ConfigClass = SpatiallySampledMetricsConfig
109 _DefaultName =
"spatiallySampledMetrics"
111 def __init__(self, **kwargs):
112 super().__init__(**kwargs)
114 self.makeSubtask(
"metricSources")
115 self.schema = afwTable.SourceTable.makeMinimalSchema()
116 self.schema.addField(
118 "X location of the metric evaluation.",
120 self.schema.addField(
122 "Y location of the metric evaluation.",
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.",
138 self.schema.addField(
139 "dipole_separation",
"F",
140 "Mean dipole separation.",
142 self.schema.addField(
143 "template_value",
"F",
144 "Median of template at location.",
146 self.schema.addField(
147 "science_value",
"F",
148 "Median of science at location.",
150 self.schema.addField(
152 "Median of diffim at location.",
154 self.schema.addField(
155 "science_psfSize",
"F",
156 "Width of the science image PSF at location.",
158 self.schema.addField(
159 "template_psfSize",
"F",
160 "Width of the template image PSF at location.",
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
169 def run(self, science, matchedTemplate, template, difference, diaSources):
170 """Calculate difference image metrics on specific locations across the images
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
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.
189 results : `lsst.pipe.base.Struct`
190 ``spatiallySampledMetrics`` : `astropy.table.Table`
191 Image quality metrics spatially sampled locations.
197 spatiallySampledMetrics.getTable().setIdFactory(idFactory)
199 self.metricSources.run(mask=science.mask, seed=difference.info.id, catalog=spatiallySampledMetrics)
201 metricsMaskPlanes = []
202 for maskPlane
in self.config.metricsMaskPlanes:
204 metricsMaskPlanes.append(maskPlane)
205 except InvalidParameterError:
206 self.log.info(
"Unable to calculate metrics for mask plane %s: not in image"%maskPlane)
208 for src
in spatiallySampledMetrics:
209 self._evaluateLocalMetric(src, science, matchedTemplate, template, difference, diaSources,
210 metricsMaskPlanes=metricsMaskPlanes)
212 return pipeBase.Struct(spatiallySampledMetrics=spatiallySampledMetrics.asAstropy())
214 def _evaluateLocalMetric(self, src, science, matchedTemplate, template, difference, diaSources,
216 """Calculate image quality metrics at spatially sampled locations.
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`
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.
233 bbox = src.getFootprint().getBBox()
234 pix = bbox.getCenter()
235 src.set(
'science_psfSize', getPsfFwhm(science.psf, position=pix))
237 src.set(
'template_psfSize', getPsfFwhm(template.psf, position=pix))
238 except InvalidParameterError:
239 src.set(
'template_psfSize', np.nan)
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
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)
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)
run(self, coaddExposures, bbox, wcs, dataIds, physical_filter=None, **kwargs)