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 kSum = spatialKernel.computeImage(kImage, False)
599 self.log.info("Final spatial kernel sum %.3f", kSum)
601 # Look at how well conditioned the matrix is
602 conditionNum = spatialSolution.getConditionNumber(
603 getattr(diffimLib.KernelSolution, self.kConfig.conditionNumberType))
604 self.log.info("Spatial model condition number %.3e", conditionNum)
606 if conditionNum < 0.0:
607 self.log.warning("Condition number is negative (%.3e)", conditionNum)
608 if conditionNum > self.kConfig.maxSpatialConditionNumber:
609 self.log.warning("Spatial solution exceeds max condition number (%.3e > %.3e)",
610 conditionNum, self.kConfig.maxSpatialConditionNumber)
612 self.metadata["spatialConditionNum"] = conditionNum
613 self.metadata["spatialKernelSum"] = kSum
615 # Look at how well the solution is constrained
616 nBasisKernels = spatialKernel.getNBasisKernels()
617 nKernelTerms = spatialKernel.getNSpatialParameters()
618 if nKernelTerms == 0: # order 0
622 nBgTerms = spatialBg.getNParameters()
624 if spatialBg.getParameters()[0] == 0.0:
630 for cell in kernelCellSet.getCellList():
631 for cand in cell.begin(False): # False = include bad candidates
633 if cand.getStatus() == afwMath.SpatialCellCandidate.GOOD:
635 if cand.getStatus() == afwMath.SpatialCellCandidate.BAD:
638 self.log.info("Doing stats of kernel candidates used in the spatial fit.")
640 # Counting statistics
642 self.log.warning("Many more candidates rejected than accepted; %d total, %d rejected, %d used",
645 self.log.info("%d candidates total, %d rejected, %d used", nTot, nBad, nGood)
647 # Some judgements on the quality of the spatial models
648 if nGood < nKernelTerms:
649 self.log.warning("Spatial kernel model underconstrained; %d candidates, %d terms, %d bases",
650 nGood, nKernelTerms, nBasisKernels)
651 self.log.warning("Consider lowering the spatial order")
652 elif nGood <= 2*nKernelTerms:
653 self.log.warning("Spatial kernel model poorly constrained; %d candidates, %d terms, %d bases",
654 nGood, nKernelTerms, nBasisKernels)
655 self.log.warning("Consider lowering the spatial order")
657 self.log.info("Spatial kernel model well constrained; %d candidates, %d terms, %d bases",
658 nGood, nKernelTerms, nBasisKernels)
661 self.log.warning("Spatial background model underconstrained; %d candidates, %d terms",
663 self.log.warning("Consider lowering the spatial order")
664 elif nGood <= 2*nBgTerms:
665 self.log.warning("Spatial background model poorly constrained; %d candidates, %d terms",
667 self.log.warning("Consider lowering the spatial order")
669 self.log.info("Spatial background model appears well constrained; %d candidates, %d terms",
672 def _displayDebug(self, kernelCellSet, spatialKernel, spatialBackground):
673 """Provide visualization of the inputs and ouputs to the Psf-matching code
677 kernelCellSet : `lsst.afw.math.SpatialCellSet`
678 The SpatialCellSet used in determining the matching kernel and background
679 spatialKernel : `lsst.afw.math.LinearCombinationKernel`
680 Spatially varying Psf-matching kernel
681 spatialBackground : `lsst.afw.math.Function2D`
682 Spatially varying background-matching function
685 displayCandidates = lsstDebug.Info(__name__).displayCandidates
686 displayKernelBasis = lsstDebug.Info(__name__).displayKernelBasis
687 displayKernelMosaic = lsstDebug.Info(__name__).displayKernelMosaic
688 plotKernelSpatialModel = lsstDebug.Info(__name__).plotKernelSpatialModel
689 plotKernelCoefficients = lsstDebug.Info(__name__).plotKernelCoefficients
690 showBadCandidates = lsstDebug.Info(__name__).showBadCandidates
691 maskTransparency = lsstDebug.Info(__name__).maskTransparency
692 if not maskTransparency:
694 afwDisplay.setDefaultMaskTransparency(maskTransparency)
696 if displayCandidates:
697 diutils.showKernelCandidates(kernelCellSet, kernel=spatialKernel, background=spatialBackground,
698 frame=lsstDebug.frame,
699 showBadCandidates=showBadCandidates)
701 diutils.showKernelCandidates(kernelCellSet, kernel=spatialKernel, background=spatialBackground,
702 frame=lsstDebug.frame,
703 showBadCandidates=showBadCandidates,
706 diutils.showKernelCandidates(kernelCellSet, kernel=spatialKernel, background=spatialBackground,
707 frame=lsstDebug.frame,
708 showBadCandidates=showBadCandidates,
712 if displayKernelBasis:
713 diutils.showKernelBasis(spatialKernel, frame=lsstDebug.frame)
716 if displayKernelMosaic:
717 diutils.showKernelMosaic(kernelCellSet.getBBox(), spatialKernel, frame=lsstDebug.frame)
720 if plotKernelSpatialModel:
721 diutils.plotKernelSpatialModel(spatialKernel, kernelCellSet, showBadCandidates=showBadCandidates)
723 if plotKernelCoefficients:
724 diutils.plotKernelCoefficients(spatialKernel, kernelCellSet)
726 def _createPcaBasis(self, kernelCellSet, nStarPerCell, ps):
727 """Create Principal Component basis
729 If a principal component analysis is requested, typically when using a delta function basis,
730 perform the PCA here and return a new basis list containing the new principal components.
734 kernelCellSet : `lsst.afw.math.SpatialCellSet`
735 a SpatialCellSet containing KernelCandidates, from which components are derived
737 the number of stars per cell to visit when doing the PCA
738 ps : `lsst.daf.base.PropertySet`
739 input property set controlling the single kernel visitor
744 number of KernelCandidates rejected during PCA loop
745 spatialBasisList : `list` of `lsst.afw.math.kernel.FixedKernel`
746 basis list containing the principal shapes as Kernels
751 If the Eigenvalues sum to zero.
753 nComponents = self.kConfig.numPrincipalComponents
754 imagePca = diffimLib.KernelPcaD()
755 importStarVisitor = diffimLib.KernelPcaVisitorF(imagePca)
756 kernelCellSet.visitCandidates(importStarVisitor, nStarPerCell)
757 if self.kConfig.subtractMeanForPca:
758 importStarVisitor.subtractMean()
761 eigenValues = imagePca.getEigenValues()
762 pcaBasisList = importStarVisitor.getEigenKernels()
764 eSum = np.sum(eigenValues)
766 raise RuntimeError("Eigenvalues sum to zero")
767 trace_logger = getTraceLogger(self.log.getChild("_solve"), 5)
768 for j in range(len(eigenValues)):
769 trace_logger.debug("Eigenvalue %d : %f (%f)", j, eigenValues[j], eigenValues[j]/eSum)
771 nToUse = min(nComponents, len(eigenValues))
773 for j in range(nToUse):
775 kimage = afwImage.ImageD(pcaBasisList[j].getDimensions())
776 pcaBasisList[j].computeImage(kimage, False)
777 if not (True in np.isnan(kimage.array)):
778 trimBasisList.append(pcaBasisList[j])
780 # Put all the power in the first kernel, which will not vary spatially
781 spatialBasisList = diffimLib.renormalizeKernelList(trimBasisList)
783 # New Kernel visitor for this new basis list (no regularization explicitly)
784 singlekvPca = diffimLib.BuildSingleKernelVisitorF(spatialBasisList, ps)
785 singlekvPca.setSkipBuilt(False)
786 kernelCellSet.visitCandidates(singlekvPca, nStarPerCell)
787 singlekvPca.setSkipBuilt(True)
788 nRejectedPca = singlekvPca.getNRejected()
790 return nRejectedPca, spatialBasisList
799 def _solve(self, kernelCellSet, basisList):
800 """Solve for the PSF matching kernel
804 kernelCellSet : `lsst.afw.math.SpatialCellSet`
805 a SpatialCellSet to use in determining the matching kernel
806 (typically as provided by _buildCellSet)
807 basisList : `list` of `lsst.afw.math.kernel.FixedKernel`
808 list of Kernels to be used in the decomposition of the spatially varying kernel
809 (typically as provided by makeKernelBasisList)
813 psfMatchingKernel : `lsst.afw.math.LinearCombinationKernel`
814 Spatially varying Psf-matching kernel
815 backgroundModel : `lsst.afw.math.Function2D`
816 Spatially varying background-matching function
820 NoKernelCandidatesError :
821 If there are no useable kernel candidates.
825 display = lsstDebug.Info(__name__).display
827 maxSpatialIterations = self.kConfig.maxSpatialIterations
828 nStarPerCell = self.kConfig.nStarPerCell
829 usePcaForSpatialKernel = self.kConfig.usePcaForSpatialKernel
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)
836 singlekv = diffimLib.BuildSingleKernelVisitorF(basisList, ps)
838 # Visitor for the kernel sum rejection
839 ksv = diffimLib.KernelSumVisitorF(ps)
842 trace_loggers = [getTraceLogger(self.log.getChild("_solve"), i) for i in range(5)]
846 while (thisIteration < maxSpatialIterations):
848 # Make sure there are no uninitialized candidates as active occupants of Cell
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
859 # Reject outliers in kernel sum
861 ksv.setMode(diffimLib.KernelSumVisitorF.AGGREGATE)
862 kernelCellSet.visitCandidates(ksv, nStarPerCell, ignoreExceptions=True)
865 for cell in kernelCellSet.getCellList():
867 allCellsEmpty = False
870 raise NoKernelCandidatesError("All spatial cells are empty of candidates")
873 ksv.processKsumDistribution()
874 except RuntimeError as e:
875 raise NoKernelCandidatesError("Unable to calculate the kernel sum") from e
877 ksv.setMode(diffimLib.KernelSumVisitorF.REJECT)
878 kernelCellSet.visitCandidates(ksv, nStarPerCell, ignoreExceptions=True)
880 nRejectedKsum = ksv.getNRejected()
881 trace_loggers[1].debug(
882 "Iteration %d, rejected %d candidates due to kernel sum",
883 thisIteration, nRejectedKsum
886 # Do we jump back to the top without incrementing thisIteration?
887 if nRejectedKsum > 0:
891 # At this stage we can either apply the spatial fit to
892 # the kernels, or we run a PCA, use these as a *new*
893 # basis set with lower dimensionality, and then apply
894 # the spatial fit to these kernels
896 if (usePcaForSpatialKernel):
897 trace_loggers[0].debug("Building Pca basis")
899 nRejectedPca, spatialBasisList = self._createPcaBasis(kernelCellSet, nStarPerCell, ps)
900 trace_loggers[1].debug(
901 "Iteration %d, rejected %d candidates due to Pca kernel fit",
902 thisIteration, nRejectedPca
905 # We don't want to continue on (yet) with the
906 # spatial modeling, because we have bad objects
907 # contributing to the Pca basis. We basically
908 # need to restart from the beginning of this loop,
909 # since the cell-mates of those objects that were
910 # rejected need their original Kernels built by
911 # singleKernelFitter.
913 # Don't count against thisIteration
914 if (nRejectedPca > 0):
918 spatialBasisList = basisList
920 # We have gotten on to the spatial modeling part
921 regionBBox = kernelCellSet.getBBox()
922 spatialkv = diffimLib.BuildSpatialKernelVisitorF(spatialBasisList, regionBBox, ps)
923 kernelCellSet.visitCandidates(spatialkv, nStarPerCell)
924 spatialkv.solveLinearEquation()
925 trace_loggers[2].debug("Spatial kernel built with %d candidates", spatialkv.getNCandidates())
926 spatialKernel, spatialBackground = spatialkv.getSolutionPair()
928 # Check the quality of the spatial fit (look at residuals)
929 assesskv = diffimLib.AssessSpatialKernelVisitorF(spatialKernel, spatialBackground, ps)
930 kernelCellSet.visitCandidates(assesskv, nStarPerCell)
931 nRejectedSpatial = assesskv.getNRejected()
932 nGoodSpatial = assesskv.getNGood()
933 trace_loggers[1].debug(
934 "Iteration %d, rejected %d candidates due to spatial kernel fit",
935 thisIteration, nRejectedSpatial
937 trace_loggers[1].debug("%d candidates used in fit", nGoodSpatial)
939 # If only nGoodSpatial == 0, might be other candidates in the cells
940 if nGoodSpatial == 0 and nRejectedSpatial == 0:
941 raise NoKernelCandidatesError("No kernel candidates for spatial fit")
943 if nRejectedSpatial == 0:
944 # Nothing rejected, finished with spatial fit
947 # Otherwise, iterate on...
950 # Final fit if above did not converge
951 if (nRejectedSpatial > 0) and (thisIteration == maxSpatialIterations):
952 trace_loggers[1].debug("Final spatial fit")
953 if (usePcaForSpatialKernel):
954 nRejectedPca, spatialBasisList = self._createPcaBasis(kernelCellSet, nStarPerCell, ps)
955 regionBBox = kernelCellSet.getBBox()
956 spatialkv = diffimLib.BuildSpatialKernelVisitorF(spatialBasisList, regionBBox, ps)
957 kernelCellSet.visitCandidates(spatialkv, nStarPerCell)
958 spatialkv.solveLinearEquation()
959 trace_loggers[2].debug("Spatial kernel built with %d candidates", spatialkv.getNCandidates())
960 spatialKernel, spatialBackground = spatialkv.getSolutionPair()
962 # Run after the final fit to use the updated kernel visitor `spatialkv`
963 if spatialkv.getNCandidates() < 1:
964 raise NoKernelCandidatesError("No kernel candidates remain after max iterations")
966 spatialSolution = spatialkv.getKernelSolution()
969 trace_loggers[0].debug("Total time to compute the spatial kernel : %.2f s", (t1 - t0))
972 self._displayDebug(kernelCellSet, spatialKernel, spatialBackground)
974 self._diagnostic(kernelCellSet, spatialSolution, spatialKernel, spatialBackground)
976 return spatialSolution, spatialKernel, spatialBackground