lsst.ip.diffim  13.0-19-g373a351+1
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Macros Groups Pages
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.meas.algorithms as measAlg
32 import lsst.pex.config as pexConfig
33 import lsst.pipe.base as pipeBase
34 from .makeKernelBasisList import makeKernelBasisList
35 from .psfMatch import PsfMatchTask, PsfMatchConfigAL
36 from . import utils as diUtils
37 import lsst.afw.display.ds9 as ds9
38 
39 __all__ = ("ModelPsfMatchTask", "ModelPsfMatchConfig")
40 
41 sigma2fwhm = 2. * np.sqrt(2. * np.log(2.))
42 
43 
45  nextInt = int(np.ceil(x))
46  return nextInt + 1 if nextInt % 2 == 0 else nextInt
47 
48 
49 class ModelPsfMatchConfig(pexConfig.Config):
50  """!Configuration for model-to-model Psf matching"""
51 
52  kernel = pexConfig.ConfigChoiceField(
53  doc="kernel type",
54  typemap=dict(
55  AL=PsfMatchConfigAL,
56  ),
57  default="AL",
58  )
59  doAutoPadPsf = pexConfig.Field(
60  dtype=bool,
61  doc=("If too small, automatically pad the science Psf? "
62  "Pad to smallest dimensions appropriate for the matching kernel dimensions, "
63  "as specified by autoPadPsfTo. If false, pad by the padPsfBy config."),
64  default=True,
65  )
66  autoPadPsfTo = pexConfig.RangeField(
67  dtype=float,
68  doc=("Minimum Science Psf dimensions as a fraction of matching kernel dimensions. "
69  "If the dimensions of the Psf to be matched are less than the "
70  "matching kernel dimensions * autoPadPsfTo, pad Science Psf to this size. "
71  "Ignored if doAutoPadPsf=False."),
72  default=1.4,
73  min=1.0,
74  max=2.0
75  )
76  padPsfBy = pexConfig.Field(
77  dtype=int,
78  doc="Pixels (even) to pad Science Psf by before matching. Ignored if doAutoPadPsf=True",
79  default=0,
80  )
81 
82  def setDefaults(self):
83  # No sigma clipping
84  self.kernel.active.singleKernelClipping = False
85  self.kernel.active.kernelSumClipping = False
86  self.kernel.active.spatialKernelClipping = False
87  self.kernel.active.checkConditionNumber = False
88 
89  # Variance is ill defined
90  self.kernel.active.constantVarianceWeighting = True
91 
92  # Do not change specified kernel size
93  self.kernel.active.scaleByFwhm = False
94 
95 
96 ## \addtogroup LSST_task_documentation
97 ## \{
98 ## \page ModelPsfMatchTask
99 ## \ref ModelPsfMatchTask_ "ModelPsfMatchTask"
100 ## \copybrief ModelPsfMatchTask
101 ## \}
102 
103 
104 class ModelPsfMatchTask(PsfMatchTask):
105  """!
106 \anchor ModelPsfMatchTask_
107 
108 \brief Matching of two model Psfs, and application of the Psf-matching kernel to an input Exposure
109 
110 \section ip_diffim_modelpsfmatch_Contents Contents
111 
112  - \ref ip_diffim_modelpsfmatch_Purpose
113  - \ref ip_diffim_modelpsfmatch_Initialize
114  - \ref ip_diffim_modelpsfmatch_IO
115  - \ref ip_diffim_modelpsfmatch_Config
116  - \ref ip_diffim_modelpsfmatch_Metadata
117  - \ref ip_diffim_modelpsfmatch_Debug
118  - \ref ip_diffim_modelpsfmatch_Example
119 
120 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
121 
122 \section ip_diffim_modelpsfmatch_Purpose Description
123 
124 This Task differs from ImagePsfMatchTask in that it matches two Psf _models_, by realizing
125 them in an Exposure-sized SpatialCellSet and then inserting each Psf-image pair into KernelCandidates.
126 Because none of the pairs of sources that are to be matched should be invalid, all sigma clipping is
127 turned off in ModelPsfMatchConfig. And because there is no tracked _variance_ in the Psf images, the
128 debugging and logging QA info should be interpreted with caution.
129 
130 One item of note is that the sizes of Psf models are fixed (e.g. its defined as a 21x21 matrix). When the
131 Psf-matching kernel is being solved for, the Psf "image" is convolved with each kernel basis function,
132 leading to a loss of information around the borders. This pixel loss will be problematic for the numerical
133 stability of the kernel solution if the size of the convolution kernel (set by ModelPsfMatchConfig.kernelSize)
134 is much bigger than: psfSize//2. Thus the sizes of Psf-model matching kernels are typically smaller
135 than their image-matching counterparts. If the size of the kernel is too small, the convolved stars will
136 look "boxy"; if the kernel is too large, the kernel solution will be "noisy". This is a trade-off that
137 needs careful attention for a given dataset.
138 
139 The primary use case for this Task is in matching an Exposure to a constant-across-the-sky Psf model for the
140 purposes of image coaddition. It is important to note that in the code, the "template" Psf is the Psf
141 that the science image gets matched to. In this sense the order of template and science image are
142 reversed, compared to ImagePsfMatchTask, which operates on the template image.
143 
144 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
145 
146 \section ip_diffim_modelpsfmatch_Initialize Task initialization
147 
148 \copydoc \_\_init\_\_
149 
150 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
151 
152 \section ip_diffim_modelpsfmatch_IO Invoking the Task
153 
154 \copydoc run
155 
156 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
157 
158 \section ip_diffim_modelpsfmatch_Config Configuration parameters
159 
160 See \ref ModelPsfMatchConfig
161 
162 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
163 
164 \section ip_diffim_modelpsfmatch_Metadata Quantities set in Metadata
165 
166 See \ref ip_diffim_psfmatch_Metadata "PsfMatchTask"
167 
168 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
169 
170 \section ip_diffim_modelpsfmatch_Debug Debug variables
171 
172 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
173 flag \c -d/--debug to import \b debug.py from your \c PYTHONPATH. The relevant contents of debug.py
174 for this Task include:
175 
176 \code{.py}
177  import sys
178  import lsstDebug
179  def DebugInfo(name):
180  di = lsstDebug.getInfo(name)
181  if name == "lsst.ip.diffim.psfMatch":
182  di.display = True # global
183  di.maskTransparency = 80 # ds9 mask transparency
184  di.displayCandidates = True # show all the candidates and residuals
185  di.displayKernelBasis = False # show kernel basis functions
186  di.displayKernelMosaic = True # show kernel realized across the image
187  di.plotKernelSpatialModel = False # show coefficients of spatial model
188  di.showBadCandidates = True # show the bad candidates (red) along with good (green)
189  elif name == "lsst.ip.diffim.modelPsfMatch":
190  di.display = True # global
191  di.maskTransparency = 30 # ds9 mask transparency
192  di.displaySpatialCells = True # show spatial cells before the fit
193  return di
194  lsstDebug.Info = DebugInfo
195  lsstDebug.frame = 1
196 \endcode
197 
198 Note that if you want addional logging info, you may add to your scripts:
199 \code{.py}
200 import lsst.log.utils as logUtils
201 logUtils.traceSetAt("ip.diffim", 4)
202 \endcode
203 
204 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
205 
206 \section ip_diffim_modelpsfmatch_Example A complete example of using ModelPsfMatchTask
207 
208 This code is modelPsfMatchTask.py in the examples directory, and can be run as \em e.g.
209 \code
210 examples/modelPsfMatchTask.py
211 examples/modelPsfMatchTask.py --debug
212 examples/modelPsfMatchTask.py --debug --template /path/to/templateExp.fits --science /path/to/scienceExp.fits
213 \endcode
214 
215 \dontinclude modelPsfMatchTask.py
216 Create a subclass of ModelPsfMatchTask that accepts two exposures. Note that the "template" exposure
217 contains the Psf that will get matched to, and the "science" exposure is the one that will be convolved:
218 \skip MyModelPsfMatchTask
219 @until return
220 
221 And allow the user the freedom to either run the script in default mode, or point to their own images on disk.
222 Note that these images must be readable as an lsst.afw.image.Exposure:
223 \skip main
224 @until parse_args
225 
226 We have enabled some minor display debugging in this script via the --debug option. However, if you
227 have an lsstDebug debug.py in your PYTHONPATH you will get additional debugging displays. The following
228 block checks for this script:
229 \skip args.debug
230 @until sys.stderr
231 
232 \dontinclude modelPsfMatchTask.py
233 Finally, we call a run method that we define below. First set up a Config and modify some of the parameters.
234 In particular we don't want to "grow" the sizes of the kernel or KernelCandidates, since we are operating with
235 fixed--size images (i.e. the size of the input Psf models).
236 \skip run(args)
237 @until False
238 
239 Make sure the images (if any) that were sent to the script exist on disk and are readable. If no images
240 are sent, make some fake data up for the sake of this example script (have a look at the code if you want
241 more details on generateFakeData):
242 \skip requested
243 @until sizeCellY
244 
245 Display the two images if --debug:
246 \skip args.debug
247 @until Science
248 
249 Create and run the Task:
250 \skip Create
251 @until result
252 
253 And finally provide optional debugging display of the Psf-matched (via the Psf models) science image:
254 \skip args.debug
255 @until result.psfMatchedExposure
256 
257 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
258 
259  """
260  ConfigClass = ModelPsfMatchConfig
261 
262  def __init__(self, *args, **kwargs):
263  """!Create a ModelPsfMatchTask
264 
265  \param *args arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__
266  \param **kwargs keyword arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__
267 
268  Upon initialization, the kernel configuration is defined by self.config.kernel.active. This Task
269  does have a run() method, which is the default way to call the Task.
270  """
271  PsfMatchTask.__init__(self, *args, **kwargs)
272  self.kConfig = self.config.kernel.active
273 
274  @pipeBase.timeMethod
275  def run(self, exposure, referencePsfModel, kernelSum=1.0):
276  """!Psf-match an exposure to a model Psf
277 
278  @param exposure: Exposure to Psf-match to the reference Psf model;
279  it must return a valid PSF model via exposure.getPsf()
280  @param referencePsfModel: The Psf model to match to (an lsst.afw.detection.Psf)
281  @param kernelSum: A multipicative factor to apply to the kernel sum (default=1.0)
282 
283  @return
284  - psfMatchedExposure: the Psf-matched Exposure. This has the same parent bbox, Wcs, Calib and
285  Filter as the input Exposure but no Psf. In theory the Psf should equal referencePsfModel but
286  the match is likely not exact.
287  - psfMatchingKernel: the spatially varying Psf-matching kernel
288  - kernelCellSet: SpatialCellSet used to solve for the Psf-matching kernel
289  - referencePsfModel: Validated and/or modified reference model used
290 
291  Raise a RuntimeError if the Exposure does not contain a Psf model
292  """
293  if not exposure.hasPsf():
294  raise RuntimeError("exposure does not contain a Psf model")
295 
296  maskedImage = exposure.getMaskedImage()
297 
298  self.log.info("compute Psf-matching kernel")
299  result = self._buildCellSet(exposure, referencePsfModel)
300  kernelCellSet = result.kernelCellSet
301  referencePsfModel = result.referencePsfModel
302  width, height = referencePsfModel.getLocalKernel().getDimensions()
303  psfAttr1 = measAlg.PsfAttributes(exposure.getPsf(), width//2, height//2)
304  psfAttr2 = measAlg.PsfAttributes(referencePsfModel, width//2, height//2)
305  s1 = psfAttr1.computeGaussianWidth(psfAttr1.ADAPTIVE_MOMENT) # gaussian sigma in pixels
306  s2 = psfAttr2.computeGaussianWidth(psfAttr2.ADAPTIVE_MOMENT) # gaussian sigma in pixels
307  fwhm1 = s1 * sigma2fwhm # science Psf
308  fwhm2 = s2 * sigma2fwhm # template Psf
309 
310  basisList = makeKernelBasisList(self.kConfig, fwhm1, fwhm2, metadata=self.metadata)
311  spatialSolution, psfMatchingKernel, backgroundModel = self._solve(kernelCellSet, basisList)
312 
313  if psfMatchingKernel.isSpatiallyVarying():
314  sParameters = np.array(psfMatchingKernel.getSpatialParameters())
315  sParameters[0][0] = kernelSum
316  psfMatchingKernel.setSpatialParameters(sParameters)
317  else:
318  kParameters = np.array(psfMatchingKernel.getKernelParameters())
319  kParameters[0] = kernelSum
320  psfMatchingKernel.setKernelParameters(kParameters)
321 
322  self.log.info("Psf-match science exposure to reference")
323  psfMatchedExposure = afwImage.ExposureF(exposure.getBBox(), exposure.getWcs())
324  psfMatchedExposure.setFilter(exposure.getFilter())
325  psfMatchedExposure.setCalib(exposure.getCalib())
326  psfMatchedExposure.setPsf(referencePsfModel)
327  psfMatchedMaskedImage = psfMatchedExposure.getMaskedImage()
328 
329  # Normalize the psf-matching kernel while convolving since its magnitude is meaningless
330  # when PSF-matching one model to another.
331  doNormalize = True
332  afwMath.convolve(psfMatchedMaskedImage, maskedImage, psfMatchingKernel, doNormalize)
333 
334  self.log.info("done")
335  return pipeBase.Struct(psfMatchedExposure=psfMatchedExposure,
336  psfMatchingKernel=psfMatchingKernel,
337  kernelCellSet=kernelCellSet,
338  metadata=self.metadata,
339  )
340 
341  def _diagnostic(self, kernelCellSet, spatialSolution, spatialKernel, spatialBg):
342  """!Print diagnostic information on spatial kernel and background fit
343 
344  The debugging diagnostics are not really useful here, since the images we are matching have
345  no variance. Thus override the _diagnostic method to generate no logging information"""
346  return
347 
348  def _buildCellSet(self, exposure, referencePsfModel):
349  """!Build a SpatialCellSet for use with the solve method
350 
351  @param exposure: The science exposure that will be convolved; must contain a Psf
352  @param referencePsfModel: Psf model to match to
353 
354  @return
355  -kernelCellSet: a SpatialCellSet to be used by self._solve
356  -referencePsfModel: Validated and/or modified reference model used to populate the SpatialCellSet
357 
358  If the reference Psf model and science Psf model have different dimensions,
359  adjust the referencePsfModel (the model to which the exposure PSF will be matched)
360  to match that of the science Psf. If the science Psf dimensions vary across the image,
361  as is common with a WarpedPsf, either pad or clip (depending on config.padPsf)
362  the dimensions to be constant.
363  """
364  scienceBBox = exposure.getBBox()
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  sizeCellX = self.kConfig.sizeCellX
374  sizeCellY = self.kConfig.sizeCellY
375 
376  kernelCellSet = afwMath.SpatialCellSet(
377  afwGeom.Box2I(afwGeom.Point2I(scienceX0, scienceY0),
378  afwGeom.Extent2I(regionSizeX, regionSizeY)),
379  sizeCellX, sizeCellY
380  )
381 
382  nCellX = regionSizeX//sizeCellX
383  nCellY = regionSizeY//sizeCellY
384 
385  if nCellX == 0 or nCellY == 0:
386  raise ValueError("Exposure dimensions=%s and sizeCell=(%s, %s). Insufficient area to match" %
387  (scienceBBox.getDimensions(), sizeCellX, sizeCellY))
388 
389  # Survey the PSF dimensions of the Spatial Cell Set
390  # to identify the minimum enclosed or maximum bounding square BBox.
391  widthList = []
392  heightList = []
393  for row in range(nCellY):
394  posY = sizeCellY*row + sizeCellY//2 + scienceY0
395  for col in range(nCellX):
396  posX = sizeCellX*col + sizeCellX//2 + scienceX0
397  widthS, heightS = sciencePsfModel.computeBBox(afwGeom.Point2D(posX, posY)).getDimensions()
398  widthList.append(widthS)
399  heightList.append(heightS)
400 
401  psfSize = max(max(heightList), max(widthList))
402 
403  if self.config.doAutoPadPsf:
404  minPsfSize = nextOddInteger(self.kConfig.kernelSize*self.config.autoPadPsfTo)
405  paddingPix = max(0, minPsfSize - psfSize)
406  else:
407  if self.config.padPsfBy % 2 != 0:
408  raise ValueError("Config padPsfBy (%i pixels) must be even number." %
409  self.config.padPsfBy)
410  paddingPix = self.config.padPsfBy
411 
412  if paddingPix > 0:
413  self.log.info("Padding Science PSF from (%s, %s) to (%s, %s) pixels" %
414  (psfSize, psfSize, paddingPix + psfSize, paddingPix + psfSize))
415  psfSize += paddingPix
416 
417  # Check that PSF is larger than the matching kernel
418  maxKernelSize = psfSize - 1
419  if maxKernelSize % 2 == 0:
420  maxKernelSize -= 1
421  if self.kConfig.kernelSize > maxKernelSize:
422  message = """
423  Kernel size (%d) too big to match Psfs of size %d.
424  Please reconfigure by setting one of the following:
425  1) kernel size to <= %d
426  2) doAutoPadPsf=True
427  3) padPsfBy to >= %s
428  """ % (self.kConfig.kernelSize, psfSize,
429  maxKernelSize, self.kConfig.kernelSize - maxKernelSize)
430  raise ValueError(message)
431 
432  dimenS = afwGeom.Extent2I(psfSize, psfSize)
433 
434  if (dimenR != dimenS):
435  self.log.info("Adjusting dimensions of reference PSF model from %s to %s" % (dimenR, dimenS))
436  referencePsfModel = referencePsfModel.resized(psfSize, psfSize)
437  dimenR = dimenS
438 
439  policy = pexConfig.makePolicy(self.kConfig)
440  for row in range(nCellY):
441  # place at center of cell
442  posY = sizeCellY * row + sizeCellY//2 + scienceY0
443 
444  for col in range(nCellX):
445  # place at center of cell
446  posX = sizeCellX * col + sizeCellX//2 + scienceX0
447 
448  log.log("TRACE4." + self.log.getName(), log.DEBUG,
449  "Creating Psf candidate at %.1f %.1f", posX, posY)
450 
451  # reference kernel image, at location of science subimage
452  kernelImageR = referencePsfModel.computeImage(afwGeom.Point2D(posX, posY)).convertF()
453  kernelMaskR = afwImage.MaskU(dimenR)
454  kernelMaskR.set(0)
455  kernelVarR = afwImage.ImageF(dimenR)
456  kernelVarR.set(1.0)
457  referenceMI = afwImage.MaskedImageF(kernelImageR, kernelMaskR, kernelVarR)
458 
459  # kernel image we are going to convolve
460  rawKernel = sciencePsfModel.computeKernelImage(afwGeom.Point2D(posX, posY)).convertF()
461  if rawKernel.getDimensions() == dimenR:
462  kernelImageS = rawKernel
463  else:
464  # make image of proper size
465  kernelImageS = afwImage.ImageF(dimenR)
466  bboxToPlace = afwGeom.Box2I(afwGeom.Point2I((psfSize - rawKernel.getWidth())//2,
467  (psfSize - rawKernel.getHeight())//2),
468  rawKernel.getDimensions())
469  kernelImageS.assign(rawKernel, bboxToPlace)
470 
471  kernelMaskS = afwImage.MaskU(dimenS)
472  kernelMaskS.set(0)
473  kernelVarS = afwImage.ImageF(dimenS)
474  kernelVarS.set(1.0)
475  scienceMI = afwImage.MaskedImageF(kernelImageS, kernelMaskS, kernelVarS)
476 
477  # The image to convolve is the science image, to the reference Psf.
478  kc = diffimLib.makeKernelCandidate(posX, posY, scienceMI, referenceMI, policy)
479  kernelCellSet.insertCandidate(kc)
480 
481  import lsstDebug
482  display = lsstDebug.Info(__name__).display
483  displaySpatialCells = lsstDebug.Info(__name__).displaySpatialCells
484  maskTransparency = lsstDebug.Info(__name__).maskTransparency
485  if not maskTransparency:
486  maskTransparency = 0
487  if display:
488  ds9.setMaskTransparency(maskTransparency)
489  if display and displaySpatialCells:
490  diUtils.showKernelSpatialCells(exposure.getMaskedImage(), kernelCellSet,
491  symb="o", ctype=ds9.CYAN, ctypeUnused=ds9.YELLOW, ctypeBad=ds9.RED,
492  size=4, frame=lsstDebug.frame, title="Image to be convolved")
493  lsstDebug.frame += 1
494  return pipeBase.Struct(kernelCellSet=kernelCellSet,
495  referencePsfModel=referencePsfModel,
496  )
def _diagnostic
Print diagnostic information on spatial kernel and background fit.
def __init__
Create a ModelPsfMatchTask.
Matching of two model Psfs, and application of the Psf-matching kernel to an input Exposure...
Configuration for model-to-model Psf matching.
def _buildCellSet
Build a SpatialCellSet for use with the solve method.
def run
Psf-match an exposure to a model Psf.