30 import lsst.pex.config 
as pexConfig
 
   33 from .imageMapReduce 
import (ImageMapReduceConfig, ImageMapper,
 
   35 from .imagePsfMatch 
import (ImagePsfMatchTask, ImagePsfMatchConfig,
 
   36                             subtractAlgorithmRegistry)
 
   38 __all__ = [
"ZogyTask", 
"ZogyConfig",
 
   39            "ZogyMapper", 
"ZogyMapReduceConfig",
 
   40            "ZogyImagePsfMatchConfig", 
"ZogyImagePsfMatchTask"]
 
   43 """Tasks for performing the "Proper image subtraction" algorithm of 
   44 Zackay, et al. (2016), hereafter simply referred to as 'ZOGY (2016)'. 
   46 `ZogyTask` contains methods to perform the basic estimation of the 
   47 ZOGY diffim `D`, its updated PSF, and the variance-normalized 
   48 likelihood image `S_corr`. We have implemented ZOGY using the 
   49 proscribed methodology, computing all convolutions in Fourier space, 
   50 and also variants in which the convolutions are performed in real 
   51 (image) space. The former is faster and results in fewer artifacts 
   52 when the PSFs are noisy (i.e., measured, for example, via 
   53 `PsfEx`). The latter is presumed to be preferred as it can account for 
   54 masks correctly with fewer "ringing" artifacts from edge effects or 
   55 saturated stars, but noisy PSFs result in their own smaller 
   56 artifacts. Removal of these artifacts is a subject of continuing 
   57 research. Currently, we "pad" the PSFs when performing the 
   58 subtractions in real space, which reduces, but does not entirely 
   59 eliminate these artifacts. 
   61 All methods in `ZogyTask` assume template and science images are 
   62 already accurately photometrically and astrometrically registered. 
   64 `ZogyMapper` is a wrapper which runs `ZogyTask` in the 
   65 `ImageMapReduce` framework, computing of ZOGY diffim's on small, 
   66 overlapping sub-images, thereby enabling complete ZOGY diffim's which 
   67 account for spatially-varying noise and PSFs across the two input 
   68 exposures. An example of the use of this task is in the `testZogy.py` 
   74     """Configuration parameters for the ZogyTask 
   76     inImageSpace = pexConfig.Field(
 
   79         doc=
"Perform all convolutions in real (image) space rather than Fourier space. " 
   80         "Currently if True, this results in artifacts when using real (noisy) PSFs." 
   83     padSize = pexConfig.Field(
 
   86         doc=
"Number of pixels to pad PSFs to avoid artifacts (when inImageSpace is True)" 
   89     templateFluxScaling = pexConfig.Field(
 
   92         doc=
"Template flux scaling factor (Fr in ZOGY paper)" 
   95     scienceFluxScaling = pexConfig.Field(
 
   98         doc=
"Science flux scaling factor (Fn in ZOGY paper)" 
  101     scaleByCalibration = pexConfig.Field(
 
  104         doc=
"Compute the flux normalization scaling based on the image calibration." 
  105         "This overrides 'templateFluxScaling' and 'scienceFluxScaling'." 
  108     doTrimKernels = pexConfig.Field(
 
  111         doc=
"Trim kernels for image-space ZOGY. Speeds up convolutions and shrinks artifacts. " 
  112         "Subject of future research." 
  115     doFilterPsfs = pexConfig.Field(
 
  118         doc=
"Filter PSFs for image-space ZOGY. Aids in reducing artifacts. " 
  119         "Subject of future research." 
  122     ignoreMaskPlanes = pexConfig.ListField(
 
  124         default=(
"INTRP", 
"EDGE", 
"DETECTED", 
"SAT", 
"CR", 
"BAD", 
"NO_DATA", 
"DETECTED_NEGATIVE"),
 
  125         doc=
"Mask planes to ignore for statistics" 
  133     """Task to perform ZOGY proper image subtraction. See module-level documentation for 
  136     In all methods, im1 is R (reference, or template) and im2 is N (new, or science). 
  138     ConfigClass = ZogyConfig
 
  139     _DefaultName = 
"ip_diffim_Zogy" 
  141     def __init__(self, templateExposure=None, scienceExposure=None, sig1=None, sig2=None,
 
  142                  psf1=None, psf2=None, *args, **kwargs):
 
  143         """Create the ZOGY task. 
  147         templateExposure : `lsst.afw.image.Exposure` 
  148             Template exposure ("Reference image" in ZOGY (2016)). 
  149         scienceExposure : `lsst.afw.image.Exposure` 
  150             Science exposure ("New image" in ZOGY (2016)). Must have already been 
  151             registered and photmetrically matched to template. 
  153             (Optional) sqrt(variance) of `templateExposure`. If `None`, it is 
  154             computed from the sqrt(mean) of the `templateExposure` variance image. 
  156             (Optional) sqrt(variance) of `scienceExposure`. If `None`, it is 
  157             computed from the sqrt(mean) of the `scienceExposure` variance image. 
  158         psf1 : 2D `numpy.array` 
  159             (Optional) 2D array containing the PSF image for the template. If 
  160             `None`, it is extracted from the PSF taken at the center of `templateExposure`. 
  161         psf2 : 2D `numpy.array` 
  162             (Optional) 2D array containing the PSF image for the science img. If 
  163             `None`, it is extracted from the PSF taken at the center of `scienceExposure`. 
  165             additional arguments to be passed to 
  166             `lsst.pipe.base.Task` 
  168             additional keyword arguments to be passed to 
  169             `lsst.pipe.base.Task` 
  171         pipeBase.Task.__init__(self, *args, **kwargs)
 
  173         self.
setup(templateExposure=templateExposure, scienceExposure=scienceExposure,
 
  174                    sig1=sig1, sig2=sig2, psf1=psf1, psf2=psf2, *args, **kwargs)
 
  176     def setup(self, templateExposure=None, scienceExposure=None, sig1=None, sig2=None,
 
  177               psf1=None, psf2=None, correctBackground=False, *args, **kwargs):
 
  178         """Set up the ZOGY task. 
  182         templateExposure : `lsst.afw.image.Exposure` 
  183             Template exposure ("Reference image" in ZOGY (2016)). 
  184         scienceExposure : `lsst.afw.image.Exposure` 
  185             Science exposure ("New image" in ZOGY (2016)). Must have already been 
  186             registered and photometrically matched to template. 
  188             (Optional) sqrt(variance) of `templateExposure`. If `None`, it is 
  189             computed from the sqrt(mean) of the `templateExposure` variance image. 
  191             (Optional) sqrt(variance) of `scienceExposure`. If `None`, it is 
  192             computed from the sqrt(mean) of the `scienceExposure` variance image. 
  193         psf1 : 2D `numpy.array` 
  194             (Optional) 2D array containing the PSF image for the template. If 
  195             `None`, it is extracted from the PSF taken at the center of `templateExposure`. 
  196         psf2 : 2D `numpy.array` 
  197             (Optional) 2D array containing the PSF image for the science img. If 
  198             `None`, it is extracted from the PSF taken at the center of `scienceExposure`. 
  199         correctBackground : `bool` 
  200             (Optional) subtract sigma-clipped mean of exposures. Zogy doesn't correct 
  201             nonzero backgrounds (unlike AL) so subtract them here. 
  203             additional arguments to be passed to 
  204             `lsst.pipe.base.Task` 
  206             additional keyword arguments to be passed to 
  207             `lsst.pipe.base.Task` 
  209         if self.
template is None and templateExposure 
is None:
 
  211         if self.
science is None and scienceExposure 
is None:
 
  220         self.
statsControl.setAndMask(afwImage.Mask.getPlaneBitMask(
 
  221             self.config.ignoreMaskPlanes))
 
  224         self.
im2 = self.
science.getMaskedImage().getImage().getArray()
 
  226         self.
im2_var = self.
science.getMaskedImage().getVariance().getArray()
 
  228         def selectPsf(psf, exposure):
 
  233                 xcen = (bbox1.getBeginX() + bbox1.getEndX()) / 2.
 
  234                 ycen = (bbox1.getBeginY() + bbox1.getEndY()) / 2.
 
  235                 return exposure.getPsf().computeKernelImage(
geom.Point2D(xcen, ycen)).getArray()
 
  245         if (pShape1[0] < pShape2[0]):
 
  246             psf1 = np.pad(psf1, ((0, pShape2[0] - pShape1[0]), (0, 0)), mode=
'constant', constant_values=0.)
 
  247         elif (pShape2[0] < pShape1[0]):
 
  248             psf2 = np.pad(psf2, ((0, pShape1[0] - pShape2[0]), (0, 0)), mode=
'constant', constant_values=0.)
 
  249         if (pShape1[1] < pShape2[1]):
 
  250             psf1 = np.pad(psf1, ((0, 0), (0, pShape2[1] - pShape1[1])), mode=
'constant', constant_values=0.)
 
  251         elif (pShape2[1] < pShape1[1]):
 
  252             psf2 = np.pad(psf2, ((0, 0), (0, pShape1[1] - pShape2[1])), mode=
'constant', constant_values=0.)
 
  255         maxLoc1 = np.unravel_index(np.argmax(psf1), psf1.shape)
 
  256         maxLoc2 = np.unravel_index(np.argmax(psf2), psf2.shape)
 
  258         while (maxLoc1[0] != maxLoc2[0]) 
or (maxLoc1[1] != maxLoc2[1]):
 
  259             if maxLoc1[0] > maxLoc2[0]:
 
  260                 psf2[1:, :] = psf2[:-1, :]
 
  261             elif maxLoc1[0] < maxLoc2[0]:
 
  262                 psf1[1:, :] = psf1[:-1, :]
 
  263             if maxLoc1[1] > maxLoc2[1]:
 
  264                 psf2[:, 1:] = psf2[:, :-1]
 
  265             elif maxLoc1[1] < maxLoc2[1]:
 
  266                 psf1[:, 1:] = psf1[:, :-1]
 
  267             maxLoc1 = np.unravel_index(np.argmax(psf1), psf1.shape)
 
  268             maxLoc2 = np.unravel_index(np.argmax(psf2), psf2.shape)
 
  271         psf1[psf1 < MIN_KERNEL] = MIN_KERNEL
 
  272         psf2[psf2 < MIN_KERNEL] = MIN_KERNEL
 
  281         if np.isnan(self.
sig1) 
or self.
sig1 == 0:
 
  283         if np.isnan(self.
sig2) 
or self.
sig2 == 0:
 
  287         if correctBackground:
 
  288             def _subtractImageMean(exposure):
 
  289                 """Compute the sigma-clipped mean of the image of `exposure`.""" 
  290                 mi = exposure.getMaskedImage()
 
  293                 mean = statObj.getValue(afwMath.MEANCLIP)
 
  294                 if not np.isnan(mean):
 
  298             _subtractImageMean(self.
science)
 
  301         self.
Fr = self.config.templateFluxScaling  
 
  302         self.
Fn = self.config.scienceFluxScaling  
 
  304         if self.config.scaleByCalibration:
 
  305             calib_template = self.
template.getPhotoCalib()
 
  306             calib_science = self.
science.getPhotoCalib()
 
  307             if calib_template 
is None:
 
  308                 self.log.
warning(
"No calibration information available for template image.")
 
  309             if calib_science 
is None:
 
  310                 self.log.
warning(
"No calibration information available for science image.")
 
  311             if calib_template 
is None or calib_science 
is None:
 
  312                 self.log.
warning(
"Due to lack of calibration information, " 
  313                                  "reverting to templateFluxScaling and scienceFluxScaling.")
 
  315                 self.
Fr = 1/calib_template.getCalibrationMean()
 
  316                 self.
Fn = 1/calib_science.getCalibrationMean()
 
  318         self.log.
info(
"Setting template image scaling to Fr=%f" % self.
Fr)
 
  319         self.log.
info(
"Setting science  image scaling to Fn=%f" % self.
Fn)
 
  321         self.
padSize = self.config.padSize  
 
  323     def _computeVarianceMean(self, exposure):
 
  324         """Compute the sigma-clipped mean of the variance image of `exposure`. 
  327                                          exposure.getMaskedImage().getMask(),
 
  329         var = statObj.getValue(afwMath.MEANCLIP)
 
  333     def _padPsfToSize(psf, size):
 
  334         """Zero-pad `psf` to the dimensions given by `size`. 
  338         psf : 2D `numpy.array` 
  339             Input psf to be padded 
  341             Two element list containing the dimensions to pad the `psf` to 
  345         psf : 2D `numpy.array` 
  346             The padded copy of the input `psf`. 
  348         newArr = np.zeros(size)
 
  350         offset = [size[0]//2 - psf.shape[0]//2, size[1]//2 - psf.shape[1]//2]
 
  351         tmp = newArr[offset[0]:(psf.shape[0] + offset[0]), offset[1]:(psf.shape[1] + offset[1])]
 
  356         """Compute standard ZOGY quantities used by (nearly) all methods. 
  358         Many of the ZOGY calculations require similar quantities, including 
  359         FFTs of the PSFs, and the "denominator" term (e.g. in eq. 13 of 
  360         ZOGY manuscript (2016). This function consolidates many of those 
  365         psf1 : 2D `numpy.array` 
  366             (Optional) Input psf of template, override if already padded 
  367         psf2 : 2D `numpy.array` 
  368             (Optional) Input psf of science image, override if already padded 
  369         padSize : `int`, optional 
  370             Number of pixels to pad the image on each side with zeroes. 
  374         A `lsst.pipe.base.Struct` containing: 
  375         - Pr : 2D `numpy.array`, the (possibly zero-padded) template PSF 
  376         - Pn : 2D `numpy.array`, the (possibly zero-padded) science PSF 
  377         - Pr_hat : 2D `numpy.array`, the FFT of `Pr` 
  378         - Pn_hat : 2D `numpy.array`, the FFT of `Pn` 
  379         - denom : 2D `numpy.array`, the denominator of equation (13) in ZOGY (2016) manuscript 
  380         - Fd : `float`, the relative flux scaling factor between science and template 
  382         psf1 = self.
im1_psf if psf1 
is None else psf1
 
  383         psf2 = self.
im2_psf if psf2 
is None else psf2
 
  384         padSize = self.
padSize if padSize 
is None else padSize
 
  387             Pr = ZogyTask._padPsfToSize(psf1, (psf1.shape[0] + padSize, psf1.shape[1] + padSize))
 
  388             Pn = ZogyTask._padPsfToSize(psf2, (psf2.shape[0] + padSize, psf2.shape[1] + padSize))
 
  390         psf1[np.abs(psf1) <= MIN_KERNEL] = MIN_KERNEL
 
  391         psf2[np.abs(psf2) <= MIN_KERNEL] = MIN_KERNEL
 
  394         Pr_hat = np.fft.fft2(Pr)
 
  395         Pr_hat2 = np.conj(Pr_hat) * Pr_hat
 
  396         Pn_hat = np.fft.fft2(Pn)
 
  397         Pn_hat2 = np.conj(Pn_hat) * Pn_hat
 
  398         denom = np.sqrt((sigN**2 * self.
Fr**2 * Pr_hat2) + (sigR**2 * self.
Fn**2 * Pn_hat2))
 
  399         Fd = self.
Fr * self.
Fn / np.sqrt(sigN**2 * self.
Fr**2 + sigR**2 * self.
Fn**2)
 
  401         res = pipeBase.Struct(
 
  402             Pr=Pr, Pn=Pn, Pr_hat=Pr_hat, Pn_hat=Pn_hat, denom=denom, Fd=Fd
 
  407         r"""Compute ZOGY diffim `D` as proscribed in ZOGY (2016) manuscript 
  411         debug : `bool`, optional 
  412             If set to True, filter the kernels by setting the edges to zero. 
  413         returnMatchedTemplate : `bool`, optional 
  414             Calculate the template image. 
  415             If not set, the returned template will be None. 
  419         In all functions, im1 is R (reference, or template) and im2 is N (new, or science) 
  420         Compute the ZOGY eqn. (13): 
  424             \widehat{D} = \frac{Fr\widehat{Pr}\widehat{N} - 
  425             F_n\widehat{Pn}\widehat{R}}{\sqrt{\sigma_n^2 Fr^2 
  426             \|\widehat{Pr}\|^2 + \sigma_r^2 F_n^2 \|\widehat{Pn}\|^2}} 
  428         where :math:`D` is the optimal difference image, :math:`R` and :math:`N` are the 
  429         reference and "new" image, respectively, :math:`Pr` and :math:`P_n` are their 
  430         PSFs, :math:`Fr` and :math:`Fn` are their flux-based zero-points (which we 
  431         will set to one here), :math:`\sigma_r^2` and :math:`\sigma_n^2` are their 
  432         variance, and :math:`\widehat{D}` denotes the FT of :math:`D`. 
  436         result : `lsst.pipe.base.Struct` 
  437             Result struct with components: 
  439             - ``D`` : 2D `numpy.array`, the proper image difference 
  440             - ``D_var`` : 2D `numpy.array`, the variance image for `D` 
  443         psf1 = ZogyTask._padPsfToSize(self.
im1_psf, self.
im1.shape)
 
  444         psf2 = ZogyTask._padPsfToSize(self.
im2_psf, self.
im2.shape)
 
  448         def _filterKernel(K, trim_amount):
 
  451             K[:ps, :] = K[-ps:, :] = 0
 
  452             K[:, :ps] = K[:, -ps:] = 0
 
  455         Kr_hat = self.
Fr * preqs.Pr_hat / preqs.denom
 
  456         Kn_hat = self.
Fn * preqs.Pn_hat / preqs.denom
 
  457         if debug 
and self.config.doTrimKernels:  
 
  460             ps = (Kn_hat.shape[1] - 80)//2
 
  461             Kn = _filterKernel(np.fft.ifft2(Kn_hat), ps)
 
  462             Kn_hat = np.fft.fft2(Kn)
 
  463             Kr = _filterKernel(np.fft.ifft2(Kr_hat), ps)
 
  464             Kr_hat = np.fft.fft2(Kr)
 
  466         def processImages(im1, im2, doAdd=False):
 
  468             im1[np.isinf(im1)] = np.nan
 
  469             im1[np.isnan(im1)] = np.nanmean(im1)
 
  470             im2[np.isinf(im2)] = np.nan
 
  471             im2[np.isnan(im2)] = np.nanmean(im2)
 
  473             R_hat = np.fft.fft2(im1)
 
  474             N_hat = np.fft.fft2(im2)
 
  476             D_hat = Kr_hat * N_hat
 
  477             D_hat_R = Kn_hat * R_hat
 
  483             D = np.fft.ifft2(D_hat)
 
  484             D = np.fft.ifftshift(D.real) / preqs.Fd
 
  487             if returnMatchedTemplate:
 
  488                 R = np.fft.ifft2(D_hat_R)
 
  489                 R = np.fft.ifftshift(R.real) / preqs.Fd
 
  494         D, R = processImages(self.
im1, self.
im2, doAdd=
False)
 
  496         D_var, R_var = processImages(self.
im1_var, self.
im2_var, doAdd=
True)
 
  498         return pipeBase.Struct(D=D, D_var=D_var, R=R, R_var=R_var)
 
  500     def _doConvolve(self, exposure, kernel, recenterKernel=False):
 
  501         """Convolve an Exposure with a decorrelation convolution kernel. 
  505         exposure : `lsst.afw.image.Exposure` 
  506             Input exposure to be convolved. 
  507         kernel : `numpy.array` 
  508             2D `numpy.array` to convolve the image with 
  509         recenterKernel : `bool`, optional 
  510             Force the kernel center to the pixel with the maximum value. 
  514         A new `lsst.afw.image.Exposure` with the convolved pixels and the (possibly 
  519         - We optionally re-center the kernel if necessary and return the possibly 
  522         kernelImg = afwImage.ImageD(kernel.shape[1], kernel.shape[0])
 
  523         kernelImg.getArray()[:, :] = kernel
 
  526             maxloc = np.unravel_index(np.argmax(kernel), kernel.shape)
 
  528         outExp = exposure.clone()  
 
  530                                                maxInterpolationDistance=0)
 
  532             afwMath.convolve(outExp.getMaskedImage(), exposure.getMaskedImage(), kern, convCntrl)
 
  533         except AttributeError:
 
  541         """Compute ZOGY diffim `D` using image-space convlutions 
  543         This method is still being debugged as it results in artifacts 
  544         when the PSFs are noisy (see module-level docstring). Thus 
  545         there are several options still enabled by the `debug` flag, 
  546         which are disabled by defult. 
  551            The amount to pad the PSFs by 
  553            Flag to enable debugging tests and options 
  557         D : `lsst.afw.Exposure` 
  558            the proper image difference, including correct variance, 
  566         Kr_hat = (preqs.Pr_hat + delta) / (preqs.denom + delta)
 
  567         Kn_hat = (preqs.Pn_hat + delta) / (preqs.denom + delta)
 
  568         Kr = np.fft.ifft2(Kr_hat).real
 
  569         Kr = np.roll(np.roll(Kr, -1, 0), -1, 1)
 
  570         Kn = np.fft.ifft2(Kn_hat).real
 
  571         Kn = np.roll(np.roll(Kn, -1, 0), -1, 1)
 
  573         def _trimKernel(self, K, trim_amount):
 
  577             K = K[ps:-ps, ps:-ps]
 
  580         padSize = self.
padSize if padSize 
is None else padSize
 
  582         if debug 
and self.config.doTrimKernels:  
 
  585             Kn = _trimKernel(Kn, padSize)
 
  586             Kr = _trimKernel(Kr, padSize)
 
  592         tmp = D.getMaskedImage()
 
  593         tmp -= exp1.getMaskedImage()
 
  595         return pipeBase.Struct(D=D, R=exp1)
 
  597     def _setNewPsf(self, exposure, psfArr):
 
  598         """Utility method to set an exposure's PSF when provided as a 2-d numpy.array 
  600         bbox = exposure.getBBox()
 
  601         center = ((bbox.getBeginX() + bbox.getEndX()) // 2., (bbox.getBeginY() + bbox.getEndY()) // 2.)
 
  603         psfI = afwImage.ImageD(psfArr.shape[1], psfArr.shape[0])
 
  604         psfI.getArray()[:, :] = psfArr
 
  606         psfNew = measAlg.KernelPsf(psfK, center)
 
  607         exposure.setPsf(psfNew)
 
  611                       returnMatchedTemplate=False, **kwargs):
 
  612         """Wrapper method to compute ZOGY proper diffim 
  614         This method should be used as the public interface for 
  615         computing the ZOGY diffim. 
  619         inImageSpace : `bool` 
  620            Override config `inImageSpace` parameter 
  622            Override config `padSize` parameter 
  623         returnMatchedTemplate : `bool` 
  624            Include the PSF-matched template in the results Struct 
  626             additional keyword arguments to be passed to 
  627             `computeDiffimFourierSpace` or `computeDiffimImageSpace`. 
  631         An lsst.pipe.base.Struct containing: 
  632            - D : `lsst.afw.Exposure` 
  633                the proper image difference, including correct variance, 
  635            - R : `lsst.afw.Exposure` 
  636                If `returnMatchedTemplate` is True, the PSF-matched template 
  640         inImageSpace = self.config.inImageSpace 
if inImageSpace 
is None else inImageSpace
 
  642             padSize = self.
padSize if padSize 
is None else padSize
 
  645             if returnMatchedTemplate:
 
  650             D.getMaskedImage().getImage().getArray()[:, :] = res.D
 
  651             D.getMaskedImage().getVariance().getArray()[:, :] = res.D_var
 
  652             if returnMatchedTemplate:
 
  654                 R.getMaskedImage().getImage().getArray()[:, :] = res.R
 
  655                 R.getMaskedImage().getVariance().getArray()[:, :] = res.R_var
 
  659         return pipeBase.Struct(D=D, R=R)
 
  662         """Compute the ZOGY diffim PSF (ZOGY manuscript eq. 14) 
  667            Override config `padSize` parameter 
  669            Return the FFT of the diffim PSF (do not inverse-FFT it) 
  670         psf1 : 2D `numpy.array` 
  671             (Optional) Input psf of template, override if already padded 
  672         psf2 : 2D `numpy.array` 
  673             (Optional) Input psf of science image, override if already padded 
  677         Pd : 2D `numpy.array` 
  678             The diffim PSF (or FFT of PSF if `keepFourier=True`) 
  680         preqs = self.
computePrereqs(psf1=psf1, psf2=psf2, padSize=padSize)
 
  682         Pd_hat_numerator = (self.
Fr * self.
Fn * preqs.Pr_hat * preqs.Pn_hat)
 
  683         Pd_hat = Pd_hat_numerator / (preqs.Fd * preqs.denom)
 
  688         Pd = np.fft.ifft2(Pd_hat)
 
  689         Pd = np.fft.ifftshift(Pd).real
 
  693     def _computeVarAstGradients(self, xVarAst=0., yVarAst=0., inImageSpace=False,
 
  694                                 R_hat=None, Kr_hat=None, Kr=None,
 
  695                                 N_hat=None, Kn_hat=None, Kn=None):
 
  696         """Compute the astrometric noise correction terms 
  698         Compute the correction for estimated astrometric noise as 
  699         proscribed in ZOGY (2016), section 3.3. All convolutions 
  700         performed either in real (image) or Fourier space. 
  704         xVarAst, yVarAst : `float` 
  705            estimated astrometric noise (variance of astrometric registration errors) 
  706         inImageSpace : `bool` 
  707            Perform all convolutions in real (image) space rather than Fourier space 
  708         R_hat : 2-D `numpy.array` 
  709            (Optional) FFT of template image, only required if `inImageSpace=False` 
  710         Kr_hat : 2-D `numpy.array` 
  711            FFT of Kr kernel (eq. 28 of ZOGY (2016)), only required if `inImageSpace=False` 
  712         Kr : 2-D `numpy.array` 
  713            Kr kernel (eq. 28 of ZOGY (2016)), only required if `inImageSpace=True`. 
  714            Kr is associated with the template (reference). 
  715         N_hat : 2-D `numpy.array` 
  716            FFT of science image, only required if `inImageSpace=False` 
  717         Kn_hat : 2-D `numpy.array` 
  718            FFT of Kn kernel (eq. 29 of ZOGY (2016)), only required if `inImageSpace=False` 
  719         Kn : 2-D `numpy.array` 
  720            Kn kernel (eq. 29 of ZOGY (2016)), only required if `inImageSpace=True`. 
  721            Kn is associated with the science (new) image. 
  725         VastSR, VastSN : 2-D `numpy.array` 
  726            Arrays containing the values in eqs. 30 and 32 of ZOGY (2016). 
  729         if xVarAst + yVarAst > 0:  
 
  732                 S_R = S_R.getMaskedImage().getImage().getArray()
 
  734                 S_R = np.fft.ifft2(R_hat * Kr_hat)
 
  735             gradRx, gradRy = np.gradient(S_R)
 
  736             VastSR = xVarAst * gradRx**2. + yVarAst * gradRy**2.
 
  740                 S_N = S_N.getMaskedImage().getImage().getArray()
 
  742                 S_N = np.fft.ifft2(N_hat * Kn_hat)
 
  743             gradNx, gradNy = np.gradient(S_N)
 
  744             VastSN = xVarAst * gradNx**2. + yVarAst * gradNy**2.
 
  746         return VastSR, VastSN
 
  749         """Compute corrected likelihood image, optimal for source detection 
  751         Compute ZOGY S_corr image. This image can be thresholded for 
  752         detection without optimal filtering, and the variance image is 
  753         corrected to account for astrometric noise (errors in 
  754         astrometric registration whether systematic or due to effects 
  755         such as DCR). The calculations here are all performed in 
  756         Fourier space, as proscribed in ZOGY (2016). 
  760         xVarAst, yVarAst : `float` 
  761            estimated astrometric noise (variance of astrometric registration errors) 
  765         result : `lsst.pipe.base.Struct` 
  766             Result struct with components: 
  768             - ``S`` : `numpy.array`, the likelihood image S (eq. 12 of ZOGY (2016)) 
  769             - ``S_var`` : the corrected variance image (denominator of eq. 25 of ZOGY (2016)) 
  770             - ``Dpsf`` : the PSF of the diffim D, likely never to be used. 
  774             """Replace any NaNs or Infs with the mean of the image.""" 
  775             isbad = ~np.isfinite(im)
 
  778                 im[isbad] = np.nanmean(im)
 
  781         self.
im1 = fix_nans(self.
im1)
 
  782         self.
im2 = fix_nans(self.
im2)
 
  787         psf1 = ZogyTask._padPsfToSize(self.
im1_psf, self.
im1.shape)
 
  788         psf2 = ZogyTask._padPsfToSize(self.
im2_psf, self.
im2.shape)
 
  793         R_hat = np.fft.fft2(self.
im1)
 
  794         N_hat = np.fft.fft2(self.
im2)
 
  795         D_hat = self.
Fr * preqs.Pr_hat * N_hat - self.
Fn * preqs.Pn_hat * R_hat
 
  798         Pd_hat = self.
computeDiffimPsf(padSize=0, keepFourier=
True, psf1=psf1, psf2=psf2)
 
  799         Pd_bar = np.conj(Pd_hat)
 
  800         S = np.fft.ifft2(D_hat * Pd_bar)
 
  804         Pn_hat2 = np.conj(preqs.Pn_hat) * preqs.Pn_hat
 
  805         Kr_hat = self.
Fr * self.
Fn**2. * np.conj(preqs.Pr_hat) * Pn_hat2 / preqs.denom**2.
 
  806         Pr_hat2 = np.conj(preqs.Pr_hat) * preqs.Pr_hat
 
  807         Kn_hat = self.
Fn * self.
Fr**2. * np.conj(preqs.Pn_hat) * Pr_hat2 / preqs.denom**2.
 
  809         Kr_hat2 = np.fft.fft2(np.fft.ifft2(Kr_hat)**2.)
 
  810         Kn_hat2 = np.fft.fft2(np.fft.ifft2(Kn_hat)**2.)
 
  811         var1c_hat = Kr_hat2 * np.fft.fft2(self.
im1_var)
 
  812         var2c_hat = Kn_hat2 * np.fft.fft2(self.
im2_var)
 
  816                                                       R_hat=R_hat, Kr_hat=Kr_hat,
 
  817                                                       N_hat=N_hat, Kn_hat=Kn_hat)
 
  819         S_var = np.sqrt(np.fft.ifftshift(np.fft.ifft2(var1c_hat + var2c_hat)) + fGradR + fGradN)
 
  822         S = np.fft.ifftshift(np.fft.ifft2(Kn_hat * N_hat - Kr_hat * R_hat))
 
  826         return pipeBase.Struct(S=S.real, S_var=S_var.real, Dpsf=Pd)
 
  829         """Compute corrected likelihood image, optimal for source detection 
  831         Compute ZOGY S_corr image. This image can be thresholded for 
  832         detection without optimal filtering, and the variance image is 
  833         corrected to account for astrometric noise (errors in 
  834         astrometric registration whether systematic or due to effects 
  835         such as DCR). The calculations here are all performed in 
  840         xVarAst, yVarAst : `float` 
  841            estimated astrometric noise (variance of astrometric registration errors) 
  845         A `lsst.pipe.base.Struct` containing: 
  846         - S : `lsst.afw.image.Exposure`, the likelihood exposure S (eq. 12 of ZOGY (2016)), 
  847             including corrected variance, masks, and PSF 
  848         - D : `lsst.afw.image.Exposure`, the proper image difference, including correct 
  849             variance, masks, and PSF 
  854         padSize = self.
padSize if padSize 
is None else padSize
 
  858         Pd_bar = np.fliplr(np.flipud(Pd))
 
  860         tmp = S.getMaskedImage()
 
  865         Pn_hat2 = np.conj(preqs.Pn_hat) * preqs.Pn_hat
 
  866         Kr_hat = self.
Fr * self.
Fn**2. * np.conj(preqs.Pr_hat) * Pn_hat2 / preqs.denom**2.
 
  867         Pr_hat2 = np.conj(preqs.Pr_hat) * preqs.Pr_hat
 
  868         Kn_hat = self.
Fn * self.
Fr**2. * np.conj(preqs.Pn_hat) * Pr_hat2 / preqs.denom**2.
 
  870         Kr = np.fft.ifft2(Kr_hat).real
 
  871         Kr = np.roll(np.roll(Kr, -1, 0), -1, 1)
 
  872         Kn = np.fft.ifft2(Kn_hat).real
 
  873         Kn = np.roll(np.roll(Kn, -1, 0), -1, 1)
 
  881         Smi = S.getMaskedImage()
 
  883         S_var = np.sqrt(var1c.getArray() + var2c.getArray() + fGradR + fGradN)
 
  884         S.getMaskedImage().getVariance().getArray()[:, :] = S_var
 
  888         return pipeBase.Struct(S=S, D=D)
 
  890     def computeScorr(self, xVarAst=0., yVarAst=0., inImageSpace=None, padSize=0, **kwargs):
 
  891         """Wrapper method to compute ZOGY corrected likelihood image, optimal for 
  894         This method should be used as the public interface for 
  895         computing the ZOGY S_corr. 
  899         xVarAst, yVarAst : `float` 
  900            estimated astrometric noise (variance of astrometric registration errors) 
  901         inImageSpace : `bool` 
  902            Override config `inImageSpace` parameter 
  904            Override config `padSize` parameter 
  908         S : `lsst.afw.image.Exposure` 
  909             The likelihood exposure S (eq. 12 of ZOGY (2016)), 
  910             including corrected variance, masks, and PSF 
  912         inImageSpace = self.config.inImageSpace 
if inImageSpace 
is None else inImageSpace
 
  920             S.getMaskedImage().getImage().getArray()[:, :] = res.S
 
  921             S.getMaskedImage().getVariance().getArray()[:, :] = res.S_var
 
  924         return pipeBase.Struct(S=S)
 
  928     """Task to be used as an ImageMapper for performing 
  929     ZOGY image subtraction on a grid of subimages. 
  931     ConfigClass = ZogyConfig
 
  932     _DefaultName = 
'ip_diffim_ZogyMapper' 
  935         ImageMapper.__init__(self, *args, **kwargs)
 
  937     def run(self, subExposure, expandedSubExposure, fullBBox, template,
 
  939         """Perform ZOGY proper image subtraction on sub-images 
  941         This method performs ZOGY proper image subtraction on 
  942         `subExposure` using local measures for image variances and 
  943         PSF. `subExposure` is a sub-exposure of the science image. It 
  944         also requires the corresponding sub-exposures of the template 
  945         (`template`). The operations are actually performed on 
  946         `expandedSubExposure` to allow for invalid edge pixels arising 
  947         from convolutions, which are then removed. 
  951         subExposure : `lsst.afw.image.Exposure` 
  952             the sub-exposure of the diffim 
  953         expandedSubExposure : `lsst.afw.image.Exposure` 
  954             the expanded sub-exposure upon which to operate 
  955         fullBBox : `lsst.geom.Box2I` 
  956             the bounding box of the original exposure 
  957         template : `lsst.afw.image.Exposure` 
  958             the template exposure, from which a corresponding sub-exposure 
  961             additional keyword arguments propagated from 
  962             `ImageMapReduceTask.run`. These include: 
  965                 Compute and return the corrected likelihood image S_corr 
  966                 rather than the proper image difference 
  967             ``inImageSpace`` : `bool` 
  968                 Perform all convolutions in real (image) space rather than 
  969                 in Fourier space. This option currently leads to artifacts 
  970                 when using real (measured and noisy) PSFs, thus it is set 
  971                 to `False` by default. 
  972                 These kwargs may also include arguments to be propagated to 
  973                 `ZogyTask.computeDiffim` and `ZogyTask.computeScorr`. 
  977         result : `lsst.pipe.base.Struct` 
  978             Result struct with components: 
  980                 ``subExposure``: Either the subExposure of the proper image difference ``D``, 
  981                     or (if `doScorr==True`) the corrected likelihood exposure ``S``. 
  985         This `run` method accepts parameters identical to those of 
  986         `ImageMapper.run`, since it is called from the 
  987         `ImageMapperTask`. See that class for more information. 
  989         bbox = subExposure.getBBox()
 
  990         center = ((bbox.getBeginX() + bbox.getEndX()) // 2., (bbox.getBeginY() + bbox.getEndY()) // 2.)
 
  993         imageSpace = kwargs.pop(
'inImageSpace', 
False)
 
  994         doScorr = kwargs.pop(
'doScorr', 
False)
 
  995         sigmas = kwargs.pop(
'sigmas', 
None)
 
  996         padSize = kwargs.pop(
'padSize', 7)
 
  999         subExp2 = expandedSubExposure
 
 1002         subExp1 = template.Factory(template, expandedSubExposure.getBBox())
 
 1008             sig1, sig2 = sigmas[0], sigmas[1]
 
 1010         def _makePsfSquare(psf):
 
 1012             if psf.shape[0] < psf.shape[1]:
 
 1013                 psf = np.pad(psf, ((0, psf.shape[1] - psf.shape[0]), (0, 0)), mode=
'constant',
 
 1015             elif psf.shape[0] > psf.shape[1]:
 
 1016                 psf = np.pad(psf, ((0, 0), (0, psf.shape[0] - psf.shape[1])), mode=
'constant',
 
 1020         psf2 = subExp2.getPsf().computeKernelImage(center).getArray()
 
 1021         psf2 = _makePsfSquare(psf2)
 
 1023         psf1 = template.getPsf().computeKernelImage(center).getArray()
 
 1024         psf1 = _makePsfSquare(psf1)
 
 1027         if subExp1.getDimensions()[0] < psf1.shape[0] 
or subExp1.getDimensions()[1] < psf1.shape[1]:
 
 1028             return pipeBase.Struct(subExposure=subExposure)
 
 1030         def _filterPsf(psf):
 
 1031             """Filter a noisy Psf to remove artifacts. Subject of future research.""" 
 1033             if psf.shape[0] == 41:  
 
 1036                 psf[0:10, :] = psf[:, 0:10] = psf[31:41, :] = psf[:, 31:41] = 0
 
 1041         psf1b = psf2b = 
None 
 1042         if self.config.doFilterPsfs:  
 
 1044             psf1b = _filterPsf(psf1)
 
 1045             psf2b = _filterPsf(psf2)
 
 1048         if imageSpace 
is True:
 
 1049             config.inImageSpace = imageSpace
 
 1050             config.padSize = padSize  
 
 1051         task = 
ZogyTask(templateExposure=subExp1, scienceExposure=subExp2,
 
 1052                         sig1=sig1, sig2=sig2, psf1=psf1b, psf2=psf2b, config=config)
 
 1055             res = task.computeDiffim(**kwargs)
 
 1058             res = task.computeScorr(**kwargs)
 
 1061         outExp = D.Factory(D, subExposure.getBBox())
 
 1062         out = pipeBase.Struct(subExposure=outExp)
 
 1067     """Config to be passed to ImageMapReduceTask 
 1069     This config targets the imageMapper to use the ZogyMapper. 
 1071     mapper = pexConfig.ConfigurableField(
 
 1072         doc=
'Zogy task to run on each sub-image',
 
 1078     """Config for the ZogyImagePsfMatchTask""" 
 1080     zogyConfig = pexConfig.ConfigField(
 
 1082         doc=
'ZogyTask config to use when running on complete exposure (non spatially-varying)',
 
 1085     zogyMapReduceConfig = pexConfig.ConfigField(
 
 1086         dtype=ZogyMapReduceConfig,
 
 1087         doc=
'ZogyMapReduce config to use when running Zogy on each sub-image (spatially-varying)',
 
 1099     """Task to perform Zogy PSF matching and image subtraction. 
 1101     This class inherits from ImagePsfMatchTask to contain the _warper 
 1102     subtask and related methods. 
 1105     ConfigClass = ZogyImagePsfMatchConfig
 
 1108         ImagePsfMatchTask.__init__(self, *args, **kwargs)
 
 1110     def _computeImageMean(self, exposure):
 
 1111         """Compute the sigma-clipped mean of the pixels image of `exposure`. 
 1114         statsControl.setNumSigmaClip(3.)
 
 1115         statsControl.setNumIter(3)
 
 1116         ignoreMaskPlanes = (
"INTRP", 
"EDGE", 
"DETECTED", 
"SAT", 
"CR", 
"BAD", 
"NO_DATA", 
"DETECTED_NEGATIVE")
 
 1117         statsControl.setAndMask(afwImage.Mask.getPlaneBitMask(ignoreMaskPlanes))
 
 1119                                          exposure.getMaskedImage().getMask(),
 
 1120                                          afwMath.MEANCLIP | afwMath.MEDIAN, statsControl)
 
 1121         mn = statObj.getValue(afwMath.MEANCLIP)
 
 1122         med = statObj.getValue(afwMath.MEDIAN)
 
 1126                           doWarping=True, spatiallyVarying=True, inImageSpace=False,
 
 1127                           doPreConvolve=False):
 
 1128         """Register, PSF-match, and subtract two Exposures using the ZOGY algorithm. 
 1130         Do the following, in order: 
 1131         - Warp templateExposure to match scienceExposure, if their WCSs do not already match 
 1132         - Compute subtracted exposure ZOGY image subtraction algorithm on the two exposures 
 1136         templateExposure : `lsst.afw.image.Exposure` 
 1137             exposure to PSF-match to scienceExposure. The exposure's mean value is subtracted 
 1139         scienceExposure : `lsst.afw.image.Exposure` 
 1140             reference Exposure. The exposure's mean value is subtracted in-place. 
 1142             what to do if templateExposure's and scienceExposure's WCSs do not match: 
 1143             - if True then warp templateExposure to match scienceExposure 
 1144             - if False then raise an Exception 
 1145         spatiallyVarying : `bool` 
 1146             If True, perform the operation over a grid of patches across the two exposures 
 1147         inImageSpace : `bool` 
 1148             If True, perform the Zogy convolutions in image space rather than in frequency space. 
 1149         doPreConvolve : `bool` 
 1150             ***Currently not implemented.*** If True assume we are to compute the match filter-convolved 
 1151             exposure which can be thresholded for detection. In the case of Zogy this would mean 
 1152             we compute the Scorr image. 
 1156         A `lsst.pipe.base.Struct` containing these fields: 
 1157         - subtractedExposure: subtracted Exposure 
 1158         - warpedExposure: templateExposure after warping to match scienceExposure (if doWarping true) 
 1163         self.log.
info(
"Exposure means=%f, %f; median=%f, %f:" % (mn1[0], mn2[0], mn1[1], mn2[1]))
 
 1164         if not np.isnan(mn1[0]) 
and np.abs(mn1[0]) > 1:
 
 1165             mi = templateExposure.getMaskedImage()
 
 1167         if not np.isnan(mn2[0]) 
and np.abs(mn2[0]) > 1:
 
 1168             mi = scienceExposure.getMaskedImage()
 
 1171         self.log.
info(
'Running Zogy algorithm: spatiallyVarying=%r' % spatiallyVarying)
 
 1173         if not self.
_validateWcs(templateExposure, scienceExposure):
 
 1175                 self.log.
info(
"Astrometrically registering template to science image")
 
 1178                                                            scienceExposure.getWcs())
 
 1179                 psfWarped = measAlg.WarpedPsf(templateExposure.getPsf(), xyTransform)
 
 1182                                                              destBBox=scienceExposure.getBBox())
 
 1184                 templateExposure.setPsf(psfWarped)
 
 1186                 self.log.
error(
"ERROR: Input images not registered")
 
 1187                 raise RuntimeError(
"Input images not registered")
 
 1190             return exp.getMaskedImage().getMask()
 
 1193             return exp.getMaskedImage().getImage().getArray()
 
 1195         if self.config.zogyConfig.inImageSpace:
 
 1197         self.log.
info(
'Running Zogy algorithm: inImageSpace=%r' % inImageSpace)
 
 1198         if spatiallyVarying:
 
 1199             config = self.config.zogyMapReduceConfig
 
 1201             results = task.run(scienceExposure, template=templateExposure, inImageSpace=inImageSpace,
 
 1202                                doScorr=doPreConvolve, forceEvenSized=
False)
 
 1203             results.D = results.exposure
 
 1210             config = self.config.zogyConfig
 
 1211             task = 
ZogyTask(scienceExposure=scienceExposure, templateExposure=templateExposure,
 
 1213             if not doPreConvolve:
 
 1214                 results = task.computeDiffim(inImageSpace=inImageSpace)
 
 1215                 results.matchedExposure = results.R
 
 1217                 results = task.computeScorr(inImageSpace=inImageSpace)
 
 1218                 results.D = results.S
 
 1221         mask = results.D.getMaskedImage().getMask()
 
 1222         mask |= scienceExposure.getMaskedImage().getMask()
 
 1223         mask |= templateExposure.getMaskedImage().getMask()
 
 1224         results.D.getMaskedImage().getMask()[:, :] = mask
 
 1225         badBitsNan = mask.addMaskPlane(
'UNMASKEDNAN')
 
 1226         resultsArr = results.D.getMaskedImage().getMask().getArray()
 
 1227         resultsArr[np.isnan(resultsArr)] |= badBitsNan
 
 1228         resultsArr[np.isnan(scienceExposure.getMaskedImage().getImage().getArray())] |= badBitsNan
 
 1229         resultsArr[np.isnan(templateExposure.getMaskedImage().getImage().getArray())] |= badBitsNan
 
 1231         results.subtractedExposure = results.D
 
 1232         results.warpedExposure = templateExposure
 
 1236                              doWarping=True, spatiallyVarying=True, inImageSpace=False,
 
 1237                              doPreConvolve=False):
 
 1238         raise NotImplementedError
 
 1241 subtractAlgorithmRegistry.register(
'zogy', ZogyImagePsfMatchTask)