25 import lsst.pex.config 
as pexConfig
 
   32     usePolynomial = pexConfig.Field(
 
   34         doc=
"Fit background difference with Chebychev polynomial interpolation " 
   35         "(using afw.math.Approximate)? If False, fit with spline interpolation using afw.math.Background",
 
   38     order = pexConfig.Field(
 
   40         doc=
"Order of Chebyshev polynomial background model. Ignored if usePolynomial False",
 
   43     badMaskPlanes = pexConfig.ListField(
 
   44         doc=
"Names of mask planes to ignore while estimating the background",
 
   45         dtype=str, default=[
"NO_DATA", 
"DETECTED", 
"DETECTED_NEGATIVE", 
"SAT", 
"BAD", 
"INTRP", 
"CR"],
 
   48     gridStatistic = pexConfig.ChoiceField(
 
   50         doc=
"Type of statistic to estimate pixel value for the grid points",
 
   55             "MEANCLIP": 
"clipped mean" 
   58     undersampleStyle = pexConfig.ChoiceField(
 
   59         doc=
"Behaviour if there are too few points in grid for requested interpolation style. " 
   60         "Note: INCREASE_NXNYSAMPLE only allowed for usePolynomial=True.",
 
   62         default=
"REDUCE_INTERP_ORDER",
 
   64             "THROW_EXCEPTION": 
"throw an exception if there are too few points",
 
   65             "REDUCE_INTERP_ORDER": 
"use an interpolation style with a lower order.",
 
   66             "INCREASE_NXNYSAMPLE": 
"Increase the number of samples used to make the interpolation grid.",
 
   69     binSize = pexConfig.Field(
 
   70         doc=
"Bin size for gridding the difference image and fitting a spatial model",
 
   74     interpStyle = pexConfig.ChoiceField(
 
   76         doc=
"Algorithm to interpolate the background values; ignored if usePolynomial is True" 
   77         "Maps to an enum; see afw.math.Background",
 
   78         default=
"AKIMA_SPLINE",
 
   80             "CONSTANT": 
"Use a single constant value",
 
   81             "LINEAR": 
"Use linear interpolation",
 
   82             "NATURAL_SPLINE": 
"cubic spline with zero second derivative at endpoints",
 
   83             "AKIMA_SPLINE": 
"higher-level nonlinear spline that is more robust to outliers",
 
   84             "NONE": 
"No background estimation is to be attempted",
 
   87     numSigmaClip = pexConfig.Field(
 
   89         doc=
"Sigma for outlier rejection; ignored if gridStatistic != 'MEANCLIP'.",
 
   92     numIter = pexConfig.Field(
 
   94         doc=
"Number of iterations of outlier rejection; ignored if gridStatistic != 'MEANCLIP'.",
 
   97     bestRefWeightCoverage = pexConfig.RangeField(
 
   99         doc=
"Weight given to coverage (number of pixels that overlap with patch), " 
  100         "when calculating best reference exposure. Higher weight prefers exposures with high coverage." 
  101         "Ignored when reference visit is supplied",
 
  105     bestRefWeightVariance = pexConfig.RangeField(
 
  107         doc=
"Weight given to image variance when calculating best reference exposure. " 
  108         "Higher weight prefers exposures with low image variance. Ignored when reference visit is supplied",
 
  112     bestRefWeightLevel = pexConfig.RangeField(
 
  114         doc=
"Weight given to mean background level when calculating best reference exposure. " 
  115         "Higher weight prefers exposures with low mean background level. " 
  116         "Ignored when reference visit is supplied.",
 
  120     approxWeighting = pexConfig.Field(
 
  122         doc=(
"Use inverse-variance weighting when approximating background offset model? " 
  123              "This will fail when the background offset is constant " 
  124              "(this is usually only the case in testing with artificial images)." 
  125              "(usePolynomial=True)"),
 
  128     gridStdevEpsilon = pexConfig.RangeField(
 
  130         doc=
"Tolerance on almost zero standard deviation in a background-offset grid bin. " 
  131         "If all bins have a standard deviation below this value, the background offset model " 
  132         "is approximated without inverse-variance weighting. (usePolynomial=True)",
 
  139     ConfigClass = MatchBackgroundsConfig
 
  140     _DefaultName = 
"matchBackgrounds" 
  143         pipeBase.Task.__init__(self, *args, **kwargs)
 
  146         self.
sctrl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.badMaskPlanes))
 
  147         self.
sctrl.setNanSafe(
True)
 
  150     def run(self, expRefList, expDatasetType, imageScalerList=None, refExpDataRef=None, refImageScaler=None):
 
  151         """Match the backgrounds of a list of coadd temp exposures to a reference coadd temp exposure. 
  153         Choose a refExpDataRef automatically if none supplied. 
  155         @param[in] expRefList: list of data references to science exposures to be background-matched; 
  156             all exposures must exist. 
  157         @param[in] expDatasetType: dataset type of exposures, e.g. 'goodSeeingCoadd_tempExp' 
  158         @param[in] imageScalerList: list of image scalers (coaddUtils.ImageScaler); 
  159             if None then the images are not scaled 
  160         @param[in] refExpDataRef: data reference for the reference exposure. 
  161             If None, then this task selects the best exposures from expRefList. 
  162             if not None then must be one of the exposures in expRefList. 
  163         @param[in] refImageScaler: image scaler for reference image; 
  164             ignored if refExpDataRef is None, else scaling is not performed if None 
  166         @return: a pipBase.Struct containing these fields: 
  167         - backgroundInfoList: a list of pipeBase.Struct, one per exposure in expRefList, 
  168             each of which contains these fields: 
  169             - isReference: this is the reference exposure (only one returned Struct will 
  170                 contain True for this value, unless the ref exposure is listed multiple times) 
  171             - backgroundModel: differential background model (afw.Math.Background or afw.Math.Approximate). 
  172                 Add this to the science exposure to match the reference exposure. 
  173             - fitRMS: rms of the fit. This is the sqrt(mean(residuals**2)). 
  174             - matchedMSE: the MSE of the reference and matched images: mean((refImage - matchedSciImage)**2); 
  175               should be comparable to difference image's mean variance. 
  176             - diffImVar: the mean variance of the difference image. 
  177             All fields except isReference will be None if isReference True or the fit failed. 
  179         @warning: all exposures must exist on disk 
  182         numExp = len(expRefList)
 
  184             raise pipeBase.TaskError(
"No exposures to match")
 
  186         if expDatasetType 
is None:
 
  187             raise pipeBase.TaskError(
"Must specify expDatasetType")
 
  189         if imageScalerList 
is None:
 
  190             self.log.
info(
"imageScalerList is None; no scaling will be performed")
 
  191             imageScalerList = [
None] * numExp
 
  193         if len(expRefList) != len(imageScalerList):
 
  194             raise RuntimeError(
"len(expRefList) = %s != %s = len(imageScalerList)" %
 
  195                                (len(expRefList), len(imageScalerList)))
 
  198         if refExpDataRef 
is None:
 
  201                 expRefList=expRefList,
 
  202                 imageScalerList=imageScalerList,
 
  203                 expDatasetType=expDatasetType,
 
  205             refExpDataRef = expRefList[refInd]
 
  206             refImageScaler = imageScalerList[refInd]
 
  211         expKeyList = refExpDataRef.butlerSubset.butler.getKeys(expDatasetType)
 
  212         refMatcher = 
DataRefMatcher(refExpDataRef.butlerSubset.butler, expDatasetType)
 
  213         refIndSet = 
set(refMatcher.matchList(ref0=refExpDataRef, refList=expRefList))
 
  215         if refInd 
is not None and refInd 
not in refIndSet:
 
  216             raise RuntimeError(
"Internal error: selected reference %s not found in expRefList")
 
  218         refExposure = refExpDataRef.get(expDatasetType, immediate=
True)
 
  219         if refImageScaler 
is not None:
 
  220             refMI = refExposure.getMaskedImage()
 
  221             refImageScaler.scaleMaskedImage(refMI)
 
  223         debugIdKeyList = tuple(
set(expKeyList) - 
set([
'tract', 
'patch']))
 
  225         self.log.
info(
"Matching %d Exposures" % (numExp))
 
  227         backgroundInfoList = []
 
  228         for ind, (toMatchRef, imageScaler) 
in enumerate(zip(expRefList, imageScalerList)):
 
  230                 backgroundInfoStruct = pipeBase.Struct(
 
  232                     backgroundModel=
None,
 
  238                 self.log.
info(
"Matching background of %s to %s" % (toMatchRef.dataId, refExpDataRef.dataId))
 
  240                     toMatchExposure = toMatchRef.get(expDatasetType, immediate=
True)
 
  241                     if imageScaler 
is not None:
 
  242                         toMatchMI = toMatchExposure.getMaskedImage()
 
  243                         imageScaler.scaleMaskedImage(toMatchMI)
 
  247                         refExposure=refExposure,
 
  248                         sciExposure=toMatchExposure,
 
  250                     backgroundInfoStruct.isReference = 
False 
  251                 except Exception 
as e:
 
  252                     self.log.
warn(
"Failed to fit background %s: %s" % (toMatchRef.dataId, e))
 
  253                     backgroundInfoStruct = pipeBase.Struct(
 
  255                         backgroundModel=
None,
 
  261             backgroundInfoList.append(backgroundInfoStruct)
 
  263         return pipeBase.Struct(
 
  264             backgroundInfoList=backgroundInfoList)
 
  268         """Find best exposure to use as the reference exposure 
  270         Calculate an appropriate reference exposure by minimizing a cost function that penalizes 
  271         high variance,  high background level, and low coverage. Use the following config parameters: 
  272         - bestRefWeightCoverage 
  273         - bestRefWeightVariance 
  276         @param[in] expRefList: list of data references to exposures. 
  277             Retrieves dataset type specified by expDatasetType. 
  278             If an exposure is not found, it is skipped with a warning. 
  279         @param[in] imageScalerList: list of image scalers (coaddUtils.ImageScaler); 
  280             must be the same length as expRefList 
  281         @param[in] expDatasetType: dataset type of exposure: e.g. 'goodSeeingCoadd_tempExp' 
  283         @return: index of best exposure 
  285         @raise pipeBase.TaskError if none of the exposures in expRefList are found. 
  287         self.log.
info(
"Calculating best reference visit")
 
  289         meanBkgdLevelList = []
 
  292         if len(expRefList) != len(imageScalerList):
 
  293             raise RuntimeError(
"len(expRefList) = %s != %s = len(imageScalerList)" %
 
  294                                (len(expRefList), len(imageScalerList)))
 
  296         for expRef, imageScaler 
in zip(expRefList, imageScalerList):
 
  297             exposure = expRef.get(expDatasetType, immediate=
True)
 
  298             maskedImage = exposure.getMaskedImage()
 
  299             if imageScaler 
is not None:
 
  301                     imageScaler.scaleMaskedImage(maskedImage)
 
  304                     varList.append(numpy.nan)
 
  305                     meanBkgdLevelList.append(numpy.nan)
 
  306                     coverageList.append(numpy.nan)
 
  309                                                afwMath.MEAN | afwMath.NPOINT | afwMath.VARIANCE, self.
sctrl)
 
  310             meanVar, meanVarErr = statObjIm.getResult(afwMath.VARIANCE)
 
  311             meanBkgdLevel, meanBkgdLevelErr = statObjIm.getResult(afwMath.MEAN)
 
  312             npoints, npointsErr = statObjIm.getResult(afwMath.NPOINT)
 
  313             varList.append(meanVar)
 
  314             meanBkgdLevelList.append(meanBkgdLevel)
 
  315             coverageList.append(npoints)
 
  317             raise pipeBase.TaskError(
 
  318                 "None of the candidate %s exist; cannot select best reference exposure" % (expDatasetType,))
 
  321         varArr = numpy.array(varList)/numpy.nanmax(varList)
 
  322         meanBkgdLevelArr = numpy.array(meanBkgdLevelList)/numpy.nanmax(meanBkgdLevelList)
 
  323         coverageArr = numpy.nanmin(coverageList)/numpy.array(coverageList)
 
  325         costFunctionArr = self.config.bestRefWeightVariance * varArr
 
  326         costFunctionArr += self.config.bestRefWeightLevel * meanBkgdLevelArr
 
  327         costFunctionArr += self.config.bestRefWeightCoverage * coverageArr
 
  328         return numpy.nanargmin(costFunctionArr)
 
  333         Match science exposure's background level to that of reference exposure. 
  335         Process creates a difference image of the reference exposure minus the science exposure, and then 
  336         generates an afw.math.Background object. It assumes (but does not require/check) that the mask plane 
  337         already has detections set. If detections have not been set/masked, sources will bias the 
  338         background estimation. 
  339         The 'background' of the difference image is smoothed by spline interpolation (by the Background class) 
  340         or by polynomial interpolation by the Approximate class. This model of difference image 
  341         is added to the science exposure in memory. 
  342         Fit diagnostics are also calculated and returned. 
  344         @param[in] refExposure: reference exposure 
  345         @param[in,out] sciExposure: science exposure; modified by changing the background level 
  346             to match that of the reference exposure 
  347         @returns a pipBase.Struct with fields: 
  348             - backgroundModel: an afw.math.Approximate or an afw.math.Background. 
  349             - fitRMS: rms of the fit. This is the sqrt(mean(residuals**2)). 
  350             - matchedMSE: the MSE of the reference and matched images: mean((refImage - matchedSciImage)**2); 
  351               should be comparable to difference image's mean variance. 
  352             - diffImVar: the mean variance of the difference image. 
  356             refExposure.writeFits(
lsstDebug.Info(__name__).figpath + 
'refExposure.fits')
 
  357             sciExposure.writeFits(
lsstDebug.Info(__name__).figpath + 
'sciExposure.fits')
 
  360         if self.config.usePolynomial:
 
  361             x, y = sciExposure.getDimensions()
 
  362             shortSideLength = 
min(x, y)
 
  363             if shortSideLength < self.config.binSize:
 
  364                 raise ValueError(
"%d = config.binSize > shorter dimension = %d" % (self.config.binSize,
 
  366             npoints = shortSideLength // self.config.binSize
 
  367             if shortSideLength % self.config.binSize != 0:
 
  370             if self.config.order > npoints - 1:
 
  371                 raise ValueError(
"%d = config.order > npoints - 1 = %d" % (self.config.order, npoints - 1))
 
  374         if (sciExposure.getDimensions() != refExposure.getDimensions()):
 
  375             wSci, hSci = sciExposure.getDimensions()
 
  376             wRef, hRef = refExposure.getDimensions()
 
  378                 "Exposures are different dimensions. sci:(%i, %i) vs. ref:(%i, %i)" %
 
  379                 (wSci, hSci, wRef, hRef))
 
  381         statsFlag = getattr(afwMath, self.config.gridStatistic)
 
  382         self.
sctrl.setNumSigmaClip(self.config.numSigmaClip)
 
  383         self.
sctrl.setNumIter(self.config.numIter)
 
  385         im = refExposure.getMaskedImage()
 
  386         diffMI = im.Factory(im, 
True)
 
  387         diffMI -= sciExposure.getMaskedImage()
 
  389         width = diffMI.getWidth()
 
  390         height = diffMI.getHeight()
 
  391         nx = width // self.config.binSize
 
  392         if width % self.config.binSize != 0:
 
  394         ny = height // self.config.binSize
 
  395         if height % self.config.binSize != 0:
 
  399         bctrl.setUndersampleStyle(self.config.undersampleStyle)
 
  406         if self.config.usePolynomial:
 
  407             order = self.config.order
 
  408             bgX, bgY, bgZ, bgdZ = self.
_gridImage(diffMI, self.config.binSize, statsFlag)
 
  409             minNumberGridPoints = 
min(len(
set(bgX)), len(
set(bgY)))
 
  411                 raise ValueError(
"No overlap with reference. Nothing to match")
 
  412             elif minNumberGridPoints <= self.config.order:
 
  414                 if self.config.undersampleStyle == 
"THROW_EXCEPTION":
 
  415                     raise ValueError(
"Image does not cover enough of ref image for order and binsize")
 
  416                 elif self.config.undersampleStyle == 
"REDUCE_INTERP_ORDER":
 
  417                     self.log.
warn(
"Reducing order to %d"%(minNumberGridPoints - 1))
 
  418                     order = minNumberGridPoints - 1
 
  419                 elif self.config.undersampleStyle == 
"INCREASE_NXNYSAMPLE":
 
  420                     newBinSize = (minNumberGridPoints*self.config.binSize) // (self.config.order + 1)
 
  421                     bctrl.setNxSample(newBinSize)
 
  422                     bctrl.setNySample(newBinSize)
 
  424                     self.log.
warn(
"Decreasing binsize to %d"%(newBinSize))
 
  427             isUniformImageDiff = 
not numpy.any(bgdZ > self.config.gridStdevEpsilon)
 
  428             weightByInverseVariance = 
False if isUniformImageDiff 
else self.config.approxWeighting
 
  432             if self.config.usePolynomial:
 
  434                                                    order, order, weightByInverseVariance)
 
  435                 undersampleStyle = getattr(afwMath, self.config.undersampleStyle)
 
  436                 approx = bkgd.getApproximate(actrl, undersampleStyle)
 
  437                 bkgdImage = approx.getImage()
 
  439                 bkgdImage = bkgd.getImageF(self.config.interpStyle, self.config.undersampleStyle)
 
  440         except Exception 
as e:
 
  441             raise RuntimeError(
"Background/Approximation failed to interp image %s: %s" % (
 
  444         sciMI = sciExposure.getMaskedImage()
 
  450         X, Y, Z, dZ = self.
_gridImage(diffMI, self.config.binSize, statsFlag)
 
  451         x0, y0 = diffMI.getXY0()
 
  452         modelValueArr = numpy.empty(len(Z))
 
  453         for i 
in range(len(X)):
 
  454             modelValueArr[i] = bkgdImage[int(X[i]-x0), int(Y[i]-y0), afwImage.LOCAL]
 
  455         resids = Z - modelValueArr
 
  456         rms = numpy.sqrt(numpy.mean(resids[~numpy.isnan(resids)]**2))
 
  459             sciExposure.writeFits(
lsstDebug.Info(__name__).figpath + 
'sciMatchedExposure.fits')
 
  462             bbox = 
geom.Box2D(refExposure.getMaskedImage().getBBox())
 
  464                 self.
_debugPlot(X, Y, Z, dZ, bkgdImage, bbox, modelValueArr, resids)
 
  465             except Exception 
as e:
 
  466                 self.log.
warn(
'Debug plot not generated: %s'%(e))
 
  469                                          afwMath.MEANCLIP, self.
sctrl).getValue()
 
  471         diffIm = diffMI.getImage()
 
  476         outBkgd = approx 
if self.config.usePolynomial 
else bkgd
 
  478         return pipeBase.Struct(
 
  479             backgroundModel=outBkgd,
 
  484     def _debugPlot(self, X, Y, Z, dZ, modelImage, bbox, model, resids):
 
  485         """Generate a plot showing the background fit and residuals. 
  487         It is called when lsstDebug.Info(__name__).savefig = True 
  488         Saves the fig to lsstDebug.Info(__name__).figpath 
  489         Displays on screen if lsstDebug.Info(__name__).display = True 
  491         @param X: array of x positions 
  492         @param Y: array of y positions 
  493         @param Z: array of the grid values that were interpolated 
  494         @param dZ: array of the error on the grid values 
  495         @param modelImage: image ofthe model of the fit 
  496         @param model: array of len(Z) containing the grid values predicted by the model 
  497         @param resids: Z - model 
  499         import matplotlib.pyplot 
as plt
 
  500         import matplotlib.colors
 
  501         from mpl_toolkits.axes_grid1 
import ImageGrid
 
  502         zeroIm = afwImage.MaskedImageF(
geom.Box2I(bbox))
 
  504         x0, y0 = zeroIm.getXY0()
 
  505         dx, dy = zeroIm.getDimensions()
 
  507             self.log.
warn(
"No grid. Skipping plot generation.")
 
  509             max, min = numpy.max(Z), numpy.min(Z)
 
  510             norm = matplotlib.colors.normalize(vmax=max, vmin=min)
 
  511             maxdiff = numpy.max(numpy.abs(resids))
 
  512             diffnorm = matplotlib.colors.normalize(vmax=maxdiff, vmin=-maxdiff)
 
  513             rms = numpy.sqrt(numpy.mean(resids**2))
 
  514             fig = plt.figure(1, (8, 6))
 
  515             meanDz = numpy.mean(dZ)
 
  516             grid = ImageGrid(fig, 111, nrows_ncols=(1, 2), axes_pad=0.1,
 
  517                              share_all=
True, label_mode=
"L", cbar_mode=
"each",
 
  518                              cbar_size=
"7%", cbar_pad=
"2%", cbar_location=
"top")
 
  519             im = grid[0].imshow(zeroIm.getImage().getArray(),
 
  520                                 extent=(x0, x0+dx, y0+dy, y0), norm=norm,
 
  522             im = grid[0].scatter(X, Y, c=Z, s=15.*meanDz/dZ, edgecolor=
'none', norm=norm,
 
  523                                  marker=
'o', cmap=
'Spectral')
 
  524             im2 = grid[1].scatter(X, Y, c=resids, edgecolor=
'none', norm=diffnorm,
 
  525                                   marker=
's', cmap=
'seismic')
 
  526             grid.cbar_axes[0].colorbar(im)
 
  527             grid.cbar_axes[1].colorbar(im2)
 
  528             grid[0].axis([x0, x0+dx, y0+dy, y0])
 
  529             grid[1].axis([x0, x0+dx, y0+dy, y0])
 
  530             grid[0].set_xlabel(
"model and grid")
 
  531             grid[1].set_xlabel(
"residuals. rms = %0.3f"%(rms))
 
  538     def _gridImage(self, maskedImage, binsize, statsFlag):
 
  539         """Private method to grid an image for debugging""" 
  540         width, height = maskedImage.getDimensions()
 
  541         x0, y0 = maskedImage.getXY0()
 
  542         xedges = numpy.arange(0, width, binsize)
 
  543         yedges = numpy.arange(0, height, binsize)
 
  544         xedges = numpy.hstack((xedges, width))  
 
  545         yedges = numpy.hstack((yedges, height))  
 
  554         for ymin, ymax 
in zip(yedges[0:-1], yedges[1:]):
 
  555             for xmin, xmax 
in zip(xedges[0:-1], xedges[1:]):
 
  558                 subIm = afwImage.MaskedImageF(maskedImage, subBBox, afwImage.PARENT, 
False)
 
  560                                                afwMath.MEAN | afwMath.MEANCLIP | afwMath.MEDIAN
 
  561                                                | afwMath.NPOINT | afwMath.STDEV,
 
  563                 npoints, _ = stats.getResult(afwMath.NPOINT)
 
  565                     stdev, _ = stats.getResult(afwMath.STDEV)
 
  566                     if stdev < self.config.gridStdevEpsilon:
 
  567                         stdev = self.config.gridStdevEpsilon
 
  568                     bgX.append(0.5 * (x0 + xmin + x0 + xmax))
 
  569                     bgY.append(0.5 * (y0 + ymin + y0 + ymax))
 
  570                     bgdZ.append(stdev/numpy.sqrt(npoints))
 
  571                     est, _ = stats.getResult(statsFlag)
 
  574         return numpy.array(bgX), numpy.array(bgY), numpy.array(bgZ), numpy.array(bgdZ)
 
  578     """Match data references for a specified dataset type 
  580     Note that this is not exact, but should suffice for this task 
  581     until there is better support for this kind of thing in the butler. 
  585         """Construct a DataRefMatcher 
  588         @param[in] datasetType: dataset type to match 
  591         self.
_keyNames = butler.getKeys(datasetType)
 
  593     def _makeKey(self, ref):
 
  594         """Return a tuple of values for the specified keyNames 
  596         @param[in] ref: data reference 
  598         @raise KeyError if ref.dataId is missing a key in keyNames 
  600         return tuple(ref.dataId[key] 
for key 
in self.
_keyNames)
 
  603         """Return True if ref0 == ref1 
  605         @param[in] ref0: data ref 0 
  606         @param[in] ref1: data ref 1 
  608         @raise KeyError if either ID is missing a key in keyNames 
  613         """Return a list of indices of matches 
  615         @return tuple of indices of matches 
  617         @raise KeyError if any ID is missing a key in keyNames 
  620         return tuple(ind 
for ind, ref 
in enumerate(refList) 
if self.
_makeKey(ref) == key0)