LSST Applications g04e9c324dd+8c5ae1fdc5,g134cb467dc+1b3060144d,g18429d2f64+f642bf4753,g199a45376c+0ba108daf9,g1fd858c14a+2dcf163641,g262e1987ae+7b8c96d2ca,g29ae962dfc+3bd6ecb08a,g2cef7863aa+aef1011c0b,g35bb328faa+8c5ae1fdc5,g3fd5ace14f+53e1a9e7c5,g4595892280+fef73a337f,g47891489e3+2efcf17695,g4d44eb3520+642b70b07e,g53246c7159+8c5ae1fdc5,g67b6fd64d1+2efcf17695,g67fd3c3899+b70e05ef52,g74acd417e5+317eb4c7d4,g786e29fd12+668abc6043,g87389fa792+8856018cbb,g89139ef638+2efcf17695,g8d7436a09f+3be3c13596,g8ea07a8fe4+9f5ccc88ac,g90f42f885a+a4e7b16d9b,g97be763408+ad77d7208f,g9dd6db0277+b70e05ef52,ga681d05dcb+a3f46e7fff,gabf8522325+735880ea63,gac2eed3f23+2efcf17695,gb89ab40317+2efcf17695,gbf99507273+8c5ae1fdc5,gd8ff7fe66e+b70e05ef52,gdab6d2f7ff+317eb4c7d4,gdc713202bf+b70e05ef52,gdfd2d52018+b10e285e0f,ge365c994fd+310e8507c4,ge410e46f29+2efcf17695,geaed405ab2+562b3308c0,gffca2db377+8c5ae1fdc5,w.2025.35
LSST Data Management Base Package
Loading...
Searching...
No Matches
makeKernel.py
Go to the documentation of this file.
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/>.
21
22__all__ = ["MakeKernelConfig", "MakeKernelTask"]
23
24import numpy as np
25
27import lsst.afw.image
28import lsst.afw.math
29import lsst.afw.table
30import lsst.daf.base
31import lsst.geom
32from lsst.meas.algorithms import SourceDetectionTask, SubtractBackgroundTask
33from lsst.meas.base import SingleFrameMeasurementTask
34from lsst.pex.exceptions import InvalidParameterError, RangeError
35import lsst.pex.config
36import lsst.pipe.base
37
38from .makeKernelBasisList import makeKernelBasisList
39from .psfMatch import PsfMatchConfig, PsfMatchTask, PsfMatchConfigAL, PsfMatchConfigDF
40
41from . import diffimLib
42from .utils import evaluateMeanPsfFwhm, getPsfFwhm
43
44
47 doc="kernel type",
48 typemap=dict(
49 AL=PsfMatchConfigAL,
50 DF=PsfMatchConfigDF
51 ),
52 default="AL",
53 )
55 target=SourceDetectionTask,
56 doc="Initial detections used to feed stars to kernel fitting",
57 )
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 )
73
74 def setDefaults(self):
75 # High sigma detections only
76 self.selectDetection.reEstimateBackground = False
77 self.selectDetection.thresholdValue = 10.0
78 self.selectDetection.excludeMaskPlanes = ["EDGE"]
79
80 # Minimal set of measurments for star selection
81 self.selectMeasurement.algorithms.names.clear()
82 self.selectMeasurement.algorithms.names = ('base_SdssCentroid', 'base_PsfFlux', 'base_PixelFlags',
83 'base_SdssShape', 'base_GaussianFlux', 'base_SkyCoord',
84 'base_ClassificationSizeExtendedness')
85 self.selectMeasurement.slots.modelFlux = None
86 self.selectMeasurement.slots.apFlux = None
87 self.selectMeasurement.slots.calibFlux = None
88
89
91 """Construct a kernel for PSF matching two exposures.
92 """
93
94 ConfigClass = MakeKernelConfig
95 _DefaultName = "makeALKernel"
96
97 def __init__(self, *args, **kwargs):
98 super().__init__(*args, **kwargs)
99 # the background subtraction task uses a config from an unusual location,
100 # so cannot easily be constructed with makeSubtask
101 self.background = SubtractBackgroundTask(config=self.kConfig.afwBackgroundConfig, name="background",
102 parentTask=self)
103
106 self.makeSubtask("selectDetection", schema=self.selectSchema)
107 self.makeSubtask("selectMeasurement", schema=self.selectSchema, algMetadata=self.selectAlgMetadata)
108
109 def run(self, template, science, kernelSources, preconvolved=False,
110 templateFwhmPix=None, scienceFwhmPix=None):
111 """Solve for the kernel and background model that best match two
112 Exposures evaluated at the given source locations.
113
114 Parameters
115 ----------
116 template : `lsst.afw.image.Exposure`
117 Exposure that will be convolved.
118 science : `lsst.afw.image.Exposure`
119 The exposure that will be matched.
120 kernelSources : `lsst.afw.table.SourceCatalog`
121 Kernel candidate sources with appropriately sized footprints.
122 Typically the output of `MakeKernelTask.selectKernelSources`.
123 preconvolved : `bool`, optional
124 Was the science image convolved with its own PSF?
125
126 Returns
127 -------
128 results : `lsst.pipe.base.Struct`
129
130 ``psfMatchingKernel`` : `lsst.afw.math.LinearCombinationKernel`
131 Spatially varying Psf-matching kernel.
132 ``backgroundModel`` : `lsst.afw.math.Function2D`
133 Spatially varying background-matching function.
134 """
135 kernelCellSet = self._buildCellSet(template.maskedImage, science.maskedImage, kernelSources)
136 if (scienceFwhmPix is None) or (templateFwhmPix is None):
137 # Calling getPsfFwhm on template.psf fails on some rare occasions when
138 # the template has no input exposures at the average position of the
139 # stars. So we try getPsfFwhm first on template, and if that fails we
140 # evaluate the PSF on a grid specified by fwhmExposure* fields.
141 # To keep consistent definitions for PSF size on the template and
142 # science images, we use the same method for both.
143 try:
144 templateFwhmPix = getPsfFwhm(template.psf)
145 scienceFwhmPix = getPsfFwhm(science.psf)
146 except (InvalidParameterError, RangeError):
147 self.log.debug("Unable to evaluate PSF at the average position. "
148 "Evaluting PSF on a grid of points."
149 )
150 templateFwhmPix = evaluateMeanPsfFwhm(template,
151 fwhmExposureBuffer=self.config.fwhmExposureBuffer,
152 fwhmExposureGrid=self.config.fwhmExposureGrid
153 )
154 scienceFwhmPix = evaluateMeanPsfFwhm(science,
155 fwhmExposureBuffer=self.config.fwhmExposureBuffer,
156 fwhmExposureGrid=self.config.fwhmExposureGrid
157 )
158
159 if preconvolved:
160 scienceFwhmPix *= np.sqrt(2)
161 basisList = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix,
162 metadata=self.metadata)
163 spatialSolution, psfMatchingKernel, backgroundModel = self._solve(kernelCellSet, basisList)
164 return lsst.pipe.base.Struct(
165 psfMatchingKernel=psfMatchingKernel,
166 backgroundModel=backgroundModel,
167 )
168
169 def selectKernelSources(self, template, science, candidateList=None, preconvolved=False,
170 templateFwhmPix=None, scienceFwhmPix=None):
171 """Select sources from a list of candidates, and extract footprints.
172
173 Parameters
174 ----------
175 template : `lsst.afw.image.Exposure`
176 Exposure that will be convolved.
177 science : `lsst.afw.image.Exposure`
178 The exposure that will be matched.
179 candidateList : `lsst.afw.table.SourceCatalog`
180 Sources to check as possible kernel candidates.
181 preconvolved : `bool`, optional
182 Was the science image convolved with its own PSF?
183
184 Returns
185 -------
186 kernelSources : `lsst.afw.table.SourceCatalog`
187 Kernel candidates with appropriate sized footprints.
188 """
189 if (scienceFwhmPix is None) or (templateFwhmPix is None):
190 # Calling getPsfFwhm on template.psf fails on some rare occasions when
191 # the template has no input exposures at the average position of the
192 # stars. So we try getPsfFwhm first on template, and if that fails we
193 # evaluate the PSF on a grid specified by fwhmExposure* fields.
194 # To keep consistent definitions for PSF size on the template and
195 # science images, we use the same method for both.
196 try:
197 templateFwhmPix = getPsfFwhm(template.psf)
198 scienceFwhmPix = getPsfFwhm(science.psf)
199 except (InvalidParameterError, RangeError):
200 self.log.debug("Unable to evaluate PSF at the average position. "
201 "Evaluting PSF on a grid of points."
202 )
203 templateFwhmPix = evaluateMeanPsfFwhm(template,
204 fwhmExposureBuffer=self.config.fwhmExposureBuffer,
205 fwhmExposureGrid=self.config.fwhmExposureGrid
206 )
207 scienceFwhmPix = evaluateMeanPsfFwhm(science,
208 fwhmExposureBuffer=self.config.fwhmExposureBuffer,
209 fwhmExposureGrid=self.config.fwhmExposureGrid
210 )
211 if preconvolved:
212 scienceFwhmPix *= np.sqrt(2)
213 kernelSize = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix)[0].getWidth()
214 kernelSources = self.makeCandidateList(template, science, kernelSize,
215 candidateList=candidateList,
216 preconvolved=preconvolved)
217 return kernelSources
218
219 def getSelectSources(self, exposure, sigma=None, doSmooth=True, idFactory=None):
220 """Get sources to use for Psf-matching.
221
222 This method runs detection and measurement on an exposure.
223 The returned set of sources will be used as candidates for
224 Psf-matching.
225
226 Parameters
227 ----------
228 exposure : `lsst.afw.image.Exposure`
229 Exposure on which to run detection/measurement
230 sigma : `float`, optional
231 PSF sigma, in pixels, used for smoothing the image for detection.
232 If `None`, the PSF width will be used.
233 doSmooth : `bool`
234 Whether or not to smooth the Exposure with Psf before detection
235 idFactory : `lsst.afw.table.IdFactory`
236 Factory for the generation of Source ids
237
238 Returns
239 -------
240 selectSources :
241 source catalog containing candidates for the Psf-matching
242 """
243 if idFactory:
244 table = lsst.afw.table.SourceTable.make(self.selectSchema, idFactory)
245 else:
247 mi = exposure.getMaskedImage()
248
249 imArr = mi.image.array
250 maskArr = mi.mask.array
251 miArr = np.ma.masked_array(imArr, mask=maskArr)
252 try:
253 fitBg = self.background.fitBackground(mi)
254 bkgd = fitBg.getImageF(self.background.config.algorithm,
255 self.background.config.undersampleStyle)
256 except Exception:
257 self.log.warning("Failed to get background model. Falling back to median background estimation")
258 bkgd = np.ma.median(miArr)
259
260 # Take off background for detection
261 mi -= bkgd
262 try:
263 table.setMetadata(self.selectAlgMetadata)
264 detRet = self.selectDetection.run(
265 table=table,
266 exposure=exposure,
267 sigma=sigma,
268 doSmooth=doSmooth
269 )
270 selectSources = detRet.sources
271 self.selectMeasurement.run(measCat=selectSources, exposure=exposure)
272 finally:
273 # Put back on the background in case it is needed down stream
274 mi += bkgd
275 del bkgd
276
277 self.log.info("Selected %d sources via detection measurement.", len(selectSources))
278 return selectSources
279
280 def makeCandidateList(self, convolved, reference, kernelSize,
281 candidateList, preconvolved=False, sigma=None):
282 """Make a list of acceptable KernelCandidates.
283
284 Generate a list of candidate sources for Psf-matching, remove sources
285 with bad pixel masks set or that extend off the image.
286
287 Parameters
288 ----------
289 convolved : `lsst.afw.image.Exposure`
290 Exposure that will be convolved. This is typically the template
291 image, and may have a large bbox than the reference exposure.
292 reference : `lsst.afw.image.Exposure`
293 Exposure that will be matched-to. This is typically the science
294 image.
295 kernelSize : `float`
296 Dimensions of the Psf-matching Kernel, used to set detection
297 footprints.
298 candidateList : `lsst.afw.table.SourceCatalog`
299 List of Sources to examine for kernel candidacy.
300 preconvolved : `bool`, optional
301 Was the science exposure already convolved with its PSF?
302
303 Returns
304 -------
305 candidates : `lsst.afw.table.SourceCatalog`
306 Candidates with footprints extended to a ``kernelSize`` box.
307
308 Raises
309 ------
310 RuntimeError
311 If ``candidateList`` is empty after sub-selection.
312 """
313 if candidateList is None:
314 candidateList = self.getSelectSources(reference, doSmooth=not preconvolved, sigma=sigma)
315 if len(candidateList) < 1:
316 raise RuntimeError("No kernel candidates after detection and measurement.")
317
318 bitmask = reference.mask.getPlaneBitMask(self.config.badMaskPlanes)
319 good = np.ones(len(candidateList), dtype=bool)
320 # Make all candidates have the same size footprint, based on kernelSize.
321 for i, candidate in enumerate(candidateList):
322 # Only use the brightest peak; the input should be pre-deblended!
323 peak = candidate.getFootprint().getPeaks()[0]
324 size = 2*kernelSize + 1 # ensure the resulting box is odd
325 bbox = lsst.geom.Box2I.makeCenteredBox(candidate.getCentroid(),
326 lsst.geom.Extent2I(size, size))
328 boxFootprint.addPeak(peak.getFx(), peak.getFy(), peak.getPeakValue())
329 candidate.setFootprint(boxFootprint)
330
331 # Reject footprints not contained in either image.
332 if not reference.getBBox().contains(bbox) or not convolved.getBBox().contains(bbox):
333 good[i] = False
334 continue
335 # Reject footprints with any bad mask bits set.
336 if (reference.subset(bbox).mask.array & bitmask).any():
337 good[i] = False
338 continue
339
340 # Reject footprints with any bad mask bits set.
341 if (convolved.subset(bbox).mask.array & bitmask).any():
342 good[i] = False
343 continue
344 candidates = candidateList[good].copy(deep=True)
345
346 self.log.info("Selected %d / %d sources as kernel candidates.", good.sum(), len(candidateList))
347
348 if len(candidates) < 1:
349 raise RuntimeError("No good kernel candidates available.")
350
351 return candidates
352
353 def makeKernelBasisList(self, targetFwhmPix=None, referenceFwhmPix=None,
354 basisDegGauss=None, basisSigmaGauss=None, metadata=None):
355 """Wrapper to set log messages for
356 `lsst.ip.diffim.makeKernelBasisList`.
357
358 Parameters
359 ----------
360 targetFwhmPix : `float`, optional
361 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`.
362 Not used for delta function basis sets.
363 referenceFwhmPix : `float`, optional
364 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`.
365 Not used for delta function basis sets.
366 basisDegGauss : `list` of `int`, optional
367 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`.
368 Not used for delta function basis sets.
369 basisSigmaGauss : `list` of `int`, optional
370 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`.
371 Not used for delta function basis sets.
372 metadata : `lsst.daf.base.PropertySet`, optional
373 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`.
374 Not used for delta function basis sets.
375
376 Returns
377 -------
378 basisList: `list` of `lsst.afw.math.kernel.FixedKernel`
379 List of basis kernels.
380 """
381 basisList = makeKernelBasisList(self.kConfig,
382 targetFwhmPix=targetFwhmPix,
383 referenceFwhmPix=referenceFwhmPix,
384 basisDegGauss=basisDegGauss,
385 basisSigmaGauss=basisSigmaGauss,
386 metadata=metadata)
387 if targetFwhmPix == referenceFwhmPix:
388 self.log.info("Target and reference psf fwhms are equal, falling back to config values")
389 elif referenceFwhmPix > targetFwhmPix:
390 self.log.info("Reference psf fwhm is the greater, normal convolution mode")
391 else:
392 self.log.info("Target psf fwhm is the greater, deconvolution mode")
393
394 return basisList
395
396 def _buildCellSet(self, convolved, reference, candidateList):
397 """Build a SpatialCellSet for use with the solve method.
398
399 Parameters
400 ----------
401 convolved : `lsst.afw.image.MaskedImage`
402 MaskedImage to PSF-matched to reference.
403 reference : `lsst.afw.image.MaskedImage`
404 Reference MaskedImage.
405 candidateList : `lsst.afw.table.SourceCatalog`
406 Kernel candidate sources with footprints.
407
408 Returns
409 -------
410 kernelCellSet : `lsst.afw.math.SpatialCellSet`
411 A SpatialCellSet for use with self._solve.
412 """
413 sizeCellX, sizeCellY = self._adaptCellSize(candidateList)
414
415 imageBBox = convolved.getBBox()
416 imageBBox.clip(reference.getBBox())
417 # Object to store the KernelCandidates for spatial modeling
418 kernelCellSet = lsst.afw.math.SpatialCellSet(imageBBox, sizeCellX, sizeCellY)
419
420 candidateConfig = lsst.pex.config.makePropertySet(self.kConfig)
421 # Place candidates within the spatial grid
422 for candidate in candidateList:
423 bbox = candidate.getFootprint().getBBox()
424 templateCutout = lsst.afw.image.MaskedImageF(convolved, bbox)
425 scienceCutout = lsst.afw.image.MaskedImageF(reference, bbox)
426
427 kernelCandidate = diffimLib.makeKernelCandidate(candidate,
428 templateCutout,
429 scienceCutout,
430 candidateConfig)
431
432 self.log.debug("Candidate %d at %.2f, %.2f rating=%f",
433 kernelCandidate.getId(),
434 kernelCandidate.getXCenter(),
435 kernelCandidate.getYCenter(),
436 kernelCandidate.getCandidateRating())
437 kernelCellSet.insertCandidate(kernelCandidate)
438
439 return kernelCellSet
440
441 def _adaptCellSize(self, candidateList):
442 """NOT IMPLEMENTED YET.
443
444 Parameters
445 ----------
446 candidateList : `list`
447 A list of footprints/maskedImages for kernel candidates;
448
449 Returns
450 -------
451 sizeCellX, sizeCellY : `int`
452 New dimensions to use for the kernel.
453 """
454 return self.kConfig.sizeCellX, self.kConfig.sizeCellY
Class to describe the properties of a detected object from an image.
Definition Footprint.h:63
A compact representation of a collection of pixels.
Definition SpanSet.h:78
A collection of SpatialCells covering an entire image.
static std::shared_ptr< SourceTable > make(Schema const &schema, std::shared_ptr< IdFactory > const &idFactory)
Construct a new table.
Definition Source.cc:400
static Schema makeMinimalSchema()
Return a minimal schema for Source tables and records.
Definition Source.h:258
Class for storing ordered metadata with comments.
static Box2I makeCenteredBox(Point2D const &center, Extent const &size)
Create a box centered as closely as possible on a particular point.
Definition Box.cc:97
makeCandidateList(self, convolved, reference, kernelSize, candidateList, preconvolved=False, sigma=None)
run(self, template, science, kernelSources, preconvolved=False, templateFwhmPix=None, scienceFwhmPix=None)
_buildCellSet(self, convolved, reference, candidateList)
makeKernelBasisList(self, targetFwhmPix=None, referenceFwhmPix=None, basisDegGauss=None, basisSigmaGauss=None, metadata=None)
selectKernelSources(self, template, science, candidateList=None, preconvolved=False, templateFwhmPix=None, scienceFwhmPix=None)
getSelectSources(self, exposure, sigma=None, doSmooth=True, idFactory=None)
_solve(self, kernelCellSet, basisList)
Definition psfMatch.py:797