lsst.ip.diffim  14.0-7-g68ded9d+5
modelPsfMatch.py
Go to the documentation of this file.
1 # LSST Data Management System
2 # Copyright 2008-2016 LSST Corporation.
3 #
4 # This product includes software developed by the
5 # LSST Project (http://www.lsst.org/).
6 #
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the LSST License Statement and
18 # the GNU General Public License along with this program. If not,
19 # see <http://www.lsstcorp.org/LegalNotices/>.
20 #
21 from __future__ import absolute_import, division, print_function
22 
23 from builtins import range
24 import numpy as np
25 
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
37 
38 __all__ = ("ModelPsfMatchTask", "ModelPsfMatchConfig")
39 
40 sigma2fwhm = 2. * np.sqrt(2. * np.log(2.))
41 
42 
44  nextInt = int(np.ceil(x))
45  return nextInt + 1 if nextInt % 2 == 0 else nextInt
46 
47 
48 class ModelPsfMatchConfig(pexConfig.Config):
49  """!Configuration for model-to-model Psf matching"""
50 
51  kernel = pexConfig.ConfigChoiceField(
52  doc="kernel type",
53  typemap=dict(
54  AL=PsfMatchConfigAL,
55  ),
56  default="AL",
57  )
58  doAutoPadPsf = pexConfig.Field(
59  dtype=bool,
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."),
63  default=True,
64  )
65  autoPadPsfTo = pexConfig.RangeField(
66  dtype=float,
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."),
71  default=1.4,
72  min=1.0,
73  max=2.0
74  )
75  padPsfBy = pexConfig.Field(
76  dtype=int,
77  doc="Pixels (even) to pad Science Psf by before matching. Ignored if doAutoPadPsf=True",
78  default=0,
79  )
80 
81  def setDefaults(self):
82  # No sigma clipping
83  self.kernel.active.singleKernelClipping = False
84  self.kernel.active.kernelSumClipping = False
85  self.kernel.active.spatialKernelClipping = False
86  self.kernel.active.checkConditionNumber = False
87 
88  # Variance is ill defined
89  self.kernel.active.constantVarianceWeighting = True
90 
91  # Do not change specified kernel size
92  self.kernel.active.scaleByFwhm = False
93 
94 
95 
101 
102 
104  """!
105 \anchor ModelPsfMatchTask_
106 
107 \brief Matching of two model Psfs, and application of the Psf-matching kernel to an input Exposure
108 
109 \section ip_diffim_modelpsfmatch_Contents Contents
110 
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
118 
119 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
120 
121 \section ip_diffim_modelpsfmatch_Purpose Description
122 
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.
128 
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.
137 
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.
142 
143 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
144 
145 \section ip_diffim_modelpsfmatch_Initialize Task initialization
146 
147 \copydoc \_\_init\_\_
148 
149 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
150 
151 \section ip_diffim_modelpsfmatch_IO Invoking the Task
152 
153 \copydoc run
154 
155 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
156 
157 \section ip_diffim_modelpsfmatch_Config Configuration parameters
158 
159 See \ref ModelPsfMatchConfig
160 
161 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
162 
163 \section ip_diffim_modelpsfmatch_Metadata Quantities set in Metadata
164 
165 See \ref ip_diffim_psfmatch_Metadata "PsfMatchTask"
166 
167 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
168 
169 \section ip_diffim_modelpsfmatch_Debug Debug variables
170 
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:
174 
175 \code{.py}
176  import sys
177  import lsstDebug
178  def DebugInfo(name):
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
192  return di
193  lsstDebug.Info = DebugInfo
194  lsstDebug.frame = 1
195 \endcode
196 
197 Note that if you want addional logging info, you may add to your scripts:
198 \code{.py}
199 import lsst.log.utils as logUtils
200 logUtils.traceSetAt("ip.diffim", 4)
201 \endcode
202 
203 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
204 
205 \section ip_diffim_modelpsfmatch_Example A complete example of using ModelPsfMatchTask
206 
207 This code is modelPsfMatchTask.py in the examples directory, and can be run as \em e.g.
208 \code
209 examples/modelPsfMatchTask.py
210 examples/modelPsfMatchTask.py --debug
211 examples/modelPsfMatchTask.py --debug --template /path/to/templateExp.fits --science /path/to/scienceExp.fits
212 \endcode
213 
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
218 @until return
219 
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:
222 \skip main
223 @until parse_args
224 
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:
228 \skip args.debug
229 @until sys.stderr
230 
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).
235 \skip run(args)
236 @until False
237 
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):
241 \skip requested
242 @until sizeCellY
243 
244 Display the two images if --debug:
245 \skip args.debug
246 @until Science
247 
248 Create and run the Task:
249 \skip Create
250 @until result
251 
252 And finally provide optional debugging display of the Psf-matched (via the Psf models) science image:
253 \skip args.debug
254 @until result.psfMatchedExposure
255 
256 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
257 
258  """
259  ConfigClass = ModelPsfMatchConfig
260 
261  def __init__(self, *args, **kwargs):
262  """!Create a ModelPsfMatchTask
263 
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__
266 
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.
269  """
270  PsfMatchTask.__init__(self, *args, **kwargs)
271  self.kConfig = self.config.kernel.active
272 
273  @pipeBase.timeMethod
274  def run(self, exposure, referencePsfModel, kernelSum=1.0):
275  """!Psf-match an exposure to a model Psf
276 
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)
281 
282  @return
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
289 
290  Raise a RuntimeError if the Exposure does not contain a Psf model
291  """
292  if not exposure.hasPsf():
293  raise RuntimeError("exposure does not contain a Psf model")
294 
295  maskedImage = exposure.getMaskedImage()
296 
297  self.log.info("compute Psf-matching kernel")
298  result = self._buildCellSet(exposure, referencePsfModel)
299  kernelCellSet = result.kernelCellSet
300  referencePsfModel = result.referencePsfModel
301  fwhmScience = exposure.getPsf().computeShape().getDeterminantRadius() * sigma2fwhm
302  fwhmModel = referencePsfModel.computeShape().getDeterminantRadius() * sigma2fwhm
303 
304  basisList = makeKernelBasisList(self.kConfig, fwhmScience, fwhmModel, metadata=self.metadata)
305  spatialSolution, psfMatchingKernel, backgroundModel = self._solve(kernelCellSet, basisList)
306 
307  if psfMatchingKernel.isSpatiallyVarying():
308  sParameters = np.array(psfMatchingKernel.getSpatialParameters())
309  sParameters[0][0] = kernelSum
310  psfMatchingKernel.setSpatialParameters(sParameters)
311  else:
312  kParameters = np.array(psfMatchingKernel.getKernelParameters())
313  kParameters[0] = kernelSum
314  psfMatchingKernel.setKernelParameters(kParameters)
315 
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()
322 
323  # Normalize the psf-matching kernel while convolving since its magnitude is meaningless
324  # when PSF-matching one model to another.
325  doNormalize = True
326  afwMath.convolve(psfMatchedMaskedImage, maskedImage, psfMatchingKernel, doNormalize)
327 
328  self.log.info("done")
329  return pipeBase.Struct(psfMatchedExposure=psfMatchedExposure,
330  psfMatchingKernel=psfMatchingKernel,
331  kernelCellSet=kernelCellSet,
332  metadata=self.metadata,
333  )
334 
335  def _diagnostic(self, kernelCellSet, spatialSolution, spatialKernel, spatialBg):
336  """!Print diagnostic information on spatial kernel and background fit
337 
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"""
340  return
341 
342  def _buildCellSet(self, exposure, referencePsfModel):
343  """!Build a SpatialCellSet for use with the solve method
344 
345  @param exposure: The science exposure that will be convolved; must contain a Psf
346  @param referencePsfModel: Psf model to match to
347 
348  @return
349  -kernelCellSet: a SpatialCellSet to be used by self._solve
350  -referencePsfModel: Validated and/or modified reference model used to populate the SpatialCellSet
351 
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.
357  """
358  sizeCellX = self.kConfig.sizeCellX
359  sizeCellY = self.kConfig.sizeCellY
360 
361  scienceBBox = exposure.getBBox()
362  # Extend for proper spatial matching kernel all the way to edge, especially for narrow strips
363  scienceBBox.grow(afwGeom.Extent2I(sizeCellX, sizeCellY))
364 
365  sciencePsfModel = exposure.getPsf()
366 
367  dimenR = referencePsfModel.getLocalKernel().getDimensions()
368  psfWidth, psfHeight = dimenR
369 
370  regionSizeX, regionSizeY = scienceBBox.getDimensions()
371  scienceX0, scienceY0 = scienceBBox.getMin()
372 
373  kernelCellSet = afwMath.SpatialCellSet(afwGeom.Box2I(scienceBBox), sizeCellX, sizeCellY)
374 
375  nCellX = regionSizeX//sizeCellX
376  nCellY = regionSizeY//sizeCellY
377 
378  if nCellX == 0 or nCellY == 0:
379  raise ValueError("Exposure dimensions=%s and sizeCell=(%s, %s). Insufficient area to match" %
380  (scienceBBox.getDimensions(), sizeCellX, sizeCellY))
381 
382  # Survey the PSF dimensions of the Spatial Cell Set
383  # to identify the minimum enclosed or maximum bounding square BBox.
384  widthList = []
385  heightList = []
386  for row in range(nCellY):
387  posY = sizeCellY*row + sizeCellY//2 + scienceY0
388  for col in range(nCellX):
389  posX = sizeCellX*col + sizeCellX//2 + scienceX0
390  widthS, heightS = sciencePsfModel.computeBBox(afwGeom.Point2D(posX, posY)).getDimensions()
391  widthList.append(widthS)
392  heightList.append(heightS)
393 
394  psfSize = max(max(heightList), max(widthList))
395 
396  if self.config.doAutoPadPsf:
397  minPsfSize = nextOddInteger(self.kConfig.kernelSize*self.config.autoPadPsfTo)
398  paddingPix = max(0, minPsfSize - psfSize)
399  else:
400  if self.config.padPsfBy % 2 != 0:
401  raise ValueError("Config padPsfBy (%i pixels) must be even number." %
402  self.config.padPsfBy)
403  paddingPix = self.config.padPsfBy
404 
405  if paddingPix > 0:
406  self.log.info("Padding Science PSF from (%s, %s) to (%s, %s) pixels" %
407  (psfSize, psfSize, paddingPix + psfSize, paddingPix + psfSize))
408  psfSize += paddingPix
409 
410  # Check that PSF is larger than the matching kernel
411  maxKernelSize = psfSize - 1
412  if maxKernelSize % 2 == 0:
413  maxKernelSize -= 1
414  if self.kConfig.kernelSize > maxKernelSize:
415  message = """
416  Kernel size (%d) too big to match Psfs of size %d.
417  Please reconfigure by setting one of the following:
418  1) kernel size to <= %d
419  2) doAutoPadPsf=True
420  3) padPsfBy to >= %s
421  """ % (self.kConfig.kernelSize, psfSize,
422  maxKernelSize, self.kConfig.kernelSize - maxKernelSize)
423  raise ValueError(message)
424 
425  dimenS = afwGeom.Extent2I(psfSize, psfSize)
426 
427  if (dimenR != dimenS):
428  try:
429  referencePsfModel = referencePsfModel.resized(psfSize, psfSize)
430  self.log.info("Adjusted dimensions of reference PSF model from %s to %s" % (dimenR, dimenS))
431  except Exception as e:
432  self.log.warn("Zero padding or clipping the reference PSF model of type %s and dimensions %s"
433  " to the science Psf dimensions %s because: %s",
434  referencePsfModel.__class__.__name__, dimenR, dimenS, e)
435  dimenR = dimenS
436 
437  policy = pexConfig.makePolicy(self.kConfig)
438  for row in range(nCellY):
439  # place at center of cell
440  posY = sizeCellY * row + sizeCellY//2 + scienceY0
441 
442  for col in range(nCellX):
443  # place at center of cell
444  posX = sizeCellX * col + sizeCellX//2 + scienceX0
445 
446  log.log("TRACE4." + self.log.getName(), log.DEBUG,
447  "Creating Psf candidate at %.1f %.1f", posX, posY)
448 
449  # reference kernel image, at location of science subimage
450  referenceMI = self._makePsfMaskedImage(referencePsfModel, posX, posY, dimensions=dimenR)
451 
452  # kernel image we are going to convolve
453  scienceMI = self._makePsfMaskedImage(sciencePsfModel, posX, posY, dimensions=dimenR)
454 
455  # The image to convolve is the science image, to the reference Psf.
456  kc = diffimLib.makeKernelCandidate(posX, posY, scienceMI, referenceMI, policy)
457  kernelCellSet.insertCandidate(kc)
458 
459  import lsstDebug
460  display = lsstDebug.Info(__name__).display
461  displaySpatialCells = lsstDebug.Info(__name__).displaySpatialCells
462  maskTransparency = lsstDebug.Info(__name__).maskTransparency
463  if not maskTransparency:
464  maskTransparency = 0
465  if display:
466  ds9.setMaskTransparency(maskTransparency)
467  if display and displaySpatialCells:
468  diUtils.showKernelSpatialCells(exposure.getMaskedImage(), kernelCellSet,
469  symb="o", ctype=ds9.CYAN, ctypeUnused=ds9.YELLOW, ctypeBad=ds9.RED,
470  size=4, frame=lsstDebug.frame, title="Image to be convolved")
471  lsstDebug.frame += 1
472  return pipeBase.Struct(kernelCellSet=kernelCellSet,
473  referencePsfModel=referencePsfModel,
474  )
475 
476  def _makePsfMaskedImage(self, psfModel, posX, posY, dimensions=None):
477  """! Return a MaskedImage of the a PSF Model of specified dimensions
478  """
479  rawKernel = psfModel.computeKernelImage(afwGeom.Point2D(posX, posY)).convertF()
480  if dimensions is None:
481  dimensions = rawKernel.getDimensions()
482  if rawKernel.getDimensions() == dimensions:
483  kernelIm = rawKernel
484  else:
485  # make image of proper size
486  kernelIm = afwImage.ImageF(dimensions)
487  bboxToPlace = afwGeom.Box2I(afwGeom.Point2I((dimensions.getX() - rawKernel.getWidth())//2,
488  (dimensions.getY() - rawKernel.getHeight())//2),
489  rawKernel.getDimensions())
490  kernelIm.assign(rawKernel, bboxToPlace)
491 
492  kernelMask = afwImage.Mask(dimensions, 0x0)
493  kernelVar = afwImage.ImageF(dimensions, 1.0)
494  return afwImage.MaskedImageF(kernelIm, kernelMask, kernelVar)
Base class for Psf Matching; should not be called directly.
Definition: psfMatch.py:528
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.
Definition: psfMatch.py:894
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.
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, bool doNormalize, bool doCopyEdge=false)
def _makePsfMaskedImage(self, psfModel, posX, posY, dimensions=None)
Return a MaskedImage of the a PSF Model of specified dimensions.