24 from . 
import diffimLib
 
   30 import lsst.pex.config 
as pexConfig
 
   32 from .makeKernelBasisList 
import makeKernelBasisList
 
   33 from .psfMatch 
import PsfMatchTask, PsfMatchConfigAL
 
   34 from . 
import utils 
as dituils
 
   36 __all__ = (
"ModelPsfMatchTask", 
"ModelPsfMatchConfig")
 
   38 sigma2fwhm = 2.*np.sqrt(2.*np.log(2.))
 
   42     nextInt = int(np.ceil(x))
 
   43     return nextInt + 1 
if nextInt%2 == 0 
else nextInt
 
   47     """Configuration for model-to-model Psf matching""" 
   49     kernel = pexConfig.ConfigChoiceField(
 
   56     doAutoPadPsf = pexConfig.Field(
 
   58         doc=(
"If too small, automatically pad the science Psf? " 
   59              "Pad to smallest dimensions appropriate for the matching kernel dimensions, " 
   60              "as specified by autoPadPsfTo. If false, pad by the padPsfBy config."),
 
   63     autoPadPsfTo = pexConfig.RangeField(
 
   65         doc=(
"Minimum Science Psf dimensions as a fraction of matching kernel dimensions. " 
   66              "If the dimensions of the Psf to be matched are less than the " 
   67              "matching kernel dimensions * autoPadPsfTo, pad Science Psf to this size. " 
   68              "Ignored if doAutoPadPsf=False."),
 
   73     padPsfBy = pexConfig.Field(
 
   75         doc=
"Pixels (even) to pad Science Psf by before matching. Ignored if doAutoPadPsf=True",
 
   81         self.
kernel.active.singleKernelClipping = 
False 
   82         self.
kernel.active.kernelSumClipping = 
False 
   83         self.
kernel.active.spatialKernelClipping = 
False 
   84         self.
kernel.active.checkConditionNumber = 
False 
   87         self.
kernel.active.constantVarianceWeighting = 
True 
   90         self.
kernel.active.scaleByFwhm = 
False 
   94     """Matching of two model Psfs, and application of the Psf-matching kernel to an input Exposure 
   99     This Task differs from ImagePsfMatchTask in that it matches two Psf _models_, by realizing 
  100     them in an Exposure-sized SpatialCellSet and then inserting each Psf-image pair into KernelCandidates. 
  101     Because none of the pairs of sources that are to be matched should be invalid, all sigma clipping is 
  102     turned off in ModelPsfMatchConfig.  And because there is no tracked _variance_ in the Psf images, the 
  103     debugging and logging QA info should be interpreted with caution. 
  105     One item of note is that the sizes of Psf models are fixed (e.g. its defined as a 21x21 matrix).  When the 
  106     Psf-matching kernel is being solved for, the Psf "image" is convolved with each kernel basis function, 
  107     leading to a loss of information around the borders. 
  108     This pixel loss will be problematic for the numerical 
  109     stability of the kernel solution if the size of the convolution kernel 
  110     (set by ModelPsfMatchConfig.kernelSize) is much bigger than: psfSize//2. 
  111     Thus the sizes of Psf-model matching kernels are typically smaller 
  112     than their image-matching counterparts.  If the size of the kernel is too small, the convolved stars will 
  113     look "boxy"; if the kernel is too large, the kernel solution will be "noisy".  This is a trade-off that 
  114     needs careful attention for a given dataset. 
  116     The primary use case for this Task is in matching an Exposure to a 
  117     constant-across-the-sky Psf model for the purposes of image coaddition. 
  118     It is important to note that in the code, the "template" Psf is the Psf 
  119     that the science image gets matched to.  In this sense the order of template and science image are 
  120     reversed, compared to ImagePsfMatchTask, which operates on the template image. 
  124     The `lsst.pipe.base.cmdLineTask.CmdLineTask` command line task interface supports a 
  125     flag -d/--debug to import debug.py from your PYTHONPATH.  The relevant contents of debug.py 
  126     for this Task include: 
  133             di = lsstDebug.getInfo(name) 
  134             if name == "lsst.ip.diffim.psfMatch": 
  135                 di.display = True                 # global 
  136                 di.maskTransparency = 80          # mask transparency 
  137                 di.displayCandidates = True       # show all the candidates and residuals 
  138                 di.displayKernelBasis = False     # show kernel basis functions 
  139                 di.displayKernelMosaic = True     # show kernel realized across the image 
  140                 di.plotKernelSpatialModel = False # show coefficients of spatial model 
  141                 di.showBadCandidates = True       # show the bad candidates (red) along with good (green) 
  142             elif name == "lsst.ip.diffim.modelPsfMatch": 
  143                 di.display = True                 # global 
  144                 di.maskTransparency = 30          # mask transparency 
  145                 di.displaySpatialCells = True     # show spatial cells before the fit 
  147         lsstDebug.Info = DebugInfo 
  150     Note that if you want addional logging info, you may add to your scripts: 
  154         import lsst.log.utils as logUtils 
  155         logUtils.traceSetAt("ip.diffim", 4) 
  159     A complete example of using ModelPsfMatchTask 
  161     This code is modelPsfMatchTask.py in the examples directory, and can be run as e.g. 
  163     .. code-block :: none 
  165         examples/modelPsfMatchTask.py 
  166         examples/modelPsfMatchTask.py --debug 
  167         examples/modelPsfMatchTask.py --debug --template /path/to/templateExp.fits 
  168         --science /path/to/scienceExp.fits 
  170     Create a subclass of ModelPsfMatchTask that accepts two exposures. 
  171     Note that the "template" exposure contains the Psf that will get matched to, 
  172     and the "science" exposure is the one that will be convolved: 
  174     .. code-block :: none 
  176         class MyModelPsfMatchTask(ModelPsfMatchTask): 
  177             def __init__(self, *args, **kwargs): 
  178                 ModelPsfMatchTask.__init__(self, *args, **kwargs) 
  179             def run(self, templateExp, scienceExp): 
  180                 return ModelPsfMatchTask.run(self, scienceExp, templateExp.getPsf()) 
  182     And allow the user the freedom to either run the script in default mode, 
  183     or point to their own images on disk. Note that these 
  184     images must be readable as an lsst.afw.image.Exposure: 
  186     .. code-block :: none 
  188         if __name__ == "__main__": 
  190             parser = argparse.ArgumentParser(description="Demonstrate the use of ModelPsfMatchTask") 
  191             parser.add_argument("--debug", "-d", action="store_true", help="Load debug.py?", default=False) 
  192             parser.add_argument("--template", "-t", help="Template Exposure to use", default=None) 
  193             parser.add_argument("--science", "-s", help="Science Exposure to use", default=None) 
  194             args = parser.parse_args() 
  196     We have enabled some minor display debugging in this script via the –debug option. 
  197     However, if you have an lsstDebug debug.py in your PYTHONPATH you will get additional 
  198     debugging displays. The following block checks for this script: 
  200     .. code-block :: none 
  205                 # Since I am displaying 2 images here, set the starting frame number for the LSST debug LSST 
  206                 debug.lsstDebug.frame = 3 
  207             except ImportError as e: 
  208                 print(e, file=sys.stderr) 
  210     Finally, we call a run method that we define below. 
  211     First set up a Config and modify some of the parameters. 
  212     In particular we don't want to "grow" the sizes of the kernel or KernelCandidates, 
  213     since we are operating with fixed–size images (i.e. the size of the input Psf models). 
  215     .. code-block :: none 
  219             # Create the Config and use sum of gaussian basis 
  221             config = ModelPsfMatchTask.ConfigClass() 
  222             config.kernel.active.scaleByFwhm = False 
  224     Make sure the images (if any) that were sent to the script exist on disk and are readable. 
  225     If no images are sent, make some fake data up for the sake of this example script 
  226     (have a look at the code if you want more details on generateFakeData): 
  228     .. code-block :: none 
  230         # Run the requested method of the Task 
  231         if args.template is not None and args.science is not None: 
  232             if not os.path.isfile(args.template): 
  233                 raise Exception("Template image %s does not exist" % (args.template)) 
  234             if not os.path.isfile(args.science): 
  235                 raise Exception("Science image %s does not exist" % (args.science)) 
  237                 templateExp = afwImage.ExposureF(args.template) 
  238             except Exception as e: 
  239                 raise Exception("Cannot read template image %s" % (args.template)) 
  241                 scienceExp = afwImage.ExposureF(args.science) 
  242             except Exception as e: 
  243                 raise Exception("Cannot read science image %s" % (args.science)) 
  245             templateExp, scienceExp = generateFakeData() 
  246             config.kernel.active.sizeCellX = 128 
  247             config.kernel.active.sizeCellY = 128 
  249     .. code-block :: none 
  252             afwDisplay.Display(frame=1).mtv(templateExp, title="Example script: Input Template") 
  253             afwDisplay.Display(frame=2).mtv(scienceExp, title="Example script: Input Science Image") 
  255     Create and run the Task: 
  257     .. code-block :: none 
  260         psfMatchTask = MyModelPsfMatchTask(config=config) 
  262         result = psfMatchTask.run(templateExp, scienceExp) 
  264     And finally provide optional debugging display of the Psf-matched (via the Psf models) science image: 
  266     .. code-block :: none 
  269             # See if the LSST debug has incremented the frame number; if not start with frame 3 
  271                 frame = debug.lsstDebug.frame + 1 
  274             afwDisplay.Display(frame=frame).mtv(result.psfMatchedExposure, 
  275                                                 title="Example script: Matched Science Image") 
  278     ConfigClass = ModelPsfMatchConfig
 
  281         """Create a ModelPsfMatchTask 
  286             arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__ 
  288             keyword arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__ 
  292         Upon initialization, the kernel configuration is defined by self.config.kernel.active.  This Task 
  293         does have a run() method, which is the default way to call the Task. 
  295         PsfMatchTask.__init__(self, *args, **kwargs)
 
  299     def run(self, exposure, referencePsfModel, kernelSum=1.0):
 
  300         """Psf-match an exposure to a model Psf 
  304         exposure : `lsst.afw.image.Exposure` 
  305             Exposure to Psf-match to the reference Psf model; 
  306             it must return a valid PSF model via exposure.getPsf() 
  307         referencePsfModel : `lsst.afw.detection.Psf` 
  308             The Psf model to match to 
  309         kernelSum : `float`, optional 
  310             A multipicative factor to apply to the kernel sum (default=1.0) 
  315             - ``psfMatchedExposure`` : the Psf-matched Exposure. 
  316                 This has the same parent bbox, Wcs, PhotoCalib and 
  317                 Filter as the input Exposure but no Psf. 
  318                 In theory the Psf should equal referencePsfModel but 
  319                 the match is likely not exact. 
  320             - ``psfMatchingKernel`` : the spatially varying Psf-matching kernel 
  321             - ``kernelCellSet`` : SpatialCellSet used to solve for the Psf-matching kernel 
  322             - ``referencePsfModel`` : Validated and/or modified reference model used 
  327             if the Exposure does not contain a Psf model 
  329         if not exposure.hasPsf():
 
  330             raise RuntimeError(
"exposure does not contain a Psf model")
 
  332         maskedImage = exposure.getMaskedImage()
 
  334         self.log.
info(
"compute Psf-matching kernel")
 
  336         kernelCellSet = result.kernelCellSet
 
  337         referencePsfModel = result.referencePsfModel
 
  338         fwhmScience = exposure.getPsf().computeShape().getDeterminantRadius()*sigma2fwhm
 
  339         fwhmModel = referencePsfModel.computeShape().getDeterminantRadius()*sigma2fwhm
 
  342         spatialSolution, psfMatchingKernel, backgroundModel = self.
_solve(kernelCellSet, basisList)
 
  344         if psfMatchingKernel.isSpatiallyVarying():
 
  345             sParameters = np.array(psfMatchingKernel.getSpatialParameters())
 
  346             sParameters[0][0] = kernelSum
 
  347             psfMatchingKernel.setSpatialParameters(sParameters)
 
  349             kParameters = np.array(psfMatchingKernel.getKernelParameters())
 
  350             kParameters[0] = kernelSum
 
  351             psfMatchingKernel.setKernelParameters(kParameters)
 
  353         self.log.
info(
"Psf-match science exposure to reference")
 
  354         psfMatchedExposure = afwImage.ExposureF(exposure.getBBox(), exposure.getWcs())
 
  355         psfMatchedExposure.setFilter(exposure.getFilter())
 
  356         psfMatchedExposure.setPhotoCalib(exposure.getPhotoCalib())
 
  357         psfMatchedExposure.getInfo().setVisitInfo(exposure.getInfo().getVisitInfo())
 
  358         psfMatchedExposure.setPsf(referencePsfModel)
 
  359         psfMatchedMaskedImage = psfMatchedExposure.getMaskedImage()
 
  364         afwMath.convolve(psfMatchedMaskedImage, maskedImage, psfMatchingKernel, doNormalize)
 
  366         self.log.
info(
"done")
 
  367         return pipeBase.Struct(psfMatchedExposure=psfMatchedExposure,
 
  368                                psfMatchingKernel=psfMatchingKernel,
 
  369                                kernelCellSet=kernelCellSet,
 
  370                                metadata=self.metadata,
 
  373     def _diagnostic(self, kernelCellSet, spatialSolution, spatialKernel, spatialBg):
 
  374         """Print diagnostic information on spatial kernel and background fit 
  376         The debugging diagnostics are not really useful here, since the images we are matching have 
  377         no variance.  Thus override the _diagnostic method to generate no logging information""" 
  380     def _buildCellSet(self, exposure, referencePsfModel):
 
  381         """Build a SpatialCellSet for use with the solve method 
  385         exposure : `lsst.afw.image.Exposure` 
  386             The science exposure that will be convolved; must contain a Psf 
  387         referencePsfModel : `lsst.afw.detection.Psf` 
  388             Psf model to match to 
  393             - ``kernelCellSet`` : a SpatialCellSet to be used by self._solve 
  394             - ``referencePsfModel`` : Validated and/or modified 
  395                 reference model used to populate the SpatialCellSet 
  399         If the reference Psf model and science Psf model have different dimensions, 
  400         adjust the referencePsfModel (the model to which the exposure PSF will be matched) 
  401         to match that of the science Psf. If the science Psf dimensions vary across the image, 
  402         as is common with a WarpedPsf, either pad or clip (depending on config.padPsf) 
  403         the dimensions to be constant. 
  405         sizeCellX = self.kConfig.sizeCellX
 
  406         sizeCellY = self.kConfig.sizeCellY
 
  408         scienceBBox = exposure.getBBox()
 
  412         sciencePsfModel = exposure.getPsf()
 
  414         dimenR = referencePsfModel.getLocalKernel().getDimensions()
 
  415         psfWidth, psfHeight = dimenR
 
  417         regionSizeX, regionSizeY = scienceBBox.getDimensions()
 
  418         scienceX0, scienceY0 = scienceBBox.getMin()
 
  422         nCellX = regionSizeX//sizeCellX
 
  423         nCellY = regionSizeY//sizeCellY
 
  425         if nCellX == 0 
or nCellY == 0:
 
  426             raise ValueError(
"Exposure dimensions=%s and sizeCell=(%s, %s). Insufficient area to match" %
 
  427                              (scienceBBox.getDimensions(), sizeCellX, sizeCellY))
 
  433         for row 
in range(nCellY):
 
  434             posY = sizeCellY*row + sizeCellY//2 + scienceY0
 
  435             for col 
in range(nCellX):
 
  436                 posX = sizeCellX*col + sizeCellX//2 + scienceX0
 
  437                 widthS, heightS = sciencePsfModel.computeBBox(
geom.Point2D(posX, posY)).getDimensions()
 
  438                 widthList.append(widthS)
 
  439                 heightList.append(heightS)
 
  441         psfSize = 
max(
max(heightList), 
max(widthList))
 
  443         if self.config.doAutoPadPsf:
 
  444             minPsfSize = 
nextOddInteger(self.kConfig.kernelSize*self.config.autoPadPsfTo)
 
  445             paddingPix = 
max(0, minPsfSize - psfSize)
 
  447             if self.config.padPsfBy % 2 != 0:
 
  448                 raise ValueError(
"Config padPsfBy (%i pixels) must be even number." %
 
  449                                  self.config.padPsfBy)
 
  450             paddingPix = self.config.padPsfBy
 
  453             self.log.
info(
"Padding Science PSF from (%s, %s) to (%s, %s) pixels" %
 
  454                           (psfSize, psfSize, paddingPix + psfSize, paddingPix + psfSize))
 
  455             psfSize += paddingPix
 
  458         maxKernelSize = psfSize - 1
 
  459         if maxKernelSize % 2 == 0:
 
  461         if self.kConfig.kernelSize > maxKernelSize:
 
  463                 Kernel size (%d) too big to match Psfs of size %d. 
  464                 Please reconfigure by setting one of the following: 
  465                 1) kernel size to <= %d 
  468                 """ % (self.kConfig.kernelSize, psfSize,
 
  469                        maxKernelSize, self.kConfig.kernelSize - maxKernelSize)
 
  470             raise ValueError(message)
 
  474         if (dimenR != dimenS):
 
  476                 referencePsfModel = referencePsfModel.resized(psfSize, psfSize)
 
  477                 self.log.
info(
"Adjusted dimensions of reference PSF model from %s to %s" % (dimenR, dimenS))
 
  478             except Exception 
as e:
 
  479                 self.log.
warn(
"Zero padding or clipping the reference PSF model of type %s and dimensions %s" 
  480                               " to the science Psf dimensions %s because: %s",
 
  481                               referencePsfModel.__class__.__name__, dimenR, dimenS, e)
 
  484         ps = pexConfig.makePropertySet(self.kConfig)
 
  485         for row 
in range(nCellY):
 
  487             posY = sizeCellY*row + sizeCellY//2 + scienceY0
 
  489             for col 
in range(nCellX):
 
  491                 posX = sizeCellX*col + sizeCellX//2 + scienceX0
 
  493                 log.log(
"TRACE4." + self.log.getName(), log.DEBUG,
 
  494                         "Creating Psf candidate at %.1f %.1f", posX, posY)
 
  497                 referenceMI = self._makePsfMaskedImage(referencePsfModel, posX, posY, dimensions=dimenR)
 
  500                 scienceMI = self._makePsfMaskedImage(sciencePsfModel, posX, posY, dimensions=dimenR)
 
  503                 kc = diffimLib.makeKernelCandidate(posX, posY, scienceMI, referenceMI, ps)
 
  504                 kernelCellSet.insertCandidate(kc)
 
  508         displaySpatialCells = 
lsstDebug.Info(__name__).displaySpatialCells
 
  510         if not maskTransparency:
 
  513             afwDisplay.setDefaultMaskTransparency(maskTransparency)
 
  514         if display 
and displaySpatialCells:
 
  515             dituils.showKernelSpatialCells(exposure.getMaskedImage(), kernelCellSet,
 
  516                                            symb=
"o", ctype=afwDisplay.CYAN, ctypeUnused=afwDisplay.YELLOW,
 
  517                                            ctypeBad=afwDisplay.RED, size=4, frame=lsstDebug.frame,
 
  518                                            title=
"Image to be convolved")
 
  520         return pipeBase.Struct(kernelCellSet=kernelCellSet,
 
  521                                referencePsfModel=referencePsfModel,
 
  524     def _makePsfMaskedImage(self, psfModel, posX, posY, dimensions=None):
 
  525         """Return a MaskedImage of the a PSF Model of specified dimensions 
  527         rawKernel = psfModel.computeKernelImage(
geom.Point2D(posX, posY)).convertF()
 
  528         if dimensions 
is None:
 
  529             dimensions = rawKernel.getDimensions()
 
  530         if rawKernel.getDimensions() == dimensions:
 
  534             kernelIm = afwImage.ImageF(dimensions)
 
  536                                                   (dimensions.getY() - rawKernel.getHeight())//2),
 
  537                                      rawKernel.getDimensions())
 
  538             kernelIm.assign(rawKernel, bboxToPlace)
 
  541         kernelVar = afwImage.ImageF(dimensions, 1.0)
 
  542         return afwImage.MaskedImageF(kernelIm, kernelMask, kernelVar)