Coverage for python/lsst/ip/diffim/makeKernel.py: 20%
143 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-04 02:42 -0700
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-04 02:42 -0700
1# This file is part of ip_diffim.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22__all__ = ["MakeKernelConfig", "MakeKernelTask"]
24import numpy as np
26import lsst.afw.detection
27import lsst.afw.image
28import lsst.afw.math
29import lsst.afw.table
30import lsst.daf.base
31from lsst.meas.algorithms import SourceDetectionTask, SubtractBackgroundTask
32from lsst.meas.base import SingleFrameMeasurementTask
33from lsst.pex.exceptions import InvalidParameterError
34import lsst.pex.config
35import lsst.pipe.base
37from .makeKernelBasisList import makeKernelBasisList
38from .psfMatch import PsfMatchConfig, PsfMatchTask, PsfMatchConfigAL, PsfMatchConfigDF
40from . import diffimLib
41from . import diffimTools
42from .utils import evaluateMeanPsfFwhm, getPsfFwhm
45class MakeKernelConfig(PsfMatchConfig):
46 kernel = lsst.pex.config.ConfigChoiceField(
47 doc="kernel type",
48 typemap=dict(
49 AL=PsfMatchConfigAL,
50 DF=PsfMatchConfigDF
51 ),
52 default="AL",
53 )
54 selectDetection = lsst.pex.config.ConfigurableField(
55 target=SourceDetectionTask,
56 doc="Initial detections used to feed stars to kernel fitting",
57 )
58 selectMeasurement = lsst.pex.config.ConfigurableField(
59 target=SingleFrameMeasurementTask,
60 doc="Initial measurements used to feed stars to kernel fitting",
61 )
62 fwhmExposureGrid = lsst.pex.config.Field(
63 doc="Grid size to compute the average PSF FWHM in an exposure",
64 dtype=int,
65 default=10,
66 )
67 fwhmExposureBuffer = lsst.pex.config.Field(
68 doc="Fractional buffer margin to be left out of all sides of the image during construction"
69 "of grid to compute average PSF FWHM in an exposure",
70 dtype=float,
71 default=0.05,
72 )
74 def setDefaults(self):
75 # High sigma detections only
76 self.selectDetection.reEstimateBackground = False
77 self.selectDetection.thresholdValue = 10.0
79 # Minimal set of measurments for star selection
80 self.selectMeasurement.algorithms.names.clear()
81 self.selectMeasurement.algorithms.names = ('base_SdssCentroid', 'base_PsfFlux', 'base_PixelFlags',
82 'base_SdssShape', 'base_GaussianFlux', 'base_SkyCoord')
83 self.selectMeasurement.slots.modelFlux = None
84 self.selectMeasurement.slots.apFlux = None
85 self.selectMeasurement.slots.calibFlux = None
88class MakeKernelTask(PsfMatchTask):
89 """Construct a kernel for PSF matching two exposures.
90 """
92 ConfigClass = MakeKernelConfig
93 _DefaultName = "makeALKernel"
95 def __init__(self, *args, **kwargs):
96 PsfMatchTask.__init__(self, *args, **kwargs)
97 self.kConfig = self.config.kernel.active
98 # the background subtraction task uses a config from an unusual location,
99 # so cannot easily be constructed with makeSubtask
100 self.background = SubtractBackgroundTask(config=self.kConfig.afwBackgroundConfig, name="background",
101 parentTask=self)
102 self.selectSchema = lsst.afw.table.SourceTable.makeMinimalSchema()
103 self.selectAlgMetadata = lsst.daf.base.PropertyList()
104 self.makeSubtask("selectDetection", schema=self.selectSchema)
105 self.makeSubtask("selectMeasurement", schema=self.selectSchema, algMetadata=self.selectAlgMetadata)
107 def run(self, template, science, kernelSources, preconvolved=False):
108 """Solve for the kernel and background model that best match two
109 Exposures evaluated at the given source locations.
111 Parameters
112 ----------
113 template : `lsst.afw.image.Exposure`
114 Exposure that will be convolved.
115 science : `lsst.afw.image.Exposure`
116 The exposure that will be matched.
117 kernelSources : `list` of `dict`
118 A list of dicts having a "source" and "footprint"
119 field for the Sources deemed to be appropriate for Psf
120 matching. Can be the output from ``selectKernelSources``.
121 preconvolved : `bool`, optional
122 Was the science image convolved with its own PSF?
124 Returns
125 -------
126 results : `lsst.pipe.base.Struct`
128 ``psfMatchingKernel`` : `lsst.afw.math.LinearCombinationKernel`
129 Spatially varying Psf-matching kernel.
130 ``backgroundModel`` : `lsst.afw.math.Function2D`
131 Spatially varying background-matching function.
132 """
133 kernelCellSet = self._buildCellSet(template.maskedImage, science.maskedImage, kernelSources)
134 # Calling getPsfFwhm on template.psf fails on some rare occasions when
135 # the template has no input exposures at the average position of the
136 # stars. So we try getPsfFwhm first on template, and if that fails we
137 # evaluate the PSF on a grid specified by fwhmExposure* fields.
138 # To keep consistent definitions for PSF size on the template and
139 # science images, we use the same method for both.
140 try:
141 templateFwhmPix = getPsfFwhm(template.psf)
142 scienceFwhmPix = getPsfFwhm(science.psf)
143 except InvalidParameterError:
144 self.log.debug("Unable to evaluate PSF at the average position. "
145 "Evaluting PSF on a grid of points."
146 )
147 templateFwhmPix = evaluateMeanPsfFwhm(template,
148 fwhmExposureBuffer=self.config.fwhmExposureBuffer,
149 fwhmExposureGrid=self.config.fwhmExposureGrid
150 )
151 scienceFwhmPix = evaluateMeanPsfFwhm(science,
152 fwhmExposureBuffer=self.config.fwhmExposureBuffer,
153 fwhmExposureGrid=self.config.fwhmExposureGrid
154 )
156 if preconvolved:
157 scienceFwhmPix *= np.sqrt(2)
158 basisList = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix,
159 metadata=self.metadata)
160 spatialSolution, psfMatchingKernel, backgroundModel = self._solve(kernelCellSet, basisList)
161 return lsst.pipe.base.Struct(
162 psfMatchingKernel=psfMatchingKernel,
163 backgroundModel=backgroundModel,
164 )
166 def selectKernelSources(self, template, science, candidateList=None, preconvolved=False):
167 """Select sources from a list of candidates, and extract footprints.
169 Parameters
170 ----------
171 template : `lsst.afw.image.Exposure`
172 Exposure that will be convolved.
173 science : `lsst.afw.image.Exposure`
174 The exposure that will be matched.
175 candidateList : `list`, optional
176 List of Sources to examine. Elements must be of type afw.table.Source
177 or a type that wraps a Source and has a getSource() method, such as
178 meas.algorithms.PsfCandidateF.
179 preconvolved : `bool`, optional
180 Was the science image convolved with its own PSF?
182 Returns
183 -------
184 kernelSources : `list` of `dict`
185 A list of dicts having a "source" and "footprint"
186 field for the Sources deemed to be appropriate for Psf
187 matching.
188 """
189 # Calling getPsfFwhm on template.psf fails on some rare occasions when
190 # the template has no input exposures at the average position of the
191 # stars. So we try getPsfFwhm first on template, and if that fails we
192 # evaluate the PSF on a grid specified by fwhmExposure* fields.
193 # To keep consistent definitions for PSF size on the template and
194 # science images, we use the same method for both.
195 try:
196 templateFwhmPix = getPsfFwhm(template.psf)
197 scienceFwhmPix = getPsfFwhm(science.psf)
198 except InvalidParameterError:
199 self.log.debug("Unable to evaluate PSF at the average position. "
200 "Evaluting PSF on a grid of points."
201 )
202 templateFwhmPix = evaluateMeanPsfFwhm(template,
203 fwhmExposureBuffer=self.config.fwhmExposureBuffer,
204 fwhmExposureGrid=self.config.fwhmExposureGrid
205 )
206 scienceFwhmPix = evaluateMeanPsfFwhm(science,
207 fwhmExposureBuffer=self.config.fwhmExposureBuffer,
208 fwhmExposureGrid=self.config.fwhmExposureGrid
209 )
210 if preconvolved:
211 scienceFwhmPix *= np.sqrt(2)
212 kernelSize = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix)[0].getWidth()
213 kernelSources = self.makeCandidateList(template, science, kernelSize,
214 candidateList=candidateList,
215 preconvolved=preconvolved)
216 return kernelSources
218 def getSelectSources(self, exposure, sigma=None, doSmooth=True, idFactory=None):
219 """Get sources to use for Psf-matching.
221 This method runs detection and measurement on an exposure.
222 The returned set of sources will be used as candidates for
223 Psf-matching.
225 Parameters
226 ----------
227 exposure : `lsst.afw.image.Exposure`
228 Exposure on which to run detection/measurement
229 sigma : `float`, optional
230 PSF sigma, in pixels, used for smoothing the image for detection.
231 If `None`, the PSF width will be used.
232 doSmooth : `bool`
233 Whether or not to smooth the Exposure with Psf before detection
234 idFactory : `lsst.afw.table.IdFactory`
235 Factory for the generation of Source ids
237 Returns
238 -------
239 selectSources :
240 source catalog containing candidates for the Psf-matching
241 """
242 if idFactory:
243 table = lsst.afw.table.SourceTable.make(self.selectSchema, idFactory)
244 else:
245 table = lsst.afw.table.SourceTable.make(self.selectSchema)
246 mi = exposure.getMaskedImage()
248 imArr = mi.getImage().getArray()
249 maskArr = mi.getMask().getArray()
250 miArr = np.ma.masked_array(imArr, mask=maskArr)
251 try:
252 fitBg = self.background.fitBackground(mi)
253 bkgd = fitBg.getImageF(self.background.config.algorithm,
254 self.background.config.undersampleStyle)
255 except Exception:
256 self.log.warning("Failed to get background model. Falling back to median background estimation")
257 bkgd = np.ma.median(miArr)
259 # Take off background for detection
260 mi -= bkgd
261 try:
262 table.setMetadata(self.selectAlgMetadata)
263 detRet = self.selectDetection.run(
264 table=table,
265 exposure=exposure,
266 sigma=sigma,
267 doSmooth=doSmooth
268 )
269 selectSources = detRet.sources
270 self.selectMeasurement.run(measCat=selectSources, exposure=exposure)
271 finally:
272 # Put back on the background in case it is needed down stream
273 mi += bkgd
274 del bkgd
275 return selectSources
277 def makeCandidateList(self, templateExposure, scienceExposure, kernelSize,
278 candidateList=None, preconvolved=False):
279 """Make a list of acceptable KernelCandidates.
281 Accept or generate a list of candidate sources for
282 Psf-matching, and examine the Mask planes in both of the
283 images for indications of bad pixels
285 Parameters
286 ----------
287 templateExposure : `lsst.afw.image.Exposure`
288 Exposure that will be convolved
289 scienceExposure : `lsst.afw.image.Exposure`
290 Exposure that will be matched-to
291 kernelSize : `float`
292 Dimensions of the Psf-matching Kernel, used to grow detection footprints
293 candidateList : `list`, optional
294 List of Sources to examine. Elements must be of type afw.table.Source
295 or a type that wraps a Source and has a getSource() method, such as
296 meas.algorithms.PsfCandidateF.
297 preconvolved : `bool`, optional
298 Was the science exposure already convolved with its PSF?
300 Returns
301 -------
302 candidateList : `list` of `dict`
303 A list of dicts having a "source" and "footprint"
304 field for the Sources deemed to be appropriate for Psf
305 matching.
307 Raises
308 ------
309 RuntimeError
310 If ``candidateList`` is empty or contains incompatible types.
311 """
312 if candidateList is None:
313 candidateList = self.getSelectSources(scienceExposure, doSmooth=not preconvolved)
315 if len(candidateList) < 1:
316 raise RuntimeError("No candidates in candidateList")
318 listTypes = set(type(x) for x in candidateList)
319 if len(listTypes) > 1:
320 raise RuntimeError("Candidate list contains mixed types: %s" % [t for t in listTypes])
322 if not isinstance(candidateList[0], lsst.afw.table.SourceRecord):
323 try:
324 candidateList[0].getSource()
325 except Exception as e:
326 raise RuntimeError(f"Candidate List is of type: {type(candidateList[0])} "
327 "Can only make candidate list from list of afwTable.SourceRecords, "
328 f"measAlg.PsfCandidateF or other type with a getSource() method: {e}")
329 candidateList = [c.getSource() for c in candidateList]
331 candidateList = diffimTools.sourceToFootprintList(candidateList,
332 templateExposure, scienceExposure,
333 kernelSize,
334 self.kConfig.detectionConfig,
335 self.log)
336 if len(candidateList) == 0:
337 raise RuntimeError("Cannot find any objects suitable for KernelCandidacy")
339 return candidateList
341 def makeKernelBasisList(self, targetFwhmPix=None, referenceFwhmPix=None,
342 basisDegGauss=None, basisSigmaGauss=None, metadata=None):
343 """Wrapper to set log messages for
344 `lsst.ip.diffim.makeKernelBasisList`.
346 Parameters
347 ----------
348 targetFwhmPix : `float`, optional
349 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`.
350 Not used for delta function basis sets.
351 referenceFwhmPix : `float`, optional
352 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`.
353 Not used for delta function basis sets.
354 basisDegGauss : `list` of `int`, optional
355 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`.
356 Not used for delta function basis sets.
357 basisSigmaGauss : `list` of `int`, optional
358 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`.
359 Not used for delta function basis sets.
360 metadata : `lsst.daf.base.PropertySet`, optional
361 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`.
362 Not used for delta function basis sets.
364 Returns
365 -------
366 basisList: `list` of `lsst.afw.math.kernel.FixedKernel`
367 List of basis kernels.
368 """
369 basisList = makeKernelBasisList(self.kConfig,
370 targetFwhmPix=targetFwhmPix,
371 referenceFwhmPix=referenceFwhmPix,
372 basisDegGauss=basisDegGauss,
373 basisSigmaGauss=basisSigmaGauss,
374 metadata=metadata)
375 if targetFwhmPix == referenceFwhmPix:
376 self.log.info("Target and reference psf fwhms are equal, falling back to config values")
377 elif referenceFwhmPix > targetFwhmPix:
378 self.log.info("Reference psf fwhm is the greater, normal convolution mode")
379 else:
380 self.log.info("Target psf fwhm is the greater, deconvolution mode")
382 return basisList
384 def _buildCellSet(self, templateMaskedImage, scienceMaskedImage, candidateList):
385 """Build a SpatialCellSet for use with the solve method.
387 Parameters
388 ----------
389 templateMaskedImage : `lsst.afw.image.MaskedImage`
390 MaskedImage to PSF-matched to scienceMaskedImage
391 scienceMaskedImage : `lsst.afw.image.MaskedImage`
392 Reference MaskedImage
393 candidateList : `list`
394 A list of footprints/maskedImages for kernel candidates;
396 - Currently supported: list of Footprints or measAlg.PsfCandidateF
398 Returns
399 -------
400 kernelCellSet : `lsst.afw.math.SpatialCellSet`
401 a SpatialCellSet for use with self._solve
403 Raises
404 ------
405 RuntimeError
406 If no `candidateList` is supplied.
407 """
408 if not candidateList:
409 raise RuntimeError("Candidate list must be populated by makeCandidateList")
411 sizeCellX, sizeCellY = self._adaptCellSize(candidateList)
413 imageBBox = templateMaskedImage.getBBox()
414 imageBBox.clip(scienceMaskedImage.getBBox())
415 # Object to store the KernelCandidates for spatial modeling
416 kernelCellSet = lsst.afw.math.SpatialCellSet(imageBBox, sizeCellX, sizeCellY)
418 ps = lsst.pex.config.makePropertySet(self.kConfig)
419 # Place candidates within the spatial grid
420 for cand in candidateList:
421 if isinstance(cand, lsst.afw.detection.Footprint):
422 bbox = cand.getBBox()
423 else:
424 bbox = cand['footprint'].getBBox()
425 tmi = lsst.afw.image.MaskedImageF(templateMaskedImage, bbox)
426 smi = lsst.afw.image.MaskedImageF(scienceMaskedImage, bbox)
428 if not isinstance(cand, lsst.afw.detection.Footprint):
429 if 'source' in cand:
430 cand = cand['source']
431 xPos = cand.getCentroid()[0]
432 yPos = cand.getCentroid()[1]
433 cand = diffimLib.makeKernelCandidate(xPos, yPos, tmi, smi, ps)
435 self.log.debug("Candidate %d at %f, %f", cand.getId(), cand.getXCenter(), cand.getYCenter())
436 kernelCellSet.insertCandidate(cand)
438 return kernelCellSet
440 def _adaptCellSize(self, candidateList):
441 """NOT IMPLEMENTED YET.
443 Parameters
444 ----------
445 candidateList : `list`
446 A list of footprints/maskedImages for kernel candidates;
448 Returns
449 -------
450 sizeCellX, sizeCellY : `int`
451 New dimensions to use for the kernel.
452 """
453 return self.kConfig.sizeCellX, self.kConfig.sizeCellY