LSST Applications g0f08755f38+82efc23009,g12f32b3c4e+e7bdf1200e,g1653933729+a8ce1bb630,g1a0ca8cf93+50eff2b06f,g28da252d5a+52db39f6a5,g2bbee38e9b+37c5a29d61,g2bc492864f+37c5a29d61,g2cdde0e794+c05ff076ad,g3156d2b45e+41e33cbcdc,g347aa1857d+37c5a29d61,g35bb328faa+a8ce1bb630,g3a166c0a6a+37c5a29d61,g3e281a1b8c+fb992f5633,g414038480c+7f03dfc1b0,g41af890bb2+11b950c980,g5fbc88fb19+17cd334064,g6b1c1869cb+12dd639c9a,g781aacb6e4+a8ce1bb630,g80478fca09+72e9651da0,g82479be7b0+04c31367b4,g858d7b2824+82efc23009,g9125e01d80+a8ce1bb630,g9726552aa6+8047e3811d,ga5288a1d22+e532dc0a0b,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc28159a63d+37c5a29d61,gcf0d15dbbd+2acd6d4d48,gd7358e8bfb+778a810b6e,gda3e153d99+82efc23009,gda6a2b7d83+2acd6d4d48,gdaeeff99f8+1711a396fd,ge2409df99d+6b12de1076,ge79ae78c31+37c5a29d61,gf0baf85859+d0a5978c5a,gf3967379c6+4954f8c433,gfb92a5be7c+82efc23009,gfec2e1e490+2aaed99252,w.2024.46
LSST Data Management Base Package
Loading...
Searching...
No Matches
imageDecorrelation.py
Go to the documentation of this file.
2# LSST Data Management System
3# Copyright 2016 AURA/LSST.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <https://www.lsstcorp.org/LegalNotices/>.
21#
22
23import numpy as np
24
25import lsst.afw.image as afwImage
26import lsst.afw.math as afwMath
27import lsst.geom as geom
28import lsst.meas.algorithms as measAlg
29import lsst.pex.config as pexConfig
30import lsst.pipe.base as pipeBase
31from lsst.pex.exceptions import InvalidParameterError
32from lsst.utils.timer import timeMethod
33
34from .imageMapReduce import (ImageMapReduceConfig, ImageMapReduceTask,
35 ImageMapper)
36from .utils import computeAveragePsf
37
38__all__ = ("DecorrelateALKernelTask", "DecorrelateALKernelConfig",
39 "DecorrelateALKernelMapper", "DecorrelateALKernelMapReduceConfig",
40 "DecorrelateALKernelSpatialConfig", "DecorrelateALKernelSpatialTask")
41
42
43class DecorrelateALKernelConfig(pexConfig.Config):
44 """Configuration parameters for the DecorrelateALKernelTask
45 """
46
47 ignoreMaskPlanes = pexConfig.ListField(
48 dtype=str,
49 doc="""Mask planes to ignore for sigma-clipped statistics""",
50 default=("INTRP", "EDGE", "DETECTED", "SAT", "CR", "BAD", "NO_DATA", "DETECTED_NEGATIVE")
51 )
52 completeVarPlanePropagation = pexConfig.Field(
53 dtype=bool,
54 default=False,
55 doc="Compute the full effect of the decorrelated matching kernel on the variance plane."
56 " Otherwise use a model weighed sum of the input variances."
57 )
58
59
60class DecorrelateALKernelTask(pipeBase.Task):
61 """Decorrelate the effect of convolution by Alard-Lupton matching kernel in image difference
62
63 """
64 ConfigClass = DecorrelateALKernelConfig
65 _DefaultName = "ip_diffim_decorrelateALKernel"
66
67 def __init__(self, *args, **kwargs):
68 """Create the image decorrelation Task
69
70 Parameters
71 ----------
72 args :
73 arguments to be passed to ``lsst.pipe.base.task.Task.__init__``
74 kwargs :
75 keyword arguments to be passed to ``lsst.pipe.base.task.Task.__init__``
76 """
77 pipeBase.Task.__init__(self, *args, **kwargs)
78
80 self.statsControl.setNumSigmaClip(3.)
81 self.statsControl.setNumIter(3)
82 self.statsControl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.ignoreMaskPlanes))
83
84 def computeVarianceMean(self, exposure):
85 statObj = afwMath.makeStatistics(exposure.variance,
86 exposure.mask,
87 afwMath.MEANCLIP, self.statsControl)
88 var = statObj.getValue(afwMath.MEANCLIP)
89 return var
90
91 @timeMethod
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.
96
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.
101
102 Parameters
103 ----------
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.
138 **kwargs
139 Additional keyword arguments propagated from DecorrelateALKernelSpatialTask.
140
141 Returns
142 -------
143 result : `lsst.pipe.base.Struct`
144 - ``correctedExposure`` : the decorrelated diffim
145
146 Notes
147 -----
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.
153
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.
157
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.
163
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.
170
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).
173
174 We are also using a constant accross-the-image measure of sigma (sqrt(variance)) to compute
175 the decorrelation kernel.
176
177 TODO DM-23857 As part of the spatially varying correction implementation
178 consider whether returning a Struct is still necessary.
179 """
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.")
183
184 spatialKernel = psfMatchingKernel
185 kimg = afwImage.ImageD(spatialKernel.getDimensions())
186 bbox = subtractedExposure.getBBox()
187 if xcen is None:
188 xcen = (bbox.getBeginX() + bbox.getEndX()) / 2.
189 if ycen is None:
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)
193
194 preConvImg = None
195 if preConvMode:
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)
201
202 if svar is None:
203 svar = self.computeVarianceMean(scienceExposure)
204 if tvar is None:
205 tvar = self.computeVarianceMean(templateExposure)
206 self.log.info("Original variance plane means. Science:%.5e, warped template:%.5e)",
207 svar, tvar)
208
209 # Should not happen unless entire image has been masked, which could happen
210 # if this is a small subimage of the main exposure. In this case, just return a full NaN
211 # exposure
212 if np.isnan(svar) or np.isnan(tvar):
213 # Double check that one of the exposures is all NaNs
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, )
219
220 if templateMatched:
221 # Regular subtraction, we convolved the template
222 self.log.info("Decorrelation after template image convolution")
223 varianceMean = svar
224 targetVarianceMean = tvar
225 # variance plane of the image that is not convolved
226 variance = scienceExposure.variance.array
227 # Variance plane of the convolved image, before convolution.
228 targetVariance = templateExposure.variance.array
229 # If the template is convolved, the PSF of the difference image is
230 # that of the science image
231 psfImg = scienceExposure.getPsf().computeKernelImage(geom.Point2D(xcen, ycen))
232 else:
233 # We convolved the science image
234 self.log.info("Decorrelation after science image convolution")
235 varianceMean = tvar
236 targetVarianceMean = svar
237 # variance plane of the image that is not convolved
238 variance = templateExposure.variance.array
239 # Variance plane of the convolved image, before convolution.
240 targetVariance = scienceExposure.variance.array
241 # If the science image is convolved, the PSF of the difference image
242 # is that of the template image, and will be a CoaddPsf which might
243 # not be defined for the entire image.
244 # Try the simple calculation first, and fall back on a slower method
245 # if the PSF of the template is not defined in the center.
246 try:
247 psfImg = templateExposure.getPsf().computeKernelImage(geom.Point2D(xcen, ycen))
248 except InvalidParameterError:
249 psfImg = computeAveragePsf(templateExposure, psfExposureBuffer=0.05, psfExposureGrid=100)
250
251 # The maximal correction value converges to sqrt(targetVarianceMean/varianceMean).
252 # Correction divergence warning if the correction exceeds 4 orders of magnitude.
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)
258
259 oldVarMean = self.computeVarianceMean(subtractedExposure)
260 self.log.info("Variance plane mean of uncorrected diffim: %f", oldVarMean)
261
262 kArr = kimg.array
263 diffimShape = subtractedExposure.image.array.shape
264 psfShape = psfImg.array.shape
265
266 if preConvMode:
267 self.log.info("Decorrelation of likelihood image")
268 self.computeCommonShape(preConvImg.array.shape, kArr.shape,
269 psfShape, diffimShape)
270 corr = self.computeScoreCorrection(kArr, varianceMean, targetVarianceMean, preConvImg.array)
271 else:
272 self.log.info("Decorrelation of difference image")
273 self.computeCommonShape(kArr.shape, psfShape, diffimShape)
274 corr = self.computeDiffimCorrection(kArr, varianceMean, targetVarianceMean)
275
276 correctedImage = self.computeCorrectedImage(corr.corrft, subtractedExposure.image.array)
277 # TODO DM-47461: only decorrelate the PSF if it is calculated for the
278 # difference image. If it is taken directly from the science (or template)
279 # image the decorrelation calculation is incorrect.
280 # correctedPsf = self.computeCorrectedDiffimPsf(corr.corrft, psfImg.array)
281
282 # The subtracted exposure variance plane is already correlated, we cannot propagate
283 # it through another convolution; instead we need to use the uncorrelated originals
284 # The whitening should scale it to varianceMean + targetVarianceMean on average
285 if self.config.completeVarPlanePropagation:
286 self.log.debug("Using full variance plane calculation in decorrelation")
287 correctedVariance = self.calculateVariancePlane(
288 variance, targetVariance,
289 varianceMean, targetVarianceMean, corr.cnft, corr.crft)
290 else:
291 self.log.debug("Using estimated variance plane calculation in decorrelation")
292 correctedVariance = self.estimateVariancePlane(
293 variance, targetVariance,
294 corr.cnft, corr.crft)
295
296 # Determine the common shape
297 kSum = np.sum(kArr)
298 kSumSq = kSum*kSum
299 self.log.debug("Matching kernel sum: %.3e", kSum)
300 if not templateMatched:
301 # ImagePsfMatch.subtractExposures re-scales the difference in
302 # the science image convolution mode
303 correctedVariance /= kSumSq
304 subtractedExposure.image.array[...] = correctedImage # Allow for numpy type casting
305 subtractedExposure.variance.array[...] = correctedVariance
306 # TODO DM-47461
307 # subtractedExposure.setPsf(correctedPsf)
308
309 newVarMean = self.computeVarianceMean(subtractedExposure)
310 self.log.info("Variance plane mean of corrected diffim: %.5e", newVarMean)
311
312 # TODO DM-23857 As part of the spatially varying correction implementation
313 # consider whether returning a Struct is still necessary.
314 return pipeBase.Struct(correctedExposure=subtractedExposure, )
315
316 def computeCommonShape(self, *shapes):
317 """Calculate the common shape for FFT operations. Set `self.freqSpaceShape`
318 internally.
319
320 Parameters
321 ----------
322 shapes : one or more `tuple` of `int`
323 Shapes of the arrays. All must have the same dimensionality.
324 At least one shape must be provided.
325
326 Returns
327 -------
328 None.
329
330 Notes
331 -----
332 For each dimension, gets the smallest even number greater than or equal to
333 `N1+N2-1` where `N1` and `N2` are the two largest values.
334 In case of only one shape given, rounds up to even each dimension value.
335 """
336 S = np.array(shapes, dtype=int)
337 if len(shapes) > 2:
338 S.sort(axis=0)
339 S = S[-2:]
340 if len(shapes) > 1:
341 commonShape = np.sum(S, axis=0) - 1
342 else:
343 commonShape = S[0]
344 commonShape[commonShape % 2 != 0] += 1
345 self.freqSpaceShape = tuple(commonShape)
346 self.log.info("Common frequency space shape %s", self.freqSpaceShape)
347
348 @staticmethod
349 def padCenterOriginArray(A, newShape: tuple, useInverse=False):
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.
353
354 Parameters
355 ----------
356 A : `numpy.ndarray`
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).
365
366 Returns
367 -------
368 R : `numpy.ndarray`
369 The padded or unpadded array with shape of `newShape` and the same dtype as A.
370
371 Notes
372 -----
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.
376 """
377
378 # The forward and inverse operations should round odd dimension halves at the opposite
379 # sides to get the pixels back to their original positions.
380 if not useInverse:
381 # Forward operation: First and second halves with respect to the axes of A.
382 firstHalves = [x//2 for x in A.shape]
383 secondHalves = [x-y for x, y in zip(A.shape, firstHalves)]
384 else:
385 # Inverse operation: Opposite rounding
386 secondHalves = [x//2 for x in newShape]
387 firstHalves = [x-y for x, y in zip(newShape, secondHalves)]
388
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]:]
394 return R
395
396 def computeDiffimCorrection(self, kappa, svar, tvar):
397 """Compute the Lupton decorrelation post-convolution kernel for decorrelating an
398 image difference, based on the PSF-matching kernel.
399
400 Parameters
401 ----------
402 kappa : `numpy.ndarray` of `float`
403 A matching kernel 2-d numpy.array derived from Alard & Lupton PSF matching.
404 svar : `float` > 0.
405 Average variance of science image used for PSF matching.
406 tvar : `float` > 0.
407 Average variance of the template (matched) image used for PSF matching.
408
409 Returns
410 -------
411 corrft : `numpy.ndarray` of `float`
412 The frequency space representation of the correction. The array is real (dtype float).
413 Shape is `self.freqSpaceShape`.
414
415 cnft, crft : `numpy.ndarray` of `complex`
416 The overall convolution (pre-conv, PSF matching, noise correction) kernel
417 for the science and template images, respectively for the variance plane
418 calculations. These are intermediate results in frequency space.
419
420 Notes
421 -----
422 The maximum correction factor converges to `sqrt(tvar/svar)` towards high frequencies.
423 This should be a plausible value.
424 """
425 kSum = np.sum(kappa) # We scale the decorrelation to preserve fluxes
426 kappa = self.padCenterOriginArray(kappa, self.freqSpaceShape)
427 kft = np.fft.fft2(kappa)
428 kftAbsSq = np.real(np.conj(kft) * kft)
429
430 denom = svar + tvar * kftAbsSq
431 corrft = np.sqrt((svar + tvar * kSum*kSum) / denom)
432 cnft = corrft
433 crft = kft*corrft
434 return pipeBase.Struct(corrft=corrft, cnft=cnft, crft=crft)
435
436 def computeScoreCorrection(self, kappa, svar, tvar, preConvArr):
437 """Compute the correction kernel for a score image.
438
439 Parameters
440 ----------
441 kappa : `numpy.ndarray`
442 A matching kernel 2-d numpy.array derived from Alard & Lupton PSF matching.
443 svar : `float`
444 Average variance of science image used for PSF matching (before pre-convolution).
445 tvar : `float`
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.
450
451 Returns
452 -------
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.
460
461 Notes
462 -----
463 To be precise, the science image should be _correlated_ by ``preConvArray`` but this
464 does not matter for this calculation.
465
466 ``cnft``, ``crft`` contain the scaling factor as well.
467
468 """
469 kSum = np.sum(kappa)
470 kappa = self.padCenterOriginArray(kappa, self.freqSpaceShape)
471 kft = np.fft.fft2(kappa)
472 preConvArr = self.padCenterOriginArray(preConvArr, self.freqSpaceShape)
473 preFt = np.fft.fft2(preConvArr)
474 preFtAbsSq = np.real(np.conj(preFt) * preFt)
475 kftAbsSq = np.real(np.conj(kft) * kft)
476 # Avoid zero division, though we don't normally approach `tiny`.
477 # We have numerical noise instead.
478 tiny = np.finfo(preFtAbsSq.dtype).tiny * 1000.
479 flt = preFtAbsSq < tiny
480 # If we pre-convolve to avoid deconvolution in AL, then kftAbsSq / preFtAbsSq
481 # theoretically expected to diverge to +inf. But we don't care about the convergence
482 # properties here, S goes to 0 at these frequencies anyway.
483 preFtAbsSq[flt] = tiny
484 denom = svar + tvar * kftAbsSq / preFtAbsSq
485 corrft = (svar + tvar * kSum*kSum) / denom
486 cnft = np.conj(preFt)*corrft
487 crft = kft*corrft
488 return pipeBase.Struct(corrft=corrft, cnft=cnft, crft=crft)
489
490 @staticmethod
491 def estimateVariancePlane(vplane1, vplane2, c1ft, c2ft):
492 """Estimate the variance planes.
493
494 The estimation assumes that around each pixel the surrounding
495 pixels' sigmas within the convolution kernel are the same.
496
497 Parameters
498 ----------
499 vplane1, vplane2 : `numpy.ndarray` of `float`
500 Variance planes of the original (before pre-convolution or matching)
501 exposures.
502 c1ft, c2ft : `numpy.ndarray` of `complex`
503 The overall convolution that includes the matching and the
504 afterburner in frequency space. The result of either
505 ``computeScoreCorrection`` or ``computeDiffimCorrection``.
506
507 Returns
508 -------
509 vplaneD : `numpy.ndarray` of `float`
510 The estimated variance plane of the difference/score image
511 as a weighted sum of the input variances.
512
513 Notes
514 -----
515 See DMTN-179 Section 5 about the variance plane calculations.
516 """
517 w1 = np.sum(np.real(np.conj(c1ft)*c1ft)) / c1ft.size
518 w2 = np.sum(np.real(np.conj(c2ft)*c2ft)) / c2ft.size
519 # w1, w2: the frequency space sum of abs(c1)^2 is the same as in image
520 # space.
521 return vplane1*w1 + vplane2*w2
522
523 def calculateVariancePlane(self, vplane1, vplane2, varMean1, varMean2, c1ft, c2ft):
524 """Full propagation of the variance planes of the original exposures.
525
526 The original variance planes of independent pixels are convolved with the
527 image space square of the overall kernels.
528
529 Parameters
530 ----------
531 vplane1, vplane2 : `numpy.ndarray` of `float`
532 Variance planes of the original (before pre-convolution or matching)
533 exposures.
534 varMean1, varMean2 : `float`
535 Replacement average values for non-finite ``vplane1`` and ``vplane2`` values respectively.
536
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``.
541
542 Returns
543 -------
544 vplaneD : `numpy.ndarray` of `float`
545 The variance plane of the difference/score images.
546
547 Notes
548 -----
549 See DMTN-179 Section 5 about the variance plane calculations.
550
551 Infs and NaNs are allowed and kept in the returned array.
552 """
553 D = np.real(np.fft.ifft2(c1ft))
554 c1SqFt = np.fft.fft2(D*D)
555
556 v1shape = vplane1.shape
557 filtInf = np.isinf(vplane1)
558 filtNan = np.isnan(vplane1)
559 # This copy could be eliminated if inf/nan handling were go into padCenterOriginArray
560 vplane1 = np.copy(vplane1)
561 vplane1[filtInf | filtNan] = varMean1
562 D = self.padCenterOriginArray(vplane1, self.freqSpaceShape)
563 v1 = np.real(np.fft.ifft2(np.fft.fft2(D) * c1SqFt))
564 v1 = self.padCenterOriginArray(v1, v1shape, useInverse=True)
565 v1[filtNan] = np.nan
566 v1[filtInf] = np.inf
567
568 D = np.real(np.fft.ifft2(c2ft))
569 c2SqFt = np.fft.fft2(D*D)
570
571 v2shape = vplane2.shape
572 filtInf = np.isinf(vplane2)
573 filtNan = np.isnan(vplane2)
574 vplane2 = np.copy(vplane2)
575 vplane2[filtInf | filtNan] = varMean2
576 D = self.padCenterOriginArray(vplane2, self.freqSpaceShape)
577 v2 = np.real(np.fft.ifft2(np.fft.fft2(D) * c2SqFt))
578 v2 = self.padCenterOriginArray(v2, v2shape, useInverse=True)
579 v2[filtNan] = np.nan
580 v2[filtInf] = np.inf
581
582 return v1 + v2
583
584 def computeCorrectedDiffimPsf(self, corrft, psfOld):
585 """Compute the (decorrelated) difference image's new PSF.
586
587 Parameters
588 ----------
589 corrft : `numpy.ndarray`
590 The frequency space representation of the correction calculated by
591 `computeCorrection`. Shape must be `self.freqSpaceShape`.
592 psfOld : `numpy.ndarray`
593 The psf of the difference image to be corrected.
594
595 Returns
596 -------
597 correctedPsf : `lsst.meas.algorithms.KernelPsf`
598 The corrected psf, same shape as `psfOld`, sum normed to 1.
599
600 Notes
601 -----
602 There is no algorithmic guarantee that the corrected psf can
603 meaningfully fit to the same size as the original one.
604 """
605 psfShape = psfOld.shape
606 psfNew = self.padCenterOriginArray(psfOld, self.freqSpaceShape)
607 psfNew = np.fft.fft2(psfNew)
608 psfNew *= corrft
609 psfNew = np.fft.ifft2(psfNew)
610 psfNew = psfNew.real
611 psfNew = self.padCenterOriginArray(psfNew, psfShape, useInverse=True)
612 psfNew = psfNew/psfNew.sum()
613
614 psfcI = afwImage.ImageD(geom.Extent2I(psfShape[1], psfShape[0]))
615 psfcI.array = psfNew
616 psfcK = afwMath.FixedKernel(psfcI)
617 correctedPsf = measAlg.KernelPsf(psfcK)
618 return correctedPsf
619
620 def computeCorrectedImage(self, corrft, imgOld):
621 """Compute the decorrelated difference image.
622
623 Parameters
624 ----------
625 corrft : `numpy.ndarray`
626 The frequency space representation of the correction calculated by
627 `computeCorrection`. Shape must be `self.freqSpaceShape`.
628 imgOld : `numpy.ndarray`
629 The difference image to be corrected.
630
631 Returns
632 -------
633 imgNew : `numpy.ndarray`
634 The corrected image, same size as the input.
635 """
636 expShape = imgOld.shape
637 imgNew = np.copy(imgOld)
638 filtInf = np.isinf(imgNew)
639 filtNan = np.isnan(imgNew)
640 imgNew[filtInf] = np.nan
641 imgNew[filtInf | filtNan] = np.nanmean(imgNew)
642 imgNew = self.padCenterOriginArray(imgNew, self.freqSpaceShape)
643 imgNew = np.fft.fft2(imgNew)
644 imgNew *= corrft
645 imgNew = np.fft.ifft2(imgNew)
646 imgNew = imgNew.real
647 imgNew = self.padCenterOriginArray(imgNew, expShape, useInverse=True)
648 imgNew[filtNan] = np.nan
649 imgNew[filtInf] = np.inf
650 return imgNew
651
652
654 """Task to be used as an ImageMapper for performing
655 A&L decorrelation on subimages on a grid across a A&L difference image.
656
657 This task subclasses DecorrelateALKernelTask in order to implement
658 all of that task's configuration parameters, as well as its `run` method.
659 """
660
661 ConfigClass = DecorrelateALKernelConfig
662 _DefaultName = 'ip_diffim_decorrelateALKernelMapper'
663
664 def __init__(self, *args, **kwargs):
665 DecorrelateALKernelTask.__init__(self, *args, **kwargs)
666
667 def run(self, subExposure, expandedSubExposure, fullBBox,
668 template, science, alTaskResult=None, psfMatchingKernel=None,
669 preConvKernel=None, **kwargs):
670 """Perform decorrelation operation on `subExposure`, using
671 `expandedSubExposure` to allow for invalid edge pixels arising from
672 convolutions.
673
674 This method performs A&L decorrelation on `subExposure` using
675 local measures for image variances and PSF. `subExposure` is a
676 sub-exposure of the non-decorrelated A&L diffim. It also
677 requires the corresponding sub-exposures of the template
678 (`template`) and science (`science`) exposures.
679
680 Parameters
681 ----------
682 subExposure : `lsst.afw.image.Exposure`
683 the sub-exposure of the diffim
684 expandedSubExposure : `lsst.afw.image.Exposure`
685 the expanded sub-exposure upon which to operate
686 fullBBox : `lsst.geom.Box2I`
687 the bounding box of the original exposure
688 template : `lsst.afw.image.Exposure`
689 the corresponding sub-exposure of the template exposure
690 science : `lsst.afw.image.Exposure`
691 the corresponding sub-exposure of the science exposure
692 alTaskResult : `lsst.pipe.base.Struct`
693 the result of A&L image differencing on `science` and
694 `template`, importantly containing the resulting
695 `psfMatchingKernel`. Can be `None`, only if
696 `psfMatchingKernel` is not `None`.
697 psfMatchingKernel : Alternative parameter for passing the
698 A&L `psfMatchingKernel` directly.
699 preConvKernel : If not None, then pre-filtering was applied
700 to science exposure, and this is the pre-convolution
701 kernel.
702 kwargs :
703 additional keyword arguments propagated from
704 `ImageMapReduceTask.run`.
705
706 Returns
707 -------
708 A `pipeBase.Struct` containing:
709
710 - ``subExposure`` : the result of the `subExposure` processing.
711 - ``decorrelationKernel`` : the decorrelation kernel, currently
712 not used.
713
714 Notes
715 -----
716 This `run` method accepts parameters identical to those of
717 `ImageMapper.run`, since it is called from the
718 `ImageMapperTask`. See that class for more information.
719 """
720 templateExposure = template # input template
721 scienceExposure = science # input science image
722 if alTaskResult is None and psfMatchingKernel is None:
723 raise RuntimeError('Both alTaskResult and psfMatchingKernel cannot be None')
724 psfMatchingKernel = alTaskResult.psfMatchingKernel if alTaskResult is not None else psfMatchingKernel
725
726 # subExp and expandedSubExp are subimages of the (un-decorrelated) diffim!
727 # So here we compute corresponding subimages of templateExposure and scienceExposure
728 subExp2 = scienceExposure.Factory(scienceExposure, expandedSubExposure.getBBox())
729 subExp1 = templateExposure.Factory(templateExposure, expandedSubExposure.getBBox())
730
731 # Prevent too much log INFO verbosity from DecorrelateALKernelTask.run
732 logLevel = self.log.level
733 self.log.setLevel(self.log.WARNING)
734 res = DecorrelateALKernelTask.run(self, subExp2, subExp1, expandedSubExposure,
735 psfMatchingKernel, preConvKernel, **kwargs)
736 self.log.setLevel(logLevel) # reset the log level
737
738 diffim = res.correctedExposure.Factory(res.correctedExposure, subExposure.getBBox())
739 out = pipeBase.Struct(subExposure=diffim, )
740 return out
741
742
744 """Configuration parameters for the ImageMapReduceTask to direct it to use
745 DecorrelateALKernelMapper as its mapper for A&L decorrelation.
746 """
747 mapper = pexConfig.ConfigurableField(
748 doc='A&L decorrelation task to run on each sub-image',
749 target=DecorrelateALKernelMapper
750 )
751
752
753class DecorrelateALKernelSpatialConfig(pexConfig.Config):
754 """Configuration parameters for the DecorrelateALKernelSpatialTask.
755 """
756 decorrelateConfig = pexConfig.ConfigField(
757 dtype=DecorrelateALKernelConfig,
758 doc='DecorrelateALKernel config to use when running on complete exposure (non spatially-varying)',
759 )
760
761 decorrelateMapReduceConfig = pexConfig.ConfigField(
762 dtype=DecorrelateALKernelMapReduceConfig,
763 doc='DecorrelateALKernelMapReduce config to use when running on each sub-image (spatially-varying)',
764 )
765
766 ignoreMaskPlanes = pexConfig.ListField(
767 dtype=str,
768 doc="""Mask planes to ignore for sigma-clipped statistics""",
769 default=("INTRP", "EDGE", "DETECTED", "SAT", "CR", "BAD", "NO_DATA", "DETECTED_NEGATIVE")
770 )
771
772 def setDefaults(self):
773 self.decorrelateMapReduceConfig.gridStepX = self.decorrelateMapReduceConfig.gridStepY = 40
774 self.decorrelateMapReduceConfig.cellSizeX = self.decorrelateMapReduceConfig.cellSizeY = 41
775 self.decorrelateMapReduceConfig.borderSizeX = self.decorrelateMapReduceConfig.borderSizeY = 8
776 self.decorrelateMapReduceConfig.reducer.reduceOperation = 'average'
777
778
780 """Decorrelate the effect of convolution by Alard-Lupton matching kernel in image difference
781
782 """
783 ConfigClass = DecorrelateALKernelSpatialConfig
784 _DefaultName = "ip_diffim_decorrelateALKernelSpatial"
785
786 def __init__(self, *args, **kwargs):
787 """Create the image decorrelation Task
788
789 Parameters
790 ----------
791 args :
792 arguments to be passed to
793 `lsst.pipe.base.task.Task.__init__`
794 kwargs :
795 additional keyword arguments to be passed to
796 `lsst.pipe.base.task.Task.__init__`
797 """
798 pipeBase.Task.__init__(self, *args, **kwargs)
799
801 self.statsControl.setNumSigmaClip(3.)
802 self.statsControl.setNumIter(3)
803 self.statsControl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.ignoreMaskPlanes))
804
805 def computeVarianceMean(self, exposure):
806 """Compute the mean of the variance plane of `exposure`.
807 """
808 statObj = afwMath.makeStatistics(exposure.variance,
809 exposure.mask,
810 afwMath.MEANCLIP, self.statsControl)
811 var = statObj.getValue(afwMath.MEANCLIP)
812 return var
813
814 def run(self, scienceExposure, templateExposure, subtractedExposure, psfMatchingKernel,
815 spatiallyVarying=True, preConvKernel=None, templateMatched=True, preConvMode=False):
816 """Perform decorrelation of an image difference exposure.
817
818 Decorrelates the diffim due to the convolution of the
819 templateExposure with the A&L psfMatchingKernel. If
820 `spatiallyVarying` is True, it utilizes the spatially varying
821 matching kernel via the `imageMapReduce` framework to perform
822 spatially-varying decorrelation on a grid of subExposures.
823
824 Parameters
825 ----------
826 scienceExposure : `lsst.afw.image.Exposure`
827 the science Exposure used for PSF matching
828 templateExposure : `lsst.afw.image.Exposure`
829 the template Exposure used for PSF matching
830 subtractedExposure : `lsst.afw.image.Exposure`
831 the subtracted Exposure produced by `ip_diffim.ImagePsfMatchTask.subtractExposures()`
832 psfMatchingKernel : an (optionally spatially-varying) PSF matching kernel produced
833 by `ip_diffim.ImagePsfMatchTask.subtractExposures()`
834 spatiallyVarying : `bool`
835 if True, perform the spatially-varying operation
836 preConvKernel : `lsst.meas.algorithms.Psf`
837 if not none, the scienceExposure has been pre-filtered with this kernel. (Currently
838 this option is experimental.)
839 templateMatched : `bool`, optional
840 If True, the template exposure was matched (convolved) to the science exposure.
841 preConvMode : `bool`, optional
842 If True, ``subtractedExposure`` is assumed to be a likelihood difference image
843 and will be noise corrected as a likelihood image.
844
845 Returns
846 -------
847 results : `lsst.pipe.base.Struct`
848 a structure containing:
849 - ``correctedExposure`` : the decorrelated diffim
850 """
851 self.log.info('Running A&L decorrelation: spatiallyVarying=%r', spatiallyVarying)
852
853 svar = self.computeVarianceMean(scienceExposure)
854 tvar = self.computeVarianceMean(templateExposure)
855 if np.isnan(svar) or np.isnan(tvar): # Should not happen unless entire image has been masked.
856 # Double check that one of the exposures is all NaNs
857 if (np.all(np.isnan(scienceExposure.image.array))
858 or np.all(np.isnan(templateExposure.image.array))):
859 self.log.warning('Template or science image is entirely NaNs: skipping decorrelation.')
860 if np.isnan(svar):
861 svar = 1e-9
862 if np.isnan(tvar):
863 tvar = 1e-9
864
865 var = self.computeVarianceMean(subtractedExposure)
866
867 if spatiallyVarying:
868 self.log.info("Variance (science, template): (%f, %f)", svar, tvar)
869 self.log.info("Variance (uncorrected diffim): %f", var)
870 config = self.config.decorrelateMapReduceConfig
871 task = ImageMapReduceTask(config=config)
872 results = task.run(subtractedExposure, science=scienceExposure,
873 template=templateExposure, psfMatchingKernel=psfMatchingKernel,
874 preConvKernel=preConvKernel, forceEvenSized=True,
875 templateMatched=templateMatched, preConvMode=preConvMode)
876 results.correctedExposure = results.exposure
877
878 # Make sure masks of input image are propagated to diffim
879 def gm(exp):
880 return exp.mask
881 gm(results.correctedExposure)[:, :] = gm(subtractedExposure)
882
883 var = self.computeVarianceMean(results.correctedExposure)
884 self.log.info("Variance (corrected diffim): %f", var)
885
886 else:
887 config = self.config.decorrelateConfig
888 task = DecorrelateALKernelTask(config=config)
889 results = task.run(scienceExposure, templateExposure,
890 subtractedExposure, psfMatchingKernel, preConvKernel=preConvKernel,
891 templateMatched=templateMatched, preConvMode=preConvMode)
892
893 return results
A kernel created from an Image.
Definition Kernel.h:471
Pass parameters to a Statistics object.
Definition Statistics.h:83
run(self, subExposure, expandedSubExposure, fullBBox, template, science, alTaskResult=None, psfMatchingKernel=None, preConvKernel=None, **kwargs)
run(self, scienceExposure, templateExposure, subtractedExposure, psfMatchingKernel, spatiallyVarying=True, preConvKernel=None, templateMatched=True, preConvMode=False)
run(self, scienceExposure, templateExposure, subtractedExposure, psfMatchingKernel, preConvKernel=None, xcen=None, ycen=None, svar=None, tvar=None, templateMatched=True, preConvMode=False, **kwargs)
calculateVariancePlane(self, vplane1, vplane2, varMean1, varMean2, c1ft, c2ft)
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Definition Statistics.h:361