LSST Applications g063fba187b+cac8b7c890,g0f08755f38+6aee506743,g1653933729+a8ce1bb630,g168dd56ebc+a8ce1bb630,g1a2382251a+b4475c5878,g1dcb35cd9c+8f9bc1652e,g20f6ffc8e0+6aee506743,g217e2c1bcf+73dee94bd0,g28da252d5a+1f19c529b9,g2bbee38e9b+3f2625acfc,g2bc492864f+3f2625acfc,g3156d2b45e+6e55a43351,g32e5bea42b+1bb94961c2,g347aa1857d+3f2625acfc,g35bb328faa+a8ce1bb630,g3a166c0a6a+3f2625acfc,g3e281a1b8c+c5dd892a6c,g3e8969e208+a8ce1bb630,g414038480c+5927e1bc1e,g41af890bb2+8a9e676b2a,g7af13505b9+809c143d88,g80478fca09+6ef8b1810f,g82479be7b0+f568feb641,g858d7b2824+6aee506743,g89c8672015+f4add4ffd5,g9125e01d80+a8ce1bb630,ga5288a1d22+2903d499ea,gb58c049af0+d64f4d3760,gc28159a63d+3f2625acfc,gcab2d0539d+b12535109e,gcf0d15dbbd+46a3f46ba9,gda6a2b7d83+46a3f46ba9,gdaeeff99f8+1711a396fd,ge79ae78c31+3f2625acfc,gef2f8181fd+0a71e47438,gf0baf85859+c1f95f4921,gfa517265be+6aee506743,gfa999e8aa5+17cd334064,w.2024.51
LSST Data Management Base Package
Loading...
Searching...
No Matches
psfMatch.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__ = ["PsfMatchConfig", "PsfMatchConfigAL", "PsfMatchConfigDF", "PsfMatchTask"]
23
24import abc
25import time
26
27import numpy as np
28
29import lsst.afw.image as afwImage
30import lsst.pex.config as pexConfig
31import lsst.afw.math as afwMath
32import lsst.afw.display as afwDisplay
33import lsst.pipe.base as pipeBase
34from lsst.meas.algorithms import SubtractBackgroundConfig
35from lsst.utils.logging import getTraceLogger
36from lsst.utils.timer import timeMethod
37from . import utils as diutils
38from . import diffimLib
39
40
41class PsfMatchConfig(pexConfig.Config):
42 """Base configuration for Psf-matching
43
44 The base configuration of the Psf-matching kernel, and of the warping, detection,
45 and background modeling subTasks."""
46
47 warpingConfig = pexConfig.ConfigField("Config for warping exposures to a common alignment",
49 afwBackgroundConfig = pexConfig.ConfigField("Controlling the Afw background fitting",
50 SubtractBackgroundConfig)
51
52 useAfwBackground = pexConfig.Field(
53 dtype=bool,
54 doc="Use afw background subtraction instead of ip_diffim",
55 default=False,
56 )
57 fitForBackground = pexConfig.Field(
58 dtype=bool,
59 doc="Include terms (including kernel cross terms) for background in ip_diffim",
60 default=False,
61 )
62 kernelBasisSet = pexConfig.ChoiceField(
63 dtype=str,
64 doc="Type of basis set for PSF matching kernel.",
65 default="alard-lupton",
66 allowed={
67 "alard-lupton": """Alard-Lupton sum-of-gaussians basis set,
68 * The first term has no spatial variation
69 * The kernel sum is conserved
70 * You may want to turn off 'usePcaForSpatialKernel'""",
71 "delta-function": """Delta-function kernel basis set,
72 * You may enable the option useRegularization
73 * You should seriously consider usePcaForSpatialKernel, which will also
74 enable kernel sum conservation for the delta function kernels"""
75 }
76 )
77 kernelSize = pexConfig.Field(
78 dtype=int,
79 doc="""Number of rows/columns in the convolution kernel; should be odd-valued.
80 Modified by kernelSizeFwhmScaling if scaleByFwhm = true""",
81 default=21,
82 )
83 scaleByFwhm = pexConfig.Field(
84 dtype=bool,
85 doc="Scale kernelSize, alardGaussians by input Fwhm",
86 default=True,
87 )
88 kernelSizeFwhmScaling = pexConfig.Field(
89 dtype=float,
90 doc="Multiplier of the largest AL Gaussian basis sigma to get the kernel bbox (pixel) size.",
91 default=6.0,
92 check=lambda x: x >= 1.0
93 )
94 kernelSizeMin = pexConfig.Field(
95 dtype=int,
96 doc="Minimum kernel bbox (pixel) size.",
97 default=21,
98 )
99 kernelSizeMax = pexConfig.Field(
100 dtype=int,
101 doc="Maximum kernel bbox (pixel) size.",
102 default=35,
103 )
104 spatialModelType = pexConfig.ChoiceField(
105 dtype=str,
106 doc="Type of spatial functions for kernel and background",
107 default="chebyshev1",
108 allowed={
109 "chebyshev1": "Chebyshev polynomial of the first kind",
110 "polynomial": "Standard x,y polynomial",
111 }
112 )
113 spatialKernelOrder = pexConfig.Field(
114 dtype=int,
115 doc="Spatial order of convolution kernel variation",
116 default=2,
117 check=lambda x: x >= 0
118 )
119 spatialBgOrder = pexConfig.Field(
120 dtype=int,
121 doc="Spatial order of differential background variation",
122 default=1,
123 check=lambda x: x >= 0
124 )
125 sizeCellX = pexConfig.Field(
126 dtype=int,
127 doc="Size (rows) in pixels of each SpatialCell for spatial modeling",
128 default=128,
129 check=lambda x: x >= 32
130 )
131 sizeCellY = pexConfig.Field(
132 dtype=int,
133 doc="Size (columns) in pixels of each SpatialCell for spatial modeling",
134 default=128,
135 check=lambda x: x >= 32
136 )
137 nStarPerCell = pexConfig.Field(
138 dtype=int,
139 doc="Maximum number of KernelCandidates in each SpatialCell to use in the spatial fitting. "
140 "Set to -1 to use all candidates in each cell.",
141 default=5,
142 )
143 maxSpatialIterations = pexConfig.Field(
144 dtype=int,
145 doc="Maximum number of iterations for rejecting bad KernelCandidates in spatial fitting",
146 default=3,
147 check=lambda x: x >= 1 and x <= 5
148 )
149 usePcaForSpatialKernel = pexConfig.Field(
150 dtype=bool,
151 doc="""Use Pca to reduce the dimensionality of the kernel basis sets.
152 This is particularly useful for delta-function kernels.
153 Functionally, after all Cells have their raw kernels determined, we run
154 a Pca on these Kernels, re-fit the Cells using the eigenKernels and then
155 fit those for spatial variation using the same technique as for Alard-Lupton kernels.
156 If this option is used, the first term will have no spatial variation and the
157 kernel sum will be conserved.""",
158 default=False,
159 )
160 subtractMeanForPca = pexConfig.Field(
161 dtype=bool,
162 doc="Subtract off the mean feature before doing the Pca",
163 default=True,
164 )
165 numPrincipalComponents = pexConfig.Field(
166 dtype=int,
167 doc="""Number of principal components to use for Pca basis, including the
168 mean kernel if requested.""",
169 default=5,
170 check=lambda x: x >= 3
171 )
172 singleKernelClipping = pexConfig.Field(
173 dtype=bool,
174 doc="Do sigma clipping on each raw kernel candidate",
175 default=True,
176 )
177 kernelSumClipping = pexConfig.Field(
178 dtype=bool,
179 doc="Do sigma clipping on the ensemble of kernel sums",
180 default=True,
181 )
182 spatialKernelClipping = pexConfig.Field(
183 dtype=bool,
184 doc="Do sigma clipping after building the spatial model",
185 default=True,
186 )
187 checkConditionNumber = pexConfig.Field(
188 dtype=bool,
189 doc="""Test for maximum condition number when inverting a kernel matrix.
190 Anything above maxConditionNumber is not used and the candidate is set as BAD.
191 Also used to truncate inverse matrix in estimateBiasedRisk. However,
192 if you are doing any deconvolution you will want to turn this off, or use
193 a large maxConditionNumber""",
194 default=False,
195 )
196 badMaskPlanes = pexConfig.ListField(
197 dtype=str,
198 doc="""Mask planes to ignore when calculating diffim statistics
199 Options: NO_DATA EDGE SAT BAD CR INTRP""",
200 default=("NO_DATA", "EDGE", "SAT")
201 )
202 candidateResidualMeanMax = pexConfig.Field(
203 dtype=float,
204 doc="""Rejects KernelCandidates yielding bad difference image quality.
205 Used by BuildSingleKernelVisitor, AssessSpatialKernelVisitor.
206 Represents average over pixels of (image/sqrt(variance)).""",
207 default=0.25,
208 check=lambda x: x >= 0.0
209 )
210 candidateResidualStdMax = pexConfig.Field(
211 dtype=float,
212 doc="""Rejects KernelCandidates yielding bad difference image quality.
213 Used by BuildSingleKernelVisitor, AssessSpatialKernelVisitor.
214 Represents stddev over pixels of (image/sqrt(variance)).""",
215 default=1.50,
216 check=lambda x: x >= 0.0
217 )
218 useCoreStats = pexConfig.Field(
219 dtype=bool,
220 doc="""Use the core of the footprint for the quality statistics, instead of the entire footprint.
221 WARNING: if there is deconvolution we probably will need to turn this off""",
222 default=False,
223 )
224 candidateCoreRadius = pexConfig.Field(
225 dtype=int,
226 doc="""Radius for calculation of stats in 'core' of KernelCandidate diffim.
227 Total number of pixels used will be (2*radius)**2.
228 This is used both for 'core' diffim quality as well as ranking of
229 KernelCandidates by their total flux in this core""",
230 default=3,
231 check=lambda x: x >= 1
232 )
233 maxKsumSigma = pexConfig.Field(
234 dtype=float,
235 doc="""Maximum allowed sigma for outliers from kernel sum distribution.
236 Used to reject variable objects from the kernel model""",
237 default=3.0,
238 check=lambda x: x >= 0.0
239 )
240 maxConditionNumber = pexConfig.Field(
241 dtype=float,
242 doc="Maximum condition number for a well conditioned matrix",
243 default=5.0e7,
244 check=lambda x: x >= 0.0
245 )
246 conditionNumberType = pexConfig.ChoiceField(
247 dtype=str,
248 doc="Use singular values (SVD) or eigen values (EIGENVALUE) to determine condition number",
249 default="EIGENVALUE",
250 allowed={
251 "SVD": "Use singular values",
252 "EIGENVALUE": "Use eigen values (faster)",
253 }
254 )
255 maxSpatialConditionNumber = pexConfig.Field(
256 dtype=float,
257 doc="Maximum condition number for a well conditioned spatial matrix",
258 default=1.0e10,
259 check=lambda x: x >= 0.0
260 )
261 iterateSingleKernel = pexConfig.Field(
262 dtype=bool,
263 doc="""Remake KernelCandidate using better variance estimate after first pass?
264 Primarily useful when convolving a single-depth image, otherwise not necessary.""",
265 default=False,
266 )
267 constantVarianceWeighting = pexConfig.Field(
268 dtype=bool,
269 doc="""Use constant variance weighting in single kernel fitting?
270 In some cases this is better for bright star residuals.""",
271 default=True,
272 )
273 calculateKernelUncertainty = pexConfig.Field(
274 dtype=bool,
275 doc="""Calculate kernel and background uncertainties for each kernel candidate?
276 This comes from the inverse of the covariance matrix.
277 Warning: regularization can cause problems for this step.""",
278 default=False,
279 )
280 useBicForKernelBasis = pexConfig.Field(
281 dtype=bool,
282 doc="""Use Bayesian Information Criterion to select the number of bases going into the kernel""",
283 default=False,
284 )
285
286
287class PsfMatchConfigAL(PsfMatchConfig):
288 """The parameters specific to the "Alard-Lupton" (sum-of-Gaussian) Psf-matching basis"""
289
290 def setDefaults(self):
291 PsfMatchConfig.setDefaults(self)
292 self.kernelBasisSet = "alard-lupton"
293 self.maxConditionNumber = 5.0e7
294
295 alardNGauss = pexConfig.Field(
296 dtype=int,
297 doc="Number of base Gaussians in alard-lupton kernel basis function generation.",
298 default=3,
299 check=lambda x: x >= 1
300 )
301 alardDegGauss = pexConfig.ListField(
302 dtype=int,
303 doc="Polynomial order of spatial modification of base Gaussians. "
304 "List length must be `alardNGauss`.",
305 default=(4, 2, 2),
306 )
307 alardSigGauss = pexConfig.ListField(
308 dtype=float,
309 doc="Default sigma values in pixels of base Gaussians. "
310 "List length must be `alardNGauss`.",
311 default=(0.7, 1.5, 3.0),
312 )
313 alardGaussBeta = pexConfig.Field(
314 dtype=float,
315 doc="Used if `scaleByFwhm==True`, scaling multiplier of base "
316 "Gaussian sigmas for automated sigma determination",
317 default=2.0,
318 check=lambda x: x >= 0.0,
319 )
320 alardMinSig = pexConfig.Field(
321 dtype=float,
322 doc="Used if `scaleByFwhm==True`, minimum sigma (pixels) for base Gaussians",
323 default=0.7,
324 check=lambda x: x >= 0.25
325 )
326 alardDegGaussDeconv = pexConfig.Field(
327 dtype=int,
328 doc="Used if `scaleByFwhm==True`, degree of spatial modification of ALL base Gaussians "
329 "in AL basis during deconvolution",
330 default=3,
331 check=lambda x: x >= 1
332 )
333 alardMinSigDeconv = pexConfig.Field(
334 dtype=float,
335 doc="Used if `scaleByFwhm==True`, minimum sigma (pixels) for base Gaussians during deconvolution; "
336 "make smaller than `alardMinSig` as this is only indirectly used",
337 default=0.4,
338 check=lambda x: x >= 0.25
339 )
340 alardNGaussDeconv = pexConfig.Field(
341 dtype=int,
342 doc="Used if `scaleByFwhm==True`, number of base Gaussians in AL basis during deconvolution",
343 default=3,
344 check=lambda x: x >= 1
345 )
346
347
348class PsfMatchConfigDF(PsfMatchConfig):
349 """The parameters specific to the delta-function (one basis per-pixel) Psf-matching basis"""
350
351 def setDefaults(self):
352 PsfMatchConfig.setDefaults(self)
353 self.kernelBasisSet = "delta-function"
354 self.maxConditionNumber = 5.0e6
355 self.usePcaForSpatialKernel = True
356 self.subtractMeanForPca = True
357 self.useBicForKernelBasis = False
358
359 useRegularization = pexConfig.Field(
360 dtype=bool,
361 doc="Use regularization to smooth the delta function kernels",
362 default=True,
363 )
364 regularizationType = pexConfig.ChoiceField(
365 dtype=str,
366 doc="Type of regularization.",
367 default="centralDifference",
368 allowed={
369 "centralDifference": "Penalize second derivative using 2-D stencil of central finite difference",
370 "forwardDifference": "Penalize first, second, third derivatives using forward finite differeces"
371 }
372 )
373 centralRegularizationStencil = pexConfig.ChoiceField(
374 dtype=int,
375 doc="Type of stencil to approximate central derivative (for centralDifference only)",
376 default=9,
377 allowed={
378 5: "5-point stencil including only adjacent-in-x,y elements",
379 9: "9-point stencil including diagonal elements"
380 }
381 )
382 forwardRegularizationOrders = pexConfig.ListField(
383 dtype=int,
384 doc="Array showing which order derivatives to penalize (for forwardDifference only)",
385 default=(1, 2),
386 itemCheck=lambda x: (x > 0) and (x < 4)
387 )
388 regularizationBorderPenalty = pexConfig.Field(
389 dtype=float,
390 doc="Value of the penalty for kernel border pixels",
391 default=3.0,
392 check=lambda x: x >= 0.0
393 )
394 lambdaType = pexConfig.ChoiceField(
395 dtype=str,
396 doc="How to choose the value of the regularization strength",
397 default="absolute",
398 allowed={
399 "absolute": "Use lambdaValue as the value of regularization strength",
400 "relative": "Use lambdaValue as fraction of the default regularization strength (N.R. 18.5.8)",
401 "minimizeBiasedRisk": "Minimize biased risk estimate",
402 "minimizeUnbiasedRisk": "Minimize unbiased risk estimate",
403 }
404 )
405 lambdaValue = pexConfig.Field(
406 dtype=float,
407 doc="Value used for absolute determinations of regularization strength",
408 default=0.2,
409 )
410 lambdaScaling = pexConfig.Field(
411 dtype=float,
412 doc="Fraction of the default lambda strength (N.R. 18.5.8) to use. 1e-4 or 1e-5",
413 default=1e-4,
414 )
415 lambdaStepType = pexConfig.ChoiceField(
416 dtype=str,
417 doc="""If a scan through lambda is needed (minimizeBiasedRisk, minimizeUnbiasedRisk),
418 use log or linear steps""",
419 default="log",
420 allowed={
421 "log": "Step in log intervals; e.g. lambdaMin, lambdaMax, lambdaStep = -1.0, 2.0, 0.1",
422 "linear": "Step in linear intervals; e.g. lambdaMin, lambdaMax, lambdaStep = 0.1, 100, 0.1",
423 }
424 )
425 lambdaMin = pexConfig.Field(
426 dtype=float,
427 doc="""If scan through lambda needed (minimizeBiasedRisk, minimizeUnbiasedRisk),
428 start at this value. If lambdaStepType = log:linear, suggest -1:0.1""",
429 default=-1.0,
430 )
431 lambdaMax = pexConfig.Field(
432 dtype=float,
433 doc="""If scan through lambda needed (minimizeBiasedRisk, minimizeUnbiasedRisk),
434 stop at this value. If lambdaStepType = log:linear, suggest 2:100""",
435 default=2.0,
436 )
437 lambdaStep = pexConfig.Field(
438 dtype=float,
439 doc="""If scan through lambda needed (minimizeBiasedRisk, minimizeUnbiasedRisk),
440 step in these increments. If lambdaStepType = log:linear, suggest 0.1:0.1""",
441 default=0.1,
442 )
443
444
445class PsfMatchTask(pipeBase.Task, abc.ABC):
446 """Base class for Psf Matching; should not be called directly
447
448 Notes
449 -----
450 PsfMatchTask is a base class that implements the core functionality for matching the
451 Psfs of two images using a spatially varying Psf-matching `lsst.afw.math.LinearCombinationKernel`.
452 The Task requires the user to provide an instance of an `lsst.afw.math.SpatialCellSet`,
453 filled with `lsst.ip.diffim.KernelCandidate` instances, and a list of `lsst.afw.math.Kernels`
454 of basis shapes that will be used for the decomposition. If requested, the Task
455 also performs background matching and returns the differential background model as an
456 `lsst.afw.math.Kernel.SpatialFunction`.
457
458 **Invoking the Task**
459
460 As a base class, this Task is not directly invoked. However, ``run()`` methods that are
461 implemented on derived classes will make use of the core ``_solve()`` functionality,
462 which defines a sequence of `lsst.afw.math.CandidateVisitor` classes that iterate
463 through the KernelCandidates, first building up a per-candidate solution and then
464 building up a spatial model from the ensemble of candidates. Sigma clipping is
465 performed using the mean and standard deviation of all kernel sums (to reject
466 variable objects), on the per-candidate substamp diffim residuals
467 (to indicate a bad choice of kernel basis shapes for that particular object),
468 and on the substamp diffim residuals using the spatial kernel fit (to indicate a bad
469 choice of spatial kernel order, or poor constraints on the spatial model). The
470 ``_diagnostic()`` method logs information on the quality of the spatial fit, and also
471 modifies the Task metadata.
472
473 .. list-table:: Quantities set in Metadata
474 :header-rows: 1
475
476 * - Parameter
477 - Description
478 * - ``spatialConditionNum``
479 - Condition number of the spatial kernel fit
480 * - ``spatialKernelSum``
481 - Kernel sum (10^{-0.4 * ``Delta``; zeropoint}) of the spatial Psf-matching kernel
482 * - ``ALBasisNGauss``
483 - If using sum-of-Gaussian basis, the number of gaussians used
484 * - ``ALBasisDegGauss``
485 - If using sum-of-Gaussian basis, the deg of spatial variation of the Gaussians
486 * - ``ALBasisSigGauss``
487 - If using sum-of-Gaussian basis, the widths (sigma) of the Gaussians
488 * - ``ALKernelSize``
489 - If using sum-of-Gaussian basis, the kernel size
490 * - ``NFalsePositivesTotal``
491 - Total number of diaSources
492 * - ``NFalsePositivesRefAssociated``
493 - Number of diaSources that associate with the reference catalog
494 * - ``NFalsePositivesRefAssociated``
495 - Number of diaSources that associate with the source catalog
496 * - ``NFalsePositivesUnassociated``
497 - Number of diaSources that are orphans
498 * - ``metric_MEAN``
499 - Mean value of substamp diffim quality metrics across all KernelCandidates,
500 for both the per-candidate (LOCAL) and SPATIAL residuals
501 * - ``metric_MEDIAN``
502 - Median value of substamp diffim quality metrics across all KernelCandidates,
503 for both the per-candidate (LOCAL) and SPATIAL residuals
504 * - ``metric_STDEV``
505 - Standard deviation of substamp diffim quality metrics across all KernelCandidates,
506 for both the per-candidate (LOCAL) and SPATIAL residuals
507
508 **Debug variables**
509
510 The ``pipetask`` command line interface supports a
511 flag --debug to import @b debug.py from your PYTHONPATH. The relevant contents of debug.py
512 for this Task include:
513
514 .. code-block:: py
515
516 import sys
517 import lsstDebug
518 def DebugInfo(name):
519 di = lsstDebug.getInfo(name)
520 if name == "lsst.ip.diffim.psfMatch":
521 # enable debug output
522 di.display = True
523 # display mask transparency
524 di.maskTransparency = 80
525 # show all the candidates and residuals
526 di.displayCandidates = True
527 # show kernel basis functions
528 di.displayKernelBasis = False
529 # show kernel realized across the image
530 di.displayKernelMosaic = True
531 # show coefficients of spatial model
532 di.plotKernelSpatialModel = False
533 # show fixed and spatial coefficients and coefficient histograms
534 di.plotKernelCoefficients = True
535 # show the bad candidates (red) along with good (green)
536 di.showBadCandidates = True
537 return di
538 lsstDebug.Info = DebugInfo
539 lsstDebug.frame = 1
540
541 Note that if you want additional logging info, you may add to your scripts:
542
543 .. code-block:: py
544
545 import lsst.utils.logging as logUtils
546 logUtils.trace_set_at("lsst.ip.diffim", 4)
547 """
548 ConfigClass = PsfMatchConfig
549 _DefaultName = "psfMatch"
550
551 def __init__(self, *args, **kwargs):
552 """Create the psf-matching Task
553
554 Parameters
555 ----------
556 *args
557 Arguments to be passed to ``lsst.pipe.base.task.Task.__init__``
558 **kwargs
559 Keyword arguments to be passed to ``lsst.pipe.base.task.Task.__init__``
560
561 Notes
562 -----
563 The initialization sets the Psf-matching kernel configuration using the value of
564 self.config.kernel.active. If the kernel is requested with regularization to moderate
565 the bias/variance tradeoff, currently only used when a delta function kernel basis
566 is provided, it creates a regularization matrix stored as member variable
567 self.hMat.
568 """
569 pipeBase.Task.__init__(self, *args, **kwargs)
570 self.kConfig = self.config.kernel.active
571
572 if 'useRegularization' in self.kConfig:
573 self.useRegularization = self.kConfig.useRegularization
574 else:
575 self.useRegularization = False
576
577 if self.useRegularization:
578 self.hMat = diffimLib.makeRegularizationMatrix(pexConfig.makePropertySet(self.kConfig))
579
580 def _diagnostic(self, kernelCellSet, spatialSolution, spatialKernel, spatialBg):
581 """Provide logging diagnostics on quality of spatial kernel fit
582
583 Parameters
584 ----------
585 kernelCellSet : `lsst.afw.math.SpatialCellSet`
586 Cellset that contains the KernelCandidates used in the fitting
587 spatialSolution : `lsst.ip.diffim.SpatialKernelSolution`
588 KernelSolution of best-fit
589 spatialKernel : `lsst.afw.math.LinearCombinationKernel`
590 Best-fit spatial Kernel model
591 spatialBg : `lsst.afw.math.Function2D`
592 Best-fit spatial background model
593 """
594 # What is the final kernel sum
595 kImage = afwImage.ImageD(spatialKernel.getDimensions())
596 kSum = spatialKernel.computeImage(kImage, False)
597 self.log.info("Final spatial kernel sum %.3f", kSum)
598
599 # Look at how well conditioned the matrix is
600 conditionNum = spatialSolution.getConditionNumber(
601 getattr(diffimLib.KernelSolution, self.kConfig.conditionNumberType))
602 self.log.info("Spatial model condition number %.3e", conditionNum)
603
604 if conditionNum < 0.0:
605 self.log.warning("Condition number is negative (%.3e)", conditionNum)
606 if conditionNum > self.kConfig.maxSpatialConditionNumber:
607 self.log.warning("Spatial solution exceeds max condition number (%.3e > %.3e)",
608 conditionNum, self.kConfig.maxSpatialConditionNumber)
609
610 self.metadata["spatialConditionNum"] = conditionNum
611 self.metadata["spatialKernelSum"] = kSum
612
613 # Look at how well the solution is constrained
614 nBasisKernels = spatialKernel.getNBasisKernels()
615 nKernelTerms = spatialKernel.getNSpatialParameters()
616 if nKernelTerms == 0: # order 0
617 nKernelTerms = 1
618
619 # Not fit for
620 nBgTerms = spatialBg.getNParameters()
621 if nBgTerms == 1:
622 if spatialBg.getParameters()[0] == 0.0:
623 nBgTerms = 0
624
625 nGood = 0
626 nBad = 0
627 nTot = 0
628 for cell in kernelCellSet.getCellList():
629 for cand in cell.begin(False): # False = include bad candidates
630 nTot += 1
631 if cand.getStatus() == afwMath.SpatialCellCandidate.GOOD:
632 nGood += 1
633 if cand.getStatus() == afwMath.SpatialCellCandidate.BAD:
634 nBad += 1
635
636 self.log.info("Doing stats of kernel candidates used in the spatial fit.")
637
638 # Counting statistics
639 if nBad > 2*nGood:
640 self.log.warning("Many more candidates rejected than accepted; %d total, %d rejected, %d used",
641 nTot, nBad, nGood)
642 else:
643 self.log.info("%d candidates total, %d rejected, %d used", nTot, nBad, nGood)
644
645 # Some judgements on the quality of the spatial models
646 if nGood < nKernelTerms:
647 self.log.warning("Spatial kernel model underconstrained; %d candidates, %d terms, %d bases",
648 nGood, nKernelTerms, nBasisKernels)
649 self.log.warning("Consider lowering the spatial order")
650 elif nGood <= 2*nKernelTerms:
651 self.log.warning("Spatial kernel model poorly constrained; %d candidates, %d terms, %d bases",
652 nGood, nKernelTerms, nBasisKernels)
653 self.log.warning("Consider lowering the spatial order")
654 else:
655 self.log.info("Spatial kernel model well constrained; %d candidates, %d terms, %d bases",
656 nGood, nKernelTerms, nBasisKernels)
657
658 if nGood < nBgTerms:
659 self.log.warning("Spatial background model underconstrained; %d candidates, %d terms",
660 nGood, nBgTerms)
661 self.log.warning("Consider lowering the spatial order")
662 elif nGood <= 2*nBgTerms:
663 self.log.warning("Spatial background model poorly constrained; %d candidates, %d terms",
664 nGood, nBgTerms)
665 self.log.warning("Consider lowering the spatial order")
666 else:
667 self.log.info("Spatial background model appears well constrained; %d candidates, %d terms",
668 nGood, nBgTerms)
669
670 def _displayDebug(self, kernelCellSet, spatialKernel, spatialBackground):
671 """Provide visualization of the inputs and ouputs to the Psf-matching code
672
673 Parameters
674 ----------
675 kernelCellSet : `lsst.afw.math.SpatialCellSet`
676 The SpatialCellSet used in determining the matching kernel and background
677 spatialKernel : `lsst.afw.math.LinearCombinationKernel`
678 Spatially varying Psf-matching kernel
679 spatialBackground : `lsst.afw.math.Function2D`
680 Spatially varying background-matching function
681 """
682 import lsstDebug
683 displayCandidates = lsstDebug.Info(__name__).displayCandidates
684 displayKernelBasis = lsstDebug.Info(__name__).displayKernelBasis
685 displayKernelMosaic = lsstDebug.Info(__name__).displayKernelMosaic
686 plotKernelSpatialModel = lsstDebug.Info(__name__).plotKernelSpatialModel
687 plotKernelCoefficients = lsstDebug.Info(__name__).plotKernelCoefficients
688 showBadCandidates = lsstDebug.Info(__name__).showBadCandidates
689 maskTransparency = lsstDebug.Info(__name__).maskTransparency
690 if not maskTransparency:
691 maskTransparency = 0
692 afwDisplay.setDefaultMaskTransparency(maskTransparency)
693
694 if displayCandidates:
695 diutils.showKernelCandidates(kernelCellSet, kernel=spatialKernel, background=spatialBackground,
696 frame=lsstDebug.frame,
697 showBadCandidates=showBadCandidates)
698 lsstDebug.frame += 1
699 diutils.showKernelCandidates(kernelCellSet, kernel=spatialKernel, background=spatialBackground,
700 frame=lsstDebug.frame,
701 showBadCandidates=showBadCandidates,
702 kernels=True)
703 lsstDebug.frame += 1
704 diutils.showKernelCandidates(kernelCellSet, kernel=spatialKernel, background=spatialBackground,
705 frame=lsstDebug.frame,
706 showBadCandidates=showBadCandidates,
707 resids=True)
708 lsstDebug.frame += 1
709
710 if displayKernelBasis:
711 diutils.showKernelBasis(spatialKernel, frame=lsstDebug.frame)
712 lsstDebug.frame += 1
713
714 if displayKernelMosaic:
715 diutils.showKernelMosaic(kernelCellSet.getBBox(), spatialKernel, frame=lsstDebug.frame)
716 lsstDebug.frame += 1
717
718 if plotKernelSpatialModel:
719 diutils.plotKernelSpatialModel(spatialKernel, kernelCellSet, showBadCandidates=showBadCandidates)
720
721 if plotKernelCoefficients:
722 diutils.plotKernelCoefficients(spatialKernel, kernelCellSet)
723
724 def _createPcaBasis(self, kernelCellSet, nStarPerCell, ps):
725 """Create Principal Component basis
726
727 If a principal component analysis is requested, typically when using a delta function basis,
728 perform the PCA here and return a new basis list containing the new principal components.
729
730 Parameters
731 ----------
732 kernelCellSet : `lsst.afw.math.SpatialCellSet`
733 a SpatialCellSet containing KernelCandidates, from which components are derived
734 nStarPerCell : `int`
735 the number of stars per cell to visit when doing the PCA
736 ps : `lsst.daf.base.PropertySet`
737 input property set controlling the single kernel visitor
738
739 Returns
740 -------
741 nRejectedPca : `int`
742 number of KernelCandidates rejected during PCA loop
743 spatialBasisList : `list` of `lsst.afw.math.kernel.FixedKernel`
744 basis list containing the principal shapes as Kernels
745
746 Raises
747 ------
748 RuntimeError
749 If the Eigenvalues sum to zero.
750 """
751 nComponents = self.kConfig.numPrincipalComponents
752 imagePca = diffimLib.KernelPcaD()
753 importStarVisitor = diffimLib.KernelPcaVisitorF(imagePca)
754 kernelCellSet.visitCandidates(importStarVisitor, nStarPerCell)
755 if self.kConfig.subtractMeanForPca:
756 importStarVisitor.subtractMean()
757 imagePca.analyze()
758
759 eigenValues = imagePca.getEigenValues()
760 pcaBasisList = importStarVisitor.getEigenKernels()
761
762 eSum = np.sum(eigenValues)
763 if eSum == 0.0:
764 raise RuntimeError("Eigenvalues sum to zero")
765 trace_logger = getTraceLogger(self.log.getChild("_solve"), 5)
766 for j in range(len(eigenValues)):
767 trace_logger.debug("Eigenvalue %d : %f (%f)", j, eigenValues[j], eigenValues[j]/eSum)
768
769 nToUse = min(nComponents, len(eigenValues))
770 trimBasisList = []
771 for j in range(nToUse):
772 # Check for NaNs?
773 kimage = afwImage.ImageD(pcaBasisList[j].getDimensions())
774 pcaBasisList[j].computeImage(kimage, False)
775 if not (True in np.isnan(kimage.array)):
776 trimBasisList.append(pcaBasisList[j])
777
778 # Put all the power in the first kernel, which will not vary spatially
779 spatialBasisList = diffimLib.renormalizeKernelList(trimBasisList)
780
781 # New Kernel visitor for this new basis list (no regularization explicitly)
782 singlekvPca = diffimLib.BuildSingleKernelVisitorF(spatialBasisList, ps)
783 singlekvPca.setSkipBuilt(False)
784 kernelCellSet.visitCandidates(singlekvPca, nStarPerCell)
785 singlekvPca.setSkipBuilt(True)
786 nRejectedPca = singlekvPca.getNRejected()
787
788 return nRejectedPca, spatialBasisList
789
790 @abc.abstractmethod
791 def _buildCellSet(self, *args):
792 """Fill a SpatialCellSet with KernelCandidates for the Psf-matching process;
793 override in derived classes"""
794 return
795
796 @timeMethod
797 def _solve(self, kernelCellSet, basisList, returnOnExcept=False):
798 """Solve for the PSF matching kernel
799
800 Parameters
801 ----------
802 kernelCellSet : `lsst.afw.math.SpatialCellSet`
803 a SpatialCellSet to use in determining the matching kernel
804 (typically as provided by _buildCellSet)
805 basisList : `list` of `lsst.afw.math.kernel.FixedKernel`
806 list of Kernels to be used in the decomposition of the spatially varying kernel
807 (typically as provided by makeKernelBasisList)
808 returnOnExcept : `bool`, optional
809 if True then return (None, None) if an error occurs, else raise the exception
810
811 Returns
812 -------
813 psfMatchingKernel : `lsst.afw.math.LinearCombinationKernel`
814 Spatially varying Psf-matching kernel
815 backgroundModel : `lsst.afw.math.Function2D`
816 Spatially varying background-matching function
817
818 Raises
819 ------
820 RuntimeError :
821 If unable to determine PSF matching kernel and ``returnOnExcept==False``.
822 """
823
824 import lsstDebug
825 display = lsstDebug.Info(__name__).display
826
827 maxSpatialIterations = self.kConfig.maxSpatialIterations
828 nStarPerCell = self.kConfig.nStarPerCell
829 usePcaForSpatialKernel = self.kConfig.usePcaForSpatialKernel
830
831 # Visitor for the single kernel fit
832 ps = pexConfig.makePropertySet(self.kConfig)
833 if self.useRegularization:
834 singlekv = diffimLib.BuildSingleKernelVisitorF(basisList, ps, self.hMat)
835 else:
836 singlekv = diffimLib.BuildSingleKernelVisitorF(basisList, ps)
837
838 # Visitor for the kernel sum rejection
839 ksv = diffimLib.KernelSumVisitorF(ps)
840
841 # Main loop
842 trace_loggers = [getTraceLogger(self.log.getChild("_solve"), i) for i in range(5)]
843 t0 = time.time()
844 totalIterations = 0
845 thisIteration = 0
846 while (thisIteration < maxSpatialIterations):
847
848 # Make sure there are no uninitialized candidates as active occupants of Cell
849 nRejectedSkf = -1
850 while (nRejectedSkf != 0):
851 trace_loggers[1].debug("Building single kernels...")
852 kernelCellSet.visitCandidates(singlekv, nStarPerCell, ignoreExceptions=True)
853 nRejectedSkf = singlekv.getNRejected()
854 trace_loggers[1].debug(
855 "Iteration %d, rejected %d candidates due to initial kernel fit",
856 thisIteration, nRejectedSkf
857 )
858
859 # Reject outliers in kernel sum
860 ksv.resetKernelSum()
861 ksv.setMode(diffimLib.KernelSumVisitorF.AGGREGATE)
862 kernelCellSet.visitCandidates(ksv, nStarPerCell, ignoreExceptions=True)
863 ksv.processKsumDistribution()
864 ksv.setMode(diffimLib.KernelSumVisitorF.REJECT)
865 kernelCellSet.visitCandidates(ksv, nStarPerCell, ignoreExceptions=True)
866
867 nRejectedKsum = ksv.getNRejected()
868 trace_loggers[1].debug(
869 "Iteration %d, rejected %d candidates due to kernel sum",
870 thisIteration, nRejectedKsum
871 )
872
873 # Do we jump back to the top without incrementing thisIteration?
874 if nRejectedKsum > 0:
875 totalIterations += 1
876 continue
877
878 # At this stage we can either apply the spatial fit to
879 # the kernels, or we run a PCA, use these as a *new*
880 # basis set with lower dimensionality, and then apply
881 # the spatial fit to these kernels
882
883 if (usePcaForSpatialKernel):
884 trace_loggers[0].debug("Building Pca basis")
885
886 nRejectedPca, spatialBasisList = self._createPcaBasis(kernelCellSet, nStarPerCell, ps)
887 trace_loggers[1].debug(
888 "Iteration %d, rejected %d candidates due to Pca kernel fit",
889 thisIteration, nRejectedPca
890 )
891
892 # We don't want to continue on (yet) with the
893 # spatial modeling, because we have bad objects
894 # contributing to the Pca basis. We basically
895 # need to restart from the beginning of this loop,
896 # since the cell-mates of those objects that were
897 # rejected need their original Kernels built by
898 # singleKernelFitter.
899
900 # Don't count against thisIteration
901 if (nRejectedPca > 0):
902 totalIterations += 1
903 continue
904 else:
905 spatialBasisList = basisList
906
907 # We have gotten on to the spatial modeling part
908 regionBBox = kernelCellSet.getBBox()
909 spatialkv = diffimLib.BuildSpatialKernelVisitorF(spatialBasisList, regionBBox, ps)
910 kernelCellSet.visitCandidates(spatialkv, nStarPerCell)
911 spatialkv.solveLinearEquation()
912 trace_loggers[2].debug("Spatial kernel built with %d candidates", spatialkv.getNCandidates())
913 spatialKernel, spatialBackground = spatialkv.getSolutionPair()
914
915 # Check the quality of the spatial fit (look at residuals)
916 assesskv = diffimLib.AssessSpatialKernelVisitorF(spatialKernel, spatialBackground, ps)
917 kernelCellSet.visitCandidates(assesskv, nStarPerCell)
918 nRejectedSpatial = assesskv.getNRejected()
919 nGoodSpatial = assesskv.getNGood()
920 trace_loggers[1].debug(
921 "Iteration %d, rejected %d candidates due to spatial kernel fit",
922 thisIteration, nRejectedSpatial
923 )
924 trace_loggers[1].debug("%d candidates used in fit", nGoodSpatial)
925
926 # If only nGoodSpatial == 0, might be other candidates in the cells
927 if nGoodSpatial == 0 and nRejectedSpatial == 0:
928 raise RuntimeError("No kernel candidates for spatial fit")
929
930 if nRejectedSpatial == 0:
931 # Nothing rejected, finished with spatial fit
932 break
933
934 # Otherwise, iterate on...
935 thisIteration += 1
936
937 # Final fit if above did not converge
938 if (nRejectedSpatial > 0) and (thisIteration == maxSpatialIterations):
939 trace_loggers[1].debug("Final spatial fit")
940 if (usePcaForSpatialKernel):
941 nRejectedPca, spatialBasisList = self._createPcaBasis(kernelCellSet, nStarPerCell, ps)
942 regionBBox = kernelCellSet.getBBox()
943 spatialkv = diffimLib.BuildSpatialKernelVisitorF(spatialBasisList, regionBBox, ps)
944 kernelCellSet.visitCandidates(spatialkv, nStarPerCell)
945 spatialkv.solveLinearEquation()
946 trace_loggers[2].debug("Spatial kernel built with %d candidates", spatialkv.getNCandidates())
947 spatialKernel, spatialBackground = spatialkv.getSolutionPair()
948
949 spatialSolution = spatialkv.getKernelSolution()
950
951 t1 = time.time()
952 trace_loggers[0].debug("Total time to compute the spatial kernel : %.2f s", (t1 - t0))
953
954 if display:
955 self._displayDebug(kernelCellSet, spatialKernel, spatialBackground)
956
957 self._diagnostic(kernelCellSet, spatialSolution, spatialKernel, spatialBackground)
958
959 return spatialSolution, spatialKernel, spatialBackground
960
961
962PsfMatch = PsfMatchTask