24 from . 
import diffimLib
 
   32 from lsst.utils.timer 
import timeMethod
 
   33 from .makeKernelBasisList 
import makeKernelBasisList
 
   34 from .psfMatch 
import PsfMatchTask, PsfMatchConfigAL
 
   35 from . 
import utils 
as dituils
 
   37 __all__ = (
"ModelPsfMatchTask", 
"ModelPsfMatchConfig")
 
   39 sigma2fwhm = 2.*np.sqrt(2.*np.log(2.))
 
   43     nextInt = int(np.ceil(x))
 
   44     return nextInt + 1 
if nextInt%2 == 0 
else nextInt
 
   48     """Configuration for model-to-model Psf matching""" 
   50     kernel = pexConfig.ConfigChoiceField(
 
   57     doAutoPadPsf = pexConfig.Field(
 
   59         doc=(
"If too small, automatically pad the science Psf? " 
   60              "Pad to smallest dimensions appropriate for the matching kernel dimensions, " 
   61              "as specified by autoPadPsfTo. If false, pad by the padPsfBy config."),
 
   64     autoPadPsfTo = pexConfig.RangeField(
 
   66         doc=(
"Minimum Science Psf dimensions as a fraction of matching kernel dimensions. " 
   67              "If the dimensions of the Psf to be matched are less than the " 
   68              "matching kernel dimensions * autoPadPsfTo, pad Science Psf to this size. " 
   69              "Ignored if doAutoPadPsf=False."),
 
   74     padPsfBy = pexConfig.Field(
 
   76         doc=
"Pixels (even) to pad Science Psf by before matching. Ignored if doAutoPadPsf=True",
 
   82         self.
kernelkernel.active.singleKernelClipping = 
False 
   83         self.
kernelkernel.active.kernelSumClipping = 
False 
   84         self.
kernelkernel.active.spatialKernelClipping = 
False 
   85         self.
kernelkernel.active.checkConditionNumber = 
False 
   88         self.
kernelkernel.active.constantVarianceWeighting = 
True 
   91         self.
kernelkernel.active.scaleByFwhm = 
False 
   95     """Matching of two model Psfs, and application of the Psf-matching kernel to an input Exposure 
  100     This Task differs from ImagePsfMatchTask in that it matches two Psf _models_, by realizing 
  101     them in an Exposure-sized SpatialCellSet and then inserting each Psf-image pair into KernelCandidates. 
  102     Because none of the pairs of sources that are to be matched should be invalid, all sigma clipping is 
  103     turned off in ModelPsfMatchConfig.  And because there is no tracked _variance_ in the Psf images, the 
  104     debugging and logging QA info should be interpreted with caution. 
  106     One item of note is that the sizes of Psf models are fixed (e.g. its defined as a 21x21 matrix).  When the 
  107     Psf-matching kernel is being solved for, the Psf "image" is convolved with each kernel basis function, 
  108     leading to a loss of information around the borders. 
  109     This pixel loss will be problematic for the numerical 
  110     stability of the kernel solution if the size of the convolution kernel 
  111     (set by ModelPsfMatchConfig.kernelSize) is much bigger than: psfSize//2. 
  112     Thus the sizes of Psf-model matching kernels are typically smaller 
  113     than their image-matching counterparts.  If the size of the kernel is too small, the convolved stars will 
  114     look "boxy"; if the kernel is too large, the kernel solution will be "noisy".  This is a trade-off that 
  115     needs careful attention for a given dataset. 
  117     The primary use case for this Task is in matching an Exposure to a 
  118     constant-across-the-sky Psf model for the purposes of image coaddition. 
  119     It is important to note that in the code, the "template" Psf is the Psf 
  120     that the science image gets matched to.  In this sense the order of template and science image are 
  121     reversed, compared to ImagePsfMatchTask, which operates on the template image. 
  125     The `lsst.pipe.base.cmdLineTask.CmdLineTask` command line task interface supports a 
  126     flag -d/--debug to import debug.py from your PYTHONPATH.  The relevant contents of debug.py 
  127     for this Task include: 
  134             di = lsstDebug.getInfo(name) 
  135             if name == "lsst.ip.diffim.psfMatch": 
  136                 di.display = True                 # global 
  137                 di.maskTransparency = 80          # mask transparency 
  138                 di.displayCandidates = True       # show all the candidates and residuals 
  139                 di.displayKernelBasis = False     # show kernel basis functions 
  140                 di.displayKernelMosaic = True     # show kernel realized across the image 
  141                 di.plotKernelSpatialModel = False # show coefficients of spatial model 
  142                 di.showBadCandidates = True       # show the bad candidates (red) along with good (green) 
  143             elif name == "lsst.ip.diffim.modelPsfMatch": 
  144                 di.display = True                 # global 
  145                 di.maskTransparency = 30          # mask transparency 
  146                 di.displaySpatialCells = True     # show spatial cells before the fit 
  148         lsstDebug.Info = DebugInfo 
  151     Note that if you want addional logging info, you may add to your scripts: 
  155         import lsst.log.utils as logUtils 
  156         logUtils.traceSetAt("lsst.ip.diffim", 4) 
  160     A complete example of using ModelPsfMatchTask 
  162     This code is modelPsfMatchTask.py in the examples directory, and can be run as e.g. 
  164     .. code-block :: none 
  166         examples/modelPsfMatchTask.py 
  167         examples/modelPsfMatchTask.py --debug 
  168         examples/modelPsfMatchTask.py --debug --template /path/to/templateExp.fits 
  169         --science /path/to/scienceExp.fits 
  171     Create a subclass of ModelPsfMatchTask that accepts two exposures. 
  172     Note that the "template" exposure contains the Psf that will get matched to, 
  173     and the "science" exposure is the one that will be convolved: 
  175     .. code-block :: none 
  177         class MyModelPsfMatchTask(ModelPsfMatchTask): 
  178             def __init__(self, *args, **kwargs): 
  179                 ModelPsfMatchTask.__init__(self, *args, **kwargs) 
  180             def run(self, templateExp, scienceExp): 
  181                 return ModelPsfMatchTask.run(self, scienceExp, templateExp.getPsf()) 
  183     And allow the user the freedom to either run the script in default mode, 
  184     or point to their own images on disk. Note that these 
  185     images must be readable as an lsst.afw.image.Exposure: 
  187     .. code-block :: none 
  189         if __name__ == "__main__": 
  191             parser = argparse.ArgumentParser(description="Demonstrate the use of ModelPsfMatchTask") 
  192             parser.add_argument("--debug", "-d", action="store_true", help="Load debug.py?", default=False) 
  193             parser.add_argument("--template", "-t", help="Template Exposure to use", default=None) 
  194             parser.add_argument("--science", "-s", help="Science Exposure to use", default=None) 
  195             args = parser.parse_args() 
  197     We have enabled some minor display debugging in this script via the –debug option. 
  198     However, if you have an lsstDebug debug.py in your PYTHONPATH you will get additional 
  199     debugging displays. The following block checks for this script: 
  201     .. code-block :: none 
  206                 # Since I am displaying 2 images here, set the starting frame number for the LSST debug LSST 
  207                 debug.lsstDebug.frame = 3 
  208             except ImportError as e: 
  209                 print(e, file=sys.stderr) 
  211     Finally, we call a run method that we define below. 
  212     First set up a Config and modify some of the parameters. 
  213     In particular we don't want to "grow" the sizes of the kernel or KernelCandidates, 
  214     since we are operating with fixed–size images (i.e. the size of the input Psf models). 
  216     .. code-block :: none 
  220             # Create the Config and use sum of gaussian basis 
  222             config = ModelPsfMatchTask.ConfigClass() 
  223             config.kernel.active.scaleByFwhm = False 
  225     Make sure the images (if any) that were sent to the script exist on disk and are readable. 
  226     If no images are sent, make some fake data up for the sake of this example script 
  227     (have a look at the code if you want more details on generateFakeData): 
  229     .. code-block :: none 
  231         # Run the requested method of the Task 
  232         if args.template is not None and args.science is not None: 
  233             if not os.path.isfile(args.template): 
  234                 raise FileNotFoundError("Template image %s does not exist" % (args.template)) 
  235             if not os.path.isfile(args.science): 
  236                 raise FileNotFoundError("Science image %s does not exist" % (args.science)) 
  238                 templateExp = afwImage.ExposureF(args.template) 
  239             except Exception as e: 
  240                 raise RuntimeError("Cannot read template image %s" % (args.template)) 
  242                 scienceExp = afwImage.ExposureF(args.science) 
  243             except Exception as e: 
  244                 raise RuntimeError("Cannot read science image %s" % (args.science)) 
  246             templateExp, scienceExp = generateFakeData() 
  247             config.kernel.active.sizeCellX = 128 
  248             config.kernel.active.sizeCellY = 128 
  250     .. code-block :: none 
  253             afwDisplay.Display(frame=1).mtv(templateExp, title="Example script: Input Template") 
  254             afwDisplay.Display(frame=2).mtv(scienceExp, title="Example script: Input Science Image") 
  256     Create and run the Task: 
  258     .. code-block :: none 
  261         psfMatchTask = MyModelPsfMatchTask(config=config) 
  263         result = psfMatchTask.run(templateExp, scienceExp) 
  265     And finally provide optional debugging display of the Psf-matched (via the Psf models) science image: 
  267     .. code-block :: none 
  270             # See if the LSST debug has incremented the frame number; if not start with frame 3 
  272                 frame = debug.lsstDebug.frame + 1 
  275             afwDisplay.Display(frame=frame).mtv(result.psfMatchedExposure, 
  276                                                 title="Example script: Matched Science Image") 
  279     ConfigClass = ModelPsfMatchConfig
 
  282         """Create a ModelPsfMatchTask 
  287             arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__ 
  289             keyword arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__ 
  293         Upon initialization, the kernel configuration is defined by self.config.kernel.active.  This Task 
  294         does have a run() method, which is the default way to call the Task. 
  296         PsfMatchTask.__init__(self, *args, **kwargs)
 
  300     def run(self, exposure, referencePsfModel, kernelSum=1.0):
 
  301         """Psf-match an exposure to a model Psf 
  305         exposure : `lsst.afw.image.Exposure` 
  306             Exposure to Psf-match to the reference Psf model; 
  307             it must return a valid PSF model via exposure.getPsf() 
  308         referencePsfModel : `lsst.afw.detection.Psf` 
  309             The Psf model to match to 
  310         kernelSum : `float`, optional 
  311             A multipicative factor to apply to the kernel sum (default=1.0) 
  316             - ``psfMatchedExposure`` : the Psf-matched Exposure. 
  317                 This has the same parent bbox, Wcs, PhotoCalib and 
  318                 Filter as the input Exposure but no Psf. 
  319                 In theory the Psf should equal referencePsfModel but 
  320                 the match is likely not exact. 
  321             - ``psfMatchingKernel`` : the spatially varying Psf-matching kernel 
  322             - ``kernelCellSet`` : SpatialCellSet used to solve for the Psf-matching kernel 
  323             - ``referencePsfModel`` : Validated and/or modified reference model used 
  328             if the Exposure does not contain a Psf model 
  330         if not exposure.hasPsf():
 
  331             raise RuntimeError(
"exposure does not contain a Psf model")
 
  333         maskedImage = exposure.getMaskedImage()
 
  335         self.log.
info(
"compute Psf-matching kernel")
 
  337         kernelCellSet = result.kernelCellSet
 
  338         referencePsfModel = result.referencePsfModel
 
  339         fwhmScience = exposure.getPsf().computeShape().getDeterminantRadius()*sigma2fwhm
 
  340         fwhmModel = referencePsfModel.computeShape().getDeterminantRadius()*sigma2fwhm
 
  343         spatialSolution, psfMatchingKernel, backgroundModel = self.
_solve_solve(kernelCellSet, basisList)
 
  345         if psfMatchingKernel.isSpatiallyVarying():
 
  346             sParameters = np.array(psfMatchingKernel.getSpatialParameters())
 
  347             sParameters[0][0] = kernelSum
 
  348             psfMatchingKernel.setSpatialParameters(sParameters)
 
  350             kParameters = np.array(psfMatchingKernel.getKernelParameters())
 
  351             kParameters[0] = kernelSum
 
  352             psfMatchingKernel.setKernelParameters(kParameters)
 
  354         self.log.
info(
"Psf-match science exposure to reference")
 
  355         psfMatchedExposure = afwImage.ExposureF(exposure.getBBox(), exposure.getWcs())
 
  356         psfMatchedExposure.info.id = exposure.info.id
 
  357         psfMatchedExposure.setFilterLabel(exposure.getFilterLabel())
 
  358         psfMatchedExposure.setPhotoCalib(exposure.getPhotoCalib())
 
  359         psfMatchedExposure.getInfo().setVisitInfo(exposure.getInfo().getVisitInfo())
 
  360         psfMatchedExposure.setPsf(referencePsfModel)
 
  361         psfMatchedMaskedImage = psfMatchedExposure.getMaskedImage()
 
  366         convolutionControl.setDoNormalize(
True)
 
  367         afwMath.convolve(psfMatchedMaskedImage, maskedImage, psfMatchingKernel, convolutionControl)
 
  369         self.log.
info(
"done")
 
  370         return pipeBase.Struct(psfMatchedExposure=psfMatchedExposure,
 
  371                                psfMatchingKernel=psfMatchingKernel,
 
  372                                kernelCellSet=kernelCellSet,
 
  373                                metadata=self.metadata,
 
  376     def _diagnostic(self, kernelCellSet, spatialSolution, spatialKernel, spatialBg):
 
  377         """Print diagnostic information on spatial kernel and background fit 
  379         The debugging diagnostics are not really useful here, since the images we are matching have 
  380         no variance.  Thus override the _diagnostic method to generate no logging information""" 
  383     def _buildCellSet(self, exposure, referencePsfModel):
 
  384         """Build a SpatialCellSet for use with the solve method 
  388         exposure : `lsst.afw.image.Exposure` 
  389             The science exposure that will be convolved; must contain a Psf 
  390         referencePsfModel : `lsst.afw.detection.Psf` 
  391             Psf model to match to 
  396             - ``kernelCellSet`` : a SpatialCellSet to be used by self._solve 
  397             - ``referencePsfModel`` : Validated and/or modified 
  398                 reference model used to populate the SpatialCellSet 
  402         If the reference Psf model and science Psf model have different dimensions, 
  403         adjust the referencePsfModel (the model to which the exposure PSF will be matched) 
  404         to match that of the science Psf. If the science Psf dimensions vary across the image, 
  405         as is common with a WarpedPsf, either pad or clip (depending on config.padPsf) 
  406         the dimensions to be constant. 
  408         sizeCellX = self.kConfig.sizeCellX
 
  409         sizeCellY = self.kConfig.sizeCellY
 
  411         scienceBBox = exposure.getBBox()
 
  415         sciencePsfModel = exposure.getPsf()
 
  417         dimenR = referencePsfModel.getLocalKernel().getDimensions()
 
  418         psfWidth, psfHeight = dimenR
 
  420         regionSizeX, regionSizeY = scienceBBox.getDimensions()
 
  421         scienceX0, scienceY0 = scienceBBox.getMin()
 
  425         nCellX = regionSizeX//sizeCellX
 
  426         nCellY = regionSizeY//sizeCellY
 
  428         if nCellX == 0 
or nCellY == 0:
 
  429             raise ValueError(
"Exposure dimensions=%s and sizeCell=(%s, %s). Insufficient area to match" %
 
  430                              (scienceBBox.getDimensions(), sizeCellX, sizeCellY))
 
  436         for row 
in range(nCellY):
 
  437             posY = sizeCellY*row + sizeCellY//2 + scienceY0
 
  438             for col 
in range(nCellX):
 
  439                 posX = sizeCellX*col + sizeCellX//2 + scienceX0
 
  440                 widthS, heightS = sciencePsfModel.computeBBox(
geom.Point2D(posX, posY)).getDimensions()
 
  441                 widthList.append(widthS)
 
  442                 heightList.append(heightS)
 
  444         psfSize = 
max(
max(heightList), 
max(widthList))
 
  446         if self.config.doAutoPadPsf:
 
  447             minPsfSize = 
nextOddInteger(self.kConfig.kernelSize*self.config.autoPadPsfTo)
 
  448             paddingPix = 
max(0, minPsfSize - psfSize)
 
  450             if self.config.padPsfBy % 2 != 0:
 
  451                 raise ValueError(
"Config padPsfBy (%i pixels) must be even number." %
 
  452                                  self.config.padPsfBy)
 
  453             paddingPix = self.config.padPsfBy
 
  456             self.log.
debug(
"Padding Science PSF from (%d, %d) to (%d, %d) pixels",
 
  457                            psfSize, psfSize, paddingPix + psfSize, paddingPix + psfSize)
 
  458             psfSize += paddingPix
 
  461         maxKernelSize = psfSize - 1
 
  462         if maxKernelSize % 2 == 0:
 
  464         if self.kConfig.kernelSize > maxKernelSize:
 
  466                 Kernel size (%d) too big to match Psfs of size %d. 
  467                 Please reconfigure by setting one of the following: 
  468                 1) kernel size to <= %d 
  471                 """ % (self.kConfig.kernelSize, psfSize,
 
  472                        maxKernelSize, self.kConfig.kernelSize - maxKernelSize)
 
  473             raise ValueError(message)
 
  477         if (dimenR != dimenS):
 
  479                 referencePsfModel = referencePsfModel.resized(psfSize, psfSize)
 
  480                 self.log.
info(
"Adjusted dimensions of reference PSF model from %s to %s", dimenR, dimenS)
 
  481             except Exception 
as e:
 
  482                 self.log.
warning(
"Zero padding or clipping the reference PSF model of type %s and dimensions" 
  483                                  " %s to the science Psf dimensions %s because: %s",
 
  484                                  referencePsfModel.__class__.__name__, dimenR, dimenS, e)
 
  487         ps = pexConfig.makePropertySet(self.kConfig)
 
  488         for row 
in range(nCellY):
 
  490             posY = sizeCellY*row + sizeCellY//2 + scienceY0
 
  492             for col 
in range(nCellX):
 
  494                 posX = sizeCellX*col + sizeCellX//2 + scienceX0
 
  496                 log.log(
"TRACE4." + self.log.name, log.DEBUG,
 
  497                         "Creating Psf candidate at %.1f %.1f", posX, posY)
 
  500                 referenceMI = self._makePsfMaskedImage(referencePsfModel, posX, posY, dimensions=dimenR)
 
  503                 scienceMI = self._makePsfMaskedImage(sciencePsfModel, posX, posY, dimensions=dimenR)
 
  506                 kc = diffimLib.makeKernelCandidate(posX, posY, scienceMI, referenceMI, ps)
 
  507                 kernelCellSet.insertCandidate(kc)
 
  511         displaySpatialCells = 
lsstDebug.Info(__name__).displaySpatialCells
 
  513         if not maskTransparency:
 
  516             afwDisplay.setDefaultMaskTransparency(maskTransparency)
 
  517         if display 
and displaySpatialCells:
 
  518             dituils.showKernelSpatialCells(exposure.getMaskedImage(), kernelCellSet,
 
  519                                            symb=
"o", ctype=afwDisplay.CYAN, ctypeUnused=afwDisplay.YELLOW,
 
  520                                            ctypeBad=afwDisplay.RED, size=4, frame=lsstDebug.frame,
 
  521                                            title=
"Image to be convolved")
 
  523         return pipeBase.Struct(kernelCellSet=kernelCellSet,
 
  524                                referencePsfModel=referencePsfModel,
 
  527     def _makePsfMaskedImage(self, psfModel, posX, posY, dimensions=None):
 
  528         """Return a MaskedImage of the a PSF Model of specified dimensions 
  530         rawKernel = psfModel.computeKernelImage(
geom.Point2D(posX, posY)).convertF()
 
  531         if dimensions 
is None:
 
  532             dimensions = rawKernel.getDimensions()
 
  533         if rawKernel.getDimensions() == dimensions:
 
  537             kernelIm = afwImage.ImageF(dimensions)
 
  539                                                   (dimensions.getY() - rawKernel.getHeight())//2),
 
  540                                      rawKernel.getDimensions())
 
  541             kernelIm.assign(rawKernel, bboxToPlace)
 
  544         kernelVar = afwImage.ImageF(dimensions, 1.0)
 
  545         return afwImage.MaskedImageF(kernelIm, kernelMask, kernelVar)
 
Represent a 2-dimensional array of bitmask pixels.
 
Parameters to control convolution.
 
A collection of SpatialCells covering an entire image.
 
An integer coordinate rectangle.
 
def __init__(self, *args, **kwargs)
 
def run(self, exposure, referencePsfModel, kernelSum=1.0)
 
def _buildCellSet(self, exposure, referencePsfModel)
 
def _buildCellSet(self, *args)
 
def _solve(self, kernelCellSet, basisList, returnOnExcept=False)
 
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
 
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
Convolve an Image or MaskedImage with a Kernel, setting pixels of an existing output image.
 
def makeKernelBasisList(config, targetFwhmPix=None, referenceFwhmPix=None, basisDegGauss=None, basisSigmaGauss=None, metadata=None)