21 from __future__
import absolute_import, division, print_function
23 from builtins
import range
26 from .
import diffimLib
27 import lsst.afw.geom
as afwGeom
28 import lsst.afw.image
as afwImage
29 import lsst.afw.math
as afwMath
30 import lsst.log
as log
31 import lsst.pex.config
as pexConfig
32 import lsst.pipe.base
as pipeBase
33 from .makeKernelBasisList
import makeKernelBasisList
34 from .psfMatch
import PsfMatchTask, PsfMatchConfigAL
35 from .
import utils
as diUtils
36 import lsst.afw.display.ds9
as ds9
38 __all__ = (
"ModelPsfMatchTask",
"ModelPsfMatchConfig")
40 sigma2fwhm = 2. * np.sqrt(2. * np.log(2.))
44 nextInt = int(np.ceil(x))
45 return nextInt + 1
if nextInt % 2 == 0
else nextInt
49 """!Configuration for model-to-model Psf matching""" 51 kernel = pexConfig.ConfigChoiceField(
58 doAutoPadPsf = pexConfig.Field(
60 doc=(
"If too small, automatically pad the science Psf? " 61 "Pad to smallest dimensions appropriate for the matching kernel dimensions, " 62 "as specified by autoPadPsfTo. If false, pad by the padPsfBy config."),
65 autoPadPsfTo = pexConfig.RangeField(
67 doc=(
"Minimum Science Psf dimensions as a fraction of matching kernel dimensions. " 68 "If the dimensions of the Psf to be matched are less than the " 69 "matching kernel dimensions * autoPadPsfTo, pad Science Psf to this size. " 70 "Ignored if doAutoPadPsf=False."),
75 padPsfBy = pexConfig.Field(
77 doc=
"Pixels (even) to pad Science Psf by before matching. Ignored if doAutoPadPsf=True",
83 self.
kernel.active.singleKernelClipping =
False 84 self.
kernel.active.kernelSumClipping =
False 85 self.
kernel.active.spatialKernelClipping =
False 86 self.
kernel.active.checkConditionNumber =
False 89 self.
kernel.active.constantVarianceWeighting =
True 92 self.
kernel.active.scaleByFwhm =
False 105 \anchor ModelPsfMatchTask_ 107 \brief Matching of two model Psfs, and application of the Psf-matching kernel to an input Exposure 109 \section ip_diffim_modelpsfmatch_Contents Contents 111 - \ref ip_diffim_modelpsfmatch_Purpose 112 - \ref ip_diffim_modelpsfmatch_Initialize 113 - \ref ip_diffim_modelpsfmatch_IO 114 - \ref ip_diffim_modelpsfmatch_Config 115 - \ref ip_diffim_modelpsfmatch_Metadata 116 - \ref ip_diffim_modelpsfmatch_Debug 117 - \ref ip_diffim_modelpsfmatch_Example 119 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 121 \section ip_diffim_modelpsfmatch_Purpose Description 123 This Task differs from ImagePsfMatchTask in that it matches two Psf _models_, by realizing 124 them in an Exposure-sized SpatialCellSet and then inserting each Psf-image pair into KernelCandidates. 125 Because none of the pairs of sources that are to be matched should be invalid, all sigma clipping is 126 turned off in ModelPsfMatchConfig. And because there is no tracked _variance_ in the Psf images, the 127 debugging and logging QA info should be interpreted with caution. 129 One item of note is that the sizes of Psf models are fixed (e.g. its defined as a 21x21 matrix). When the 130 Psf-matching kernel is being solved for, the Psf "image" is convolved with each kernel basis function, 131 leading to a loss of information around the borders. This pixel loss will be problematic for the numerical 132 stability of the kernel solution if the size of the convolution kernel (set by ModelPsfMatchConfig.kernelSize) 133 is much bigger than: psfSize//2. Thus the sizes of Psf-model matching kernels are typically smaller 134 than their image-matching counterparts. If the size of the kernel is too small, the convolved stars will 135 look "boxy"; if the kernel is too large, the kernel solution will be "noisy". This is a trade-off that 136 needs careful attention for a given dataset. 138 The primary use case for this Task is in matching an Exposure to a constant-across-the-sky Psf model for the 139 purposes of image coaddition. It is important to note that in the code, the "template" Psf is the Psf 140 that the science image gets matched to. In this sense the order of template and science image are 141 reversed, compared to ImagePsfMatchTask, which operates on the template image. 143 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 145 \section ip_diffim_modelpsfmatch_Initialize Task initialization 147 \copydoc \_\_init\_\_ 149 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 151 \section ip_diffim_modelpsfmatch_IO Invoking the Task 155 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 157 \section ip_diffim_modelpsfmatch_Config Configuration parameters 159 See \ref ModelPsfMatchConfig 161 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 163 \section ip_diffim_modelpsfmatch_Metadata Quantities set in Metadata 165 See \ref ip_diffim_psfmatch_Metadata "PsfMatchTask" 167 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 169 \section ip_diffim_modelpsfmatch_Debug Debug variables 171 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a 172 flag \c -d/--debug to import \b debug.py from your \c PYTHONPATH. The relevant contents of debug.py 173 for this Task include: 179 di = lsstDebug.getInfo(name) 180 if name == "lsst.ip.diffim.psfMatch": 181 di.display = True # global 182 di.maskTransparency = 80 # ds9 mask transparency 183 di.displayCandidates = True # show all the candidates and residuals 184 di.displayKernelBasis = False # show kernel basis functions 185 di.displayKernelMosaic = True # show kernel realized across the image 186 di.plotKernelSpatialModel = False # show coefficients of spatial model 187 di.showBadCandidates = True # show the bad candidates (red) along with good (green) 188 elif name == "lsst.ip.diffim.modelPsfMatch": 189 di.display = True # global 190 di.maskTransparency = 30 # ds9 mask transparency 191 di.displaySpatialCells = True # show spatial cells before the fit 193 lsstDebug.Info = DebugInfo 197 Note that if you want addional logging info, you may add to your scripts: 199 import lsst.log.utils as logUtils 200 logUtils.traceSetAt("ip.diffim", 4) 203 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 205 \section ip_diffim_modelpsfmatch_Example A complete example of using ModelPsfMatchTask 207 This code is modelPsfMatchTask.py in the examples directory, and can be run as \em e.g. 209 examples/modelPsfMatchTask.py 210 examples/modelPsfMatchTask.py --debug 211 examples/modelPsfMatchTask.py --debug --template /path/to/templateExp.fits --science /path/to/scienceExp.fits 214 \dontinclude modelPsfMatchTask.py 215 Create a subclass of ModelPsfMatchTask that accepts two exposures. Note that the "template" exposure 216 contains the Psf that will get matched to, and the "science" exposure is the one that will be convolved: 217 \skip MyModelPsfMatchTask 220 And allow the user the freedom to either run the script in default mode, or point to their own images on disk. 221 Note that these images must be readable as an lsst.afw.image.Exposure: 225 We have enabled some minor display debugging in this script via the --debug option. However, if you 226 have an lsstDebug debug.py in your PYTHONPATH you will get additional debugging displays. The following 227 block checks for this script: 231 \dontinclude modelPsfMatchTask.py 232 Finally, we call a run method that we define below. First set up a Config and modify some of the parameters. 233 In particular we don't want to "grow" the sizes of the kernel or KernelCandidates, since we are operating with 234 fixed--size images (i.e. the size of the input Psf models). 238 Make sure the images (if any) that were sent to the script exist on disk and are readable. If no images 239 are sent, make some fake data up for the sake of this example script (have a look at the code if you want 240 more details on generateFakeData): 244 Display the two images if --debug: 248 Create and run the Task: 252 And finally provide optional debugging display of the Psf-matched (via the Psf models) science image: 254 @until result.psfMatchedExposure 256 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 259 ConfigClass = ModelPsfMatchConfig
262 """!Create a ModelPsfMatchTask 264 \param *args arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__ 265 \param **kwargs keyword arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__ 267 Upon initialization, the kernel configuration is defined by self.config.kernel.active. This Task 268 does have a run() method, which is the default way to call the Task. 270 PsfMatchTask.__init__(self, *args, **kwargs)
274 def run(self, exposure, referencePsfModel, kernelSum=1.0):
275 """!Psf-match an exposure to a model Psf 277 @param exposure: Exposure to Psf-match to the reference Psf model; 278 it must return a valid PSF model via exposure.getPsf() 279 @param referencePsfModel: The Psf model to match to (an lsst.afw.detection.Psf) 280 @param kernelSum: A multipicative factor to apply to the kernel sum (default=1.0) 283 - psfMatchedExposure: the Psf-matched Exposure. This has the same parent bbox, Wcs, Calib and 284 Filter as the input Exposure but no Psf. In theory the Psf should equal referencePsfModel but 285 the match is likely not exact. 286 - psfMatchingKernel: the spatially varying Psf-matching kernel 287 - kernelCellSet: SpatialCellSet used to solve for the Psf-matching kernel 288 - referencePsfModel: Validated and/or modified reference model used 290 Raise a RuntimeError if the Exposure does not contain a Psf model 292 if not exposure.hasPsf():
293 raise RuntimeError(
"exposure does not contain a Psf model")
295 maskedImage = exposure.getMaskedImage()
297 self.log.info(
"compute Psf-matching kernel")
299 kernelCellSet = result.kernelCellSet
300 referencePsfModel = result.referencePsfModel
301 fwhmScience = exposure.getPsf().computeShape().getDeterminantRadius() * sigma2fwhm
302 fwhmModel = referencePsfModel.computeShape().getDeterminantRadius() * sigma2fwhm
305 spatialSolution, psfMatchingKernel, backgroundModel = self.
_solve(kernelCellSet, basisList)
307 if psfMatchingKernel.isSpatiallyVarying():
308 sParameters = np.array(psfMatchingKernel.getSpatialParameters())
309 sParameters[0][0] = kernelSum
310 psfMatchingKernel.setSpatialParameters(sParameters)
312 kParameters = np.array(psfMatchingKernel.getKernelParameters())
313 kParameters[0] = kernelSum
314 psfMatchingKernel.setKernelParameters(kParameters)
316 self.log.info(
"Psf-match science exposure to reference")
317 psfMatchedExposure = afwImage.ExposureF(exposure.getBBox(), exposure.getWcs())
318 psfMatchedExposure.setFilter(exposure.getFilter())
319 psfMatchedExposure.setCalib(exposure.getCalib())
320 psfMatchedExposure.setPsf(referencePsfModel)
321 psfMatchedMaskedImage = psfMatchedExposure.getMaskedImage()
326 afwMath.convolve(psfMatchedMaskedImage, maskedImage, psfMatchingKernel, doNormalize)
328 self.log.info(
"done")
329 return pipeBase.Struct(psfMatchedExposure=psfMatchedExposure,
330 psfMatchingKernel=psfMatchingKernel,
331 kernelCellSet=kernelCellSet,
332 metadata=self.metadata,
335 def _diagnostic(self, kernelCellSet, spatialSolution, spatialKernel, spatialBg):
336 """!Print diagnostic information on spatial kernel and background fit 338 The debugging diagnostics are not really useful here, since the images we are matching have 339 no variance. Thus override the _diagnostic method to generate no logging information""" 343 """!Build a SpatialCellSet for use with the solve method 345 @param exposure: The science exposure that will be convolved; must contain a Psf 346 @param referencePsfModel: Psf model to match to 349 -kernelCellSet: a SpatialCellSet to be used by self._solve 350 -referencePsfModel: Validated and/or modified reference model used to populate the SpatialCellSet 352 If the reference Psf model and science Psf model have different dimensions, 353 adjust the referencePsfModel (the model to which the exposure PSF will be matched) 354 to match that of the science Psf. If the science Psf dimensions vary across the image, 355 as is common with a WarpedPsf, either pad or clip (depending on config.padPsf) 356 the dimensions to be constant. 358 scienceBBox = exposure.getBBox()
359 sciencePsfModel = exposure.getPsf()
361 dimenR = referencePsfModel.getLocalKernel().getDimensions()
362 psfWidth, psfHeight = dimenR
364 regionSizeX, regionSizeY = scienceBBox.getDimensions()
365 scienceX0, scienceY0 = scienceBBox.getMin()
367 sizeCellX = self.kConfig.sizeCellX
368 sizeCellY = self.kConfig.sizeCellY
370 kernelCellSet = afwMath.SpatialCellSet(
371 afwGeom.Box2I(afwGeom.Point2I(scienceX0, scienceY0),
372 afwGeom.Extent2I(regionSizeX, regionSizeY)),
376 nCellX = regionSizeX//sizeCellX
377 nCellY = regionSizeY//sizeCellY
379 if nCellX == 0
or nCellY == 0:
380 raise ValueError(
"Exposure dimensions=%s and sizeCell=(%s, %s). Insufficient area to match" %
381 (scienceBBox.getDimensions(), sizeCellX, sizeCellY))
387 for row
in range(nCellY):
388 posY = sizeCellY*row + sizeCellY//2 + scienceY0
389 for col
in range(nCellX):
390 posX = sizeCellX*col + sizeCellX//2 + scienceX0
391 widthS, heightS = sciencePsfModel.computeBBox(afwGeom.Point2D(posX, posY)).getDimensions()
392 widthList.append(widthS)
393 heightList.append(heightS)
395 psfSize = max(max(heightList), max(widthList))
397 if self.config.doAutoPadPsf:
398 minPsfSize =
nextOddInteger(self.kConfig.kernelSize*self.config.autoPadPsfTo)
399 paddingPix = max(0, minPsfSize - psfSize)
401 if self.config.padPsfBy % 2 != 0:
402 raise ValueError(
"Config padPsfBy (%i pixels) must be even number." %
403 self.config.padPsfBy)
404 paddingPix = self.config.padPsfBy
407 self.log.info(
"Padding Science PSF from (%s, %s) to (%s, %s) pixels" %
408 (psfSize, psfSize, paddingPix + psfSize, paddingPix + psfSize))
409 psfSize += paddingPix
412 maxKernelSize = psfSize - 1
413 if maxKernelSize % 2 == 0:
415 if self.kConfig.kernelSize > maxKernelSize:
417 Kernel size (%d) too big to match Psfs of size %d. 418 Please reconfigure by setting one of the following: 419 1) kernel size to <= %d 422 """ % (self.kConfig.kernelSize, psfSize,
423 maxKernelSize, self.kConfig.kernelSize - maxKernelSize)
424 raise ValueError(message)
426 dimenS = afwGeom.Extent2I(psfSize, psfSize)
428 if (dimenR != dimenS):
430 referencePsfModel = referencePsfModel.resized(psfSize, psfSize)
431 self.log.info(
"Adjusted dimensions of reference PSF model from %s to %s" % (dimenR, dimenS))
432 except Exception
as e:
433 self.log.warn(
"Zero padding or clipping the reference PSF model of type %s and dimensions %s" 434 " to the science Psf dimensions %s because: %s",
435 referencePsfModel.__class__.__name__, dimenR, dimenS, e)
438 policy = pexConfig.makePolicy(self.kConfig)
439 for row
in range(nCellY):
441 posY = sizeCellY * row + sizeCellY//2 + scienceY0
443 for col
in range(nCellX):
445 posX = sizeCellX * col + sizeCellX//2 + scienceX0
447 log.log(
"TRACE4." + self.log.getName(), log.DEBUG,
448 "Creating Psf candidate at %.1f %.1f", posX, posY)
451 referenceMI = self._makePsfMaskedImage(referencePsfModel, posX, posY, dimensions=dimenR)
454 scienceMI = self._makePsfMaskedImage(sciencePsfModel, posX, posY, dimensions=dimenR)
457 kc = diffimLib.makeKernelCandidate(posX, posY, scienceMI, referenceMI, policy)
458 kernelCellSet.insertCandidate(kc)
461 display = lsstDebug.Info(__name__).display
462 displaySpatialCells = lsstDebug.Info(__name__).displaySpatialCells
463 maskTransparency = lsstDebug.Info(__name__).maskTransparency
464 if not maskTransparency:
467 ds9.setMaskTransparency(maskTransparency)
468 if display
and displaySpatialCells:
469 diUtils.showKernelSpatialCells(exposure.getMaskedImage(), kernelCellSet,
470 symb=
"o", ctype=ds9.CYAN, ctypeUnused=ds9.YELLOW, ctypeBad=ds9.RED,
471 size=4, frame=lsstDebug.frame, title=
"Image to be convolved")
473 return pipeBase.Struct(kernelCellSet=kernelCellSet,
474 referencePsfModel=referencePsfModel,
478 """! Return a MaskedImage of the a PSF Model of specified dimensions 480 rawKernel = psfModel.computeKernelImage(afwGeom.Point2D(posX, posY)).convertF()
481 if dimensions
is None:
482 dimensions = rawKernel.getDimensions()
483 if rawKernel.getDimensions() == dimensions:
487 kernelIm = afwImage.ImageF(dimensions)
488 bboxToPlace = afwGeom.Box2I(afwGeom.Point2I((dimensions.getX() - rawKernel.getWidth())//2,
489 (dimensions.getY() - rawKernel.getHeight())//2),
490 rawKernel.getDimensions())
491 kernelIm.assign(rawKernel, bboxToPlace)
493 kernelMask = afwImage.Mask(dimensions, 0x0)
494 kernelVar = afwImage.ImageF(dimensions, 1.0)
495 return afwImage.MaskedImageF(kernelIm, kernelMask, kernelVar)
Base class for Psf Matching; should not be called directly.
def makeKernelBasisList(config, targetFwhmPix=None, referenceFwhmPix=None, basisDegGauss=None, metadata=None)
def _buildCellSet(self, exposure, referencePsfModel)
Build a SpatialCellSet for use with the solve method.
def _solve(self, kernelCellSet, basisList, returnOnExcept=False)
Solve for the PSF matching kernel.
Matching of two model Psfs, and application of the Psf-matching kernel to an input Exposure...
Configuration for model-to-model Psf matching.
def run(self, exposure, referencePsfModel, kernelSum=1.0)
Psf-match an exposure to a model Psf.
def _diagnostic(self, kernelCellSet, spatialSolution, spatialKernel, spatialBg)
Print diagnostic information on spatial kernel and background fit.
def __init__(self, args, kwargs)
Create a ModelPsfMatchTask.
def _makePsfMaskedImage(self, psfModel, posX, posY, dimensions=None)
Return a MaskedImage of the a PSF Model of specified dimensions.