582 def _diagnostic(self, kernelCellSet, spatialSolution, spatialKernel, spatialBg):
583 """Provide logging diagnostics on quality of spatial kernel fit
587 kernelCellSet : `lsst.afw.math.SpatialCellSet`
588 Cellset that contains the KernelCandidates used in the fitting
589 spatialSolution : `lsst.ip.diffim.SpatialKernelSolution`
590 KernelSolution of best-fit
591 spatialKernel : `lsst.afw.math.LinearCombinationKernel`
592 Best-fit spatial Kernel model
593 spatialBg : `lsst.afw.math.Function2D`
594 Best-fit spatial background model
596 # What is the final kernel sum
597 kImage = afwImage.ImageD(spatialKernel.getDimensions())
598 # Evaluate the kernel at the cell-set center, not at the world origin.
599 xcen, ycen = kernelCellSet.getBBox().getCenter()
600 kSum = spatialKernel.computeImage(kImage, doNormalize=False, x=xcen, y=ycen)
601 self.log.info("Final spatial kernel sum %.3f", kSum)
603 # Look at how well conditioned the matrix is
604 conditionNum = spatialSolution.getConditionNumber(
605 getattr(diffimLib.KernelSolution, self.kConfig.conditionNumberType))
606 self.log.info("Spatial model condition number %.3e", conditionNum)
608 if conditionNum < 0.0:
609 self.log.warning("Condition number is negative (%.3e)", conditionNum)
610 if conditionNum > self.kConfig.maxSpatialConditionNumber:
611 self.log.warning("Spatial solution exceeds max condition number (%.3e > %.3e)",
612 conditionNum, self.kConfig.maxSpatialConditionNumber)
614 self.metadata["spatialConditionNum"] = conditionNum
615 self.metadata["spatialKernelSum"] = kSum
617 # Look at how well the solution is constrained
618 nBasisKernels = spatialKernel.getNBasisKernels()
619 nKernelTerms = spatialKernel.getNSpatialParameters()
620 if nKernelTerms == 0: # order 0
624 nBgTerms = spatialBg.getNParameters()
626 if spatialBg.getParameters()[0] == 0.0:
632 for cell in kernelCellSet.getCellList():
633 for cand in cell.begin(False): # False = include bad candidates
635 if cand.getStatus() == afwMath.SpatialCellCandidate.GOOD:
637 if cand.getStatus() == afwMath.SpatialCellCandidate.BAD:
640 self.log.info("Doing stats of kernel candidates used in the spatial fit.")
642 # Counting statistics
644 self.log.warning("Many more candidates rejected than accepted; %d total, %d rejected, %d used",
647 self.log.info("%d candidates total, %d rejected, %d used", nTot, nBad, nGood)
649 # Some judgements on the quality of the spatial models
650 if nGood < nKernelTerms:
651 self.log.warning("Spatial kernel model underconstrained; %d candidates, %d terms, %d bases",
652 nGood, nKernelTerms, nBasisKernels)
653 self.log.warning("Consider lowering the spatial order")
654 elif nGood <= 2*nKernelTerms:
655 self.log.warning("Spatial kernel model poorly constrained; %d candidates, %d terms, %d bases",
656 nGood, nKernelTerms, nBasisKernels)
657 self.log.warning("Consider lowering the spatial order")
659 self.log.info("Spatial kernel model well constrained; %d candidates, %d terms, %d bases",
660 nGood, nKernelTerms, nBasisKernels)
663 self.log.warning("Spatial background model underconstrained; %d candidates, %d terms",
665 self.log.warning("Consider lowering the spatial order")
666 elif nGood <= 2*nBgTerms:
667 self.log.warning("Spatial background model poorly constrained; %d candidates, %d terms",
669 self.log.warning("Consider lowering the spatial order")
671 self.log.info("Spatial background model appears well constrained; %d candidates, %d terms",
674 def _displayDebug(self, kernelCellSet, spatialKernel, spatialBackground):
675 """Provide visualization of the inputs and ouputs to the Psf-matching code
679 kernelCellSet : `lsst.afw.math.SpatialCellSet`
680 The SpatialCellSet used in determining the matching kernel and background
681 spatialKernel : `lsst.afw.math.LinearCombinationKernel`
682 Spatially varying Psf-matching kernel
683 spatialBackground : `lsst.afw.math.Function2D`
684 Spatially varying background-matching function
687 displayCandidates = lsstDebug.Info(__name__).displayCandidates
688 displayKernelBasis = lsstDebug.Info(__name__).displayKernelBasis
689 displayKernelMosaic = lsstDebug.Info(__name__).displayKernelMosaic
690 plotKernelSpatialModel = lsstDebug.Info(__name__).plotKernelSpatialModel
691 plotKernelCoefficients = lsstDebug.Info(__name__).plotKernelCoefficients
692 showBadCandidates = lsstDebug.Info(__name__).showBadCandidates
693 maskTransparency = lsstDebug.Info(__name__).maskTransparency
694 if not maskTransparency:
696 afwDisplay.setDefaultMaskTransparency(maskTransparency)
698 if displayCandidates:
699 diutils.showKernelCandidates(kernelCellSet, kernel=spatialKernel, background=spatialBackground,
700 frame=lsstDebug.frame,
701 showBadCandidates=showBadCandidates)
703 diutils.showKernelCandidates(kernelCellSet, kernel=spatialKernel, background=spatialBackground,
704 frame=lsstDebug.frame,
705 showBadCandidates=showBadCandidates,
708 diutils.showKernelCandidates(kernelCellSet, kernel=spatialKernel, background=spatialBackground,
709 frame=lsstDebug.frame,
710 showBadCandidates=showBadCandidates,
714 if displayKernelBasis:
715 diutils.showKernelBasis(spatialKernel, frame=lsstDebug.frame)
718 if displayKernelMosaic:
719 diutils.showKernelMosaic(kernelCellSet.getBBox(), spatialKernel, frame=lsstDebug.frame)
722 if plotKernelSpatialModel:
723 diutils.plotKernelSpatialModel(spatialKernel, kernelCellSet, showBadCandidates=showBadCandidates)
725 if plotKernelCoefficients:
726 diutils.plotKernelCoefficients(spatialKernel, kernelCellSet)
728 def _createPcaBasis(self, kernelCellSet, nStarPerCell, ps):
729 """Create Principal Component basis
731 If a principal component analysis is requested, typically when using a delta function basis,
732 perform the PCA here and return a new basis list containing the new principal components.
736 kernelCellSet : `lsst.afw.math.SpatialCellSet`
737 a SpatialCellSet containing KernelCandidates, from which components are derived
739 the number of stars per cell to visit when doing the PCA
740 ps : `lsst.daf.base.PropertySet`
741 input property set controlling the single kernel visitor
746 number of KernelCandidates rejected during PCA loop
747 spatialBasisList : `list` of `lsst.afw.math.kernel.FixedKernel`
748 basis list containing the principal shapes as Kernels
753 If the Eigenvalues sum to zero.
755 nComponents = self.kConfig.numPrincipalComponents
756 imagePca = diffimLib.KernelPcaD()
757 importStarVisitor = diffimLib.KernelPcaVisitorF(imagePca)
758 kernelCellSet.visitCandidates(importStarVisitor, nStarPerCell)
759 if self.kConfig.subtractMeanForPca:
760 importStarVisitor.subtractMean()
763 eigenValues = imagePca.getEigenValues()
764 pcaBasisList = importStarVisitor.getEigenKernels()
766 eSum = np.sum(eigenValues)
768 raise RuntimeError("Eigenvalues sum to zero")
769 trace_logger = getTraceLogger(self.log.getChild("_solve"), 5)
770 for j in range(len(eigenValues)):
771 trace_logger.debug("Eigenvalue %d : %f (%f)", j, eigenValues[j], eigenValues[j]/eSum)
773 nToUse = min(nComponents, len(eigenValues))
775 for j in range(nToUse):
777 kimage = afwImage.ImageD(pcaBasisList[j].getDimensions())
778 pcaBasisList[j].computeImage(kimage, doNormalize=False)
779 if not (True in np.isnan(kimage.array)):
780 trimBasisList.append(pcaBasisList[j])
782 # Put all the power in the first kernel, which will not vary spatially
783 spatialBasisList = diffimLib.renormalizeKernelList(trimBasisList)
785 # New Kernel visitor for this new basis list (no regularization explicitly)
786 singlekvPca = diffimLib.BuildSingleKernelVisitorF(spatialBasisList, ps)
787 singlekvPca.setSkipBuilt(False)
788 kernelCellSet.visitCandidates(singlekvPca, nStarPerCell)
789 singlekvPca.setSkipBuilt(True)
790 nRejectedPca = singlekvPca.getNRejected()
792 return nRejectedPca, spatialBasisList
801 def _solve(self, kernelCellSet, basisList):
802 """Solve for the PSF matching kernel
806 kernelCellSet : `lsst.afw.math.SpatialCellSet`
807 a SpatialCellSet to use in determining the matching kernel
808 (typically as provided by _buildCellSet)
809 basisList : `list` of `lsst.afw.math.kernel.FixedKernel`
810 list of Kernels to be used in the decomposition of the spatially varying kernel
811 (typically as provided by makeKernelBasisList)
815 psfMatchingKernel : `lsst.afw.math.LinearCombinationKernel`
816 Spatially varying Psf-matching kernel
817 backgroundModel : `lsst.afw.math.Function2D`
818 Spatially varying background-matching function
822 NoKernelCandidatesError :
823 If there are no useable kernel candidates.
827 display = lsstDebug.Info(__name__).display
829 maxSpatialIterations = self.kConfig.maxSpatialIterations
830 nStarPerCell = self.kConfig.nStarPerCell
831 usePcaForSpatialKernel = self.kConfig.usePcaForSpatialKernel
833 # Visitor for the single kernel fit
834 ps = pexConfig.makePropertySet(self.kConfig)
835 if self.useRegularization:
836 singlekv = diffimLib.BuildSingleKernelVisitorF(basisList, ps, self.hMat)
838 singlekv = diffimLib.BuildSingleKernelVisitorF(basisList, ps)
840 # Visitor for the kernel sum rejection
841 ksv = diffimLib.KernelSumVisitorF(ps)
844 trace_loggers = [getTraceLogger(self.log.getChild("_solve"), i) for i in range(5)]
848 while (thisIteration < maxSpatialIterations):
850 # Make sure there are no uninitialized candidates as active occupants of Cell
852 while (nRejectedSkf != 0):
853 trace_loggers[1].debug("Building single kernels...")
854 kernelCellSet.visitCandidates(singlekv, nStarPerCell, ignoreExceptions=True)
855 nRejectedSkf = singlekv.getNRejected()
856 trace_loggers[1].debug(
857 "Iteration %d, rejected %d candidates due to initial kernel fit",
858 thisIteration, nRejectedSkf
861 # Reject outliers in kernel sum
863 ksv.setMode(diffimLib.KernelSumVisitorF.AGGREGATE)
864 kernelCellSet.visitCandidates(ksv, nStarPerCell, ignoreExceptions=True)
867 for cell in kernelCellSet.getCellList():
869 allCellsEmpty = False
872 raise NoKernelCandidatesError("All spatial cells are empty of candidates")
875 ksv.processKsumDistribution()
876 except RuntimeError as e:
877 raise NoKernelCandidatesError("Unable to calculate the kernel sum") from e
879 ksv.setMode(diffimLib.KernelSumVisitorF.REJECT)
880 kernelCellSet.visitCandidates(ksv, nStarPerCell, ignoreExceptions=True)
882 nRejectedKsum = ksv.getNRejected()
883 trace_loggers[1].debug(
884 "Iteration %d, rejected %d candidates due to kernel sum",
885 thisIteration, nRejectedKsum
888 # Do we jump back to the top without incrementing thisIteration?
889 if nRejectedKsum > 0:
893 # At this stage we can either apply the spatial fit to
894 # the kernels, or we run a PCA, use these as a *new*
895 # basis set with lower dimensionality, and then apply
896 # the spatial fit to these kernels
898 if (usePcaForSpatialKernel):
899 trace_loggers[0].debug("Building Pca basis")
901 nRejectedPca, spatialBasisList = self._createPcaBasis(kernelCellSet, nStarPerCell, ps)
902 trace_loggers[1].debug(
903 "Iteration %d, rejected %d candidates due to Pca kernel fit",
904 thisIteration, nRejectedPca
907 # We don't want to continue on (yet) with the
908 # spatial modeling, because we have bad objects
909 # contributing to the Pca basis. We basically
910 # need to restart from the beginning of this loop,
911 # since the cell-mates of those objects that were
912 # rejected need their original Kernels built by
913 # singleKernelFitter.
915 # Don't count against thisIteration
916 if (nRejectedPca > 0):
920 spatialBasisList = basisList
922 # We have gotten on to the spatial modeling part
923 regionBBox = kernelCellSet.getBBox()
924 spatialkv = diffimLib.BuildSpatialKernelVisitorF(spatialBasisList, regionBBox, ps)
925 kernelCellSet.visitCandidates(spatialkv, nStarPerCell)
926 spatialkv.solveLinearEquation()
927 trace_loggers[2].debug("Spatial kernel built with %d candidates", spatialkv.getNCandidates())
928 spatialKernel, spatialBackground = spatialkv.getSolutionPair()
930 # Check the quality of the spatial fit (look at residuals)
931 assesskv = diffimLib.AssessSpatialKernelVisitorF(spatialKernel, spatialBackground, ps)
932 kernelCellSet.visitCandidates(assesskv, nStarPerCell)
933 nRejectedSpatial = assesskv.getNRejected()
934 nGoodSpatial = assesskv.getNGood()
935 trace_loggers[1].debug(
936 "Iteration %d, rejected %d candidates due to spatial kernel fit",
937 thisIteration, nRejectedSpatial
939 trace_loggers[1].debug("%d candidates used in fit", nGoodSpatial)
941 # If only nGoodSpatial == 0, might be other candidates in the cells
942 if nGoodSpatial == 0 and nRejectedSpatial == 0:
943 raise NoKernelCandidatesError("No kernel candidates for spatial fit")
945 if nRejectedSpatial == 0:
946 # Nothing rejected, finished with spatial fit
949 # Otherwise, iterate on...
952 # Final fit if above did not converge
953 if (nRejectedSpatial > 0) and (thisIteration == maxSpatialIterations):
954 trace_loggers[1].debug("Final spatial fit")
955 if (usePcaForSpatialKernel):
956 nRejectedPca, spatialBasisList = self._createPcaBasis(kernelCellSet, nStarPerCell, ps)
957 regionBBox = kernelCellSet.getBBox()
958 spatialkv = diffimLib.BuildSpatialKernelVisitorF(spatialBasisList, regionBBox, ps)
959 kernelCellSet.visitCandidates(spatialkv, nStarPerCell)
960 spatialkv.solveLinearEquation()
961 trace_loggers[2].debug("Spatial kernel built with %d candidates", spatialkv.getNCandidates())
962 spatialKernel, spatialBackground = spatialkv.getSolutionPair()
964 # Run after the final fit to use the updated kernel visitor `spatialkv`
965 if spatialkv.getNCandidates() < 1:
966 raise NoKernelCandidatesError("No kernel candidates remain after max iterations")
968 spatialSolution = spatialkv.getKernelSolution()
971 trace_loggers[0].debug("Total time to compute the spatial kernel : %.2f s", (t1 - t0))
974 self._displayDebug(kernelCellSet, spatialKernel, spatialBackground)
976 self._diagnostic(kernelCellSet, spatialSolution, spatialKernel, spatialBackground)
978 return spatialSolution, spatialKernel, spatialBackground