30from lsst.ip.diffim.utils
import getPsfFwhm, angleMean, evaluateMaskFraction, getKernelCenterDisplacement
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 psfMatchingKernel = pipeBase.connectionTypes.Input(
76 doc=
"Kernel used to PSF match the science and template images.",
77 dimensions=(
"instrument",
"visit",
"detector"),
78 storageClass=
"MatchingKernel",
79 name=
"{fakesType}{coaddName}Diff_psfMatchKernel",
81 spatiallySampledMetrics = pipeBase.connectionTypes.Output(
82 doc=
"Summary metrics computed at randomized locations.",
83 dimensions=(
"instrument",
"visit",
"detector"),
84 storageClass=
"ArrowAstropy",
85 name=
"{fakesType}{coaddName}Diff_spatiallySampledMetrics",
89class SpatiallySampledMetricsConfig(pipeBase.PipelineTaskConfig,
90 pipelineConnections=SpatiallySampledMetricsConnections):
91 """Config for SpatiallySampledMetricsTask
95 doc=
"List of mask planes to include in metrics",
96 default=(
'BAD',
'CLIPPED',
'CR',
'DETECTED',
'DETECTED_NEGATIVE',
'EDGE',
97 'INEXACT_PSF',
'INJECTED',
'INJECTED_TEMPLATE',
'INTRP',
'NOT_DEBLENDED',
98 'NO_DATA',
'REJECTED',
'SAT',
'SAT_TEMPLATE',
'SENSOR_EDGE',
'STREAK',
'SUSPECT',
102 metricSources = pexConfig.ConfigurableField(
103 target=SkyObjectsTask,
104 doc=
"Generate QA metric sources",
107 def setDefaults(self):
108 self.metricSources.avoidMask = [
"NO_DATA",
"EDGE"]
111class SpatiallySampledMetricsTask(lsst.pipe.base.PipelineTask):
112 """Detect and measure sources on a difference image.
114 ConfigClass = SpatiallySampledMetricsConfig
115 _DefaultName =
"spatiallySampledMetrics"
117 def __init__(self, **kwargs):
118 super().__init__(**kwargs)
120 self.makeSubtask(
"metricSources")
121 self.schema = afwTable.SourceTable.makeMinimalSchema()
122 self.schema.addField(
124 "X location of the metric evaluation.",
126 self.schema.addField(
128 "Y location of the metric evaluation.",
130 self.metricSources.skySourceKey = self.schema.addField(
"sky_source", type=
"Flag",
131 doc=
"Metric evaluation objects.")
132 self.schema.addField(
133 "source_density",
"F",
134 "Density of diaSources at location.",
135 units=
"count/degree^2")
136 self.schema.addField(
137 "dipole_density",
"F",
138 "Density of dipoles at location.",
139 units=
"count/degree^2")
140 self.schema.addField(
141 "dipole_direction",
"F",
142 "Mean dipole orientation.",
144 self.schema.addField(
145 "dipole_separation",
"F",
146 "Mean dipole separation.",
148 self.schema.addField(
149 "template_value",
"F",
150 "Median of template at location.",
152 self.schema.addField(
153 "science_value",
"F",
154 "Median of science at location.",
156 self.schema.addField(
158 "Median of diffim at location.",
160 self.schema.addField(
161 "science_psfSize",
"F",
162 "Width of the science image PSF at location.",
164 self.schema.addField(
165 "template_psfSize",
"F",
166 "Width of the template image PSF at location.",
168 for maskPlane
in self.config.metricsMaskPlanes:
169 self.schema.addField(
170 "%s_mask_fraction"%maskPlane.lower(),
"F",
171 "Fraction of pixels with %s mask"%maskPlane
173 self.schema.addField(
174 "psfMatchingKernel_sum",
"F",
175 "PSF matching kernel sum at location.")
176 self.schema.addField(
177 "psfMatchingKernel_dx",
"F",
178 "PSF matching kernel centroid offset in x at location.",
180 self.schema.addField(
181 "psfMatchingKernel_dy",
"F",
182 "PSF matching kernel centroid offset in y at location.",
184 self.schema.addField(
185 "psfMatchingKernel_length",
"F",
186 "PSF matching kernel centroid offset module.",
188 self.schema.addField(
189 "psfMatchingKernel_position_angle",
"F",
190 "PSF matching kernel centroid offset position angle.",
192 self.schema.addField(
193 "psfMatchingKernel_direction",
"F",
194 "PSF matching kernel centroid offset direction in detector plane.",
198 def run(self, science, matchedTemplate, template, difference, diaSources, psfMatchingKernel):
199 """Calculate difference image metrics on specific locations across the images
203 science : `lsst.afw.image.ExposureF`
204 Science exposure that the template was subtracted from.
205 matchedTemplate : `lsst.afw.image.ExposureF`
206 Warped and PSF-matched template that was used produce the
208 template : `lsst.afw.image.ExposureF`
209 Warped and non PSF-matched template that was used produce
210 the difference image.
211 difference : `lsst.afw.image.ExposureF`
212 Result of subtracting template from the science image.
213 diaSources : `lsst.afw.table.SourceCatalog`
214 The catalog of detected sources.
215 psfMatchingKernel : `~lsst.afw.math.LinearCombinationKernel`
216 The PSF matching kernel of the subtraction to evaluate.
220 results : `lsst.pipe.base.Struct`
221 ``spatiallySampledMetrics`` : `astropy.table.Table`
222 Image quality metrics spatially sampled locations.
228 spatiallySampledMetrics.getTable().setIdFactory(idFactory)
230 self.metricSources.run(mask=science.mask, seed=difference.info.id, catalog=spatiallySampledMetrics)
232 metricsMaskPlanes = []
233 for maskPlane
in self.config.metricsMaskPlanes:
235 metricsMaskPlanes.append(maskPlane)
236 except InvalidParameterError:
237 self.log.info(
"Unable to calculate metrics for mask plane %s: not in image"%maskPlane)
239 for src
in spatiallySampledMetrics:
240 self._evaluateLocalMetric(src, science, matchedTemplate, template, difference, diaSources,
241 metricsMaskPlanes=metricsMaskPlanes,
242 psfMatchingKernel=psfMatchingKernel)
244 return pipeBase.Struct(spatiallySampledMetrics=spatiallySampledMetrics.asAstropy())
246 def _evaluateLocalMetric(self, src, science, matchedTemplate, template, difference, diaSources,
247 metricsMaskPlanes, psfMatchingKernel):
248 """Calculate image quality metrics at spatially sampled locations.
252 src : `lsst.afw.table.SourceRecord`
253 The source record to be updated with metric calculations.
254 diaSources : `lsst.afw.table.SourceCatalog`
255 The catalog of detected sources.
256 science : `lsst.afw.image.Exposure`
258 matchedTemplate : `lsst.afw.image.Exposure`
259 The reference image, warped and psf-matched to the science image.
260 difference : `lsst.afw.image.Exposure`
261 Result of subtracting template from the science image.
262 metricsMaskPlanes : `list` of `str`
263 Mask planes to calculate metrics from.
264 psfMatchingKernel : `~lsst.afw.math.LinearCombinationKernel`
265 The PSF matching kernel of the subtraction to evaluate.
267 bbox = src.getFootprint().getBBox()
268 pix = bbox.getCenter()
269 src.set(
'science_psfSize', getPsfFwhm(science.psf, position=pix))
271 src.set(
'template_psfSize', getPsfFwhm(template.psf, position=pix))
272 except (InvalidParameterError, RangeError):
273 src.set(
'template_psfSize', np.nan)
275 metricRegionSize = 100
276 bbox.grow(metricRegionSize)
277 bbox = bbox.clippedTo(science.getBBox())
278 nPix = bbox.getArea()
279 pixScale = science.wcs.getPixelScale(bbox.getCenter())
280 area = nPix*pixScale.asDegrees()**2
281 peak = src.getFootprint().getPeaks()[0]
282 src.set(
'x', peak[
'i_x'])
283 src.set(
'y', peak[
'i_y'])
284 src.setCoord(science.wcs.pixelToSky(peak[
'i_x'], peak[
'i_y']))
285 selectSources = diaSources[bbox.contains(diaSources.getX(), diaSources.getY())]
286 sourceDensity = len(selectSources)/area
287 dipoleSources = selectSources[selectSources[
"ip_diffim_DipoleFit_flag_classification"]]
288 dipoleDensity = len(dipoleSources)/area
291 meanDipoleOrientation = angleMean(dipoleSources[
"ip_diffim_DipoleFit_orientation"])
292 src.set(
'dipole_direction', meanDipoleOrientation)
293 meanDipoleSeparation = np.mean(dipoleSources[
"ip_diffim_DipoleFit_separation"])
294 src.set(
'dipole_separation', meanDipoleSeparation)
296 templateVal = np.median(matchedTemplate[bbox].image.array)
297 scienceVal = np.median(science[bbox].image.array)
298 diffimVal = np.median(difference[bbox].image.array)
299 src.set(
'source_density', sourceDensity)
300 src.set(
'dipole_density', dipoleDensity)
301 src.set(
'template_value', templateVal)
302 src.set(
'science_value', scienceVal)
303 src.set(
'diffim_value', diffimVal)
304 for maskPlane
in metricsMaskPlanes:
305 src.set(
"%s_mask_fraction"%maskPlane.lower(),
306 evaluateMaskFraction(difference.mask[bbox], maskPlane)
309 krnlSum, dx, dy, direction, length = getKernelCenterDisplacement(
310 psfMatchingKernel, src.get(
'x'), src.get(
'y'))
313 src.get(
'coord_ra'), src.get(
'coord_dec'),
315 point2 = science.wcs.pixelToSky(src.get(
'x') + dx, src.get(
'y') + dy)
316 bearing = point1.bearingTo(point2)
318 pa = pa_ref_angle - bearing
321 position_angle = pa.asRadians()
323 src.set(
'psfMatchingKernel_sum', krnlSum)
324 src.set(
'psfMatchingKernel_dx', dx)
325 src.set(
'psfMatchingKernel_dy', dy)
326 src.set(
'psfMatchingKernel_length', length*pixScale.asArcseconds())
327 src.set(
'psfMatchingKernel_position_angle', position_angle)
328 src.set(
'psfMatchingKernel_direction', direction)
A class representing an angle.
Point in an unspecified spherical coordinate system.
run(self, coaddExposures, bbox, wcs, dataIds, physical_filter)