92 def run(self, scienceExposure, templateExposure, subtractedExposure, psfMatchingKernel,
93 preConvKernel=None, xcen=None, ycen=None, svar=None, tvar=None,
94 templateMatched=True, preConvMode=False, **kwargs):
95 """Perform decorrelation of an image difference or of a score difference exposure.
97 Corrects the difference or score image due to the convolution of the
98 templateExposure with the A&L PSF matching kernel.
99 See [DMTN-021, Equation 1](http://dmtn-021.lsst.io/#equation-1) and
100 [DMTN-179](http://dmtn-179.lsst.io/) for details.
104 scienceExposure : `lsst.afw.image.Exposure`
105 The original science exposure (before pre-convolution, if ``preConvMode==True``).
106 templateExposure : `lsst.afw.image.Exposure`
107 The original template exposure warped, but not psf-matched, to the science exposure.
108 subtractedExposure : `lsst.afw.image.Exposure`
109 the subtracted exposure produced by
110 `ip_diffim.ImagePsfMatchTask.subtractExposures()`. The `subtractedExposure` must
111 inherit its PSF from `exposure`, see notes below.
112 psfMatchingKernel : `lsst.afw.detection.Psf`
113 An (optionally spatially-varying) PSF matching kernel produced
114 by `ip_diffim.ImagePsfMatchTask.subtractExposures()`.
115 preConvKernel : `lsst.afw.math.Kernel`, optional
116 If not `None`, then the `scienceExposure` was pre-convolved with (the reflection of)
117 this kernel. Must be normalized to sum to 1.
118 Allowed only if ``templateMatched==True`` and ``preConvMode==True``.
119 Defaults to the PSF of the science exposure at the image center.
120 xcen : `float`, optional
121 X-pixel coordinate to use for computing constant matching kernel to use
122 If `None` (default), then use the center of the image.
123 ycen : `float`, optional
124 Y-pixel coordinate to use for computing constant matching kernel to use
125 If `None` (default), then use the center of the image.
126 svar : `float`, optional
127 Image variance for science image
128 If `None` (default) then compute the variance over the entire input science image.
129 tvar : `float`, optional
130 Image variance for template image
131 If `None` (default) then compute the variance over the entire input template image.
132 templateMatched : `bool`, optional
133 If True, the template exposure was matched (convolved) to the science exposure.
134 See also notes below.
135 preConvMode : `bool`, optional
136 If True, ``subtractedExposure`` is assumed to be a likelihood difference image
137 and will be noise corrected as a likelihood image.
139 Additional keyword arguments propagated from DecorrelateALKernelSpatialTask.
143 result : `lsst.pipe.base.Struct`
144 - ``correctedExposure`` : the decorrelated diffim
148 If ``preConvMode==True``, ``subtractedExposure`` is assumed to be a
149 score image and the noise correction for likelihood images
150 is applied. The resulting image is an optimal detection likelihood image
151 when the templateExposure has noise. (See DMTN-179) If ``preConvKernel`` is
152 not specified, the PSF of ``scienceExposure`` is assumed as pre-convolution kernel.
154 The ``subtractedExposure`` is NOT updated. The returned ``correctedExposure``
155 has an updated but spatially fixed PSF. It is calculated as the center of
156 image PSF corrected by the center of image matching kernel.
158 If ``templateMatched==True``, the templateExposure was matched (convolved)
159 to the ``scienceExposure`` by ``psfMatchingKernel`` during image differencing.
160 Otherwise the ``scienceExposure`` was matched (convolved) by ``psfMatchingKernel``.
161 In either case, note that the original template and science images are required,
162 not the psf-matched version.
164 This task discards the variance plane of ``subtractedExposure`` and re-computes
165 it from the variance planes of ``scienceExposure`` and ``templateExposure``.
166 The image plane of ``subtractedExposure`` must be at the photometric level
167 set by the AL PSF matching in `ImagePsfMatchTask.subtractExposures`.
168 The assumptions about the photometric level are controlled by the
169 `templateMatched` option in this task.
171 Here we currently convert a spatially-varying matching kernel into a constant kernel,
172 just by computing it at the center of the image (tickets DM-6243, DM-6244).
174 We are also using a constant accross-the-image measure of sigma (sqrt(variance)) to compute
175 the decorrelation kernel.
177 TODO DM-23857 As part of the spatially varying correction implementation
178 consider whether returning a Struct is still necessary.
180 if preConvKernel
is not None and not (templateMatched
and preConvMode):
181 raise ValueError(
"Pre-convolution kernel is allowed only if "
182 "preConvMode==True and templateMatched==True.")
184 spatialKernel = psfMatchingKernel
185 kimg = afwImage.ImageD(spatialKernel.getDimensions())
186 bbox = subtractedExposure.getBBox()
188 xcen = (bbox.getBeginX() + bbox.getEndX()) / 2.
190 ycen = (bbox.getBeginY() + bbox.getEndY()) / 2.
191 self.log.info(
"Using matching kernel computed at (%d, %d)", xcen, ycen)
192 spatialKernel.computeImage(kimg,
False, xcen, ycen)
196 if preConvKernel
is None:
197 pos = scienceExposure.getPsf().getAveragePosition()
198 preConvKernel = scienceExposure.getPsf().getLocalKernel(pos)
199 preConvImg = afwImage.ImageD(preConvKernel.getDimensions())
200 preConvKernel.computeImage(preConvImg,
True)
206 self.log.info(
"Original variance plane means. Science:%.5e, warped template:%.5e)",
212 if np.isnan(svar)
or np.isnan(tvar):
214 if (np.all(np.isnan(scienceExposure.image.array))
215 or np.all(np.isnan(templateExposure.image.array))):
216 self.log.warning(
'Template or science image is entirely NaNs: skipping decorrelation.')
217 outExposure = subtractedExposure.clone()
218 return pipeBase.Struct(correctedExposure=outExposure, )
222 self.log.info(
"Decorrelation after template image convolution")
224 targetVarianceMean = tvar
226 variance = scienceExposure.variance.array
228 targetVariance = templateExposure.variance.array
231 psfImg = scienceExposure.getPsf().computeKernelImage(
geom.Point2D(xcen, ycen))
234 self.log.info(
"Decorrelation after science image convolution")
236 targetVarianceMean = svar
238 variance = templateExposure.variance.array
240 targetVariance = scienceExposure.variance.array
247 psfImg = templateExposure.getPsf().computeKernelImage(
geom.Point2D(xcen, ycen))
248 except InvalidParameterError:
249 psfImg = computeAveragePsf(templateExposure, psfExposureBuffer=0.05, psfExposureGrid=100)
253 mOverExpVar = targetVarianceMean/varianceMean
254 if mOverExpVar > 1e8:
255 self.log.warning(
"Diverging correction: matched image variance is "
256 " much larger than the unconvolved one's"
257 ", targetVarianceMean/varianceMean:%.2e", mOverExpVar)
260 self.log.info(
"Variance plane mean of uncorrected diffim: %f", oldVarMean)
263 diffimShape = subtractedExposure.image.array.shape
264 psfShape = psfImg.array.shape
267 self.log.info(
"Decorrelation of likelihood image")
269 psfShape, diffimShape)
272 self.log.info(
"Decorrelation of difference image")
285 if self.config.completeVarPlanePropagation:
286 self.log.debug(
"Using full variance plane calculation in decorrelation")
288 variance, targetVariance,
289 varianceMean, targetVarianceMean, corr.cnft, corr.crft)
291 self.log.debug(
"Using estimated variance plane calculation in decorrelation")
293 variance, targetVariance,
294 corr.cnft, corr.crft)
299 self.log.debug(
"Matching kernel sum: %.3e", kSum)
300 if not templateMatched:
303 correctedVariance /= kSumSq
304 subtractedExposure.image.array[...] = correctedImage
305 subtractedExposure.variance.array[...] = correctedVariance
310 self.log.info(
"Variance plane mean of corrected diffim: %.5e", newVarMean)
314 return pipeBase.Struct(correctedExposure=subtractedExposure, )
350 """Zero pad an image where the origin is at the center and replace the
351 origin to the corner as required by the periodic input of FFT. Implement also
352 the inverse operation, crop the padding and re-center data.
357 An array to copy from.
358 newShape : `tuple` of `int`
359 The dimensions of the resulting array. For padding, the resulting array
360 must be larger than A in each dimension. For the inverse operation this
361 must be the original, before padding size of the array.
362 useInverse : bool, optional
363 Selector of forward, add padding, operation (False)
364 or its inverse, crop padding, operation (True).
369 The padded or unpadded array with shape of `newShape` and the same dtype as A.
373 For odd dimensions, the splitting is rounded to
374 put the center pixel into the new corner origin (0,0). This is to be consistent
375 e.g. for a dirac delta kernel that is originally located at the center pixel.
382 firstHalves = [x//2
for x
in A.shape]
383 secondHalves = [x-y
for x, y
in zip(A.shape, firstHalves)]
386 secondHalves = [x//2
for x
in newShape]
387 firstHalves = [x-y
for x, y
in zip(newShape, secondHalves)]
389 R = np.zeros_like(A, shape=newShape)
390 R[-firstHalves[0]:, -firstHalves[1]:] = A[:firstHalves[0], :firstHalves[1]]
391 R[:secondHalves[0], -firstHalves[1]:] = A[-secondHalves[0]:, :firstHalves[1]]
392 R[:secondHalves[0], :secondHalves[1]] = A[-secondHalves[0]:, -secondHalves[1]:]
393 R[-firstHalves[0]:, :secondHalves[1]] = A[:firstHalves[0], -secondHalves[1]:]
437 """Compute the correction kernel for a score image.
441 kappa : `numpy.ndarray`
442 A matching kernel 2-d numpy.array derived from Alard & Lupton PSF matching.
444 Average variance of science image used for PSF matching (before pre-convolution).
446 Average variance of the template (matched) image used for PSF matching.
447 preConvArr : `numpy.ndarray`
448 The pre-convolution kernel of the science image. It should be the PSF
449 of the science image or an approximation of it. It must be normed to sum 1.
453 corrft : `numpy.ndarray` of `float`
454 The frequency space representation of the correction. The array is real (dtype float).
455 Shape is `self.freqSpaceShape`.
456 cnft, crft : `numpy.ndarray` of `complex`
457 The overall convolution (pre-conv, PSF matching, noise correction) kernel
458 for the science and template images, respectively for the variance plane
459 calculations. These are intermediate results in frequency space.
463 To be precise, the science image should be _correlated_ by ``preConvArray`` but this
464 does not matter for this calculation.
466 ``cnft``, ``crft`` contain the scaling factor as well.
471 kft = np.fft.fft2(kappa)
473 preFt = np.fft.fft2(preConvArr)
474 preFtAbsSq = np.real(np.conj(preFt) * preFt)
475 kftAbsSq = np.real(np.conj(kft) * kft)
478 tiny = np.finfo(preFtAbsSq.dtype).tiny * 1000.
479 flt = preFtAbsSq < tiny
483 preFtAbsSq[flt] = tiny
484 denom = svar + tvar * kftAbsSq / preFtAbsSq
485 corrft = (svar + tvar * kSum*kSum) / denom
486 cnft = np.conj(preFt)*corrft
488 return pipeBase.Struct(corrft=corrft, cnft=cnft, crft=crft)
524 """Full propagation of the variance planes of the original exposures.
526 The original variance planes of independent pixels are convolved with the
527 image space square of the overall kernels.
531 vplane1, vplane2 : `numpy.ndarray` of `float`
532 Variance planes of the original (before pre-convolution or matching)
534 varMean1, varMean2 : `float`
535 Replacement average values for non-finite ``vplane1`` and ``vplane2`` values respectively.
537 c1ft, c2ft : `numpy.ndarray` of `complex`
538 The overall convolution that includes the matching and the
539 afterburner in frequency space. The result of either
540 ``computeScoreCorrection`` or ``computeDiffimCorrection``.
544 vplaneD : `numpy.ndarray` of `float`
545 The variance plane of the difference/score images.
549 See DMTN-179 Section 5 about the variance plane calculations.
551 Infs and NaNs are allowed and kept in the returned array.
553 D = np.real(np.fft.ifft2(c1ft))
554 c1SqFt = np.fft.fft2(D*D)
556 v1shape = vplane1.shape
557 filtInf = np.isinf(vplane1)
558 filtNan = np.isnan(vplane1)
560 vplane1 = np.copy(vplane1)
561 vplane1[filtInf | filtNan] = varMean1
563 v1 = np.real(np.fft.ifft2(np.fft.fft2(D) * c1SqFt))
568 D = np.real(np.fft.ifft2(c2ft))
569 c2SqFt = np.fft.fft2(D*D)
571 v2shape = vplane2.shape
572 filtInf = np.isinf(vplane2)
573 filtNan = np.isnan(vplane2)
574 vplane2 = np.copy(vplane2)
575 vplane2[filtInf | filtNan] = varMean2
577 v2 = np.real(np.fft.ifft2(np.fft.fft2(D) * c2SqFt))